Python运行Shell命令

现今,Python如此火爆,也不需进行多余的介绍(如果身为一个程序员,还不知道这个语言,那就要有危机感了),直入正题。本人也是Python新手一名,作为一名新手,当然选择新版本的Python啦,也就是Python3系列。刚开始,也不知道写点啥,不过光看文档也没啥用,所以就写了这个小程序(程序总代码也才70行,包括注释可怜)。程序功能也比较简单,就是在Python中运行Shell命令,简单地说就是接收用户输入shell命令,然后运行shell命令,并将shell命令的输出结果显示给用户。说起简单,做起来也不容易,身为菜鸟的我也折腾了将近1天。“麻雀虽小,五脏俱全”,程序虽然挺小,但也包含了几个有意思的小技术:子进程(Python3中新加入的功能)、多线程以及线程间通信等等。下面先介绍下相关基础知识(大神们如果无意间看到这篇文章,就直接略过基础知识吧,或者有时间的大神可以看看,有错的话希望能帮我这个菜鸟指出来,我也多学学吐舌头)。

基础知识

对于Python的语法知识,这里就不进行赘述了,主要讲一些关键技术,包括:子进程(Popen)、多线程(Thread)以及线程通信(Queue)

子进程(Popen)

在Python3中,提供了subprocess模块,其中提供了创建新的子进程的方法。subprocess模块中提供了两种方式进行子进程的创建和调用:通过模块内方法(例如,call、check_call方法)和通过创建Popen对象。前者也是通过Popen对象实现的方便的调用接口,Python官方文档中建议使用这些方法,但如果需要一些高级功能的话,则需要直接使用Popen对象进行子进程的创建。创建子进程之后,可以通过链接input/output/err管道,从而与子进程进行交互。我本人觉得Popen使用起来更自由方便些,所有就使用了Popen对象进行子进程的对象。当然,还有另一方面原因就是,写的过程中也不断搜索相关资料,Popen用的人比较多,可吸取的经验也就比较多。下面直接介绍Popen对象,对于前一种方法这里不进行介绍。
Popen类的构造方法如下:
class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=()) 
这个构造函数包含的参数颇多,所以解释清楚还需要多费些口舌。
args:一个字符串或者是是一个序列(seuquence,Python中的sequence结构包括list,tuple和range)。args传递的参数的第一项(不管是字符串,还是sequence)应该是可执行程序的名称。例如:
Popen(['/bin/sh', '-c', args[0], args[1], ...])
bufsize:定义了子程序的缓冲方式。0代表不进行缓冲,1代表缓冲一行,其他正整数代表了缓冲区的大小。如果设置为负数,那么是指使用系统默认缓冲大小。
executable:定义了需要运行的可执行程序。当在此处制定了可执行程序时,args中的第一项仍然代表程序的名称,但是可以和可执行程序的名字不同。这个参数很少使用,一般直接在args给出执行程序。
stdin,stdout,stderr:这三个参数指定了子程序的输入、输出、错误管道绑定。通过将这三个参数设置为标准管道或者文件,从而与子进程进行通信。其可以设置为:PIPE、DEVNULL、文件描述符、文件对象或者None。PIPE意味着会为子进程创建一个新的管道;DEVNULL意味着会使用os.devnull文件(代表没有实际设备的管道,虚拟设备管道);None意味着不会对输入输出等进行重定向。
preexec_fn:代表在运行子进程之间要运行的程序。仅适用于Unix操作系统。
close_fsd:此参数如果设置为Ture,那么除了0,1,2三个描述符,在子进程执行前都会被关闭(我也不明白啥意思)。在Unix系统上,默认值为True。在Windows操作系统上,当stdin/stdout/stderr为None时,此参数默认值为True,其他情况下默认值为False。此外,在Windows上如果此参数为True,那么子进程不会继承父进程呢过的任何句柄。需要注意的是,在Windows上,不能即重定向stdin/stdout/stderr,又将close_fds设置为True
shell:设置为True的时候,代表命令使用标准SHELL进行执行。
cwd:设置子进程的当前路径。需要注意的是,这个路径不影响可执行程序的路径,即不能将可执行的路径写为相对此路径。
env:子进程的环境变量,需为非None(不明白啥意思,使用的时候没有设置,也没有出错)。
universal_newline:如果设为True,那么stdin,stdout,stderr就在universal_newline模式下使用text stream。在此模式下,stdin,stdout和stderr使用locale.getpreferredencoding(False)返回的变法方式进行编码。stdin中的'\n'会被转换为os.linesep,stdout和stderr中行结束符会被转换为'\n'。
其他参数,文档中没介绍,这里也就不讲了,用的也不多。
Popen对象包含的主要方法包括:
Popen.poll():检查子进程是否结束了。如果结束了,返回非None值,否则返回None。
Popen.wait(timeout=None):等待子进程结束。
Popen.communicate(input=Node,timeout=None):与子进程进行交互,返回一个tuple(stdoutdata,stderrdata)
Popen.send_signal(signal):向子进程发送信号
Popen.terminate():在Posix OS上向子进程发送SIDTERM,在Win32平台上调用TerminateProcess()方法。
Popen.kill():在Posi OS上向子进程发送SIGKILL,在Windows上等同于terminate()方法

多线程

Python中对多线程支持由threading模块实现,该模块在是在底层模块_thread之上实现的。此模块中包含了很多模块方法,例如:active_count()返回进程子线程数,current_thread()返回当前线程等等。这里不一一介绍,主要介绍Thread对象。
Python中的实现多线程有两种方式:重写run方法或者向Thread对象传递一段可执行的代码。前者是通过继承的方式实现的,在继承Thread时,只能重写Thread的构造函数__init__()和run()函数。当线程对象创建之后,调用start()方法开始线程的执行。此后线程会一直执行直到run方法结束(正常结束)或者执行过程中产生没有处理的异常。可以通过is_alive()方法检查线程是否还在运行。Thread对象的构造方法如下:
Thread(group=None, target=None, name=None, args=(), kwargs={}, verbose=None, *, daemon=None) 
group:这个参数是为了将来实现ThreadGroup后使用的,现在应该设置为None。
target:代表了需要执行的代码段。一般情况下设置为一个函数名。
name:线程名。默认情况下,每个线程都会创建一个线程名:"Thread-N"
args:是一个tuple对象,用于向target传递参数。
kwargs:是一个dictionary对象,用来向target传递keyword参数。
vervose:是一个调试消息标志(不懂)。
daemon:标记次线程是否是后台线程。
主要成员方法:
start():开始线程
run():采用继承方式,需要重写的方法
join(timeout=None):阻塞次线程的执行,直到调用join方法的线程执行完成,或者timeout超时。
getName()/setName():获取/设置线程的名字
is_alive():线程是否在运行
isDaemon()/setDaemon():获取/设置线程后台

线程通信

Python中的queue模块提供了多生产-多消费者队列,从而可以用于适用此模式的线程间进行通信(还有其他通信机制,例如锁机制等,本文中没用到,也就不进行介绍)。在此模块中,提供三种不同类型的队列:FIFO队列、LILO队列以及Priority队列。构造函数依次为:
class queue.Queue(maxsize=0) 
class queue.LifoQueue(maxsize=0) 
class queue.PriorityQueue(maxsize=0) 
这三种队列都支持下面的方法:
qsize():获取队列的 大概(approximate)长度。需要注意的是,qsize()>0不意味着get()不会阻塞,同样qsize()<maxsize也不意味着puy()不会阻塞。
empty():获取队列是否为空。
full():获取队列是否为满
put(item,block=True,timeout=None):向队列中放入一个元素
put_nowait():等价于put(item,block=False)
get(block=True,timeout=None):从队列中取一个元素
get_nowait():等价于get(False)
task_done():通知队列,当前取出item已经处理完。
join():阻塞队列中所有item,直到队列中所有item都处理完。

程序模块

程序包括三个主要模块:主模块,子进程输出读取模块以及用户输入模块,说是模块,其实有的模块仅有几行代码,不过功能是相互独立的嘛,也就划分出来了。

输入模块

Python中提供了内置方法input()来提示用户进行输入,但应用到本文程序中,会有一个问题,即输入完shell成后,需要连按两次回车才能够显示shell命令执行的结结果,或者会多现实一个换行。如下图:


图中,输入hello命令后,会多出一个换行符,然后才显示输出。为了解决此问题,在网上找了个别人写的用来输入密码的方法,然后稍微改了改,修改为新的input方法,代码如下:
#input method
def pwd_input():  
    chars = [] 
    while True:
        try:
            newChar = msvcrt.getch().decode(encoding="utf-8")
        except:
            return input()
        if newChar in '\r\n': # 如果是换行,则输入结束             
             break 
        elif newChar == '\b': # 如果是退格,则删除密码末尾一位并且删除一个星号 
             if chars:  
                 del chars[-1] 
                 msvcrt.putch('\b'.encode(encoding='utf-8')) # 光标回退一格
                 msvcrt.putch( ' '.encode(encoding='utf-8')) # 输出一个空格覆盖原来的星号
                 msvcrt.putch('\b'.encode(encoding='utf-8')) # 光标回退一格准备接受新的输入                 
        else:
            chars.append(newChar)
            msvcrt.putch(newChar.encode(encoding='utf-8'))
    return (''.join(chars) )
代码中,使用了Python中提供的msvcrt方法,每次读取一个字符,然后如果字符是换行则直接返回;如果是回车则删除一个字符并将光标绘图,其他字符则直接显示(在密码输入的时候,用'*'代替newChar进行输出就行)。需要说明的是,之所以一开始需要捕获输入时的异常,是因为当在Python shell中执行,而不是在控制台执行时,会出现异常。这时候,直接用内置input()函数代替此函数。

子进程输出读取模块

为了读取子进程(也就是控制台子进程)的输出,程序中设置了两个线程,一个用来读取正常输出的消息,一个用来读取子进程的错误输出。线程执行的内容比较简单,即循环读取子进程输出流或者错误流,并将读取的内容放入到公共队列中。这里有个问题就是每次读取内容的多少,开始的时候使用每次读取一行(readline())但这样无法读取到输入提示的那一行(即C:\>这一行),只会在下次命令执行的时候才能读取到。所以采用了读取一定字节数的方式,再尝试了几种大小后,发现设置为64字节每次比较合理,出现显示错误的概率比较低。下面是这一模块的代码:
#function read data from of io object
def __reader(stream,q):
    while True:
        byte=stream.read(64)
        q.put(byte)
此函数包括两个参数,stream代表了需要读取的IO对象(本文中就是stdout和stderr),q代表了读取数据后放入的队列。

主模块

主模块负责整个程序的初始化、子进程、子线程等的创建。代码如下:
def cmd():
    cmdStack=[]# command stack
    #sharing queue
    q=Queue()
    #create shell subprocess
    p=Popen("cmd",stdin=PIPE,stdout=PIPE,stderr=PIPE,shell=True,cwd=os.environ['homepath'])
    #start reader
    outThread=Thread(target=__reader,kwargs={'stream':p.stdout,"q":q})
    errThread=Thread(target=__reader,kwargs={'stream':p.stderr,"q":q})
    outThread.setDaemon(True)
    errThread.setDaemon(True)
    outThread.start()
    errThread.start()
    #main loop
    while True:
        time.sleep(0.1)
        if p.poll()!=None:
            break
        #read messages
        output=b''
        while(q.qsize()>0):
           output+=q.get(False)
        output=output.decode('gb2312')
        #skip the command
        if len(cmdStack)>0:
            cmd=cmdStack[-1]
            start=output.find(cmd[0:-1])
            #print('<'+output+'><'+'<'+cmd[0:-1]+'>')
            if start!=-1:
                output=output[start+len(cmd):len(output)]
        print(output,end="")
        #read input
        cmd=pwd_input()
        cmdStack=[]
        cmdStack.append(cmd)
        cmd=cmd+'\n'
        p.stdin.write(cmd.encode(encoding='utf-8'))
首先创建了一个命令栈来存储执行过的命令,然后创建了一个公共队列,再然后创建了一个控制台子进程(用来执行SHELL命令)、两个控制台输出子线程,下面是循环读取SHELL输出的内容显示出来,然后再提示用户输入,并将输入的命令发送给SEHLL子进程。需要解释的主要代码包括以下几个部分:
1、SHELL子进程的创建
p=Popen("cmd",stdin=PIPE,stdout=PIPE,stderr=PIPE,shell=True,cwd=os.environ['homepath'])
上述语句创建了一个shell子进程,并将子进程的输入输出都设置为PIPE,也就是重定向为标准输入输出流。然后设置了子进程的执行路径为默认用户文件夹(在unix系统中使用'home'代替'homepath')。
2、子线程的创建
 #start reader
    outThread=Thread(target=__reader,kwargs={'stream':p.stdout,"q":q})
    errThread=Thread(target=__reader,kwargs={'stream':p.stderr,"q":q})
    outThread.setDaemon(True)
    errThread.setDaemon(True)
    outThread.start()
    errThread.start()
上面创建了两个进程,分别来读取标准输出流和标准错误流,然后并且设置为后台线程。之所以设置为后台线程,是因为这两个线程在读取输出的时候会被堵塞,即使子线程已经结束,此时如果不设置为后台线程,会导致主线程无法退出。设为后台线程后,主线程退出时会直接强制终止这两个线程。
3、读取子进程输出
 #read messages
        output=b''
        while(q.qsize()>0):
           output+=q.get(False)
        output=output.decode('gb2312')
        #skip the command
        if len(cmdStack)>0:
            cmd=cmdStack[-1]
            start=output.find(cmd[0:-1])
            #print('<'+output+'><'+'<'+cmd[0:-1]+'>')
            if start!=-1:
                output=output[start+len(cmd):len(output)]
        print(output,end="")
由于队列中的输出内容是字节数组的形式存储的,所以先进行字节数组的拼接,然后再进行解码,解码为字符串(不同平台采用不同字符集进行解码,这里针对Windows平台采用了gb2312)。由于命令在输入的时候已经显示了出来,所以应该跳过子进程输出中的命令,因此下面要查找命令,然后将此命令删除。最后将输出内容打印出来。
4、读取用户输入命令并执行
#read input
        cmd=pwd_input()
        cmdStack=[]
        cmdStack.append(cmd)
        cmd=cmd+'\n'
        p.stdin.write(cmd.encode(encoding='utf-8'))
上面读取用户的输入,并将输入,并在输入后面给放入一个换行符,通过标准输入流发送给子进程进行执行。这里,如果不加入一个换行符,则命令就不会执行。
5、其他说明
首先,在主循环中,每次需要休息0.1秒,从而放弃一段时间CPU,给子线程执行的时间。其次,通过Popen的poll()函数判断子进程是否结束执行(当输入exit命令时,子进程会结束执行),如果已经结束,则跳出循环,主程序结束执行。

运行效果


总结

总的来说,这个小程序实现了大部分命令的调用执行功能,但是有些功能还是没有实现的,主要是控制台相关的命令,例如:cls清空控制台。这些控制台控制相关的命令或者快捷键仍然没有实现。另外,解码方面也会有一些问题,有些命令会导致解码出错,无法显示输出结果等等。总的来说,还好吧,菜鸟练手还算凑合吧。要是有哪位大神做过或者有兴趣做个完整的控制台,希望能够予以赐教,我也好多学学。。。。 大笑

主要参考文章

Python官方文档





其他网上参考资源就不一一列举了

完整程序

from subprocess import *
import os
from queue import Queue
from threading import Thread
import time
import msvcrt
#input method
def pwd_input():  
    chars = [] 
    while True:
        try:
            newChar = msvcrt.getch().decode(encoding="utf-8")
        except:
            return input()
        if newChar in '\r\n': # 如果是换行,则输入结束             
             break 
        elif newChar == '\b': # 如果是退格,则删除密码末尾一位并且删除一个星号 
             if chars:  
                 del chars[-1] 
                 msvcrt.putch('\b'.encode(encoding='utf-8')) # 光标回退一格
                 msvcrt.putch( ' '.encode(encoding='utf-8')) # 输出一个空格覆盖原来的星号
                 msvcrt.putch('\b'.encode(encoding='utf-8')) # 光标回退一格准备接受新的输入                 
        elif newChar not in '\t':
            chars.append(newChar)
            msvcrt.putch(newChar.encode(encoding='utf-8'))
    return (''.join(chars) )
#function read data from of io object
def __reader(stream,q):
    while True:
        byte=stream.read(64)
        q.put(byte)
def cmd():
    cmdStack=[]# command stack
    #sharing queue
    q=Queue()
    #create shell subprocess
    p=Popen("cmd",stdin=PIPE,stdout=PIPE,stderr=PIPE,shell=True,cwd=os.environ['homepath'])
    #start reader
    outThread=Thread(target=__reader,kwargs={'stream':p.stdout,"q":q})
    errThread=Thread(target=__reader,kwargs={'stream':p.stderr,"q":q})
    outThread.setDaemon(True)
    errThread.setDaemon(True)
    outThread.start()
    errThread.start()
    #main loop
    while True:
        time.sleep(0.1)
        if p.poll()!=None:
            break
        #read messages
        output=b''
        while(q.qsize()>0):
           output+=q.get(False)
        output=output.decode('gb2312')
        #skip the command
        if len(cmdStack)>0:
            cmd=cmdStack[-1]
            start=output.find(cmd[0:-1])
            #print('<'+output+'><'+'<'+cmd[0:-1]+'>')
            if start!=-1:
                output=output[start+len(cmd):len(output)]
        print(output,end="")
        #read input
        cmd=pwd_input()
        cmdStack=[]
        cmdStack.append(cmd)
        cmd=cmd+'\n'
        p.stdin.write(cmd.encode(encoding='utf-8'))
if __name__=='__main__':
    cmd()
    print("\npython exitted")


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值