一、fire
安装
要使用 Python Fire 库,首先需要安装它。以下是安装步骤:
使用 pip 安装
可以通过 pip 直接安装 Python Fire:
pip install fire
特性
-
自动生成命令行界面:将任何 Python 对象(函数、类、模块、字典等)自动转换为命令行界面。
-
简洁性:只需一行代码即可生成命令行界面,大大减少了开发时间和代码复杂度。
-
灵活性:支持多种数据类型和参数,能够处理复杂的命令行需求。
-
易用性:与 Python 标准库无缝集成,易于上手和使用。
基本功能
将函数转换为命令行工具
可以将一个简单的函数转换为命令行工具:
import fire
def greet(name):
return f'Hello, {name}!'
if __name__ == '__main__':
fire.Fire(greet)
在命令行中运行:
python greet.py John
输出:
Hello, John!
将类转换为命令行工具
可以将一个类转换为命令行工具:
import fire
class Calculator:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
if __name__ == '__main__':
fire.Fire(Calculator)
在命令行中运行:
python calculator.py add 2 3
python calculator.py multiply 2 3
输出:
5
6
将字典转换为命令行工具
可以将一个字典转换为命令行工具:
import fire
operations = {
'add': lambda a, b: a + b,
'multiply': lambda a, b: a * b,
}
if __name__ == '__main__':
fire.Fire(operations)
在命令行中运行:
python operations.py add 2 3
python operations.py multiply 2 3
输出:
5
6
高级功能
处理复杂的数据类型
Python Fire 支持处理复杂的数据类型,如列表、字典等:
import fire
def process_list(items):
return [item.upper() for item in items]
if __name__ == '__main__':
fire.Fire(process_list)
在命令行中运行:
python process_list.py --items a b c
输出:
['A', 'B', 'C']
使用嵌套命令
可以使用嵌套命令来处理复杂的命令行操作:
import fire
class FileManager:
def read(self, filename):
with open(filename, 'r') as file:
return file.read()
def write(self, filename, content):
with open(filename, 'w') as file:
file.write(content)
return f'{filename} has been written.'
if __name__ == '__main__':
fire.Fire(FileManager)
在命令行中运行:
python filemanager.py read test.txt
python filemanager.py write test.txt "Hello, World!"
自定义命令行参数
可以自定义命令行参数,提供更多控制和灵活性:
import fire
def greet(name, greeting='Hello'):
return f'{greeting}, {name}!'
if __name__ == '__main__':
fire.Fire(greet)
在命令行中运行:
python greet.py John --greeting Hi
输出:
Hi, John!
实际应用场景
数据处理脚本
在数据处理脚本中,通过 Python Fire 将函数或类转换为命令行工具,简化数据处理流程。
import fire
import pandas as pd
def process_data(filename):
df = pd.read_csv(filename)
df['processed'] = df['data'] * 2
df.to_csv('processed_' + filename, index=False)
return 'Data processed.'
if __name__ == '__main__':
fire.Fire(process_data)
在命令行中运行:
python process_data.py data.csv
自动化运维脚本
在自动化运维脚本中,通过 Python Fire 将类转换为命令行工具,简化服务器管理和运维操作。
import fire
import os
class ServerManager:
def start(self, service):
os.system(f'systemctl start {service}')
return f'{service} started.'
def stop(self, service):
os.system(f'systemctl stop {service}')
return f'{service} stopped.'
if __name__ == '__main__':
fire.Fire(ServerManager)
在命令行中运行:
python server_manager.py start nginx
python server_manager.py stop nginx
开发测试工具
在开发测试工具中,通过 Python Fire 将函数或类转换为命令行工具,简化测试流程。
import fire
import requests
def test_api(endpoint):
response = requests.get(endpoint)
return response.json()
if __name__ == '__main__':
fire.Fire(test_api)
在命令行中运行:
python test_api.py https://api.example.com/data
二、Linux shell 中传递参数
来自命令行的参数传递
有时候 shell 脚本需要与用户进行交互,传递参数以供脚本读取,这时候就需要了解参数传递的过程。
传递参数的语法:./int.sh $HOME/LearningPerl/* 10
以上向脚本int.sh
传递了两个参数,参数之间及参数和脚本之间用空格分开。参数之间的前后顺序也有规定的,这些称为位置参数,之所以说参数之间的顺序有规定,使因为其存入特殊变量不同,也就导致后续应用参数时的不同。
存入位置参数的特殊变量分别是:$1
是第一个参数,$2
是第二个参数,...,直到第九个参数$9
,而$0
是程序名,也就是脚本名。
因为参数之间是空格间隔的,所以当参数含有空格时,需要引号(单引号或双引号均可)将其圈起来。
#!/bin/bash
echo "I'm learning $1!"
./int.sh 'Linux shell'
# I'm learning Linux shell!
当传入的参数超过 ≥10 个时,这时就会有引用上的歧义,这个后面遇到再说何为歧义,这时候需要用花括号${}
将 10 括起来,让 shell 知道你要引用第 10 个参数。
例如:
#!/bin/bash
echo "The tenth parameter is ${10}"
./int.sh 1 2 3 4 5 6 7 8 9 'Linux shell'
# The tenth parameter is Linux shell!
获取脚本名
虽然$0
可以获取脚本名,但是有两个问题,一是命令会出现在$0
中,比如脚本int.sh
,在$0
却是./int.sh
,多了./
,这不是我们想要的;还有就是,当指定脚本的路径时,$0
也会完整保存路径和脚本名。
比如:bash /home/yan/LearningPerl/int.sh
,$0
保存/home/yan/LearningPerl/int.sh
,而不是int.sh
,很多时候,我们只需要int.sh
就行了,而不想要./
和/home/yan/LearningPerl/
这些附属的字符。
幸运的是,有个basename
命令可以很轻松的去除这些附属字符,只留下脚本名
#!/bin/bash
echo "The basename is $(basename $0)!"
这样,无论是./int.sh
还是bash /home/yan/LearningPerl/int.sh
就只留下int.sh
了,这样,在调用脚本的时候就非常方便了。
测试参数
有时候,脚本设定要求提供参数,而实际中未提供时,会发生语法错误而导致程序崩溃,为了避免这种情况,就需要判断/测试是否提供了相应的参数给脚本使用。
-n
用于测试参数。
#!/bin/bash
if test -n "$1"; then
for line in $( cat $1)
do
echo $line | grep -Eo '\(\w+\)'
done
else
echo "Please provide your file to iterate!"
fi
统计参数的个数
特殊变量$#
储存有命令行提供的参数的个数。获取最后一个命令行参数,需要特别的写法${!#}
。
#!/bin/bash
params=$#
echo The number of parameters is $params
echo The last parameter is ${!#}
./int.sh a b c
输出:
The number of parameters is 3
The last parameter is c
在这里,可以清楚的看到$#
和!#
的区别。获取第 10 命令行参数时,需要用花括号圈起来,如:${10}
还有一种特殊情况,就是没有参数传入时,$#
为 0 可以理解,但是${!#}
会返回脚本名。
./int.sh
输出:
The number of parameters is 0
The last parameter is ./int.sh
bash /home/yan/LearningPerl/int.sh
输出:
The number of parameters is 0
The last parameter is /home/yan/LearningPerl/int.sh
遍历命令行所有参数
bash shell 里面有两个特殊变量,储存命令行的所有参数,但是这两个特殊变量又稍有不同。
./int.sh fred dino barney
$*
这个变量会将所有命令行参数当成一个整体对待,这个整体有所有的命令行参数。
#!/bin/bash
for param in "$*"
do
echo "\$* parameter is $param"
done
# $* parameter is fred dino barney
$@
这个变量会将每一个的命令行参数当成一个个独立的单词,这样可以用 for 循环遍历所有的参数。
#!/bin/bash
for param in "$@"
do
echo "\$* parameter is $param"
done
# $* parameter is fred
# $* parameter is dino
# $* parameter is barney
有个好的方法记住这一特性,那就是在Perl里面@表示数组,自然而然地就会想到能迭代了。
shift 移动参数
shift的功能是将左边第一个参数弹出,而后参数顺移补上。先来看一个简单的例子了解其工作原理:
./int.sh fred dino barney
#!/bin/bash
echo "$*"
shift
echo "$*"
# fred dino barney
# dino barney
第一个echo
打印了所有的参数,而第二个echo
删除了最左边的 fred,这就是shift的工作原理。原先$1
是 fred,shift之后的$1
顺移变成了 dino,所以在使用 shift 后要牢记参数位置发生的变化。
shift命令默认删除最左边的第一个参数(也就是shift 1
是默认的),也可以指定数字来删几个位置的参数。
#!/bin/bash
echo "$*"
shift 2
echo "$*"
echo $1
# fred dino barney
# barney
# barney
shift 2
表示删除了最左边的两个参数,现在只剩最后一个参数了,相应的,$1
也变成了 barney。
命令行选项
之前一直把命令行参数和选项搞混了,现在再来重新确定一下命令行参数和选项的区别。
为了区分选项和参数,Linux 特意将选项设置为单破折线后面跟一个字母,而参数则不需要单破折线。
例如:./int.sh -a sample
,在这里,-a
是选项,sample
是参数。
要想写出一个鲁棒性强的脚本,必须兼顾各种情况,而选项恰恰是提供这种鲁棒性的工具。
-
首先,需要区分是选项还是参数?
这部分内容的实现,需要一个判断语句case,配合之前的shift命令,结合while循环进行判断。
#!/bin/bash
while test -n "$1"
do
case "$1" in
-a) echo "-a is a option" ;;
-b) echo "-b is a option" ;;
-c) echo "-c is a option" ;;
*) echo "$1 is not a option" ;;
esac
shift
done
这样就能识别出特定的选项了。
-
区分选项和参数
之所以要区分选项和参数,原因是没有必要遍历所有的命令行选项和参数,你把所有的命令行选项和参数遍历了一遍,shift命令就将所有的输入删除殆尽了,你要想调用参数已经被删除掉了,这种为了判断选项而遍历所有的输入不值当。换句话说,对于选项而言,我只需要遍历和判断选项,剩下的都当作参数处理。
来看一个具体的例子就知道上面说的啥意思了:
#!/bin/bash
while test -n "$1"
do
case "$1" in
-a) echo "-a is a option" ;;
-b) echo "-b is a option" ;;
*) echo "$1 is not a option" ;;
esac
shift
done
for var in "$@"
do
echo "opening file $var"
done
./int.sh -a -b -c fred dino barney
输出:
-a is a option
-b is a option
-c is not a option
fred is not a option
dino is not a option
barney is not a option
可以看到,后面的 fred、dino 和 barney 文件并没有被输出。这就是没有区别选项和参数的局限所在了。参数都被删除完了,后面想迭代已经没有了。
在 Linux 中,通常使用双破折线--
来界定选项和参数。这样分开的好处是,可以及时打破循环,只对选项进行迭代,终止迭代参数,这样在后面的命令在就可以继续使用参数了,来看例子:
#!/bin/bash
while test -n "$1"
do
case "$1" in
-a) echo "-a is a option" ;;
-b) echo "-b is a option" ;;
--) shift
break ;;
*) echo "$1 is not a option" ;;
esac
shift
done
for var in "$@"
do
echo "opening file $var"
done
./int.sh -a -b -c -- fred dino barney
输出:
-a is a option
-b is a option
-c is not a option
opening file fred
opening file dino
opening file barney
这个就是选项和参数分开的妙处。
-
处理带值的选项
这要是选项和参数完全分开没啥问题,但是现实是有的选项带有值,这时候就需要针对带值的选项进行特殊处理。
#!/bin/bash
while test -n "$1"
do
case "$1" in
-a) echo "-a is a option" ;;
-b) value="$2"
echo "-b is a option with a value '$value'" ;;
--) shift
break ;;
*) echo "$1 is not a option" ;;
esac
shift
done
for param in "$@"
do
echo "opening a file $param"
done
./int.sh -a -b today -c -- fred dino barney
,现在-b
选项带有值today
,来看输出结果:
-a is a option
-b is a option with a value 'today'
today is not a option
-c is not a option
opening a file fred
opening a file dino
opening a file barney
有点小问题,就是today
值不应该被判断为选项而打印出来,这时需要将其shift删除掉。
#!/bin/bash
while test -n "$1"
do
case "$1" in
-a) echo "-a is a option" ;;
-b) value="$2"
echo "-b is a option with a value '$value'"
shift ;;
--) shift
break ;;
*) echo "$1 is not a option" ;;
esac
shift
done
for param in "$@"
do
echo "opening a file $param"
done
./int.sh -a -b today -c -- fred dino barney
,再来看输出结果:
-a is a option
-b is a option with a value 'today'
-c is not a option
opening a file fred
opening a file dino
opening a file barney
这次就完美了,非常符合预期结果,既个性化处理了带值的选项,同时也能对参数进行处理。
基于shift命令的特性,每次读取后就将其删除,所以,只要带值的选项格式是:选项+值(例如本例中-b today
),无论无何变换位置(如:-b today -a -c
或-c -a -b today
等),都不影响处理带值的选项,但是不能把选项及其值分开(如本例:``-b -a today -c`这样的不行)。
需要强调的是:不管按什么顺序位置放置选项,都必须将带值的选项当作一个整体放置,程序都能正常运行。
./int.sh -b today -c -a -- fred dino barney
-b is a option with a value 'today'
-c is not a option
-a is a option
opening a file fred
opening a file dino
opening a file barney
-
处理合并选项
现实中有许多人喜欢将选项合并写,这样可以省去键入的次数,但是以上所学还不足以实现选项的合并处理,这时需要使用getopt命令来帮助实现这一需求。
getopt命令的用法:
getopt optstring parameters
来看具体例子(选项不带值):
getopt abc -a -b -c fred dino barney
# -a -b -c -- fred dino barbey
选项带值:
getopt a:bc -a value1 -b -c fred dino barney
# -a value1 -b -c -- fred dino barbey
getopt a:b:c -a value1 -b value2 -c fred dino barney
# -a value1 -b value2 -c -- fred dino barbey
getopt a:b::c -a value1 -b value2 value3 -c fred dino barney
# -a value1 -b -c -- value2 value3 fred dino barbey
getopt a:b::c -a value1 -b value2 -c fred dino barbey
# -a value1 -b -c -- value2 fred dino barbey
getopt a:b:c -a value1 -b value2 value3 -c fred dino barbey
# -a value1 -b value2 -c -- value3 fred dino barbey
getopt a:bc -bc -a value1 fred dino barbey
# -b -c -a value1 -- fred dino barbey
getopt a:b:c -bc -a value1 fred dino barbey
# -b c -a value1 -- fred dino barbey
从以上例子可以看出,optstring
先是定义了所要的选项,这些例子中是 a、b 和 c,如果某个选项带有值,则在其后面加上冒号:
,选项不能带有多个值,也不能写两个冒号及以上表示多个值,只能由一个冒号表示一个值,否则结果将不符合预期。所以,安全起见,就一个冒号一个值吧。
再来说说getopt命令的运行原理,首先,我们将getopt命令分为三个部分。
getopt a:bc -a value1 -bc fred dino barney
|----| |--| |------------------------------|
⬇ ⬇ ⬇
getopt optstring parameters
当其运行时,getopt命令会检查parameters
部分,并基于提供的optstring
部分进行配对解析(顺序不影响其配对解析),当有合并的选项时(如这里的-bc
),它会自动将其分成单独的两个选项,并用双破折线将选项和参数分开。
当optstring
部分有选项,而parameters
部分没有时,会显示错误信息:
getopt a:bc -a value1 -bcd fred dino barney
# getopt: invalid option -- 'd'
# -a value1 -b -c -- fred dino barney
getopt命令的-q
选项可以忽略这条消息,并同时给选项值和参数加上单引号:
getopt -q a:bc -a value1 -bcd fred dino barney
# -a 'value1' -b -c -- 'fred' 'dino' 'barney'
现在,就可以在脚本中实现选项的合并了。
#!/bin/bash
set -- $(getopt -q ab:c "$@")
while test -n "$1"
do
case "$1" in
-a) echo "-a is a option" ;;
-b) value="$2"
echo "-b is a option with a value '$value'"
shift ;;
-c) echo "-c is a option" ;;
--) shift
break ;;
*) echo "$1 is not a option" ;;
esac
shift
done
for param in "$@"
do
echo "opening a file $param"
done
./int.sh -ac -b value fred dino
-a is a option
-c is a option
-b is a option with a value ''value''
opening a file 'fred'
opening a file 'dino'
这样,即使合并选项,也能正常处理选项和参数,并且能够做到选项和参数分开各自处理,已经接近完美了,但是还是差一点点。就是它在处理带空格和引号的参数时有问题。
./int.sh -ac "fred dino" barney
-a is a option
-c is a option
opening a file 'fred
opening a file dino'
opening a file 'barney'
可以看出,并不能正确解析参数。要想解决这一问题,还得引入更加高级的getopts 命令
-
更高级的 getopts 命令
getopts optstring variable
去掉错误消息,需要在
optstring
前加冒号:
,如getopts :abc
那么,
variable
保存的是什么呢?来看一个例子就懂了
#!/bin/bash
while getopts :abc var
do
echo "$var"
done
./int.sh -abcd
输出:
a
b
c
?
由此可见,variable
保存的是在 while 循环中每次迭代optstring
的单个选项,如果optstring
中没有,就以问号?
表示。
此外,getopts命令有两个特殊变量:
-
OPTARG 变量
当选项带有值时,OPTARG 变量就保存这个选项的值。
./int.sh -a -b fred -c
while getopts :ab:c var
do
echo "$var"
echo "-b option have a value $OPTARG"
done
输出:
a
-b option have a value
b
-b option have a value fred
c
-b option have a value
OPTIND 变量
OPTIND 变量保存了 getopts 正在处理的参数位置。如果在循环体内,则 OPTARG 变量会随着循环迭代而增加,如果是在循环体外,则保存有最后一次处理选项的位置信息。选项的值也会被计入其中。
#!/bin/bash
while getopts :ab:c var
do
echo "$var"
echo "-b option have a value $OPTARG"
done
echo $OPTIND
输出:
a
-b option have a value
b
-b option have a value fred
c
-b option have a value
5
OPTIND 变量在循环体外,所以其保存了最后一次的选项位置,选项的值也被计入其中,而且,OPTIND 变量的值是位置上加 1,所以就成了 5,当我们需要 shift 选项时,则应在其基础上减 1 才符合实际。
另外需要注意的是,当合并键入选项时,会将那些合并的选项当成一个整体,然后在 OPTIND 变量中也相应的减少一,具体来看例子:
./int.sh -ab fred -c
a
-b option have a value
b
-b option have a value fred
c
-b option have a value
4
可以看到,尽管合并了选项,还是能正确解析出各个选项及其值,但是 OPTIND 变量变成 4 了,不是原来的 5 了。这么做也合理,你耐心看到后面就懂了。
getopts 命令在写法上也和 getopt 命令有所不同。
#!/bin/bash
while getopts :ab:c var
do
case "$var" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with a value $OPTARG" ;;
c) echo "Found the -c option" ;;
*) echo "Unknown option: $var" ;;
esac
done
echo $OPTIND
shift $[ $OPTIND - 1 ]
echo
for param in "$@"
do
echo "Parameter: $param"
done
./int.sh -ab today -c dino barney peter
输出:
Found the -a option
Found the -b option, with a value today
Found the -c option
4
Parameter: dino
Parameter: barney
Parameter: peter
./int.sh -a -b today -c dino barney peter
Found the -a option
Found the -b option, with value today
Found the -c option
5
Parameter: dino
Parameter: barney
Parameter: peter
case语句中没有了破折线,并且直接在 while 循环语言的判断式中调用 getopts 命令对选项参数列表进行循环。
正如上面所说的,OPTIND 变量不论是合并的选项,还是分开键入的选项,都能够很好的解析选项和参数,结合 shift 命令,如果是合并的选项,就将其一块计入 OPTIND 变量中,shift 命令也能将其一并删除,选项解析还是以 case 进行判断,真可谓妙啊。
getopts 命令还可以正确处理带空格和引号的参数,这就是 getopts 命令比较高级的地方。
#!/bin/bash
while getopts :ab:c opt
do
case "opt" in
a) echo "Input -a option, processing..." ;;
b) echo "Input -b option with a value $OPTARG, processing..." ;;
c) echo "Input -c option, processing..." ;;
*) echo "Sorry, error input.."
esac
done
echo
shift $[ $OPTIND - 1 ]
for param in "$@"
do
echo "Proccessing the parameter $param"
done
./int.sh -a -b today -c "dino barney" fred
Input -a option, processing...
Input -b option with a value today, processing...
Input -c option, processing...
Proccessing the parameter dino barney
Proccessing the parameter fred
嗯,带双引号和空格的选项都能正常解析了,符合预期。
./int.sh -acb today "dino barney" fred
或./int.sh -ab today -c "dino barney" fred
等等都能正常解析,但是一定要把 b 选项和 today 的顺序放一起,b 选项和 today 中间不能有别的值,如./int.sh -abc today "dino barney" fred
这样就不能正常解析。
选项标准化
说人话就是,你自己创建 shell 脚本时,有权力去定义属于自己的脚本,选项可以按照自己的想法去定义,但是这样别人的学习成本就高了,将选项标准化的意思是将你的 shell 脚本写得更通俗普遍,而有些约定俗成的选项就不要去更改了,这样别人用起来也减少了学习成本,让你的脚本更容易被大众所接受,这就是选项标准化的实质。
已经约定俗成的选项代表的意义如下(部分例举):
-
-a
显示所有对象 -
-c
生成一个计数 -
-d
指定一个目录 -
-f
指定一个文件 -
-i
忽略文本大小写 -
-y
对所有问题回答 yes
获得输入(包括用户和文件的输入)
read命令接受来自标准键盘或文件的输入。
-
read命令读取文件时,需要借助cat命令和管道符,结合while循环读取。别忘了,当退出码为零时while循环继续,非零退出码时while终止。
#!/bin/bash
cat sample | while read line
do
echo "$line"
done
-
read读取标准键盘输入。
#!/bin/bash
echo -n "Please enter your name: "
read name
echo "Hello, $name!"
#!/bin/bash
read -p "Please enter your name: " name
echo "Hello, $name!"
以上两个例子实现的效果一样,只是方式不同。
#!/bin/bash
read -p "Please enter your name and age: " name age
echo "Hi, $name, you are $age years old."
read命令会将输入分配给变量,如果输入与变量的数目一致,则变量得到对应位置的输入,如果变量的数量不够,剩下的输入数据就全部分配给最后的一个变量。
#!/bin/bash
read -p "Please enter your files: " fred dino barney
echo "here is the files your input: $fred $dino $barney "
Please enter your files: fred dino barney peter
here is the files your input: fred dino barney peter
Linux shell中还有一个特殊的变量,REPLY变量负责接收read命令输入的数据。前提是将设定的变量删除,否则$REPLY
内空无一物。
#!/bin/bash
read -p "Please enter your files: "
for file in $REPLY
do
echo "Processing file: $file"
done
等待超时
read命令-t
选项来指定输入的时间,如果超过,规定时间,则终止输入并给予提示。
#!/bin/bash
if read -t 10 -p "Please enter your telephone whithin 10 seconds: ";
then
echo
echo "Your telephone number $REPLY has been saved."
else
echo
echo "Time out!"
fi
隐藏方式读取
-s
选项可以避免将数据出现在显示器中,在输入密码时尤为合适。
#!/bin/bash
read -s -p "Enter your password: " pass
if test $pass -eq '1234'; then
echo
echo "your password is correct!"
else
echo
echo "Sorry, It doesn't work!"
fi
现在,可以自己写 shell 脚本或 perl 脚本或 r 脚本,定义自己的选项和参数,创建一个鲁棒性强的脚本啦。