Skip to content

Commit b8e990f

Browse files
dugshubclaude
andcommitted
CLI-8: Implement TextParser and ShellParser
- TextParser: Handles standard commands with flags and options - Proper quote handling using shlex - Support for short flags (-a, -abc) and long options (--key=value) - Comprehensive error handling with suggestions - ShellParser: Handles shell command passthrough - Commands prefixed with ! are passed to shell - Preserves pipes, redirects, and complex shell syntax - Provides shell command suggestions Both parsers implement the Parser protocol interface. 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 581606d commit b8e990f

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
"""Parser implementations for text and shell commands."""
2+
3+
from __future__ import annotations
4+
5+
import shlex
6+
7+
from cli_patterns.ui.parser.types import Context, ParseError, ParseResult
8+
9+
10+
class TextParser:
11+
"""Parser for standard text-based commands with flags and options.
12+
13+
Handles parsing of commands with arguments, short/long flags, and key-value options.
14+
Supports proper quote handling using shlex for shell-like parsing.
15+
"""
16+
17+
def can_parse(self, input: str, context: Context) -> bool:
18+
"""Check if input can be parsed by this text parser.
19+
20+
Args:
21+
input: Input string to check
22+
context: Parsing context
23+
24+
Returns:
25+
True if input is non-empty text that doesn't start with shell prefix
26+
"""
27+
if not input or not input.strip():
28+
return False
29+
30+
# Don't handle shell commands (those start with !)
31+
if input.lstrip().startswith("!"):
32+
return False
33+
34+
return True
35+
36+
def parse(self, input: str, context: Context) -> ParseResult:
37+
"""Parse text input into structured command result.
38+
39+
Args:
40+
input: Input string to parse
41+
context: Parsing context
42+
43+
Returns:
44+
ParseResult with parsed command, args, flags, and options
45+
46+
Raises:
47+
ParseError: If parsing fails (e.g., unmatched quotes, empty input)
48+
"""
49+
if not self.can_parse(input, context):
50+
if not input.strip():
51+
raise ParseError(
52+
error_type="EMPTY_INPUT",
53+
message="Empty input cannot be parsed",
54+
suggestions=["Enter a command to execute"],
55+
)
56+
else:
57+
raise ParseError(
58+
error_type="INVALID_INPUT",
59+
message="Input cannot be parsed by text parser",
60+
suggestions=["Check command format"],
61+
)
62+
63+
try:
64+
# Use shlex for proper quote handling
65+
tokens = shlex.split(input.strip())
66+
except ValueError as e:
67+
# Handle shlex errors (e.g., unmatched quotes)
68+
error_msg = str(e).replace("quotation", "quote")
69+
raise ParseError(
70+
error_type="QUOTE_MISMATCH",
71+
message=f"Syntax error in command: {error_msg}",
72+
suggestions=["Check quote pairing", "Escape special characters"],
73+
) from e
74+
75+
if not tokens:
76+
raise ParseError(
77+
error_type="EMPTY_INPUT",
78+
message="No command found after parsing",
79+
suggestions=["Enter a valid command"],
80+
)
81+
82+
# First token is the command
83+
command = tokens[0]
84+
85+
# Parse remaining tokens into args, flags, and options
86+
args = []
87+
flags = set()
88+
options = {}
89+
90+
i = 1
91+
while i < len(tokens):
92+
token = tokens[i]
93+
94+
if token.startswith("--"):
95+
# Long option handling
96+
if "=" in token:
97+
# Format: --key=value
98+
key_value = token[2:] # Remove --
99+
if "=" in key_value:
100+
key, value = key_value.split("=", 1)
101+
options[key] = value
102+
else:
103+
# Format: --key value (next token is value)
104+
key = token[2:] # Remove --
105+
if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"):
106+
options[key] = tokens[i + 1]
107+
i += 1 # Skip the value token
108+
else:
109+
# Treat as flag if no value follows
110+
flags.add(key)
111+
112+
elif token.startswith("-") and len(token) > 1:
113+
# Short flag(s) handling
114+
flag_chars = token[1:] # Remove -
115+
for char in flag_chars:
116+
flags.add(char)
117+
118+
else:
119+
# Regular argument
120+
args.append(token)
121+
122+
i += 1
123+
124+
return ParseResult(
125+
command=command, args=args, flags=flags, options=options, raw_input=input
126+
)
127+
128+
def get_suggestions(self, partial: str) -> list[str]:
129+
"""Get completion suggestions for partial input.
130+
131+
Args:
132+
partial: Partial input to complete
133+
134+
Returns:
135+
List of completion suggestions (empty for base implementation)
136+
"""
137+
# Base implementation returns no suggestions
138+
# This could be extended to provide command suggestions
139+
return []
140+
141+
142+
class ShellParser:
143+
"""Parser for shell commands prefixed with '!'.
144+
145+
Handles commands that should be executed directly in the shell,
146+
preserving the full command after the '!' prefix.
147+
"""
148+
149+
def can_parse(self, input: str, context: Context) -> bool:
150+
"""Check if input is a shell command.
151+
152+
Args:
153+
input: Input string to check
154+
context: Parsing context
155+
156+
Returns:
157+
True if input starts with '!' and has content after it
158+
"""
159+
if not input or not input.strip():
160+
return False
161+
162+
stripped = input.strip()
163+
164+
# Must start with ! and have content after it
165+
if not stripped.startswith("!"):
166+
return False
167+
168+
# Must have content after the !
169+
shell_content = stripped[1:].strip()
170+
return len(shell_content) > 0
171+
172+
def parse(self, input: str, context: Context) -> ParseResult:
173+
"""Parse shell command input.
174+
175+
Args:
176+
input: Input string starting with '!'
177+
context: Parsing context
178+
179+
Returns:
180+
ParseResult with '!' as command and shell command preserved
181+
182+
Raises:
183+
ParseError: If input is not a valid shell command
184+
"""
185+
if not self.can_parse(input, context):
186+
if not input.strip():
187+
raise ParseError(
188+
error_type="EMPTY_INPUT",
189+
message="Empty input cannot be parsed",
190+
suggestions=["Enter a shell command prefixed with '!'"],
191+
)
192+
elif not input.strip().startswith("!"):
193+
raise ParseError(
194+
error_type="NOT_SHELL_COMMAND",
195+
message="Not a shell command (must start with '!')",
196+
suggestions=["Use '!' prefix for shell commands"],
197+
)
198+
else:
199+
raise ParseError(
200+
error_type="EMPTY_SHELL_COMMAND",
201+
message="Shell command prefix found but no command specified",
202+
suggestions=["Add a command after the '!' prefix"],
203+
)
204+
205+
stripped = input.strip()
206+
shell_command = stripped[1:].strip() # Remove ! prefix
207+
208+
return ParseResult(
209+
command="!",
210+
args=[], # Shell parser doesn't break down the shell command
211+
flags=set(),
212+
options={},
213+
raw_input=input,
214+
shell_command=shell_command,
215+
)
216+
217+
def get_suggestions(self, partial: str) -> list[str]:
218+
"""Get completion suggestions for partial shell input.
219+
220+
Args:
221+
partial: Partial input to complete
222+
223+
Returns:
224+
List of shell command suggestions
225+
"""
226+
# Base implementation for shell commands
227+
if not partial.startswith("!"):
228+
# For empty or non-shell input, suggest shell prefix
229+
return ["!ls", "!pwd", "!ps", "!grep", "!find"]
230+
231+
# Could suggest common shell commands
232+
shell_partial = partial[1:].strip()
233+
if not shell_partial:
234+
return ["!ls", "!pwd", "!ps", "!grep", "!find"]
235+
236+
common_commands = [
237+
"ls",
238+
"pwd",
239+
"ps",
240+
"grep",
241+
"find",
242+
"cat",
243+
"less",
244+
"head",
245+
"tail",
246+
]
247+
suggestions = []
248+
249+
for cmd in common_commands:
250+
if cmd.startswith(shell_partial):
251+
suggestions.append(f"!{cmd}")
252+
253+
return suggestions

0 commit comments

Comments
 (0)