我编写了一个小* nix实用程序,每次检测到文件系统更改时都会'重新运行'给定的命令。所以我将命令作为带引号的参数运行,例如
rerun "my command"
重新运行是用Python编写的,最后调用:
subprocess.call("my command", shell=True, executable=USERS_DEFAULT_SHELL)
在我的情况下,我的默认shell是'/ bin / bash'。但是,subprocess.call调用的shell不是“交互式”shell,因此无法识别我的.bashrc中定义的shell函数和别名。
Man bash告诉我,为了启动交互式shell,我将'-i'传递给/ bin / bash。但是,可以预见,
subprocess.call(..., executable='/bin/bash -i')
无效 - 无法找到该名称的可执行文件。 (即使它确实有效,我也试图为这个用户默认的shell创建这个函数,而不仅仅是Bash。可能'-i'对所有其他shell都没有做同样的事情。)
如果用户将其输入终端,我怎样才能从Python执行“我的命令”,就像解释它一样?
答案 0 :(得分:6)
Posix需要shell的-i
选项以“交互模式”启动shell。交互模式的精确定义因shell而异 - 显然zsh
和csh
不会尝试解释.bashrc
中的命令 - 而是使用-i
旗帜应该用合理的贝壳做正确的事。
通常,您可以通过使用列表调用subprocess.call
(或某些Popen
变体)来传递参数:
subprocess.call(['bash', '-i'])
当然,这不会尊重用户的shell偏好。您应该能够从SHELL
环境变量中获取它:
subprocess.call([os.getenv('SHELL'), '-i'])
为了使shell执行特定的命令行,您需要使用-c
命令行选项,它也是Posix标准,因此它应该适用于所有shell:
subprocess.call([os.getenv('SHELL'), '-i', '-c', command_to_run])
这在许多情况下都可以正常工作,但如果shell决定exec
command_to_run
中的最后一个(或唯一)命令,它可能会失败(请参阅this answer on {{ 3}}以获取一些细节。)然后您尝试调用另一个shell来执行另一个命令。
例如,考虑一下简单的python程序:
import subprocess
subprocess.call(['/bin/bash', '-i', '-c', 'ls'])
subprocess.call(['/bin/bash', '-i', '-c', 'echo second']);
启动第一个bash
进程。由于它是交互式shell,因此它会创建一个新的进程组并将终端附加到该进程组。然后它检查要运行的命令,确定它是一个运行外部实用程序的简单命令,因此它可以exec
命令。所以它就是这样做的,用ls
实用程序代替它,它现在是终端进程组的领导者。当ls
终止时,终端进程组变空,但终端仍然连接到它。因此,当第二个bash进程启动时,它会尝试创建一个新进程组并将终端连接到该进程组,但这是不可能的,因为终端处于一种不确定状态。根据Posix标准(基本定义,§11.1.2):
当不再有进程ID或进程组ID与前台进程组ID匹配的进程时,终端不应具有前台进程组。当进程ID与前台进程组ID匹配但进程组ID不匹配时,终端是否具有前台进程组是未指定的。除了分配控制终端或成功调用
tcsetpgrp()
之外,POSIX.1-2008中定义的任何操作都不会导致进程组成为终端的前台进程组。
使用bash,只有当作为-c
参数的值传递的字符串是一个简单的命令时才会发生这种情况,所以有一个简单的解决方法:确保字符串不是一个简单的命令。例如,
subprocess.call([os.getenv('SHELL'), '-i', '-c', ':;' + command_to_run])
在命令之前加上no-op,使其成为复合命令。但是,这不适用于在尾部调用优化中更具攻击性的其他shell。因此,一般解决方案需要遵循Posix建议的路径,同时还要处理tcsetpgrp
系统调用的描述:
尝试在与其控制终端关联的fildes上使用作为后台进程组成员的进程的
tcsetpgrp()
将导致进程组发送SIGTTOU
信号。如果调用线程阻塞SIGTTOU
信号或进程忽略SIGTTOU
信号,则应允许进程执行操作,并且不发送信号。
由于SIGTTOU
信号的默认操作是停止该过程,我们需要忽略或阻止该信号。所以我们最终得到以下结论:
!/usr/bin/python
import signal
import subprocess
import os
# Ignore SIGTTOU
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
def run_command_in_shell(cmd):
# Run the command
subprocess.call([os.getenv('SHELL'), '-i', '-c', cmd])
# Retrieve the terminal
os.tcsetpgrp(0,os.getpgrp())
run_command_in_shell('ls')
run_command_in_shell('ls')