Python下的系统命令模块subprocess

在Python3中,commands模块被移除掉,commands模块执行shell命令的相关模块和函数的功能,在subprocess模块总均可实现,并且提供了更加丰富的功能

Python Version: 3.5+

call

执行命令,返回状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> ret = subprocess.call(["df","-h"], shell=False)
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk
>>> print(ret)
0
------------
# 以shell的模式运行
>>> ret = subprocess.call("df -h", shell=True)
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk
>>> print(ret)
0

check_call

检查执行命令后的返回值,如果是0则返回,如果不是则抛出异常

1
2
3
4
5
6
>>> ret = subprocess.check_call("exit 1", shell=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/subprocess.py", line 584, in check_call
raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1

check_output

检查执行命令后的返回值,如果是0则返回执行结果,如果不是则抛出异常

1
2
3
>>> ret = subprocess.check_output("df -h", shell=True)
>>> print(ret)
b'Filesystem Size Used Avail Use% Mounted on\n/dev/disk1 233G 107G 127G 46% /\n/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk\n'

Popen

用于执行复杂的系统命令

参数:

  • args:shell命令,可以是字符串或者序列类型(如:list,元组)
  • bufsize:指定缓冲。0 无缓冲,1 行缓冲,其他 缓冲区大小,负值 系统缓冲
  • stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
  • preexec_fn:只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用
  • close_sfs:在windows平台下,如果close_fds被设置为True,则新创建的子进程将不会继承父进程的输入、输出、错误管道。
    所以不能将close_fds设置为True同时重定向子进程的标准输入、输出与错误(stdin, stdout, stderr)。
  • shell:同上
  • cwd:用于设置子进程的当前目录
  • env:用于指定子进程的环境变量。如果env = None,子进程的环境变量将从父进程中继承。
  • universal_newlines:不同系统的换行符不同,True -> 同意使用 \n
  • startupinfo与createionflags只在windows下有效
    将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如:主窗口的外观,进程的优先级等等
1
2
3
import subprocess
ret1 = subprocess.Popen(["mkdir","t1"])
ret2 = subprocess.Popen("mkdir t2", shell=True)

非交互模式的命令

1
2
3
4
5
6
7
8
9
10
11
import subprocess

obj = subprocess.Popen("df -h", shell=True)

print("polarsnow")

------------
polarsnow
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk

从上面的例子🌰看到,父进程在开启子进程之后并没有等待子进程结束,而是直接运行了print

1
2
3
4
5
6
7
8
9
10
11
import subprocess

obj = subprocess.Popen("df -h", shell=True)
obj.wait()
print("polarsnow")

------------
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk
polarsnow

加入了wait之后,可以看到,父进程会等待子进程运行结束后再执行print

此外,你还可以在父进程中对子进程进行如下其他的操作:

  • obj.poll() 检查子进程的状态
  • obj.kill() 终止子进程
  • obj.sent_signal() 向子进程发送信号
  • obj.terminate() 终止子进程
  • obj.pid 查看子进程的pid
  • obj.args 查看shell命令

子进程的文本流控制

  • obj.stdin 标准输入
  • obj.stdout 标准输出
  • obj.stderr 标准错误

可以在Popen建立子进程的时候改变子进程的标准输入、标准输出和标准错误。并可以利用subprocess.PIPE将多个子进程的输入输出连接在一起,构成管道

1
2
3
4
import subprocess

obj = subprocess.Popen("df -h", shell=True, stdout=subprocess.PIPE)
print(obj.stdout.read())

使用了subprocess提供的管道,子进程执行命令后的输出不会再默认输出到屏幕上了,而是放到了管道里,等待用户提取管道中的数据

1
2
3
4
5
6
7
import subprocess

obj = subprocess.Popen("df -h", shell=True, stdout=subprocess.PIPE)
print(obj.communicate())

------------
(b'Filesystem Size Used Avail Use% Mounted on\n/dev/disk1 233G 107G 127G 46% /\n/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk\n', None)

communicate()会返回执行命令的结果和标准错误,但是上面的例子中,我只是将子进程的标准输出放到的subprocess的管道中,并没有对标准错误应用subprocess的管道,所以,不管有没有报错,这里的返回值返回的标准错误永远是None。下面来验证一下

1
2
3
4
5
6
7
8
9
import subprocess

obj = subprocess.Popen("df -y", shell=True, stdout=subprocess.PIPE)
print(obj.communicate())

------------
df: invalid option -- 'y'
Try 'df --help' for more information.
(b'', None)

果然,报错默认打在了屏幕上,而communicate的返回值中的标准错误依然是None

实现Linux中 | 管道的效果

1
2
3
4
5
6
7
8
9
import subprocess

obj1 = subprocess.Popen("cat /etc/passwd", shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
obj2 = subprocess.Popen("grep root", shell=True, stdin=obj1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = obj2.communicate()
print(out)

------------
(b'root:*:0:0:System Administrator:/var/root:/bin/sh\ndaemon:*:1:1:System Services:/var/root:/usr/bin/false\n_cvmsroot:*:212:212:CVMS Root:/var/empty:/usr/bin/false\n', b'')

第3行:创建了一个子进程去执行命令,并将标准输入输出和错误放到了subprocess提供的管道中。

第4行:创建了另一个子进程去执行命令,命令的标准输入是上一条命令的标准输出,所以把obj1中的标准输出从subprocess的管道中取出当做标准输入传递给第二个子进程

你可能会想,在之前的例子中,如果不加wait的话,父进程不管子进程有没有执行完都会往下执行,那么假如第一条命令执行时间比较长,父进程如果继续向下走,执行第二条命令时,还没有拿到第一条的标准输出,会不会报错呢?首先,这是个好问题!下面详细分析一下

  • 程序会继续向下执行,但是会分两种情况,一种情况是第二条命令是阻塞的命令,第二种情况是非阻塞的命令。
    • 假如第二条是阻塞的命令,进程会一直卡住,等待接收输入,这种情况下,程序会一直等下去,等到第一条命令执行结束,把标准输出放到管道为止。
    • 假如第二条命令是非阻塞的命令,父进程会立即向下执行,虽然第一条命令还没有把标准输出放到管道中,第二条命令会认为你要给我传的就是空,所以第二条命令会被迅速执行完毕,而上面的例子中,调用了第二条命令对象的communicate方法,只会等待第二条命令执行完毕,一旦第二条命令执行完毕,不管第一条有没有执行完都会立即退出

subprocess.PIPE实际上为文本流提供一个缓存区。child1的stdout将文本输出到缓存区,随后child2的stdin从该PIPE中将文本读取走。child2的输出文本也被存放在PIPE中,直到communicate()方法从PIPE中读取出PIPE中的文本。
注意:communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成

实现类似pexpect的交互式命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import subprocess

obj = subprocess.Popen("passwd ps", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
obj.stdin.write("123\n")
obj.stdin.write("123\n")
obj.stdin.close()

cmd_out = obj.stdout.read()
obj.stdout.close()
cmd_error = obj.stderr.read()
obj.stderr.close()

print(cmd_out)
print(cmd_error)

------------
Changing password for user ps.
passwd: all authentication tokens updated successfully.

New password: BAD PASSWORD: it is WAY too short
BAD PASSWORD: is too simple
Retype new password:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import subprocess

obj = subprocess.Popen("passwd ps", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
obj.stdin.write("123\n")
obj.stdin.write("123\n")

cmd_out, cmd_error = obj.communicate()
print(cmd_out)
print(cmd_error)

------------
Changing password for user ps.
passwd: all authentication tokens updated successfully.

New password: BAD PASSWORD: it is WAY too short
BAD PASSWORD: is too simple
Retype new password:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import subprocess

obj = subprocess.Popen("passwd ps", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
cmd_out, cmd_error = obj.communicate('123\n123\n')
print(cmd_out)
print(cmd_error)

------------
Changing password for user ps.
passwd: all authentication tokens updated successfully.

New password: BAD PASSWORD: it is WAY too short
BAD PASSWORD: is too simple
Retype new password: