Skip to content

Commit 0a067f0

Browse files
committed
feat: implement robust command pattern extraction using shell-quote
- Replace regex-based parsing with shell-quote library for deterministic parsing - Handle complex shell syntax including quotes, escapes, and special characters - Maintain backward compatibility with existing command patterns - Add comprehensive test coverage for edge cases - Addresses PR feedback about regex limitations and parsing reliability The shell-quote library provides proper shell command parsing that handles: - Single and double quotes with proper escape sequences - Environment variable expansion - Command operators (&&, ||, |, ;) - Glob patterns and special characters - Nested quotes and complex argument structures This ensures commands are extracted exactly as they would be interpreted by a shell.
1 parent f903871 commit 0a067f0

File tree

2 files changed

+399
-175
lines changed

2 files changed

+399
-175
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { describe, it, expect } from "vitest"
2+
import { extractCommandPatterns, extractCommandPattern, getPatternDescription } from "../commandPatterns"
3+
4+
describe("commandPatterns", () => {
5+
describe("extractCommandPatterns", () => {
6+
it("should handle empty or null commands", () => {
7+
expect(extractCommandPatterns("")).toEqual([])
8+
expect(extractCommandPatterns(" ")).toEqual([])
9+
expect(extractCommandPatterns(null as any)).toEqual([])
10+
expect(extractCommandPatterns(undefined as any)).toEqual([])
11+
})
12+
13+
it("should extract simple commands", () => {
14+
expect(extractCommandPatterns("ls")).toEqual(["ls"])
15+
expect(extractCommandPatterns("pwd")).toEqual(["pwd"])
16+
expect(extractCommandPatterns("echo hello")).toEqual(["echo"])
17+
})
18+
19+
it("should handle npm commands correctly", () => {
20+
expect(extractCommandPatterns("npm install")).toEqual(["npm install"])
21+
expect(extractCommandPatterns("npm run build")).toEqual(["npm run"])
22+
expect(extractCommandPatterns("npm run test:unit")).toEqual(["npm run"])
23+
expect(extractCommandPatterns("npm test")).toEqual(["npm test"])
24+
expect(extractCommandPatterns("npm run build -- --watch")).toEqual(["npm run"])
25+
})
26+
27+
it("should handle yarn/pnpm/bun commands", () => {
28+
expect(extractCommandPatterns("yarn install")).toEqual(["yarn install"])
29+
expect(extractCommandPatterns("pnpm run dev")).toEqual(["pnpm run"])
30+
expect(extractCommandPatterns("bun test")).toEqual(["bun test"])
31+
})
32+
33+
it("should handle git commands", () => {
34+
expect(extractCommandPatterns("git status")).toEqual(["git status"])
35+
expect(extractCommandPatterns('git commit -m "message"')).toEqual(["git commit"])
36+
expect(extractCommandPatterns("git push origin main")).toEqual(["git push"])
37+
expect(extractCommandPatterns("git log --oneline")).toEqual(["git log"])
38+
})
39+
40+
it("should handle docker/kubectl/helm commands", () => {
41+
expect(extractCommandPatterns("docker run nginx")).toEqual(["docker run"])
42+
expect(extractCommandPatterns("kubectl get pods")).toEqual(["kubectl get"])
43+
expect(extractCommandPatterns("helm install myapp ./chart")).toEqual(["helm install"])
44+
})
45+
46+
it("should handle make commands", () => {
47+
expect(extractCommandPatterns("make build")).toEqual(["make build"])
48+
expect(extractCommandPatterns("make test")).toEqual(["make test"])
49+
expect(extractCommandPatterns("make -j4 all")).toEqual(["make"])
50+
})
51+
52+
it("should handle interpreter commands", () => {
53+
expect(extractCommandPatterns("python script.py")).toEqual(["python"])
54+
expect(extractCommandPatterns("python3 -m venv env")).toEqual(["python3"])
55+
expect(extractCommandPatterns("node index.js --port 3000")).toEqual(["node"])
56+
expect(extractCommandPatterns("ruby app.rb")).toEqual(["ruby"])
57+
})
58+
59+
it("should handle dangerous commands", () => {
60+
expect(extractCommandPatterns("rm -rf node_modules")).toEqual(["rm"])
61+
expect(extractCommandPatterns("chmod 755 script.sh")).toEqual(["chmod"])
62+
expect(extractCommandPatterns('find . -name "*.log" -delete')).toEqual(["find"])
63+
})
64+
65+
it("should handle cd commands", () => {
66+
expect(extractCommandPatterns("cd /home/user")).toEqual(["cd"])
67+
expect(extractCommandPatterns("cd ..")).toEqual(["cd"])
68+
expect(extractCommandPatterns("cd ~/projects")).toEqual(["cd"])
69+
})
70+
71+
it("should handle chained commands with &&", () => {
72+
const patterns = extractCommandPatterns("npm install && npm run build")
73+
expect(patterns).toEqual(["npm install", "npm run"])
74+
})
75+
76+
it("should handle chained commands with ||", () => {
77+
const patterns = extractCommandPatterns('npm test || echo "Tests failed"')
78+
expect(patterns).toEqual(["echo", "npm test"])
79+
})
80+
81+
it("should handle chained commands with ;", () => {
82+
const patterns = extractCommandPatterns("cd /tmp; ls -la; pwd")
83+
expect(patterns).toEqual(["cd", "ls", "pwd"])
84+
})
85+
86+
it("should handle piped commands", () => {
87+
const patterns = extractCommandPatterns("ps aux | grep node")
88+
expect(patterns).toEqual(["grep", "ps"])
89+
})
90+
91+
it("should handle complex chained commands", () => {
92+
const patterns = extractCommandPatterns('git pull && npm install && npm run build || echo "Build failed"')
93+
expect(patterns).toEqual(["echo", "git pull", "npm install", "npm run"])
94+
})
95+
96+
it("should handle quoted arguments correctly", () => {
97+
expect(extractCommandPatterns('echo "hello world"')).toEqual(["echo"])
98+
expect(extractCommandPatterns("echo 'hello world'")).toEqual(["echo"])
99+
expect(extractCommandPatterns('git commit -m "feat: add new feature"')).toEqual(["git commit"])
100+
})
101+
102+
it("should handle escaped characters", () => {
103+
expect(extractCommandPatterns("echo hello\\ world")).toEqual(["echo"])
104+
expect(extractCommandPatterns('echo "hello \\"world\\""')).toEqual(["echo"])
105+
})
106+
107+
it("should handle environment variables", () => {
108+
expect(extractCommandPatterns("NODE_ENV=production npm start")).toEqual(["npm start"])
109+
expect(extractCommandPatterns("PORT=3000 node server.js")).toEqual(["node"])
110+
})
111+
112+
it("should handle script files", () => {
113+
expect(extractCommandPatterns("./deploy.sh")).toEqual(["./deploy.sh"])
114+
expect(extractCommandPatterns("/usr/local/bin/script.py")).toEqual(["/usr/local/bin/script.py"])
115+
expect(extractCommandPatterns("./scripts/test.js --verbose")).toEqual(["./scripts/test.js"])
116+
})
117+
118+
it("should handle complex npm scripts", () => {
119+
expect(extractCommandPatterns("npm run build:prod -- --source-maps")).toEqual(["npm run"])
120+
expect(extractCommandPatterns("npm run test:coverage -- --watch")).toEqual(["npm run"])
121+
})
122+
123+
it("should return unique sorted patterns", () => {
124+
const patterns = extractCommandPatterns("npm install && npm install && npm run build")
125+
expect(patterns).toEqual(["npm install", "npm run"])
126+
})
127+
128+
it("should handle commands with glob patterns", () => {
129+
expect(extractCommandPatterns("rm *.log")).toEqual(["rm"])
130+
expect(extractCommandPatterns("ls *.{js,ts}")).toEqual(["ls"])
131+
})
132+
133+
it("should handle commands with redirects", () => {
134+
expect(extractCommandPatterns('echo "test" > file.txt')).toEqual(["echo"])
135+
expect(extractCommandPatterns("cat file.txt >> output.log")).toEqual(["cat"])
136+
})
137+
138+
it("should handle subshells and command substitution", () => {
139+
expect(extractCommandPatterns("echo $(date)")).toEqual(["echo"])
140+
expect(extractCommandPatterns("echo `pwd`")).toEqual(["echo"])
141+
})
142+
})
143+
144+
describe("extractCommandPattern", () => {
145+
it("should return the first pattern for backward compatibility", () => {
146+
expect(extractCommandPattern("npm install && npm run build")).toBe("npm install")
147+
expect(extractCommandPattern("git status")).toBe("git status")
148+
expect(extractCommandPattern("")).toBe("")
149+
})
150+
})
151+
152+
describe("getPatternDescription", () => {
153+
it("should describe npm patterns", () => {
154+
expect(getPatternDescription("npm run")).toBe("all npm run scripts")
155+
expect(getPatternDescription("npm install")).toBe("npm install commands")
156+
expect(getPatternDescription("npm test")).toBe("npm test commands")
157+
})
158+
159+
it("should describe git patterns", () => {
160+
expect(getPatternDescription("git commit")).toBe("git commit commands")
161+
expect(getPatternDescription("git push")).toBe("git push commands")
162+
})
163+
164+
it("should describe script files", () => {
165+
expect(getPatternDescription("./deploy.sh")).toBe("this specific script")
166+
expect(getPatternDescription("/usr/bin/script.py")).toBe("this specific script")
167+
})
168+
169+
it("should describe interpreter patterns", () => {
170+
expect(getPatternDescription("python")).toBe("python scripts")
171+
expect(getPatternDescription("node")).toBe("node scripts")
172+
expect(getPatternDescription("ruby")).toBe("ruby scripts")
173+
})
174+
175+
it("should describe docker/kubectl patterns", () => {
176+
expect(getPatternDescription("docker run")).toBe("docker run commands")
177+
expect(getPatternDescription("kubectl get")).toBe("kubectl get commands")
178+
})
179+
180+
it("should describe make patterns", () => {
181+
expect(getPatternDescription("make build")).toBe("make build target")
182+
expect(getPatternDescription("make test")).toBe("make test target")
183+
})
184+
185+
it("should describe cd pattern", () => {
186+
expect(getPatternDescription("cd")).toBe("directory navigation")
187+
})
188+
189+
it("should handle empty patterns", () => {
190+
expect(getPatternDescription("")).toBe("")
191+
})
192+
193+
it("should provide default description", () => {
194+
expect(getPatternDescription("custom-command")).toBe("custom-command commands")
195+
})
196+
})
197+
})

0 commit comments

Comments
 (0)