Python中运行依赖型Shell多行命令的优雅实现及规避shell=True方案
Absolutely! You can run dependent shell commands in a Pythonic, safe way without ever using shell=True—and honestly, this approach is better because it avoids shell injection risks and keeps your code cross-platform. Let’s walk through the key strategies with examples:
shell=True 1. Manage Environment Variables Explicitly
Instead of relying on the shell to export variables, handle them directly in Python using a copied environment dictionary. This lets you pass the same environment to subsequent commands seamlessly.
import subprocess import os # Copy the current environment to modify env = os.environ.copy() env["MY_PROJECT_PATH"] = "./my-project" # First command: create a directory using the env var (no shell expansion needed) subprocess.run(["mkdir", "-p", env["MY_PROJECT_PATH"]], check=True) # Second command: list the directory, using the same environment result = subprocess.run( ["ls", env["MY_PROJECT_PATH"]], capture_output=True, text=True, env=env, check=True ) print(result.stdout.strip())
If the command itself reads from environment variables (instead of needing shell expansion), just pass the env argument to subprocess.run()—the target program will pick up the variables automatically.
2. Chain Command Outputs with Pipes
For commands that depend on the output of a previous one (like cmd1 | cmd2 in shell), use subprocess.PIPE to connect the stdout of the first command to the stdin of the second.
Option 1: Capture Output First, Then Pass to Next Command
Great for simple chains where you don’t need real-time streaming:
import subprocess # Step 1: Generate output with the first command cmd1 = subprocess.run( ["cat", "requirements.txt"], capture_output=True, text=True, check=True ) # Step 2: Use the output as input for the second command cmd2 = subprocess.run( ["grep", "requests"], input=cmd1.stdout, capture_output=True, text=True, check=True ) print("Found dependency:", cmd2.stdout.strip())
Option 2: Real-Time Pipe with subprocess.Popen
Use this if you need to stream data between commands as they run:
import subprocess # Start the first command, pipe its stdout with subprocess.Popen(["tail", "-f", "log.txt"], stdout=subprocess.PIPE, text=True) as p1: # Start the second command, feeding it p1's stdout with subprocess.Popen(["grep", "ERROR"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True) as p2: # Read the final output error_lines = p2.communicate()[0] print("Errors found:\n", error_lines)
3. Control Working Directories Per Command
Instead of using cd in shell, specify the working directory directly in each subprocess call with the cwd parameter. This avoids changing the global working directory of your Python script.
import subprocess # Instead of `cd /tmp && touch temp_file.txt` subprocess.run(["touch", "temp_file.txt"], cwd="/tmp", check=True) # Verify the file exists in /tmp result = subprocess.run(["ls", "/tmp/temp_file.txt"], capture_output=True, text=True, check=True) print(result.stdout.strip())
4. Encapsulate Workflows with Functions
For complex, multi-step workflows, wrap each logical step in a function that returns necessary state (like environment variables, file paths, or command outputs). This makes your code modular and easy to debug.
import subprocess import os def setup_build_env(): """Set up environment and create build directory""" env = os.environ.copy() env["BUILD_DIR"] = "./build" subprocess.run(["mkdir", "-p", env["BUILD_DIR"]], check=True) return env def compile_project(env): """Compile the project in the build directory""" subprocess.run(["make"], cwd=env["BUILD_DIR"], env=env, check=True) # Return the path to the compiled binary return f"{env['BUILD_DIR']}/bin/my_app" def run_compiled_app(app_path): """Run the compiled application and print output""" result = subprocess.run([app_path], capture_output=True, text=True, check=True) print("Application Output:\n", result.stdout.strip()) # Execute the full workflow build_env = setup_build_env() app_binary = compile_project(build_env) run_compiled_app(app_binary)
Why Avoid shell=True?
- Security:
shell=Trueexposes you to shell injection attacks if you’re using untrusted input. - Cross-Platform Compatibility: Shell syntax (like
&&,|, or variable expansion) varies between Unix-like systems and Windows. Usingsubprocessdirectly keeps your code portable. - Clarity: Explicitly passing arguments and environment variables makes your code easier to read and maintain—no hidden shell logic.
内容的提问来源于stack exchange,提问作者Kevin Kostlan

