]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package stdlib |
2 | ||
3 | import ( | |
4 | "bytes" | |
5 | "fmt" | |
6 | "math/big" | |
7 | "strings" | |
8 | ||
9 | "github.com/apparentlymart/go-textseg/textseg" | |
10 | ||
11 | "github.com/zclconf/go-cty/cty" | |
12 | "github.com/zclconf/go-cty/cty/convert" | |
13 | "github.com/zclconf/go-cty/cty/function" | |
14 | "github.com/zclconf/go-cty/cty/json" | |
15 | ) | |
16 | ||
17 | //go:generate ragel -Z format_fsm.rl | |
18 | //go:generate gofmt -w format_fsm.go | |
19 | ||
20 | var FormatFunc = function.New(&function.Spec{ | |
21 | Params: []function.Parameter{ | |
22 | { | |
23 | Name: "format", | |
24 | Type: cty.String, | |
25 | }, | |
26 | }, | |
27 | VarParam: &function.Parameter{ | |
28 | Name: "args", | |
29 | Type: cty.DynamicPseudoType, | |
30 | AllowNull: true, | |
31 | }, | |
32 | Type: function.StaticReturnType(cty.String), | |
33 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { | |
34 | for _, arg := range args[1:] { | |
35 | if !arg.IsWhollyKnown() { | |
36 | // We require all nested values to be known because the only | |
37 | // thing we can do for a collection/structural type is print | |
38 | // it as JSON and that requires it to be wholly known. | |
39 | return cty.UnknownVal(cty.String), nil | |
40 | } | |
41 | } | |
42 | str, err := formatFSM(args[0].AsString(), args[1:]) | |
43 | return cty.StringVal(str), err | |
44 | }, | |
45 | }) | |
46 | ||
47 | var FormatListFunc = function.New(&function.Spec{ | |
48 | Params: []function.Parameter{ | |
49 | { | |
50 | Name: "format", | |
51 | Type: cty.String, | |
52 | }, | |
53 | }, | |
54 | VarParam: &function.Parameter{ | |
55 | Name: "args", | |
56 | Type: cty.DynamicPseudoType, | |
57 | AllowNull: true, | |
58 | AllowUnknown: true, | |
59 | }, | |
60 | Type: function.StaticReturnType(cty.List(cty.String)), | |
61 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { | |
62 | fmtVal := args[0] | |
63 | args = args[1:] | |
64 | ||
65 | if len(args) == 0 { | |
66 | // With no arguments, this function is equivalent to Format, but | |
67 | // returning a single-element list result. | |
68 | result, err := Format(fmtVal, args...) | |
69 | return cty.ListVal([]cty.Value{result}), err | |
70 | } | |
71 | ||
72 | fmtStr := fmtVal.AsString() | |
73 | ||
74 | // Each of our arguments will be dealt with either as an iterator | |
75 | // or as a single value. Iterators are used for sequence-type values | |
76 | // (lists, sets, tuples) while everything else is treated as a | |
77 | // single value. The sequences we iterate over are required to be | |
78 | // all the same length. | |
79 | iterLen := -1 | |
80 | lenChooser := -1 | |
81 | iterators := make([]cty.ElementIterator, len(args)) | |
82 | singleVals := make([]cty.Value, len(args)) | |
83 | for i, arg := range args { | |
84 | argTy := arg.Type() | |
85 | switch { | |
86 | case (argTy.IsListType() || argTy.IsSetType() || argTy.IsTupleType()) && !arg.IsNull(): | |
107c1cdb ND |
87 | if !argTy.IsTupleType() && !arg.IsKnown() { |
88 | // We can't iterate this one at all yet then, so we can't | |
89 | // yet produce a result. | |
90 | return cty.UnknownVal(retType), nil | |
91 | } | |
15c0b25d AP |
92 | thisLen := arg.LengthInt() |
93 | if iterLen == -1 { | |
94 | iterLen = thisLen | |
95 | lenChooser = i | |
96 | } else { | |
97 | if thisLen != iterLen { | |
98 | return cty.NullVal(cty.List(cty.String)), function.NewArgErrorf( | |
99 | i+1, | |
100 | "argument %d has length %d, which is inconsistent with argument %d of length %d", | |
101 | i+1, thisLen, | |
102 | lenChooser+1, iterLen, | |
103 | ) | |
104 | } | |
105 | } | |
106 | iterators[i] = arg.ElementIterator() | |
107 | default: | |
108 | singleVals[i] = arg | |
109 | } | |
110 | } | |
111 | ||
112 | if iterLen == 0 { | |
113 | // If our sequences are all empty then our result must be empty. | |
114 | return cty.ListValEmpty(cty.String), nil | |
115 | } | |
116 | ||
117 | if iterLen == -1 { | |
118 | // If we didn't encounter any iterables at all then we're going | |
119 | // to just do one iteration with items from singleVals. | |
120 | iterLen = 1 | |
121 | } | |
122 | ||
123 | ret := make([]cty.Value, 0, iterLen) | |
124 | fmtArgs := make([]cty.Value, len(iterators)) | |
125 | Results: | |
126 | for iterIdx := 0; iterIdx < iterLen; iterIdx++ { | |
127 | ||
128 | // Construct our arguments for a single format call | |
129 | for i := range fmtArgs { | |
130 | switch { | |
131 | case iterators[i] != nil: | |
132 | iterator := iterators[i] | |
133 | iterator.Next() | |
134 | _, val := iterator.Element() | |
135 | fmtArgs[i] = val | |
136 | default: | |
137 | fmtArgs[i] = singleVals[i] | |
138 | } | |
139 | ||
140 | // If any of the arguments to this call would be unknown then | |
141 | // this particular result is unknown, but we'll keep going | |
142 | // to see if any other iterations can produce known values. | |
143 | if !fmtArgs[i].IsWhollyKnown() { | |
144 | // We require all nested values to be known because the only | |
145 | // thing we can do for a collection/structural type is print | |
146 | // it as JSON and that requires it to be wholly known. | |
147 | ret = append(ret, cty.UnknownVal(cty.String)) | |
148 | continue Results | |
149 | } | |
150 | } | |
151 | ||
152 | str, err := formatFSM(fmtStr, fmtArgs) | |
153 | if err != nil { | |
154 | return cty.NullVal(cty.List(cty.String)), fmt.Errorf( | |
155 | "error on format iteration %d: %s", iterIdx, err, | |
156 | ) | |
157 | } | |
158 | ||
159 | ret = append(ret, cty.StringVal(str)) | |
160 | } | |
161 | ||
162 | return cty.ListVal(ret), nil | |
163 | }, | |
164 | }) | |
165 | ||
166 | // Format produces a string representation of zero or more values using a | |
167 | // format string similar to the "printf" function in C. | |
168 | // | |
169 | // It supports the following "verbs": | |
170 | // | |
171 | // %% Literal percent sign, consuming no value | |
172 | // %v A default formatting of the value based on type, as described below. | |
173 | // %#v JSON serialization of the value | |
174 | // %t Converts to boolean and then produces "true" or "false" | |
175 | // %b Converts to number, requires integer, produces binary representation | |
176 | // %d Converts to number, requires integer, produces decimal representation | |
177 | // %o Converts to number, requires integer, produces octal representation | |
178 | // %x Converts to number, requires integer, produces hexadecimal representation | |
179 | // with lowercase letters | |
180 | // %X Like %x but with uppercase letters | |
181 | // %e Converts to number, produces scientific notation like -1.234456e+78 | |
182 | // %E Like %e but with an uppercase "E" representing the exponent | |
183 | // %f Converts to number, produces decimal representation with fractional | |
184 | // part but no exponent, like 123.456 | |
185 | // %g %e for large exponents or %f otherwise | |
186 | // %G %E for large exponents or %f otherwise | |
187 | // %s Converts to string and produces the string's characters | |
188 | // %q Converts to string and produces JSON-quoted string representation, | |
189 | // like %v. | |
190 | // | |
191 | // The default format selections made by %v are: | |
192 | // | |
193 | // string %s | |
194 | // number %g | |
195 | // bool %t | |
196 | // other %#v | |
197 | // | |
198 | // Null values produce the literal keyword "null" for %v and %#v, and produce | |
199 | // an error otherwise. | |
200 | // | |
201 | // Width is specified by an optional decimal number immediately preceding the | |
202 | // verb letter. If absent, the width is whatever is necessary to represent the | |
203 | // value. Precision is specified after the (optional) width by a period | |
204 | // followed by a decimal number. If no period is present, a default precision | |
205 | // is used. A period with no following number is invalid. | |
206 | // For examples: | |
207 | // | |
208 | // %f default width, default precision | |
209 | // %9f width 9, default precision | |
210 | // %.2f default width, precision 2 | |
211 | // %9.2f width 9, precision 2 | |
212 | // | |
213 | // Width and precision are measured in unicode characters (grapheme clusters). | |
214 | // | |
215 | // For most values, width is the minimum number of characters to output, | |
216 | // padding the formatted form with spaces if necessary. | |
217 | // | |
218 | // For strings, precision limits the length of the input to be formatted (not | |
219 | // the size of the output), truncating if necessary. | |
220 | // | |
221 | // For numbers, width sets the minimum width of the field and precision sets | |
222 | // the number of places after the decimal, if appropriate, except that for | |
223 | // %g/%G precision sets the total number of significant digits. | |
224 | // | |
225 | // The following additional symbols can be used immediately after the percent | |
226 | // introducer as flags: | |
227 | // | |
228 | // (a space) leave a space where the sign would be if number is positive | |
229 | // + Include a sign for a number even if it is positive (numeric only) | |
230 | // - Pad with spaces on the left rather than the right | |
231 | // 0 Pad with zeros rather than spaces. | |
232 | // | |
233 | // Flag characters are ignored for verbs that do not support them. | |
234 | // | |
235 | // By default, % sequences consume successive arguments starting with the first. | |
236 | // Introducing a [n] sequence immediately before the verb letter, where n is a | |
237 | // decimal integer, explicitly chooses a particular value argument by its | |
238 | // one-based index. Subsequent calls without an explicit index will then | |
239 | // proceed with n+1, n+2, etc. | |
240 | // | |
241 | // An error is produced if the format string calls for an impossible conversion | |
242 | // or accesses more values than are given. An error is produced also for | |
243 | // an unsupported format verb. | |
244 | func Format(format cty.Value, vals ...cty.Value) (cty.Value, error) { | |
245 | args := make([]cty.Value, 0, len(vals)+1) | |
246 | args = append(args, format) | |
247 | args = append(args, vals...) | |
248 | return FormatFunc.Call(args) | |
249 | } | |
250 | ||
251 | // FormatList applies the same formatting behavior as Format, but accepts | |
252 | // a mixture of list and non-list values as arguments. Any list arguments | |
253 | // passed must have the same length, which dictates the length of the | |
254 | // resulting list. | |
255 | // | |
256 | // Any non-list arguments are used repeatedly for each iteration over the | |
257 | // list arguments. The list arguments are iterated in order by key, so | |
258 | // corresponding items are formatted together. | |
259 | func FormatList(format cty.Value, vals ...cty.Value) (cty.Value, error) { | |
260 | args := make([]cty.Value, 0, len(vals)+1) | |
261 | args = append(args, format) | |
262 | args = append(args, vals...) | |
263 | return FormatListFunc.Call(args) | |
264 | } | |
265 | ||
266 | type formatVerb struct { | |
267 | Raw string | |
268 | Offset int | |
269 | ||
270 | ArgNum int | |
271 | Mode rune | |
272 | ||
273 | Zero bool | |
274 | Sharp bool | |
275 | Plus bool | |
276 | Minus bool | |
277 | Space bool | |
278 | ||
279 | HasPrec bool | |
280 | Prec int | |
281 | ||
282 | HasWidth bool | |
283 | Width int | |
284 | } | |
285 | ||
286 | // formatAppend is called by formatFSM (generated by format_fsm.rl) for each | |
287 | // formatting sequence that is encountered. | |
288 | func formatAppend(verb *formatVerb, buf *bytes.Buffer, args []cty.Value) error { | |
289 | argIdx := verb.ArgNum - 1 | |
290 | if argIdx >= len(args) { | |
291 | return fmt.Errorf( | |
292 | "not enough arguments for %q at %d: need index %d but have %d total", | |
293 | verb.Raw, verb.Offset, | |
294 | verb.ArgNum, len(args), | |
295 | ) | |
296 | } | |
297 | arg := args[argIdx] | |
298 | ||
299 | if verb.Mode != 'v' && arg.IsNull() { | |
300 | return fmt.Errorf("unsupported value for %q at %d: null value cannot be formatted", verb.Raw, verb.Offset) | |
301 | } | |
302 | ||
303 | // Normalize to make some things easier for downstream formatters | |
304 | if !verb.HasWidth { | |
305 | verb.Width = -1 | |
306 | } | |
307 | if !verb.HasPrec { | |
308 | verb.Prec = -1 | |
309 | } | |
310 | ||
311 | // For our first pass we'll ensure the verb is supported and then fan | |
312 | // out to other functions based on what conversion is needed. | |
313 | switch verb.Mode { | |
314 | ||
315 | case 'v': | |
316 | return formatAppendAsIs(verb, buf, arg) | |
317 | ||
318 | case 't': | |
319 | return formatAppendBool(verb, buf, arg) | |
320 | ||
321 | case 'b', 'd', 'o', 'x', 'X', 'e', 'E', 'f', 'g', 'G': | |
322 | return formatAppendNumber(verb, buf, arg) | |
323 | ||
324 | case 's', 'q': | |
325 | return formatAppendString(verb, buf, arg) | |
326 | ||
327 | default: | |
328 | return fmt.Errorf("unsupported format verb %q in %q at offset %d", verb.Mode, verb.Raw, verb.Offset) | |
329 | } | |
330 | } | |
331 | ||
332 | func formatAppendAsIs(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { | |
333 | ||
334 | if !verb.Sharp && !arg.IsNull() { | |
335 | // Unless the caller overrode it with the sharp flag, we'll try some | |
336 | // specialized formats before we fall back on JSON. | |
337 | switch arg.Type() { | |
338 | case cty.String: | |
339 | fmted := arg.AsString() | |
340 | fmted = formatPadWidth(verb, fmted) | |
341 | buf.WriteString(fmted) | |
342 | return nil | |
343 | case cty.Number: | |
344 | bf := arg.AsBigFloat() | |
345 | fmted := bf.Text('g', -1) | |
346 | fmted = formatPadWidth(verb, fmted) | |
347 | buf.WriteString(fmted) | |
348 | return nil | |
349 | } | |
350 | } | |
351 | ||
352 | jb, err := json.Marshal(arg, arg.Type()) | |
353 | if err != nil { | |
354 | return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) | |
355 | } | |
356 | fmted := formatPadWidth(verb, string(jb)) | |
357 | buf.WriteString(fmted) | |
358 | ||
359 | return nil | |
360 | } | |
361 | ||
362 | func formatAppendBool(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { | |
363 | var err error | |
364 | arg, err = convert.Convert(arg, cty.Bool) | |
365 | if err != nil { | |
366 | return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) | |
367 | } | |
368 | ||
369 | if arg.True() { | |
370 | buf.WriteString("true") | |
371 | } else { | |
372 | buf.WriteString("false") | |
373 | } | |
374 | return nil | |
375 | } | |
376 | ||
377 | func formatAppendNumber(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { | |
378 | var err error | |
379 | arg, err = convert.Convert(arg, cty.Number) | |
380 | if err != nil { | |
381 | return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) | |
382 | } | |
383 | ||
384 | switch verb.Mode { | |
385 | case 'b', 'd', 'o', 'x', 'X': | |
386 | return formatAppendInteger(verb, buf, arg) | |
387 | default: | |
388 | bf := arg.AsBigFloat() | |
389 | ||
390 | // For floats our format syntax is a subset of Go's, so it's | |
391 | // safe for us to just lean on the existing Go implementation. | |
392 | fmtstr := formatStripIndexSegment(verb.Raw) | |
393 | fmted := fmt.Sprintf(fmtstr, bf) | |
394 | buf.WriteString(fmted) | |
395 | return nil | |
396 | } | |
397 | } | |
398 | ||
399 | func formatAppendInteger(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { | |
400 | bf := arg.AsBigFloat() | |
401 | bi, acc := bf.Int(nil) | |
402 | if acc != big.Exact { | |
403 | return fmt.Errorf("unsupported value for %q at %d: an integer is required", verb.Raw, verb.Offset) | |
404 | } | |
405 | ||
406 | // For integers our format syntax is a subset of Go's, so it's | |
407 | // safe for us to just lean on the existing Go implementation. | |
408 | fmtstr := formatStripIndexSegment(verb.Raw) | |
409 | fmted := fmt.Sprintf(fmtstr, bi) | |
410 | buf.WriteString(fmted) | |
411 | return nil | |
412 | } | |
413 | ||
414 | func formatAppendString(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { | |
415 | var err error | |
416 | arg, err = convert.Convert(arg, cty.String) | |
417 | if err != nil { | |
418 | return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) | |
419 | } | |
420 | ||
421 | // We _cannot_ directly use the Go fmt.Sprintf implementation for strings | |
422 | // because it measures widths and precisions in runes rather than grapheme | |
423 | // clusters. | |
424 | ||
425 | str := arg.AsString() | |
426 | if verb.Prec > 0 { | |
427 | strB := []byte(str) | |
428 | pos := 0 | |
429 | wanted := verb.Prec | |
430 | for i := 0; i < wanted; i++ { | |
431 | next := strB[pos:] | |
432 | if len(next) == 0 { | |
433 | // ran out of characters before we hit our max width | |
434 | break | |
435 | } | |
436 | d, _, _ := textseg.ScanGraphemeClusters(strB[pos:], true) | |
437 | pos += d | |
438 | } | |
439 | str = str[:pos] | |
440 | } | |
441 | ||
442 | switch verb.Mode { | |
443 | case 's': | |
444 | fmted := formatPadWidth(verb, str) | |
445 | buf.WriteString(fmted) | |
446 | case 'q': | |
447 | jb, err := json.Marshal(cty.StringVal(str), cty.String) | |
448 | if err != nil { | |
449 | // Should never happen, since we know this is a known, non-null string | |
450 | panic(fmt.Errorf("failed to marshal %#v as JSON: %s", arg, err)) | |
451 | } | |
452 | fmted := formatPadWidth(verb, string(jb)) | |
453 | buf.WriteString(fmted) | |
454 | default: | |
455 | // Should never happen because formatAppend should've already validated | |
456 | panic(fmt.Errorf("invalid string formatting mode %q", verb.Mode)) | |
457 | } | |
458 | return nil | |
459 | } | |
460 | ||
461 | func formatPadWidth(verb *formatVerb, fmted string) string { | |
462 | if verb.Width < 0 { | |
463 | return fmted | |
464 | } | |
465 | ||
466 | // Safe to ignore errors because ScanGraphemeClusters cannot produce errors | |
467 | givenLen, _ := textseg.TokenCount([]byte(fmted), textseg.ScanGraphemeClusters) | |
468 | wantLen := verb.Width | |
469 | if givenLen >= wantLen { | |
470 | return fmted | |
471 | } | |
472 | ||
473 | padLen := wantLen - givenLen | |
474 | padChar := " " | |
475 | if verb.Zero { | |
476 | padChar = "0" | |
477 | } | |
478 | pads := strings.Repeat(padChar, padLen) | |
479 | ||
480 | if verb.Minus { | |
481 | return fmted + pads | |
482 | } | |
483 | return pads + fmted | |
484 | } | |
485 | ||
486 | // formatStripIndexSegment strips out any [nnn] segment present in a verb | |
487 | // string so that we can pass it through to Go's fmt.Sprintf with a single | |
488 | // argument. This is used in cases where we're just leaning on Go's formatter | |
489 | // because it's a superset of ours. | |
490 | func formatStripIndexSegment(rawVerb string) string { | |
491 | // We assume the string has already been validated here, since we should | |
492 | // only be using this function with strings that were accepted by our | |
493 | // scanner in formatFSM. | |
494 | start := strings.Index(rawVerb, "[") | |
495 | end := strings.Index(rawVerb, "]") | |
496 | if start == -1 || end == -1 { | |
497 | return rawVerb | |
498 | } | |
499 | ||
500 | return rawVerb[:start] + rawVerb[end+1:] | |
501 | } |