A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 1235 lines 27 kB view raw
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}