| package stdlib |
| |
| import ( |
| "bytes" |
| "fmt" |
| "math/big" |
| "strings" |
| |
| "github.com/apparentlymart/go-textseg/textseg" |
| |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/convert" |
| "github.com/zclconf/go-cty/cty/function" |
| "github.com/zclconf/go-cty/cty/json" |
| ) |
| |
| //go:generate ragel -Z format_fsm.rl |
| //go:generate gofmt -w format_fsm.go |
| |
| var FormatFunc = function.New(&function.Spec{ |
| Params: []function.Parameter{ |
| { |
| Name: "format", |
| Type: cty.String, |
| }, |
| }, |
| VarParam: &function.Parameter{ |
| Name: "args", |
| Type: cty.DynamicPseudoType, |
| AllowNull: true, |
| }, |
| Type: function.StaticReturnType(cty.String), |
| Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { |
| for _, arg := range args[1:] { |
| if !arg.IsWhollyKnown() { |
| // We require all nested values to be known because the only |
| // thing we can do for a collection/structural type is print |
| // it as JSON and that requires it to be wholly known. |
| return cty.UnknownVal(cty.String), nil |
| } |
| } |
| str, err := formatFSM(args[0].AsString(), args[1:]) |
| return cty.StringVal(str), err |
| }, |
| }) |
| |
| var FormatListFunc = function.New(&function.Spec{ |
| Params: []function.Parameter{ |
| { |
| Name: "format", |
| Type: cty.String, |
| }, |
| }, |
| VarParam: &function.Parameter{ |
| Name: "args", |
| Type: cty.DynamicPseudoType, |
| AllowNull: true, |
| AllowUnknown: true, |
| }, |
| Type: function.StaticReturnType(cty.List(cty.String)), |
| Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { |
| fmtVal := args[0] |
| args = args[1:] |
| |
| if len(args) == 0 { |
| // With no arguments, this function is equivalent to Format, but |
| // returning a single-element list result. |
| result, err := Format(fmtVal, args...) |
| return cty.ListVal([]cty.Value{result}), err |
| } |
| |
| fmtStr := fmtVal.AsString() |
| |
| // Each of our arguments will be dealt with either as an iterator |
| // or as a single value. Iterators are used for sequence-type values |
| // (lists, sets, tuples) while everything else is treated as a |
| // single value. The sequences we iterate over are required to be |
| // all the same length. |
| iterLen := -1 |
| lenChooser := -1 |
| iterators := make([]cty.ElementIterator, len(args)) |
| singleVals := make([]cty.Value, len(args)) |
| for i, arg := range args { |
| argTy := arg.Type() |
| switch { |
| case (argTy.IsListType() || argTy.IsSetType() || argTy.IsTupleType()) && !arg.IsNull(): |
| if !argTy.IsTupleType() && !arg.IsKnown() { |
| // We can't iterate this one at all yet then, so we can't |
| // yet produce a result. |
| return cty.UnknownVal(retType), nil |
| } |
| thisLen := arg.LengthInt() |
| if iterLen == -1 { |
| iterLen = thisLen |
| lenChooser = i |
| } else { |
| if thisLen != iterLen { |
| return cty.NullVal(cty.List(cty.String)), function.NewArgErrorf( |
| i+1, |
| "argument %d has length %d, which is inconsistent with argument %d of length %d", |
| i+1, thisLen, |
| lenChooser+1, iterLen, |
| ) |
| } |
| } |
| iterators[i] = arg.ElementIterator() |
| default: |
| singleVals[i] = arg |
| } |
| } |
| |
| if iterLen == 0 { |
| // If our sequences are all empty then our result must be empty. |
| return cty.ListValEmpty(cty.String), nil |
| } |
| |
| if iterLen == -1 { |
| // If we didn't encounter any iterables at all then we're going |
| // to just do one iteration with items from singleVals. |
| iterLen = 1 |
| } |
| |
| ret := make([]cty.Value, 0, iterLen) |
| fmtArgs := make([]cty.Value, len(iterators)) |
| Results: |
| for iterIdx := 0; iterIdx < iterLen; iterIdx++ { |
| |
| // Construct our arguments for a single format call |
| for i := range fmtArgs { |
| switch { |
| case iterators[i] != nil: |
| iterator := iterators[i] |
| iterator.Next() |
| _, val := iterator.Element() |
| fmtArgs[i] = val |
| default: |
| fmtArgs[i] = singleVals[i] |
| } |
| |
| // If any of the arguments to this call would be unknown then |
| // this particular result is unknown, but we'll keep going |
| // to see if any other iterations can produce known values. |
| if !fmtArgs[i].IsWhollyKnown() { |
| // We require all nested values to be known because the only |
| // thing we can do for a collection/structural type is print |
| // it as JSON and that requires it to be wholly known. |
| ret = append(ret, cty.UnknownVal(cty.String)) |
| continue Results |
| } |
| } |
| |
| str, err := formatFSM(fmtStr, fmtArgs) |
| if err != nil { |
| return cty.NullVal(cty.List(cty.String)), fmt.Errorf( |
| "error on format iteration %d: %s", iterIdx, err, |
| ) |
| } |
| |
| ret = append(ret, cty.StringVal(str)) |
| } |
| |
| return cty.ListVal(ret), nil |
| }, |
| }) |
| |
| // Format produces a string representation of zero or more values using a |
| // format string similar to the "printf" function in C. |
| // |
| // It supports the following "verbs": |
| // |
| // %% Literal percent sign, consuming no value |
| // %v A default formatting of the value based on type, as described below. |
| // %#v JSON serialization of the value |
| // %t Converts to boolean and then produces "true" or "false" |
| // %b Converts to number, requires integer, produces binary representation |
| // %d Converts to number, requires integer, produces decimal representation |
| // %o Converts to number, requires integer, produces octal representation |
| // %x Converts to number, requires integer, produces hexadecimal representation |
| // with lowercase letters |
| // %X Like %x but with uppercase letters |
| // %e Converts to number, produces scientific notation like -1.234456e+78 |
| // %E Like %e but with an uppercase "E" representing the exponent |
| // %f Converts to number, produces decimal representation with fractional |
| // part but no exponent, like 123.456 |
| // %g %e for large exponents or %f otherwise |
| // %G %E for large exponents or %f otherwise |
| // %s Converts to string and produces the string's characters |
| // %q Converts to string and produces JSON-quoted string representation, |
| // like %v. |
| // |
| // The default format selections made by %v are: |
| // |
| // string %s |
| // number %g |
| // bool %t |
| // other %#v |
| // |
| // Null values produce the literal keyword "null" for %v and %#v, and produce |
| // an error otherwise. |
| // |
| // Width is specified by an optional decimal number immediately preceding the |
| // verb letter. If absent, the width is whatever is necessary to represent the |
| // value. Precision is specified after the (optional) width by a period |
| // followed by a decimal number. If no period is present, a default precision |
| // is used. A period with no following number is invalid. |
| // For examples: |
| // |
| // %f default width, default precision |
| // %9f width 9, default precision |
| // %.2f default width, precision 2 |
| // %9.2f width 9, precision 2 |
| // |
| // Width and precision are measured in unicode characters (grapheme clusters). |
| // |
| // For most values, width is the minimum number of characters to output, |
| // padding the formatted form with spaces if necessary. |
| // |
| // For strings, precision limits the length of the input to be formatted (not |
| // the size of the output), truncating if necessary. |
| // |
| // For numbers, width sets the minimum width of the field and precision sets |
| // the number of places after the decimal, if appropriate, except that for |
| // %g/%G precision sets the total number of significant digits. |
| // |
| // The following additional symbols can be used immediately after the percent |
| // introducer as flags: |
| // |
| // (a space) leave a space where the sign would be if number is positive |
| // + Include a sign for a number even if it is positive (numeric only) |
| // - Pad with spaces on the left rather than the right |
| // 0 Pad with zeros rather than spaces. |
| // |
| // Flag characters are ignored for verbs that do not support them. |
| // |
| // By default, % sequences consume successive arguments starting with the first. |
| // Introducing a [n] sequence immediately before the verb letter, where n is a |
| // decimal integer, explicitly chooses a particular value argument by its |
| // one-based index. Subsequent calls without an explicit index will then |
| // proceed with n+1, n+2, etc. |
| // |
| // An error is produced if the format string calls for an impossible conversion |
| // or accesses more values than are given. An error is produced also for |
| // an unsupported format verb. |
| func Format(format cty.Value, vals ...cty.Value) (cty.Value, error) { |
| args := make([]cty.Value, 0, len(vals)+1) |
| args = append(args, format) |
| args = append(args, vals...) |
| return FormatFunc.Call(args) |
| } |
| |
| // FormatList applies the same formatting behavior as Format, but accepts |
| // a mixture of list and non-list values as arguments. Any list arguments |
| // passed must have the same length, which dictates the length of the |
| // resulting list. |
| // |
| // Any non-list arguments are used repeatedly for each iteration over the |
| // list arguments. The list arguments are iterated in order by key, so |
| // corresponding items are formatted together. |
| func FormatList(format cty.Value, vals ...cty.Value) (cty.Value, error) { |
| args := make([]cty.Value, 0, len(vals)+1) |
| args = append(args, format) |
| args = append(args, vals...) |
| return FormatListFunc.Call(args) |
| } |
| |
| type formatVerb struct { |
| Raw string |
| Offset int |
| |
| ArgNum int |
| Mode rune |
| |
| Zero bool |
| Sharp bool |
| Plus bool |
| Minus bool |
| Space bool |
| |
| HasPrec bool |
| Prec int |
| |
| HasWidth bool |
| Width int |
| } |
| |
| // formatAppend is called by formatFSM (generated by format_fsm.rl) for each |
| // formatting sequence that is encountered. |
| func formatAppend(verb *formatVerb, buf *bytes.Buffer, args []cty.Value) error { |
| argIdx := verb.ArgNum - 1 |
| if argIdx >= len(args) { |
| return fmt.Errorf( |
| "not enough arguments for %q at %d: need index %d but have %d total", |
| verb.Raw, verb.Offset, |
| verb.ArgNum, len(args), |
| ) |
| } |
| arg := args[argIdx] |
| |
| if verb.Mode != 'v' && arg.IsNull() { |
| return fmt.Errorf("unsupported value for %q at %d: null value cannot be formatted", verb.Raw, verb.Offset) |
| } |
| |
| // Normalize to make some things easier for downstream formatters |
| if !verb.HasWidth { |
| verb.Width = -1 |
| } |
| if !verb.HasPrec { |
| verb.Prec = -1 |
| } |
| |
| // For our first pass we'll ensure the verb is supported and then fan |
| // out to other functions based on what conversion is needed. |
| switch verb.Mode { |
| |
| case 'v': |
| return formatAppendAsIs(verb, buf, arg) |
| |
| case 't': |
| return formatAppendBool(verb, buf, arg) |
| |
| case 'b', 'd', 'o', 'x', 'X', 'e', 'E', 'f', 'g', 'G': |
| return formatAppendNumber(verb, buf, arg) |
| |
| case 's', 'q': |
| return formatAppendString(verb, buf, arg) |
| |
| default: |
| return fmt.Errorf("unsupported format verb %q in %q at offset %d", verb.Mode, verb.Raw, verb.Offset) |
| } |
| } |
| |
| func formatAppendAsIs(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { |
| |
| if !verb.Sharp && !arg.IsNull() { |
| // Unless the caller overrode it with the sharp flag, we'll try some |
| // specialized formats before we fall back on JSON. |
| switch arg.Type() { |
| case cty.String: |
| fmted := arg.AsString() |
| fmted = formatPadWidth(verb, fmted) |
| buf.WriteString(fmted) |
| return nil |
| case cty.Number: |
| bf := arg.AsBigFloat() |
| fmted := bf.Text('g', -1) |
| fmted = formatPadWidth(verb, fmted) |
| buf.WriteString(fmted) |
| return nil |
| } |
| } |
| |
| jb, err := json.Marshal(arg, arg.Type()) |
| if err != nil { |
| return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) |
| } |
| fmted := formatPadWidth(verb, string(jb)) |
| buf.WriteString(fmted) |
| |
| return nil |
| } |
| |
| func formatAppendBool(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { |
| var err error |
| arg, err = convert.Convert(arg, cty.Bool) |
| if err != nil { |
| return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) |
| } |
| |
| if arg.True() { |
| buf.WriteString("true") |
| } else { |
| buf.WriteString("false") |
| } |
| return nil |
| } |
| |
| func formatAppendNumber(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { |
| var err error |
| arg, err = convert.Convert(arg, cty.Number) |
| if err != nil { |
| return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) |
| } |
| |
| switch verb.Mode { |
| case 'b', 'd', 'o', 'x', 'X': |
| return formatAppendInteger(verb, buf, arg) |
| default: |
| bf := arg.AsBigFloat() |
| |
| // For floats our format syntax is a subset of Go's, so it's |
| // safe for us to just lean on the existing Go implementation. |
| fmtstr := formatStripIndexSegment(verb.Raw) |
| fmted := fmt.Sprintf(fmtstr, bf) |
| buf.WriteString(fmted) |
| return nil |
| } |
| } |
| |
| func formatAppendInteger(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { |
| bf := arg.AsBigFloat() |
| bi, acc := bf.Int(nil) |
| if acc != big.Exact { |
| return fmt.Errorf("unsupported value for %q at %d: an integer is required", verb.Raw, verb.Offset) |
| } |
| |
| // For integers our format syntax is a subset of Go's, so it's |
| // safe for us to just lean on the existing Go implementation. |
| fmtstr := formatStripIndexSegment(verb.Raw) |
| fmted := fmt.Sprintf(fmtstr, bi) |
| buf.WriteString(fmted) |
| return nil |
| } |
| |
| func formatAppendString(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error { |
| var err error |
| arg, err = convert.Convert(arg, cty.String) |
| if err != nil { |
| return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err) |
| } |
| |
| // We _cannot_ directly use the Go fmt.Sprintf implementation for strings |
| // because it measures widths and precisions in runes rather than grapheme |
| // clusters. |
| |
| str := arg.AsString() |
| if verb.Prec > 0 { |
| strB := []byte(str) |
| pos := 0 |
| wanted := verb.Prec |
| for i := 0; i < wanted; i++ { |
| next := strB[pos:] |
| if len(next) == 0 { |
| // ran out of characters before we hit our max width |
| break |
| } |
| d, _, _ := textseg.ScanGraphemeClusters(strB[pos:], true) |
| pos += d |
| } |
| str = str[:pos] |
| } |
| |
| switch verb.Mode { |
| case 's': |
| fmted := formatPadWidth(verb, str) |
| buf.WriteString(fmted) |
| case 'q': |
| jb, err := json.Marshal(cty.StringVal(str), cty.String) |
| if err != nil { |
| // Should never happen, since we know this is a known, non-null string |
| panic(fmt.Errorf("failed to marshal %#v as JSON: %s", arg, err)) |
| } |
| fmted := formatPadWidth(verb, string(jb)) |
| buf.WriteString(fmted) |
| default: |
| // Should never happen because formatAppend should've already validated |
| panic(fmt.Errorf("invalid string formatting mode %q", verb.Mode)) |
| } |
| return nil |
| } |
| |
| func formatPadWidth(verb *formatVerb, fmted string) string { |
| if verb.Width < 0 { |
| return fmted |
| } |
| |
| // Safe to ignore errors because ScanGraphemeClusters cannot produce errors |
| givenLen, _ := textseg.TokenCount([]byte(fmted), textseg.ScanGraphemeClusters) |
| wantLen := verb.Width |
| if givenLen >= wantLen { |
| return fmted |
| } |
| |
| padLen := wantLen - givenLen |
| padChar := " " |
| if verb.Zero { |
| padChar = "0" |
| } |
| pads := strings.Repeat(padChar, padLen) |
| |
| if verb.Minus { |
| return fmted + pads |
| } |
| return pads + fmted |
| } |
| |
| // formatStripIndexSegment strips out any [nnn] segment present in a verb |
| // string so that we can pass it through to Go's fmt.Sprintf with a single |
| // argument. This is used in cases where we're just leaning on Go's formatter |
| // because it's a superset of ours. |
| func formatStripIndexSegment(rawVerb string) string { |
| // We assume the string has already been validated here, since we should |
| // only be using this function with strings that were accepted by our |
| // scanner in formatFSM. |
| start := strings.Index(rawVerb, "[") |
| end := strings.Index(rawVerb, "]") |
| if start == -1 || end == -1 { |
| return rawVerb |
| } |
| |
| return rawVerb[:start] + rawVerb[end+1:] |
| } |