使用bash和Python3进行系统编程

2020-05-13 08:59:06

2020年4月30日,我有机会为SysAdminShow Podcast与Dustin Reybrouck交谈。我们主要讨论了为什么系统管理员除了shell脚本编写之外还想添加Python作为一种工具。他的大多数听众可能都熟悉bash或Powershell,所以我展示了如何在bash中编写一些简单但参数化且有文档记录的Head版本,以及如何将其转换为Python3。以下是我们讨论的总结。

虽然可以用这些语言编写许多有用的程序,但是考虑到Python程序可以在本机理解bash的系统(例如,linux或mac)和powershell( - )之间移植,所以作为练习,让我们编写一个bash命令的bash实现,可能会是一个更好的选择,尤其是考虑到python程序可以在本地理解bash的系统(例如,linux或mac)和powershell(窗口)之间移植。作为练习,让我们编写一个bash实现的head命令,这是一种更好的选择,它可以在本地理解bash的系统(例如,linux或mac)和powershell(窗口)之间进行移植。作为练习,让我们编写一个bash实现的head命令,

让我们首先设想一下如何编写我们自己的Head命令的实现。例如,给定美国宪法的文本,我们预计会看到给定文件的前几行,通常是10行:

$head Const.txt我们美利坚合众国人民,为了组成一个更完美的联邦,建立正义,确保国内安宁,提供共同防御,促进公共福利,并确保自由对我们自己和我们的后代的祝福,颁布并制定美利坚合众国的本宪法。第1条本宪法第1款授予的所有立法权均属于美国国会。

我们希望能够使用-n这样的选项修改该数字:

我们美利坚合众国人民,为了组成一个更完美的联邦,建立正义,确保国内安宁,提供共同防御,促进公共福利,并确保自由的祝福,以…“。

命令行工具响应有关如何调用程序的-h或--help用法语句是很常见的。在head[1]的情况下,它没有给出用法,因为我们请求它,而是因为它不认为这些是有效选项。尽管如此,在某些情况下,它仍设法生成用法,这总比没有强:

$head-hhead:非法选项--huage:head[-n行|-c字节][文件.]

让我们从bash中的一个版本开始,该版本仅处理一个文件,可能的行数将默认为10。如果不带参数运行,它将打印一条";用法语句:

将文件作为唯一参数运行时,它将打印前10行:

$./Simple-head.sh con.txt我们美利坚合众国人民,为了组成一个更完美的联邦,建立正义,确保国内安宁,提供共同防御,促进公共福利,并确保自由对我们自己和我们的后代的祝福,颁布并制定美利坚合众国的本宪法。第1条本宪法第1款授予的所有立法权均属于美利坚合众国国会。

我们可以提供可选的第二个参数来更改显示的行数:

$./Simple-head.sh con.txt 2我们美国人民,为了组成一个更完美的联邦,建立正义,确保国内安宁,提供共同生活。

请注意,该程序会非常随意地失败。例如,如果我们给它一个不存在的文件:

如果第二个参数不是数字,我们的程序也不会显示错误,并且实际上将打印整个文件。尝试按如下方式运行程序:

#!/bin/bash(1)##作者:Ken Youens-Clark<;[email protected]>;(2)#目的:`head`的简单bash实现##检查参数个数是1或2If[[$#-lt 1]]||[[$#-gt 2]];则(3)echo";用法:$(basename";$0";)file[NUM]";(4)退出1(5)fiFILE=$1(6)NUM=${2:-10}(7)LINE_NUM=0(8)同时读取-r行;DO(9)ECHO";$LINE";(10)LINE_NUM=$((LINE_NUM+1))(11)如果[[$LINE_NUM-EQ$NUM]];则(12)中断(13)Fidone<;";$

这行通常被称为";shebang,";,通常可以看到bash的路径是这样硬编码的。但是,这不一定是最佳实践,因为bash很可能位于/usr/local/bin/bash。

#后面的任何文本都会被bash忽略。在这里,我们向程序添加注释,但您也可以使用此注释临时禁用代码。记录您的代码是礼貌的,这样其他人可能会联系您提出问题。

bash中的所有内容都是字符串,但是我们可以使用-lt(小于)和-gt(大于)这样的运算符进行数值比较。$#变量保存程序的参数数量,因此我们尝试查看是否正好没有1个或2个参数。

我们打印一条用法类型的语句,向用户解释如何调用该程序。该文件是必需的位置参数,而[NUM]显示在[]中,表示它是可选的。

我们退出时返回一个非零值(1就可以了),表示程序未能按预期运行。

因为我们知道我们至少有一个参数,所以我们可以将$1中第一个参数的值复制到我们的file变量中。

我们可能有第二个参数,也可能没有第二个参数,因此我们可以将$2或默认值10复制到NUM变量。

将LINE_NUM变量初始化为0,这样我们就可以计算显示了多少行文件。

$(())求值将允许我们使用字符串值执行一些算术运算。在这里,我们想要将LINE_NUM的值加1。

eq是bash中的数字相等运算符。在这里,我们检查LINE_NUM是否等于我们想要显示的行数。

前面的Simple-head.sh版本展示了如何在bash中处理许多系统级任务的一些基本思想,例如:

当程序未按预期正常完成时,使用非零值退出程序。

没有-n选项,因为程序只处理位置参数,因此不能处理选项。

程序不会再次打印-h的用法";,因为它无法处理选项。

#!/usr/bin/env bash(1)##作者:Ken Youens-Clark<;[email protected]>;(2)#目的:bash实现`head`##使用uninitialize变量集-u(3)#参数NUM_LINES的默认值=10(4)#打印";用法";函数用法(){(5)printf";用法:$(basename";$0";)";ECHO";选项:";ECHO";-n NUM_LINES";ECHO EXIT";${1:-0}";}#如果我们完全没有参数,则退出[[$#-eq0]]&;&;用法1(6)#处理命令行选项,而获取选项:n:h opt;do(7)case$opt。(9)SHIFT 2(10);;h)用法(11);;:)ECHO";错误:选项-$OPTARG需要参数。";(12)退出1;;\?)。ECHO";错误:无效选项:-${OPTARG:-";";}";(13)EXIT 1 esacone#验证NUM_LINES看起来是否为正整数如果[[$NUM_LINES-lt 1]];则(14)ECHO";-n\";${NUM_LINES}\";必须是>;0";EXIT 1FI#处理位置参数FNU1。do(16)FNUM=$((FNUM+1))(17)#如果[[!-f";$FILE";]]||[!-r";$FILE";]];则(18)ECHO";\";${FILE}\";不是可读文件";CONTINUE(19)FI#如果有多个文件[[$],则打印标题。echo";==>;${file}<;==";(20)#初始化计数器变量LINE_NUM=0(21)#读取-r行时循环遍历文件的每一行;执行(22)ECHO$LINE#递增计数器,查看是否中断LINE_NUM=$((LINE_NUM+1))[[$LINE_NUM-eq$NUM_LINES]]&;&。[[$#->1]]&;&;[[$FNUM-lt$#]]&;&;echo(24)已退出0。

使用env程序(通常位于/usr/bin/env)查找bash比将路径硬编码为/bin/bash更灵活。

如果我们试图使用未初始化的变量,这将导致bash死亡,这是该语言提供的为数不多的安全特性之一。

在这里,我们为NUM_LINES设置一个默认值,以显示可以由选项覆盖的值。

由于我可能多次想要显示用法并退出,但出现错误(例如,没有参数或根据-h的请求),所以我可以将其放入函数中稍后调用。

如果程序$#的参数数量为0,则使用";USAGE";语句和非零值退出。

我们可以在bash中使用getopts手动解析命令行参数。我们特别寻找标志-n,它接受一个值,而-h,它不接受一个值。

$opt将具有标志值,例如n代表-n,h代表-h。

$OPTARG将具有-n标志的值。我们可以将其复制到NUM_LINES变量以保存它。

现在我们已经处理了-n3,例如,我们使用Shift2从程序参数[email protected]中删除这两个值。

如果处理-h标志,请调用将导致程序退出的用法函数。

这使用-lt运算符将NUM_LINES强制为数字值。如果它小于-lt 1,则抛出错误并使用非零值退出。

既然我们已经处理了可选参数,我们就可以处理[email protected]中的其余位置参数了。我们从定义FNUM开始,这样我们就可以跟踪我们正在使用的文件号。也就是说,这是当前文件的索引值。

我们可以使用for循环迭代在[email protected]中找到的位置参数。

如果给定参数是文件,则-f测试将返回";true";值,并且!会否定这一点。ditto as-r将报告参数是否为可读文件。

CONTINUE语句将使for循环立即前进到下一迭代,跳过下面的所有代码。

如果位置参数的数量大于-gt 1,则打印一个标题,显示当前文件的名称。

这与我们以前从文件中读取给定行数的循环相同。但是,这个改进了,因为我们检查来自用户的数字参数是否真的是正整数!

如果有多个文件要处理,并且我们当前不在最后一个文件上,则额外打印一个换行符来分隔输出。

如果您是bash编程的新手,语法可能看起来相当神秘!完全手动处理命令行选项和位置参数特别麻烦。我承认这不是一个很容易正确编写的程序,而且,即使当它最终在我的Linux和MAX机器上运行时,我也无法将其提供给Windows用户,除非他们安装了WSL(Windows Subsystem For Linux)或Cygwin之类的软件。

不过,这个程序运行得相当好!如果我们不带参数运行,或者如果您运行./head.sh-h,它将打印出不错的文档,这实际上是对head的改进:

它可以处理选项和位置参数,为-n选项提供合理的默认值,并正确跳过非文件参数:

$./head.sh-n 3foo con.txt";foo&34;不是一个可读的文件=>;const.txt<;=我们美利坚合众国人民,为了组成更完美的联邦,建立正义,确保国内安宁,提供共同防御,促进公共福利,并确保自由的祝福。

$./head.sh-n 1 st.txt Simple-head.sh head.sh==>;con.txt<;=我们美国人民,为了形成一个更完美的联邦,==>;Simple-head.sh<;=#!/bin/bash==>;head.sh<;=#!/usr/bin/env bash;==#!/usr/bin/env bash;==#!

不管怎样,我使用了附带的new_bash.py程序来创建这个程序。如果您发现自己陷入了编写bash程序的困境,并且不希望从头开始,那么这个程序可能会对您有用。

我已经包含了一个test.py,它是一个Python程序,它将运行head.sh程序以确保它确实执行其应该执行的操作。如果您查看此程序的内容,您将看到许多名称以test_开头的函数。这是因为我使用pytest模块/程序将这些函数作为测试套件运行。我喜欢使用-x标志来指示测试应在第一次失败的测试时停止,并使用-v标志来指示";Verbose";输出。这些

$pytest-XV test.py=测试会话启动=.test.py::test_exists通过[14%]test.py::TEST_USAGE通过[28%]test.py::TEST_BAD_FILE通过[42%]test.py::TEST_BAD_NUM通过[57%]test.py::TEST_DEFAULT通过[71%]test.py::TEST_n通过[85%]test.py::

不得不用与程序本身不同的语言为程序编写测试有点麻烦,但我知道在bash中没有可以使用(或想要学习)的可以运行上述测试套件的测试框架!

要用Python编写类似的版本,我们将非常依赖标准的argparse模块来处理所有命令行参数的验证以及生成用法语句。下面是一个类似于simple-head.py的版本,它将只处理一个文件:

#!/usr/bin/env python3(1)";";";(2)作者:Ken Youens-ClarkPurpose:head的Python实现本版本只处理一个文件!";";";导入argparse(3)导入osimport系统#PythonGET_args():(4)";";";获取命令行参数";";";(5)parser=argparse.ArgumentParser((6)DESCRIPTION=';-def头';,formatter_class=argparse.ArgumentDefaultsHelpFormatter)parser.add_Argument(';文件';,(7)metavar=';文件';,类型=argparse.FileType(';rt';),(8)Help=';输入文件';)parser.add_Argument(';-n';,(9)';--Num&39;,Help=';行数';,type=int,(10)default=10)(11)args=parser.parse_args()(12)if args.num<;1:(13)parser.error(f';--num";{args.num}";必须为>;0';)(14)返回args(15)#-def Main():(16)";";";在这里制造爵士乐噪音";";";args=Get_args()(17)for I,行in Enumerate(args.file,Start=1):(18)打印(行,结束=';)(19)如果I==args.num:(20)Break(21)#-if__NAME__==';__main__';:(22)main()。

三重引号允许我们创建跨越多行的字符串。在这里,我们创建了一个字符串,但没有将其赋给变量。这是创建文档的约定,也称为文档字符串。此文档字符串汇总程序本身。我至少喜欢记录是谁写的,为什么写的。

我们可以从其他模块导入代码。虽然我们可以导入几个用逗号分隔的模块,但建议将每个模块放在单独的行上。具体地说,我们希望使用argparse来处理命令行参数,我们还将使用os(操作系统)和sys(系统)模块。

我喜欢总是定义一个get_args()函数,该函数专门处理argparse来创建程序的参数和验证参数。我总是把这个放在第一位,这样我在阅读程序时就可以立即看到它。

这是函数的文档字符串。它会像注释一样被忽略,但它对Python很重要,如果我导入此模块并请求帮助(Get_Args),它就会出现。

这将创建一个将处理命令行参数的解析器。我添加了将出现在任何";USAGE";语句中的程序描述,并且我总是喜欢让argparse为用户显示任何默认值。

位置参数的名称中没有前导破折号。在这里,我们定义了一个位置参数,我们可以在内部将其称为file。

所有参数的默认类型都是str(字符串)。我们可以要求argparse强制执行不同的类型(如int),当用户无法提供可以解析为整数值的值时,它将打印错误。在这里,我们使用特殊的argparse类型,它定义了";可读";(';r';)";文本";(';t';)文件。如果用户提供的不是可读文本文件,argparse将暂停程序,打印错误和用法,然后以非零值退出。

";number";参数的前导-on-n(短名称)和--num(长名称)意味着这将是一个选项。

在定义了程序的参数之后,我们要求解析器解析参数。如果出现参数数量或类型错误等问题,argparse将在此处停止程序。

如果我们谈到这一点,就argparse而言,这些参数是有效的。我们可以执行其他手动检查,例如验证args.num是否大于0。

parser.error()函数是我们手动调用argparse的错误输出函数的一种方式。

Python中的函数必须显式返回值,否则默认情况下将返回NONE。这里希望将参数返回给调用函数。

约定将启动函数称为main(),但这不是必需的,并且Python不会自动调用此函数来启动程序。get_args()和main()都不接受参数,但如果它们接受,它们将列在括号中。

定义参数、验证参数以及处理帮助和用法的所有工作现在都隐藏在get_args()函数中。我们可以把它想象成一个单元,它封装了这些想法。如果我们的程序成功调用get_args()并返回一些参数,那么我们就可以知道参数实际上是正确和有用的。

我们不必像在bash中那样初始化计数变量,因为我们可以使用Enumererate()函数返回任何项目序列的索引和值。这里,args.file实际上是argparse提供的打开文件句柄,因为我们将args.file定义为";file";类型。这意味着我将迭代文件句柄中的行。我可以使用start选项枚举()从1开始计数,而不是从0开始计数。

print()函数类似于bash中的echo语句。这里,文件中的行将有一个换行符,因此我使用end=';';来表示print()不应该将惯常的换行符添加到输出中。

bash使用-eq进行数字比较,使用==进行字符串相等,而Python使用==进行这两种操作。

Python和bash在循环中分别使用Continue和Break来跳过和离开循环。

这是Python中检测程序/模块何时从命令行运行的习惯用法。在这里,我们想要执行main()函数来启动程序运行。

上面的程序以py-head/olution1.py的形式提供,您可以运行它来查看它将如何创建不带参数的用法:

请注意,我们没有定义argparse的-h和--help标志,因为它们是专门为生成帮助而保留的:

$./olution1.py-huage:解决方案1.py[-h][-n int]头位置参数的Python实现:文件输入文件可选参数:-h,--help显示此帮助消息并退出-n int,--num int行数(默认值:10)。

$./解决方案1.py const.txt-n我们美利坚合众国人民,为了组成一个更完美的联邦,建立正义,确保国内安宁,提供共同防卫,促进公共福利,并确保自由的祝福,以…。

以前的Python版本演示了ma。

..