@@ -111,7 +111,7 @@ class ExecutableApp(Protocol):
111111 class MyApp(ExecutableApp):
112112 @property
113113 def command(self) -> Command:
114- return Command(cmd="echo hello", output_parser=identity_parser)
114+ return Command(cmd=[ "echo", " hello"] , output_parser=identity_parser)
115115 ```
116116 """
117117
@@ -170,7 +170,7 @@ async def run_async(self, command: "Command") -> CommandResult:
170170
171171TOutput = TypeVar ("TOutput" )
172172
173- OutputParser : TypeAlias = Callable [[CommandResult ], TOutput ]
173+ _OutputParser : TypeAlias = Callable [[CommandResult ], TOutput ]
174174
175175
176176class Command (Generic [TOutput ]):
@@ -181,14 +181,20 @@ class Command(Generic[TOutput]):
181181 Supports both synchronous and asynchronous execution patterns with type-safe
182182 output parsing.
183183
184+ Commands are provided as a list of strings, which is consistent with subprocess
185+ and executed directly without shell interpretation. This approach:
186+ - Avoids shell injection vulnerabilities
187+ - Handles arguments with spaces correctly without manual quoting
188+ - Is more portable across platforms
189+
184190 Attributes:
185- cmd: The command string to execute
191+ cmd: The command to execute as a list of strings
186192 result: The result of command execution (available after execution)
187193
188194 Example:
189195 ```python
190- # Create a simple command
191- cmd = Command(cmd="echo hello" , output_parser=identity_parser)
196+ # Create a command
197+ cmd = Command(cmd=["python", "-c", "print(' hello')"] , output_parser=identity_parser)
192198
193199 # Execute with a synchronous executor
194200 executor = LocalExecutor()
@@ -198,24 +204,25 @@ class Command(Generic[TOutput]):
198204 def parse_json(result: CommandResult) -> dict:
199205 return json.loads(result.stdout)
200206
201- cmd = Command(cmd="get-data --json", output_parser=parse_json)
207+ cmd = Command(cmd=[ "get-data", " --json"] , output_parser=parse_json)
202208 data = cmd.execute(executor)
203209 ```
204210 """
205211
206- def __init__ (self , cmd : str , output_parser : OutputParser [TOutput ]) -> None :
212+ def __init__ (self , cmd : list [ str ] , output_parser : _OutputParser [TOutput ]) -> None :
207213 """Initialize the Command instance.
214+
208215 Args:
209- cmd: The command string to execute
216+ cmd: The command to execute as a list of strings. The first element
217+ is the program to run, followed by its arguments.
210218 output_parser: Function to parse the command result into desired output type
211219
212220 Example:
213221 ```python
214- # Create a simple command
215- cmd = Command(cmd="echo hello", output_parser=identity_parser)
222+ cmd = Command(cmd=["echo", "hello"], output_parser=identity_parser)
216223 ```
217224 """
218- self ._cmd = cmd
225+ self ._cmd : list [ str ] = cmd
219226 self ._output_parser = output_parser
220227 self ._result : Optional [CommandResult ] = None
221228
@@ -227,16 +234,30 @@ def result(self) -> CommandResult:
227234 return self ._result
228235
229236 @property
230- def cmd (self ) -> str :
231- """Get the command string ."""
237+ def cmd (self ) -> list [ str ] :
238+ """Get the command as a list of strings ."""
232239 return self ._cmd
233240
234241 def append_arg (self , args : str | list [str ]) -> Self :
235- """Append an argument to the command."""
242+ """Append arguments to the command.
243+
244+ Args:
245+ args: Argument(s) to append. Can be a single string or list of strings.
246+ Empty strings are filtered out.
247+
248+ Returns:
249+ Self for method chaining.
250+
251+ Example:
252+ ```python
253+ cmd = Command(cmd=["python"], output_parser=identity_parser)
254+ cmd.append_arg(["-m", "pytest"]) # Results in ["python", "-m", "pytest"]
255+ ```
256+ """
236257 if isinstance (args , str ):
237258 args = [args ]
238259 args = [arg for arg in args if arg ]
239- self ._cmd = ( self .cmd + f" { ' ' . join ( args ) } " ). strip ()
260+ self ._cmd = self ._cmd + args
240261 return self
241262
242263 def execute (self , executor : Executor ) -> TOutput :
@@ -267,9 +288,18 @@ def _parse_output(self, result: CommandResult) -> TOutput:
267288
268289
269290class StdCommand (Command [CommandResult ]):
270- """Standard command that returns the raw CommandResult."""
291+ """Standard command that returns the raw CommandResult.
292+
293+ A convenience class that creates a Command with the identity_parser,
294+ returning the raw CommandResult without transformation.
295+
296+ Example:
297+ ```python
298+ cmd = StdCommand(["echo", "hello"])
299+ ```
300+ """
271301
272- def __init__ (self , cmd : str ) -> None :
302+ def __init__ (self , cmd : list [ str ] ) -> None :
273303 super ().__init__ (cmd , identity_parser )
274304
275305
0 commit comments