从Python开始,可以解释函数和别名的shell

时间:2014-08-02 22:16:05

标签: python bash shell subprocess

我编写了一个小* 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执行“我的命令”,就像解释它一样?

1 个答案:

答案 0 :(得分:6)

Posix需要shell的-i选项以“交互模式”启动shell。交互模式的精确定义因shell而异 - 显然zshcsh不会尝试解释.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')