Python3中subprocess.call调用bash脚本时SSH命令异常,os.system可正常运行的问题排查
这个问题的核心在于**subprocess.call()的列表传参方式和os.system()的字符串传参方式对shell参数解析的逻辑完全不同**,导致你的bash脚本接收到了错误的参数。
具体原因分析
当你使用os.system()时,你传入的是一个完整的shell命令字符串:
system(f"./get_api.sh {save_file_name} 'filter[id]={id}'")
shell会自动解析这个字符串,把外层的单引号去掉,将filter[id]=1234作为一个完整的参数传递给get_api.sh。此时脚本里的$options变量的值是filter[id]=1234(不带单引号),拼接后的SSH命令正常执行:
ssh USER@SERVER.com "wget 'https://API.com/?filter[id]=1234' -O '${file_path}'"
但当你用subprocess.call()的列表形式传参时:
subprocess.call(["./get_api.sh", save_file_name, f"'filter[id]={id}'"])
subprocess会把列表里的每个元素原封不动作为参数传给脚本,也就是说$options的值是'filter[id]=1234'(包括两边的单引号)。这时候拼接后的SSH命令就变成了:
ssh USER@SERVER.com "wget 'https://API.com/?'filter[id]=1234'' -O '${file_path}'"
远程shell解析这个命令时,会把'https://API.com/?'当成一个独立的字符串,filter[id]=1234变成另一个参数,''是空字符串,最后-O就被当成了一个单独的命令执行,所以才会报错bash: -O: command not found,同时wget因为没有指定输出文件,只能用默认的文件名保存响应。
解决方法
方法1:去掉subprocess参数中的多余单引号(推荐)
subprocess的列表传参方式本身就会保证每个参数的完整性,不需要手动添加单引号。直接这样写:
subprocess.call(["./get_api.sh", save_file_name, f"filter[id]={id}"])
这样脚本里的$options会接收到正确的filter[id]=1234,拼接后的SSH命令就能正常执行。
方法2:使用shell=True让subprocess调用shell解析命令(不推荐,除非必要)
如果你确实需要shell解析参数(比如涉及管道、通配符等),可以设置shell=True,并传入完整的命令字符串:
subprocess.call(f"./get_api.sh {save_file_name} 'filter[id]={id}'", shell=True)
⚠️ 注意:这种方式存在shell注入风险,如果id是来自用户输入的不可信内容,不要使用这种方法。
额外建议
为了让你的bash脚本更健壮,建议在使用变量时加上双引号,避免参数中包含空格或特殊字符时出错:
# 优化前 ssh USER@SERVER.com "wget 'https://API.com/?${options}' -O '${file_path}'" # 优化后 ssh USER@SERVER.com "wget \"https://API.com/?${options}\" -O \"${file_path}\""
通过转义双引号嵌套,确保参数被远程shell正确解析。
内容的提问来源于stack exchange,提问作者Quifisto

