9 "github.com/apparentlymart/go-textseg/textseg"
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"
17 //go:generate ragel -Z format_fsm.rl
18 //go:generate gofmt -w format_fsm.go
20 var FormatFunc = function.New(&function.Spec{
21 Params: []function.Parameter{
27 VarParam: &function.Parameter{
29 Type: cty.DynamicPseudoType,
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
42 str, err := formatFSM(args[0].AsString(), args[1:])
43 return cty.StringVal(str), err
47 var FormatListFunc = function.New(&function.Spec{
48 Params: []function.Parameter{
54 VarParam: &function.Parameter{
56 Type: cty.DynamicPseudoType,
60 Type: function.StaticReturnType(cty.List(cty.String)),
61 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
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
72 fmtStr := fmtVal.AsString()
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.
81 iterators := make([]cty.ElementIterator, len(args))
82 singleVals := make([]cty.Value, len(args))
83 for i, arg := range args {
86 case (argTy.IsListType() || argTy.IsSetType() || argTy.IsTupleType()) && !arg.IsNull():
87 thisLen := arg.LengthInt()
92 if thisLen != iterLen {
93 return cty.NullVal(cty.List(cty.String)), function.NewArgErrorf(
95 "argument %d has length %d, which is inconsistent with argument %d of length %d",
97 lenChooser+1, iterLen,
101 iterators[i] = arg.ElementIterator()
108 // If our sequences are all empty then our result must be empty.
109 return cty.ListValEmpty(cty.String), nil
113 // If we didn't encounter any iterables at all then we're going
114 // to just do one iteration with items from singleVals.
118 ret := make([]cty.Value, 0, iterLen)
119 fmtArgs := make([]cty.Value, len(iterators))
121 for iterIdx := 0; iterIdx < iterLen; iterIdx++ {
123 // Construct our arguments for a single format call
124 for i := range fmtArgs {
126 case iterators[i] != nil:
127 iterator := iterators[i]
129 _, val := iterator.Element()
132 fmtArgs[i] = singleVals[i]
135 // If any of the arguments to this call would be unknown then
136 // this particular result is unknown, but we'll keep going
137 // to see if any other iterations can produce known values.
138 if !fmtArgs[i].IsWhollyKnown() {
139 // We require all nested values to be known because the only
140 // thing we can do for a collection/structural type is print
141 // it as JSON and that requires it to be wholly known.
142 ret = append(ret, cty.UnknownVal(cty.String))
147 str, err := formatFSM(fmtStr, fmtArgs)
149 return cty.NullVal(cty.List(cty.String)), fmt.Errorf(
150 "error on format iteration %d: %s", iterIdx, err,
154 ret = append(ret, cty.StringVal(str))
157 return cty.ListVal(ret), nil
161 // Format produces a string representation of zero or more values using a
162 // format string similar to the "printf" function in C.
164 // It supports the following "verbs":
166 // %% Literal percent sign, consuming no value
167 // %v A default formatting of the value based on type, as described below.
168 // %#v JSON serialization of the value
169 // %t Converts to boolean and then produces "true" or "false"
170 // %b Converts to number, requires integer, produces binary representation
171 // %d Converts to number, requires integer, produces decimal representation
172 // %o Converts to number, requires integer, produces octal representation
173 // %x Converts to number, requires integer, produces hexadecimal representation
174 // with lowercase letters
175 // %X Like %x but with uppercase letters
176 // %e Converts to number, produces scientific notation like -1.234456e+78
177 // %E Like %e but with an uppercase "E" representing the exponent
178 // %f Converts to number, produces decimal representation with fractional
179 // part but no exponent, like 123.456
180 // %g %e for large exponents or %f otherwise
181 // %G %E for large exponents or %f otherwise
182 // %s Converts to string and produces the string's characters
183 // %q Converts to string and produces JSON-quoted string representation,
186 // The default format selections made by %v are:
193 // Null values produce the literal keyword "null" for %v and %#v, and produce
194 // an error otherwise.
196 // Width is specified by an optional decimal number immediately preceding the
197 // verb letter. If absent, the width is whatever is necessary to represent the
198 // value. Precision is specified after the (optional) width by a period
199 // followed by a decimal number. If no period is present, a default precision
200 // is used. A period with no following number is invalid.
203 // %f default width, default precision
204 // %9f width 9, default precision
205 // %.2f default width, precision 2
206 // %9.2f width 9, precision 2
208 // Width and precision are measured in unicode characters (grapheme clusters).
210 // For most values, width is the minimum number of characters to output,
211 // padding the formatted form with spaces if necessary.
213 // For strings, precision limits the length of the input to be formatted (not
214 // the size of the output), truncating if necessary.
216 // For numbers, width sets the minimum width of the field and precision sets
217 // the number of places after the decimal, if appropriate, except that for
218 // %g/%G precision sets the total number of significant digits.
220 // The following additional symbols can be used immediately after the percent
221 // introducer as flags:
223 // (a space) leave a space where the sign would be if number is positive
224 // + Include a sign for a number even if it is positive (numeric only)
225 // - Pad with spaces on the left rather than the right
226 // 0 Pad with zeros rather than spaces.
228 // Flag characters are ignored for verbs that do not support them.
230 // By default, % sequences consume successive arguments starting with the first.
231 // Introducing a [n] sequence immediately before the verb letter, where n is a
232 // decimal integer, explicitly chooses a particular value argument by its
233 // one-based index. Subsequent calls without an explicit index will then
234 // proceed with n+1, n+2, etc.
236 // An error is produced if the format string calls for an impossible conversion
237 // or accesses more values than are given. An error is produced also for
238 // an unsupported format verb.
239 func Format(format cty.Value, vals ...cty.Value) (cty.Value, error) {
240 args := make([]cty.Value, 0, len(vals)+1)
241 args = append(args, format)
242 args = append(args, vals...)
243 return FormatFunc.Call(args)
246 // FormatList applies the same formatting behavior as Format, but accepts
247 // a mixture of list and non-list values as arguments. Any list arguments
248 // passed must have the same length, which dictates the length of the
251 // Any non-list arguments are used repeatedly for each iteration over the
252 // list arguments. The list arguments are iterated in order by key, so
253 // corresponding items are formatted together.
254 func FormatList(format cty.Value, vals ...cty.Value) (cty.Value, error) {
255 args := make([]cty.Value, 0, len(vals)+1)
256 args = append(args, format)
257 args = append(args, vals...)
258 return FormatListFunc.Call(args)
261 type formatVerb struct {
281 // formatAppend is called by formatFSM (generated by format_fsm.rl) for each
282 // formatting sequence that is encountered.
283 func formatAppend(verb *formatVerb, buf *bytes.Buffer, args []cty.Value) error {
284 argIdx := verb.ArgNum - 1
285 if argIdx >= len(args) {
287 "not enough arguments for %q at %d: need index %d but have %d total",
288 verb.Raw, verb.Offset,
289 verb.ArgNum, len(args),
294 if verb.Mode != 'v' && arg.IsNull() {
295 return fmt.Errorf("unsupported value for %q at %d: null value cannot be formatted", verb.Raw, verb.Offset)
298 // Normalize to make some things easier for downstream formatters
306 // For our first pass we'll ensure the verb is supported and then fan
307 // out to other functions based on what conversion is needed.
311 return formatAppendAsIs(verb, buf, arg)
314 return formatAppendBool(verb, buf, arg)
316 case 'b', 'd', 'o', 'x', 'X', 'e', 'E', 'f', 'g', 'G':
317 return formatAppendNumber(verb, buf, arg)
320 return formatAppendString(verb, buf, arg)
323 return fmt.Errorf("unsupported format verb %q in %q at offset %d", verb.Mode, verb.Raw, verb.Offset)
327 func formatAppendAsIs(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
329 if !verb.Sharp && !arg.IsNull() {
330 // Unless the caller overrode it with the sharp flag, we'll try some
331 // specialized formats before we fall back on JSON.
334 fmted := arg.AsString()
335 fmted = formatPadWidth(verb, fmted)
336 buf.WriteString(fmted)
339 bf := arg.AsBigFloat()
340 fmted := bf.Text('g', -1)
341 fmted = formatPadWidth(verb, fmted)
342 buf.WriteString(fmted)
347 jb, err := json.Marshal(arg, arg.Type())
349 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
351 fmted := formatPadWidth(verb, string(jb))
352 buf.WriteString(fmted)
357 func formatAppendBool(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
359 arg, err = convert.Convert(arg, cty.Bool)
361 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
365 buf.WriteString("true")
367 buf.WriteString("false")
372 func formatAppendNumber(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
374 arg, err = convert.Convert(arg, cty.Number)
376 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
380 case 'b', 'd', 'o', 'x', 'X':
381 return formatAppendInteger(verb, buf, arg)
383 bf := arg.AsBigFloat()
385 // For floats our format syntax is a subset of Go's, so it's
386 // safe for us to just lean on the existing Go implementation.
387 fmtstr := formatStripIndexSegment(verb.Raw)
388 fmted := fmt.Sprintf(fmtstr, bf)
389 buf.WriteString(fmted)
394 func formatAppendInteger(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
395 bf := arg.AsBigFloat()
396 bi, acc := bf.Int(nil)
397 if acc != big.Exact {
398 return fmt.Errorf("unsupported value for %q at %d: an integer is required", verb.Raw, verb.Offset)
401 // For integers our format syntax is a subset of Go's, so it's
402 // safe for us to just lean on the existing Go implementation.
403 fmtstr := formatStripIndexSegment(verb.Raw)
404 fmted := fmt.Sprintf(fmtstr, bi)
405 buf.WriteString(fmted)
409 func formatAppendString(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
411 arg, err = convert.Convert(arg, cty.String)
413 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
416 // We _cannot_ directly use the Go fmt.Sprintf implementation for strings
417 // because it measures widths and precisions in runes rather than grapheme
420 str := arg.AsString()
425 for i := 0; i < wanted; i++ {
428 // ran out of characters before we hit our max width
431 d, _, _ := textseg.ScanGraphemeClusters(strB[pos:], true)
439 fmted := formatPadWidth(verb, str)
440 buf.WriteString(fmted)
442 jb, err := json.Marshal(cty.StringVal(str), cty.String)
444 // Should never happen, since we know this is a known, non-null string
445 panic(fmt.Errorf("failed to marshal %#v as JSON: %s", arg, err))
447 fmted := formatPadWidth(verb, string(jb))
448 buf.WriteString(fmted)
450 // Should never happen because formatAppend should've already validated
451 panic(fmt.Errorf("invalid string formatting mode %q", verb.Mode))
456 func formatPadWidth(verb *formatVerb, fmted string) string {
461 // Safe to ignore errors because ScanGraphemeClusters cannot produce errors
462 givenLen, _ := textseg.TokenCount([]byte(fmted), textseg.ScanGraphemeClusters)
463 wantLen := verb.Width
464 if givenLen >= wantLen {
468 padLen := wantLen - givenLen
473 pads := strings.Repeat(padChar, padLen)
481 // formatStripIndexSegment strips out any [nnn] segment present in a verb
482 // string so that we can pass it through to Go's fmt.Sprintf with a single
483 // argument. This is used in cases where we're just leaning on Go's formatter
484 // because it's a superset of ours.
485 func formatStripIndexSegment(rawVerb string) string {
486 // We assume the string has already been validated here, since we should
487 // only be using this function with strings that were accepted by our
488 // scanner in formatFSM.
489 start := strings.Index(rawVerb, "[")
490 end := strings.Index(rawVerb, "]")
491 if start == -1 || end == -1 {
495 return rawVerb[:start] + rawVerb[end+1:]