这篇文章介绍如何写个系统命令以及我为什么要写命令
“一切皆文件”是linux的基本哲学之一,我们在linux下执行的诸如ls
之类的命令实际上都是去执行了系统上的某个文件,which
命令可以查看到我们执行的命令对应的是系统上的哪个文件,例如常用的ls
命令实际上就是执行了/bin/ls
这个文件
root@ops-coffee:~# which ls
/bin/ls
基于此,我们就知道了定义一个命令很简单,只需要写个可执行的文件就行了,python的标准模块argparse
就可以帮助我们快速方便的构建一个用户友好的命令
相比于自己实现个命令文件,argparse
模块能够自动生成帮助和使用手册,并在用户传入无效参数时报错。一个简单的示例如下
#!/usr/bin/env python3
# coding:utf8
import argparse
parser = argparse.ArgumentParser(description='整数处理')
parser.add_argument('integers', type=int, help='要处理整数')
args = parser.parse_args()
print(args.integers)
这个示例的意思是接收一个数字,并将这个数字输出,接下来看一下详细的解释
首先创建了一个ArgumentParser
对象,ArgumentParser
对象有很多参数可以选择,这里的description
定义在参数帮助文档之前显示的文本,通常用来定义这个程序做什么以及怎么做
parser = argparse.ArgumentParser(description='整数处理')
然后通过add_argument
给ArgumentParser
对象添加参数,第一个参数integers
为参数名,type
指定类型为int
,help
指定这个字段的帮助信息
parser.add_argument('integers', type=int, help='要处理的整数')
通过调用ArgumentParser
对象的parse_args
方法返回一个具有所有参数属性的对象
args = parser.parse_args()
最后通过args.参数名
获取到传入的参数值
print(args.integers)
我们将以上文件命名为opscoffee
,并赋予执行权限,放在系统环境变量/bin
下,就可以当作命令直接执行了
# chmod +x opscoffee
# mv opscoffee /bin/
如果直接执行opscoffee
命令的话将会收到一个报错,提示你必须有一个参数integers
# opscoffee
usage: opscoffee [-h] integers
opscoffee: error: the following arguments are required: integers
同时也通过usage
告诉了你这个命令的用法,默认有一个-h
参数可以打印命令帮助
# opscoffee -h
usage: opscoffee [-h] integers
整数处理
positional arguments:
integers 要处理的整数
optional arguments:
-h, --help show this help message and exit
这就是使用argparse
模块的好处,自动生成帮助,提供友好的使用体验
argparse
能实现的远不止于此,还有更加强大的功能,主要在于add_argument
方法参数的运用,以下以一些例子来学习下一些常用的参数
当我们在参数名前添加-
或者--
时,argparse
会默认认为这是一个可选参数,可以不传值,例如
parser.add_argument('--age', type=int, help='年龄')
可选参数当没有传值时的默认值为None
,可以通过default
来设置默认值
parser.add_argument('--age', type=int, default=37, help='年龄')
当没有参数--age
时,显示default
设置的值(没有设置default
则显示None),有--age
则显示--age
指定的值
# opscoffee
37
# opscoffee --age 38
38
如果你想让可选参数也变成必选的,则只需要设置required=True
即可
parser.add_argument('--age', type=int, default=37, required=True, help='年龄')
type
用来指定参数的类型,允许任何类型检查和类型转换,例如str
、int
、float
、open
甚至是你自定义的方法都可以。
#!/usr/bin/env python3
# coding:utf8
import argparse
def even(string):
value = int(string)
if value%2!=0:
msg = "%r 不是偶数" % string
raise argparse.ArgumentTypeError(msg)
return value
parser = argparse.ArgumentParser(description='整数处理')
parser.add_argument('integers', type=even, help='要处理的整数')
args = parser.parse_args()
print(args.integers)
以上命令接收一个参数integers
,并将其type
设置为了自定义方法even
,这个方法会判断用户输入的数字是否是偶数,如果不是则报错,执行结果如下
# opscoffee 1
usage: opscoffee [-h] integers
opscoffee: error: argument integers: '1' 不是偶数
# opscoffee 2
2
choices
参数可以限制参数的范围,例如我们只想让用户从ops
或coffee
两个参数中选择一个输入,则可以这样用
parser.add_argument('site', choices=['ops','coffee'], help='Site')
那么当我们输入的内容不是ops
或coffee
时,则报错
# opscoffee cn
usage: opscoffee [-h] {ops,coffee}
opscoffee: error: argument site: invalid choice: 'cn' (choose from 'ops', 'coffee')
通常我们在argparse
中定义的参数数量与传入的参数数量应当相等,但有些时候我们需要接收未知数量的参数,nargs
就可以帮助我们
nargs支持以下值:N
(整数)、'?'
、'*'
、'+'
、argarse.REMAINDER
N:
表示N个参数会被聚集到一个列表中,例如
import argparse
parser = argparse.ArgumentParser(description='整数处理')
parser.add_argument('integers', type=int, nargs=2, help='要处理的整数')
args = parser.parse_args()
print(args.integers)
将输出
# opscoffee 9 10
[9, 10]
'?':
表示使用一个或不使用参数,当不传参数时,默认为None
parser.add_argument('integers', type=int, nargs='?', help='要处理的整数')
'*':
表示使用所有参数,参数个数可以为0
parser.add_argument('integers', type=int, nargs='*', help='要处理的整数')
'+':
与'*'
类似,但至少要有一个参数,否则将会报错
parser.add_argument('integers', type=int, nargs='+', help='要处理的整数')
先说我为什么要写个系统命令?
文章『Probius:一个功能强大的自定义任务系统』中介绍了我们的自定义任务系统,这个系统可以用来编排任务,而在编排CICD任务中会用到配置文件,我们的配置文件都是通过Kerrigan配置中心来管理的,目前获取配置中心的配置主要有两种方法
脚本里如果想要使用配置中心的配置,则只能通过API的方式去获取,这样就要在每个需要用到配置的地方写一段代码来获取及处理,不仅会出现大量的重复代码,并且非常的不优雅,更为重要的是请求API的Token将会出现在脚本里,带来一定的安全风险
基于以上考虑,写个自定义命令来做这件事情更为妥当,于是便写了下边这个命令
#!/usr/bin/env python3
# coding:utf8
# 这是一个系统命令用来获取kerrigan配置中心的配置并写入本地文件,需要将此文件copy到目录/bin下
import sys
import argparse
import requests
parser = argparse.ArgumentParser(description='获取配置中心Kerrigan配置')
parser.add_argument('configkey', type=str, help='配置中心中文件的Key')
parser.add_argument('localfile', type=str, help='保存到本地文件的路径')
args = parser.parse_args()
# 获得传入的参数
configkey = args.configkey
localfile = args.localfile
header = {
'Authorization': 'Token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.JTdCJTIyZXhwJTIyJTNBMTkwNjcwNzAyMCUyQyUyMmlhdCUyMiUzQTE1OTEzNDcwMjAlMkMlMjJkYXRhJTIyJTNBJTdCJTIydXNlcm5hbWUlMjIlM0ElMjJwcm9iaXVzQG9wcy1jb2ZmZWUuY24lMjIlN0QlN0Q.ops1ZNhq19XSEL2PUo-iQqzbhimDnpFiYc_7EUXftF4'}
uri = 'http://kerrigan.ops-coffee.cn/api/config/?key=' + configkey
r = requests.get(uri, headers=header)
if r.json()['state']:
content = r.json()['message']['content']
try:
with open(localfile, 'w') as f:
f.write(content)
sys.exit(0)
except Exception as e:
print('write local file failed: ', str(e))
sys.exit(3)
else:
print('get config failed: ', r.json()['message'])
sys.exit(1)
以上代码的意思是根据传入的key和file路径,去配置中心获取对应配置文件的内容并写入到本地file中。需要注意的是exit
,返回合适的退出状态是个很好的习惯,这样我们就可以通过$?
来获取命令执行成功还是失败
将此文件命名为getconfig并移动到/bin目录下添加执行权限,就可以在系统任何地方使用getconfig
命令了
getconfig /conf/coffee/prod/docker/Dockerfile /home/project/coffee/Dockerfile