使用 Python 編寫腳本並發布
P1: 腳本
通常在 Linux 服務器上會遇到在命令行中輸入命令的操作,而有些操作包含的命令數目較多或者其中的命令包含的參數較多,如果一個一個的敲命令的話就太麻煩了,有幾種做法可以簡化操作:
- 使用 alias 為命令編寫別名,比如我之前開發一個網站程序 minor-sspymgr 時,經常需要上傳修改后的代碼,更新服務器上的代碼,重啟網站程序。為了方便,我定義一個 alias 別名命令:
alias updateMgr='cd ~/minor-sspymgr/ && git pull origin master && pm2 restart sspymgr'
這種方法的優點是很方便簡單,把常用的命令組合到一起,用一個別名表示即可使用。當然,它的缺點也很明顯,就是不能執行一些復雜的命令,而且難以將參數應用到命令中。
- 編寫一個 .sh 腳本,我之前寫簡單腳本時用的都是 bash,對於上述例子,用 sh 腳本編寫起來也很類似,可以簡單的把上述命令以
&&拆分為 3 行:
#!/bin/bash
cd ~/minor-sspymgr/
git pull origin master
pm2 restart sspymgr
當然了,實際的腳本遠非如此簡單,可以用 $1 ... $n 獲取命令行參數,執行一些更復雜的邏輯。
為什么用 Python 寫腳本
既然用 bash 已經可以編寫一些腳本了,那么為什么我還要用 Python 編寫腳本呢?原因有兩個:1. bash 用的不多,學的也早,而且 bash 和常用的編程語言的語法有些差別,很多內容都容易忘記,用 Python 寫的話也可以熟悉 Python(盡管我是從 flask 制作一個 WSGIServer 開始使用 Python 的),2. Python 的庫很多也很方便使用,編寫腳本給人一種流暢的感覺。當然最主要的原因就是因為 Python 里面有 argparse 這個庫,對於解析命令行參數來說十分方便。
如何解析命令行參數
bash 腳本里面獲取命令行參數的方式很簡單,但是解析起來卻比較麻煩,如果有 bash 解析命令行參數的庫,希望可以推薦給我。不過即使有這樣的庫,估計我還是會選擇 Python 來編寫腳本了。在 Python 里解析命令行參數的模塊比較多,有 getopt、optparse、argparse 等 1。目前我只用過 argparse,因此這里也只會涉及到如何用 argparse 來解析命令行參數。至於前兩個模塊,如果有興趣的話,可以自行了解。argparse 使用起來非常簡單:
from argparse import ArgumentParser
parser = ArgumentParser(description="""Run this script under SUPER USER privilege. Create or remove git repository
under direcotry: {}. Currently only works for Linux. Most operations are only tested under Linux.
Script version : {}.
""".format(config["repo_dir"], __version__))
parser.add_argument("-V", "--version", help="Print this script version", action="store_true")
args = parser.parse_args()
if args.version:
print("Script version is {}".format(__version__))
上面這幾行代碼就做好了一個簡單的命令行解析功能,當我們輸入 script.py -V 的時候就會打印出腳本的版本。不需要在意命令行參數的位置或者是否必須要加上這個參數,只需要將注意力放在代碼的邏輯上即可,argparse 這個模塊可以極大的方便我們解析命令行參數。
P2: 示例:初始化 git 倉庫的腳本
之前用 git 搭建過源代碼管理服務器 2、3。如果要在這樣的服務器上添加一個共享倉庫,就需要在倉庫根目錄下面執行一些操作了。比如說,我的 git 倉庫存放的目錄是 /src 這個目錄下面的所有文件及文件夾的屬主和屬組都是 git,要想新建一個共享倉庫,最開始我的做法是:
- cd /src
- mkdir newrepo && cd newrepo
- git init --bare --shared
- cd ..
- sudo chown -R git:git newrepo && sudo chmod -R g+rwx newrepo
步驟還是有點多的,每次都要敲這么多命令很麻煩,把這些命令寫到 bash 腳本中,比如叫做 gitrepo.sh,給它加上 x 執行權限,在終端里輸入 sudo gitrepo.sh reponame 就能完成上面的操作。
用 Bash 編寫的腳本
Bash 腳本,編寫簡單,就是把在命令行中敲過的命令依次寫到文件中即可,但是對於不常寫 bash 腳本的我來說,要編寫一個完備的腳本,並且要包含參數、異常處理等邏輯來說比較麻煩:
#!/bin/bash
# filename: gitrepo.sh
# @author BriFuture
# @details create bare and shared repository within /src directory
repoName=$1
if [ -z "$repoName" ]; then
echo "Repo is empty!"
exit 1
fi
dotpos=`expr index "$repoName" "."`
if [ "$dotpos" -gt 0 ]; then
echo "found";
else
repoName=$repoName".git"
fi
# go to the repository dir
cd /src
mkdir $repoName
cd $repoName
echo "Init git repo in $repoName"
git init --bare --shared
# change the own of repository
cd ../
sudo chown -R git:git $repoName
sudo chmod -R g+rwx $repoName
用 Python 編寫的腳本
實際上,我用 Python 重新編寫這個腳本時,在參數中加入了更多的可選項,此前的 bash 腳本只能夠添加倉庫,這個全新的腳本加入了其他操作:添加新的倉庫,列出所有倉庫,刪除一個倉庫。並且完善了命令行的幫助信息,為腳本添加了配置文件以及輸出日志。
另外在執行腳本的過程中需要用到超級用戶的權限,因此在腳本中添加了檢查當前用戶權限的功能:
def is_root():
if hasattr(os, "getuid"):
return os.getuid() == 0
return False
該 Python 腳本的主要功能是添加和刪除倉庫(目錄),添加倉庫的操作如下,分別是創建目錄,執行 git init --bare --shared 命令,更改文件屬主和屬組的操作(實際的腳本中包含一些日志記錄和異常處理的代碼):
from pathlib import Path
def createRepo(repo: Path):
if repo.exists():
return
repo.mkdir(parents=True)
subprocess.run(["git", "init", "--bare", "--shared", str(repo.absolute())])
shutil.chown(repo, config["user"], config["group"])
刪除倉庫的操作更加簡單,調用 shutil 遞歸刪除文件夾即可,不能使用 repo.rmdir() 函數,因為一般倉庫文件夾是非空的:
import shutil
def deleteRepo(repo: Path):
if not repo.exists():
return
shutil.rmtree(str(repo))
羅列所有倉庫使用 Path.iterdir() 函數即可。
這樣一個可以使用的腳本就制作完成了,如果只是單純的編寫一個腳本的話,可以在文件的開頭加上 #!/usr/bin/python3 這樣的標記,表明這是一個可執行的 Python3 腳本,在 linux 系統上給它加上 x 權限,比如這個文件的名稱為 gitrepo.py,我們可以在終端中輸入 gitrepo.py -h 或者 python3 gitrepo.py -h 查看這個命令的幫助信息。
詳細的代碼可以在 Github 上的 gitrepo.py 文件中查看。
P3: 以 wheel 包的形式發布
前面說過,我們可以把寫好的 Python 文件當做腳本執行,但是每次都要敲 gitrepo.py -h 這樣的命令,能不能就像平時用 ls 這些命令一樣直接敲 gitrepo -h 呢?最簡單的是用 ln 建立軟鏈接:sudo ln -s /path/to/gitrepo.py /usr/bin/gitrepo,但是這里我們可以借助 pypi 發布我們已經寫好的命令,將我們的腳本發布到 pypi 上,那么還可以在沒有該腳本的機器上利用 pip 進行安裝。
以 brifuture-facilities 這個項目為例,在項目的根目錄下面新建一個 setup.py 文件,輸入以下內容:
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
with open('requirements.txt', "r", encoding="utf-8") as f:
requires = f.read().splitlines()
from myfacilities import __version__
setup(
name = "brifuture-facilities",
packages = find_packages(where='.'),
version = __version__,
entry_points = {
"console_scripts": [
'bf_broadcast = myfacilities.broadcast:main',
'bf_gitrepo = myfacilities.gitrepo:main',
]
},
description = "BriFuture's scripts set, all scripts will be written with Python3",
author = "BriFuture",
author_email = "jw.brifuture@gmail.com",
license = "GPLv3",
url = "http://github.com/brifuture/",
install_requires = requires,
include_package_data = True,
zip_safe=True,
exclude_package_data = {'': ['__pycache__']},
# download_url = "",
keywords = [ "webserver", "socks-manager" ],
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3" ,
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent"
],
long_description = long_description,
long_description_content_type="text/markdown",
)
我們需要利用的是 setuptools,從這個模塊中導入 setup,它的參數都很直觀,我們想要添加一個可執行的腳本時,在 entry_points 關鍵字參數中添加即可,如:
entry_points = {
"console_scripts": [
'bf_broadcast = myfacilities.broadcast:main',
'bf_gitrepo = myfacilities.gitrepo:main',
]
},
由於這個項目中的文件是包的一部分,所以我沒法直接使用 python 運行其中的某個腳本,像 python3 myfacilities.gitrepo -h 是無法運行的,但是借助於 setuptools,我們可以將寫好的程序安裝到機器中,python3 setup.py install 可以通過 setup.py 文件進行安裝,也可以使用 pip install . 進行安裝,詳細的教程可以在 文檔 中找到。
接下來制作 wheel 包,命令很簡單:
python3 setup.py sdist bdist_wheel
這樣會將我們的代碼打包到 dist/ 目錄下。
我們需要將制作好的程序發布到 pypi 上,安裝好 twine: pip install twine, 在 $HOME 目錄(linux) 或者 C:\Users\yourname\ 目錄(windows)下新建一個 .pypirc 文件,輸入下面的內容:
[distutils]
index-servers=
pypi
testpypi
[pypi]
repository: https://upload.pypi.org/legacy/
username: yourname
password: yourpasswd
[testpypi]
repository: https://test.pypi.org/legacy/
username: yourname
password: yourpasswd
接下來就可以發布了,但是不要着急,正式發布前都請將制作好的包發布到 testpypi 上:
python3 -m twine upload -r testpypi dist/* --skip-existing
等你確認一切無誤后,在將 -r testpypi 參數換成 -r pypi 以便正式發布。
P4: 小結
-
盡管制作一個小巧、實用而且完備的 Python 腳本很有意思,但是要想做出一個實用、對用戶友好的腳本,還是需要花一些時間的。實際上如果要為用戶提供更加友好的實用方式,可以考慮以界面的形式為用戶提供操作,比如說以網頁的形式提供操作,在服務器后台對用戶操作進行處理。不過你也可能注意到了,這個腳本的功能其實就是很基礎的增刪查改。
-
上面示例的 Python 腳本在使用過程中會用到超級用戶權限,為了防止操作失敗,在添加、刪除倉庫時必須提供超級用戶權限,否則腳本將會退出,還有一種方式可以在運行時獲得超級用戶權限,就是使用 [elevate][https://github.com/barneygale/elevate] 進行提權,具體的使用方式還沒來得及看,不過我猜測應該需要向 elevate 提供一個超級用戶密碼的文件。
如果你想仔細查看上述 Python 示例代碼的話,可以在 Github 上找到 brifuture-facilities 項目的代碼。如果你覺得我的文章或者其中的代碼對你有幫助,請給它點個贊。
參考
掘金:Python中最好用的命令行參數解析工具
cnblogs:搭建簡單的Git服務器
cnblogs:lighttpd 與 gitweb 搭建服務器
setuptools documentation
