A quick tip for properly parsing shell-style command strings when using Python’s subprocess.run() with list arguments.
The Problem
When running commands with subprocess.run() using a list, shell-style command strings don’t work properly if passed as single elements.
Example Issue
You have a command loaded from YAML or configuration:
command: bash -c 'cd /app && ./bin/service run standalone'
If you naively pass it to subprocess:
cmd = ['apptainer', 'instance', 'run']
cmd.extend([command]) # Wrong!
subprocess.run(cmd)
This passes the entire string as ONE argument: "bash -c 'cd /app && ./bin/service run standalone'"
Result:
[FATAL] exec bash -c 'cd /app && ./bin/service run standalone' failed: No such file or directory
The command tries to execute a binary literally named bash -c 'cd /app && ./bin/service run standalone' instead of running bash with arguments -c and the quoted command.
The Solution: Use shlex.split()
import shlex
import subprocess
# Load command from YAML/config
command = "bash -c 'cd /app && ./bin/service run standalone'"
# Properly split the command
cmd = ['apptainer', 'instance', 'run']
cmd.extend(shlex.split(command))
subprocess.run(cmd)
What shlex.split() Does
Converts:
"bash -c 'cd /app && ./bin/service run standalone'"
Into:
['bash', '-c', 'cd /app && ./bin/service run standalone']
It understands shell quoting rules and properly splits while preserving grouped arguments.
Full Example
import subprocess
import shlex
import yaml
# Load configuration
with open('config.yaml') as f:
config = yaml.safe_load(f)
# Build apptainer command
cmd = ['apptainer', 'instance', 'run', '--nv', '--bind', '/path/to/volume']
# Add command from YAML (properly split)
command = config['command']
cmd.extend(shlex.split(command))
# Run it
subprocess.run(cmd)
Result: The command executes correctly with all arguments properly parsed.
Why This Matters
os.system() vs subprocess.run()
os.system(): Uses shell parsing automaticallyos.system("bash -c 'cd /app && ./bin/service run'") # Workssubprocess.run()with list: Needs manual parsingsubprocess.run(["bash -c 'cd /app && ./bin/service run'"]) # Fails! subprocess.run(shlex.split("bash -c 'cd /app && ./bin/service run'")) # Works!
Security Benefit
Using subprocess.run() with a list (instead of shell=True) prevents shell injection attacks:
# Unsafe - vulnerable to injection
user_input = "; rm -rf /"
subprocess.run(f"echo {user_input}", shell=True) # Dangerous!
# Safe - shell metacharacters are treated as literals
subprocess.run(['echo', user_input]) # Safe
shlex.split() gives you both safety and convenience.
Edge Cases
Handling Paths with Spaces
command = "program --file '/path/with spaces/file.txt'"
args = shlex.split(command)
# Result: ['program', '--file', '/path/with spaces/file.txt']
Escaping Special Characters
command = r"grep 'pattern\twith\ttabs' file.txt"
args = shlex.split(command)
# Result: ['grep', 'pattern\\twith\\ttabs', 'file.txt']
Windows Compatibility
⚠️ Note: shlex.split() uses POSIX quoting rules by default. For Windows-style command parsing:
import shlex
args = shlex.split(command, posix=False) # Windows-style
Key Takeaways
subprocess.run()with lists requires manual parsing - arguments must be separate list elements- Use
shlex.split()to convert shell-style strings into proper list arguments - Preserve security - avoid
shell=Truewhen possible - Handle quoting correctly -
shlex.split()understands single/double quotes and escaping
Context
This pattern came up when spawning Milvus in an Apptainer container where the command was loaded from YAML configuration. The command worked with os.system() but failed with subprocess.run() until properly split with shlex.split().
Related Reading
Date: 2025-12-02 Tags: #python #subprocess #shlex #shell-commands