Python 调用 Shell 命令:sh 库 vs subprocess 全面对比 发表于 2025-12-21 | 更新于 2025-12-21
| 浏览量:
TL;DR
维度
subprocess
sh
类型
标准库
第三方库
语法风格
列表 / 字符串
方法链
学习曲线
中等
低
代码量
多
少
跨平台
✅ Windows/Linux/macOS
❌ 仅 POSIX
类型提示
✅ 完整
⚠️ 有限
推荐场景
跨平台、生产代码
Linux 脚本、快速开发
1. 基础介绍 subprocess(标准库) Python 3.5+ 内置的进程管理模块,是 os.system() 和 os.popen() 的现代替代品。
1 2 import subprocessfrom subprocess import run, check_call, check_output, Popen, PIPE, CalledProcessError
sh(第三方库) 一个让 shell 命令调用更 Pythonic 的库,通过动态属性访问实现命令调用。
1 2 3 pip install sh uv add sh
2. 基础用法对比 2.1 执行简单命令 1 2 3 4 5 6 7 8 9 10 11 12 import subprocessresult = subprocess.run(["ls" , "-la" ], capture_output=True , text=True ) print (result.stdout)subprocess.check_call(["echo" , "hello" ]) output = subprocess.check_output(["date" ], text=True )
1 2 3 4 5 6 7 8 9 10 11 import shprint (sh.ls("-la" ))sh.echo("hello" ) output = str (sh.date())
对比 :sh 的语法更简洁,不需要列表包装参数。
2.2 传递参数 1 2 3 4 5 6 subprocess.run(["git" , "commit" , "-m" , "fix: bug" ]) subprocess.run("git commit -m 'fix: bug'" , shell=True )
1 2 3 4 5 6 7 8 9 10 sh.git.commit("-m" , "fix: bug" ) sh.git.commit(m="fix: bug" ) sh.docker.run("nginx" , d=True , p="8080:80" , name="web" )
对比 :sh 支持关键字参数自动转换,更接近 Python 风格。
2.3 获取命令输出 1 2 3 4 5 6 7 8 9 result = subprocess.run(["kubectl" , "get" , "pods" ], capture_output=True , text=True ) stdout = result.stdout stderr = result.stderr return_code = result.returncode output = subprocess.check_output(["kubectl" , "get" , "pods" ], text=True )
1 2 3 4 5 6 7 8 9 10 11 output = sh.kubectl.get.pods() stdout = str (output) stdout = output.stdout stderr = output.stderr exit_code = output.exit_code
2.4 工作目录和环境变量 1 2 3 4 5 6 7 8 import ossubprocess.run( ["npm" , "run" , "build" ], cwd="/path/to/project" , env={**os.environ, "NODE_ENV" : "production" } )
1 2 3 4 5 sh.npm.run.build( _cwd="/path/to/project" , _env={**os.environ, "NODE_ENV" : "production" } )
注意 :sh 使用下划线前缀 _cwd, _env 来区分命令参数和控制参数。
3. 进阶用法对比 3.1 管道(Pipe) 1 2 3 4 5 6 7 8 9 10 from subprocess import Popen, PIPEp1 = Popen(["cat" , "file.txt" ], stdout=PIPE) p2 = Popen(["grep" , "error" ], stdin=p1.stdout, stdout=PIPE) p3 = Popen(["wc" , "-l" ], stdin=p2.stdout, stdout=PIPE, text=True ) p1.stdout.close() p2.stdout.close() output = p3.communicate()[0 ]
1 2 3 4 5 6 output = sh.wc(sh.grep(sh.cat("file.txt" ), "error" ), "-l" ) output = sh.cat("file.txt" ) | sh.grep("error" ) | sh.wc("-l" )
对比 :sh 的管道语法极其简洁,几乎与 shell 一致。
3.2 后台执行 1 2 3 4 5 6 7 8 9 10 11 12 import subprocessprocess = subprocess.Popen(["python" , "server.py" ]) print ("Server started in background" )process.terminate() process.wait()
1 2 3 4 5 6 7 8 9 10 11 12 process = sh.python("server.py" , _bg=True ) print ("Server started in background" )process.wait() process.terminate()
3.3 实时输出流(Streaming) 1 2 3 4 5 6 7 8 9 10 process = subprocess.Popen( ["docker" , "build" , "." ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True ) for line in process.stdout: print (line, end="" ) process.wait()
1 2 3 4 5 6 7 8 9 10 11 12 13 for line in sh.docker.build("." , _iter =True ): print (line, end="" ) def process_line (line ): print (f"[BUILD] {line} " , end="" ) sh.docker.build("." , _out=process_line) sh.docker.build("." , _out=process_line, _err=process_line)
3.4 超时控制 1 2 3 4 5 6 7 8 9 try : result = subprocess.run( ["long_running_command" ], timeout=30 , capture_output=True ) except subprocess.TimeoutExpired: print ("Command timed out" )
1 2 3 4 5 try : result = sh.long_running_command(_timeout=30 ) except sh.TimeoutException: print ("Command timed out" )
3.5 错误处理 1 2 3 4 5 6 7 8 9 10 from subprocess import CalledProcessErrortry : subprocess.check_call(["false" ]) except CalledProcessError as e: print (f"Command failed with code {e.returncode} " ) print (f"Command: {e.cmd} " ) print (f"Output: {e.output} " ) print (f"Stderr: {e.stderr} " )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 try : sh.false() except sh.ErrorReturnCode as e: print (f"Command failed with code {e.exit_code} " ) print (f"Full command: {e.full_cmd} " ) print (f"Stdout: {e.stdout} " ) print (f"Stderr: {e.stderr} " ) except sh.ErrorReturnCode_1: print ("Exit code 1" ) except sh.ErrorReturnCode_2: print ("Exit code 2" ) sh.grep("pattern" , "file.txt" , _ok_code=[0 , 1 ])
3.6 交互式命令 1 2 3 4 5 6 7 8 process = subprocess.Popen( ["python" ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True ) stdout, stderr = process.communicate(input ="print('hello')\n" )
1 2 3 4 5 6 7 8 9 output = sh.python(_in ="print('hello')\n" ) output = sh.python(_stdin="print('hello')\n" ) sh.sudo.command(_in ="password\n" )
4. 高级特性 4.1 sh 的特殊命令名处理 1 2 3 4 5 6 sh.Command("apt-get" ).install("vim" ) sh.Command("2to3" )("script.py" ) sh.apt_get.install("vim" )
4.2 sh 的命令缓存 1 2 3 4 5 6 7 docker = sh.docker kubectl = sh.kubectl.bake("--namespace" , "production" ) docker.ps() kubectl.get.pods()
4.3 sh 的全局配置 1 2 3 4 5 6 7 8 9 import shsh2 = sh.bake(_cwd="/tmp" , _env={"LC_ALL" : "C" }) sh2.ls() docker = sh.docker.bake("--log-level" , "error" ) docker.build("." )
5. 实际场景对比 场景 1: Docker 镜像构建 1 2 3 4 5 6 7 8 9 10 11 12 13 import osfrom subprocess import check_callcmd = [ "docker" , "buildx" , "build" , "--secret" , "id=token,env=GITHUB_TOKEN" , "--build-arg" , f"VERSION={version} " , "-t" , f"myapp:{tag} " , "-f" , "Dockerfile" , "." ] check_call(cmd, env={**os.environ, "DOCKER_BUILDKIT" : "1" })
1 2 3 4 5 6 7 8 9 10 11 12 import osimport shsh.docker.buildx.build( "--secret" , "id=token,env=GITHUB_TOKEN" , "--build-arg" , f"VERSION={version} " , "-t" , f"myapp:{tag} " , "-f" , "Dockerfile" , "." , _env={**os.environ, "DOCKER_BUILDKIT" : "1" } )
场景 2: Git 操作 1 2 3 4 5 6 7 8 9 10 from subprocess import check_output, check_callbranch = check_output(["git" , "branch" , "--show-current" ], text=True ).strip() check_call(["git" , "add" , "." ]) check_call(["git" , "commit" , "-m" , "feat: new feature" ]) check_call(["git" , "push" , "origin" , branch])
1 2 3 4 5 6 7 8 9 10 import shbranch = str (sh.git.branch("--show-current" )).strip() sh.git.add("." ) sh.git.commit(m="feat: new feature" ) sh.git.push("origin" , branch)
场景 3: Kubernetes 操作 1 2 3 4 5 6 7 8 9 10 11 12 import jsonfrom subprocess import check_outputoutput = check_output( ["kubectl" , "get" , "pods" , "-n" , "default" , "-o" , "json" ], text=True ) pods = json.loads(output) for pod in pods["items" ]: print (pod["metadata" ]["name" ])
1 2 3 4 5 6 7 8 9 10 11 12 import jsonimport shoutput = sh.kubectl.get.pods("-n" , "default" , "-o" , "json" ) pods = json.loads(str (output)) for pod in pods["items" ]: print (pod["metadata" ]["name" ]) names = sh.jq(".items[].metadata.name" , sh.kubectl.get.pods("-o" , "json" ))
场景 4: 日志监控 1 2 3 4 5 6 7 8 9 10 from subprocess import Popen, PIPEprocess = Popen(["tail" , "-f" , "/var/log/app.log" ], stdout=PIPE, text=True ) try : for line in process.stdout: if "ERROR" in line: send_alert(line) except KeyboardInterrupt: process.terminate()
1 2 3 4 5 6 7 8 9 10 11 import shdef on_line (line ): if "ERROR" in line: send_alert(line) try : sh.tail("-f" , "/var/log/app.log" , _out=on_line) except KeyboardInterrupt: pass
6. 性能对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import timeimport subprocessimport shN = 1000 start = time.time() for _ in range (N): subprocess.run(["echo" , "test" ], capture_output=True ) print (f"subprocess: {time.time() - start:.2 f} s" )start = time.time() for _ in range (N): sh.echo("test" ) print (f"sh: {time.time() - start:.2 f} s" )
典型结果 :
subprocess: ~2.5s
sh: ~3.0s
结论 :sh 略慢(约 20%),主要是动态属性查找的开销。对于大多数场景,这个差异可以忽略。
7. 迁移指南 7.1 从 subprocess 迁移到 sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from subprocess import check_call, check_output, CalledProcessErrortry : check_call(["git" , "pull" ]) output = check_output(["git" , "log" , "-1" ], text=True ) except CalledProcessError as e: print (f"Error: {e} " ) import shtry : sh.git.pull() output = str (sh.git.log("-1" )) except sh.ErrorReturnCode as e: print (f"Error: {e} " )
7.2 迁移对照表
subprocess
sh
check_call(["cmd", "arg"])
sh.cmd("arg")
check_output([...], text=True)
str(sh.cmd(...))
run([...], capture_output=True)
sh.cmd(...)
Popen([...])
sh.cmd(..., _bg=True)
CalledProcessError
sh.ErrorReturnCode
cwd="path"
_cwd="path"
env={...}
_env={...}
timeout=30
_timeout=30
stdin=PIPE
_in="input"
stdout=PIPE
默认捕获
8. 最佳实践 何时使用 subprocess
需要跨平台支持 (Windows)
生产环境代码 (标准库更稳定)
需要完整的类型提示
团队不熟悉 sh
何时使用 sh
Linux/macOS 专用脚本
DevOps 工具和 CLI
快速原型开发
大量管道操作
需要实时输出流处理
混合使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import subprocessimport shoutput = sh.git.status(s=True ) process = subprocess.Popen( ["complex" , "command" ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
9. 常见陷阱 sh 的陷阱 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 try : sh.nonexistent_command() except sh.CommandNotFound: print ("Command not found" ) sh.echo("hello world" ) sh.echo("hello" , "world" ) sh.ls(l=True ) sh.ls(l=False ) sh.git.log(oneline=True ) sh.git.log(n=5 )
subprocess 的陷阱 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 user_input = "file.txt; rm -rf /" subprocess.run(f"cat {user_input} " , shell=True ) subprocess.run(["cat" , user_input]) result = subprocess.run(["cmd" ], capture_output=True ) if result.returncode != 0 : print (result.stderr) process = subprocess.Popen(["cmd" ], stdout=PIPE, stderr=PIPE) stdout, stderr = process.communicate()
10. 总结 个人迁移计划
新脚本 :优先使用 sh
现有代码 :逐步迁移,不急于一次性替换
生产代码 :保持 subprocess,确保稳定性
跨平台项目 :继续使用 subprocess
推荐工具链 1 2 3 4 5 6 7 import shfrom sh import git, docker, kubectlk = sh.kubectl.bake("-n" , "default" ) dc = sh.docker.compose.bake("-f" , "docker-compose.yml" )
11. 其他竞品对比 除了 subprocess 和 sh,还有几个值得了解的库:
11.1 Plumbum
“Shell combinators and more” - 功能最丰富的竞品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from plumbum import localfrom plumbum.cmd import git, grep, wcprint (git["status" ]())chain = git["log" ] | grep["fix" ] | wc["-l" ] print (chain())from plumbum import SshMachinewith SshMachine("server.com" ) as remote: print (remote["ls" ]("-la" ))
特点 :
✅ 支持远程执行(SSH)
✅ 支持 Windows
✅ 路径操作集成
✅ 颜色终端支持
⚠️ 语法比 sh 复杂
11.2 Invoke
Fabric 的基础库,专注于任务自动化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from invoke import run, Contextresult = run("git status" ) print (result.stdout)from invoke import task@task def build (c ): c.run("docker build -t myapp ." ) @task def deploy (c, env="staging" ): c.run(f"kubectl apply -f k8s/{env} /" )
特点 :
✅ 任务编排能力强
✅ 支持任务依赖
✅ 内置帮助生成
⚠️ 主要用于任务定义,不是通用命令执行
11.3 Pexpect
专门用于交互式命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import pexpectchild = pexpect.spawn("ssh user@server" ) child.expect("password:" ) child.sendline("mypassword" ) child.expect("$" ) child.sendline("ls -la" ) child.expect("$" ) print (child.before.decode())child = pexpect.spawn("sudo apt update" ) child.expect("[sudo] password" ) child.sendline("password" ) child.expect(pexpect.EOF)
特点 :
✅ 交互式命令的最佳选择
✅ 模式匹配(expect)
✅ 超时控制
⚠️ 不适合简单命令
❌ 不支持 Windows(用 wexpect 替代)
11.4 Delegator.py
Kenneth Reitz(requests 作者)的作品,极简主义
1 pip install delegator.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import delegatorc = delegator.run("ls -la" ) print (c.out)print (c.return_code)c = delegator.chain("cat file.txt | grep error | wc -l" ) print (c.out)c = delegator.run("sleep 10" , block=False ) c.kill()
特点 :
✅ API 极简
✅ 管道语法直观
⚠️ 功能较少
⚠️ 维护不活跃
11.5 对比总表
库
语法风格
Windows
远程执行
交互式
活跃度
Stars
subprocess
标准库
✅
❌
⚠️
✅
N/A
sh
方法链
❌
❌
⚠️
✅
6.9k
plumbum
索引式
✅
✅ SSH
⚠️
✅
2.8k
invoke
装饰器
✅
✅ Fabric
⚠️
✅
4.3k
pexpect
交互式
❌
⚠️
✅
✅
2.6k
delegator
极简
⚠️
❌
❌
⚠️
1.7k
11.6 选择建议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 需要跨平台? ├── 是 → subprocess 或 plumbum └── 否 → 继续 ↓ 需要远程执行? ├── 是 → plumbum 或 invoke + fabric └── 否 → 继续 ↓ 需要交互式? ├── 是 → pexpect └── 否 → 继续 ↓ 需要任务编排? ├── 是 → invoke └── 否 → 继续 ↓ 追求简洁语法? ├── 是 → sh ⭐(推荐) └── 否 → subprocess
11.7 快速示例对比 同一个任务:执行 git log -5 --oneline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from subprocess import check_outputoutput = check_output(["git" , "log" , "-5" , "--oneline" ], text=True ) import shoutput = str (sh.git.log("-5" , "--oneline" )) from plumbum.cmd import gitoutput = git["log" , "-5" , "--oneline" ]() from invoke import runresult = run("git log -5 --oneline" , hide=True ) output = result.stdout import delegatoroutput = delegator.run("git log -5 --oneline" ).out
参考资料