用Python创建你自己的Shell

开发 后端
我们不能否认将整个程序当作纯粹的功能发挥作用的能力,以及将一个程序的输出传递到另一个程序的自然程度。因此,我想知道,我们能否将bash的某些功能与Python结合起来。

 [[380870]]

介绍

很多人讨厌bash脚本。每当我要做最简单的事情时,我都必须查阅文档。如何将函数的参数转发给子命令?如何将字符串分配给变量,然后作为命令调用该字符串?如何检查两个字符串变量是否相等?如何分割字符串并获得后半部分?等等。不是我找不到这些答案,而是每次都必须查找它们。

但是,我们不能否认将整个程序当作纯粹的功能发挥作用的能力,以及将一个程序的输出传递到另一个程序的自然程度。因此,我想知道,我们能否将bash的某些功能与Python结合起来?

基础知识

让我们从一个类开始。这是一个简单的方法,将其初始化参数保存到局部变量,然后使用subprocess.run对其自身进行延迟求值并保存结果。 

  1. import subprocess  
  2. class PipePy:  
  3.     def __init__(self, *args):  
  4.         self._args = args  
  5.         self._result = None  
  6.     def _evaluate(self):  
  7.         if self._result is not None:  
  8.             return  
  9.         self._result = subprocess.run(self._args,  
  10.                                       capture_output=True 
  11.                                       text=True 
  12.      @property  
  13.     def returncode(self):  
  14.         self._evaluate()  
  15.         return self._result.returncode  
  16.     @property  
  17.     def stdout(self):  
  18.         self._evaluate()  
  19.         return self._result.stdout  
  20.     def __str__(self):  
  21.         return self.stdout  
  22.     @property  
  23.     def stderr(self):  
  24.         self._evaluate()  
  25.         return self._result.stderr 

我们让它旋转一下: 

  1. ls = PipePy('ls')  
  2. ls_l = PipePy('ls', '-l')  
  3. print(ls)  
  4. <<< files.txt  
  5. # ... main.py  
  6. # ... tags  
  7. print(ls_l)  
  8. <<< total 16  
  9. # ... -rw-r--r-- 1 kbairak kbairak  125 Jan 22 08:53 files.txt  
  10. # ... -rw-r--r-- 1 kbairak kbairak 5425 Feb  1 21:54 main.py  
  11. # ... -rw-r--r-- 1 kbairak kbairak 1838 Feb  1 21:54 tags 

使其看起来更像“命令式”

不用每次我们要自定义命令时都去调用PipePy。 

  1. ls_l = PipePy('ls', '-l')  
  2. print(ls_l) 

相当于 

  1. ls = PipePy('ls')  
  2. print(ls('-l')) 

换句话说,我们要使: 

  1. PipePy('ls', '-l') 

相当于 

  1. PipePy('ls')('-l') 

值得庆幸的是,我们的类创建了惰性对象这一事实在很大程度上帮助了我们: 

  1. class PipePy:  
  2.     # __init__, etc  
  3.     def __call__(self, *args):  
  4.         args = self._args + args  
  5.         return self.__class__(*args)  
  6. ls = PipePy('ls')  
  7. print(ls('-l'))  
  8. <<< total 16  
  9. # ... -rw-r--r-- 1 kbairak kbairak  125 Jan 22 08:53 files.txt  
  10. # ... -rw-r--r-- 1 kbairak kbairak 5425 Feb  1 21:54 main.py 
  11. # ... -rw-r--r-- 1 kbairak kbairak 1838 Feb  1 21:54 tags 

关键字参数

如果要向ls传递更多参数,则可能会遇到--sort = size。我们可以轻松地执行ls('-l','--sort = size')。我们可以做得更好吗? 

  1.  class PipePy:  
  2. -    def __init__(self, *args):  
  3. +    def __init__(self, *args, **kwargs):  
  4.          self._args = args  
  5. +        self._kwargs = kwargs  
  6.          self._result = None  
  7.      def _evaluate(self):  
  8.          if self._result is not None:  
  9.              return  
  10. -        self._result = subprocess.run(self._args,  
  11. +        self._result = subprocess.run(self._convert_args(),  
  12.                                        capture_output=True 
  13.                                        text=True 
  14. +    def _convert_args(self):  
  15. +        args = [str(arg) for arg in self._args]  
  16. +        for key, value in self._kwargs.items(): 
  17. +            keykey = key.replace('_', '-')  
  18. +            args.append(f"--{key}={value}")  
  19. +        return args  
  20. -    def __call__(self, *args):  
  21. +    def __call__(self, *args, **kwargs):  
  22.          args = self._args + args  
  23. +        kwargs = {**self._kwargs, **kwargs}  
  24. -        return self.__class__(*args)  
  25. +        return self.__class__(*args, **kwargs)  
  26.      # returncode, etc 

让我们来旋转一下: 

  1. print(ls('-l'))  
  2. <<< total 16  
  3. # ... -rw-r--r-- 1 kbairak kbairak  125 Jan 22 08:53 files.txt  
  4. # ... -rw-r--r-- 1 kbairak kbairak 5425 Feb  1 21:54 main.py  
  5. # ... -rw-r--r-- 1 kbairak kbairak 1838 Feb  1 21:54 tags  
  6. print(ls('-l', sort="size"))  
  7. <<< total 16  
  8. # ... -rw-r--r-- 1 kbairak kbairak 5425 Feb  1 21:54 main.py  
  9. # ... -rw-r--r-- 1 kbairak kbairak 1838 Feb  1 21:54 tags  
  10. # ... -rw-r--r-- 1 kbairak kbairak  125 Jan 22 08:53 files.txt 

Piping

事情开始变得有趣起来。我们的最终目标是能够做到: 

  1. ls = PipePy('ls')  
  2. grep = PipePy('grep')  
  3. print(ls | grep('tags'))  
  4. <<< tags 

我们的过程是:

1、让__init__和__call__方法接受一个仅用于关键字的新_pipe_input关键字参数,该参数将保存在self上。

2、在评估期间,如果设置了_pipe_input,它将作为输入参数传递给subprocess.run。

3、重写__or__方法以将左操作数的结果作为pipe输入传递给右操作数。 

  1.  class PipePy:  
  2. -    def __init__(self, *args, **kwargs):  
  3. +    def __init__(self, *args, _pipe_input=None, **kwargs):  
  4.          self._args = args  
  5.          self._kwargs = kwargs  
  6. +        self._pipe_input = _pipe_input  
  7.          self._result = None   
  8. -    def __call__(self, *args, **kwargs):  
  9. +    def __call__(self, *args, _pipe_input=None, **kwargs):  
  10.          args = self._args + args  
  11.          kwargs = {**self._kwargs, **kwargs}  
  12. -        return self.__class__(*args, **kwargs)  
  13. +        return self.__class__(*args, _pipe_input_pipe_input=_pipe_input, **kwargs) 
  14.       def _evaluate(self):  
  15.          if self._result is not None:  
  16.              return  
  17.          self._result = subprocess.run(self._convert_args(),  
  18. +                                      input=self._pipe_input,  
  19.                                        capture_output=True 
  20.                                        text=True 
  21. +    def __or__(left, right):  
  22. +        return right(_pipe_input=left.stdout) 

让我们尝试一下(从之前稍微修改命令以证明它确实有效): 

  1. ls = PipePy('ls')  
  2. grep = PipePy('grep') 
  3. print(ls('-l') | grep('tags'))  
  4. <<< -rw-r--r-- 1 kbairak kbairak 1838 Feb  1 21:54 tags 

让我们添加一些简单的东西

1、真实性: 

  1. class PipePy:  
  2.     # __init__, etc  
  3.     def __bool__(self):  
  4.         return self.returncode == 0 

现在我们可以作出如下处理: 

  1. git = PipePy('git')  
  2. grep = PipePy('grep')  
  3. if git('branch') | grep('my_feature'):  
  4.     print("Branch 'my_feature' found") 

2、读取/写入文件: 

  1. class PipePy:  
  2.     # __init__, etc  
  3.     def __gt__(self, filename):  
  4.         with open(filename, 'w') as f:  
  5.             f.write(self.stdout)  
  6.     def __rshift__(self, filename):  
  7.         with open(filename, 'a') as f: 
  8.             f.write(self.stdout)  
  9.     def __lt__(self, filename):  
  10.         with open(filename) as f:  
  11.             return self(_pipe_input=f.read()) 

现在可以作出如下操作: 

  1. ls = PipePy('ls')  
  2. grep = PipePy('grep')  
  3. cat = PipePy('cat')  
  4. ls > 'files.txt'  
  5. print(grep('main') < 'files.txt')  
  6. <<< main.py  
  7. ls >> 'files.txt'  
  8. print(cat('files.txt'))  
  9. <<< files.txt  
  10. # ... main.py  
  11. # ... tags  
  12. # ... files.txt  
  13. # ... main.py  
  14. # ... tags 

3、迭代 

  1. class PipePy:  
  2.     # __init__, etc  
  3.     def __iter__(self):  
  4.         return iter(self.stdout.split()) 

现在可以作出如下操作: 

  1. ls = PipePy('ls')  
  2. for name in ls:  
  3.     print(name.upper())  
  4. <<< FILES.TXT  
  5. # ... MAIN.PY 
  6. # ... TAGS 

4、表格: 

  1. class PipePy:  
  2.     # __init__, etc  
  3.     def as_table(self):  
  4.         lines = self.stdout.splitlines()  
  5.         fields = lines[0].split()  
  6.         result = []  
  7.         for line in lines[1:]:  
  8.             item = {}  
  9.             for i, value in enumerate(line.split(maxsplit=len(fields) - 1)):  
  10.                 item[fields[i]] = value  
  11.             result.append(item)  
  12.         return result 

现在可以作出下面操作: 

  1. ps = PipePy('ps')  
  2. print(ps)  
  3. <<<     PID TTY          TIME CMD  
  4. # ...    4205 pts/4    00:00:00 zsh  
  5. # ...   13592 pts/4    00:00:22 ptipython  
  6. # ...   16253 pts/4    00:00:00 ps  
  7. ps.as_table()  
  8. <<< [{'PID': '4205', 'TTY': 'pts/4', 'TIME': '00:00:00', 'CMD': 'zsh'},  
  9. # ...  {'PID': '13592', 'TTY': 'pts/4', 'TIME': '00:00:22', 'CMD': 'ptipython'},  
  10. # ...  {'PID': '16208', 'TTY': 'pts/4', 'TIME': '00:00:00', 'CMD': 'ps'}] 

5、普通bash实用程序:

在子进程中更改工作目录不会影响当前的脚本或python shell。与更改环境变量相同,以下内容不是PipePy的补充,但很不错: 

  1. import os  
  2. cd = os.chdir  
  3. export = os.environ.__setitem__  
  4. pwd = PipePy('pwd')  
  5. pwd  
  6. <<< /home/kbairak/prog/python/pipepy  
  7. cd('..')  
  8. pwd  
  9. <<< /home/kbairak/prog/python 

使事情看起来更shell-like

如果我在交互式shell中,则希望能够简单地键入ls并完成它。 

  1. class PipePy:  
  2.     # __init__, etc  
  3.     def __repr__(self):  
  4.         return self.stdout + self.stderr 

交互式shell 

  1. >>> ls = PipePy('ls')  
  2. >>> ls  
  3. files.txt  
  4. main.py  
  5. tags 

我们的实例是惰性的,这意味着如果我们对它们的结果感兴趣,则将对它们进行评估,此后不再进行评估。如果我们只是想确保已执行该操作怎么办?例如,假设我们有以下脚本: 

  1. from pipepy import PipePy  
  2. tar = PipePy('tar')  
  3. tar('-xf', 'some_archive.tar')  
  4. print("File extracted") 

该脚本实际上不会执行任何操作,因为tar调用实际上并未得到评估。我认为一个不错的惯例是,如果不带参数调用__call__强制求值: 

  1.  class PipePy:  
  2.      def __call__(self, *args, _pipe_input=None, **kwargs):  
  3.          args = self._args + args  
  4.          kwargs = {**self._kwargs, **kwargs}  
  5. -        return self.__class__(*args, _pipe_input_pipe_input=_pipe_input, **kwargs)  
  6. +        result = self.__class__(*args, _pipe_input_pipe_input=_pipe_input, **kwargs)  
  7. +        if not args and not _pipe_input and not kwargs:  
  8. +            result._evaluate()  
  9. +        return result 

因此在编写脚本时,如果要确保实际上已调用命令,则必须用一对括号来调用它: 

  1.  from pipepy import PipePy  
  2.  tar = PipePy('tar')  
  3. -tar('-xf', 'some_archive.tar')  
  4. +tar('-xf', 'some_archive.tar')()  
  5.  print("File extracted") 

但是,我们还没有解决问题。考虑一下: 

  1. date = PipePy('date')  
  2. date  
  3. <<< Mon Feb  1 10:43:08 PM EET 2021  
  4. # Wait 5 seconds  
  5. date  
  6. <<< Mon Feb  1 10:43:08 PM EET 2021 

不好!date没有改变。date对象将其_result保留在内存中。随后的评估实际上不会调用该命令,而只是返回存储的值。

一种解决方案是通过使用空括号来强制创建副本: 

  1. date = PipePy('date')  
  2. date()  
  3. <<< Mon Feb  1 10:45:09 PM EET 2021  
  4. # Wait 5 seconds  
  5. date()  
  6. <<< Mon Feb  1 10:45:14 PM EET 2021 

另一个解决方案是:由PipePy构造函数返回的实例不应该是惰性的,但由__call__调用返回的实例将是惰性的。 

  1.  class PipePy:  
  2. -    def __init__(self, *args, _pipe_input=None, **kwargs):  
  3. +    def __init__(self, *args, _pipe_input=None_lazy=False, **kwargs):  
  4.          self._args = args  
  5.          self._kwargs = kwargs  
  6.          self._pipe_input = _pipe_input  
  7. +        self._lazy = _lazy  
  8.          self._result = None  
  9.      def __call__(self, *args, _pipe_input=None, **kwargs):  
  10.          args = self._args + args  
  11.          kwargs = {**self._kwargs, **kwargs}  
  12. -        result = self.__class__(*args, _pipe_input_pipe_input=_pipe_input, **kwargs)  
  13. +        result = self.__class__(*args, 
  14. +                                _pipe_input_pipe_input=_pipe_input,  
  15. +                                _lazy=True 
  16. +                                **kwargs) 
  17.          if not args and not _pipe_input and not kwargs:  
  18.              result._evaluate()  
  19.          return result  
  20.      def _evaluate(self):  
  21. -        if self._result is not None:  
  22. +        if self._result is not None and self._lazy:  
  23.              return  
  24.          self._result = subprocess.run(self._convert_args(),  
  25.                                        input=self._pipe_input,  
  26.                                        capture_output=True 
  27.                                        text=True

旋转一下: 

  1. date = PipePy('date')  
  2. date  
  3. <<< Mon Feb  1 10:54:09 PM EET 2021  
  4. # Wait 5 seconds  
  5. date  
  6. <<< Mon Feb  1 10:54:14 PM EET 2021 

并且可以预见的是,使用空调用的返回值将具有之前的行为: 

  1. date = PipePy('date') 
  2. d = date()  
  3.  
  4. <<< Mon Feb  1 10:56:21 PM EET 2021  
  5. # Wait 5 seconds  
  6.  
  7. <<< Mon Feb  1 10:56:21 PM EET 2021 

没关系 您不会期望d会更新其值。

越来越危险

好吧,ls('-l')不错,但是如果我们像人类一样简单地做ls -l,那就太好了。嗯,我有个主意: 

  1. class PipePy:  
  2.     # __init__, etc  
  3.     def __sub__(left, right):  
  4.         return left(f"-{right}") 

现在可以作如下操作: 

  1. ls = PipePy('ls')  
  2. ls - 'l'  
  3. <<< total 16  
  4. # ... -rw-r--r-- 1 kbairak kbairak   46 Feb  1 23:04 files.txt  
  5. # ... -rw-r--r-- 1 kbairak kbairak 5425 Feb  1 21:54 main.py  
  6. # ... -rw-r--r-- 1 kbairak kbairak 1838 Feb  1 21:54 tags 

我们还有一步: 

  1. l = 'l'  
  2. ls -l 

现在无济于事: 

  1. import string  
  2. for char in string.ascii_letters:  
  3.     if char in locals():  
  4.         continue 
  5.     locals()[char] = char  
  6. class PipePy:  
  7.     # __init__, etc 

更危险的事情

用locals()给了我一个灵感。为什么我们必须一直实例化PipePy?我们无法在路径中找到所有可执行文件,并根据它们创建PipePy实例吗?我们当然可以! 

  1. import os  
  2. import stat  
  3. for path in os.get_exec_path():  
  4.     try:  
  5.         names = os.listdir(path)  
  6.     except FileNotFoundError:  
  7.         continue  
  8.     for name in names:  
  9.         if name in locals():  
  10.             continue  
  11.         if 'x' in stat.filemode(os.lstat(os.path.join(path, name)).st_mode):  
  12.             locals()[name] = PipePy(name) 

因此,现在,将我们拥有的所有内容都放在一个python文件中,并删除脚本(这是实际bash脚本的转录): 

  1. from pipepy import mysqladmin, sleep, drush, grep  
  2. for i in range(10):  
  3.     if mysqladmin('ping',  
  4.                   host="mysql_drupal7" 
  5.                   user="user" 
  6.                   password="password"):  
  7.         break  
  8.     sleep(1)()  # Remember to actually invoke  
  9. if not drush('status', 'bootstrap') | grep('-q', 'Successful'):  
  10.     drush('-y', 'site-install', 'standard',  
  11.           db_url="mysql://user:password@mysql_drupal7:3306/drupal" 
  12.           acount_pass="kbairak")()  # Remember to actually invoke  
  13. drush('en', 'tmgmt_ui', 'tmgmt_entity_ui', 'tmgmt_node_ui')()  

 

责任编辑:庞桂玉 来源: Python中文社区 (ID:python-china)
相关推荐

2016-07-29 11:06:48

编程PythonShell

2021-05-26 10:21:31

Python音乐软件包

2018-03-22 11:00:45

PythonRSS

2018-10-19 09:50:15

Linuxman手册Linux命令

2017-05-23 14:34:58

python大数据UUID

2014-03-06 09:23:19

Git服务器Github

2022-02-15 09:40:45

提示符Starship

2017-03-07 17:12:46

LinuxUbuntu发行版

2012-02-27 13:56:19

Java服务器

2022-05-17 12:45:31

LinuxLinux发行版

2021-06-23 16:40:58

JavaTomcatWeb

2021-08-10 09:01:48

Python网络爬虫自动化

2021-01-06 18:10:22

ShellLoki系统运维

2016-09-27 11:31:34

JavaScript编程语言

2010-03-12 18:42:58

Python目录

2018-05-21 14:44:33

LinuxshellPython

2011-05-17 10:46:14

TAP

2019-04-04 14:23:08

GTK2GTK3Linux

2019-04-08 16:41:55

Oomox图形应用Linux

2011-07-07 10:39:10

yum源createrepo
点赞
收藏

51CTO技术栈公众号