blob: 664790b46b46322a50479723da2bf0f5fd58a7d7 [file] [log] [blame]
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:]
}