使用 Python 编写脚本并发布 使用 Python 编写脚本并发布

P1: 脚本

通常在 Linux 服务器上会遇到在命令行中输入命令的操作,而有些操作包含的命令数目较多或者其中的命令包含的参数较多,如果一个一个的敲命令的话就太麻烦了,有几种做法可以简化操作:

  1. 使用 alias 为命令编写别名,比如我之前开发一个网站程序 minor-sspymgr 时,经常需要上传修改后的代码,更新服务器上的代码,重启网站程序。为了方便,我定义一个 alias 别名命令:
alias updateMgr='cd ~/minor-sspymgr/ && git pull origin master && pm2 restart sspymgr'

这种方法的优点是很方便简单,把常用的命令组合到一起,用一个别名表示即可使用。当然,它的缺点也很明显,就是不能执行一些复杂的命令,而且难以将参数应用到命令中。

  1. 编写一个 .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 里解析命令行参数的模块比较多,有 getoptoptparseargparse1。目前我只用过 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 搭建过源代码管理服务器 23。如果要在这样的服务器上添加一个共享仓库,就需要在仓库根目录下面执行一些操作了。比如说,我的 git 仓库存放的目录是 /src 这个目录下面的所有文件及文件夹的属主和属组都是 git,要想新建一个共享仓库,最开始我的做法是:

  1. cd /src
  2. mkdir newrepo && cd newrepo
  3. git init --bare --shared
  4. cd ..
  5. 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:Usersyourname 目录(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