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(command): port seq from just-bash

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 5360e6f8 b05d06a4

+388
+228
command/internal/seq/seq.go
··· 1 + package seq 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "math" 9 + "strconv" 10 + "strings" 11 + 12 + "mvdan.cc/sh/v3/interp" 13 + "tangled.org/xeiaso.net/kefka/command" 14 + ) 15 + 16 + type Impl struct{} 17 + 18 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 19 + if ec == nil { 20 + return errors.New("seq: nil ExecContext") 21 + } 22 + 23 + stdout := ec.Stdout 24 + if stdout == nil { 25 + stdout = io.Discard 26 + } 27 + stderr := ec.Stderr 28 + if stderr == nil { 29 + stderr = io.Discard 30 + } 31 + 32 + usage := func() { 33 + fmt.Fprint(stderr, "Usage: seq [OPTION]... LAST\n") 34 + fmt.Fprint(stderr, " or: seq [OPTION]... FIRST LAST\n") 35 + fmt.Fprint(stderr, " or: seq [OPTION]... FIRST INCREMENT LAST\n") 36 + fmt.Fprint(stderr, "Print numbers from FIRST to LAST, in steps of INCREMENT.\n\n") 37 + fmt.Fprint(stderr, " -s STRING use STRING to separate numbers (default: newline)\n") 38 + fmt.Fprint(stderr, " -w equalize width by padding with leading zeros\n") 39 + fmt.Fprint(stderr, " --help display this help and exit\n") 40 + } 41 + 42 + separator := "\n" 43 + equalizeWidth := false 44 + var nums []string 45 + 46 + i := 0 47 + for i < len(args) { 48 + arg := args[i] 49 + 50 + if arg == "-s" && i+1 < len(args) { 51 + separator = args[i+1] 52 + i += 2 53 + continue 54 + } 55 + if arg == "-w" { 56 + equalizeWidth = true 57 + i++ 58 + continue 59 + } 60 + if arg == "--" { 61 + i++ 62 + break 63 + } 64 + if arg == "--help" { 65 + usage() 66 + return nil 67 + } 68 + if strings.HasPrefix(arg, "-") && arg != "-" { 69 + if strings.HasPrefix(arg, "-s") && len(arg) > 2 { 70 + separator = arg[2:] 71 + i++ 72 + continue 73 + } 74 + if arg == "-ws" || arg == "-sw" { 75 + equalizeWidth = true 76 + if i+1 < len(args) { 77 + separator = args[i+1] 78 + i += 2 79 + continue 80 + } 81 + } 82 + } 83 + 84 + nums = append(nums, arg) 85 + i++ 86 + } 87 + for i < len(args) { 88 + nums = append(nums, args[i]) 89 + i++ 90 + } 91 + 92 + if len(nums) == 0 { 93 + fmt.Fprint(stderr, "seq: missing operand\n") 94 + return interp.ExitStatus(1) 95 + } 96 + 97 + first := 1.0 98 + increment := 1.0 99 + var last float64 100 + 101 + parseNum := func(s string) (float64, error) { 102 + v, err := strconv.ParseFloat(s, 64) 103 + if err != nil || math.IsNaN(v) { 104 + return 0, fmt.Errorf("seq: invalid floating point argument: '%s'", s) 105 + } 106 + return v, nil 107 + } 108 + 109 + switch len(nums) { 110 + case 1: 111 + v, err := parseNum(nums[0]) 112 + if err != nil { 113 + fmt.Fprintln(stderr, err) 114 + return interp.ExitStatus(1) 115 + } 116 + last = v 117 + case 2: 118 + fv, err := parseNum(nums[0]) 119 + if err != nil { 120 + fmt.Fprintln(stderr, err) 121 + return interp.ExitStatus(1) 122 + } 123 + lv, err := parseNum(nums[1]) 124 + if err != nil { 125 + fmt.Fprintln(stderr, err) 126 + return interp.ExitStatus(1) 127 + } 128 + first, last = fv, lv 129 + default: 130 + fv, err := parseNum(nums[0]) 131 + if err != nil { 132 + fmt.Fprintln(stderr, err) 133 + return interp.ExitStatus(1) 134 + } 135 + iv, err := parseNum(nums[1]) 136 + if err != nil { 137 + fmt.Fprintln(stderr, err) 138 + return interp.ExitStatus(1) 139 + } 140 + lv, err := parseNum(nums[2]) 141 + if err != nil { 142 + fmt.Fprintln(stderr, err) 143 + return interp.ExitStatus(1) 144 + } 145 + first, increment, last = fv, iv, lv 146 + } 147 + 148 + if increment == 0 { 149 + fmt.Fprint(stderr, "seq: invalid Zero increment value: '0'\n") 150 + return interp.ExitStatus(1) 151 + } 152 + 153 + precision := getPrecision(first) 154 + if p := getPrecision(increment); p > precision { 155 + precision = p 156 + } 157 + if p := getPrecision(last); p > precision { 158 + precision = p 159 + } 160 + 161 + const maxIterations = 100000 162 + const epsilon = 1e-10 163 + 164 + var results []string 165 + if increment > 0 { 166 + for n := first; n <= last+epsilon; n += increment { 167 + if len(results) >= maxIterations { 168 + break 169 + } 170 + results = append(results, formatNum(n, precision)) 171 + } 172 + } else { 173 + for n := first; n >= last-epsilon; n += increment { 174 + if len(results) >= maxIterations { 175 + break 176 + } 177 + results = append(results, formatNum(n, precision)) 178 + } 179 + } 180 + 181 + if equalizeWidth && len(results) > 0 { 182 + maxLen := 0 183 + for _, r := range results { 184 + l := len(strings.TrimPrefix(r, "-")) 185 + if l > maxLen { 186 + maxLen = l 187 + } 188 + } 189 + for j, r := range results { 190 + negative := strings.HasPrefix(r, "-") 191 + num := strings.TrimPrefix(r, "-") 192 + if pad := maxLen - len(num); pad > 0 { 193 + num = strings.Repeat("0", pad) + num 194 + } 195 + if negative { 196 + results[j] = "-" + num 197 + } else { 198 + results[j] = num 199 + } 200 + } 201 + } 202 + 203 + output := strings.Join(results, separator) 204 + if output != "" { 205 + output += "\n" 206 + } 207 + fmt.Fprint(stdout, output) 208 + return nil 209 + } 210 + 211 + func getPrecision(n float64) int { 212 + s := strconv.FormatFloat(n, 'g', -1, 64) 213 + if strings.ContainsAny(s, "eE") { 214 + return 0 215 + } 216 + dotIndex := strings.Index(s, ".") 217 + if dotIndex == -1 { 218 + return 0 219 + } 220 + return len(s) - dotIndex - 1 221 + } 222 + 223 + func formatNum(n float64, precision int) string { 224 + if precision > 0 { 225 + return strconv.FormatFloat(n, 'f', precision, 64) 226 + } 227 + return strconv.FormatInt(int64(math.Round(n)), 10) 228 + }
+160
command/internal/seq/seq_test.go
··· 1 + package seq 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "strings" 7 + "testing" 8 + 9 + "tangled.org/xeiaso.net/kefka/command" 10 + ) 11 + 12 + func run(t *testing.T, args []string) (string, string, error) { 13 + t.Helper() 14 + var stdout, stderr bytes.Buffer 15 + ec := &command.ExecContext{ 16 + Stdout: &stdout, 17 + Stderr: &stderr, 18 + Dir: ".", 19 + } 20 + err := Impl{}.Exec(context.Background(), ec, args) 21 + return stdout.String(), stderr.String(), err 22 + } 23 + 24 + func TestSeq(t *testing.T) { 25 + tests := []struct { 26 + name string 27 + args []string 28 + wantStdout string 29 + wantErrSub string 30 + wantErr bool 31 + }{ 32 + { 33 + name: "single LAST arg counts from 1", 34 + args: []string{"5"}, 35 + wantStdout: "1\n2\n3\n4\n5\n", 36 + }, 37 + { 38 + name: "FIRST LAST", 39 + args: []string{"3", "5"}, 40 + wantStdout: "3\n4\n5\n", 41 + }, 42 + { 43 + name: "FIRST INCREMENT LAST", 44 + args: []string{"1", "2", "9"}, 45 + wantStdout: "1\n3\n5\n7\n9\n", 46 + }, 47 + { 48 + name: "negative increment counts down", 49 + args: []string{"5", "-1", "1"}, 50 + wantStdout: "5\n4\n3\n2\n1\n", 51 + }, 52 + { 53 + name: "negative FIRST passes through as positional", 54 + args: []string{"-3", "0"}, 55 + wantStdout: "-3\n-2\n-1\n0\n", 56 + }, 57 + { 58 + name: "FIRST greater than LAST yields nothing", 59 + args: []string{"5", "1"}, 60 + wantStdout: "", 61 + }, 62 + { 63 + name: "single FIRST equals LAST", 64 + args: []string{"3", "3"}, 65 + wantStdout: "3\n", 66 + }, 67 + { 68 + name: "custom separator -s", 69 + args: []string{"-s", " ", "1", "5"}, 70 + wantStdout: "1 2 3 4 5\n", 71 + }, 72 + { 73 + name: "attached separator -sSTRING", 74 + args: []string{"-s,", "1", "3"}, 75 + wantStdout: "1,2,3\n", 76 + }, 77 + { 78 + name: "equalize width -w", 79 + args: []string{"-w", "8", "10"}, 80 + wantStdout: "08\n09\n10\n", 81 + }, 82 + { 83 + name: "equalize width pads negatives with leading zeros", 84 + args: []string{"-w", "-1", "10"}, 85 + wantStdout: "-01\n00\n01\n02\n03\n04\n05\n06\n07\n08\n09\n10\n", 86 + }, 87 + { 88 + name: "float precision matches widest input", 89 + args: []string{"1", "0.5", "3"}, 90 + wantStdout: "1.0\n1.5\n2.0\n2.5\n3.0\n", 91 + }, 92 + { 93 + name: "double dash terminates option parsing", 94 + args: []string{"--", "1", "3"}, 95 + wantStdout: "1\n2\n3\n", 96 + }, 97 + { 98 + name: "missing operand", 99 + args: []string{}, 100 + wantErrSub: "seq: missing operand", 101 + wantErr: true, 102 + }, 103 + { 104 + name: "zero increment is rejected", 105 + args: []string{"1", "0", "5"}, 106 + wantErrSub: "seq: invalid Zero increment value: '0'", 107 + wantErr: true, 108 + }, 109 + { 110 + name: "non-numeric argument is rejected", 111 + args: []string{"abc"}, 112 + wantErrSub: "seq: invalid floating point argument: 'abc'", 113 + wantErr: true, 114 + }, 115 + { 116 + name: "non-numeric increment is rejected", 117 + args: []string{"1", "x", "5"}, 118 + wantErrSub: "seq: invalid floating point argument: 'x'", 119 + wantErr: true, 120 + }, 121 + } 122 + 123 + for _, tt := range tests { 124 + t.Run(tt.name, func(t *testing.T) { 125 + stdout, stderr, err := run(t, tt.args) 126 + if tt.wantErr { 127 + if err == nil { 128 + t.Fatalf("expected error, got nil; stdout=%q stderr=%q", stdout, stderr) 129 + } 130 + } else if err != nil { 131 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 132 + } 133 + if stdout != tt.wantStdout { 134 + t.Errorf("stdout mismatch\n got: %q\nwant: %q", stdout, tt.wantStdout) 135 + } 136 + if tt.wantErrSub != "" && !strings.Contains(stderr, tt.wantErrSub) { 137 + t.Errorf("stderr = %q, want substring %q", stderr, tt.wantErrSub) 138 + } 139 + }) 140 + } 141 + } 142 + 143 + func TestHelp(t *testing.T) { 144 + stdout, stderr, err := run(t, []string{"--help"}) 145 + if err != nil { 146 + t.Fatalf("unexpected error: %v", err) 147 + } 148 + if stdout != "" { 149 + t.Errorf("expected empty stdout, got %q", stdout) 150 + } 151 + if !strings.Contains(stderr, "Usage: seq") { 152 + t.Errorf("usage line missing from stderr: %q", stderr) 153 + } 154 + if !strings.Contains(stderr, "-s STRING") { 155 + t.Errorf("-s flag missing from help: %q", stderr) 156 + } 157 + if !strings.Contains(stderr, "-w") { 158 + t.Errorf("-w flag missing from help: %q", stderr) 159 + } 160 + }