A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1package printf
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "math"
9 "regexp"
10 "slices"
11 "strconv"
12 "strings"
13 "time"
14 "unicode/utf8"
15
16 "mvdan.cc/sh/v3/expand"
17 "mvdan.cc/sh/v3/interp"
18 "tangled.org/xeiaso.net/kefka/command"
19)
20
21type Impl struct{}
22
23func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error {
24 if ec == nil {
25 return errors.New("printf: nil ExecContext")
26 }
27
28 stdout := ec.Stdout
29 if stdout == nil {
30 stdout = io.Discard
31 }
32 stderr := ec.Stderr
33 if stderr == nil {
34 stderr = io.Discard
35 }
36
37 if slices.Contains(args, "--help") {
38 printHelp(stderr)
39 return nil
40 }
41
42 if len(args) == 0 {
43 fmt.Fprint(stderr, "printf: usage: printf format [arguments]\n")
44 return interp.ExitStatus(2)
45 }
46
47 // -v VAR is a kefka extension influenced by the bash builtin: it stores
48 // the formatted output in VAR instead of writing to stdout. GNU coreutils
49 // printf does not support -v, but kefka's shell integration depends on
50 // it, so it is preserved here. Passing "--" before the format disables
51 // option parsing, matching POSIX usage so a literal "-v" can still be
52 // printed via `printf -- '%s' '-v'`.
53 var targetVar string
54 hasTargetVar := false
55 argIdx := 0
56
57 for argIdx < len(args) {
58 arg := args[argIdx]
59 if arg == "--" {
60 argIdx++
61 break
62 }
63 if arg == "-v" {
64 if argIdx+1 >= len(args) {
65 fmt.Fprint(stderr, "printf: -v: option requires an argument\n")
66 return interp.ExitStatus(1)
67 }
68 targetVar = args[argIdx+1]
69 hasTargetVar = true
70 if !validIdentifier(targetVar) {
71 fmt.Fprintf(stderr, "printf: `%s': not a valid identifier\n", targetVar)
72 return interp.ExitStatus(2)
73 }
74 argIdx += 2
75 continue
76 }
77 break
78 }
79
80 if argIdx >= len(args) {
81 fmt.Fprint(stderr, "printf: usage: printf format [arguments]\n")
82 return interp.ExitStatus(1)
83 }
84
85 format := args[argIdx]
86 formatArgs := args[argIdx+1:]
87 processed := processEscapes(format)
88
89 tz := ""
90 if ec.Environ != nil {
91 if v := ec.Environ.Get("TZ"); v.IsSet() {
92 tz = v.String()
93 }
94 }
95
96 var output strings.Builder
97 var errMsg string
98 hadError := false
99 argPos := 0
100
101 for {
102 out, consumed, gotErr, gotMsg, stopped := formatOnce(processed, formatArgs, argPos, tz)
103 output.WriteString(out)
104 argPos += consumed
105 if gotErr {
106 hadError = true
107 if gotMsg != "" {
108 errMsg = gotMsg
109 }
110 }
111 if stopped {
112 break
113 }
114 if consumed == 0 || argPos >= len(formatArgs) {
115 break
116 }
117 }
118
119 if errMsg != "" {
120 fmt.Fprint(stderr, errMsg)
121 }
122
123 if hasTargetVar {
124 if err := assignVar(ec.Environ, targetVar, output.String()); err != nil {
125 fmt.Fprintf(stderr, "printf: %s\n", err)
126 return interp.ExitStatus(1)
127 }
128 } else {
129 io.WriteString(stdout, output.String())
130 }
131
132 if hadError {
133 return interp.ExitStatus(1)
134 }
135 return nil
136}
137
138func printHelp(w io.Writer) {
139 io.WriteString(w, "Usage: printf [-v var] FORMAT [ARGUMENT...]\n")
140 io.WriteString(w, "Format and print data.\n\n")
141 io.WriteString(w, " -v var assign the output to shell variable VAR rather than display it\n")
142 io.WriteString(w, " --help display this help and exit\n\n")
143 io.WriteString(w, "FORMAT controls the output like in C printf.\n")
144 io.WriteString(w, "Escape sequences: \\n (newline), \\t (tab), \\\\ (backslash)\n")
145 io.WriteString(w, "Format specifiers: %s (string), %d (integer), %f (float), %x (hex), %o (octal), %% (literal %)\n")
146 io.WriteString(w, "Width and precision: %10s (width 10), %.2f (2 decimal places), %010d (zero-padded)\n")
147 io.WriteString(w, "Flags: %- (left-justify), %+ (show sign), %0 (zero-pad)\n")
148}
149
150var identifierRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*(\[[a-zA-Z0-9_@*"'$]+\])?$`)
151
152func validIdentifier(s string) bool {
153 return identifierRe.MatchString(s)
154}
155
156var dollarVarRe = regexp.MustCompile(`\$([a-zA-Z_][a-zA-Z0-9_]*)`)
157
158// parseArraySubscript matches name[key], name['key'], or name["key"]. Go's RE2
159// does not support the backreference the original TS regex used, so this is a
160// manual parse.
161func parseArraySubscript(s string) (name, key string, ok bool) {
162 open := strings.IndexByte(s, '[')
163 if open < 1 || !strings.HasSuffix(s, "]") {
164 return "", "", false
165 }
166 name = s[:open]
167 inner := s[open+1 : len(s)-1]
168 if len(inner) >= 2 {
169 first, last := inner[0], inner[len(inner)-1]
170 if (first == '\'' && last == '\'') || (first == '"' && last == '"') {
171 inner = inner[1 : len(inner)-1]
172 }
173 }
174 return name, inner, true
175}
176
177func assignVar(env expand.Environ, name, value string) error {
178 we, ok := env.(expand.WriteEnviron)
179 if !ok {
180 return errors.New("cannot assign: environment is read-only")
181 }
182 if arrayName, key, ok := parseArraySubscript(name); ok {
183 key = dollarVarRe.ReplaceAllStringFunc(key, func(s string) string {
184 varName := s[1:]
185 if v := env.Get(varName); v.IsSet() {
186 return v.String()
187 }
188 return ""
189 })
190 return we.Set(arrayName+"_"+key, expand.Variable{Set: true, Kind: expand.String, Str: value})
191 }
192 return we.Set(name, expand.Variable{Set: true, Kind: expand.String, Str: value})
193}
194
195var strftimeSpecRe = regexp.MustCompile(`^%(-?\d*)(?:\.(\d+))?\(([^)]*)\)T`)
196
197func formatOnce(format string, args []string, argPos int, tz string) (string, int, bool, string, bool) {
198 var result strings.Builder
199 consumed := 0
200 hadError := false
201 errMsg := ""
202
203 i := 0
204 for i < len(format) {
205 if format[i] != '%' || i+1 >= len(format) {
206 result.WriteByte(format[i])
207 i++
208 continue
209 }
210
211 specStart := i
212 i++ // skip %
213
214 if format[i] == '%' {
215 result.WriteByte('%')
216 i++
217 continue
218 }
219
220 if m := strftimeSpecRe.FindStringSubmatch(format[specStart:]); m != nil {
221 width := 0
222 if m[1] != "" {
223 width, _ = strconv.Atoi(m[1])
224 }
225 precision := -1
226 if m[2] != "" {
227 precision, _ = strconv.Atoi(m[2])
228 }
229 strftimeFmt := m[3]
230 fullMatch := m[0]
231
232 arg := ""
233 if argPos+consumed < len(args) {
234 arg = args[argPos+consumed]
235 }
236 consumed++
237
238 var ts time.Time
239 if arg == "" || arg == "-1" || arg == "-2" {
240 ts = time.Now()
241 } else if n, err := strconv.ParseInt(arg, 10, 64); err == nil {
242 ts = time.Unix(n, 0)
243 } else {
244 ts = time.Unix(0, 0)
245 }
246
247 formatted := formatStrftime(strftimeFmt, ts, tz)
248 if precision >= 0 && len(formatted) > precision {
249 formatted = formatted[:precision]
250 }
251 if width != 0 {
252 abs := width
253 if abs < 0 {
254 abs = -abs
255 }
256 if len(formatted) < abs {
257 if width < 0 {
258 formatted = padRight(formatted, abs, ' ')
259 } else {
260 formatted = padLeft(formatted, abs, ' ')
261 }
262 }
263 }
264 result.WriteString(formatted)
265 i = specStart + len(fullMatch)
266 continue
267 }
268
269 // Flags
270 for i < len(format) && strings.ContainsRune("+-0 #'", rune(format[i])) {
271 i++
272 }
273
274 // Width (* or digits)
275 widthFromArg := false
276 if i < len(format) && format[i] == '*' {
277 widthFromArg = true
278 i++
279 } else {
280 for i < len(format) && format[i] >= '0' && format[i] <= '9' {
281 i++
282 }
283 }
284
285 // Precision (. then * or digits)
286 precisionFromArg := false
287 if i < len(format) && format[i] == '.' {
288 i++
289 if i < len(format) && format[i] == '*' {
290 precisionFromArg = true
291 i++
292 } else {
293 for i < len(format) && format[i] >= '0' && format[i] <= '9' {
294 i++
295 }
296 }
297 }
298
299 // Length modifier
300 if i < len(format) && strings.ContainsRune("hlL", rune(format[i])) {
301 i++
302 }
303
304 if i >= len(format) {
305 result.WriteString(format[specStart:])
306 break
307 }
308
309 specifier := format[i]
310 i++
311
312 fullSpec := format[specStart:i]
313 adjustedSpec := fullSpec
314
315 if widthFromArg {
316 w := 0
317 if argPos+consumed < len(args) {
318 w, _ = strconv.Atoi(args[argPos+consumed])
319 }
320 consumed++
321 adjustedSpec = strings.Replace(adjustedSpec, "*", strconv.Itoa(w), 1)
322 }
323 if precisionFromArg {
324 p := 0
325 if argPos+consumed < len(args) {
326 p, _ = strconv.Atoi(args[argPos+consumed])
327 }
328 consumed++
329 adjustedSpec = strings.Replace(adjustedSpec, ".*", "."+strconv.Itoa(p), 1)
330 }
331
332 arg := ""
333 if argPos+consumed < len(args) {
334 arg = args[argPos+consumed]
335 }
336 consumed++
337
338 val, gotErr, gotMsg, stopped := formatValue(adjustedSpec, specifier, arg)
339 result.WriteString(val)
340 if gotErr {
341 hadError = true
342 if gotMsg != "" {
343 errMsg = gotMsg
344 }
345 }
346 if stopped {
347 return result.String(), consumed, hadError, errMsg, true
348 }
349 }
350
351 return result.String(), consumed, hadError, errMsg, false
352}
353
354var (
355 intSpecRe = regexp.MustCompile(`^%([- +#0']*)(\d*)(\.(\d*))?[diu]$`)
356 octalSpecRe = regexp.MustCompile(`^%([- +#0']*)(\d*)(\.(\d*))?o$`)
357 hexSpecRe = regexp.MustCompile(`^%([- +#0']*)(\d*)(\.(\d*))?[xX]$`)
358 floatSpecRe = regexp.MustCompile(`^%([- +#0']*)(\d*)(\.(\d*))?[eEfFgG]$`)
359 stringSpecRe = regexp.MustCompile(`^%(-?)(\d*)(\.(\d*))?s$`)
360 quotedSpecRe = regexp.MustCompile(`^%(-?)(\d*)q$`)
361)
362
363func formatValue(spec string, specifier byte, arg string) (string, bool, string, bool) {
364 switch specifier {
365 case 'd', 'i':
366 num, parseErr := parseIntArg(arg)
367 var msg string
368 if parseErr {
369 msg = fmt.Sprintf("printf: %s: invalid number\n", arg)
370 }
371 return formatInteger(spec, num), parseErr, msg, false
372 case 'o':
373 num, parseErr := parseIntArg(arg)
374 var msg string
375 if parseErr {
376 msg = fmt.Sprintf("printf: %s: invalid number\n", arg)
377 }
378 return formatOctal(spec, num), parseErr, msg, false
379 case 'u':
380 num, parseErr := parseIntArg(arg)
381 var msg string
382 if parseErr {
383 msg = fmt.Sprintf("printf: %s: invalid number\n", arg)
384 }
385 unsigned := num
386 if num < 0 {
387 unsigned = int64(uint32(num))
388 }
389 return formatInteger(strings.Replace(spec, "u", "d", 1), unsigned), parseErr, msg, false
390 case 'x', 'X':
391 num, parseErr := parseIntArg(arg)
392 var msg string
393 if parseErr {
394 msg = fmt.Sprintf("printf: %s: invalid number\n", arg)
395 }
396 return formatHex(spec, num), parseErr, msg, false
397 case 'e', 'E', 'f', 'F', 'g', 'G':
398 num, _ := strconv.ParseFloat(arg, 64)
399 return formatFloat(spec, specifier, num), false, "", false
400 case 'c':
401 if arg == "" {
402 return "", false, "", false
403 }
404 return string(rune(arg[0])), false, "", false
405 case 's':
406 return formatString(spec, arg), false, "", false
407 case 'q':
408 return formatQuoted(spec, arg), false, "", false
409 case 'b':
410 val, stopped := processBEscapes(arg)
411 return val, false, "", stopped
412 default:
413 return "", true, fmt.Sprintf("printf: %%%c: invalid directive\n", specifier), false
414 }
415}
416
417func parseIntArg(arg string) (int64, bool) {
418 trimmed := strings.TrimLeft(arg, " \t\n\r\v\f")
419 hasTrailing := trimmed != strings.TrimRight(trimmed, " \t\n\r\v\f")
420 arg = strings.TrimRight(trimmed, " \t\n\r\v\f")
421
422 // Character notation: 'x' or "x" or \'x or \"x
423 if strings.HasPrefix(arg, "\\'") && len(arg) >= 3 {
424 r, _ := utf8.DecodeRuneInString(arg[2:])
425 return int64(r), false
426 }
427 if strings.HasPrefix(arg, "\\\"") && len(arg) >= 3 {
428 r, _ := utf8.DecodeRuneInString(arg[2:])
429 return int64(r), false
430 }
431 if strings.HasPrefix(arg, "'") && len(arg) >= 2 {
432 r, _ := utf8.DecodeRuneInString(arg[1:])
433 return int64(r), false
434 }
435 if strings.HasPrefix(arg, "\"") && len(arg) >= 2 {
436 r, _ := utf8.DecodeRuneInString(arg[1:])
437 return int64(r), false
438 }
439
440 if arg == "" {
441 return 0, false
442 }
443
444 arg = strings.TrimPrefix(arg, "+")
445
446 if strings.HasPrefix(arg, "0x") || strings.HasPrefix(arg, "0X") {
447 n, err := strconv.ParseInt(arg[2:], 16, 64)
448 if err != nil {
449 return 0, true
450 }
451 return n, hasTrailing
452 }
453 if strings.HasPrefix(arg, "-0x") || strings.HasPrefix(arg, "-0X") {
454 n, err := strconv.ParseInt(arg[3:], 16, 64)
455 if err != nil {
456 return 0, true
457 }
458 return -n, hasTrailing
459 }
460
461 // Octal: leading 0 followed by octal digits
462 if octalNumRe.MatchString(arg) {
463 n, _ := strconv.ParseInt(arg, 8, 64)
464 return n, hasTrailing
465 }
466
467 // Reject base notation like 64#a
468 if m := baseNotationRe.FindStringSubmatch(arg); m != nil {
469 n, _ := strconv.ParseInt(m[1], 10, 64)
470 return n, true
471 }
472
473 if !decimalRe.MatchString(arg) {
474 // Try to parse what we can (bash behavior: 3abc -> 3)
475 n, err := strconv.ParseInt(arg, 10, 64)
476 if err != nil {
477 n = bestEffortInt(arg)
478 }
479 return n, true
480 }
481
482 n, _ := strconv.ParseInt(arg, 10, 64)
483 return n, hasTrailing
484}
485
486var (
487 octalNumRe = regexp.MustCompile(`^-?0[0-7]+$`)
488 baseNotationRe = regexp.MustCompile(`^(\d+)#`)
489 decimalRe = regexp.MustCompile(`^-?\d+$`)
490 leadingIntRe = regexp.MustCompile(`^-?\d+`)
491)
492
493func bestEffortInt(s string) int64 {
494 m := leadingIntRe.FindString(s)
495 if m == "" {
496 return 0
497 }
498 n, _ := strconv.ParseInt(m, 10, 64)
499 return n
500}
501
502func formatInteger(spec string, num int64) string {
503 m := intSpecRe.FindStringSubmatch(spec)
504 if m == nil {
505 return fmt.Sprintf("%d", num)
506 }
507 flags := m[1]
508 width := 0
509 if m[2] != "" {
510 width, _ = strconv.Atoi(m[2])
511 }
512 precision := -1
513 if m[3] != "" {
514 if m[4] == "" {
515 precision = 0
516 } else {
517 precision, _ = strconv.Atoi(m[4])
518 }
519 }
520
521 negative := num < 0
522 abs := num
523 if negative {
524 abs = -num
525 }
526 numStr := strconv.FormatInt(abs, 10)
527
528 if precision >= 0 {
529 numStr = padLeft(numStr, precision, '0')
530 }
531
532 sign := ""
533 switch {
534 case negative:
535 sign = "-"
536 case strings.Contains(flags, "+"):
537 sign = "+"
538 case strings.Contains(flags, " "):
539 sign = " "
540 }
541
542 result := sign + numStr
543 if width > len(result) {
544 switch {
545 case strings.Contains(flags, "-"):
546 result = padRight(result, width, ' ')
547 case strings.Contains(flags, "0") && precision < 0:
548 result = sign + padLeft(numStr, width-len(sign), '0')
549 default:
550 result = padLeft(result, width, ' ')
551 }
552 }
553 return result
554}
555
556func formatOctal(spec string, num int64) string {
557 m := octalSpecRe.FindStringSubmatch(spec)
558 if m == nil {
559 return strconv.FormatInt(num, 8)
560 }
561 flags := m[1]
562 width := 0
563 if m[2] != "" {
564 width, _ = strconv.Atoi(m[2])
565 }
566 precision := -1
567 if m[3] != "" {
568 if m[4] == "" {
569 precision = 0
570 } else {
571 precision, _ = strconv.Atoi(m[4])
572 }
573 }
574
575 abs := num
576 if abs < 0 {
577 abs = -abs
578 }
579 numStr := strconv.FormatInt(abs, 8)
580
581 if precision >= 0 {
582 numStr = padLeft(numStr, precision, '0')
583 }
584 if strings.Contains(flags, "#") && !strings.HasPrefix(numStr, "0") {
585 numStr = "0" + numStr
586 }
587
588 result := numStr
589 if width > len(result) {
590 switch {
591 case strings.Contains(flags, "-"):
592 result = padRight(result, width, ' ')
593 case strings.Contains(flags, "0") && precision < 0:
594 result = padLeft(result, width, '0')
595 default:
596 result = padLeft(result, width, ' ')
597 }
598 }
599 return result
600}
601
602func formatHex(spec string, num int64) string {
603 m := hexSpecRe.FindStringSubmatch(spec)
604 upper := strings.Contains(spec, "X")
605 if m == nil {
606 if upper {
607 return strings.ToUpper(strconv.FormatInt(num, 16))
608 }
609 return strconv.FormatInt(num, 16)
610 }
611 flags := m[1]
612 width := 0
613 if m[2] != "" {
614 width, _ = strconv.Atoi(m[2])
615 }
616 precision := -1
617 if m[3] != "" {
618 if m[4] == "" {
619 precision = 0
620 } else {
621 precision, _ = strconv.Atoi(m[4])
622 }
623 }
624
625 abs := num
626 if abs < 0 {
627 abs = -abs
628 }
629 numStr := strconv.FormatInt(abs, 16)
630 if upper {
631 numStr = strings.ToUpper(numStr)
632 }
633 if precision >= 0 {
634 numStr = padLeft(numStr, precision, '0')
635 }
636
637 prefix := ""
638 if strings.Contains(flags, "#") && num != 0 {
639 if upper {
640 prefix = "0X"
641 } else {
642 prefix = "0x"
643 }
644 }
645
646 result := prefix + numStr
647 if width > len(result) {
648 switch {
649 case strings.Contains(flags, "-"):
650 result = padRight(result, width, ' ')
651 case strings.Contains(flags, "0") && precision < 0:
652 result = prefix + padLeft(numStr, width-len(prefix), '0')
653 default:
654 result = padLeft(result, width, ' ')
655 }
656 }
657 return result
658}
659
660func formatFloat(spec string, specifier byte, num float64) string {
661 m := floatSpecRe.FindStringSubmatch(spec)
662 if m == nil {
663 return strconv.FormatFloat(num, byte(specifier), 6, 64)
664 }
665 flags := m[1]
666 width := 0
667 if m[2] != "" {
668 width, _ = strconv.Atoi(m[2])
669 }
670 precision := 6
671 if m[3] != "" {
672 if m[4] == "" {
673 precision = 0
674 } else {
675 precision, _ = strconv.Atoi(m[4])
676 }
677 }
678
679 var result string
680 lower := specifier
681 if lower >= 'A' && lower <= 'Z' {
682 lower += 32
683 }
684
685 switch lower {
686 case 'e':
687 result = strconv.FormatFloat(num, 'e', precision, 64)
688 result = ensureExponentTwoDigits(result)
689 if specifier == 'E' {
690 result = strings.ToUpper(result)
691 }
692 case 'f':
693 result = strconv.FormatFloat(num, 'f', precision, 64)
694 if strings.Contains(flags, "#") && precision == 0 && !strings.Contains(result, ".") {
695 result += "."
696 }
697 case 'g':
698 p := precision
699 if p == 0 {
700 p = 1
701 }
702 result = strconv.FormatFloat(num, 'g', p, 64)
703 if !strings.Contains(flags, "#") {
704 // Go's %g already trims trailing zeros, no-op
705 }
706 result = ensureExponentTwoDigits(result)
707 if specifier == 'G' {
708 result = strings.ToUpper(result)
709 }
710 default:
711 result = strconv.FormatFloat(num, 'g', -1, 64)
712 }
713
714 if num >= 0 && !math.IsNaN(num) {
715 switch {
716 case strings.Contains(flags, "+"):
717 result = "+" + result
718 case strings.Contains(flags, " "):
719 result = " " + result
720 }
721 }
722
723 if width > len(result) {
724 switch {
725 case strings.Contains(flags, "-"):
726 result = padRight(result, width, ' ')
727 case strings.Contains(flags, "0"):
728 signPrefix := ""
729 rest := result
730 if len(result) > 0 && (result[0] == '+' || result[0] == '-' || result[0] == ' ') {
731 signPrefix = result[:1]
732 rest = result[1:]
733 }
734 result = signPrefix + padLeft(rest, width-len(signPrefix), '0')
735 default:
736 result = padLeft(result, width, ' ')
737 }
738 }
739 return result
740}
741
742var expDigitRe = regexp.MustCompile(`e([+-])(\d)$`)
743
744func ensureExponentTwoDigits(s string) string {
745 return expDigitRe.ReplaceAllString(s, "e${1}0${2}")
746}
747
748func formatString(spec string, str string) string {
749 m := stringSpecRe.FindStringSubmatch(spec)
750 if m == nil {
751 return str
752 }
753 leftJustify := m[1] == "-"
754 width := 0
755 if m[2] != "" {
756 width, _ = strconv.Atoi(m[2])
757 }
758 precision := -1
759 if m[3] != "" {
760 if m[4] == "" {
761 precision = 0
762 } else {
763 precision, _ = strconv.Atoi(m[4])
764 }
765 }
766
767 if precision >= 0 && len(str) > precision {
768 str = str[:precision]
769 }
770 if width > len(str) {
771 if leftJustify {
772 str = padRight(str, width, ' ')
773 } else {
774 str = padLeft(str, width, ' ')
775 }
776 }
777 return str
778}
779
780func formatQuoted(spec string, str string) string {
781 quoted := shellQuote(str)
782 m := quotedSpecRe.FindStringSubmatch(spec)
783 if m == nil {
784 return quoted
785 }
786 leftJustify := m[1] == "-"
787 width := 0
788 if m[2] != "" {
789 width, _ = strconv.Atoi(m[2])
790 }
791 if width > len(quoted) {
792 if leftJustify {
793 return padRight(quoted, width, ' ')
794 }
795 return padLeft(quoted, width, ' ')
796 }
797 return quoted
798}
799
800var safeQuoteRe = regexp.MustCompile(`^[a-zA-Z0-9_./-]+$`)
801
802func shellQuote(s string) string {
803 if s == "" {
804 return "''"
805 }
806 if safeQuoteRe.MatchString(s) {
807 return s
808 }
809
810 needsDollar := false
811 for _, c := range s {
812 if c < 0x20 || (c >= 0x7f && c <= 0xff) {
813 needsDollar = true
814 break
815 }
816 }
817
818 var b strings.Builder
819 if needsDollar {
820 b.WriteString("$'")
821 for _, c := range s {
822 switch {
823 case c == '\'':
824 b.WriteString("\\'")
825 case c == '\\':
826 b.WriteString("\\\\")
827 case c == '\n':
828 b.WriteString("\\n")
829 case c == '\t':
830 b.WriteString("\\t")
831 case c == '\r':
832 b.WriteString("\\r")
833 case c == 0x07:
834 b.WriteString("\\a")
835 case c == '\b':
836 b.WriteString("\\b")
837 case c == '\f':
838 b.WriteString("\\f")
839 case c == '\v':
840 b.WriteString("\\v")
841 case c == 0x1b:
842 b.WriteString("\\E")
843 case c < 0x20 || (c >= 0x7f && c <= 0xff):
844 fmt.Fprintf(&b, "\\%03o", c)
845 case c == '"':
846 b.WriteString("\\\"")
847 default:
848 b.WriteRune(c)
849 }
850 }
851 b.WriteByte('\'')
852 return b.String()
853 }
854
855 specials := " \t|&;<>()$`\\\"'*?[#~=%!{}"
856 for _, c := range s {
857 if strings.ContainsRune(specials, c) {
858 b.WriteByte('\\')
859 }
860 b.WriteRune(c)
861 }
862 return b.String()
863}
864
865// processEscapes interprets backslash escapes in a printf format string.
866// Supports \n \t \r \\ \a \b \f \v \e \E \NNN \xHH \uHHHH \UHHHHHHHH.
867func processEscapes(s string) string {
868 var b strings.Builder
869 i := 0
870 for i < len(s) {
871 if s[i] != '\\' || i+1 >= len(s) {
872 b.WriteByte(s[i])
873 i++
874 continue
875 }
876 next := s[i+1]
877 switch next {
878 case 'n':
879 b.WriteByte('\n')
880 i += 2
881 case 't':
882 b.WriteByte('\t')
883 i += 2
884 case 'r':
885 b.WriteByte('\r')
886 i += 2
887 case '\\':
888 b.WriteByte('\\')
889 i += 2
890 case 'a':
891 b.WriteByte(0x07)
892 i += 2
893 case 'b':
894 b.WriteByte('\b')
895 i += 2
896 case 'f':
897 b.WriteByte('\f')
898 i += 2
899 case 'v':
900 b.WriteByte('\v')
901 i += 2
902 case 'e', 'E':
903 b.WriteByte(0x1b)
904 i += 2
905 case '0', '1', '2', '3', '4', '5', '6', '7':
906 oct, end := readOctalEscape(s, i+1, 3)
907 b.WriteByte(byte(oct))
908 i = end
909 case 'x':
910 bytes := []byte{}
911 j := i
912 for j+1 < len(s) && s[j] == '\\' && s[j+1] == 'x' {
913 hex := ""
914 k := j + 2
915 for k < len(s) && k < j+4 && isHex(s[k]) {
916 hex += string(s[k])
917 k++
918 }
919 if hex == "" {
920 break
921 }
922 v, _ := strconv.ParseInt(hex, 16, 32)
923 bytes = append(bytes, byte(v))
924 j = k
925 }
926 if len(bytes) > 0 {
927 if utf8.Valid(bytes) {
928 b.Write(bytes)
929 } else {
930 for _, c := range bytes {
931 b.WriteRune(rune(c))
932 }
933 }
934 i = j
935 } else {
936 b.WriteByte(s[i])
937 i++
938 }
939 case 'u':
940 hex, end := readHexEscape(s, i+2, 4)
941 if hex != "" {
942 v, _ := strconv.ParseInt(hex, 16, 32)
943 b.WriteRune(rune(v))
944 i = end
945 } else {
946 b.WriteString("\\u")
947 i += 2
948 }
949 case 'U':
950 hex, end := readHexEscape(s, i+2, 8)
951 if hex != "" {
952 v, _ := strconv.ParseInt(hex, 16, 32)
953 b.WriteRune(rune(v))
954 i = end
955 } else {
956 b.WriteString("\\U")
957 i += 2
958 }
959 default:
960 b.WriteByte(s[i])
961 i++
962 }
963 }
964 return b.String()
965}
966
967func readOctalEscape(s string, start, max int) (int, int) {
968 var b strings.Builder
969 j := start
970 for j < len(s) && j < start+max && s[j] >= '0' && s[j] <= '7' {
971 b.WriteByte(s[j])
972 j++
973 }
974 if b.Len() == 0 {
975 return 0, start
976 }
977 v, _ := strconv.ParseInt(b.String(), 8, 32)
978 return int(v), j
979}
980
981func readHexEscape(s string, start, max int) (string, int) {
982 var b strings.Builder
983 j := start
984 for j < len(s) && j < start+max && isHex(s[j]) {
985 b.WriteByte(s[j])
986 j++
987 }
988 return b.String(), j
989}
990
991func isHex(c byte) bool {
992 return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
993}
994
995// processBEscapes interprets escapes in a %b argument. Returns (value, stopped)
996// where stopped=true means \c was encountered and output should halt.
997func processBEscapes(s string) (string, bool) {
998 var b strings.Builder
999 i := 0
1000 for i < len(s) {
1001 if s[i] != '\\' || i+1 >= len(s) {
1002 b.WriteByte(s[i])
1003 i++
1004 continue
1005 }
1006 next := s[i+1]
1007 switch next {
1008 case 'n':
1009 b.WriteByte('\n')
1010 i += 2
1011 case 't':
1012 b.WriteByte('\t')
1013 i += 2
1014 case 'r':
1015 b.WriteByte('\r')
1016 i += 2
1017 case '\\':
1018 b.WriteByte('\\')
1019 i += 2
1020 case 'a':
1021 b.WriteByte(0x07)
1022 i += 2
1023 case 'b':
1024 b.WriteByte('\b')
1025 i += 2
1026 case 'f':
1027 b.WriteByte('\f')
1028 i += 2
1029 case 'v':
1030 b.WriteByte('\v')
1031 i += 2
1032 case 'c':
1033 return b.String(), true
1034 case 'x':
1035 bytes := []byte{}
1036 j := i
1037 for j+1 < len(s) && s[j] == '\\' && s[j+1] == 'x' {
1038 hex, k := readHexEscape(s, j+2, 2)
1039 if hex == "" {
1040 break
1041 }
1042 v, _ := strconv.ParseInt(hex, 16, 32)
1043 bytes = append(bytes, byte(v))
1044 j = k
1045 }
1046 if len(bytes) > 0 {
1047 if utf8.Valid(bytes) {
1048 b.Write(bytes)
1049 } else {
1050 for _, c := range bytes {
1051 b.WriteRune(rune(c))
1052 }
1053 }
1054 i = j
1055 } else {
1056 b.WriteString("\\x")
1057 i += 2
1058 }
1059 case 'u':
1060 hex, end := readHexEscape(s, i+2, 4)
1061 if hex != "" {
1062 v, _ := strconv.ParseInt(hex, 16, 32)
1063 b.WriteRune(rune(v))
1064 i = end
1065 } else {
1066 b.WriteString("\\u")
1067 i += 2
1068 }
1069 case '0':
1070 oct, end := readOctalEscape(s, i+2, 3)
1071 if end > i+2 {
1072 b.WriteByte(byte(oct))
1073 i = end
1074 } else {
1075 b.WriteByte(0)
1076 i += 2
1077 }
1078 case '1', '2', '3', '4', '5', '6', '7':
1079 oct, end := readOctalEscape(s, i+1, 3)
1080 b.WriteByte(byte(oct))
1081 i = end
1082 default:
1083 b.WriteByte(s[i])
1084 i++
1085 }
1086 }
1087 return b.String(), false
1088}
1089
1090func padLeft(s string, width int, pad byte) string {
1091 if len(s) >= width {
1092 return s
1093 }
1094 return strings.Repeat(string(pad), width-len(s)) + s
1095}
1096
1097func padRight(s string, width int, pad byte) string {
1098 if len(s) >= width {
1099 return s
1100 }
1101 return s + strings.Repeat(string(pad), width-len(s))
1102}
1103
1104// formatStrftime formats a Unix timestamp using a strftime-style format string.
1105// Mirrors the date command's UTC-only semantics; tz is accepted but ignored.
1106func formatStrftime(format string, t time.Time, tz string) string {
1107 t = t.UTC()
1108 if tz != "" {
1109 if loc, err := time.LoadLocation(tz); err == nil {
1110 t = t.In(loc)
1111 }
1112 }
1113
1114 var b strings.Builder
1115 for i := 0; i < len(format); i++ {
1116 if format[i] != '%' || i+1 >= len(format) {
1117 b.WriteByte(format[i])
1118 continue
1119 }
1120 i++
1121 switch format[i] {
1122 case 'a':
1123 b.WriteString(t.Weekday().String()[:3])
1124 case 'A':
1125 b.WriteString(t.Weekday().String())
1126 case 'b', 'h':
1127 b.WriteString(t.Month().String()[:3])
1128 case 'B':
1129 b.WriteString(t.Month().String())
1130 case 'c':
1131 fmt.Fprintf(&b, "%s %s %2d %02d:%02d:%02d %d",
1132 t.Weekday().String()[:3], t.Month().String()[:3],
1133 t.Day(), t.Hour(), t.Minute(), t.Second(), t.Year())
1134 case 'C':
1135 fmt.Fprintf(&b, "%02d", t.Year()/100)
1136 case 'd':
1137 fmt.Fprintf(&b, "%02d", t.Day())
1138 case 'D':
1139 fmt.Fprintf(&b, "%02d/%02d/%02d", int(t.Month()), t.Day(), t.Year()%100)
1140 case 'e':
1141 fmt.Fprintf(&b, "%2d", t.Day())
1142 case 'F':
1143 fmt.Fprintf(&b, "%d-%02d-%02d", t.Year(), int(t.Month()), t.Day())
1144 case 'H':
1145 fmt.Fprintf(&b, "%02d", t.Hour())
1146 case 'I':
1147 h := t.Hour() % 12
1148 if h == 0 {
1149 h = 12
1150 }
1151 fmt.Fprintf(&b, "%02d", h)
1152 case 'j':
1153 fmt.Fprintf(&b, "%03d", t.YearDay())
1154 case 'k':
1155 fmt.Fprintf(&b, "%2d", t.Hour())
1156 case 'l':
1157 h := t.Hour() % 12
1158 if h == 0 {
1159 h = 12
1160 }
1161 fmt.Fprintf(&b, "%2d", h)
1162 case 'm':
1163 fmt.Fprintf(&b, "%02d", int(t.Month()))
1164 case 'M':
1165 fmt.Fprintf(&b, "%02d", t.Minute())
1166 case 'n':
1167 b.WriteByte('\n')
1168 case 'N':
1169 b.WriteString("000000000")
1170 case 'p':
1171 if t.Hour() < 12 {
1172 b.WriteString("AM")
1173 } else {
1174 b.WriteString("PM")
1175 }
1176 case 'P':
1177 if t.Hour() < 12 {
1178 b.WriteString("am")
1179 } else {
1180 b.WriteString("pm")
1181 }
1182 case 'r':
1183 h := t.Hour() % 12
1184 if h == 0 {
1185 h = 12
1186 }
1187 ap := "AM"
1188 if t.Hour() >= 12 {
1189 ap = "PM"
1190 }
1191 fmt.Fprintf(&b, "%02d:%02d:%02d %s", h, t.Minute(), t.Second(), ap)
1192 case 'R':
1193 fmt.Fprintf(&b, "%02d:%02d", t.Hour(), t.Minute())
1194 case 's':
1195 fmt.Fprintf(&b, "%d", t.Unix())
1196 case 'S':
1197 fmt.Fprintf(&b, "%02d", t.Second())
1198 case 't':
1199 b.WriteByte('\t')
1200 case 'T', 'X':
1201 fmt.Fprintf(&b, "%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second())
1202 case 'u':
1203 w := int(t.Weekday())
1204 if w == 0 {
1205 w = 7
1206 }
1207 fmt.Fprintf(&b, "%d", w)
1208 case 'w':
1209 fmt.Fprintf(&b, "%d", int(t.Weekday()))
1210 case 'x':
1211 fmt.Fprintf(&b, "%02d/%02d/%02d", int(t.Month()), t.Day(), t.Year()%100)
1212 case 'y':
1213 fmt.Fprintf(&b, "%02d", t.Year()%100)
1214 case 'Y':
1215 fmt.Fprintf(&b, "%d", t.Year())
1216 case 'z':
1217 _, off := t.Zone()
1218 sign := "+"
1219 if off < 0 {
1220 sign = "-"
1221 off = -off
1222 }
1223 fmt.Fprintf(&b, "%s%02d%02d", sign, off/3600, (off%3600)/60)
1224 case 'Z':
1225 name, _ := t.Zone()
1226 b.WriteString(name)
1227 case '%':
1228 b.WriteByte('%')
1229 default:
1230 b.WriteByte('%')
1231 b.WriteByte(format[i])
1232 }
1233 }
1234 return b.String()
1235}