Bootstrap

python中的fire和Linux shell中的参数传递

一、fire

安装

要使用 Python Fire 库,首先需要安装它。以下是安装步骤:

使用 pip 安装

可以通过 pip 直接安装 Python Fire:

pip install fire

特性

  1. 自动生成命令行界面:将任何 Python 对象(函数、类、模块、字典等)自动转换为命令行界面。

  2. 简洁性:只需一行代码即可生成命令行界面,大大减少了开发时间和代码复杂度。

  3. 灵活性:支持多种数据类型和参数,能够处理复杂的命令行需求。

  4. 易用性:与 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是参数。

要想写出一个鲁棒性强的脚本,必须兼顾各种情况,而选项恰恰是提供这种鲁棒性的工具。

  1. 首先,需要区分是选项还是参数?

    这部分内容的实现,需要一个判断语句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

这样就能识别出特定的选项了。

  1. 区分选项和参数

    之所以要区分选项和参数,原因是没有必要遍历所有的命令行选项和参数,你把所有的命令行选项和参数遍历了一遍,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

这个就是选项和参数分开的妙处。

  1. 处理带值的选项

    这要是选项和参数完全分开没啥问题,但是现实是有的选项带有值,这时候就需要针对带值的选项进行特殊处理。

#!/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
  1. 处理合并选项

    现实中有许多人喜欢将选项合并写,这样可以省去键入的次数,但是以上所学还不足以实现选项的合并处理,这时需要使用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 命令

  1. 更高级的 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 脚本,定义自己的选项和参数,创建一个鲁棒性强的脚本啦。

;