Python subprocess.run异常处理不一致原因及解决咨询
问题
我正在写一个能执行任意Shell命令并返回stdout的函数,不担心异常抛出。测试时发现两个奇怪的情况:
- 执行不存在的命令
xyz testing,抛出的是FileNotFoundError,不是预期的CalledProcessError - 执行参数错误的
ls -xyz,却能正常抛出CalledProcessError
加上shell=True参数后,异常情况又反转了——xyz testing开始抛CalledProcessError,想知道这是为啥,以及怎么解决。
测试代码如下:
try: cmd_result = subprocess.run( cmd, capture_output=True, text=True, check=True, ) except subprocess.CalledProcessError as exc: click.echo(exc.stderr) click.echo(cmd) sys.exit(1)
原因分析
默认shell=False的逻辑
subprocess.run默认会直接解析你传入的命令,尝试启动对应的程序:
- 对于
ls -xyz:系统能找到ls这个可执行文件,进程正常启动,但ls因为参数不对返回了非0的退出码,check=True会检测到这个错误,抛出CalledProcessError。 - 对于
xyz testing:系统里根本找不到叫xyz的程序,连进程都启动不了,这时候直接抛出FileNotFoundError——进程都没起来,哪来的退出码?自然不会触发CalledProcessError。
开启shell=True后的逻辑
这时候subprocess会先启动一个系统Shell(比如Linux的bash、Windows的cmd),再把整个命令字符串丢给Shell去执行:
- 对于
xyz testing:Shell进程本身能正常启动,它去执行xyz testing时找不到命令,会返回一个非0的退出码,check=True就会抛出CalledProcessError。 - 对于
ls -xyz:Shell启动ls后,ls参数错误返回非0,Shell把这个退出码传给subprocess,所以还是会抛出CalledProcessError。你说的“异常反转”其实是指原本抛FileNotFoundError的xyz testing现在变成了CalledProcessError,而ls -xyz的异常类型没变。
解决办法
方案1:同时捕获两种异常
如果希望不管是找不到命令还是执行失败,都用同一套逻辑处理,可以把两种异常一起捕获:
import subprocess import click import sys def run_cmd(cmd): try: cmd_result = subprocess.run( cmd, capture_output=True, text=True, check=True, ) return cmd_result.stdout except (subprocess.CalledProcessError, FileNotFoundError) as exc: if isinstance(exc, FileNotFoundError): click.echo(f"找不到命令: {' '.join(cmd)}") else: click.echo(exc.stderr) click.echo(cmd) sys.exit(1)
方案2:用shell=True统一处理(注意风险)
如果能接受shell=True的安全问题(比如命令注入),可以开启这个参数,这样所有命令都通过Shell执行,只要Shell能启动,所有执行失败的情况都会抛出CalledProcessError:
import subprocess import click import sys def run_cmd(cmd_str): try: cmd_result = subprocess.run( cmd_str, capture_output=True, text=True, check=True, shell=True ) return cmd_result.stdout except subprocess.CalledProcessError as exc: click.echo(exc.stderr) click.echo(cmd_str) sys.exit(1)
重点提醒:
shell=True时要传完整的命令字符串,不能传列表;而且如果命令里包含用户输入,很容易出现命令注入漏洞,这种情况绝对不能用这个方案。
方案3:提前检查命令是否存在(安全首选)
如果想保持shell=False的安全性,又想提前处理命令不存在的情况,可以用shutil.which先检查命令是否存在:
import subprocess import click import sys import shutil def run_cmd(cmd): # 检查命令是否存在 if not shutil.which(cmd[0]): click.echo(f"找不到命令: {cmd[0]}") sys.exit(1) try: cmd_result = subprocess.run( cmd, capture_output=True, text=True, check=True, ) return cmd_result.stdout except subprocess.CalledProcessError as exc: click.echo(exc.stderr) click.echo(cmd) sys.exit(1)
内容的提问来源于stack exchange,提问作者Raviverma Chamarti
相关产品推荐
相关产品推荐

