为paramiko.SSHClient().exec_command转义参数
怎样才能安全地处理字符串,以便在命令行参数中使用呢?我知道使用 subprocess.Popen
可以通过 list2cmdline()
来解决这个问题,但这在使用 paramiko 时似乎不太管用。举个例子:
from subprocess import Popen
Popen(['touch', 'foo;uptime']).wait()
这个命令会创建一个名为 foo;uptime
的文件,这正是我想要的。对比一下:
from paramiko import SSHClient()
from subprocess import list2cmdline
ssh = SSHClient()
#... load host keys and connect to a server
stdin, stdout, stderr = ssh.exec_command(list2cmdline(['touch', 'foo;uptime']))
print stdout.read()
这个命令创建了一个叫 foo
的文件,并且打印了远程主机的运行时间。它把 uptime
当作第二个命令执行了,而不是把它当作第一个命令 touch
的一部分。这不是我想要的结果。
我尝试在发送给 list2cmdline
之前,用反斜杠转义分号,但结果是我得到了一个叫 foo\;uptime
的文件。
另外,如果用一个带空格的命令代替 uptime
,它就能正常工作:
stdin, stdout, stderr = ssh.exec_command(list2cmdline(['touch', 'foo;echo test']))
print stdout.read()
这个命令会创建一个名为 foo;echo test
的文件,因为 list2cmdline
用引号把它包裹起来了。
我还尝试了 pipes.quote()
,结果和 list2cmdline
一样。
补充说明:我需要确保在远程主机上只执行一个命令,无论我收到什么输入数据,这就意味着需要转义像 ;
、&
和反引号这样的字符。
3 个回答
我正在寻找的是re.escape()
这个功能。
re.*escape(**字符串*)
这个功能会把**字符串*中的所有非字母数字字符前面加上反斜杠...
举个例子:
from paramiko import SSHClient()
from subprocess import list2cmdline
import re
ssh = SSHClient()
#... load host keys and connect to a server
stdin, stdout, stderr = ssh.exec_command(' '.join(['touch', re.escape('foo;uptime')]))
这段代码在服务器上创建了一个叫做foo;uptime
的文件,这正是我想要的。
我尝试了我能想到的所有shell元字符,它都能正常工作:
stdin, stdout, stderr = ssh.exec_command(' '.join(['touch', re.escape('test;rm foo&echo "Uptime: `uptime`"')]))
你在使用 list2cmdline()
时遇到问题,是因为它是针对微软的命令行设计的,而微软的命令行规则和你通过SSH使用的POSIX命令行规则不一样。
你可以使用Python自带的 pipes.quote()
方法,记得要对命令中的每个参数单独使用它。这样你就能得到一个适用于SSH的命令行:
from pipes import quote
command = ['touch', 'foo;uptime']
print ' '.join(quote(s) for s in command)
输出中小心地对第二个参数进行了引号处理,以保护 ;
字符:
touch 'foo;uptime'
假设远程用户使用的是POSIX shell,这个方法应该可以用:
def shell_escape(arg):
return "'%s'" % (arg.replace(r"'", r"'\''"), )
为什么这样可以?
POSIX shell中的单引号是这样定义的:
用单引号('')包裹的字符会保持每个字符的字面值。单引号不能出现在单引号内部。
这里的意思是,你把字符串放在单引号里面。这样做几乎就足够了——除了单引号以外的每个字符都会被直接理解。对于单引号,你需要先退出单引号字符串(第一个'
),然后添加一个单引号(\'
),最后再继续单引号字符串(最后一个'
)。
这个方法适用于什么?
这个方法应该适用于任何POSIX shell。我在dash和bash上测试过。Solaris 5.10的/bin/sh
(我认为它不兼容POSIX,我也找不到相关规范)似乎也可以用。
对于任意的远程主机,我认为这是不可能的。我觉得ssh
会用远程用户的shell来执行你的命令(这个shell在/etc/passwd
或类似文件中配置)。如果远程用户可能在运行,比如说/usr/bin/python
或者git-shell
之类的,不仅任何引用方案可能会遇到跨shell的不一致性,而且你的命令执行可能也会失败。
csh / tcsh
稍微复杂一点的是,远程用户可能在使用tcsh
,因为确实有些人会在实际环境中使用它,并且可能会期待paramiko的exec_command
能正常工作。(使用/usr/bin/python
作为shell的用户可能没有这样的期待...)
tcsh似乎大部分情况下都能工作。不过,我找不到一种方法可以让它开心地处理换行符。在单引号字符串中包含换行符似乎会让tcsh不高兴:
$ tcsh -c $'echo \'foo\nbar\''
Unmatched '.
Unmatched '.
除了换行符,我尝试的其他所有内容在tcsh中似乎都能正常工作(包括单引号、双引号、反斜杠、嵌入的制表符、星号等)。
测试shell转义
如果你有一个转义方案,这里有一些你可能想测试的内容:
- 转义序列(
\n
,\t
, ...) - 引号(
'
,"
,\
) - 通配符字符(
*
,?
,[]
, 等等) - 作业控制和管道(
|
,&
,||
,&&
, ...) - 换行符
换行符值得特别注意。re.escape
的解决方案处理得不太对——它会转义任何非字母数字字符,而POSIX shell认为转义的换行符(也就是在Python中,两个字符的字符串"\\\n"
)是零个字符,而不是一个换行符。我认为,re.escape
能正确处理其他所有情况,尽管我用一个为正则表达式设计的东西来处理shell转义让我有点担心。它可能会有效,但我会担心re.escape
或shell转义规则(比如换行符)中的某些细微情况,或者未来API的变化。
你还应该知道,转义序列可能在不同阶段被处理,这让测试变得复杂——你只关心shell传递给程序的内容,而不是程序如何处理这些内容。使用printf "%s\n" escaped-string-to-test
可能是最好的选择。echo
的表现出乎意料地差:在dash中,echo
内置命令会处理反斜杠转义,比如\n
。使用/bin/echo
通常是安全的,但在我测试的Solaris 5.10机器上,它也会处理像\n
这样的序列。