从子进程更改父进程的环境
如果我有一个用其他语言写的程序,比如说用 python
,我该怎么在这个程序里改变环境变量或者当前工作目录,让这些变化能在调用这个程序的命令行中生效呢?
我想用这个来写一个“命令行助手”,让一些常见的操作变得简单。例如,智能的 cd
命令。当我在命令行中输入一个目录的名字时,它应该能直接 cd
进去。
[~/]$ Downloads
[~/Downloads]$
或者甚至可以这样做:
[~/]$ project5
[~/projects/project5]$
我后来发现了一个链接,里面讲了如何在 command_not_found_handle
中改变当前工作目录(这正是我想做的事情),它让我知道了 shopt -s autocd
。不过,这个方法还是不能处理那些不在 ./
目录下的情况。
另外,如果我想在一个 python 脚本中设置 http_proxy
变量,或者更新 PATH
变量,我还有哪些选择呢?
附注:我知道可能没有一个明显的方法可以在 python 脚本中写出一个神奇的命令,自动更新调用这个脚本的命令行中的环境变量。我在寻找一个可行的解决方案,不一定要优雅。
2 个回答
基本上,Charles Duffy说得很对,我在这里提供另一种看法。
你问的其实是进程间通信:你有一个进程,这个进程可能是shell的子进程,也可能不是(我觉得这没什么大不了的),你想让这个进程把信息传递给原来的shell(顺便说一下,shell也是一个进程),并让它改变状态。
一种可能的方法是使用信号。比如,在你的shell中,你可以这样做:
trap 'cd /tmp; pwd;' SIGUSR2
现在:
- 在你的shell中输入echo $$,这会给你一个数字,称为PID
- 在你的shell中切换到一个目录(除了/tmp以外的任何目录)
- 打开另一个shell窗口,输入:kill SIGUSR2 PID
- 你会发现你原来的shell现在在/tmp目录下了。
这就是一个通信的例子。当然,细节很重要。你的问题有两个方面:如何让shell与程序通信(如果command_not_found_handle能用,那就很好),以及如何让你的程序与shell通信。下面我来讲讲后一个问题:
你可以在原来的shell中设置一个trap语句:
trap 'eval $(/path/to/my/fancy/command $(pwd) $$)' SIGUSR2
...你的命令会把原shell的当前工作目录作为第一个参数传给它,并且会传递shell的进程ID(这样它就知道要发送信号给谁),然后它可以根据这些信息进行操作。如果你的命令发送一个可执行的shell命令字符串给eval命令,它会在原shell的环境中执行。
例如:
trap 'eval $(/tmp/doit $$ $(pwd)); pwd;' SIGUSR2
/tmp/doit就是那个特别的命令。它可以是任何可执行类型的程序(Python、C、Perl等),关键是它要输出一个shell可以评估的字符串。在/tmp/doit中,我提供了一个bash脚本:
#!/bin/bash
echo "echo PID: $1 original directory: $2; cd /tmp"
(我确保这个文件是可执行的,使用命令:chmod 755 /tmp/doit)。现在如果我输入:
cd; echo $$
然后,在另一个shell中,拿上面echo输出的数字(“NNNNN”),再输入:
kill -s SIGUSR2 NNNNN
...然后我会在原来的shell中看到类似这样的信息出现:
PID: NNNNN original directory: /home/myhomepath
/tmp
如果我在原来的shell中输入“pwd”,我会看到我在/tmp目录下。
想要在当前shell环境中让command_not_found_handle做点什么的人,可以使用信号来达到他想要的效果。在这里我手动运行kill,但其实shell函数也可以做到这一点。
在前端做一些复杂的工作,比如重新解释或预先解释用户输入给shell,可能需要用户运行一个前端程序,这可能会相当复杂,具体取决于你想做什么。老派的“expect”程序非常适合这种情况,但现在年轻人不太会用TCL了:-)。
这件事只能在父级命令行的帮助下完成。举个实际的例子,你可以看看 ssh-agent
是怎么使用的:
eval "$(ssh-agent -s)"
...它会读取 ssh-agent
的输出,并在当前的命令行中运行它(-s
表示输出格式兼容 Bourne shell,而不是 csh)。
如果你在用 Python,记得使用 pipes.quote()
(或者在 Python 3.x 中用 shlex.quote()
)来安全处理你的输出:
import pipes
dirname='/path/to/directory with spaces'
foo_val='value with * wildcards * that need escaping and \t\t tabs!'
print 'cd %s; export FOO=%s;' % (pipes.quote(dirname), pipes.quote(foo_val))
...因为不小心使用可能会导致命令行注入攻击。
相对而言,如果你是在写一个外部的 bash 脚本,记得使用 printf %q
来安全转义(不过要注意,它的输出是针对其他 bash shell 的,而不是为了符合 POSIX sh 的标准):
#!/bin/bash
dirname='/path/to/directory with spaces'
foo_val='value with * wildcards * that need escaping and \t\t tabs!'
printf 'cd %q; export FOO=%q;' "$dirname" "$foo_val"
如果根据你的问题来看,你希望你的命令看起来像是一个本地的 shell 函数,我建议把它包装在一个函数里(这个做法也可以和 command_not_found_handle
一起使用)。比如,安装时可以在自己的 .bashrc
文件中放入类似下面的内容:
my_command() {
eval "$(command /path/to/my_command.py "$@")"
}
...这样用户就不需要输入 eval
了。