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 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
92 thisLen := arg.LengthInt()
97 if thisLen != iterLen {
98 return cty.NullVal(cty.List(cty.String)), function.NewArgErrorf(
100 "argument %d has length %d, which is inconsistent with argument %d of length %d",
102 lenChooser+1, iterLen,
106 iterators[i] = arg.ElementIterator()
113 // If our sequences are all empty then our result must be empty.
114 return cty.ListValEmpty(cty.String), nil
118 // If we didn't encounter any iterables at all then we're going
119 // to just do one iteration with items from singleVals.
123 ret := make([]cty.Value, 0, iterLen)
124 fmtArgs := make([]cty.Value, len(iterators))
126 for iterIdx := 0; iterIdx < iterLen; iterIdx++ {
128 // Construct our arguments for a single format call
129 for i := range fmtArgs {
131 case iterators[i] != nil:
132 iterator := iterators[i]
134 _, val := iterator.Element()
137 fmtArgs[i] = singleVals[i]
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))
152 str, err := formatFSM(fmtStr, fmtArgs)
154 return cty.NullVal(cty.List(cty.String)), fmt.Errorf(
155 "error on format iteration %d: %s", iterIdx, err,
159 ret = append(ret, cty.StringVal(str))
162 return cty.ListVal(ret), nil
166 // Format produces a string representation of zero or more values using a
167 // format string similar to the "printf" function in C.
169 // It supports the following "verbs":
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,
191 // The default format selections made by %v are:
198 // Null values produce the literal keyword "null" for %v and %#v, and produce
199 // an error otherwise.
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.
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
213 // Width and precision are measured in unicode characters (grapheme clusters).
215 // For most values, width is the minimum number of characters to output,
216 // padding the formatted form with spaces if necessary.
218 // For strings, precision limits the length of the input to be formatted (not
219 // the size of the output), truncating if necessary.
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.
225 // The following additional symbols can be used immediately after the percent
226 // introducer as flags:
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.
233 // Flag characters are ignored for verbs that do not support them.
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.
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)
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
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)
266 type formatVerb struct {
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) {
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),
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)
303 // Normalize to make some things easier for downstream formatters
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.
316 return formatAppendAsIs(verb, buf, arg)
319 return formatAppendBool(verb, buf, arg)
321 case 'b', 'd', 'o', 'x', 'X', 'e', 'E', 'f', 'g', 'G':
322 return formatAppendNumber(verb, buf, arg)
325 return formatAppendString(verb, buf, arg)
328 return fmt.Errorf("unsupported format verb %q in %q at offset %d", verb.Mode, verb.Raw, verb.Offset)
332 func formatAppendAsIs(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
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.
339 fmted := arg.AsString()
340 fmted = formatPadWidth(verb, fmted)
341 buf.WriteString(fmted)
344 bf := arg.AsBigFloat()
345 fmted := bf.Text('g', -1)
346 fmted = formatPadWidth(verb, fmted)
347 buf.WriteString(fmted)
352 jb, err := json.Marshal(arg, arg.Type())
354 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
356 fmted := formatPadWidth(verb, string(jb))
357 buf.WriteString(fmted)
362 func formatAppendBool(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
364 arg, err = convert.Convert(arg, cty.Bool)
366 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
370 buf.WriteString("true")
372 buf.WriteString("false")
377 func formatAppendNumber(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
379 arg, err = convert.Convert(arg, cty.Number)
381 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
385 case 'b', 'd', 'o', 'x', 'X':
386 return formatAppendInteger(verb, buf, arg)
388 bf := arg.AsBigFloat()
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)
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)
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)
414 func formatAppendString(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
416 arg, err = convert.Convert(arg, cty.String)
418 return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
421 // We _cannot_ directly use the Go fmt.Sprintf implementation for strings
422 // because it measures widths and precisions in runes rather than grapheme
425 str := arg.AsString()
430 for i := 0; i < wanted; i++ {
433 // ran out of characters before we hit our max width
436 d, _, _ := textseg.ScanGraphemeClusters(strB[pos:], true)
444 fmted := formatPadWidth(verb, str)
445 buf.WriteString(fmted)
447 jb, err := json.Marshal(cty.StringVal(str), cty.String)
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))
452 fmted := formatPadWidth(verb, string(jb))
453 buf.WriteString(fmted)
455 // Should never happen because formatAppend should've already validated
456 panic(fmt.Errorf("invalid string formatting mode %q", verb.Mode))
461 func formatPadWidth(verb *formatVerb, fmted string) string {
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 {
473 padLen := wantLen - givenLen
478 pads := strings.Repeat(padChar, padLen)
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 {
500 return rawVerb[:start] + rawVerb[end+1:]