Skip to content

Commit b3b965a

Browse files
committed
Improve command normalization and test coverage
Refine flag handling for go, tar, and stow handlers to better support subshells and fused arguments. Expand test suites across all handlers to cover edge cases like trailing flags, subshell values, and shell redirection.
1 parent 9b6eae9 commit b3b965a

22 files changed

Lines changed: 520 additions & 32 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
- uses: actions/setup-go@v5
1515
with:
1616
go-version-file: go.mod
17+
cache: false
1718

1819
- name: Check formatting
1920
run: test -z "$(gofmt -l .)"

handler_dmesg_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ func TestDmesg(t *testing.T) {
4242
{"human with facility", "dmesg -H -f kern", "dmesg -H -f <facility>"},
4343
{"kernel json", "dmesg -kJ", "dmesg -kJ"},
4444

45+
// Subshell as standalone token (hits isSubshellToken in main loop)
46+
{"subshell token", "dmesg $(get-flags)", "dmesg $(get-flags)"},
47+
48+
// Subshell as argument to each flag category
49+
{"level subshell", "dmesg -l $(get-level)", "dmesg -l $(get-level)"},
50+
{"facility subshell", "dmesg -f $(get-facility)", "dmesg -f $(get-facility)"},
51+
{"size subshell", "dmesg -s $(calc-size)", "dmesg -s $(calc-size)"},
52+
{"since subshell", "dmesg --since $(date-cmd)", "dmesg --since $(date-cmd)"},
53+
{"time-format subshell", "dmesg --time-format $(get-fmt)", "dmesg --time-format $(get-fmt)"},
54+
55+
// Unexpected positional argument
56+
{"positional path", "dmesg /var/log/messages", "dmesg <path>"},
57+
4558
// Redirect
4659
{"redirect", "dmesg > /tmp/kern.log", "dmesg > <path>"},
4760
{"pipe grep", "dmesg -T | grep error", "dmesg -T | grep <pattern>"},

handler_free_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,26 @@ func TestFree(t *testing.T) {
4545
{"seconds equals", "free --seconds=5", "free --seconds=<val>"},
4646
{"count equals", "free --count=10", "free --count=<val>"},
4747

48+
// Consuming flag at end of input (no value follows)
49+
{"seconds short trailing", "free -s", "free -s"},
50+
{"count short trailing", "free -c", "free -c"},
51+
{"seconds long trailing", "free --seconds", "free --seconds"},
52+
{"count long trailing", "free --count", "free --count"},
53+
54+
// Combined flag ending in consuming letter at end of input
55+
{"combined trailing s", "free -hs", "free -hs"},
56+
{"combined trailing c", "free -hc", "free -hc"},
57+
58+
// Combined flag ending in consuming letter with subshell value
59+
{"combined s subshell", "free -hs $(calc)", "free -hs $(calc)"},
60+
{"combined c subshell", "free -hc $(calc)", "free -hc $(calc)"},
61+
62+
// Subshell as standalone token
63+
{"subshell standalone", "free $(flags)", "free $(flags)"},
64+
65+
// Consuming flag with subshell value
66+
{"count subshell", "free -c $(echo 5)", "free -c $(echo <str>)"},
67+
4868
// Edge cases
4969
{"redirect", "free -h > output.txt", "free -h > <path>"},
5070
{"pipe ignored in shape", "free -m", "free -m"},

handler_go.go

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -118,22 +118,22 @@ func handleGoTest(tokens []string) []string {
118118
"-fuzztime": true,
119119
}
120120
pathFlags := map[string]bool{
121-
"-coverprofile": true,
122-
"-cpuprofile": true,
123-
"-memprofile": true,
124-
"-mutexprofile": true,
125-
"-blockprofile": true,
126-
"-trace": true,
127-
"-outputdir": true,
128-
"-o": true,
121+
"-coverprofile": true,
122+
"-cpuprofile": true,
123+
"-memprofile": true,
124+
"-mutexprofile": true,
125+
"-blockprofile": true,
126+
"-trace": true,
127+
"-outputdir": true,
128+
"-o": true,
129129
}
130130
valFlags := map[string]bool{
131-
"-covermode": true,
132-
"-coverpkg": true,
133-
"-benchtime": true,
134-
"-cpu": true,
135-
"-blockprofilerate": true,
136-
"-memprofilerate": true,
131+
"-covermode": true,
132+
"-coverpkg": true,
133+
"-benchtime": true,
134+
"-cpu": true,
135+
"-blockprofilerate": true,
136+
"-memprofilerate": true,
137137
"-mutexprofilefraction": true,
138138
}
139139
// merge build val flags
@@ -398,18 +398,18 @@ func handleGoMod(tokens []string) []string {
398398

399399
// go mod edit flags that consume a value
400400
modEditValFlags := map[string]bool{
401-
"-replace": true,
402-
"-require": true,
401+
"-replace": true,
402+
"-require": true,
403403
"-dropreplace": true,
404404
"-droprequire": true,
405-
"-exclude": true,
405+
"-exclude": true,
406406
"-dropexclude": true,
407-
"-retract": true,
407+
"-retract": true,
408408
"-dropretract": true,
409-
"-go": true,
410-
"-toolchain": true,
411-
"-json": true,
412-
"-fmt": true,
409+
"-go": true,
410+
"-toolchain": true,
411+
"-json": true,
412+
"-fmt": true,
413413
}
414414

415415
var result []string
@@ -532,33 +532,63 @@ func handleGoList(tokens []string) []string {
532532
return result
533533
}
534534

535+
// Flags for go tool subcommands (cover, pprof, trace) that consume a path arg.
536+
var goToolPathFlags = map[string]bool{
537+
"-func": true, "-html": true, "-o": true,
538+
"-http": true, // pprof: -http :8080 (actually a listen addr, but collapse)
539+
}
540+
535541
func handleGoTool(tokens []string) []string {
536542
args, redirects := splitRedirects(tokens)
537543

538544
var result []string
539545
seenTool := false
540-
for i := 0; i < len(args); i++ {
546+
i := 0
547+
for i < len(args) {
541548
tok := args[i]
542549

543550
if isSubshellToken(tok) {
544551
result = append(result, tok)
552+
i++
553+
continue
554+
}
555+
556+
// Handle fused -flag=value (e.g. -func=/tmp/cov.out)
557+
if isFlagToken(tok) && strings.Contains(tok, "=") {
558+
eqIdx := strings.IndexByte(tok, '=')
559+
key := tok[:eqIdx]
560+
if goToolPathFlags[key] {
561+
result = append(result, key+"=<path>")
562+
} else {
563+
result = append(result, classifyToken(tok))
564+
}
565+
i++
566+
continue
567+
}
568+
569+
// Handle separate -flag value
570+
if goToolPathFlags[tok] {
571+
result, i = consumeFlagArg(tok, args, i, result, "<path>")
545572
continue
546573
}
547574

548575
if isFlagToken(tok) {
549576
result = append(result, tok)
577+
i++
550578
continue
551579
}
552580

553581
if !seenTool {
554-
// First positional is the tool name (pprof, trace, dist) — structural
582+
// First positional is the tool name (cover, pprof, trace, dist) — structural
555583
result = append(result, tok)
556584
seenTool = true
585+
i++
557586
continue
558587
}
559588

560589
// After tool name, classify remaining tokens
561590
result = append(result, classifyToken(tok))
591+
i++
562592
}
563593

564594
result = append(result, redirects...)

handler_go_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ func TestGo(t *testing.T) {
128128
{"tool trace", "go tool trace trace.out", "go tool trace <dotted-id>"},
129129
{"tool pprof path", "go tool pprof ./cpu.prof", "go tool pprof <path>"},
130130
{"tool dist list", "go tool dist list", "go tool dist list"},
131+
{"tool cover func fused", "go tool cover -func=/tmp/cov.out", "go tool cover -func=<path>"},
132+
{"tool cover html fused", "go tool cover -html=/tmp/cov.out -o /tmp/cov.html", "go tool cover -html=<path> -o <path>"},
133+
{"tool cover func separate", "go tool cover -func /tmp/cov.out", "go tool cover -func <path>"},
134+
{"tool cover func subshell", "go tool cover -func $(get-file)", "go tool cover -func $(get-file)"},
135+
{"tool cover unknown fused", "go tool cover -cpuprofile=cpu.out", "go tool cover -cpuprofile=cpu.out"},
131136

132137
// Shared build flags across subcommands
133138
{"build p flag", "go build -p 4 .", "go build -p N ."},
@@ -139,6 +144,106 @@ func TestGo(t *testing.T) {
139144
{"test stderr redirect", "go test ./... 2>&1", "go test <path> 2>&1"},
140145
{"vet stderr", "go vet ./... 2>&1", "go vet <path> 2>&1"},
141146

147+
// Fused flag forms (go test specific)
148+
{"test run=pattern fused", "go test -run=TestFoo ./...", "go test -run=<pattern> <path>"},
149+
{"test coverprofile=path fused", "go test -coverprofile=cov.out ./...", "go test -coverprofile=<path> <path>"},
150+
{"test coverpkg=val fused", "go test -coverpkg=./pkg/... ./...", "go test -coverpkg=<val> <path>"},
151+
152+
// Subshell in flag argument positions
153+
{"test run subshell", "go test -run $(get-pattern) ./...", "go test -run $(get-pattern) <path>"},
154+
{"test timeout subshell", "go test -timeout $(calc-timeout) ./...", "go test -timeout $(calc-timeout) <path>"},
155+
{"test coverprofile subshell", "go test -coverprofile $(mktemp) ./...", "go test -coverprofile $(mktemp) <path>"},
156+
{"test count subshell", "go test -count $(nproc) ./...", "go test -count $(nproc) <path>"},
157+
{"test coverpkg subshell", "go test -coverpkg $(get-pkg) ./...", "go test -coverpkg $(get-pkg) <path>"},
158+
{"build ldflags subshell", "go build -ldflags $(gen-flags) .", "go build -ldflags $(gen-flags) ."},
159+
{"build o subshell", "go build -o $(get-path) .", "go build -o $(get-path) ."},
160+
161+
// Trailing flag with no arg
162+
{"test trailing run", "go test -run", "go test -run"},
163+
{"test trailing count", "go test -count", "go test -count"},
164+
165+
// go run edge cases
166+
{"run subshell file", "go run $(find-main)", "go run $(find-main)"},
167+
{"run with build flags", "go run -tags integration ./cmd/app arg1 arg2", "go run -tags <val> <path> arg1 arg2"},
168+
{"run ldflags", "go run -ldflags \"-X main.v=1\" main.go", "go run -ldflags <val> <path>"},
169+
{"run p flag", "go run -p 2 main.go", "go run -p N <path>"},
170+
{"run subshell in flags", "go run -tags $(get-tags) main.go", "go run -tags $(get-tags) <path>"},
171+
172+
// go install edge cases
173+
{"install with tags", "go install -tags netgo ./cmd/server", "go install -tags <val> <path>"},
174+
{"install subshell", "go install $(get-pkg)", "go install $(get-pkg)"},
175+
{"install ldflags", "go install -ldflags \"-s\" golang.org/x/tools/gopls@latest", "go install -ldflags <val> golang.org/x/tools/gopls@latest"},
176+
{"install p flag", "go install -p 4 ./cmd/app", "go install -p N <path>"},
177+
178+
// go get edge cases
179+
{"get subshell", "go get $(cat go.mod-dep)", "go get $(cat go.mod-dep)"},
180+
{"get relative path", "go get ../other-module", "go get <path>"},
181+
182+
// go mod edge cases
183+
{"mod edit subshell", "go mod edit -replace $(gen-replace)", "go mod edit -replace $(gen-replace)"},
184+
{"mod subshell positional", "go mod why $(get-dep)", "go mod why $(get-dep)"},
185+
{"mod edit go flag", "go mod edit -go 1.21", "go mod edit -go <val>"},
186+
{"mod edit dropexclude", "go mod edit -dropexclude github.com/old/dep@v1.0", "go mod edit -dropexclude <val>"},
187+
{"mod edit trailing flag", "go mod edit -require", "go mod edit -require"},
188+
189+
// go doc edge cases
190+
{"doc subshell", "go doc $(get-symbol)", "go doc $(get-symbol)"},
191+
192+
// go env edge cases
193+
{"env subshell", "go env $(get-var)", "go env $(get-var)"},
194+
{"env multiple set", "go env -w GOBIN=/usr/local/bin GOPROXY=direct", "go env -w GOBIN=<val> GOPROXY=<val>"},
195+
196+
// go list edge cases
197+
{"list with tags", "go list -tags integration ./...", "go list -tags <val> <path>"},
198+
{"list subshell", "go list $(get-pattern)", "go list $(get-pattern)"},
199+
{"list f subshell", "go list -f $(get-fmt) ./...", "go list -f $(get-fmt) <path>"},
200+
201+
// go tool edge cases
202+
{"tool subshell", "go tool $(get-tool)", "go tool $(get-tool)"},
203+
204+
// go default handler edge cases
205+
{"vet subshell", "go vet $(get-pkg)", "go vet $(get-pkg)"},
206+
{"vet p flag", "go vet -p 4 ./...", "go vet -p N <path>"},
207+
{"fmt subshell", "go fmt $(get-pkg)", "go fmt $(get-pkg)"},
208+
{"generate tags", "go generate -tags wireinject ./...", "go generate -tags <val> <path>"},
209+
{"default subshell flag arg", "go vet -tags $(get-tags) ./...", "go vet -tags $(get-tags) <path>"},
210+
{"default trailing tags", "go vet -tags", "go vet -tags"},
211+
212+
// Unknown fused flag - classifyToken keeps it verbatim since it's a flag
213+
{"test unknown fused flag", "go test -benchmem=true ./...", "go test -benchmem=true <path>"},
214+
215+
// go test subshell in test-specific fused positions
216+
{"test fused timeout subshell", "go test -timeout=30s ./...", "go test -timeout=<duration> <path>"},
217+
218+
// go build subshell in build-specific flag positions
219+
{"build tags subshell", "go build -tags $(get-tags) .", "go build -tags $(get-tags) ."},
220+
{"build subshell positional", "go build $(get-pkg)", "go build $(get-pkg)"},
221+
222+
// go install build flag subshells
223+
{"install tags subshell", "go install -tags $(get-tags) ./cmd/app", "go install -tags $(get-tags) <path>"},
224+
{"install subshell positional", "go install $(get-url)", "go install $(get-url)"},
225+
226+
// go mod subshell in edit flag positions
227+
{"mod edit replace subshell", "go mod edit -replace $(gen-replace)", "go mod edit -replace $(gen-replace)"},
228+
{"mod edit flag subshell", "go mod edit -go $(get-version)", "go mod edit -go $(get-version)"},
229+
{"mod edit boolean flag", "go mod edit -json", "go mod edit -json"},
230+
231+
// go tool subshell and flag handling
232+
{"tool flag before name", "go tool -n pprof", "go tool -n pprof"},
233+
{"tool subshell after name", "go tool pprof $(get-profile)", "go tool pprof $(get-profile)"},
234+
235+
// go install boolean flag
236+
{"install verbose", "go install -v golang.org/x/tools/gopls@latest", "go install -v golang.org/x/tools/gopls@latest"},
237+
{"install parent path", "go install ../other/cmd", "go install <path>"},
238+
239+
// go mod more coverage
240+
{"mod edit boolean only", "go mod edit -print", "go mod edit -print"},
241+
{"mod why subshell", "go mod why $(get-dep)", "go mod why $(get-dep)"},
242+
{"mod edit go subshell", "go mod edit -go $(get-version)", "go mod edit -go $(get-version)"},
243+
244+
// go run boolean flag after positional
245+
{"run race file args", "go run -race main.go --port 8080", "go run -race <path> --port 8080"},
246+
142247
// Edge cases
143248
{"bare go", "go", "go"},
144249
{"version", "go version", "go version"},

handler_helm_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ func TestHelm(t *testing.T) {
5151
{"rollback", "helm rollback my-release 2", "helm rollback my-release N"},
5252
{"status", "helm status my-release", "helm status my-release"},
5353
{"get values", "helm get values my-release", "helm get values my-release"},
54+
55+
// subshell as value-flag argument
56+
{"set with subshell", "helm install my-release bitnami/nginx --set $(get-value)", "helm install my-release <path> --set $(get-value)"},
57+
// subshell as path-flag argument
58+
{"values file subshell", "helm install my-release bitnami/nginx -f $(find-values)", "helm install my-release <path> -f $(find-values)"},
5459
}
5560

5661
for _, tt := range tests {

handler_htop_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,29 @@ func TestHtop(t *testing.T) {
4242
{"delay and sort", "htop -d 20 -s PERCENT_CPU", "htop -d N -s <col>"},
4343
{"tree no color user", "htop -t -C -u admin", "htop -t -C -u <user>"},
4444

45+
// Numeric flag at end of input (no value follows)
46+
{"delay short trailing", "htop -d", "htop -d"},
47+
{"highlight trailing", "htop -H", "htop -H"},
48+
49+
// Value flag at end of input (no value follows)
50+
{"user short trailing", "htop -u", "htop -u"},
51+
{"pid short trailing", "htop -p", "htop -p"},
52+
{"sort short trailing", "htop -s", "htop -s"},
53+
{"filter short trailing", "htop -F", "htop -F"},
54+
55+
// Numeric flag with subshell value
56+
{"delay subshell", "htop -d $(echo 10)", "htop -d $(echo <str>)"},
57+
{"highlight subshell", "htop --highlight-changes $(calc)", "htop --highlight-changes $(calc)"},
58+
59+
// Value flag with subshell value
60+
{"user subshell", "htop -u $(whoami)", "htop -u $(whoami)"},
61+
{"pid subshell", "htop -p $(pgrep foo)", "htop -p $(pgrep foo)"},
62+
{"sort subshell", "htop --sort-key $(echo CPU)", "htop --sort-key $(echo <str>)"},
63+
{"filter subshell", "htop -F $(echo nginx)", "htop -F $(echo <str>)"},
64+
65+
// Subshell as standalone token
66+
{"subshell standalone", "htop $(flags)", "htop $(flags)"},
67+
4568
// Redirect
4669
{"redirect", "htop -p 123 > out.txt", "htop -p <pid> > <path>"},
4770
}

handler_ip_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ func TestIP(t *testing.T) {
5252
// tunnel
5353
{"tunnel show", "ip tunnel show", "ip tunnel show"},
5454

55+
// subshell as globalValFlag argument
56+
{"family subshell", "ip -f $(get-family) addr show", "ip -f $(get-family) addr show"},
57+
// subshell as valKeyword argument
58+
{"dev subshell", "ip link set dev $(get-dev) up", "ip link set dev $(get-dev) up"},
59+
// subshell as numKeyword argument
60+
{"mtu subshell", "ip link set dev eth0 mtu $(get-mtu)", "ip link set dev <val> mtu $(get-mtu)"},
61+
// subshell as addrKeyword argument
62+
{"address subshell", "ip link set dev eth0 address $(get-mac)", "ip link set dev <val> address $(get-mac)"},
63+
5564
// redirect preserved
5665
{"with redirect", "ip addr show > /tmp/out.txt", "ip addr show > <path>"},
5766
}

handler_iptables_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ func TestIptables(t *testing.T) {
4949

5050
// Destination and source together
5151
{"src and dst", "iptables -A FORWARD -s 10.0.0.0/8 -d 172.16.0.0/12 -j ACCEPT", "iptables -A FORWARD -s <addr> -d <addr> -j ACCEPT"},
52+
53+
// Subshell as standalone token in args
54+
{"subshell standalone", "iptables $(get-flags) -j ACCEPT", "iptables $(get-flags) -j ACCEPT"},
55+
// Subshell as verbatim flag argument
56+
{"subshell verbatim arg", "iptables -A INPUT -p $(get-proto) -j ACCEPT", "iptables -A INPUT -p $(get-proto) -j ACCEPT"},
57+
// Subshell as val flag argument
58+
{"subshell val arg", "iptables -A INPUT --dport $(get-port) -j ACCEPT", "iptables -A INPUT --dport $(get-port) -j ACCEPT"},
59+
// Subshell as str flag argument
60+
{"subshell str arg", "iptables -A INPUT -m comment --comment $(gen-comment) -j ACCEPT", "iptables -A INPUT -m comment --comment $(gen-comment) -j ACCEPT"},
5261
}
5362
for _, tt := range tests {
5463
t.Run(tt.name, func(t *testing.T) {

handler_kill_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ func TestKill(t *testing.T) {
2020
// List mode
2121
{"list signals", "kill -l", "kill -l"},
2222
{"list with exit status", "kill -l 1", "kill -l N"},
23+
// Bare dash (not a numeric signal, treated as positional)
24+
{"bare dash", "kill - 1234", "kill <pid>+"},
25+
// Mixed digit-letter flag (not a valid numeric signal)
26+
{"invalid numeric signal", "kill -9a 1234", "kill <pid>+"},
27+
// Double-dash with subshell
28+
{"double dash subshell", "kill -- $(pgrep nginx)", "kill -- $(pgrep nginx)"},
29+
// Subshell as standalone token
30+
{"subshell standalone", "kill $(pgrep nginx)", "kill $(pgrep nginx)"},
31+
// -s with no following token
32+
{"dash-s trailing", "kill -s", "kill -s"},
33+
// -l followed by a letter flag (does not consume)
34+
{"dash-l followed by flag", "kill -l -HUP", "kill -l -HUP"},
2335
// Special pids
2436
{"job spec", "kill %1", "kill <pid>"},
2537
{"double dash", "kill -- -1", "kill -- <pid>"},

0 commit comments

Comments
 (0)