python製作命令列工具——fire
阿新 • • 發佈:2020-12-14
**前言**
本篇教程的目的是希望大家可以通讀完此篇之後,可以使用python製作一款符合自己需求的linux工具。
本教程使用的是google開源的python第三方庫:fire
無論是學生黨自己做著練手,還是工作中確有需求,本篇都儘可能通過簡單的例子來示範該第三方庫的用法,其中若有描述不當的地方,望留言指出。
------
### 一、快速介紹
來一波官方介紹。
> - Python Fire是一個庫,用於從任何Python物件自動生成命令列介面。
> - 是用python建立CLI的一種簡單方法。
> - 是開發和除錯Python程式碼的一個有用工具。
> - Python Fire幫助探索現有程式碼或將其他人的程式碼轉換為CLI。
> - 使得Bash和Python之間的轉換更加容易。
> - 通過使用已經匯入和建立的模組和變數來設定REPL, Python Fire使使用Python REPL變得更容易。
沒聽懂 **???**
不是太明白 **???**
不要緊,看完本篇就懂了。
------
### 二、快速安裝
- pip安裝:`pip install fire`
- conda安裝:`conda install fire -c conda-forge`
- 原始碼安裝:
```
1. git clone https://github.com/google/python-fire.git
2. cd python-fire
3. python setup.py install
```
Github地址:[python-fire](https://github.com/google/python-fire.git)
------
### 三、快速上手
實踐出真知
建立一個test.py檔案,寫入以下內容
```python
import fire
def test(your_var="default_value"):
return 'This is a test ! value : %s' % your_var
if __name__ == '__main__':
fire.Fire(test)
```
咱們來看一下效果
```shell
# 預設引數
root@node:~# python test.py
This is a test ! value : default_value
# 關鍵字引數
root@node:~# python test.py --your_var=newValue
This is a test ! value : newValue
# 位置引數
root@node:~# python test.py localtionValue
This is a test ! value : localtionValue
```
現在呢,我們反過頭來看一下官方介紹的第一行:
> Python Fire是一個庫,用於從任何Python物件自動生成命令列介面。
注意關鍵字:任何python物件。這意味著什麼?
我們來看一段程式碼:
```
import fire
boy_name = 'XiaoMing'
girl_name = 'XiaoHong'
if __name__ == '__main__':
fire.Fire()
```
試一下:`python test.py boy_name`
是不是明白了些什麼。
聊完這**預設引數**、**關鍵字引數**、**位置引數**,當然不能少了 ***args** 和 ** **kwargs** .
還是來看程式碼示例:
```python
import fire
def run(*args):
arg_list = list(args)
return ' | '.join(arg_list)
if __name__ == '__main__':
fire.Fire(run)
```
跑一下就懂啦
```
root@node:~# python test.py run qwe rty uio asd fgh
qwe | rty | uio | asd | fgh
```
官方給的示例是這個樣子的~~~
```python
import fire
def order_by_length(*items):
"""Orders items by length, breaking ties alphabetically."""
sorted_items = sorted(items, key=lambda item: (len(str(item)), str(item)))
return ' '.join(sorted_items)
if __name__ == '__main__':
fire.Fire(order_by_length)
```
就是加了個長度和字母順序的排序,來跑一下,看一下效果:
```shell
$ python example.py dog cat elephant
cat dog elephant
```
除此之外呢,我們還可以給輸出結果加點料,還是剛才我們寫的那個例子:
```
root@node:~# python test.py run qwe rty uio asd fgh - upper
QWE | RTY | UIO | ASD | FGH
```
在這裡,我們通過命令列對傳入的物件和呼叫結果執行相同的操作,譬如這裡的 `upper`
敲黑板劃重點:**分隔符 “ - ” 之後的所有引數都將用於處理函式的結果,而不是傳遞給函式本身。預設的分隔符是連字元 “ - ”。**
預設的分隔符也是可以改的,用到了`fire`的內建引數。
```
root@node:~# python test.py run qwe rty uio asd fgh X upper -- --separator=X
QWE | RTY | UIO | ASD | FGH
```
其中的`separator`就是fire的一個內建引數,更多內建引數文末有提到。
我們再來看一下`fire`給我們提供的命令列傳參時,**資料的型別**。比較特殊的是,`fire`根據值決定型別。
```python
import fire
fire.Fire(lambda obj: type(obj).__name__)
```
如果有剛學python的小夥伴,記得一定要學一下`lambda`函式,在這裡我可以轉化為普通寫法。
```
import fire
def test(obj):
return type(obj).__name__
if __name__ == '__main__':
fire.Fire(test)
```
通過簡單的一行程式碼來看一下各種資料型別如何通過命令列傳參:
```shell
$ python example.py 10
int
$ python example.py 10.0
float
$ python example.py hello
str
$ python example.py '(1,2)'
tuple
$ python example.py [1,2]
list
$ python example.py True
bool
$ python example.py {name:David}
dict
```
但是當你想傳遞一個str型別的10,你就要注意了,看以下例子:
```shell
$ python example.py 10
int
$ python example.py "10"
int
$ python example.py '"10"'
str
$ python example.py "'10'"
str
$ python example.py \"10\"
str
```
我們可以看到,你雖然敲了`"10"`,但是依然被判定為`int`,bash會自動處理掉你引數的第一層引號。所以,如果想傳`str`型別的10,要再加一層引號,單雙引號分開用,或者把引號轉義。
如果要傳的是dict引數,那就更要小心謹慎了。
```shell
# 標準寫法
$ python example.py '{"name": "David Bieber"}'
dict
# 要這麼寫也沒啥問題
$ python example.py {"name":'"David Bieber"'}
dict
# 但要這麼寫就解析成了字串了
$ python example.py {"name":"David Bieber"}
str
# 再加個空格,字串都不是了
$ python example.py {"name": "David Bieber"} # Wrong. This isn't even treated as a single argument.
```
到這裡,我想大家應該大概明白了 fire 的方便快捷之處。
到了這一步的時候,雖然實現了基本功能,但還是和平時我們使用的 linux 命令列工具有很大的區別:
1. 每次跑命令都要再敲一個python
2. 每次還要指向指定的py檔案或到指定的目錄下
首先說第一個問題,每次多敲六個字母和一個空格,作為一個linux命令列工具是非常不合格的,本來命令列工具就在追求簡單化,這種指定直譯器的操作我們當然要儘可能省掉咯
第二個問題,老是指定檔案的目錄就更麻煩了,日常使用的時候在不同的伺服器跑命令還要想想放在哪裡,而且如果使用絕對路徑的話,更會導致命令的冗長。
**下面我們來解決一下這兩個“小”問題:**
1. 在檔案的第一行指定python直譯器,這樣就無需在我們執行該檔案時再指定直譯器
```python
#!/usr/bin/python
import fire
def test(your_var="default_value"):
return 'This is a test ! value : %s' % your_var
if __name__ == '__main__':
fire.Fire(test)
```
2. 增加檔案的可執行許可權
```shell
root@node:~# chmod +x test.py
```
3. 美化以下,去掉小尾巴(僅僅是給檔案改了個名字, 這一步非必須)
```
root@node:~# mv test.py mytool
```
4. 做個軟連線,可以隨時隨地找得到該命令
```
root@node:~# ln -s /root/mytool /usr/bin/mytool
```
***附:如果需要指定編碼的話,可以在檔案頭部加一行,比如***
```python
#!/usr/bin/python
# coding: utf-8
```
這個時候,我們隨意在伺服器的任意位置執行
```
root@node:~# mytool
This is a test ! value : default_value
root@node:~# mytool --your_var=newValue
This is a test ! value : newValue
root@node:~# mytool localtionValue
This is a test ! value : localtionValue
```
**Perfection !**
如果你已經走到這一步的話,其實已經能寫很多簡單的命令列工具了。
為什麼說簡單呢,目前都是使用函式來完成一個個命令的邏輯,多一個子命令多寫一個函式,慢慢的就會讓這個檔案變的龐雜和冗餘。而且久而久之,肯定會出現一些看起來很相似,卻要使用ctrl + c-v大法去完成的事情。甚至有一些邏輯,想要實現還要自己去做更復雜的邏輯。
------
### 四、快速進階
此時,一年級的已經可以下課了,二年級的請注意聽講了,下面,我們要講的是:
**類的使用**
**命令巢狀**
**屬性訪問**
**鏈式呼叫**
#### 4.1 類的使用
通過一個簡單的算數類來了解其用法,在下列用法中,我們在fire中註冊了一個類物件。
```python
import fire
class Calculator(object):
def add(self, x, y):
return x + y
def multiply(self, x, y):
return x * y
if __name__ == '__main__':
calculator = Calculator()
fire.Fire(calculator)
```
以下是呼叫測試
```python
$ python example.py add 10 20
30
$ python example.py multiply 10 20
200
```
當然我們也可以註冊一個類。
```python
import fire
class Calculator(object):
def add(self, x, y):
return x + y
def multiply(self, x, y):
return x * y
if __name__ == '__main__':
fire.Fire(Calculator)
```
跑一下看看:
```
$ python example.py add 10 20
30
$ python example.py multiply 10 20
200
```
就這?當然不會,我們還可以通過引數控制例項屬性,就像下面的例子:
```python
import fire
class BrokenCalculator(object):
def __init__(self, offset=1):
self._offset = offset
def add(self, x, y):
return x + y + self._offset
def multiply(self, x, y):
return x * y + self._offset
if __name__ == '__main__':
fire.Fire(BrokenCalculator)
```
我們可以看到,新增了一個offset的例項屬性,預設值是1.
```
$ python example.py add 10 20
31
$ python example.py multiply 10 20
201
```
重點來了,我們可以直接給屬性賦值,以此來增加你命令列工具的自由度。
```
$ python example.py add 10 20 --offset=0
30
$ python example.py multiply 10 20 --offset=0
200
```
#### 4.2 命令巢狀
通過不同的類來控制某些同名命令,其實也是將各個命令分門別類更具條理性的管理。可以看到以下用法。
```python
import fire
class Sing:
def run(self):
print('sing sing sing ...')
class Dance:
def run(self):
print('dance dance dance ...')
def status(self):
print('Around.')
class Pipeline:
def __init__(self):
self.sing = Sing()
self.dance = Dance()
def run(self):
self.sing.run()
self.dance.run()
self.dance.status()
if __name__ == '__main__':
fire.Fire(Pipeline)
```
跑跑看:
```shell
# python3 ball.py run
sing sing sing ...
dance dance dance ...
Around.
# python3 ball.py sing run
sing sing sing ...
# python3 ball.py dance run
dance dance dance ...
# python3 ball.py dance status
Around.
```
根據自定義的一個Pipeline類,我們可以自己組合想要的命令列效果,給子命令再分配不同的子集。
#### 4.3 屬性訪問
其實前面說到類的時候已經簡單的說過屬性訪問(就是那個offset的例子,行啦,忘了就不用往上翻了),這裡再詳細舉例說明一下。
```shell
# python3 test.py --age=6 outinfo
Xiao Ming is 6 years old and in the First grade
# python3 test.py --age=7 outinfo
Xiao Ming is 7 years old and in the Second grade
# python3 test.py --age=8 outinfo
Xiao Ming is 8 years old and in the Third grade
```
綜上,我們可以通過控制類的屬性來構造類物件。
嘮到這兒了,再嘮一個騷操作
#### 4.4 鏈式呼叫
官方給的例子不太好看,沒有那麼讓人一眼就看懂的感覺,找了個四則運算的簡單示例:
```python
import fire
class Calculator:
def __init__(self):
self.result = 0
self.express = '0'
def __str__(self):
return f'{self.express} = {self.result}'
def add(self, x):
self.result += x
self.express = f'{self.express}+{x}'
return self
def sub(self, x):
self.result -= x
self.express = f'{self.express}-{x}'
return self
def mul(self, x):
self.result *= x
self.express = f'({self.express})*{x}'
return self
def div(self, x):
self.result /= x
self.express = f'({self.express})/{x}'
return self
if __name__ == '__main__':
fire.Fire(Calculator)
```
函式名呢,`add`、`sub`、`mul`、`div`分別對應 加、減、乘、除四則運算,每個方法都接受 `x` 引數去運算,返回`self`,這樣不論往後鏈式呼叫多少次都可以,結束呼叫到 `__str__` 打印出結果。
`__str__` 在 `fire` 中用來完成自定義序列化。如果不提供這個方法,在鏈式呼叫完成後將會列印幫助內容。
```shell
# python3 test.py add 2 sub 1.5 mul 3 div 2
((0+2-1.5)*3)/2 = 0.75
# python3 test.py add 4 sub 2.5 mul 2 div 4 mul 3 sub 5 add 2
(((0+4-2.5)*2)/4)*3-5+2 = -0.75
```
看完這個大家應該明白鏈式呼叫的運用了,這個時候再來看一下官方示例也許會輕鬆一些。
```python
import fire
class BinaryCanvas(object):
"""A canvas with which to make binary art, one bit at a time."""
def __init__(self, size=10):
self.pixels = [[0] * size for _ in range(size)]
self._size = size
self._row = 0 # The row of the cursor.
self._col = 0 # The column of the cursor.
def __str__(self):
return '\n'.join(' '.join(str(pixel) for pixel in row) for row in self.pixels)
def show(self):
print(self)
return self
def move(self, row, col):
self._row = row % self._size
self._col = col % self._size
return self
def on(self):
return self.set(1)
def off(self):
return self.set(0)
def set(self, value):
self.pixels[self._row][self._col] = value
return self
if __name__ == '__main__':
fire.Fire(BinaryCanvas)
```
跑一下看看:
```shell
$ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 1 0 0 0
0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
```
PS:我要不說,誰能看出來這是個笑臉???
------
### 最後一課
最後看看官方給出的 fire 的內建引數吧,具體怎麼應用大家就自己研究咯。
##### Flags
| Using a CLI | Command | Notes |
| ----------- | --------------------------------- | ---------------------------------------- |
| Help | `command -- --help` | 顯示命令的幫助和使用資訊。 |
| REPL | `command -- --interactive` | 進入互動模式。 |
| Separator | `command -- --separator=X` | 這將分隔符設定為' X '。預設分隔符是“-”。 |
| Completion | `command -- --completion [shell]` | 為CLI生成一個補全的shell指令碼。 |
| Trace | `command -- --trace` | 跟蹤fire命令呼叫後發生了啥子。 |
| Verbose | `command -- --verbose` | 在輸出中包含私有成員。 |
[fire-GitHub地址](https://github.com/google/pyth