Skip to content

Commit e3bba58

Browse files
committed
add-mvn-java-dotnet-handlers
Add specialized handlers for several major build tools and runtimes: - Java/Javac: Handle classpath, module flags, and system properties - Maven: Preserve phases/goals, collapse -D properties and -P profiles - Gradle: Keep tasks structural, handle -D/-P properties and path flags - .NET: Support subcommands (build, run, test), MSBuild properties, and NuGet - Deno: Handle permissions, subcommands like task/eval, and URL scripts - CMake: Support configure/build/install modes and -D cache variables - Ansible: Handle ad-hoc and playbook commands with inventory/module flags - HTTP Benchmarking: Support ab, wrk, and hey with numeric/header flags - HTTPie: Collapse request items (key=val) and handle auth/output flags - Database Shells: Support mongo/mongosh, mysql, psql, and redis-cli - Environment/Process: Support env, ldd, otool, and strace - Others: fly/flyctl, just, perl, ruby, swift, task, and vagrant These handlers ensure that structural elements (like task names or subcommands) are preserved while data (like paths, URLs, and property values) is collapsed to improve command grouping and analysis.
1 parent f13f13e commit e3bba58

58 files changed

Lines changed: 7450 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

handler_ansible.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package shellshape
2+
3+
func init() {
4+
Register("ansible", handleAnsibleAdhoc)
5+
Register("ansible-playbook", handleAnsiblePlaybook)
6+
}
7+
8+
var ansibleCategories = []flagCategory{
9+
{map[string]bool{
10+
"-a": true, "--args": true,
11+
"-e": true, "--extra-vars": true,
12+
"-u": true, "--user": true,
13+
"--become-user": true,
14+
"--become-method": true,
15+
"--tags": true,
16+
"--skip-tags": true,
17+
"--start-at": true,
18+
"--start-at-task": true,
19+
"--connection": true,
20+
"--ssh-extra-args": true,
21+
"--ssh-common-args": true,
22+
"--scp-extra-args": true,
23+
"--sftp-extra-args": true,
24+
}, "<val>"},
25+
{map[string]bool{
26+
"-i": true, "--inventory": true, "--inventory-file": true,
27+
"--private-key": true, "--key-file": true,
28+
"--vault-password-file": true, "--vault-id": true,
29+
}, "<path>"},
30+
{map[string]bool{
31+
"-m": true, "--module-name": true,
32+
}, "<module>"},
33+
{map[string]bool{
34+
"-f": true, "--forks": true,
35+
"-t": true, "--timeout": true,
36+
}, "N"},
37+
{map[string]bool{
38+
"-l": true, "--limit": true,
39+
}, "<target>"},
40+
}
41+
42+
// handleAnsibleAdhoc handles the `ansible` ad-hoc command.
43+
// First positional is the host pattern (<target>), remaining positionals
44+
// use classifyToken. Module flags (-m) collapse to <module>.
45+
func handleAnsibleAdhoc(subcommand string, tokens []string) []string {
46+
args, redirects := splitRedirects(tokens)
47+
48+
var result []string
49+
firstPositional := true
50+
51+
i := 0
52+
for i < len(args) {
53+
tok := args[i]
54+
55+
if isSubshellToken(tok) {
56+
result = append(result, tok)
57+
firstPositional = false
58+
i++
59+
continue
60+
}
61+
62+
if placeholder, ok := matchFlagCategory(tok, ansibleCategories); ok {
63+
result, i = consumeFlagArg(tok, args, i, result, placeholder)
64+
continue
65+
}
66+
67+
if fused, ok := consumeFusedFlag(tok, ansibleCategories); ok {
68+
result = append(result, fused)
69+
i++
70+
continue
71+
}
72+
73+
if isFlagToken(tok) {
74+
result = append(result, tok)
75+
i++
76+
continue
77+
}
78+
79+
// First positional is the host pattern.
80+
if firstPositional {
81+
result = append(result, "<target>")
82+
firstPositional = false
83+
} else {
84+
result = append(result, classifyToken(tok))
85+
}
86+
i++
87+
}
88+
89+
result = append(result, redirects...)
90+
return result
91+
}
92+
93+
// handleAnsiblePlaybook handles the `ansible-playbook` command.
94+
// All positionals are playbook files (classifyToken). Shares flag
95+
// categories with the ad-hoc handler.
96+
func handleAnsiblePlaybook(subcommand string, tokens []string) []string {
97+
args, redirects := splitRedirects(tokens)
98+
99+
var result []string
100+
101+
i := 0
102+
for i < len(args) {
103+
tok := args[i]
104+
105+
if isSubshellToken(tok) {
106+
result = append(result, tok)
107+
i++
108+
continue
109+
}
110+
111+
if placeholder, ok := matchFlagCategory(tok, ansibleCategories); ok {
112+
result, i = consumeFlagArg(tok, args, i, result, placeholder)
113+
continue
114+
}
115+
116+
if fused, ok := consumeFusedFlag(tok, ansibleCategories); ok {
117+
result = append(result, fused)
118+
i++
119+
continue
120+
}
121+
122+
if isFlagToken(tok) {
123+
result = append(result, tok)
124+
i++
125+
continue
126+
}
127+
128+
// All positionals are playbook files.
129+
result = append(result, classifyToken(tok))
130+
i++
131+
}
132+
133+
result = append(result, redirects...)
134+
return result
135+
}

handler_ansible_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package shellshape
2+
3+
import "testing"
4+
5+
func TestAnsible(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
input string
9+
want string
10+
}{
11+
// Basic ansible usage
12+
{"ping all", "ansible all -m ping", "ansible <target> -m <module>"},
13+
{"shell module with args", "ansible webservers -m shell -a 'uptime'", "ansible <target> -m <module> -a <val>"},
14+
{"command module with args", "ansible dbservers -m command -a 'df -h'", "ansible <target> -m <module> -a <val>"},
15+
{"with inventory", "ansible all -i hosts.ini -m ping", "ansible <target> -i <path> -m <module>"},
16+
{"with user", "ansible all -u deploy -m setup", "ansible <target> -u <val> -m <module>"},
17+
{"become flags", "ansible all --become --ask-become-pass -m command -a 'whoami'", "ansible <target> --become --ask-become-pass -m <module> -a <val>"},
18+
{"with limit", "ansible all -l webservers -m ping", "ansible <target> -l <target> -m <module>"},
19+
{"with extra vars", "ansible all -e 'foo=bar' -m debug -a 'var=foo'", "ansible <target> -e <val> -m <module> -a <val>"},
20+
{"with forks", "ansible all -f 20 -m ping", "ansible <target> -f N -m <module>"},
21+
{"with timeout", "ansible all -t 30 -m ping", "ansible <target> -t N -m <module>"},
22+
{"with private key", "ansible all --private-key /home/user/.ssh/id_rsa -m ping", "ansible <target> --private-key <path> -m <module>"},
23+
{"verbose", "ansible all -vvv -m ping", "ansible <target> -vvv -m <module>"},
24+
{"list hosts", "ansible webservers --list-hosts", "ansible <target> --list-hosts"},
25+
26+
// ansible-playbook usage
27+
{"playbook simple", "ansible-playbook site.yml", "ansible-playbook <path>"},
28+
{"playbook with inventory", "ansible-playbook -i inventory/prod deploy.yml", "ansible-playbook -i <path>+"},
29+
{"playbook with extra vars", "ansible-playbook playbook.yml -e 'version=1.2'", "ansible-playbook <path> -e <val>"},
30+
{"playbook with tags", "ansible-playbook playbook.yml --tags deploy,setup", "ansible-playbook <path> --tags <val>"},
31+
{"playbook with skip tags", "ansible-playbook playbook.yml --skip-tags slow", "ansible-playbook <path> --skip-tags <val>"},
32+
{"playbook with limit", "ansible-playbook playbook.yml --limit staging", "ansible-playbook <path> --limit <target>"},
33+
{"playbook with start-at", "ansible-playbook playbook.yml --start-at 'Install nginx'", "ansible-playbook <path> --start-at <val>"},
34+
{"playbook check mode", "ansible-playbook playbook.yml -C -D", "ansible-playbook <path> -C -D"},
35+
{"playbook with forks and user", "ansible-playbook playbook.yml -f 10 -u deploy", "ansible-playbook <path> -f N -u <val>"},
36+
{"playbook with vault", "ansible-playbook playbook.yml --vault-password-file /tmp/vault.txt", "ansible-playbook <path> --vault-password-file <path>"},
37+
{"playbook with become user", "ansible-playbook playbook.yml -b --become-user root", "ansible-playbook <path> -b --become-user <val>"},
38+
{"playbook extra vars json file", "ansible-playbook playbook.yml -e @variables.json", "ansible-playbook <path> -e <val>"},
39+
{"playbook multiple plays", "ansible-playbook site.yml deploy.yml", "ansible-playbook <path>+"},
40+
}
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
got := Normalize(tt.input)
44+
if got != tt.want {
45+
t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)
46+
}
47+
})
48+
}
49+
50+
// COLLISION TESTS
51+
t.Run("different hosts collide", func(t *testing.T) {
52+
a := Normalize("ansible webservers -m ping")
53+
b := Normalize("ansible dbservers -m ping")
54+
if a != b {
55+
t.Errorf("expected %q == %q", a, b)
56+
}
57+
})
58+
59+
t.Run("different playbooks collide", func(t *testing.T) {
60+
a := Normalize("ansible-playbook site.yml -e 'env=prod'")
61+
b := Normalize("ansible-playbook deploy.yml -e 'env=staging'")
62+
if a != b {
63+
t.Errorf("expected %q == %q", a, b)
64+
}
65+
})
66+
67+
// SAFETY TEST: subshell must not collapse
68+
t.Run("subshell not collapsed", func(t *testing.T) {
69+
benign := Normalize("ansible all -m ping")
70+
subshell := Normalize("ansible $(cat hosts) -m ping")
71+
if benign == subshell {
72+
t.Error("subshell must produce different shape than literal")
73+
}
74+
})
75+
}

handler_clang.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package shellshape
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
func init() {
9+
for _, name := range []string{"clang", "clang++", "gcc", "g++", "cc", "c++"} {
10+
Register(name, handleClang)
11+
}
12+
}
13+
14+
// Fused short-flag prefixes: -I<path>, -L<path>, -D<def>, -U<def>, -l<lib>.
15+
var clangFusedShortRE = regexp.MustCompile(`^-(I|L)(.+)$`)
16+
var clangFusedDefRE = regexp.MustCompile(`^-(D|U)(.+)$`)
17+
var clangFusedLibRE = regexp.MustCompile(`^-l(.+)$`)
18+
19+
// Fused long-flag=value patterns.
20+
var clangFusedLongRE = regexp.MustCompile(`^(-std|-march|-mcpu|-mtune|-mabi|-mfpu|-mfloat-abi|--target)=(.+)$`)
21+
var clangFusedLongPathRE = regexp.MustCompile(`^(--sysroot)=(.+)$`)
22+
23+
// handleClang handles clang, clang++, gcc, g++, cc, c++.
24+
//
25+
// Path flags (-o, -I, -L, -isystem, etc.) collapse the next arg to <path>.
26+
// Define flags (-D, -U) collapse the next arg to <def>.
27+
// Value flags (-l, -std, --target, -arch, -x, etc.) collapse the next arg to <val>.
28+
// Fused forms like -I/path, -DFOO, -lm, -std=c++17, --target=... are split and collapsed.
29+
// Optimization flags (-O0, -O2, -Os, etc.) and warning flags (-W...) are kept verbatim.
30+
// Positionals use classifyToken.
31+
func handleClang(subcommand string, tokens []string) []string {
32+
args, redirects := splitRedirects(tokens)
33+
34+
pathFlags := map[string]bool{
35+
"-o": true, "--output": true,
36+
"-I": true, "-L": true,
37+
"-isystem": true, "-isysroot": true,
38+
"-iquote": true, "-idirafter": true,
39+
"-iprefix": true, "-iwithprefix": true, "-iwithprefixbefore": true,
40+
"-F": true, "-B": true,
41+
"-MF": true, "-MQ": true, "-MT": true,
42+
"-include": true, "-imacros": true,
43+
}
44+
45+
defFlags := map[string]bool{
46+
"-D": true, "-U": true,
47+
}
48+
49+
valFlags := map[string]bool{
50+
"-l": true, "-Xlinker": true,
51+
"-target": true, "--target": true,
52+
"-arch": true, "-march": true, "-mcpu": true, "-mtune": true,
53+
"-mabi": true, "-mfpu": true, "-mfloat-abi": true,
54+
"-std": true, "--std": true,
55+
"-x": true, "-T": true, "-e": true,
56+
"--serialize-diagnostics": true, "-MJ": true,
57+
"-rpath": true,
58+
}
59+
60+
categories := []flagCategory{
61+
{pathFlags, "<path>"},
62+
{defFlags, "<def>"},
63+
{valFlags, "<val>"},
64+
}
65+
66+
var result []string
67+
i := 0
68+
for i < len(args) {
69+
tok := args[i]
70+
71+
if isSubshellToken(tok) {
72+
result = append(result, tok)
73+
i++
74+
continue
75+
}
76+
77+
// Fused short flags: -I/path, -L/path
78+
if m := clangFusedShortRE.FindStringSubmatch(tok); m != nil {
79+
result = append(result, "-"+m[1], "<path>")
80+
i++
81+
continue
82+
}
83+
84+
// Fused define flags: -DFOO, -UFOO
85+
if m := clangFusedDefRE.FindStringSubmatch(tok); m != nil {
86+
result = append(result, "-"+m[1], "<def>")
87+
i++
88+
continue
89+
}
90+
91+
// Fused library: -lfoo
92+
if m := clangFusedLibRE.FindStringSubmatch(tok); m != nil {
93+
result = append(result, "-l", "<val>")
94+
i++
95+
continue
96+
}
97+
98+
// Fused long flag=value (val): -std=c++17, --target=..., -march=...
99+
if m := clangFusedLongRE.FindStringSubmatch(tok); m != nil {
100+
result = append(result, m[1]+"=<val>")
101+
i++
102+
continue
103+
}
104+
105+
// Fused long flag=value (path): --sysroot=...
106+
if m := clangFusedLongPathRE.FindStringSubmatch(tok); m != nil {
107+
result = append(result, m[1]+"=<path>")
108+
i++
109+
continue
110+
}
111+
112+
// Space-separated flag categories
113+
if placeholder, ok := matchFlagCategory(tok, categories); ok {
114+
result, i = consumeFlagArg(tok, args, i, result, placeholder)
115+
continue
116+
}
117+
118+
// Other flags (including -O*, -W*, -f*, -m (boolean), -g, -c, -S, -E, etc.)
119+
if isFlagToken(tok) {
120+
// Check for --flag=value on remaining long flags
121+
if strings.Contains(tok, "=") {
122+
eqIdx := strings.IndexByte(tok, '=')
123+
key := tok[:eqIdx]
124+
if pathFlags[key] {
125+
result = append(result, key+"=<path>")
126+
} else if defFlags[key] {
127+
result = append(result, key+"=<def>")
128+
} else if valFlags[key] {
129+
result = append(result, key+"=<val>")
130+
} else {
131+
result = append(result, tok)
132+
}
133+
i++
134+
continue
135+
}
136+
result = append(result, tok)
137+
i++
138+
continue
139+
}
140+
141+
// Positional: source files, object files, etc.
142+
result = append(result, classifyToken(tok))
143+
i++
144+
}
145+
146+
result = append(result, redirects...)
147+
return result
148+
}

0 commit comments

Comments
 (0)