github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/printf/printf.go (about) 1 // Package printf implements a parser for fmt.Printf-style format 2 // strings. 3 // 4 // It parses verbs according to the following syntax: 5 // 6 // Numeric -> '0'-'9' 7 // Letter -> 'a'-'z' | 'A'-'Z' 8 // Index -> '[' Numeric+ ']' 9 // Star -> '*' 10 // Star -> Index '*' 11 // 12 // Precision -> Numeric+ | Star 13 // Width -> Numeric+ | Star 14 // 15 // WidthAndPrecision -> Width '.' Precision 16 // WidthAndPrecision -> Width '.' 17 // WidthAndPrecision -> Width 18 // WidthAndPrecision -> '.' Precision 19 // WidthAndPrecision -> '.' 20 // 21 // Flag -> '+' | '-' | '#' | ' ' | '0' 22 // Verb -> Letter | '%' 23 // 24 // Input -> '%' [ Flag+ ] [ WidthAndPrecision ] [ Index ] Verb 25 package printf 26 27 import ( 28 "errors" 29 "regexp" 30 "strconv" 31 "strings" 32 ) 33 34 // ErrInvalid is returned for invalid format strings or verbs. 35 var ErrInvalid = errors.New("invalid format string") 36 37 type Verb struct { 38 Letter rune 39 Flags string 40 41 Width Argument 42 Precision Argument 43 // Which value in the argument list the verb uses. 44 // -1 denotes the next argument, 45 // values > 0 denote explicit arguments. 46 // The value 0 denotes that no argument is consumed. This is the case for %%. 47 Value int 48 49 Raw string 50 } 51 52 // Argument is an implicit or explicit width or precision. 53 type Argument interface { 54 isArgument() 55 } 56 57 // The Default value, when no width or precision is provided. 58 type Default struct{} 59 60 // Zero is the implicit zero value. 61 // This value may only appear for precisions in format strings like %6.f 62 type Zero struct{} 63 64 // Star is a * value, which may either refer to the next argument (Index == -1) or an explicit argument. 65 type Star struct{ Index int } 66 67 // A Literal value, such as 6 in %6d. 68 type Literal int 69 70 func (Default) isArgument() {} 71 func (Zero) isArgument() {} 72 func (Star) isArgument() {} 73 func (Literal) isArgument() {} 74 75 // Parse parses f and returns a list of actions. 76 // An action may either be a literal string, or a Verb. 77 func Parse(f string) ([]interface{}, error) { 78 var out []interface{} 79 for len(f) > 0 { 80 if f[0] == '%' { 81 v, n, err := ParseVerb(f) 82 if err != nil { 83 return nil, err 84 } 85 f = f[n:] 86 out = append(out, v) 87 } else { 88 n := strings.IndexByte(f, '%') 89 if n > -1 { 90 out = append(out, f[:n]) 91 f = f[n:] 92 } else { 93 out = append(out, f) 94 f = "" 95 } 96 } 97 } 98 99 return out, nil 100 } 101 102 func atoi(s string) int { 103 n, _ := strconv.Atoi(s) 104 return n 105 } 106 107 // ParseVerb parses the verb at the beginning of f. 108 // It returns the verb, how much of the input was consumed, and an error, if any. 109 func ParseVerb(f string) (Verb, int, error) { 110 if len(f) < 2 { 111 return Verb{}, 0, ErrInvalid 112 } 113 const ( 114 flags = 1 115 116 width = 2 117 widthStar = 3 118 widthIndex = 5 119 120 dot = 6 121 prec = 7 122 precStar = 8 123 precIndex = 10 124 125 verbIndex = 11 126 verb = 12 127 ) 128 129 m := re.FindStringSubmatch(f) 130 if m == nil { 131 return Verb{}, 0, ErrInvalid 132 } 133 134 v := Verb{ 135 Letter: []rune(m[verb])[0], 136 Flags: m[flags], 137 Raw: m[0], 138 } 139 140 if m[width] != "" { 141 // Literal width 142 v.Width = Literal(atoi(m[width])) 143 } else if m[widthStar] != "" { 144 // Star width 145 if m[widthIndex] != "" { 146 v.Width = Star{atoi(m[widthIndex])} 147 } else { 148 v.Width = Star{-1} 149 } 150 } else { 151 // Default width 152 v.Width = Default{} 153 } 154 155 if m[dot] == "" { 156 // default precision 157 v.Precision = Default{} 158 } else { 159 if m[prec] != "" { 160 // Literal precision 161 v.Precision = Literal(atoi(m[prec])) 162 } else if m[precStar] != "" { 163 // Star precision 164 if m[precIndex] != "" { 165 v.Precision = Star{atoi(m[precIndex])} 166 } else { 167 v.Precision = Star{-1} 168 } 169 } else { 170 // Zero precision 171 v.Precision = Zero{} 172 } 173 } 174 175 if m[verb] == "%" { 176 v.Value = 0 177 } else if m[verbIndex] != "" { 178 v.Value = atoi(m[verbIndex]) 179 } else { 180 v.Value = -1 181 } 182 183 return v, len(m[0]), nil 184 } 185 186 const ( 187 flags = `([+#0 -]*)` 188 verb = `([a-zA-Z%])` 189 index = `(?:\[([0-9]+)\])` 190 star = `((` + index + `)?\*)` 191 width1 = `([0-9]+)` 192 width2 = star 193 width = `(?:` + width1 + `|` + width2 + `)` 194 precision = width 195 widthAndPrecision = `(?:(?:` + width + `)?(?:(\.)(?:` + precision + `)?)?)` 196 ) 197 198 var re = regexp.MustCompile(`^%` + flags + widthAndPrecision + `?` + index + `?` + verb)