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.

feat(od): expand -t grammar and add -v/-j/-N/XSI shorthands

Implements GNU coreutils compatibility for od:

- Full -t grammar: a, c, d, o, u, x, f with optional size
suffix (digits or C/S/I/L), comma-separated lists, and
multiple -t invocations
- -v (verbose, no * compression)
- -j skip and -N count (octal, hex, b/k/m suffixes)
- XSI shorthands: -b, -c, -d, -o, -s, -x
- Repeat-line collapse with single * unless -v
- -A radix for d/o/x/n addresses with -w width LCM rounding

Refs: docs/posix2018/CONFORMANCE.md
Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 11c6c20c 4c2d5a76

+925 -89
+652 -70
command/internal/od/od.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/binary" 5 6 "errors" 6 7 "fmt" 7 8 "io" 9 + "math" 8 10 "path" 9 - "slices" 11 + "strconv" 10 12 "strings" 11 13 12 14 "github.com/pborman/getopt/v2" ··· 16 18 17 19 type Impl struct{} 18 20 19 - type outputFormat int 21 + type formatKind int 20 22 21 23 const ( 22 - fmtOctal outputFormat = iota 24 + fmtNamedChar formatKind = iota 25 + fmtChar 26 + fmtSignedDec 27 + fmtUnsignedDec 28 + fmtOctal 23 29 fmtHex 24 - fmtChar 30 + fmtFloat 25 31 ) 32 + 33 + type formatSpec struct { 34 + kind formatKind 35 + size int 36 + width int 37 + raw string 38 + } 26 39 27 40 func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 28 41 if ec == nil { ··· 40 53 41 54 set := getopt.New() 42 55 set.SetProgram("od") 43 - set.SetParameters("[FILE]") 56 + set.SetParameters("[FILE]...") 44 57 45 58 usage := func() { 46 - fmt.Fprint(stderr, "Usage: od [OPTION]... [FILE]\n") 59 + fmt.Fprint(stderr, "Usage: od [OPTION]... [FILE]...\n") 47 60 fmt.Fprint(stderr, "Write an unambiguous representation, octal bytes by default,\n") 48 61 fmt.Fprint(stderr, "of FILE to standard output.\n\n") 49 - fmt.Fprint(stderr, " -A, --address-radix=RADIX output format for addresses; n means no address\n") 50 - fmt.Fprint(stderr, " -c same as -t c\n") 51 - fmt.Fprint(stderr, " -t, --format=TYPE select output format; one of x1, c, o*\n") 52 - fmt.Fprint(stderr, " --help display this help and exit\n") 62 + fmt.Fprint(stderr, " -A, --address-radix=RADIX output format for file offsets; RADIX is one\n") 63 + fmt.Fprint(stderr, " of [doxn], for Decimal, Octal, Hex or None\n") 64 + fmt.Fprint(stderr, " -j, --skip-bytes=BYTES skip BYTES input bytes first\n") 65 + fmt.Fprint(stderr, " -N, --read-bytes=BYTES limit dump to BYTES input bytes\n") 66 + fmt.Fprint(stderr, " -t, --format=TYPE select output format or formats\n") 67 + fmt.Fprint(stderr, " -v, --output-duplicates do not use * to mark line suppression\n") 68 + fmt.Fprint(stderr, " -w, --width[=BYTES] output BYTES bytes per line (default 16)\n") 69 + fmt.Fprint(stderr, " -b same as -t o1, select octal bytes\n") 70 + fmt.Fprint(stderr, " -c same as -t c, select printable chars\n") 71 + fmt.Fprint(stderr, " -d same as -t u2, select unsigned decimal 2-byte units\n") 72 + fmt.Fprint(stderr, " -o same as -t o2, select octal 2-byte units\n") 73 + fmt.Fprint(stderr, " -s same as -t d2, select signed decimal 2-byte units\n") 74 + fmt.Fprint(stderr, " -x same as -t x2, select hexadecimal 2-byte units\n") 75 + fmt.Fprint(stderr, " --help display this help and exit\n") 53 76 } 54 77 set.SetUsage(usage) 55 78 56 - addrOpt := set.StringLong("address-radix", 'A', "o", "output format for addresses; n means no address") 79 + addrOpt := set.StringLong("address-radix", 'A', "o", "output format for file offsets") 80 + skipOpt := set.StringLong("skip-bytes", 'j', "", "skip BYTES input bytes first") 81 + readOpt := set.StringLong("read-bytes", 'N', "", "limit dump to BYTES input bytes") 82 + verbose := set.BoolLong("output-duplicates", 'v', "do not use * to mark line suppression") 83 + widthOpt := set.StringLong("width", 'w', "16", "output BYTES bytes per line") 84 + _ = set.Bool('b', "same as -t o1") 57 85 _ = set.Bool('c', "same as -t c") 58 - tList := set.ListLong("format", 't', "select output format; one of x1, c, o*") 86 + _ = set.Bool('d', "same as -t u2") 87 + _ = set.Bool('o', "same as -t o2") 88 + _ = set.Bool('s', "same as -t d2") 89 + _ = set.Bool('x', "same as -t x2") 90 + tList := set.ListLong("format", 't', "select output format or formats") 59 91 helpFlag := set.BoolLong("help", 0, "display this help and exit") 60 92 61 - var formats []outputFormat 93 + var formats []formatSpec 62 94 tIdx := 0 95 + parseErr := "" 63 96 callback := func(opt getopt.Option) bool { 64 97 switch opt.ShortName() { 98 + case "b": 99 + formats = append(formats, makeSpec(fmtOctal, 1, "o1")) 65 100 case "c": 66 - formats = append(formats, fmtChar) 101 + formats = append(formats, makeSpec(fmtChar, 1, "c")) 102 + case "d": 103 + formats = append(formats, makeSpec(fmtUnsignedDec, 2, "u2")) 104 + case "o": 105 + formats = append(formats, makeSpec(fmtOctal, 2, "o2")) 106 + case "s": 107 + formats = append(formats, makeSpec(fmtSignedDec, 2, "d2")) 108 + case "x": 109 + formats = append(formats, makeSpec(fmtHex, 2, "x2")) 67 110 case "t": 68 - if tIdx < len(*tList) { 111 + // ListLong splits on commas, so a single -t can produce 112 + // multiple list entries. Consume everything appended since 113 + // the last -t callback. 114 + for tIdx < len(*tList) { 69 115 v := (*tList)[tIdx] 70 116 tIdx++ 71 - switch { 72 - case v == "x1": 73 - formats = append(formats, fmtHex) 74 - case v == "c": 75 - formats = append(formats, fmtChar) 76 - case strings.HasPrefix(v, "o"): 77 - formats = append(formats, fmtOctal) 117 + specs, err := parseTypeString(v) 118 + if err != nil { 119 + parseErr = v 120 + return false 78 121 } 122 + formats = append(formats, specs...) 79 123 } 80 124 } 81 125 return true 82 126 } 83 127 84 128 if err := set.Getopt(append([]string{"od"}, args...), callback); err != nil { 85 - fmt.Fprintf(stderr, "od: %s\n", err) 86 - usage() 129 + if parseErr != "" { 130 + fmt.Fprintf(stderr, "od: invalid type string '%s'\n", parseErr) 131 + } else { 132 + fmt.Fprintf(stderr, "od: %s\n", err) 133 + usage() 134 + } 135 + return interp.ExitStatus(1) 136 + } 137 + 138 + if parseErr != "" { 139 + fmt.Fprintf(stderr, "od: invalid type string '%s'\n", parseErr) 87 140 return interp.ExitStatus(1) 88 141 } 89 142 ··· 92 145 return nil 93 146 } 94 147 95 - addressNone := *addrOpt == "n" 148 + addrRadix := *addrOpt 149 + switch addrRadix { 150 + case "d", "o", "x", "n": 151 + default: 152 + fmt.Fprintf(stderr, "od: invalid output address radix '%s'; it must be one character from [doxn]\n", addrRadix) 153 + return interp.ExitStatus(1) 154 + } 155 + 156 + skip := int64(0) 157 + if *skipOpt != "" { 158 + n, err := parseByteCount(*skipOpt) 159 + if err != nil { 160 + fmt.Fprintf(stderr, "od: invalid argument '%s' for '--skip-bytes'\n", *skipOpt) 161 + return interp.ExitStatus(1) 162 + } 163 + skip = n 164 + } 165 + 166 + limit := int64(-1) 167 + if *readOpt != "" { 168 + n, err := parseByteCount(*readOpt) 169 + if err != nil { 170 + fmt.Fprintf(stderr, "od: invalid argument '%s' for '--read-bytes'\n", *readOpt) 171 + return interp.ExitStatus(1) 172 + } 173 + limit = n 174 + } 96 175 97 176 if len(formats) == 0 { 98 - formats = []outputFormat{fmtOctal} 177 + formats = []formatSpec{makeSpec(fmtOctal, 2, "o2")} 178 + } 179 + 180 + bytesPerLine := 16 181 + if *widthOpt != "" { 182 + n, err := strconv.Atoi(*widthOpt) 183 + if err != nil || n <= 0 { 184 + fmt.Fprintf(stderr, "od: invalid width specification '%s'\n", *widthOpt) 185 + return interp.ExitStatus(1) 186 + } 187 + bytesPerLine = n 188 + } 189 + // Round bytesPerLine up to a multiple of the LCM of format sizes so 190 + // that no field straddles a line boundary. 191 + lcm := 1 192 + for _, f := range formats { 193 + lcm = lcmInt(lcm, f.size) 194 + } 195 + if bytesPerLine%lcm != 0 { 196 + bytesPerLine = ((bytesPerLine / lcm) + 1) * lcm 99 197 } 100 198 101 199 files := set.Args() ··· 104 202 return err 105 203 } 106 204 107 - io.WriteString(stdout, buildOutput(data, formats, addressNone)) 205 + if skip > 0 { 206 + if int64(len(data)) < skip { 207 + fmt.Fprint(stderr, "od: cannot skip past end of combined input\n") 208 + return interp.ExitStatus(1) 209 + } 210 + data = data[skip:] 211 + } 212 + if limit >= 0 && int64(len(data)) > limit { 213 + data = data[:limit] 214 + } 215 + 216 + io.WriteString(stdout, buildOutput(data, formats, addrRadix, skip, *verbose, bytesPerLine)) 108 217 return nil 109 218 } 110 219 220 + func lcmInt(a, b int) int { 221 + if a == 0 || b == 0 { 222 + return 0 223 + } 224 + g := gcdInt(a, b) 225 + return a / g * b 226 + } 227 + 228 + func gcdInt(a, b int) int { 229 + for b != 0 { 230 + a, b = b, a%b 231 + } 232 + if a < 0 { 233 + return -a 234 + } 235 + return a 236 + } 237 + 238 + func makeSpec(kind formatKind, size int, raw string) formatSpec { 239 + return formatSpec{kind: kind, size: size, width: itemWidth(kind, size), raw: raw} 240 + } 241 + 242 + func itemWidth(kind formatKind, size int) int { 243 + switch kind { 244 + case fmtNamedChar, fmtChar: 245 + return 4 246 + case fmtSignedDec: 247 + switch size { 248 + case 1: 249 + return 5 250 + case 2: 251 + return 7 252 + case 4: 253 + return 12 254 + case 8: 255 + return 21 256 + } 257 + case fmtUnsignedDec: 258 + switch size { 259 + case 1: 260 + return 4 261 + case 2: 262 + return 6 263 + case 4: 264 + return 11 265 + case 8: 266 + return 21 267 + } 268 + case fmtOctal: 269 + switch size { 270 + case 1: 271 + return 4 272 + case 2: 273 + return 7 274 + case 4: 275 + return 12 276 + case 8: 277 + return 23 278 + } 279 + case fmtHex: 280 + switch size { 281 + case 1: 282 + return 3 283 + case 2: 284 + return 5 285 + case 4: 286 + return 9 287 + case 8: 288 + return 17 289 + } 290 + case fmtFloat: 291 + switch size { 292 + case 4: 293 + return 16 294 + case 8: 295 + return 25 296 + } 297 + } 298 + return 4 299 + } 300 + 301 + func parseTypeString(s string) ([]formatSpec, error) { 302 + var out []formatSpec 303 + i := 0 304 + for i < len(s) { 305 + ch := s[i] 306 + i++ 307 + switch ch { 308 + case 'a': 309 + out = append(out, makeSpec(fmtNamedChar, 1, "a")) 310 + case 'c': 311 + out = append(out, makeSpec(fmtChar, 1, "c")) 312 + case 'd', 'o', 'u', 'x': 313 + size := 4 314 + raw := string(ch) 315 + if i < len(s) { 316 + if n, w, ok := parseIntSize(s[i:]); ok { 317 + size = n 318 + raw += s[i : i+w] 319 + i += w 320 + } else if l, ok := parseLetterSize(s[i], "CSIL"); ok { 321 + size = l 322 + raw += string(s[i]) 323 + i++ 324 + } 325 + } 326 + if !validIntSize(size) { 327 + return nil, fmt.Errorf("invalid size") 328 + } 329 + var kind formatKind 330 + switch ch { 331 + case 'd': 332 + kind = fmtSignedDec 333 + case 'o': 334 + kind = fmtOctal 335 + case 'u': 336 + kind = fmtUnsignedDec 337 + case 'x': 338 + kind = fmtHex 339 + } 340 + out = append(out, makeSpec(kind, size, raw)) 341 + case 'f': 342 + size := 8 343 + raw := "f" 344 + if i < len(s) { 345 + if n, w, ok := parseIntSize(s[i:]); ok { 346 + size = n 347 + raw += s[i : i+w] 348 + i += w 349 + } else { 350 + switch s[i] { 351 + case 'F': 352 + size = 4 353 + raw += "F" 354 + i++ 355 + case 'D': 356 + size = 8 357 + raw += "D" 358 + i++ 359 + case 'L': 360 + return nil, fmt.Errorf("long double unsupported") 361 + } 362 + } 363 + } 364 + if size != 4 && size != 8 { 365 + return nil, fmt.Errorf("invalid float size") 366 + } 367 + out = append(out, makeSpec(fmtFloat, size, raw)) 368 + case ',', ' ': 369 + continue 370 + default: 371 + return nil, fmt.Errorf("unknown type char %q", ch) 372 + } 373 + } 374 + if len(out) == 0 { 375 + return nil, fmt.Errorf("empty type string") 376 + } 377 + return out, nil 378 + } 379 + 380 + func parseIntSize(s string) (int, int, bool) { 381 + w := 0 382 + for w < len(s) && s[w] >= '0' && s[w] <= '9' { 383 + w++ 384 + } 385 + if w == 0 { 386 + return 0, 0, false 387 + } 388 + n, err := strconv.Atoi(s[:w]) 389 + if err != nil { 390 + return 0, 0, false 391 + } 392 + return n, w, true 393 + } 394 + 395 + func parseLetterSize(c byte, allowed string) (int, bool) { 396 + if !strings.ContainsRune(allowed, rune(c)) { 397 + return 0, false 398 + } 399 + switch c { 400 + case 'C': 401 + return 1, true 402 + case 'S': 403 + return 2, true 404 + case 'I': 405 + return 4, true 406 + case 'L': 407 + return 8, true 408 + } 409 + return 0, false 410 + } 411 + 412 + func validIntSize(n int) bool { 413 + return n == 1 || n == 2 || n == 4 || n == 8 414 + } 415 + 416 + func parseByteCount(s string) (int64, error) { 417 + if s == "" { 418 + return 0, fmt.Errorf("empty") 419 + } 420 + mult := int64(1) 421 + last := s[len(s)-1] 422 + hexPrefix := strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") 423 + switch last { 424 + case 'b': 425 + if !hexPrefix { 426 + mult = 512 427 + s = s[:len(s)-1] 428 + } 429 + case 'k': 430 + mult = 1024 431 + s = s[:len(s)-1] 432 + case 'm': 433 + mult = 1048576 434 + s = s[:len(s)-1] 435 + } 436 + base := 10 437 + if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { 438 + base = 16 439 + s = s[2:] 440 + } else if strings.HasPrefix(s, "0") && len(s) > 1 { 441 + base = 8 442 + s = s[1:] 443 + } 444 + n, err := strconv.ParseInt(s, base, 64) 445 + if err != nil { 446 + return 0, err 447 + } 448 + return n * mult, nil 449 + } 450 + 111 451 func readInput(ec *command.ExecContext, files []string, stderr io.Writer) ([]byte, error) { 112 452 if len(files) == 0 || files[0] == "-" { 113 453 if ec.Stdin == nil { ··· 129 469 return io.ReadAll(f) 130 470 } 131 471 132 - func buildOutput(data []byte, formats []outputFormat, addressNone bool) string { 133 - if len(data) == 0 { 472 + func buildOutput(data []byte, formats []formatSpec, addrRadix string, baseAddr int64, verbose bool, bytesPerLine int) string { 473 + if bytesPerLine <= 0 { 474 + bytesPerLine = 16 475 + } 476 + addressNone := addrRadix == "n" 477 + 478 + totalLen := int64(len(data)) 479 + if totalLen == 0 { 480 + if !addressNone { 481 + return formatAddress(baseAddr, addrRadix) + "\n" 482 + } 134 483 return "" 135 484 } 136 485 137 - hasCharFormat := slices.Contains(formats, fmtChar) 138 - 139 - const bytesPerLine = 16 140 486 var b strings.Builder 487 + var prevChunk []byte 488 + starShown := false 141 489 142 - for offset := 0; offset < len(data); offset += bytesPerLine { 143 - chunk := data[offset:min(offset+bytesPerLine, len(data))] 490 + bpl := int64(bytesPerLine) 491 + for offset := int64(0); offset < totalLen; offset += bpl { 492 + end := offset + bpl 493 + if end > totalLen { 494 + end = totalLen 495 + } 496 + chunk := data[offset:end] 497 + isLast := end == totalLen 144 498 145 - for fIdx, f := range formats { 146 - switch { 147 - case fIdx == 0 && !addressNone: 148 - fmt.Fprintf(&b, "%07o ", offset) 149 - case fIdx > 0 && !addressNone: 150 - b.WriteString(" ") 499 + if !verbose && !isLast && int64(len(chunk)) == bpl && prevChunk != nil && bytesEqual(prevChunk, chunk) { 500 + if !starShown { 501 + b.WriteString("*\n") 502 + starShown = true 151 503 } 152 - 153 - for _, code := range chunk { 154 - switch f { 155 - case fmtChar: 156 - b.WriteString(formatCharByte(code)) 157 - case fmtHex: 158 - b.WriteString(formatHexByte(code, hasCharFormat)) 159 - case fmtOctal: 160 - fmt.Fprintf(&b, " %03o", code) 161 - } 162 - } 163 - b.WriteByte('\n') 504 + prevChunk = chunk 505 + continue 164 506 } 507 + starShown = false 508 + prevChunk = chunk 509 + 510 + writeBlock(&b, chunk, formats, addrRadix, baseAddr+offset) 165 511 } 166 512 167 513 if !addressNone { 168 - fmt.Fprintf(&b, "%07o\n", len(data)) 514 + b.WriteString(formatAddress(baseAddr+totalLen, addrRadix)) 515 + b.WriteByte('\n') 169 516 } 170 517 171 518 return b.String() 172 519 } 173 520 174 - func formatCharByte(code byte) string { 175 - switch code { 521 + func writeBlock(b *strings.Builder, chunk []byte, formats []formatSpec, addrRadix string, addr int64) { 522 + addressNone := addrRadix == "n" 523 + 524 + maxByteWidth := 0 525 + for _, f := range formats { 526 + if f.size == 1 && f.width > maxByteWidth { 527 + maxByteWidth = f.width 528 + } 529 + } 530 + 531 + totalWidth := 0 532 + for _, f := range formats { 533 + w := lineWidthForFormat(f, len(chunk), maxByteWidth) 534 + if w > totalWidth { 535 + totalWidth = w 536 + } 537 + } 538 + 539 + for fIdx, f := range formats { 540 + switch { 541 + case fIdx == 0 && !addressNone: 542 + b.WriteString(formatAddress(addr, addrRadix)) 543 + case fIdx > 0 && !addressNone: 544 + b.WriteString(strings.Repeat(" ", addressWidth(addrRadix))) 545 + } 546 + 547 + text := renderFormat(f, chunk, maxByteWidth) 548 + if pad := totalWidth - visibleWidth(f, len(chunk), maxByteWidth); pad > 0 { 549 + text = strings.Repeat(" ", pad) + text 550 + } 551 + b.WriteString(text) 552 + b.WriteByte('\n') 553 + } 554 + } 555 + 556 + func lineWidthForFormat(f formatSpec, chunkLen, maxByteWidth int) int { 557 + return visibleWidth(f, chunkLen, maxByteWidth) 558 + } 559 + 560 + func visibleWidth(f formatSpec, chunkLen, maxByteWidth int) int { 561 + if f.size == 1 { 562 + w := f.width 563 + if maxByteWidth > w { 564 + w = maxByteWidth 565 + } 566 + return w * chunkLen 567 + } 568 + items := chunkLen / f.size 569 + rem := chunkLen % f.size 570 + total := items * f.width 571 + if rem > 0 { 572 + total += f.width 573 + } 574 + return total 575 + } 576 + 577 + func renderFormat(f formatSpec, chunk []byte, maxByteWidth int) string { 578 + var b strings.Builder 579 + switch f.kind { 580 + case fmtNamedChar: 581 + w := f.width 582 + if maxByteWidth > w { 583 + w = maxByteWidth 584 + } 585 + for _, c := range chunk { 586 + s := namedChar(c) 587 + fmt.Fprintf(&b, "%*s", w, s) 588 + } 589 + case fmtChar: 590 + w := f.width 591 + if maxByteWidth > w { 592 + w = maxByteWidth 593 + } 594 + for _, c := range chunk { 595 + s := charRepr(c) 596 + fmt.Fprintf(&b, "%*s", w, s) 597 + } 598 + case fmtHex: 599 + renderInts(&b, chunk, f, false, 16, maxByteWidth) 600 + case fmtOctal: 601 + renderInts(&b, chunk, f, false, 8, maxByteWidth) 602 + case fmtUnsignedDec: 603 + renderInts(&b, chunk, f, false, 10, maxByteWidth) 604 + case fmtSignedDec: 605 + renderInts(&b, chunk, f, true, 10, maxByteWidth) 606 + case fmtFloat: 607 + renderFloats(&b, chunk, f) 608 + } 609 + return b.String() 610 + } 611 + 612 + func renderInts(b *strings.Builder, chunk []byte, f formatSpec, signed bool, base int, maxByteWidth int) { 613 + w := f.width 614 + if f.size == 1 && maxByteWidth > w { 615 + w = maxByteWidth 616 + } 617 + pad := w - 1 618 + zeroPad := !signed && (base == 8 || base == 16) 619 + digits := digitsForType(f.kind, f.size) 620 + for i := 0; i < len(chunk); i += f.size { 621 + end := i + f.size 622 + if end > len(chunk) { 623 + end = len(chunk) 624 + } 625 + buf := make([]byte, f.size) 626 + copy(buf, chunk[i:end]) 627 + var num string 628 + if signed { 629 + num = signedString(buf, base) 630 + } else { 631 + num = unsignedString(buf, base) 632 + } 633 + if zeroPad && len(num) < digits { 634 + num = strings.Repeat("0", digits-len(num)) + num 635 + } 636 + fmt.Fprintf(b, " %*s", pad, num) 637 + } 638 + } 639 + 640 + func digitsForType(kind formatKind, size int) int { 641 + switch kind { 642 + case fmtOctal: 643 + switch size { 644 + case 1: 645 + return 3 646 + case 2: 647 + return 6 648 + case 4: 649 + return 11 650 + case 8: 651 + return 22 652 + } 653 + case fmtHex: 654 + return size * 2 655 + } 656 + return 0 657 + } 658 + 659 + func unsignedString(buf []byte, base int) string { 660 + var v uint64 661 + switch len(buf) { 662 + case 1: 663 + v = uint64(buf[0]) 664 + case 2: 665 + v = uint64(binary.LittleEndian.Uint16(buf)) 666 + case 4: 667 + v = uint64(binary.LittleEndian.Uint32(buf)) 668 + case 8: 669 + v = binary.LittleEndian.Uint64(buf) 670 + } 671 + return strconv.FormatUint(v, base) 672 + } 673 + 674 + func signedString(buf []byte, base int) string { 675 + var v int64 676 + switch len(buf) { 677 + case 1: 678 + v = int64(int8(buf[0])) 679 + case 2: 680 + v = int64(int16(binary.LittleEndian.Uint16(buf))) 681 + case 4: 682 + v = int64(int32(binary.LittleEndian.Uint32(buf))) 683 + case 8: 684 + v = int64(binary.LittleEndian.Uint64(buf)) 685 + } 686 + return strconv.FormatInt(v, base) 687 + } 688 + 689 + func renderFloats(b *strings.Builder, chunk []byte, f formatSpec) { 690 + pad := f.width - 1 691 + for i := 0; i < len(chunk); i += f.size { 692 + end := i + f.size 693 + if end > len(chunk) { 694 + end = len(chunk) 695 + } 696 + buf := make([]byte, f.size) 697 + copy(buf, chunk[i:end]) 698 + var s string 699 + switch f.size { 700 + case 4: 701 + v := math.Float32frombits(binary.LittleEndian.Uint32(buf)) 702 + s = strconv.FormatFloat(float64(v), 'g', 8, 32) 703 + case 8: 704 + v := math.Float64frombits(binary.LittleEndian.Uint64(buf)) 705 + s = strconv.FormatFloat(v, 'g', 16, 64) 706 + } 707 + fmt.Fprintf(b, " %*s", pad, s) 708 + } 709 + } 710 + 711 + func charRepr(c byte) string { 712 + switch c { 176 713 case 0: 177 - return ` \0` 714 + return `\0` 178 715 case 7: 179 - return ` \a` 716 + return `\a` 180 717 case 8: 181 - return ` \b` 718 + return `\b` 182 719 case 9: 183 - return ` \t` 720 + return `\t` 184 721 case 10: 185 - return ` \n` 722 + return `\n` 186 723 case 11: 187 - return ` \v` 724 + return `\v` 188 725 case 12: 189 - return ` \f` 726 + return `\f` 190 727 case 13: 191 - return ` \r` 728 + return `\r` 192 729 } 193 - if code >= 32 && code < 127 { 194 - return fmt.Sprintf(" %c", code) 730 + if c >= 32 && c < 127 { 731 + return string(rune(c)) 195 732 } 196 - return fmt.Sprintf(" %03o", code) 733 + return fmt.Sprintf("%03o", c) 197 734 } 198 735 199 - func formatHexByte(code byte, padForChar bool) string { 200 - if padForChar { 201 - return fmt.Sprintf(" %02x", code) 736 + func namedChar(c byte) string { 737 + c &= 0x7f 738 + if c == 0x7f { 739 + return "del" 202 740 } 203 - return fmt.Sprintf(" %02x", code) 741 + names := [...]string{ 742 + "nul", "soh", "stx", "etx", "eot", "enq", "ack", "bel", 743 + "bs", "ht", "nl", "vt", "ff", "cr", "so", "si", 744 + "dle", "dc1", "dc2", "dc3", "dc4", "nak", "syn", "etb", 745 + "can", "em", "sub", "esc", "fs", "gs", "rs", "us", 746 + "sp", 747 + } 748 + if int(c) < len(names) { 749 + return names[c] 750 + } 751 + return string(rune(c)) 752 + } 753 + 754 + func formatAddress(addr int64, radix string) string { 755 + switch radix { 756 + case "d": 757 + return fmt.Sprintf("%07d", addr) 758 + case "x": 759 + return fmt.Sprintf("%06x", addr) 760 + case "o": 761 + return fmt.Sprintf("%07o", addr) 762 + } 763 + return "" 764 + } 765 + 766 + func addressWidth(radix string) int { 767 + switch radix { 768 + case "d", "o": 769 + return 7 770 + case "x": 771 + return 6 772 + } 773 + return 0 774 + } 775 + 776 + func bytesEqual(a, b []byte) bool { 777 + if len(a) != len(b) { 778 + return false 779 + } 780 + for i := range a { 781 + if a[i] != b[i] { 782 + return false 783 + } 784 + } 785 + return true 204 786 } 205 787 206 788 func resolvePath(ec *command.ExecContext, p string) string {
+273 -19
command/internal/od/od_test.go
··· 25 25 } 26 26 write("hi.txt", []byte("Hi")) 27 27 write("ab.bin", []byte{0x01, 0x41, 0x7f, 0xff}) 28 + write("dup.bin", []byte(strings.Repeat("a", 34))) 29 + write("twelve.bin", []byte("ABCDEFGHIJKL")) 30 + write("zeros.bin", make([]byte, 64)) 28 31 return fs 29 32 } 30 33 ··· 55 58 name: "default octal from stdin", 56 59 args: nil, 57 60 stdin: "Hi", 58 - wantStdout: "0000000 110 151\n0000002\n", 61 + wantStdout: "0000000 064510\n0000002\n", 59 62 }, 60 63 { 61 64 name: "default octal from file", 62 65 args: []string{"hi.txt"}, 63 - wantStdout: "0000000 110 151\n0000002\n", 66 + wantStdout: "0000000 064510\n0000002\n", 64 67 }, 65 68 { 66 69 name: "dash means stdin", 67 70 args: []string{"-"}, 68 71 stdin: "Hi", 69 - wantStdout: "0000000 110 151\n0000002\n", 72 + wantStdout: "0000000 064510\n0000002\n", 70 73 }, 71 74 { 72 - name: "empty input produces no output", 75 + name: "empty input prints terminating address", 73 76 args: nil, 74 77 stdin: "", 75 - wantStdout: "", 78 + wantStdout: "0000000\n", 76 79 }, 77 80 { 78 81 name: "char format printable", 79 82 args: []string{"-c"}, 80 83 stdin: "ABC", 81 - wantStdout: "0000000 A B C\n0000003\n", 84 + wantStdout: "0000000 A B C\n0000003\n", 82 85 }, 83 86 { 84 87 name: "char format named escape", 85 88 args: []string{"-c"}, 86 89 stdin: "A\nB", 87 - wantStdout: "0000000 A \\n B\n0000003\n", 90 + wantStdout: "0000000 A \\n B\n0000003\n", 88 91 }, 89 92 { 90 93 name: "char format octal fallback for non-printable", 91 94 args: []string{"-c", "ab.bin"}, 92 - wantStdout: "0000000 001 A 177 377\n0000004\n", 95 + wantStdout: "0000000 001 A 177 377\n0000004\n", 93 96 }, 94 97 { 95 98 name: "hex via -t x1", 96 99 args: []string{"-t", "x1"}, 97 100 stdin: "Hi", 98 - wantStdout: "0000000 48 69\n0000002\n", 101 + wantStdout: "0000000 48 69\n0000002\n", 102 + }, 103 + { 104 + name: "octal via -t o1", 105 + args: []string{"-t", "o1"}, 106 + stdin: "Hi", 107 + wantStdout: "0000000 110 151\n0000002\n", 99 108 }, 100 109 { 101 - name: "octal via -t o", 110 + name: "octal via -t o (default size 4)", 102 111 args: []string{"-t", "o"}, 103 112 stdin: "Hi", 104 - wantStdout: "0000000 110 151\n0000002\n", 113 + wantStdout: "0000000 00000064510\n0000002\n", 105 114 }, 106 115 { 107 116 name: "char via -t c", 108 117 args: []string{"-t", "c"}, 109 118 stdin: "AB", 110 - wantStdout: "0000000 A B\n0000002\n", 119 + wantStdout: "0000000 A B\n0000002\n", 111 120 }, 112 121 { 113 122 name: "no address with -An", ··· 125 134 name: "char and hex together widens hex field", 126 135 args: []string{"-c", "-t", "x1"}, 127 136 stdin: "A", 128 - wantStdout: "0000000 A\n 41\n0000001\n", 137 + wantStdout: "0000000 A\n 41\n0000001\n", 129 138 }, 130 139 { 131 140 name: "format order is preserved", 132 141 args: []string{"-t", "x1", "-c"}, 133 142 stdin: "A", 134 - wantStdout: "0000000 41\n A\n0000001\n", 143 + wantStdout: "0000000 41\n A\n0000001\n", 135 144 }, 136 145 { 137 146 name: "wraps after 16 bytes per line", 138 147 args: []string{"-t", "x1"}, 139 148 stdin: "0123456789abcdefXY", 140 - wantStdout: "0000000 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66\n0000020 58 59\n0000022\n", 149 + wantStdout: "0000000 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66\n0000020 58 59\n0000022\n", 141 150 }, 142 151 { 143 - name: "unrecognized -t format is silently ignored", 144 - args: []string{"-t", "d"}, 145 - stdin: "Hi", 146 - wantStdout: "0000000 110 151\n0000002\n", 152 + name: "signed decimal -t d2 little-endian", 153 + args: []string{"-t", "d2", "-An"}, 154 + stdin: "AB", 155 + wantStdout: " 16961\n", 156 + }, 157 + { 158 + name: "unsigned decimal -t u1", 159 + args: []string{"-An", "-t", "u1"}, 160 + stdin: "AB", 161 + wantStdout: " 65 66\n", 162 + }, 163 + { 164 + name: "named character -t a", 165 + args: []string{"-An", "-t", "a"}, 166 + stdin: "A\nB", 167 + wantStdout: " A nl B\n", 168 + }, 169 + { 170 + name: "concatenated type chars -t x1c", 171 + args: []string{"-An", "-t", "x1c"}, 172 + stdin: "AB", 173 + wantStdout: " 41 42\n A B\n", 174 + }, 175 + { 176 + name: "duplicate blocks compressed to *", 177 + args: []string{"dup.bin"}, 178 + wantStdout: "0000000 060541 060541 060541 060541 060541 060541 060541 060541\n*\n0000040 060541\n0000042\n", 179 + }, 180 + { 181 + name: "verbose -v emits all blocks", 182 + args: []string{"-v", "dup.bin"}, 183 + wantStdout: "0000000 060541 060541 060541 060541 060541 060541 060541 060541\n0000020 060541 060541 060541 060541 060541 060541 060541 060541\n0000040 060541\n0000042\n", 184 + }, 185 + { 186 + name: "skip and limit -j 4 -N 4", 187 + args: []string{"-j", "4", "-N", "4", "-An", "-t", "x1", "twelve.bin"}, 188 + wantStdout: " 45 46 47 48\n", 189 + }, 190 + { 191 + name: "skip increments address", 192 + args: []string{"-j", "4", "-t", "x1", "twelve.bin"}, 193 + wantStdout: "0000004 45 46 47 48 49 4a 4b 4c\n0000014\n", 194 + }, 195 + { 196 + name: "skip past end is an error", 197 + args: []string{"-j", "100", "twelve.bin"}, 198 + wantErr: true, 199 + wantErrSub: "od: cannot skip past end of combined input", 200 + }, 201 + { 202 + name: "decimal address with -A d", 203 + args: []string{"-A", "d", "-t", "x1"}, 204 + stdin: "AB", 205 + wantStdout: "0000000 41 42\n0000002\n", 206 + }, 207 + { 208 + name: "hex address with -A x", 209 + args: []string{"-A", "x", "-t", "x1"}, 210 + stdin: "AB", 211 + wantStdout: "000000 41 42\n000002\n", 147 212 }, 148 213 { 149 214 name: "missing file reports error", ··· 157 222 args: []string{"--no-such-flag"}, 158 223 wantErr: true, 159 224 }, 225 + { 226 + name: "invalid -t type string is an error", 227 + args: []string{"-t", "z"}, 228 + stdin: "Hi", 229 + wantErr: true, 230 + wantErrSub: "od: invalid type string 'z'", 231 + }, 232 + { 233 + name: "invalid -A radix is an error", 234 + args: []string{"-A", "q"}, 235 + stdin: "Hi", 236 + wantErr: true, 237 + wantErrSub: "od: invalid output address radix 'q'", 238 + }, 239 + // XSI shorthand options. 240 + { 241 + name: "-b is same as -t o1", 242 + args: []string{"-An", "-b"}, 243 + stdin: "AB", 244 + wantStdout: " 101 102\n", 245 + }, 246 + { 247 + name: "-d is same as -t u2", 248 + args: []string{"-An", "-d"}, 249 + stdin: "AB", 250 + wantStdout: " 16961\n", 251 + }, 252 + { 253 + name: "-o is same as -t o2", 254 + args: []string{"-An", "-o"}, 255 + stdin: "AB", 256 + wantStdout: " 041101\n", 257 + }, 258 + { 259 + name: "-s is same as -t d2", 260 + args: []string{"-An", "-s"}, 261 + stdin: "AB", 262 + wantStdout: " 16961\n", 263 + }, 264 + { 265 + name: "-x is same as -t x2", 266 + args: []string{"-An", "-x"}, 267 + stdin: "AB", 268 + wantStdout: " 4241\n", 269 + }, 270 + { 271 + name: "XSI shorthands stack in order", 272 + args: []string{"-An", "-x", "-c"}, 273 + stdin: "AB", 274 + wantStdout: " 4241\n A B\n", 275 + }, 276 + // Extended -t grammar. 277 + { 278 + name: "-t d4 reads 4-byte signed decimal", 279 + args: []string{"-An", "-t", "d4"}, 280 + stdin: "ABCD", 281 + wantStdout: " 1145258561\n", 282 + }, 283 + { 284 + name: "-t u4 reads 4-byte unsigned decimal", 285 + args: []string{"-An", "-t", "u4"}, 286 + stdin: "ABCD", 287 + wantStdout: " 1145258561\n", 288 + }, 289 + { 290 + name: "-t x2 reads 2-byte hex little-endian", 291 + args: []string{"-An", "-t", "x2"}, 292 + stdin: "AB", 293 + wantStdout: " 4241\n", 294 + }, 295 + { 296 + name: "-t o2 reads 2-byte octal little-endian", 297 + args: []string{"-An", "-t", "o2"}, 298 + stdin: "AB", 299 + wantStdout: " 041101\n", 300 + }, 301 + { 302 + name: "-t dC alias for -t d1", 303 + args: []string{"-An", "-t", "dC"}, 304 + stdin: "AB", 305 + wantStdout: " 65 66\n", 306 + }, 307 + { 308 + name: "-t dS alias for -t d2", 309 + args: []string{"-An", "-t", "dS"}, 310 + stdin: "AB", 311 + wantStdout: " 16961\n", 312 + }, 313 + { 314 + name: "-t dI alias for -t d4", 315 + args: []string{"-An", "-t", "dI"}, 316 + stdin: "ABCD", 317 + wantStdout: " 1145258561\n", 318 + }, 319 + { 320 + name: "-t comma list", 321 + args: []string{"-An", "-t", "x1,c"}, 322 + stdin: "AB", 323 + wantStdout: " 41 42\n A B\n", 324 + }, 325 + { 326 + name: "-t multiple invocations preserved", 327 + args: []string{"-An", "-t", "x1", "-t", "c"}, 328 + stdin: "AB", 329 + wantStdout: " 41 42\n A B\n", 330 + }, 331 + { 332 + name: "-t f4 emits float", 333 + args: []string{"-An", "-t", "f4"}, 334 + stdin: "\x00\x00\x80\x3f", // 1.0 little-endian 335 + wantStdout: " 1\n", 336 + }, 337 + { 338 + name: "-t fF alias for -t f4", 339 + args: []string{"-An", "-t", "fF"}, 340 + stdin: "\x00\x00\x80\x3f", 341 + wantStdout: " 1\n", 342 + }, 343 + { 344 + name: "-t fD alias for -t f8", 345 + args: []string{"-An", "-t", "fD"}, 346 + stdin: "\x00\x00\x00\x00\x00\x00\xf0\x3f", // 1.0 double LE 347 + wantStdout: " 1\n", 348 + }, 349 + // -j byte-count parsing variants. 350 + { 351 + name: "-j with octal prefix", 352 + args: []string{"-j", "04", "-N", "4", "-An", "-t", "x1", "twelve.bin"}, 353 + wantStdout: " 45 46 47 48\n", 354 + }, 355 + { 356 + name: "-j with hex prefix", 357 + args: []string{"-j", "0x4", "-N", "4", "-An", "-t", "x1", "twelve.bin"}, 358 + wantStdout: " 45 46 47 48\n", 359 + }, 360 + { 361 + name: "-N with octal", 362 + args: []string{"-N", "04", "-An", "-t", "x1", "twelve.bin"}, 363 + wantStdout: " 41 42 43 44\n", 364 + }, 365 + // * collapsing edge cases. 366 + { 367 + name: "all zeros collapse", 368 + args: []string{"-An", "-t", "x1", "zeros.bin"}, 369 + wantStdout: " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n*\n 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n", 370 + }, 371 + { 372 + name: "verbose disables collapse for zeros", 373 + args: []string{"-v", "-An", "-t", "x1", "zeros.bin"}, 374 + wantStdout: " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n", 375 + }, 376 + // -w sets width. 377 + { 378 + name: "-w 8 limits to 8 bytes per line", 379 + args: []string{"-w", "8", "-An", "-t", "x1"}, 380 + stdin: "0123456789ab", 381 + wantStdout: " 30 31 32 33 34 35 36 37\n 38 39 61 62\n", 382 + }, 383 + // -A modes. 384 + { 385 + name: "-A o is the default address radix", 386 + args: []string{"-A", "o", "-t", "x1"}, 387 + stdin: "AB", 388 + wantStdout: "0000000 41 42\n0000002\n", 389 + }, 390 + // Type-string error cases. 391 + { 392 + name: "invalid -t size errors", 393 + args: []string{"-t", "d3"}, 394 + stdin: "ABCD", 395 + wantErr: true, 396 + wantErrSub: "od: invalid type string 'd3'", 397 + }, 398 + { 399 + name: "unknown -t character errors", 400 + args: []string{"-t", "z"}, 401 + stdin: "Hi", 402 + wantErr: true, 403 + wantErrSub: "od: invalid type string 'z'", 404 + }, 160 405 } 161 406 162 407 for _, tt := range tests { ··· 195 440 } 196 441 if !strings.Contains(stderr, "-t, --format") { 197 442 t.Errorf("format flag missing from help: %q", stderr) 443 + } 444 + if !strings.Contains(stderr, "-v, --output-duplicates") { 445 + t.Errorf("verbose flag missing from help: %q", stderr) 446 + } 447 + if !strings.Contains(stderr, "-j, --skip-bytes") { 448 + t.Errorf("skip-bytes flag missing from help: %q", stderr) 449 + } 450 + if !strings.Contains(stderr, "-N, --read-bytes") { 451 + t.Errorf("read-bytes flag missing from help: %q", stderr) 198 452 } 199 453 }