Linux Shell 编程训练营(全)
原文:
zh.annas-archive.org/md5/65C572CE82539328A9B0D1458096FD51
译者:飞龙
前言
在 Linux Shell Scripting Bootcamp 中,您将首先学习脚本创建的基础知识。您将学习如何验证参数,以及如何检查文件的存在。接着,您将熟悉 Linux 系统上变量的工作原理以及它们与脚本的关系。您还将学习如何创建和调用子例程以及创建交互式脚本。最后,您将学习如何调试脚本和脚本编写的最佳实践,这将使您每次都能编写出优秀的代码!通过本书,您将能够编写能够高效地从网络中获取数据并处理数据的 shell 脚本。
本书涵盖内容
第一章,开始 shell 脚本,从脚本设计的基础知识开始。展示了如何使脚本可执行,以及创建一个信息丰富的Usage
消息。还介绍了返回代码的重要性,并使用和验证参数。
第二章,使用变量,讨论了如何声明和使用环境变量和本地变量。我们还讨论了如何执行数学运算以及如何使用数组。
第三章,使用循环和 sleep 命令,介绍了使用循环执行迭代操作的方法。它还展示了如何在脚本中创建延迟。读者还将学习如何在脚本中使用循环和sleep
命令。
第四章,创建和调用子例程,从一些非常简单的脚本开始,然后继续介绍一些接受参数的简单子例程。
第五章,创建交互式脚本,解释了使用read
内置命令来查询键盘的用法。此外,我们探讨了一些不同的读取选项,并介绍了陷阱的使用。
第六章,使用脚本自动化任务,描述了创建脚本来自动执行任务。还介绍了使用 cron 在特定时间自动运行脚本的正确方法。还讨论了执行压缩备份的存档命令zip
和tar
。
第七章,处理文件,介绍了使用重定向运算符将文件写出以及使用read
命令读取文件的方法。还讨论了校验和和文件加密,以及将文件内容转换为变量的方法。
第八章,使用 wget 和 curl,讨论了在脚本中使用wget
和curl
的用法。除此之外,还讨论了返回代码,并提供了一些示例脚本。
第九章,调试脚本,解释了一些防止常见语法和逻辑错误的技术。还讨论了使用重定向运算符将脚本的输出发送到另一个终端的方法。
第十章,脚本编写最佳实践,讨论了一些实践和技术,将帮助读者每次都编写出优秀的代码。
本书适用对象
任何安装了 Bash 的 Linux 机器都应该能够运行这些脚本。这包括台式机、笔记本电脑、嵌入式设备、BeagleBone 等。运行 Cygwin 或其他模拟 Linux 环境的 Windows 机器也可以。
没有最低内存要求。
本书适用对象
这本书既适用于想要在 shell 中做出惊人成就的 GNU/Linux 用户,也适用于寻找方法让他们在 shell 中的生活更加高效的高级用户。
约定
在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:您可以看到echo
语句Start of x loop
被显示为代码块如下所示:
echo "Start of x loop"
x=0
while [ $x -lt 5 ]
do
echo "x: $x"
let x++
任何命令行输入或输出都以以下方式编写:
guest1 $ ps auxw | grep script7
新术语和重要单词以粗体显示。例如,屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“点击下一步按钮将您移至下一个屏幕。”
注意
警告或重要提示会显示在这样的框中。
提示
提示和技巧会以这样的方式出现。
读者反馈
我们的读者的反馈总是受欢迎的。让我们知道您对本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它可以帮助我们开发您真正能够充分利用的书籍。
要向我们发送一般反馈,只需发送电子邮件至<[email protected]>
,并在主题中提及书籍的标题。
如果您在某个专题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南,网址为www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的自豪所有者,我们有一些东西可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com
的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support
并注册,以便文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册到我们的网站。
-
将鼠标指针悬停在顶部的支持选项卡上。
-
点击代码下载和勘误。
-
在搜索框中输入书名。
-
选择您要下载代码文件的书籍。
-
从下拉菜单中选择您购买本书的地点。
-
点击下载代码。
您还可以通过在 Packt Publishing 网站上的书籍网页上点击代码文件按钮来下载代码文件。可以通过在搜索框中输入书名来访问此页面。请注意,您需要登录到您的 Packt 帐户。
下载文件后,请确保使用最新版本的以下工具解压或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Linux-Shell-Scripting-Bootcamp
。我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/
上找到。去看看吧!
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在我们的书籍中发现错误——也许是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata
报告,选择您的书籍,点击勘误提交表链接,并输入您的勘误详情。一旦您的勘误被验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该标题的勘误部分的任何现有勘误列表中。
要查看先前提交的勘误表,请访问www.packtpub.com/books/content/support
,并在搜索框中输入书名。所需信息将出现在勘误表部分。
在互联网上盗版受版权保护的材料是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何形式的非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。
盗版
请通过<[email protected]>
与我们联系,并附上涉嫌盗版材料的链接。
我们感谢您帮助保护我们的作者和我们为您提供有价值内容的能力。
问题
如果您对本书的任何方面有问题,可以通过<[email protected]>
与我们联系,我们将尽力解决问题。
第一章:开始使用 Shell 脚本
本章是关于 shell 脚本的简要介绍。它将假定读者对脚本基础知识大多熟悉,并将作为复习。
本章涵盖的主题如下:
-
脚本的一般格式。
-
如何使文件可执行。
-
创建良好的使用消息和处理返回代码。
-
展示如何从命令行传递参数。
-
展示如何使用条件语句验证参数。
-
解释如何确定文件的属性。
入门
您始终可以在访客账户下创建这些脚本,并且大多数脚本都可以从那里运行。当需要 root 访问权限来运行特定脚本时,将明确说明。
本书将假定用户已在该帐户的路径开头放置了(.
)。如果没有,请在文件名前加上./
来运行脚本。例如:
$ ./runme
使用chmod
命令使脚本可执行。
建议用户在其访客账户下创建一个专门用于本书示例的目录。例如,像这样的东西效果很好:
$ /home/guest1/LinuxScriptingBook/chapters/chap1
当然,随意使用最适合您的方法。
遵循 bash 脚本的一般格式,第一行将只包含此内容:
#!/bin/sh
请注意,在其他情况下,#
符号后面的文本被视为注释。
例如,
整行都是注释
chmod 755 filename # This text after the # is a comment
根据需要使用注释。有些人每行都加注释,有些人什么都不加注释。我试图在这两个极端之间取得平衡。
使用好的文本编辑器
我发现大多数人在 UNIX/Linux 环境下使用 vi 创建和编辑文本文档时感到舒适。这很好,因为 vi 是一个非常可靠的应用程序。我建议不要使用任何类型的文字处理程序,即使它声称具有代码开发选项。这些程序可能仍然会在文件中放入不可见的控制字符,这可能会导致脚本失败。除非您擅长查看二进制文件,否则可能需要花费数小时甚至数天来解决这个问题。
此外,我认为,如果您计划进行大量的脚本和/或代码开发,建议查看 vi 之外的其他文本编辑器。您几乎肯定会变得更加高效。
演示脚本的使用
这是一个非常简单的脚本示例。它可能看起来不起眼,但这是每个脚本的基础:
第一章 - 脚本 1
#!/bin/sh
#
# 03/27/2017
#
exit 0
注意
按照惯例,在本书中,脚本行通常会编号。这仅用于教学目的,在实际脚本中,行不会编号。
以下是带有行号的相同脚本:
1 #!/bin/sh
2 #
3 # 03/27/2017
4 #
5 exit 0
6
以下是每行的解释:
-
第 1 行告诉操作系统要使用哪个 shell 解释器。请注意,在某些发行版上,
/bin/sh
实际上是指向解释器的符号链接。 -
以
#
开头的行是注释。此外,#
后面的任何内容也被视为注释。 -
在脚本中包含日期是一个好习惯,可以在注释部分和/或
Usage
部分(下一节介绍)中包含日期。 -
第 5 行是此脚本的返回代码。这是可选的,但强烈建议。
-
第 6 行是空行,也是脚本的最后一行。
使用您喜欢的文本编辑器,编辑一个名为script1
的新文件,并将前面的脚本复制到其中,不包括行号。保存文件。
要将文件转换为可执行脚本,请运行以下命令:
$ chmod 755 script1
现在运行脚本:
$ script1
如果您没有像介绍中提到的那样在路径前加上.
,则运行:
$ ./script1
现在检查返回代码:
$ echo $?
0
这是一个执行得更有用的脚本:
第一章 - 脚本 2
#!/bin/sh
#
# 3/26/2017
#
ping -c 1 google.com # ping google.com just 1 time
echo Return code: $?
ping
命令成功返回零,失败返回非零。如您所见,echoing $?
显示了其前一个命令的返回值。稍后会详细介绍。
现在让我们传递一个参数并包括一个Usage
语句:
第一章 - 脚本 3
1 #!/bin/sh
2 #
3 # 6/13/2017
4 #
5 if [ $# -ne 1 ] ; then
6 echo "Usage: script3 file"
7 echo " Will determine if the file exists."
8 exit 255
9 fi
10
11 if [ -f $1 ] ; then
12 echo File $1 exists.
13 exit 0
14 else
15 echo File $1 does not exist.
16 exit 1
17 fi
18
以下是每行的解释:
-
第
5
行检查是否给出了参数。如果没有,将执行第6
到9
行。请注意,通常最好在脚本中包含一个信息性的Usage
语句。还要提供有意义的返回代码。 -
第
11
行检查文件是否存在,如果是,则执行第12
-13
行。否则运行第14
-17
行。 -
关于返回代码的说明:在 Linux/UNIX 下,如果命令成功,则返回零是标准做法,如果不成功则返回非零。这样返回的代码可以有一些有用的含义,不仅对人类有用,对其他脚本和程序也有用。但这并不是强制性的。如果你希望你的脚本返回不是错误而是指示其他条件的代码,那么请这样做。
下一个脚本扩展了这个主题:
第一章 - 脚本 4
1 #!/bin/sh
2 #
3 # 6/13/2017
4 #
5 if [ $# -ne 1 ] ; then
6 echo "Usage: script4 filename"
7 echo " Will show various attributes of the file given."
8 exit 255
9 fi
10
11 echo -n "$1 " # Stay on the line
12
13 if [ ! -e $1 ] ; then
14 echo does not exist.
15 exit 1 # Leave script now
16 fi
17
18 if [ -f $1 ] ; then
19 echo is a file.
20 elif [ -d $1 ] ; then
21 echo is a directory.
22 fi
23
24 if [ -x $1 ] ; then
25 echo Is executable.
26 fi
27
28 if [ -r $1 ] ; then
29 echo Is readable.
30 else
31 echo Is not readable.
32 fi
33
34 if [ -w $1 ] ; then
35 echo Is writable.
36 fi
37
38 if [ -s $1 ] ; then
39 echo Is not empty.
40 else
41 echo Is empty.
42 fi
43
44 exit 0 # No error
45
以下是每行的解释:
-
第
5
-9
行:如果脚本没有使用参数运行,则显示Usage
消息并以返回代码255
退出。 -
第
11
行显示了如何echo
一个文本字符串但仍然保持在同一行(没有换行)。 -
第
13
行显示了如何确定给定的参数是否是现有文件。 -
第
15
行如果文件不存在,则退出脚本没有继续的理由。
剩下的行的含义可以通过脚本本身确定。请注意,可以对文件执行许多其他检查,这只是其中的一部分。
以下是在我的系统上运行script4
的一些示例:
guest1 $ script4
Usage: script4 filename
Will show various attributes of the file given.
guest1 $ script4 /tmp
/tmp is a directory.
Is executable.
Is readable.
Is writable.
Is not empty.
guest1 $ script4 script4.numbered
script4.numbered is a file.
Is readable.
Is not empty.
guest1 $ script4 /usr
/usr is a directory.
Is executable.
Is readable.
Is not empty.
guest1 $ script4 empty1
empty1 is a file.
Is readable.
Is writable.
Is empty.
guest1 $ script4 empty-noread
empty-noread is a file.
Is not readable.
Is empty.
下一个脚本显示了如何确定传递给它的参数数量:
第一章 - 脚本 5
#!/bin/sh
#
# 3/27/2017
#
echo The number of parameters is: $#
exit 0
让我们尝试一些例子:
guest1 $ script5
The number of parameters is: 0
guest1 $ script5 parm1
The number of parameters is: 1
guest1 $ script5 parm1 Hello
The number of parameters is: 2
guest1 $ script5 parm1 Hello 15
The number of parameters is: 3
guest1 $ script5 parm1 Hello 15 "A string"
The number of parameters is: 4
guest1 $ script5 parm1 Hello 15 "A string" lastone
The number of parameters is: 5
提示
记住,引用的字符串被计算为 1 个参数。这是传递包含空格的字符串的一种方法。
下一个脚本显示了如何更详细地处理多个参数:
第一章 - 脚本 6
#!/bin/sh
#
# 3/27/2017
#
if [ $# -ne 3 ] ; then
echo "Usage: script6 parm1 parm2 parm3"
echo " Please enter 3 parameters."
exit 255
fi
echo Parameter 1: $1
echo Parameter 2: $2
echo Parameter 3: $3
exit 0
这个脚本的行没有编号,因为它相当简单。$#
包含传递给脚本的参数数量。
总结
在本章中,我们讨论了脚本设计的基础知识。展示了如何使脚本可执行,以及创建信息性的Usage
消息。还介绍了返回代码的重要性,以及参数的使用和验证。
下一章将更详细地讨论变量和条件语句。
第二章:使用变量
本章将展示变量在 Linux 系统和脚本中的使用方式。
本章涵盖的主题有:
-
在脚本中使用变量
-
使用条件语句验证参数
-
字符串的比较运算符
-
环境变量
在脚本中使用变量
变量只是一些值的占位符。值可以改变;但是,变量名称将始终相同。这是一个简单的例子:
a=1
这将值1
分配给变量a
。这里还有一个:
b=2
要显示变量包含的内容,请使用echo
语句:
echo Variable a is: $a
注意
请注意变量名称前面的$
。这是为了显示变量的内容而必需的。
如果您在任何时候看不到您期望的结果,请首先检查$
。
以下是使用命令行的示例:
$ a=1
$ echo a
a
$ echo $a
1
$ b="Jim"
$ echo b
b
$ echo $b
Jim
Bash 脚本中的所有变量都被视为字符串。这与 C 等编程语言不同,那里一切都是强类型的。在前面的示例中,即使a
和b
看起来是整数,它们也是字符串。
这是一个简短的脚本,让我们开始:
第二章-脚本 1
#!/bin/sh
#
# 6/13/2017
#
echo "script1"
# Variables
a="1"
b=2
c="Jim"
d="Lewis"
e="Jim Lewis"
pi=3.141592
# Statements
echo $a
echo $b
echo $c
echo $d
echo $e
echo $pi
echo "End of script1"
在我的系统上运行时的输出如下:
第二章-脚本 1
由于所有变量都是字符串,我也可以这样做:
a="1"
b="2"
当字符串包含空格时,引用字符串很重要,例如这里的变量d
和e
。
注意
我发现如果我引用程序中的所有字符串,但不引用数字,那么更容易跟踪我如何使用变量(即作为字符串还是数字)。
使用条件语句验证参数
当将变量用作数字时,可以测试和比较变量与其他变量。
以下是可以使用的一些运算符的列表:
运算符 | 说明 |
---|---|
-eq |
这代表等于 |
-ne |
这代表不等于 |
-gt |
这代表大于 |
-lt |
这代表小于 |
-ge |
这代表大于或等于 |
-le |
这代表小于或等于 |
! |
这代表否定运算符 |
让我们在下一个示例脚本中看一下这个:
第二章-脚本 2
#!/bin/sh
#
# 6/13/2017
#
echo "script2"
# Numeric variables
a=100
b=100
c=200
d=300
echo a=$a b=$b c=$c d=$d # display the values
# Conditional tests
if [ $a -eq $b ] ; then
echo a equals b
fi
if [ $a -ne $b ] ; then
echo a does not equal b
fi
if [ $a -gt $c ] ; then
echo a is greater than c
fi
if [ $a -lt $c ] ; then
echo a is less than c
fi
if [ $a -ge $d ] ; then
echo a is greater than or equal to d
fi
if [ $a -le $d ] ; then
echo a is less than or equal to d
fi
echo Showing the negation operator:
if [ ! $a -eq $b ] ; then
echo Clause 1
else
echo Clause 2
fi
echo "End of script2"
输出如下:
为了帮助理解本章,请在您的系统上运行脚本。尝试更改变量的值,看看它如何影响输出。
我们在第一章中看到了否定运算符,开始使用 Shell 脚本,当我们查看文件时。作为提醒,它否定了表达式。您还可以说它执行与原始语句相反的操作。
考虑以下示例:
a=1
b=1
if [ $a -eq $b ] ; then
echo Clause 1
else
echo Clause 2
fi
运行此脚本时,它将显示条款 1
。现在考虑这个:
a=1
b=1
if [ ! $a -eq $b ] ; then # negation
echo Clause 1
else
echo Clause 2
fi
由于否定运算符,它现在将显示条款 2
。在您的系统上试一试。
字符串的比较运算符
字符串的比较与数字的比较不同。以下是部分列表:
运算符 | 说明 |
---|---|
= |
这代表等于 |
!= |
这代表不等于 |
> |
这代表大于 |
< |
这代表小于 |
现在让我们看一下脚本 3:
第二章-脚本 3
1 #!/bin/sh
2 #
3 # 6/13/2017
4 #
5 echo "script3"
6
7 # String variables
8 str1="Kirk"
9 str2="Kirk"
10 str3="Spock"
11 str3="Dr. McCoy"
12 str4="Engineer Scott"
13 str5="A"
14 str6="B"
15
16 echo str1=$str1 str2=$str2 str3=$str3 str4=$str4
17
18 if [ "$str1" = "$str2" ] ; then
19 echo str1 equals str2
20 else
21 echo str1 does not equal str2
22 fi
23
24 if [ "$str1" != "$str2" ] ; then
25 echo str1 does not equal str2
26 else
27 echo str1 equals str2
28 fi
29
30 if [ "$str1" = "$str3" ] ; then
31 echo str1 equals str3
32 else
33 echo str1 does not equal str3
34 fi
35
36 if [ "$str3" = "$str4" ] ; then
37 echo str3 equals str4
38 else
39 echo str3 does not equal str4
40 fi
41
42 echo str5=$str5 str6=$str6
43
44 if [ "$str5" \> "$str6" ] ; then # must escape the >
45 echo str5 is greater than str6
46 else
47 echo str5 is not greater than str6
48 fi
49
50 if [[ "$str5" > "$str6" ]] ; then # or use double brackets
51 echo str5 is greater than str6
52 else
53 echo str5 is not greater than str6
54 fi
55
56 if [[ "$str5" < "$str6" ]] ; then # double brackets
57 echo str5 is less than str6
58 else
59 echo str5 is not less than str6
60 fi
61
62 if [ -n "$str1" ] ; then # test if str1 is not null
63 echo str1 is not null
64 fi
65
66 if [ -z "$str7" ] ; then # test if str7 is null
67 echo str7 is null
68 fi
69 echo "End of script3"
70
这是我系统的输出:
让我们逐行看一下这个:
-
第 7-14 行设置了变量
-
第 16 行显示它们的值
-
第 18 行检查相等性
-
第 24 行使用不等运算符
-
直到第 50 行的内容都是不言自明的
-
第 44 行需要一些澄清。为了避免语法错误,必须转义
>
和<
运算符 -
这是通过使用反斜杠(或转义)
\
字符来实现的 -
第 50 行显示了如何使用双括号处理大于运算符。正如您在第 58 行中看到的那样,它也适用于小于运算符。我的偏好将是在需要时使用双括号。
-
第 62 行显示了如何检查一个字符串是否为
not null
。 -
第 66 行显示了如何检查一个字符串是否为
null
。
仔细查看这个脚本,确保你能够清楚地理解它。还要注意str7
被显示为null
,但实际上我们并没有声明str7
。在脚本中这样做是可以的,不会产生错误。然而,作为编程的一般规则,最好在使用变量之前声明所有变量。这样你和其他人都能更容易理解和调试你的代码。
在编程中经常出现的一种情况是有多个条件需要测试。例如,如果某件事是真的,而另一件事也是真的,就采取这个行动。这是通过使用逻辑运算符来实现的。
这里是脚本 4,展示了逻辑运算符的使用:
第二章 - 脚本 4
#!/bin/sh
#
# 5/1/2017
#
echo "script4 - Linux Scripting Book"
if [ $# -ne 4 ] ; then
echo "Usage: script4 number1 number2 number3 number4"
echo " Please enter 4 numbers."
exit 255
fi
echo Parameters: $1 $2 $3 $4
echo Showing logical AND
if [[ $1 -eq $2 && $3 -eq $4 ]] ; then # logical AND
echo Clause 1
else
echo Clause 2
fi
echo Showing logical OR
if [[ $1 -eq $2 || $3 -eq $4 ]] ; then # logical OR
echo Clause 1
else
echo Clause 2
fi
echo "End of script4"
exit 0
这是我的系统上的输出:
在你的系统上使用不同的参数运行这个脚本。在每次尝试时,尝试确定输出是什么,然后运行它。直到你每次都能做对为止,重复这个过程。现在理解这个概念将对我们在后面处理更复杂的脚本时非常有帮助。
现在让我们看一下脚本 5,看看如何执行数学运算:
第二章 - 脚本 5
#!/bin/sh
#
# 5/1/2017
#
echo "script5 - Linux Scripting Book"
num1=1
num2=2
num3=0
num4=0
sum=0
echo num1=$num1
echo num2=$num2
let sum=num1+num2
echo "The sum is: $sum"
let num1++
echo "num1 is now: $num1"
let num2--
echo "num2 is now: $num2"
let num3=5
echo num3=$num3
let num3=num3+10
echo "num3 is now: $num3"
let num3+=10
echo "num3 is now: $num3"
let num4=50
echo "num4=$num4"
let num4-=10
echo "num4 is now: $num4"
echo "End of script5"
以下是输出:
如你所见,变量和以前一样设置。使用let
命令执行数学运算。注意没有使用$
前缀:
let sum=num1+num2
还要注意一些操作的简写方式。例如,假设你想将变量num1
增加1
。你可以这样做:
let num1=num1+1
或者,你可以使用简写表示法:
let num1++
运行这个脚本,并改变一些值,以了解数学运算的工作原理。我们将在后面的章节中更详细地讨论这个问题。
环境变量
到目前为止,我们只谈到了脚本中局部的变量。还有一些系统范围的环境变量(env vars),它们在任何 Linux 系统中都扮演着非常重要的角色。以下是一些,读者可能已经知道其中一些:
变量 | 角色 |
---|---|
HOME |
用户的主目录 |
PATH |
用于搜索命令的目录 |
PS1 |
命令行提示符 |
HOSTNAME |
主机名 |
SHELL |
正在使用的 shell |
USER |
本次会话的用户 |
EDITOR |
用于crontab 和其他程序的文本编辑器 |
HISTSIZE |
历史命令中将显示的命令数 |
TERM |
正在使用的命令行终端的类型 |
这些大多是不言自明的,但我会提到一些。
PS1
环境变量控制 shell 提示作为命令行的一部分显示的内容。默认设置通常是类似[guest1@big1 ~]$
的东西,这并不像它本来可以做的那样有用。至少,一个好的提示至少显示主机名和当前目录。
例如,当我在这一章上工作时,我的系统提示看起来就像这样:
big1 ~/LinuxScriptingBook/chapters/chap2 $
big1
是我的系统的主机名,~/LinuxScriptingBook/chapters/chap2
是当前目录。记住波浪号~
代表用户的home
目录;所以在我的情况下,这会扩展到:
/home/guest1/LinuxScriptingBook/chapters/chap2
"$"
表示我是在一个访客账户下运行。
为了启用这个功能,我的PS1
环境变量在/home/guest1/.bashrc
中定义如下:
export PS1="\h \w $ "
"\h"
显示主机名,\w
显示当前目录。这是一个非常有用的提示,我已经使用了很多年。这是如何显示用户名的方法:
export PS1="\u \h \w $ "
现在提示看起来是这样的:
guest1 big1 ~/LinuxScriptingBook/chapters/chap2 $
如果你在.bashrc
文件中更改PS1
变量,请确保在文件中已经存在的任何其他行之后这样做。
例如,这是我的guest1
账户下原始.bashrc
文件的内容:
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# User specific aliases and functions
在这些行之后放置你的PS1
定义。
注意
如果你每天登录很多不同的机器,有一个我发现非常有用的PS1
技巧。这将在后面的章节中展示。
你可能已经注意到,在本书的示例中,我并不总是使用一个良好的PS1
变量。这是在书的创作过程中编辑掉的,以节省空间。
EDITOR
变量非常有用。这告诉系统要使用哪个文本编辑器来编辑用户的crontab
(crontab -e
)等内容。如果没有设置,默认为 vi 编辑器。可以通过将其放入用户的.bashrc
文件中进行更改。这是我 root 账户的样子:
export EDITOR=/lewis/bin64/kw
当我运行crontab -l
(或-e
)时,我的自己编写的文本编辑器会出现,而不是 vi。非常方便!
在这里我们将看一下脚本 6,它展示了我guest1
账户下系统上的一些变量:
第二章 - 脚本 6
#!/bin/sh
#
# 5/1/2017
#
echo "script6 - Linux Scripting Book"
echo HOME - $HOME
echo PATH - $PATH
echo HOSTNAME - $HOSTNAME
echo SHELL - $SHELL
echo USER - $USER
echo EDITOR - $EDITOR
echo HISTSIZE - $HISTSIZE
echo TERM - $TERM
echo "End of script6"
这是输出:
你也可以创建和使用自己的环境变量。这是 Linux 系统的一个非常强大的功能。这里有一些我在/root/.bashrc
文件中使用的例子:
BIN=/lewis/bin64
DOWN=/home/guest1/Downloads
DESK=/home/guest1/Desktop
JAVAPATH=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.99.x86_64/include/
KW_WORKDIR=/root
[email protected]
[email protected]
LBCUR=/home/guest1/LinuxScriptingBook/chapters/chap2
export BIN DOWN DESK JAVAPATH KW_WORKDIR L1 L4 LBCUR
-
BIN
:这是我的可执行文件和脚本的目录在根目录下 -
DOWN
:这是用于电子邮件附件下载的目录等 -
DESK
:这是屏幕截图的下载目录 -
JAVAPATH
:这是我编写 Java 应用程序时要使用的目录 -
KW_WORKDIR
:这是我的编辑器放置其工作文件的位置 -
L1
和L2
:这是我笔记本电脑的 IP 地址 -
LBCUR
:这是我为本书工作的当前目录
确保导出你的变量,以便其他终端可以访问它们。还记得当你做出改变时要源化你的.bashrc
。在我的系统上,命令是:
guest1 $ . /home/guest1/.bashrc
提示
不要忘记命令开头的句点!
我将在后面的章节中展示这些环境变量如何与别名配对。例如,我的系统上的bin
命令是一个将当前目录更改为/lewis/bin64
目录的别名。这是 Linux 系统中最强大的功能之一,然而,我总是惊讶地发现它并不经常被使用。
我们在本章中要介绍的最后一种变量类型叫做数组。假设你想编写一个包含实验室中所有机器 IP 地址的脚本。你可以这样做:
L0=192.168.1.1
L1=192.168.1.10
L2=192.168.1.15
L3=192.168.1.16
L4=192.168.1.20
L5=192.168.1.26
这将起作用,事实上我在我的家庭办公室/实验室中做了类似的事情。然而,假设你有很多机器。使用数组可以让你的生活变得简单得多。
看一下脚本 7:
第二章 - 脚本 7
#!/bin/sh
#
# 5/1/2017
#
echo "script7 - Linux Scripting Book"
array_var=(1 2 3 4 5 6)
echo ${array_var[0]}
echo ${array_var[1]}
echo ${array_var[2]}
echo ${array_var[3]}
echo ${array_var[4]}
echo ${array_var[5]}
echo "List all elements:"
echo ${array_var[*]}
echo "List all elements (alternative method):"
echo ${array_var[@]}
echo "Number of elements: ${#array_var[*]}"
labip[0]="192.168.1.1"
labip[1]="192.168.1.10"
labip[2]="192.168.1.15"
labip[3]="192.168.1.16"
labip[4]="192.168.1.20"
echo ${labip[0]}
echo ${labip[1]}
echo ${labip[2]}
echo ${labip[3]}
echo ${labip[4]}
echo "List all elements:"
echo ${labip[*]}
echo "Number of elements: ${#labip[*]}"
echo "End of script7"
这是我系统上的输出:
在你的系统上运行这个脚本并尝试进行实验。如果你以前从未见过或使用过数组,不要让它们吓到你;你很快就会熟悉它们。这是另一个容易忘记${数组变量}
语法的地方,所以如果脚本不按你的意愿执行(或生成错误),首先检查这个。
在下一章中,当我们讨论循环时,我们将更详细地讨论数组。
总结
在本章中,我们介绍了如何声明和使用环境变量和本地变量。我们讨论了如何进行数学运算以及如何处理数组。
我们还介绍了在脚本中使用变量。脚本 1展示了如何分配一个变量并显示其值。脚本 2展示了如何处理数字变量,脚本 3展示了如何比较字符串。脚本 4展示了逻辑运算符,脚本 5展示了如何进行数学运算。脚本 6展示了如何使用环境变量,脚本 7展示了如何使用数组。
第三章:使用循环和 sleep 命令
本章展示了如何使用循环执行迭代操作。它还展示了如何在脚本中创建延迟。读者将学习如何在脚本中使用循环和sleep
命令。
本章涵盖的主题如下:
-
标准的
for
、while
和until
循环。 -
循环的嵌套,以及如何避免混淆。
-
介绍
sleep
命令以及它在脚本中如何用于造成延迟。 -
讨论使用
sleep
的一个常见陷阱。
使用循环
任何编程语言最重要的特性之一就是能够执行一个任务或多个任务,然后在满足结束条件时停止。这是通过使用循环来实现的。
下一节展示了一个非常简单的while
循环的例子:
第三章 - 脚本 1
#!/bin/sh
#
# 5/2/2017
#
echo "script1 - Linux Scripting Book"
x=1
while [ $x -le 10 ]
do
echo x: $x
let x++
done
echo "End of script1"
exit 0
以下是输出:
我们首先将变量x
设置为1
。while
语句检查x
是否小于或等于10
,如果是,则运行do
和done
语句之间的命令。它将继续这样做,直到x
等于11
,此时done
语句后的行将被运行。
在你的系统上运行这个。理解这个脚本非常重要,这样我们才能进入更高级的循环。
让我们在下一节看另一个脚本,看看你能否确定它有什么问题。
第三章 - 脚本 2
#!/bin/sh
#
# 5/2/2017
#
echo "script2 - Linux Scripting Book"
x=1
while [ $x -ge 0 ]
do
echo x: $x
let x++
done
echo "End of script2"
exit 0
随意跳过这个脚本的运行,除非你真的想要。仔细看while
测试。它说当x
大于或等于0
时,运行循环内的命令。x
会不会不满足这个条件?不会,这就是所谓的无限循环。不用担心;你仍然可以通过按下Ctrl + C(按住Ctrl键然后按C键)来终止脚本。
我想立即介绍无限循环,因为你几乎肯定会偶尔这样做,我想让你知道当发生这种情况时如何终止脚本。当我刚开始学习时,我肯定做过几次。
好了,让我们做一些更有用的事情。假设你正在开始一个新项目,需要在你的系统上创建一些目录。你可以一次执行一个命令,或者在脚本中使用循环。
我们将在脚本 3中看到这个。
第三章 - 脚本 3
#!/bin/sh
#
# 5/2/2017
#
echo "script3 - Linux Scripting Book"
x=1
while [ $x -le 10 ]
do
echo x=$x
mkdir chapter$x
let x++
done
echo "End of script3"
exit 0
这个简单的脚本假设你是从基本目录开始的。运行时,它将创建chapter 1
到chapter 10
的目录,然后继续到结束。
在运行对计算机进行更改的脚本时,最好在真正运行之前确保逻辑是正确的。例如,在运行这个脚本之前,我注释掉了mkdir
行。然后我运行脚本,确保它在显示x
等于10
后停止。然后我取消注释该行并真正运行它。
屏幕操作
我们将在下一节中看到另一个使用循环在屏幕上显示文本的脚本:
第三章 - 脚本 4
#!/bin/sh
#
# 5/2/2017
#
echo "script4 - Linux Scripting Book"
if [ $# -ne 1 ] ; then
echo "Usage: script4 string"
echo "Will display the string on every line."
exit 255
fi
tput clear # clear the screen
x=1
while [ $x -le $LINES ]
do
echo "********** $1 **********"
let x++
done
exit 0
在执行这个脚本之前运行以下命令:
echo $LINES
如果终端中没有显示行数,请运行以下命令:
export LINES=$LINES
然后继续运行脚本。在我的系统上,当使用script4
Linux
运行时,输出如下:
好吧,我同意这可能不是非常有用,但它确实展示了一些东西。LINES
环境变量包含当前终端中的行数。这对于在更复杂的脚本中限制输出可能很有用,这将在后面的章节中展示。这个例子还展示了如何在脚本中操作屏幕。
如果需要导出LINES
变量,你可能希望将其放在你的.bashrc
文件中并重新加载它。
我们将在下一节中看另一个脚本:
第三章 - 脚本 5
#!/bin/sh
#
# 5/2/2017
#
# script5 - Linux Scripting Book
tput clear # clear the screen
row=1
while [ $row -le $LINES ]
do
col=1
while [ $col -le $COLUMNS ]
do
echo -n "#"
let col++
done
echo "" # output a carriage return
let row++
done
exit 0
这与脚本 4类似,它展示了如何在终端的范围内显示输出。注意,你可能需要像我们使用LINES
变量一样导出COLUMNS
环境变量。
您可能已经注意到这个脚本中有一点不同。在while
语句内部有一个while
语句。这称为嵌套循环,在编程中经常使用。
我们首先声明row=1
,然后开始外部while
循环。然后将col
变量设置为1
,然后启动内部循环。这个内部循环显示了该行每一列的字符。当到达行的末尾时,循环结束,echo
语句输出回车。然后增加row
变量,然后再次开始该过程。在最后一行结束后结束。
通过仅使用LINES
和COLUMNS
环境变量,可以将实际屏幕写入。您可以通过运行程序然后扩展终端来测试这一点。
在使用嵌套循环时,很容易混淆哪里放什么。这是我每次都尝试做的事情。当我第一次意识到程序(可以是脚本、C、Java 等)需要一个循环时,我首先编写循环体,就像这样:
while [ condition ]
do
other statements will go here
done
这样我就不会忘记done
语句,而且它也排列得很整齐。如果我需要另一个循环,我只需再次执行它:
while [ condition ]
do
while [ condition ]
do
other statements will go here
done
done
您可以嵌套任意多个循环。
缩进您的代码
现在可能是谈论缩进的好时机。在过去(即 30 多年前),每个人都使用等宽字体的文本编辑器来编写他们的代码,因此只需一个空格的缩进就可以相对容易地保持一切对齐。后来,当人们开始使用具有可变间距字体的文字处理器时,变得更难看到缩进,因此使用了更多的空格(或制表符)。我的建议是使用您感觉最舒适的方式。但是,话虽如此,您可能必须学会阅读和使用公司制定的任何代码风格。
到目前为止,我们只谈到了while
语句。现在让我们在下一节中看看until
循环:
第三章 - 脚本 6
#!/bin/sh
#
# 5/3/2017
#
echo "script6 - Linux Scripting Book"
echo "This shows the while loop"
x=1
while [ $x -lt 11 ] # perform the loop while the condition
do # is true
echo "x: $x"
let x++
done
echo "This shows the until loop"
x=1
until [ $x -gt 10 ] # perform the loop until the condition
do # is true
echo "x: $x"
let x++
done
echo "End of script6"
exit 0
输出:
看看这个脚本。两个循环的输出是相同的;但是,条件是相反的。第一个循环在条件为真时继续,第二个循环在条件为真时继续。这是一个不那么微妙的区别,所以要注意这一点。
使用for
语句
循环的另一种方法是使用for
语句。在处理文件和其他列表时通常使用。for
循环的一般语法如下:
for variable in list
do
some commands
done
列表可以是字符串集合,也可以是文件名通配符等。我们可以在下一节中给出的示例中看一下这一点。
第三章 - 脚本 7
#!/bin/sh
#
# 5/4/2017
#
echo "script7 - Linux Scripting Book"
for i in jkl.c bob Linux "Hello there" 1 2 3
do
echo -n "$i "
done
for i in script* # returns the scripts in this directory
do
echo $i
done
echo "End of script7"
exit 0
以及我的系统输出。这是我的chap3
目录:
下一个脚本显示了for
语句如何与文件一起使用:
第三章 - 脚本 8
#!/bin/sh
#
# 5/3/2017
#
echo "script8 - Linux Scripting Book"
if [ $# -eq 0 ] ; then
echo "Please enter at least 1 parameter."
exit 255
fi
for i in $* # the "$*" returns every parameter given
do # to the script
echo -n "$i "
done
echo "" # carriage return
echo "End of script8"
exit 0
以下是输出:
您可以使用for
语句做一些其他事情,请参阅 Bash 的man
页面以获取更多信息。
提前离开循环
有时在编写脚本时,您会遇到一种情况,希望在满足结束条件之前提前退出循环。可以使用break
和continue
命令来实现这一点。
这是一个显示这些命令的脚本。我还介绍了sleep
命令,将在下一个脚本中详细讨论。
第三章 - 脚本 9
#!/bin/sh
#
# 5/3/2017
#
echo "script9 - Linux Scripting Book"
FN1=/tmp/break.txt
FN2=/tmp/continue.txt
x=1
while [ $x -le 1000000 ]
do
echo "x:$x"
if [ -f $FN1 ] ; then
echo "Running the break command"
rm -f $FN1
break
fi
if [ -f $FN2 ] ; then
echo "Running the continue command"
rm -f $FN2
continue
fi
let x++
sleep 1
done
echo "x:$x"
echo "End of script9"
exit 0
这是我的系统输出:
在您的系统上运行此命令,并在另一个终端中cd
到/tmp
目录。运行命令touch continue.txt
并观察发生了什么。如果愿意,您可以多次执行此操作(请记住,上箭头会调用上一个命令)。请注意,当命中continue
命令时,变量x
不会增加。这是因为控制立即返回到while
语句。
现在运行touch break.txt
命令。脚本将结束,再次,x
没有被增加。这是因为break
立即导致循环结束。
break
和continue
命令在脚本中经常使用,因此一定要充分尝试,真正理解发生了什么。
睡眠命令
我之前展示了sleep
命令,让我们更详细地看一下。一般来说,sleep
命令用于在脚本中引入延迟。例如,在前面的脚本中,如果我没有使用sleep
,输出会太快而无法看清发生了什么。
sleep
命令接受一个参数,指示延迟的时间。例如,sleep 1
表示引入 1 秒的延迟。以下是一些示例:
sleep 1 # sleep 1 second (the default is seconds)
sleep 1s # sleep 1 second
sleep 1m # sleep 1 minute
sleep 1h # sleep 1 hour
sleep 1d # sleep 1 day
sleep
命令实际上比这里展示的更有能力。有关更多信息,请参阅man
页面(man sleep
)。
以下是一个更详细展示了sleep
工作原理的脚本:
第三章 - 脚本 10
#!/bin/sh
#
# 5/3/2017
#
echo "script10 - Linux Scripting Book"
echo "Sleeping seconds..."
x=1
while [ $x -le 5 ]
do
date
let x++
sleep 1
done
echo "Sleeping minutes..."
x=1
while [ $x -le 2 ]
do
date
let x++
sleep 1m
done
echo "Sleeping hours..."
x=1
while [ $x -le 2 ]
do
date
let x++
sleep 1h
done
echo "End of script10"
exit 0
和输出:
您可能已经注意到,我按下了Ctrl + C来终止脚本,因为我不想等待 2 个小时才能完成。这种类型的脚本在 Linux 系统中被广泛使用,用于监视进程,观察文件等。
在使用sleep
命令时有一个常见的陷阱需要提到。
注意
请记住,sleep
命令会在脚本中引入延迟。明确地说,当您编写sleep 60
时,这意味着引入 60 秒的延迟;而不是每 60 秒运行一次脚本。这是一个很大的区别。
我们将在下一节中看到一个例子:
第三章 - 脚本 11
#!/bin/sh
#
# 5/3/2017
#
echo "script11 - Linux Scripting Book"
while [ true ]
do
date
sleep 60 # 60 seconds
done
echo "End of script11"
exit 0
这是我的系统输出。最终会出现不同步的情况:
对于绝大多数脚本来说,这永远不会成为一个问题。只要记住,如果您要完成的任务是时间关键的,比如每天晚上准确在 12:00 运行一个命令,您可能需要考虑其他方法。请注意,crontab
也不会做到这一点,因为在运行命令之前会有大约 1 到 2 秒的延迟。
监视一个进程
在本章中,还有一些其他主题需要我们看一下。假设您希望在系统上运行的进程结束时收到警报。
以下是一个脚本,当指定的进程结束时通知用户。请注意,还有其他方法可以完成这个任务,这只是一种方法。
第三章 - 脚本 12
#!/bin/sh
#
# 5/3/2017
#
echo "script12 - Linux Scripting Book"
if [ $# -ne 1 ] ; then
echo "Usage: script12 process-directory"
echo " For example: script12 /proc/20686"
exit 255
fi
FN=$1 # process directory i.e. /proc/20686
rc=1
while [ $rc -eq 1 ]
do
if [ ! -d $FN ] ; then # if directory is not there
echo "Process $FN is not running or has been terminated."
let rc=0
else
sleep 1
fi
done
echo "End of script12"
exit 0
要查看此脚本的运行情况,请运行以下命令:
-
在终端中运行
script9
-
在另一个终端中运行
ps auxw | grep script9
。输出将类似于这样:
guest1 20686 0.0 0.0 106112 1260 pts/34 S+ 17:20 0:00 /bin/sh ./script9
guest1 23334 0.0 0.0 103316 864 pts/18 S+ 17:24 0:00 grep script9
- 使用
script9
的进程 ID(在本例中为20686
),并将其用作运行script12
的参数:
$ script12 /proc/20686
如果您愿意,可以让它运行一段时间。最终返回到运行script9
的终端,并使用Ctrl + C终止它。您将看到script12
输出一条消息,然后也终止。随时尝试这个,因为它包含了很多重要信息。
您可能会注意到,在这个脚本中,我使用了一个变量rc
来确定何时结束循环。我可以使用我们在本章前面看到的break
命令。然而,使用控制变量(通常被称为)被认为是更好的编程风格。
当您启动一个命令然后它花费的时间比您预期的时间长时,这样的脚本非常有用。
例如,前段时间我使用mkfs
命令在一个外部 1TB USB 驱动器上启动了一个格式化操作。它花了几天的时间才完成,我想确切地知道何时完成,以便我可以继续使用该驱动器。
创建编号的备份文件
现在作为一个奖励,这是一个可以直接运行的脚本,可以用来创建编号的备份文件。在我想出这个方法之前(很多年前),我会手工制作备份的仪式。我的编号方案并不总是一致的,我很快意识到让脚本来做这件事会更容易。这正是计算机擅长的事情。
我称这个脚本为cbS
。我写这个脚本已经很久了,我甚至不确定它代表什么。也许是计算机备份脚本之类的东西。
第三章-脚本 13
#!/bin/sh
#
echo "cbS by Lewis 5/4/2017"
if [ $# -eq 0 ] ; then
echo "Usage: cbS filename(s) "
echo " Will make a numbered backup of the files(s) given."
echo " Files must be in the current directory."
exit 255
fi
rc=0 # return code, default is no error
for fn in $* # for each filename given on the command line
do
if [ ! -f $fn ] ; then # if not found
echo "File $fn not found."
rc=1 # one or more files were not found
else
cnt=1 # file counter
loop1=0 # loop flag
while [ $loop1 -eq 0 ]
do
tmp=bak-$cnt.$fn
if [ ! -f $tmp ] ; then
cp $fn $tmp
echo "File "$tmp" created."
loop1=1 # end the inner loop
else
let cnt++ # try the next one
fi
done
fi
done
exit $rc # exit with return code
它以一个Usage
消息开始,因为它至少需要一个文件名来操作。
请注意,这个命令要求文件在当前目录中,所以像cbS /tmp/file1.txt
这样的操作会产生错误。
rc
变量被初始化为0
。如果找不到文件,它将被设置为1
。
现在让我们来看内部循环。这里的逻辑是使用cp
命令从原始文件创建一个备份文件。备份文件的命名方案是bak-(数字).原始文件名
,其中数字
是下一个顺序中的数字。代码通过查看所有的bak-#.文件名
文件来确定下一个数字是什么。直到找不到一个为止。然后那个就成为新的文件名。
在你的系统上让这个脚本运行起来。随意给它取任何你喜欢的名字,但要小心给它取一个不同于现有的 Linux 命令的名字。使用which
命令来检查。
这是我系统上的一些示例输出:
这个脚本可以得到很大的改进。它可以被制作成适用于路径/文件,并且应该检查cp
命令是否有错误。这种编码水平将在后面的章节中介绍。
总结
在本章中,我们介绍了不同类型的循环语句以及它们之间的区别。还介绍了嵌套循环和sleep
命令。还提到了使用sleep
命令时的常见陷阱,并介绍了一个备份脚本,展示了如何轻松创建编号的备份文件。
在下一章中,我们将介绍子程序的创建和调用。
第四章:创建和调用子程序
本章介绍了如何在脚本中创建和调用子程序。
本章涵盖的主题如下:
-
显示一些简单的子程序。
-
显示更高级的例程。
-
再次提到返回代码以及它们在脚本中的工作方式。
在前几章中,我们主要看到了一些不太复杂的简单脚本。脚本实际上可以做更多的事情,我们将很快看到。
首先,让我们从一些简单但强大的脚本开始。这些主要是为了让读者了解脚本可以快速完成的工作。
清屏
tput clear
终端命令可用于清除当前的命令行会话。您可以一直输入tput clear
,但只输入cls
会不会更好?
这是一个简单的清除当前屏幕的脚本:
第四章 - 脚本 1
#!/bin/sh
#
# 5/8/2017
#
tput clear
请注意,这是如此简单,以至于我甚至都没有包括Usage
消息或返回代码。记住,要在您的系统上将其作为命令执行,请执行以下操作:
-
cd $HOME/bin
-
创建/编辑名为
cls
的文件 -
将上述代码复制并粘贴到此文件中
-
保存文件
-
运行
chmod 755 cls
现在您可以在任何终端(在该用户下)输入cls
,屏幕将被清除。试试看。
文件重定向
在这一点上,我们需要讨论文件重定向。这是将命令或脚本的输出复制到文件而不是显示在屏幕上的能力。这是通过使用重定向运算符来完成的,实际上就是大于号。
这是我在我的系统上运行的一些命令的屏幕截图:
如您所见,ifconfig
命令的输出被发送(或重定向)到ifconfig.txt
文件。
命令管道
现在让我们看看命令管道,即运行一个命令并将其输出作为另一个命令的输入的能力。
假设您的系统上正在运行名为loop1
的程序或脚本,并且您想知道其 PID。您可以运行ps auxw
命令到一个文件,然后使用grep
命令在文件中搜索loop1
。或者,您可以使用管道一步完成如下操作:
很酷,对吧?这是 Linux 系统中非常强大的功能,并且被广泛使用。我们很快将看到更多。
接下来的部分显示了另一个非常简短的使用一些命令管道的脚本。它清除屏幕,然后仅显示dmesg
的前 10 行:
第四章 - 脚本 2
#!/bin/sh
#
# 5/8/2017
#
tput clear
dmesg | head
以下是输出:
接下来的部分显示文件重定向。
第四章 - 脚本 3
#!/bin/sh
#
# 5/8/2017
#
FN=/tmp/dmesg.txt
dmesg > $FN
echo "File $FN created."
exit 0
在您的系统上试一试。
这显示了创建一个脚本来执行通常在命令行上键入的命令是多么容易。还要注意FN
变量的使用。如果以后要使用不同的文件名,您只需在一个地方进行更改。
子程序
现在让我们真正进入子程序。为此,我们将使用更多的tput
命令:
tput cup <row><col> # moves the cursor to row, col
tput cup 0 0 # cursor to the upper left hand side
tput cup $LINES $COLUMNS # cursor to bottom right hand side
tput clear # clears the terminal screen
tput smso # bolds the text that follows
tput rmso # un-bolds the text that follows
这是脚本。这主要是为了展示子程序的概念,但也可以作为编写交互式工具的指南使用。
第四章 - 脚本 4
#!/bin/sh
# 6/13/2017
# script4
# Subroutines
cls()
{
tput clear
return 0
}
home()
{
tput cup 0 0
return 0
}
end()
{
let x=$COLUMNS-1
tput cup $LINES $x
echo -n "X" # no newline or else will scroll
}
bold()
{
tput smso
}
unbold()
{
tput rmso
}
underline()
{
tput smul
}
normalline()
{
tput rmul
}
# Code starts here
rc=0 # return code
if [ $# -ne 1 ] ; then
echo "Usage: script4 parameter"
echo "Where parameter can be: "
echo " home - put an X at the home position"
echo " cls - clear the terminal screen"
echo " end - put an X at the last screen position"
echo " bold - bold the following output"
echo " underline - underline the following output"
exit 255
fi
parm=$1 # main parameter 1
if [ "$parm" = "home" ] ; then
echo "Calling subroutine home."
home
echo -n "X"
elif [ "$parm" = "cls" ] ; then
cls
elif [ "$parm" = "end" ] ; then
echo "Calling subroutine end."
end
elif [ "$parm" = "bold" ] ; then
echo "Calling subroutine bold."
bold
echo "After calling subroutine bold."
unbold
echo "After calling subroutine unbold."
elif [ "$parm" = "underline" ] ; then
echo "Calling subroutine underline."
underline
echo "After subroutine underline."
normalline
echo "After subroutine normalline."
else
echo "Unknown parameter: $parm"
rc=1
fi
exit $rc
以下是输出:
在您的系统上尝试一下。如果您使用home
参数运行它,可能会对您看起来有点奇怪。代码在home 位置
(0,0)放置了一个大写的X
,这会导致提示打印一个字符。这里没有错,只是看起来有点奇怪。如果这对您来说仍然不合理,不要担心,继续查看脚本 5。
使用参数
好的,让我们向这个脚本添加一些例程,以展示如何在子例程
中使用参数。为了使输出看起来更好,首先调用cls
例程清除屏幕:
第四章 - 脚本 5
#!/bin/sh
# 6/13/2017
# script5
# Subroutines
cls()
{
tput clear
return 0
}
home()
{
tput cup 0 0
return 0
}
end()
{
let x=$COLUMNS-1
tput cup $LINES $x
echo -n "X" # no newline or else will scroll
}
bold()
{
tput smso
}
unbold()
{
tput rmso
}
underline()
{
tput smul
}
normalline()
{
tput rmul
}
move() # move cursor to row, col
{
tput cup $1 $2
}
movestr() # move cursor to row, col
{
tput cup $1 $2
echo $3
}
# Code starts here
cls # clear the screen to make the output look better
rc=0 # return code
if [ $# -ne 1 ] ; then
echo "Usage: script5 parameter"
echo "Where parameter can be: "
echo " home - put an X at the home position"
echo " cls - clear the terminal screen"
echo " end - put an X at the last screen position"
echo " bold - bold the following output"
echo " underline - underline the following output"
echo " move - move cursor to row,col"
echo " movestr - move cursor to row,col and output string"
exit 255
fi
parm=$1 # main parameter 1
if [ "$parm" = "home" ] ; then
home
echo -n "X"
elif [ "$parm" = "cls" ] ; then
cls
elif [ "$parm" = "end" ] ; then
move 0 0
echo "Calling subroutine end."
end
elif [ "$parm" = "bold" ] ; then
echo "Calling subroutine bold."
bold
echo "After calling subroutine bold."
unbold
echo "After calling subroutine unbold."
elif [ "$parm" = "underline" ] ; then
echo "Calling subroutine underline."
underline
echo "After subroutine underline."
normalline
echo "After subroutine normalline."
elif [ "$parm" = "move" ] ; then
move 10 20
echo "This line started at row 10 col 20"
elif [ "$parm" = "movestr" ] ; then
movestr 15 40 "This line started at 15 40"
else
echo "Unknown parameter: $parm"
rc=1
fi
exit $rc
由于此脚本只有两个额外的功能,您可以直接运行它们。这将逐个命令显示如下:
guest1 $ script5
guest1 $ script5 move
guest1 $ script5 movestr
由于我们现在将光标放在特定位置,输出对您来说应该更有意义。请注意,命令行提示重新出现在上次光标位置的地方。
您可能已经注意到,子例程的参数与脚本的参数工作方式相同。参数 1 是$1
,参数 2 是$2
,依此类推。这既是好事也是坏事,好的是您不必学习任何根本不同的东西。但坏的是,如果不小心,很容易混淆$1
,$2
等变量。
一个可能的解决方案,也是我使用的解决方案,是将主脚本中的$1
,$2
等变量分配给一个有意义的变量。
例如,在这些示例脚本中,我将parm1
设置为$1(parm1=$1)
,依此类推。
请仔细查看下一节中的脚本:
第四章-脚本 6
#!/bin/sh
#
# 6/13/2017
# script6
# Subroutines
sub1()
{
echo "Entering sub1"
rc1=0 # default is no error
if [ $# -ne 1 ] ; then
echo "sub1 requires 1 parameter"
rc1=1 # set error condition
else
echo "1st parm: $1"
fi
echo "Leaving sub1"
return $rc1 # routine return code
}
sub2()
{
echo "Entering sub2"
rc2=0 # default is no error
if [ $# -ne 2 ] ; then
echo "sub2 requires 2 parameters"
rc2=1 # set error condition
else
echo "1st parm: $1"
echo "2nd parm: $2"
fi
echo "Leaving sub2"
return $rc2 # routine return code
}
sub3()
{
echo "Entering sub3"
rc3=0 # default is no error
if [ $# -ne 3 ] ; then
echo "sub3 requires 3 parameters"
rc3=1 # set error condition
else
echo "1st parm: $1"
echo "2nd parm: $2"
echo "3rd parm: $3"
fi
echo "Leaving sub3"
return $rc3 # routine return code
}
cls() # clear screen
{
tput clear
return $? # return code from tput
}
causeanerror()
{
echo "Entering causeanerror"
tput firephasers
return $? # return code from tput
}
# Code starts here
cls # clear the screen
rc=$?
echo "return code from cls: $rc"
rc=0 # reset the return code
if [ $# -ne 3 ] ; then
echo "Usage: script6 parameter1 parameter2 parameter3"
echo "Where all parameters are simple strings."
exit 255
fi
parm1=$1 # main parameter 1
parm2=$2 # main parameter 2
parm3=$3 # main parameter 3
# show main parameters
echo "parm1: $parm1 parm2: $parm2 parm3: $parm3"
sub1 "sub1-parm1"
echo "return code from sub1: $?"
sub2 "sub2-parm1"
echo "return code from sub2: $?"
sub3 $parm1 $parm2 $parm3
echo "return code from sub3: $?"
causeanerror
echo "return code from causeanerror: $?"
exit $rc
以下是输出
这里有一些新概念,所以我们会非常仔细地讲解这个。
首先,我们定义了子例程。请注意,已添加了返回代码。还包括了一个cls
例程,以便显示返回代码。
我们现在开始编码。调用cls
例程,然后将其返回值存储在rc
变量中。然后将显示显示脚本标题的echo
语句。
那么,为什么我必须将cls
命令的返回代码放入rc
变量中呢?我不能在显示脚本标题的echo
之后直接显示它吗?不行,因为echo $?
总是指的是紧随其后的命令。这很容易被忘记,所以请确保您理解这一点。
好的,现在我们将rc
变量重置为0
并继续。我本可以使用不同的变量,但由于rc
的值不会再次需要,我选择重用rc
变量。
现在,在检查参数时,如果没有三个参数,将显示Usage
语句。
输入三个参数后,我们会显示它们。这总是一个好主意,特别是在首次编写脚本/程序时。如果不需要,您随时可以将其删除。
第一个子例程sub1
以1
个参数运行。这将进行检查,如果需要,将显示错误。
sub2
也是一样的情况,但在这种情况下,我故意设置它只运行一个参数,以便显示错误消息。
对于sub3
,您可以看到主要参数仍然可以从子例程中访问。实际上,所有命名变量都可以访问,还有通配符*
和其他文件扩展标记。只有主脚本参数无法访问,这就是为什么我们将它们放入变量中的原因。
最后,创建了最终例程以展示如何处理错误。您可以看到,tput
命令本身显示了错误,然后我们还在脚本中捕获了它。
最后,脚本以主rc
变量退出。
正如前面提到的,这个脚本包含了很多内容,所以一定要仔细研究它。请注意,当我想在tput
中显示错误时,我只是假设firephasers
将成为一个未知的命令。如果一些相位器实际上从我的计算机中射出(或更糟的是,射入),我会感到非常惊讶!
备份您的工作
现在,作为另一个奖励,下一节显示了我用来每 60 秒备份当前书籍章节的脚本:
第四章-脚本 7
#!/bin/sh
#
# Auto backs up the file given if it has changed
# Assumes the cbS command exists
# Checks that ../back exists
# Copies to specific USB directory
# Checks if filename.bak exists on startup, copy if it doesn't
echo "autobackup by Lewis 5/9/2017 A"
if [ $# -ne 3 ] ; then
echo "Usage: autobackup filename USB-backup-dir delay"
exit 255
fi
# Create back directory if it does not exist
if [ ! -d back ] ; then
mkdir back
fi
FN=$1 # filename to monitor
USBdir=$2 # USB directory to copy to
DELAY=$3 # how often to check
if [ ! -f $FN ] ; then # if no filename abort
echo "File: $FN does not exist."
exit 5
fi
if [ ! -f $FN.bak ] ; then
cp $FN $FN.bak
fi
filechanged=0
while [ 1 ]
do
cmp $FN $FN.bak
rc=$?
if [ $rc -ne 0 ] ; then
cp $FN back
cp $FN $USBdir
cd back
cbS $FN
cd ..
cp $FN $FN.bak
filechanged=1
fi
sleep $DELAY
done
在我的系统上的输出
这个脚本中没有我们尚未涵盖的内容。顶部的非正式注释主要是为了我自己,这样我就不会忘记我写了什么或为什么写了。
检查参数并在不存在时创建后备子目录。我似乎总是记不住要创建它,所以我让脚本来做。
接下来,设置了主要变量,然后如果.bak
文件不存在就创建它(这有助于逻辑)。
在while
循环中,你可以看到它永远运行,使用cmp
Linux 命令来查看原始文件是否与备份文件发生了变化。如果是,cmp
命令返回非零值,文件将使用我们的cbS
脚本作为带编号的备份复制回subdir
。该文件也会被复制到备份目录,这种情况下是我的 USB 驱动器。循环会一直持续,直到我开始新的章节,这时我按下Ctrl + C退出。
这是脚本自动化的一个很好的例子,将在第六章使用脚本自动化任务中更详细地介绍。
总结
我们从一些非常简单的脚本开始,然后继续展示一些简单的子程序。
然后我们展示了一些带参数的子程序。再次提到了返回码,以展示它们在子程序中的工作原理。我们包括了几个脚本来展示这些概念,并且还额外免费包含了一个特别的奖励脚本。
在下一章中,我们将介绍如何创建交互式脚本。
第五章:创建交互式脚本
本章展示了如何读取键盘以创建交互式脚本。
本章涵盖的主题有:
-
如何使用
read
内置命令查询键盘。 -
使用
read
的不同方式。 -
使用陷阱(中断)。
读者将学习如何创建交互式脚本。
到目前为止我们看过的脚本都没有太多用户交互。read
命令用于创建可以查询键盘的脚本。然后根据输入采取行动。
这是一个简单的例子:
第五章 - 脚本 1
#!/bin/sh
#
# 5/16/2017
#
echo "script1 - Linux Scripting Book"
echo "Enter 'q' to quit."
rc=0
while [ $rc -eq 0 ]
do
echo -n "Enter a string: "
read str
echo "str: $str"
if [ "$str" = "q" ] ; then
rc=1
fi
done
echo "End of script1"
exit 0
在我的系统上运行时的输出如下:
这是一个在您的系统上运行的好例子。尝试几种不同的字符串、数字等。注意返回的字符串包含空格、特殊字符等。你不必引用任何东西,如果你这样做了,那些也会被返回。
您还可以使用read
命令在脚本中加入简单的暂停。这将允许您在屏幕上滚动之前看到输出。它也可以在调试时使用,将在第九章 调试脚本中显示。
以下脚本显示了如何在输出到屏幕的最后一行时创建暂停:
第五章 - 脚本 2
#!/bin/sh
#
# 5/16/2017
# Chapter 5 - Script 2
#
linecnt=1 # line counter
loop=0 # loop control var
while [ $loop -eq 0 ]
do
echo "$linecnt $RANDOM" # display next random number
let linecnt++
if [ $linecnt -eq $LINES ] ; then
linecnt=1
echo -n "Press Enter to continue or q to quit: "
read str # pause
if [ "$str" = "q" ] ; then
loop=1 # end the loop
fi
fi
done
echo "End of script2"
exit 0
在我的系统上运行时的输出如下:
我按了两次Enter,然后在最后一个上按了Q和Enter。
让我们尝试一些更有趣的东西。下一个脚本显示了如何用从键盘获取的值填充数组:
第五章 - 脚本 3
#!/bin/sh
#
# 5/16/2017
#
echo "script3 - Linux Scripting Book"
if [ "$1" = "--help" ] ; then
echo "Usage: script3"
echo " Queries the user for values and puts them into an array."
echo " Entering 'q' will halt the script."
echo " Running 'script3 --help' shows this Usage message."
exit 255
fi
x=0 # subscript into array
loop=0 # loop control variable
while [ $loop -eq 0 ]
do
echo -n "Enter a value or q to quit: "
read value
if [ "$value" = "q" ] ; then
loop=1
else
array[$x]="$value"
let x++
fi
done
let size=x
x=0
while [ $x -lt $size ]
do
echo "array $x: ${array[x]}"
let x++
done
echo "End of script3"
exit 0
和输出:
由于这个脚本不需要任何参数,我决定添加一个Usage
语句。如果用户使用--help
运行它,这将显示,并且在许多系统脚本和程序中是一个常见的特性。
这个脚本中唯一新的东西是read
命令。loop
和array
变量在之前的章节中已经讨论过。再次注意,使用read
命令,你输入的就是你得到的。
现在让我们创建一个完整的交互式脚本。但首先我们需要检查当前终端的大小。如果太小,你的脚本输出可能会变得混乱,用户可能不知道原因或如何修复。
以下脚本包含一个检查终端大小的子例程:
第五章 - 脚本 4
#!/bin/sh
#
# 5/16/2017
#
echo "script4 - Linux Scripting Book"
checktermsize()
{
rc1=0 # default is no error
if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then
rc1=1 # set return code
fi
return $rc1
}
rc=0 # default is no error
checktermsize 40 90 # check terminal size
rc=$?
if [ $rc -ne 0 ] ; then
echo "Return code: $rc from checktermsize"
fi
exit $rc
在您的系统上以不同大小的终端运行此脚本以检查结果。从代码中可以看出,如果终端比所需的大,那没问题;只是不能太小。
注意
关于终端大小的一点说明:当使用tput
光标移动命令时,请记住是先行后列。然而,大多数现代 GUI 是按列然后行。这是不幸的,因为很容易把它们弄混。
现在让我们看一个完整的交互式脚本:
第五章 - 脚本 5
#!/bin/sh
#
# 5/27/2017
#
echo "script5 - Linux Scripting Book"
# Subroutines
cls()
{
tput clear
}
move() # move cursor to row, col
{
tput cup $1 $2
}
movestr() # move cursor to row, col
{
tput cup $1 $2
echo -n "$3" # display string
}
checktermsize()
{
rc1=0 # default is no error
if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then
rc1=1 # set return code
fi
return $rc1
}
init() # set up the cursor position array
{
srow[0]=2; scol[0]=7 # name
srow[1]=4; scol[1]=12 # address 1
srow[2]=6; scol[2]=12 # address 2
srow[3]=8; scol[3]=7 # city
srow[4]=8; scol[4]=37 # state
srow[5]=8; scol[5]=52 # zip code
srow[6]=10; scol[6]=8 # email
}
drawscreen() # main screen draw routine
{
cls # clear the screen
movestr 0 25 "Chapter 5 - Script 5"
movestr 2 1 "Name:"
movestr 4 1 "Address 1:"
movestr 6 1 "Address 2:"
movestr 8 1 "City:"
movestr 8 30 "State:"
movestr 8 42 "Zip code:"
movestr 10 1 "Email:"
}
getdata()
{
x=0 # array subscript
rc1=0 # loop control variable
while [ $rc1 -eq 0 ]
do
row=${srow[x]}; col=${scol[x]}
move $row $col
read array[x]
let x++
if [ $x -eq $sizeofarray ] ; then
rc1=1
fi
done
return 0
}
showdata()
{
fn=0
echo ""
read -p "Enter filename, or just Enter to skip: " filename
if [ -n "$filename" ] ; then # if not blank
echo "Writing to '$filename'"
fn=1 # a filename was given
fi
echo "" # skip 1 line
echo "Data array contents: "
y=0
while [ $y -lt $sizeofarray ]
do
echo "$y - ${array[$y]}"
if [ $fn -eq 1 ] ; then
echo "$y - ${array[$y]}">>"$filename"
fi
let y++
done
return 0
}
# Code starts here
sizeofarray=7 # number of array elements
if [ "$1" = "--help" ] ; then
echo "Usage: script5 --help"
echo " This script shows how to create an interactive screen program."
exit 255
fi
checktermsize 25 80
rc=$?
if [ $rc -ne 0 ] ; then
echo "Please size the terminal to 25x80 and try again."
exit 1
fi
init # initialize the screen array
drawscreen # draw the screen
getdata # cursor movement and data input routine
showdata # display the data
exit 0
这是一些示例输出:
这里有很多新信息,让我们来看看。首先定义了子例程,你可以看到我们从前面的脚本 4中包含了checktermsize
子例程。
init
例程设置了光标放置数组。将初始值放入子例程是良好的编程实践,特别是如果它将被再次调用。
drawscreen
例程显示初始表单。请注意,我可以在这里使用srow
和scol
数组中的值,但我不想让脚本看起来太乱。
非常仔细地看getdata
例程,因为这是乐趣开始的地方:
-
首先,数组下标
x
和控制变量rc1
被设置为0
。 -
在循环中,光标放置在第一个位置(
Name:
)。 -
查询键盘,用户的输入进入子
x
的数组。 -
x
增加,我们进入下一个字段。 -
如果
x
等于数组的大小,我们离开循环。请记住我们从0
开始计数。
showdata
例程显示数组数据,然后我们就完成了。
提示
请注意,如果使用--help
选项运行脚本,则会显示Usage
消息。
这只是一个交互式脚本的小例子,展示了基本概念。在后面的章节中,我们将更详细地讨论这个问题。
read
命令可以以多种不同的方式使用。以下是一些示例:
read var
Wait for input of characters into the variable var.
read -p "string" var
Display contents of string, stay on the line, and wait for input.
read -p "Enter password:" -s var
Display "Enter password:", but do not echo the typing of the input. Note that a carriage return is not output after Enter is pressed.
read -n 1 var
-n
选项意味着等待那么多个字符,然后继续,它不会等待Enter按键。
在这个例子中,它将等待 1 个字符,然后继续。这在实用脚本和游戏中很有用:
第五章-脚本 6
#!/bin/sh
#
# 5/27/2017
#
echo "Chapter 5 - Script 6"
rc=0 # return code
while [ $rc -eq 0 ]
do
read -p "Enter value or q to quit: " var
echo "var: $var"
if [ "$var" = "q" ] ; then
rc=1
fi
done
rc=0 # return code
while [ $rc -eq 0 ]
do
read -p "Password: " -s var
echo "" # carriage return
echo "var: $var"
if [ "$var" = "q" ] ; then
rc=1
fi
done
echo "Press some keys and q to quit."
rc=0 # return code
while [ $rc -eq 0 ]
do
read -n 1 -s var # wait for 1 char, does not output it
echo $var # output it here
if [ "$var" = "q" ] ; then
rc=1
fi
done
exit $rc
输出:
脚本中的注释应该使这个脚本相当容易理解。read
命令还有一些其他选项,其中一个将在下一个脚本中显示。
通过使用所谓的陷阱,还有另一种查询键盘的方法。这是一个在按下特殊键序列时访问的子例程,比如Ctrl + C。
这是使用陷阱的一个例子:
第五章-脚本 7
#!/bin/sh
#
# 5/16/2017
#
echo "script7 - Linux Scripting Book"
trap catchCtrlC INT # Initialize the trap
# Subroutines
catchCtrlC()
{
echo "Entering catchCtrlC routine."
}
# Code starts here
echo "Press Ctrl-C to trigger the trap, 'Q' to exit."
loop=0
while [ $loop -eq 0 ]
do
read -t 1 -n 1 str # wait 1 sec for input or for 1 char
rc=$?
if [ $rc -gt 128 ] ; then
echo "Timeout exceeded."
fi
if [ "$str" = "Q" ] ; then
echo "Exiting the script."
loop=1
fi
done
exit 0
这是我系统上的输出:
在你的系统上运行这个脚本。按一些键,看看反应。也按几次Ctrl + C。完成后按Q。
那个read
语句需要进一步解释。使用带有-t
选项(超时)的read
意味着等待那么多秒钟的字符。如果在规定的时间内没有输入字符,它将返回一个值大于 128 的代码。正如我们之前看到的,-n 1
选项告诉read
等待 1 个字符。这意味着我们等待 1 秒钟来输入 1 个字符。这是read
可以用来创建游戏或其他交互式脚本的另一种方式。
注意
使用陷阱是捕捉意外按下Ctrl + C的好方法,这可能会导致数据丢失。然而,需要注意的是,如果你决定捕捉Ctrl + C,请确保你的脚本有其他退出方式。在上面的简单脚本中,用户必须输入“Q”才能退出。
如果你陷入无法退出脚本的情况,可以使用kill
命令。
例如,如果我需要停止script7
,指示如下:
guest1 $ ps auxw | grep script7
guest1 17813 0.0 0.0 106112 1252 pts/32 S+ 17:23 0:00 /bin/sh ./script7
guest1 17900 0.0 0.0 103316 864 pts/18 S+ 17:23 0:00 grep script7
guest1 29880 0.0 0.0 10752 1148 pts/17 S+ 16:47 0:00 kw script7
guest1 $ kill -9 17813
guest1 $
在运行script7
的终端上,你会看到它停在那里,并显示Killed
。
请注意,一定要终止正确的进程!
在上面的例子中,PID29880
是我正在写script7
的文本编辑器会话。杀死它不是一个好主意:)。
现在来点乐趣!下一个脚本允许你在屏幕上画粗糙的图片:
第五章-脚本 8
#!/bin/sh
#
# 5/16/2017
#
echo "script8 - Linux Scripting Book"
# Subroutines
cls()
{
tput clear
}
move() # move cursor to row, col
{
tput cup $1 $2
}
movestr() # move cursor to row, col
{
tput cup $1 $2
echo -n "$3" # display string
}
init() # set initial values
{
minrow=1 # terminal boundaries
maxrow=24
mincol=0
maxcol=79
startrow=1
startcol=0
}
restart() # clears screen, sets initial cursor position
{
cls
movestr 0 0 "Arrow keys move cursor. 'x' to draw, 'd' to erase, '+' to restart, 'Q' to quit."
row=$startrow
col=$startcol
draw=0 # default is not drawing
drawchar=""
}
checktermsize2() # must be the specified size
{
rc1=0 # default is no error
if [[ $LINES -ne $1 || $COLUMNS -ne $2 ]] ; then
rc1=1 # set return code
fi
return $rc1
}
# Code starts here
if [ "$1" = "--help" ] ; then
echo "Usage: script7 --help"
echo " This script shows the basics on how to create a game."
echo " Use the arrow keys to move the cursor."
echo " Press c to restart and Q to quit."
exit 255
fi
checktermsize2 25 80 # terminal must be this size
rc=$?
if [ $rc -ne 0 ] ; then
echo "Please size the terminal to 25x80 and try again."
exit 1
fi
init # initialize values
restart # set starting cursor pos and clear screen
loop=1
while [ $loop -eq 1 ]
do
move $row $col # position the cursor here
read -n 1 -s ch
case "$ch" in
A) if [ $row -gt $minrow ] ; then
let row--
fi
;;
B) if [ $row -lt $maxrow ] ; then
let row++
fi
;;
C) if [ $col -lt $maxcol ] ; then
let col++
fi
;;
D) if [ $col -gt $mincol ] ; then
let col--
fi
;;
d) echo -n "" # delete char
;;
x) if [ $col -lt $maxcol ] ; then
echo -n "X" # put char
let col++
fi
;;
+) restart ;;
Q) loop=0 ;;
esac
done
movestr 24 0 "Script completed normally."
echo "" # carriage return
exit 0
写这个脚本很有趣,比我预期的更有趣一些。
我们还没有涉及的一件事是case
语句。这类似于if...then...else
,但使代码更易读。基本上,检查输入到read
语句的值是否与每个case
子句中的匹配。如果匹配,那个部分就会被执行,然后控制转到esac
语句后的行。如果没有匹配,它也会这样做。
尝试这个脚本,并记住将终端设置为 25x80(或者如果你的 GUI 是这样工作的,80x25)。
这只是这个脚本可以做的一个例子:
好吧,我想这表明我不是一个很好的艺术家。我会继续从事编程和写书。
总结
在本章中,我们展示了如何使用read
内置命令来查询键盘。我们解释了一些不同的读取选项,并介绍了陷阱的使用。还包括了一个简单的绘图游戏。
下一章将展示如何自动运行脚本,使其可以无人值守地运行。我们将解释如何使用cron
在特定时间运行脚本。还将介绍归档程序zip
和tar
,因为它们在创建自动化备份脚本时非常有用。
第六章:使用脚本自动化任务
本章介绍了如何使用脚本自动化各种任务。
本章涵盖的主题如下:
-
如何创建一个自动化任务的脚本。
-
使用 cron 在特定时间自动运行脚本的正确方法。
-
如何使用
ZIP
和TAR
进行压缩备份。 -
源代码示例。
读者将学习如何创建自动化脚本。
我们在第三章使用循环和 sleep 命令中谈到了sleep
命令。只要遵循一些准则,它可以用来创建一个自动化脚本(即在特定时间运行而无需用户干预),。
这个非常简单的脚本将强化我们在第三章使用循环和 sleep 命令中所讨论的关于使用sleep
命令进行自动化的内容:
第六章 - 脚本 1
#!/bin/sh
#
# 5/23/2017
#
echo "script1 - Linux Scripting Book"
while [ true ]
do
date
sleep 1d
done
echo "End of script1"
exit 0
如果你在你的系统上运行它并等几天,你会发现日期会有所偏移。这是因为sleep
命令在脚本中插入了延迟,这并不意味着它会每天在同一时间运行脚本。
注意
以下脚本更详细地展示了这个问题。请注意,这是一个不应该做的例子。
第六章 - 脚本 2
#!/bin/sh
#
# 5/23/2017
#
echo "script2 - Linux Scripting Book"
while [ true ]
do
# Run at 3 am
date | grep -q 03:00:
rc=$?
if [ $rc -eq 0 ] ; then
echo "Run commands here."
date
fi
sleep 60 # sleep 60 seconds
done
echo "End of script2"
exit 0
你会注意到的第一件事是,这个脚本会一直运行,直到它被手动终止,或者使用kill
命令终止(或者机器因为任何原因而关闭)。自动化脚本通常会一直运行。
date
命令在没有任何参数的情况下返回类似这样的东西:
guest1 $ date
Fri May 19 15:11:54 HST 2017
现在我们只需要使用grep
来匹配那个时间。不幸的是,这里有一个非常微妙的问题。已经验证可能会偶尔漏掉。例如,如果时间刚刚变成凌晨 3 点,程序现在在休眠中,当它醒来时可能已经是 3:01 了。在我早期的计算机工作中,我经常看到这样的代码,从来没有想过。当有一天重要的备份被错过时,我的团队被要求找出问题所在,我们发现了这个问题。一个快速的解决方法是将秒数改为 59,但更好的方法是使用 cron,这将在本章后面展示。
注意grep
的-q
选项,这只是告诉它抑制任何输出。如果你愿意,可以在编写脚本时去掉这个选项。还要注意,grep
在找到匹配时返回0
,否则返回非零值。
说了这么多,让我们来看一些简单的自动化脚本。我从 1996 年开始在我的 Linux 系统上运行以下脚本:
第六章 - 脚本 3
#!/bin/sh
#
# 5/23/2017
#
echo "script3 - Linux Scripting Book"
FN=/tmp/log1.txt # log file
while [ true ]
do
echo Pinging $PROVIDER
ping -c 1 $PROVIDER
rc=$?
if [ $rc -ne 0 ] ; then
echo Cannot ping $PROVIDER
date >> $FN
echo Cannot ping $PROVIDER >> $FN
fi
sleep 60
done
echo "End of script3" # 60 seconds
exit 0
以及在我的系统上的输出:
我只运行了三次,但它可以一直运行。在你的系统上运行之前,让我们谈谈PROVIDER
环境变量。我的系统上有几个处理互联网的脚本,我发现自己不断地更改提供者。很快我意识到这是一个很好的时机来使用一个环境变量,因此是PROVIDER
。
这是在我的/root/.bashrc
和/home/guest1/.bashrc
文件中的:
export PROVIDER=twc.com
根据需要替换你自己的。还要注意,当发生故障时,它会被写入屏幕和文件中。由于使用了>>
追加操作符,文件可能最终会变得相当大,所以如果你的连接不太稳定,要做好相应的计划。
提示
小心,不要在短时间内多次 ping 或以其他方式访问公司网站。这可能会被检测到,你的访问可能会被拒绝。
以下是一个脚本,用于检测用户何时登录或退出系统:
第六章 - 脚本 4
#!/bin/sh
#
# 5/23/2017
#
echo "Chapter 6 - Script 4"
numusers=`who | wc -l`
while [ true ]
do
currusers=`who | wc -l` # get current number of users
if [ $currusers -gt $numusers ] ; then
echo "Someone new has logged on!!!!!!!!!!!"
date
who
# beep
numusers=$currusers
elif [ $currusers -lt $numusers ] ; then
echo "Someone logged off."
date
numusers=$currusers
fi
sleep 1 # sleep 1 second
done
以下是输出(根据长度调整):
这个脚本检查 who
命令的输出,看看自上次运行以来是否有变化。如果有变化,它会采取适当的行动。如果你的系统上有 beep
命令或等效命令,这是一个很好的使用场景。
看一下这个陈述:
currusers=`who | wc -l` # get current number of users
这需要一些澄清,因为我们还没有涵盖它。那些反引号字符表示在其中运行命令,并将结果放入变量中。在这种情况下,who
命令被管道传递到 wc -l
命令中以计算行数。然后将这个值放入 currusers
变量中。如果这听起来有点复杂,不用担心,下一章将更详细地介绍。
脚本的其余部分应该已经很清楚了,因为我们之前已经涵盖过这部分。如果你决定在你的系统上运行类似的东西,只需记住,它将在每次打开新终端时触发。
Cron
好了,现在来玩点真正的东西。即使你只是短时间使用 Linux,你可能已经意识到了 cron。这是一个守护进程,或者说是后台进程,它在特定的时间执行命令。
Cron 每分钟读取一个名为 crontab
的文件,以确定是否需要运行命令。
在本章的示例中,我们将只关注访客账户的 crontab
(而不是 root 的)。
使用我的 guest1
账户,第一次运行时会是这个样子。在你的系统上以访客账户跟着做可能是个好主意:
guest1 $ crontab -l
no crontab for guest1
guest1 $
这是有道理的,因为我们还没有为 guest1
创建 crontab
文件。它不是用来直接编辑的,所以使用 crontab -e
命令。
现在在你的系统上以访客账户运行 crontab -e
。
这是我在使用 vi 时在我的系统上的样子的屏幕截图:
正如你所看到的,crontab
命令创建了一个临时文件。不幸的是,这个文件是空的,因为他们应该提供一个模板。现在让我们添加一个。将以下文本复制并粘贴到文件中:
# this is the crontab file for guest1
# min hour day of month month day of week command
# 0-59 0-23 1-31 1-12 0-6
# Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6
将 guest1
替换为你的用户名。现在我们知道了应该放在哪里。
在这个文件中添加以下行:
* * * * * date > /dev/pts/31
*
表示匹配字段中的所有内容。因此,这行实际上每分钟触发一次。
我们使用重定向运算符将 echo
命令的输出写入另一个终端。根据需要替换你自己的。
在你的系统上尝试上述操作。记住,你必须先保存文件,然后你应该看到这个输出:
guest1 $ crontab -e
crontab: installing new crontab
guest1 $
这意味着添加成功了。现在等待下一分钟到来。你应该在另一个终端看到当前日期显示出来。
现在我们可以看到 cron 的基础知识。以下是一些快速提示:
0 0 * * * command # run every day at midnight
0 3 * * * command # run every day at 3 am
30 9 1 * * command # run at 9:30 am on the first of the month
45 14 * * 0 command # run at 2:45 pm on Sundays
0 0 25 12 * command # run at midnight on my birthday
这只是 cron 中日期和时间设置的一个非常小的子集。要了解更多信息,请参考 cron 和 crontab
的 man
页面。
需要提到的一件事是用户的 cron 的 PATH
。它不会源自用户的 .bashrc
文件。你可以通过添加以下行来验证这一点:
* * * * * echo $PATH > /dev/pts/31 # check the PATH
在我的 CentOS 6.8 系统上显示为:
/usr/bin:/bin
为了解决这个问题,你可以源自你的 .bashrc
文件:
* * * * * source $HOME/.bashrc; echo $PATH > /dev/pts/31 # check the PATH
现在应该显示真实路径。EDITOR
环境变量在第二章中提到,变量处理。如果你想让 crontab
使用不同的文本编辑器,你可以将 EDITOR
设置为你想要的路径/名称。
例如,在我的系统上,我有这个:
export EDITOR=/home/guest1/bin/kw
当我运行 crontab -e
时,我得到这个:
还有一件事需要提到的是,如果在使用 crontab
时出现错误,有些情况下它会在你尝试保存文件时告诉你。但它无法检查所有内容,所以要小心。此外,如果一个命令出现错误,crontab
将使用邮件系统通知用户。因此,记住这一点,当使用 cron 时,你可能需要不时地运行 mail
命令。
现在我们已经了解了基础知识,让我们创建一个使用zip
命令的备份脚本。如果你不熟悉zip
,不用担心,这会让你迅速掌握。在 Linux 系统上,大多数人只使用tar
命令,然而,如果你知道zip
的工作原理,你可以更容易地与 Windows 用户共享文件。
在一个访客账户的目录下,在你的系统上运行这些命令。像往常一样,我使用了/home/guest1/LinuxScriptingBook
:
创建一个work
目录:
guest1 ~/LinuxScriptingBook $ mkdir work
切换到它:
guest1 ~/LinuxScriptingBook $ cd work
创建一些临时文件,和/或将一些现有文件复制到这个目录:
guest1 ~/LinuxScriptingBook/work $ route > route.txt
guest1 ~/LinuxScriptingBook/work $ ifconfig > ifconfig.txt
guest1 ~/LinuxScriptingBook/work $ ls -la /usr > usr.txt
guest1 ~/LinuxScriptingBook/work $ cp /etc/motd .
获取一个列表:
guest1 ~/LinuxScriptingBook/work $ ls -la
total 24
drwxrwxr-x 2 guest1 guest1 4096 May 23 09:44 .
drwxr-xr-x 8 guest1 guest1 4096 May 22 15:18 ..
-rw-rw-r-- 1 guest1 guest1 1732 May 23 09:44 ifconfig.txt
-rw-r--r-- 1 guest1 guest1 1227 May 23 09:44 motd
-rw-rw-r-- 1 guest1 guest1 335 May 23 09:44 route.txt
-rw-rw-r-- 1 guest1 guest1 724 May 23 09:44 usr.txt
把它们压缩起来:
guest1 ~/LinuxScriptingBook/work $ zip work1.zip *
adding: ifconfig.txt (deflated 69%)
adding: motd (deflated 49%)
adding: route.txt (deflated 52%)
adding: usr.txt (deflated 66%)
再获取一个列表:
guest1 ~/LinuxScriptingBook/work $ ls -la
total 28
drwxrwxr-x 2 guest1 guest1 4096 May 23 09:45 .
drwxr-xr-x 8 guest1 guest1 4096 May 22 15:18 ..
-rw-rw-r-- 1 guest1 guest1 1732 May 23 09:44 ifconfig.txt
-rw-r--r-- 1 guest1 guest1 1227 May 23 09:44 motd
-rw-rw-r-- 1 guest1 guest1 335 May 23 09:44 route.txt
-rw-rw-r-- 1 guest1 guest1 724 May 23 09:44 usr.txt
-rw-rw-r-- 1 guest1 guest1 2172 May 23 09:45 work1.zip
现在在那个目录中有一个名为work1.zip
的文件。创建zip
文件的语法是:
zip [optional parameters] filename.zip list-of-files-to-include
要解压缩它:
unzip filename.zip
要查看(或列出)zip
文件的内容而不解压缩它:
unzip -l filename.zip
这也是确保.zip
文件正确创建的好方法,因为如果无法读取文件,解压缩会报错。请注意,zip
命令不仅创建了一个.zip
文件,还压缩了数据。这样可以生成更小的备份文件。
这是一个使用zip
备份一些文件的简短脚本:
第六章 - 脚本 5
#!/bin/sh
#
# 5/23/2017
#
echo "script5 - Linux Scripting Book"
FN=work1.zip
cd /tmp
mkdir work 2> /dev/null # suppress message if directory already exists
cd work
cp /etc/motd .
cp /etc/issue .
ls -la /tmp > tmp.txt
ls -la /usr > usr.txt
rm $FN 2> /dev/null # remove any previous file
zip $FN *
echo File "$FN" created.
# cp to an external drive, and/or scp to another computer
echo "End of script5"
exit 0
在我的系统上的输出:
这是一个非常简单的脚本,但它展示了使用zip
命令备份一些文件的基础知识。
假设我们想每天在午夜运行这个命令。假设script5
位于/tmp
下,crontab
的条目将如下:
guest1 /tmp/work $ crontab -l
# this is the crontab file for guest1
# min hour day of month month day of week command
# 0-59 0-23 1-31 1-12 0-6 Sun=0
# Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6
0 0 * * * /tmp/script5
在这种情况下,我们不需要源/home/guest1/.bashrc
文件。还要注意,任何错误都会发送到用户的邮件账户。zip
命令不仅可以做到这一点,例如它可以递归到目录中。要了解更多信息,请参考 man 手册。
现在让我们谈谈 Linux 的tar
命令。它比zip
命令更常用,更擅长获取所有文件,甚至是隐藏的文件。回到/tmp/work
目录,这是你如何使用tar
来备份它的。假设文件仍然存在于上一个脚本中:
guest1 /tmp $ tar cvzf work1.gz work/
work/
work/motd
work/tmp.txt
work/issue
work/work1.zip
work/usr.txt
guest1 /tmp $
现在在/tmp
目录下有一个名为work1.gz
的文件。它是/tmp/work
目录下所有文件的压缩存档,包括我们之前创建的.zip
文件。
tar 的语法一开始可能有点晦涩,但你会习惯的。tar 中可用的一些功能包括:
参数 | 特性 |
---|---|
c |
创建一个归档 |
x |
提取一个归档 |
v |
使用详细选项 |
z |
使用 gunzip 风格的压缩(.gz) |
f |
要创建/提取的文件名 |
请注意,如果不包括z
选项,文件将不会被压缩。按照惯例,文件扩展名将只是 tar。请注意,用户控制文件的实际名称,而不是tar
命令。
好了,现在我们有一个压缩的tar-gz 文件
(或存档)。这是如何解压缩和提取文件的方法。我们将在/home/guest1
下进行操作:
guest1 /home/guest1 $ tar xvzf /tmp/work1.gz
work/
work/motd
work/tmp.txt
work/issue
work/work1.zip
work/usr.txt
guest1 /home/guest1 $
使用 tar 备份系统真的很方便。这也是配置新机器使用你的个人文件的好方法。例如,我经常备份主系统上的以下目录:
/home/guest1
/lewis
/temp
/root
这些文件然后自动复制到外部 USB 驱动器。请记住,tar 会自动递归到目录中,并获取每个文件,包括隐藏的文件。Tar 还有许多其他选项,可以控制如何创建存档。最常见的选项之一是排除某些目录。
例如,当备份/home/guest1
时,真的没有理由包括.cache
、Cache
、.thumbnails
等目录。
排除目录的选项是--exclude=<目录名>
,在下一个脚本中显示。
以下是我在主要 Linux 系统上使用的备份程序。这是两个脚本,一个用于安排备份,另一个用于实际执行工作。我主要是这样做的,以便我可以对实际备份脚本进行更改而不关闭调度程序脚本。需要设置的第一件事是crontab
条目。这是我系统上的样子:
guest1 $ crontab -l
# this is the crontab file for guest1
# min hour day of month month day of week command
# 0-59 0-23 1-31 1-12 0-6 Sun=0
# Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6
TTY=/dev/pts/31
0 3 * * * touch /tmp/runbackup-cron.txt
这将在每天凌晨 3 点左右创建文件/tmp/backup-cron.txt
。
请注意,以下脚本必须以 root 身份运行:
第六章-脚本 6
#!/bin/sh
#
# runbackup1 - this version watches for file from crontab
#
# 6/3/2017 - mainlogs now under /data/mainlogs
#
VER="runbackup1 6/4/2017 A"
FN=/tmp/runbackup-cron.txt
DR=/wd1 # symbolic link to external drive
tput clear
echo $VER
# Insure backup drive is mounted
file $DR | grep broken
rc=$?
if [ $rc -eq 0 ] ; then
echo "ERROR: USB drive $DR is not mounted!!!!!!!!!!!!!!"
beep
exit 255
fi
cd $LDIR/backup
while [ true ]
do
# crontab creates the file at 3 am
if [ -f $FN ] ; then
rm $FN
echo Running backup1 ...
backup1 | tee /data/mainlogs/mainlog`date '+%Y%m%d'`.txt
echo $VER
fi
sleep 60 # check every minute
done
这里有很多信息,所以我们将逐行进行解释:
-
脚本首先设置变量,清除屏幕,并显示脚本的名称。
-
DR
变量分配给我的 USB 外部驱动器(wd1
),它是一个符号链接。 -
然后使用
file
命令执行检查,以确保/wd1
已挂载。如果没有,file
命令将返回损坏的符号链接,grep
将触发此操作,脚本将中止。 -
如果驱动器已挂载,则进入循环。每分钟检查文件的存在以查看是否是开始备份的时间。
-
找到文件后,将运行
backup1
脚本(见下文)。它的输出将使用tee
命令发送到屏幕和文件。 -
日期格式说明符
'+%Y%m%d'
以 YYYYMMDD 格式显示日期
我不时检查/data/mainlogs
目录中的文件,以确保我的备份正确创建且没有错误。
以下脚本用于备份我的系统。这里的逻辑是当前的每日备份存储在$TDIR
目录中的硬盘上。它们也被复制到外部驱动器上的编号目录中。这些目录从 1 到 7 编号。当达到最后一个时,它会重新从 1 开始。这样,外部驱动器上始终有 7 天的备份可用。
此脚本也必须以 root 身份运行:
第六章-脚本 7
#!/bin/sh
# Jim's backup program
# Runs standalone
# Copies to /data/backups first, then to USB backup drive
VER="File backup by Jim Lewis 5/27/2017 A"
TDIR=/data/backups
RUNDIR=$LDIR/backup
DR=/wd1
echo $VER
cd $RUNDIR
# Insure backup drive is mounted
file $DR | grep broken
a=$?
if [ "$a" != "1" ] ; then
echo "ERROR: USB drive $DR is not mounted!!!!!!!!!!!!!!"
beep
exit 255
fi
date >> datelog.txt
date
echo "Removing files from $TDIR"
cd "$TDIR"
rc=$?
if [ $rc -ne 0 ] ; then
echo "backup1: Error cannot change to $TDIR!"
exit 250
fi
rm *.gz
echo "Backing up files to $TDIR"
X=`date '+%Y%m%d'`
cd /
tar cvzf "$TDIR/lewis$X.gz" lewis
tar cvzf "$TDIR/temp$X.gz" temp
tar cvzf "$TDIR/root$X.gz" root
cd /home
tar cvzf "$TDIR/guest$X.gz" --exclude=Cache --exclude=.cache --exclude=.evolution --exclude=vmware --exclude=.thumbnails --exclude=.gconf --exclude=.kde --exclude=.adobe --exclude=.mozilla --exclude=.gconf --exclude=thunderbird --exclude=.local --exclude=.macromedia --exclude=.config guest1
cd $RUNDIR
T=`cat filenum1`
BACKDIR=$DR/backups/$T
rm $BACKDIR/*.gz
cd "$TDIR"
cp *.gz $BACKDIR
echo $VER
cd $BACKDIR
pwd
ls -lah
cd $RUNDIR
let T++
if [ $T -gt 7 ] ; then
T=1
fi
echo $T > filenum1
这比以前的脚本要复杂一些,所以让我们逐行进行解释:
-
RUNDIR
变量保存脚本的起始目录。 -
DR
变量指向外部备份驱动器。 -
检查驱动器以确保它已挂载。
-
当前日期被附加到
datelog.txt
文件。 -
TDIR
变量是备份的目标目录。 -
执行
cd
到该目录并检查返回代码。出现错误时,脚本将以250
退出。 -
删除前一天的备份。
现在它返回到/
目录执行 tar 备份。
请注意,guest1
目录中排除了几个目录。
-
cd $RUNDIR
将其放回到起始目录。 -
T=
filenum1``从该文件获取值并将其放入T
变量中。这是用于在外部驱动器上下一个目录的计数器。 -
BACKDIR
设置为旧备份,然后它们被删除。 -
控制再次返回到起始目录,并将当前备份复制到外部驱动器上的适当目录。
-
程序的版本再次显示,以便在杂乱的屏幕上轻松找到。
-
控制转到备份目录,
pwd
显示名称,然后显示目录的内容。 -
T
变量递增 1。如果大于 7,则设置回 1。
最后,更新后的T
变量被写回filenum1
文件。
这个脚本应该作为您想要开发的任何备份过程的良好起点。请注意,scp
命令可用于在没有用户干预的情况下直接将文件复制到另一台计算机。这将在第十章中介绍,脚本最佳实践。
总结
我们描述了如何创建一个脚本来自动化一个任务。我们讨论了如何使用 cron 在特定时间自动运行脚本的正确方法。我们讨论了存档命令zip
和tar
,以展示如何执行压缩备份。我们还包括并讨论了完整的调度程序和备份脚本。
在下一章中,我们将展示如何在脚本中读写文件。
第七章:文件操作
本章将展示如何从文本文件中读取和写入。它还将涵盖文件加密和校验和。
本章涵盖的主题如下:
-
展示如何使用重定向操作符写出文件
-
展示如何读取文件
-
解释如何捕获命令的输出并在脚本中使用
-
查看
cat
和其他重要命令 -
涵盖文件加密和校验和程序,如 sum 和 OpenSSL
写文件
我们在之前的一些章节中展示了如何使用重定向操作符创建和写入文件。简而言之,此命令将创建文件ifconfig.txt
(或覆盖文件,如果文件已经存在):
ifconfig > ifconfig.txt
以下命令将追加到任何先前的文件,如果文件不存在,则创建一个新文件:
ifconfig >> ifconfig.txt
之前的一些脚本使用反引号操作符从文件中检索数据。让我们通过查看脚本 1来回顾一下:
第七章-脚本 1
#!/bin/sh
#
# 6/1/2017
#
echo "Chapter 7 - Script 1"
FN=file1.txt
rm $FN 2> /dev/null # remove it silently if it exists
x=1
while [ $x -le 10 ] # 10 lines
do
echo "x: $x"
echo "Line $x" >> $FN # append to file
let x++
done
echo "End of script1"
exit 0
这是一个截图:
这很简单。如果文件存在,它会将文件(静默地)删除,然后输出每一行到文件,每次增加x
。当x
达到10
时,循环终止。
读取文件
现在让我们再次看看上一章中备份脚本用于从文件中获取值的方法:
第七章-脚本 2
#!/bin/sh
#
# 6/2/2017
#
echo "Chapter 7 - Script 2"
FN=filenum1.txt # input/output filename
MAXFILES=5 # maximum number before going back to 1
if [ ! -f $FN ] ; then
echo 1 > $FN # create the file if it does not exist
fi
echo -n "Contents of $FN: "
cat $FN # display the contents
count=`cat $FN` # put the output of cat into variable count
echo "Initial value of count from $FN: $count"
let count++
if [ $count -gt $MAXFILES ] ; then
count=1
fi
echo "New value of count: $count"
echo $count > $FN
echo -n "New contents of $FN: "
cat $FN
echo "End of script2"
exit 0
这是脚本 2的截图:
我们首先将FN
变量设置为文件名(filenum1.txt
)。它由cat
命令显示,然后文件的内容被分配给count
变量。它被显示,然后增加 1。新值被写回文件,然后再次显示。至少运行 6 次以查看其如何循环。
这只是创建和读取文件的一种简单方法。现在让我们看一个从文件中读取多行的脚本。它将使用前面脚本 1创建的文件file1.txt
。
第七章-脚本 3
#!/bin/sh
#
# 6/1/2017
#
echo "Chapter 7 - Script 3"
FN=file1.txt # filename
while IFS= read -r linevar # use read to put line into linevar
do
echo "$linevar" # display contents of linevar
done < $FN # the file to use as input
echo "End of script3"
exit 0
以下是输出:
这里的结构可能看起来有点奇怪,因为它与我们以前看到的非常不同。此脚本使用read
命令获取文件的每一行。在语句中:
while IFS= read -r linevar
IFS=
(内部字段分隔符)防止read
修剪前导和尾随的空白字符。-r
参数使read
忽略反斜杠转义序列。下一行使用重定向操作符,将file1.txt
作为read
的输入。
done < $FN
这里有很多新材料,所以仔细查看,直到你对它感到舒适为止。
上面的脚本有一个小缺陷。如果文件不存在,将会出现错误。看看下面的截图:
Shell 脚本是解释性的,这意味着系统会逐行检查并运行。这与用 C 语言编写的程序不同,后者是经过编译的。这意味着任何语法错误都会在编译阶段出现,而不是在运行程序时出现。我们将在第九章“调试脚本”中讨论如何避免大多数 shell 脚本语法错误。
这是脚本 4,解决了缺少文件的问题:
第七章-脚本 4
#!/bin/sh
#
# 6/1/2017
#
echo "Chapter 7 - Script 4"
FN=file1.txt # filename
if [ ! -f $FN ] ; then
echo "File $FN does not exist."
exit 100
fi
while IFS= read -r linevar # use read to put line into linevar
do
echo "$linevar" # display contents of linevar
done < $FN # the file to use as input
echo "End of script4"
exit 0
以下是输出:
在使用文件时请记住这一点,并始终检查文件是否存在,然后再尝试读取它。
读写文件
下一个脚本读取一个文本文件并创建其副本:
第七章-脚本 5
#!/bin/sh
#
# 6/1/2017
#
echo "Chapter 7 - Script 5"
if [ $# -ne 2 ] ; then
echo "Usage: script5 infile outfile"
echo " Copies text file infile to outfile."
exit 255
fi
INFILE=$1
OUTFILE=$2
if [ ! -f $INFILE ] ; then
echo "Error: File $INFILE does not exist."
exit 100
fi
if [ $INFILE = $OUTFILE ] ; then
echo "Error: Cannot copy to same file."
exit 101
fi
rm $OUTFILE 2> /dev/null # remove it
echo "Reading file $INFILE ..."
x=0
while IFS= read -r linevar # use read to put line into linevar
do
echo "$linevar" >> $OUTFILE # append to file
let x++
done < $INFILE # the file to use as input
echo "$x lines read."
diff $INFILE $OUTFILE # use diff to check the output
rc=$?
if [ $rc -ne 0 ] ; then
echo "Error, files do not match."
exit 103
else
echo "File $OUTFILE created."
fi
sum $INFILE $OUTFILE # show the checksums
echo "End of script5"
exit $rc
这是脚本 5的截图:
这展示了如何在脚本中读写文本文件。以下解释了每一行:
-
脚本开始时检查是否给出了两个参数,如果没有,则显示“用法”消息。
-
然后检查输入文件是否存在,如果不存在,则以代码
100
退出。 -
检查以确保用户没有尝试复制到相同的文件,因为在第 34 行可能会发生语法错误。这段代码确保不会发生这种情况。
-
如果输出文件存在,则删除它。这是因为我们想要复制到一个新文件,而不是追加到现有文件。
-
while
循环读取和写入行。对x
中行数进行计数。 -
循环结束时输出行数。
-
作为一个健全性检查,使用
diff
命令来确保文件是相同的。 -
并且作为额外的检查,对这两个文件运行
sum
命令。
交互式地读写文件
这个脚本与第五章中的一个类似,创建交互式脚本。它读取指定的文件,显示一个表单,并允许用户编辑然后保存它:
第七章-脚本 6
#!/bin/sh
# 6/2/2017
# Chapter 7 - Script 6
trap catchCtrlC INT # Initialize the trap
# Subroutines
catchCtrlC()
{
move 13 0
savefile
movestr 23 0 "Script terminated by user."
echo "" # carriage return
exit 0
}
cls()
{
tput clear
}
move() # move cursor to row, col
{
tput cup $1 $2
}
movestr() # move cursor to row, col
{
tput cup $1 $2
echo -n "$3" # display string
}
checktermsize()
{
rc1=0 # default is no error
if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then
rc1=1 # set return code
fi
return $rc1
}
init() # set up the cursor position array
{
srow[0]=2; scol[0]=7 # name
srow[1]=4; scol[1]=12 # address 1
srow[2]=6; scol[2]=12 # address 2
srow[3]=8; scol[3]=7 # city
srow[4]=8; scol[4]=37 # state
srow[5]=8; scol[5]=52 # zip code
srow[6]=10; scol[6]=8 # email
}
drawscreen() # main screen draw routine
{
cls # clear the screen
movestr 0 25 "Chapter 7 - Script 6"
movestr 2 1 "Name: ${array[0]}"
movestr 4 1 "Address 1: ${array[1]}"
movestr 6 1 "Address 2: ${array[2]}"
movestr 8 1 "City: ${array[3]}"
movestr 8 30 "State: ${array[4]}"
movestr 8 42 "Zip code: ${array[5]}"
movestr 10 1 "Email: ${array[6]}"
}
getdata()
{
x=0 # start at the first field
while [ true ]
do
row=${srow[x]}; col=${scol[x]}
move $row $col
read var
if [ -n "$var" ] ; then # if not blank assign to array
array[$x]=$var
fi
let x++
if [ $x -eq $sizeofarray ] ; then
x=0 # go back to first field
fi
done
return 0
}
savefile()
{
rm $FN 2> /dev/null # remove any existing file
echo "Writing file $FN ..."
y=0
while [ $y -lt $sizeofarray ]
do
echo "$y - '${array[$y]}'" # display to screen
echo "${array[$y]}" >> "$FN" # write to file
let y++
done
echo "File written."
return 0
}
getfile()
{
x=0
if [ -n "$FN" ] ; then # check that file exists
while IFS= read -r linevar # use read to put line into linevar
do
array[$x]="$linevar"
let x++
done < $FN # the file to use as input
fi
return 0
}
# Code starts here
if [ $# -ne 1 ] ; then
echo "Usage: script6 file"
echo " Reads existing file or creates a new file"
echo " and allows user to enter data into fields."
echo " Press Ctrl-C to end."
exit 255
fi
FN=$1 # filename (input and output)
sizeofarray=7 # number of array elements
checktermsize 25 80
rc=$?
if [ $rc -ne 0 ] ; then
echo "Please size the terminal to 25x80 and try again."
exit 1
fi
init # initialize the screen array
getfile # read in file if it exists
drawscreen # draw the screen
getdata # read in the data and put into the fields
exit 0
在我的系统上是这样的:
这是代码的描述:
-
在这个脚本中设置的第一件事是一个Ctrl + C的陷阱,它会导致文件被保存并且脚本结束。
-
定义子例程。
-
使用
getdata
例程读取用户输入。 -
savefile
例程写出数据数组。 -
getfile
例程将文件(如果存在)读入数组。 -
检查参数,因为需要一个文件名。
-
将
FN
变量设置为文件的名称。 -
在使用数组时,最好有一个固定的大小,即
sizeofarray
。 -
检查终端的大小,确保它是 25x80(或 80x25,取决于你的 GUI)。
-
调用
init
例程设置屏幕数组。 -
调用
getfile
和drawscreen
例程。 -
getdata
例程用于移动光标并将字段中的数据放入正确的数组位置。 -
Ctrl + C用于保存文件并终止脚本。
这是一个简单的 Bash 屏幕输入/输出例程的示例。这个脚本可能需要一些改进,以下是部分列表:
-
检查现有文件是否有特定的头。这可以帮助确保文件格式正确,避免语法错误。
-
检查输入文件,确保它是文本而不是二进制。提示:使用
file
和grep
命令。 -
如果文件无法正确写出,请确保优雅地捕获错误。
文件校验和
你可能注意到了上面使用了sum
命令。它显示文件的校验和和块计数,可用于确定两个或更多个文件是否是相同的文件(即具有完全相同的内容)。
这是一个真实世界的例子:
假设你正在写一本书,文件正在从作者发送到出版商进行审阅。出版商进行了一些修订,然后将修订后的文件发送回作者。有时很容易出现不同步的情况,并收到一个看起来没有任何不同的文件。如果对这两个文件运行sum
命令,你可以轻松地确定它们是否相同。
看一下下面的截图:
第一列是校验和,第二列是块计数。如果这两者都相同,那意味着文件的内容是相同的。所以,在这个例子中,bookfiles 1、2 和 4 是相同的。Bookfiles 3 和 5 也是相同的。然而,bookfiles 6、7 和 8 与任何文件都不匹配,最后两个甚至没有相同的块计数。
提示
注意:sum
命令只查看文件的内容和块计数。它不查看文件名或其他文件属性,如所有权或权限。要做到这一点,你可以使用ls
和stat
命令。
文件加密
有时候你可能想要加密系统中一些重要和/或机密的文件。有些人把他们的密码存储在计算机的文件中,这可能没问题,但前提是要使用某种类型的文件加密。有许多加密程序可用,在这里我们将展示 OpenSSL。
OpenSSL 命令行工具非常流行,很可能已经安装在您的计算机上(它默认安装在我的 CentOS 6.8 系统上)。它有几个选项和加密方法,但我们只会涵盖基础知识。
再次使用上面的file1.txt
在您的系统上尝试以下操作:
我们首先对file1.txt
文件执行求和,然后运行openssl
。以下是语法:
-
enc
:指定要使用的编码,在本例中是aes-256-cbc
-
-in
:输入文件 -
-out
:输出文件 -
-d
:解密
运行openssl
命令后,我们执行ls -la
来验证输出文件是否确实已创建。
然后我们解密文件。请注意文件的顺序和添加-d
参数(用于解密)。我们再次进行求和,以验证生成的文件与原始文件相同。
由于我不可能一直这样输入,让我们写一个快速脚本来做到这一点:
第七章-脚本 7
#!/bin/sh
#
# 6/2/2017
#
echo "Chapter 7 - Script 7"
if [ $# -ne 3 ] ; then
echo "Usage: script7 -e|-d infile outfile"
echo " Uses openssl to encrypt files."
echo " -e to encrypt"
echo " -d to decrypt"
exit 255
fi
PARM=$1
INFILE=$2
OUTFILE=$3
if [ ! -f $INFILE ] ; then
echo "Input file $INFILE does not exist."
exit 100
fi
if [ "$PARM" = "-e" ] ; then
echo "Encrypting"
openssl enc -aes-256-cbc -in $INFILE -out $OUTFILE
elif [ "$PARM" = "-d" ] ; then
echo "Decrypting"
openssl enc -aes-256-cbc -d -in $INFILE -out $OUTFILE
else
echo "Please specify either -e or -d."
exit 101
fi
ls -la $OUTFILE
echo "End of script7"
exit 0
这是屏幕截图:
这显然比输入(或尝试记住)openssl 的语法要容易得多。正如您所看到的,解密后的文件(file2.txt
)与file1.txt
文件相同。
摘要
在本章中,我们展示了如何使用重定向运算符写出文件,以及如何使用(格式正确的)read
命令读取文件。涵盖了将文件内容转换为变量的内容,以及使用校验和和文件加密。
在下一章中,我们将介绍一些可以用来从互联网上的网页收集信息的实用程序。
第八章:使用 wget 和 curl
本章将展示如何使用wget
和curl
直接从互联网上收集信息。
本章涵盖的主题有:
-
展示如何使用
wget
获取信息。 -
展示如何使用
curl
获取信息。
以这种方式收集数据的脚本可以是非常强大的工具。正如您从本章中所看到的,您可以从世界各地的网站自动获取股票报价、湖泊水位等等。
介绍 wget 程序
您可能已经听说过或者甚至使用过wget
程序。它是一个命令行实用程序,可用于从互联网下载文件。
这里有一张截图显示了wget
的最简单形式:
wget 选项
在输出中,您可以看到wget
从我的jklewis.com网站下载了index.html
文件。
这是wget
的默认行为。标准用法是:
wget [options] URL
其中URL代表统一资源定位符,或者网站的地址。
这里只是wget
的许多可用选项的简短列表:
参数 | 解释 |
---|---|
-o |
log 文件,消息将被写入这里,而不是到STDOUT |
-a |
与-o 相同,除了它附加到log 文件 |
-O |
输出文件,将文件复制到这个名称 |
-d |
打开调试 |
-q |
静默模式 |
-v |
详细模式 |
-r |
递归模式 |
让我们试试另一个例子:
在这种情况下使用了-o
选项。检查了返回代码,代码0
表示没有失败。没有输出,因为它被定向到log
文件,然后由cat
命令显示。
在这种情况下使用了-o
选项,将输出写入文件。没有显示输出,因为它被定向到log
文件,然后由cat
命令显示。检查了wget
的返回代码,代码0
表示没有失败。
请注意,这次它将下载的文件命名为index.html.1
。这是因为index.html
是在上一个例子中创建的。这个应用程序的作者这样做是为了避免覆盖先前下载的文件。非常好!
看看下面的例子:
在这里,我们告诉wget
下载给定的文件(shipfire.gif
)。
在下一个截图中,我们展示了wget
如何返回一个有用的错误代码:
wget 返回代码
这个错误发生是因为在我的网站的基本目录中没有名为shipfire100.gif
的文件。请注意输出显示了404 Not Found消息,这在网络上经常看到。一般来说,这意味着在那个时间点请求的资源不可用。在这种情况下,文件不存在,所以会出现这个消息。
还要注意wget
如何返回了一个8
错误代码。wget
的 man 页面显示了可能的退出代码:
错误代码 | 解释 |
---|---|
0 |
没有发生问题。 |
1 |
通用错误代码。 |
2 |
解析错误。例如在解析命令行选项时,.wgetrc 或.netrc 文件 |
3 |
文件 I/O 错误。 |
4 |
网络故障。 |
5 |
SSL 验证失败。 |
6 |
用户名/密码验证失败。 |
7 |
协议错误。 |
8 |
服务器发出错误响应。 |
返回8
是非常合理的。服务器找不到文件,因此返回了404
错误代码。
wget 配置文件
现在是时候提到不同的wget
配置文件了。有两个主要文件,/etc/wgetrc
是全局wget
启动文件的默认位置。在大多数情况下,您可能不应该编辑这个文件,除非您真的想要进行影响所有用户的更改。文件$HOME/.wgetrc
是放置任何您想要的选项的更好位置。一个好的方法是在文本编辑器中打开/etc/wgetrc
和$HOME/.wgetrc
,然后将您想要的部分复制到您的$HOME./wgetrc
文件中。
有关wget
配置文件的更多信息,请参阅man
页面(man wget
)。
现在让我们看看wget
的运行情况。我写了这个脚本一段时间,以跟踪我曾经划船的湖泊的水位:
第八章-脚本 1
#!/bin/sh
# 6/5/2017
# Chapter 8 - Script 1
URL=http://www.arlut.utexas.edu/omg/weather.html
FN=weather.html
TF=temp1.txt # temp file
LF=logfile.txt # log file
loop=1
while [ $loop -eq 1 ]
do
rm $FN 2> /dev/null # remove old file
wget -o $LF $URL
rc=$?
if [ $rc -ne 0 ] ; then
echo "wget returned code: $rc"
echo "logfile:"
cat $LF
exit 200
fi
date
grep "Lake Travis Level:" $FN > $TF
cat $TF | cut -d ' ' -f 12 --complement
sleep 1h
done
exit 0
这个输出是从 2017 年 6 月 5 日。它看起来不怎么样,但在这里:
第八章-脚本 1
您可以从脚本和输出中看到,它每小时运行一次。如果您想知道为什么会有人写这样的东西,我需要知道湖泊水位是否低于 640 英尺,因为我必须把我的船移出码头。这是德克萨斯州的一次严重干旱期间。
编写这样的脚本时需要记住一些事情:
-
首次编写脚本时,手动执行
wget
一次,然后使用下载的文件进行操作。 -
不要在短时间内多次使用
wget
,否则您可能会被网站屏蔽。 -
请记住,HTML 程序员喜欢随时更改事物,因此您可能需要相应地调整您的脚本。
-
当您最终调整好脚本时,一定要再次激活
wget
。
wget 和递归
wget
程序还可以使用递归(-r
)选项下载整个网站的内容。
例如,请查看以下屏幕截图:
使用无冗长(-nv
)选项来限制输出。wget
命令完成后,使用 more 命令来查看日志的内容。根据文件数量,输出可能会非常长。
在使用wget
时,您可能会遇到意外问题。它可能不会获取任何文件,或者可能获取其中一些但不是全部。它甚至可能在没有合理错误消息的情况下失败。如果发生这种情况,请非常仔细地查看man
页面(man wget
)。可能有一个选项可以帮助您解决问题。特别是要查看以下内容。
在您的系统上运行wget --version
。它将显示选项和功能的详细列表,以及wget
的编译方式。
以下是从我运行 CentOS 6.8 64 位系统中获取的示例:
wget 选项
通常情况下,wget
的默认设置对大多数用户来说已经足够好,但是,您可能需要不时地进行调整,以使其按照您的意愿进行工作。
以下是一些wget
选项的部分列表:
wget 选项 | 解释 |
---|---|
--- | --- |
-o 文件名 |
将输出消息输出到log 文件。这在本章中已经介绍过了。 |
-t 数字 |
在放弃连接之前尝试的次数。 |
-c |
继续从以前的wget 中下载部分下载的文件。 |
-S |
显示服务器发送的标头。 |
-Q 数字 |
下载的总字节数配额。数字可以是字节,千字节(k)或兆字节(m)。设置为 0 或 inf 表示没有配额。 |
-l 数字 |
这指定了最大递归级别。默认值为 5。 |
-m |
在尝试创建站点的镜像时很有用。相当于使用-r -N -l inf --no-remove-listing 选项。 |
您可能尝试的另一件事是使用-d
选项打开调试。请注意,这仅在您的wget
版本编译时带有调试支持时才有效。让我们看看当我在我的系统上尝试时会发生什么:
我不确定调试是否已打开,现在我知道了。这个输出可能不是很有用,除非你是开发人员,但是,如果你需要发送关于wget
的错误报告,他们会要求调试输出。
正如你所看到的,wget
是一个非常强大的程序,有许多选项。
注意
记得小心使用wget
,不要忘记在循环中至少放一个睡眠。一个小时会更好。
curl
现在让我们看一下curl
程序,因为它与wget
有些相似。wget
和curl
之间的主要区别之一是它们如何处理输出。
wget
程序默认在屏幕上显示一些进度信息,然后下载index.html
文件。相比之下,curl
通常在屏幕上显示文件本身。
这是curl
在我的系统上运行的一个例子,使用了我最喜欢的网站(截图缩短以节省空间):
将输出重定向到文件的另一种方法是使用重定向,就像这样:
当重定向到文件时,你会注意到传输进度显示在屏幕上。还要注意,如果重定向了,任何错误输出都会进入文件而不是屏幕。
curl 选项
这里是 curl 中可用选项的一个非常简要的列表:
Curl 选项 | 说明 |
---|---|
-o |
输出文件名 |
-s |
静默模式。什么都不显示,甚至错误也不显示 |
-S |
在静默模式下显示错误 |
-v |
详细模式,用于调试 |
curl
还有许多其他选项,以及几页的返回代码。要了解更多信息,请参阅curl man
页面。
现在这里有一个脚本,展示了如何使用 curl 自动获取道琼斯工业平均指数的当前值:
第八章-脚本 2
#!/bin/sh
# 6/6/2017
# Chapter 8 - Script 2
URL="https://www.google.com/finance?cid=983582"
FN=outfile1.txt # output file
TF=temp1.txt # temp file for grep
loop=1
while [ $loop -eq 1 ]
do
rm $FN 2> /dev/null # remove old file
curl -o $FN $URL # output to file
rc=$?
if [ $rc -ne 0 ] ; then
echo "curl returned code: $rc"
echo "outfile:"
cat $FN
exit 200
fi
echo "" # carriage return
date
grep "ref_983582_l" $FN > $TF
echo -n "DJIA: "
cat $TF | cut -c 25-33
sleep 1h
done
exit 0
这是在我的系统上的样子。通常情况下,你可能会使用-s
选项将进度信息从输出中去掉,但我觉得它看起来很酷,所以留了下来:
你可以看到curl
和wget
基本上是以相同的方式工作的。记住,当编写这样的脚本时,要牢记页面的格式几乎肯定会不时改变,所以要做好相应的计划。
总结
在本章中,我们展示了如何在脚本中使用wget
和curl
。展示了这些程序的默认行为,以及其中的许多选项。还讨论了返回代码,并呈现了一些示例脚本。
以下章节将介绍如何更轻松地调试脚本中的语法和逻辑错误。
第九章:调试脚本
本章介绍了如何调试 Bash shell 脚本。
使用任何语言进行编程,无论是 C、Java、FORTRAN、COBOL*还是 Bash,都可能非常有趣。然而,通常不有趣的是当出现问题时,需要花费大量时间找到问题并解决问题。本章将尝试向读者展示如何避免一些常见的语法和逻辑错误,以及在出现这些错误时如何找到它们。
*COBOL:好吧,我必须说,在 COBOL 中编程从来都不是一件有趣的事情!
本章涵盖的主题是:
-
如何防止一些常见的语法和逻辑错误。
-
shell 调试命令,如
set -x
和set -v
。 -
其他设置调试的方法。
-
如何使用重定向实时调试。
语法错误
在编写脚本或程序时,遇到语法错误弹出来可能会让人非常沮丧。在某些情况下,解决方案非常简单,您可以立即找到并解决它。在其他情况下,可能需要花费几分钟甚至几个小时。以下是一些建议:
编写循环时,首先放入整个while...do...done
结构。有时很容易忘记结束的done
语句,特别是如果代码跨越了一页以上。
看看脚本 1:
第九章-脚本 1
#!/bin/sh
#
# 6/7/2017
#
echo "Chapter 9 - Script 1"
x=0
while [ $x -lt 5 ]
do
echo "x: $x"
let x++
y=0
while [ $y -lt 5 ]
do
echo "y: $y"
let y++
done
# more code here
# more code here
echo "End of script1"
exit 0
以下是输出:
仔细看,它说错误出现在第 26 行。哇,怎么可能,当文件只有 25 行时?简单的答案是这就是 Bash 解释器处理这种情况的方式。如果您还没有找到错误,实际上是在第 12 行。这就是应该出现done
语句的地方,我故意省略了它,导致了错误。现在想象一下,如果这是一个非常长的脚本。根据情况,可能需要很长时间才能找到导致问题的行。
现在看看脚本 2,它只是脚本 1,带有一些额外的echo
语句:
第九章-脚本 2
#!/bin/sh
#
# 6/7/2017
#
echo "Chapter 9 - Script 2"
echo "Start of x loop"
x=0
while [ $x -lt 5 ]
do
echo "x: $x"
let x++
echo "Start of y loop"
y=0
while [ $y -lt 5 ]
do
echo "y: $y"
let y++
done
# more code here
# more code here
echo "End of script2"
exit 0
以下是输出:
您可以看到echo
语句“x 循环的开始”已显示。但是,第二个“y 循环的开始”没有显示。这让你很清楚,错误出现在第二个echo
语句之前的某个地方。在这种情况下,就在前面,但不要指望每次都那么幸运。
自动备份
现在给出一些免费的编程建议,备份文件的自动备份在第四章中提到过,创建和调用子例程。我强烈建议在编写任何稍微复杂的东西时使用类似的方法。没有什么比在编写程序或脚本时工作得很顺利,只是做了一些更改,然后以一种奇怪的方式失败更令人沮丧的了。几分钟前它还在工作,然后砰!它出现了故障,您无法弄清楚是什么更改导致了它。如果您没有编号的备份,您可能会花费几个小时(也许是几天)来寻找错误。我见过人们花费数小时撤消每个更改,直到找到问题。是的,我也这样做过。
显然,如果您有编号的备份,只需返回并找到最新的没有故障的备份。然后您可以对比两个版本,可能会非常快地找到错误。如果没有编号的备份,那么您就自己解决了。不要像我一样等待 2 年或更长时间才意识到所有这些。
更多的语法错误
Shell 脚本的一个基本问题是,语法错误通常直到解释器解析具有问题的行时才会显示出来。以下是一个我经常犯的常见错误。看看你能否通过阅读脚本找到问题:
第九章-脚本 3
#!/bin/sh
#
# 6/7/2017
#
echo "Chapter 9 - Script 3"
if [ $# -ne 1 ] ; then
echo "Usage: script3 parameter"
exit 255
fi
parm=$1
echo "parm: $parm"
if [ "$parm" = "home" ] ; then
echo "parm is home."
elif if [ "$parm" = "cls" ] ; then
echo "parm is cls."
elif [ "$parm" = "end" ] ; then
echo "parm is end."
else
echo "Unknown parameter: $parm"
fi
echo "End of script3"
exit 0
以下是输出:
你找到我的错误了吗?当我编写if...elif...else
语句时,我倾向于复制并粘贴第一个if
语句。然后我在下一个语句前加上elif
,但忘记删除if
。这几乎每次都会让我犯错。
看看我是如何运行这个脚本的。我首先只用脚本的名称来调用Usage
子句。你可能会发现有趣的是,解释器没有报告语法错误。那是因为它从来没有执行到那一行。这可能是脚本的一个真正问题,因为它可能运行数天、数周,甚至数年,然后在有语法错误的代码部分运行并失败。在编写和测试脚本时请记住这一点。
这里是另一个经典语法错误的快速示例(经典是指我刚刚再次犯了这个错误):
for i in *.txt
echo "i: $i"
done
运行时输出如下:
./script-bad: line 8: syntax error near unexpected token `echo'
./script-bad: line 8: ` echo "i: $i"'
你能找到我的错误吗?如果找不到,请再看一遍。我忘了在for
语句后加上do
语句。糟糕的 Jim!
在脚本中最容易出错的事情之一是忘记在变量前加上$
。如果你在其他语言如 C 或 Java 中编码,特别容易出错,因为在这些语言中你不需要在变量前加上$
。我能给出的唯一真正的建议是,如果你的脚本似乎做不对任何事情,请检查所有的变量是否有$
。但要小心,不要过度添加它们!
逻辑错误
现在让我们谈谈逻辑错误。这些很难诊断,不幸的是我没有任何神奇的方法来避免这些错误。然而,有一些事情可以指出来,以帮助追踪它们。
编码中的一个常见问题是所谓的 1 偏差错误。这是由于计算机语言设计者在六十年代决定从 0 开始编号事物而引起的。计算机可以愉快地从任何地方开始计数,而且从不抱怨,但大多数人类在从 1 开始计数时通常做得更好。我的大多数同行可能会不同意这一点,但由于我总是不得不修复他们的 1 偏差缺陷,我坚持我的看法。
现在让我们看一下以下非常简单的脚本:
第九章 - 脚本 4
#!/bin/sh
#
# 6/7/2017
#
echo "Chapter 9 - Script 4"
x=0
while [ $x -lt 5 ]
do
echo "x: $x"
let x++
done
echo "x after loop: $x"
let maxx=x
y=1
while [ $y -le 5 ]
do
echo "y: $y"
let y++
done
echo "y after loop: $y"
let maxy=y-1 # must subtract 1
echo "Max. number of x: $maxx"
echo "Max. number of y: $maxy"
echo "End of script4"
exit 0
输出:
看一下两个循环之间的微妙差异:
-
在
x
循环中,计数从0
开始。 -
x
在小于5
的情况下递增。 -
循环后
x
的值为5
。 -
变量
maxx
,它应该等于迭代次数,被设置为x
。 -
在
y
循环中,计数从1
开始。 -
y
在小于或等于5
的情况下递增。 -
循环后
y
的值为6
。 -
变量
maxy
,它应该等于迭代次数,被设置为y-1
。
如果你已经完全理解了上面的内容,你可能永远不会遇到 1 偏差错误的问题,那太好了。
对于我们其他人,我建议你仔细看一下,直到你完全理解为止。
使用 set 调试脚本
你可以使用set
命令来帮助调试你的脚本。set
有两个常见的选项,x
和v
。以下是每个选项的描述。
请注意,-
激活set
,而+
则取消激活。如果这对你来说听起来很反常,那是因为它确实是反常的。
使用:
-
set -x
:在运行命令之前显示扩展的跟踪 -
set -v
:显示解析输入行
看一下脚本 5,它展示了set -x
的作用:
第九章 - 脚本 5 和脚本 6
#!/bin/sh
#
# 6/7/2017
#
set -x # turn debugging on
echo "Chapter 9 - Script 5"
x=0
while [ $x -lt 5 ]
do
echo "x: $x"
let x++
done
echo "End of script5"
exit 0
输出:
如果一开始看起来有点奇怪,不要担心,你看得越多就会变得更容易。实质上,以+
开头的行是扩展的源代码行,而没有+
的行是脚本的输出。
看一下前两行。它显示:
+ echo 'Chapter 9 - Script 5'
Chapter 9 - Script 5
第一行显示了扩展的命令,第二行显示了输出。
您还可以使用set -v
选项。这是Script 6的屏幕截图,它只是Script 5,但这次使用了set -v
:
您可以看到输出有很大的不同。
请注意,使用set
命令,您可以在脚本中的任何时候打开和关闭它们。这样可以将输出限制为您感兴趣的代码区域。
让我们看一个例子:
第九章 - 脚本 7
#!/bin/sh
#
# 6/8/2017
#
set +x # turn debugging off
echo "Chapter 9 - Script 7"
x=0
for fn in *.txt
do
echo "x: $x - fn: $fn"
array[$x]="$fn"
let x++
done
maxx=$x
echo "Number of files: $maxx"
set -x # turn debugging on
x=0
while [ $x -lt $maxx ]
do
echo "File: ${array[$x]}"
let x++
done
set +x # turn debugging off
echo "End of script7"
exit 0
和输出:
请注意,尽管默认情况下关闭了调试,但在脚本开头明确关闭了调试。这是一个很好的方法,可以跟踪何时关闭和何时打开调试。仔细查看输出,看看调试语句直到第二个循环与数组开始显示。然后在运行最后两行之前关闭它。
使用set
命令时的输出有时可能很难看,因此这是限制您必须浏览以找到感兴趣的行的好方法。
还有一种调试技术,我经常使用。在许多情况下,我认为它优于使用set
命令,因为显示不会变得太混乱。您可能还记得在第六章中,使用脚本自动化任务,我们能够将输出显示到其他终端。这是一个非常方便的功能。
以下脚本显示了如何在另一个终端中显示输出。一个子例程用于方便:
第九章 - 脚本 8
#!/bin/sh
#
# 6/8/2017
#
echo "Chapter 9 - Script 8"
TTY=/dev/pts/35 # TTY of other terminal
# Subroutines
p1() # display to TTY
{
rc1=0 # default is no error
if [ $# -ne 1 ] ; then
rc1=2 # missing parameter
else
echo "$1" > $TTY
rc1=$? # set error status of echo command
fi
return $rc1
}
# Code
p1 # missing parameter
echo $?
p1 Hello
echo $?
p1 "Linux Rules!"
echo $?
p1 "Programming is fun!"
echo $?
echo "End of script8"
exit 0
和输出:
记得引用p1
的参数,以防它包含空格字符。
这个子例程可能有点过度使用于调试,但它涵盖了本书中之前讨论的许多概念。这种方法也可以用于在脚本中在多个终端中显示信息。我们将在下一章中讨论这一点。
提示
当写入终端时,如果收到类似于此的消息:
./script8: 第 26 行:/dev/pts/99:权限被拒绝
这可能意味着终端尚未打开。还要记住将终端设备字符串放入变量中,因为这些在重新启动后往往会更改。像TTY=/dev/pts/35
这样的东西是个好主意。
使用这种调试技术的好时机是在编写表单脚本时,就像我们在第五章中所做的那样,创建交互式脚本。因此,让我们再次看一下该脚本,并使用这个新的子例程。
第九章 - 脚本 9
#!/bin/sh
# 6/8/2017
# Chapter 9 - Script 9
#
TTY=/dev/pts/35 # debug terminal
# Subroutines
cls()
{
tput clear
}
move() # move cursor to row, col
{
tput cup $1 $2
}
movestr() # move cursor to row, col
{
tput cup $1 $2
echo -n "$3" # display string
}
checktermsize()
{
p1 "Entering routine checktermsize."
rc1=0 # default is no error
if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then
rc1=1 # set return code
fi
return $rc1
}
init() # set up the cursor position array
{
p1 "Entering routine init."
srow[0]=2; scol[0]=7 # name
srow[1]=4; scol[1]=12 # address 1
srow[2]=6; scol[2]=12 # address 2
srow[3]=8; scol[3]=7 # city
srow[4]=8; scol[4]=37 # state
srow[5]=8; scol[5]=52 # zip code
srow[6]=10; scol[6]=8 # email
}
drawscreen() # main screen draw routine
{
p1 "Entering routine drawscreen."
cls # clear the screen
movestr 0 25 "Chapter 9 - Script 9"
movestr 2 1 "Name:"
movestr 4 1 "Address 1:"
movestr 6 1 "Address 2:"
movestr 8 1 "City:"
movestr 8 30 "State:"
movestr 8 42 "Zip code:"
movestr 10 1 "Email:"
}
getdata()
{
p1 "Entering routine getdata."
x=0 # array subscript
rc1=0 # loop control variable
while [ $rc1 -eq 0 ]
do
row=${srow[x]}; col=${scol[x]}
p1 "row: $row col: $col"
move $row $col
read array[x]
let x++
if [ $x -eq $sizeofarray ] ; then
rc1=1
fi
done
return 0
}
showdata()
{
p1 "Entering routine showdata."
fn=0
echo ""
read -p "Enter filename, or just Enter to skip: " filename
if [ -n "$filename" ] ; then # if not blank
echo "Writing to '$filename'"
fn=1 # a filename was given
fi
echo "" # skip 1 line
echo "Data array contents: "
y=0
while [ $y -lt $sizeofarray ]
do
echo "$y - ${array[$y]}"
if [ $fn -eq 1 ] ; then
echo "$y - ${array[$y]}" >> "$filename"
fi
let y++
done
return 0
}
p1() # display to TTY
{
rc1=0 # default is no error
if [ $# -ne 1 ] ; then
rc1=2 # missing parameter
else
echo "$1" > $TTY
rc1=$? # set error status of echo command
fi
return $rc1
}
# Code starts here
p1 " " # carriage return
p1 "Starting debug of script9"
sizeofarray=7 # number of array elements
if [ "$1" = "--help" ] ; then
p1 "In Usage clause."
echo "Usage: script9 --help"
echo " This script shows how to create an interactive screen program"
echo " and how to use another terminal for debugging."
exit 255
fi
checktermsize 25 80
rc=$?
if [ $rc -ne 0 ] ; then
echo "Please size the terminal to 25x80 and try again."
exit 1
fi
init # initialize the screen array
drawscreen # draw the screen
getdata # cursor movement and data input routine
showdata # display the data
p1 "At exit."
exit 0
这是调试终端的输出(dev/pts/35
):
通过在另一个终端中显示调试信息,更容易看到代码中发生了什么。
您可以将p1
例程放在您认为问题可能出现的任何地方。标记正在使用的子例程也可以帮助确定问题是在子例程中还是在主代码体中。
当您的脚本完成并准备好使用时,您不必删除对p1
例程的调用,除非您真的想这样做。您只需在例程顶部编写return 0
。
我在调试 shell 脚本或 C 程序时使用这种方法,它对我来说总是非常有效。
摘要
在本章中,我们解释了如何防止一些常见的语法和逻辑错误。还描述了 shell 调试命令set -x
和set -v
。还展示了使用重定向将脚本的输出发送到另一个终端以实时调试的方法。
在下一章中,我们将讨论脚本编写的最佳实践。这包括仔细备份您的工作并选择一个好的文本编辑器。还将讨论使用环境变量和别名来帮助您更有效地使用命令行的方法。
第十章:脚本最佳实践
本章解释了一些实践和技术,这些实践和技术将帮助读者成为更好、更高效的程序员。
在本章中,我们将讨论我认为是脚本(或编程)最佳实践。自 1977 年以来,我一直在编程计算机,积累了相当丰富的经验。我很高兴教人们有关计算机的知识,希望我的想法能对一些人有所帮助。
涵盖的主题如下:
-
备份将再次被讨论,包括验证
-
我将解释如何选择一个你感到舒适的文本编辑器,并了解它的功能
-
我将涵盖一些基本的命令行项目,比如使用良好的提示符、命令完成、环境变量和别名
-
我将提供一些额外的脚本
验证备份
我已经在本书中至少两次谈到了备份,这将是我承诺的最后一次。创建您的备份脚本,并确保它们在应该运行时运行。但我还没有谈到的一件事是验证备份。您可能有 10 太拉夸德的备份存放在某个地方,但它们真的有效吗?您上次检查是什么时候?
使用tar
命令时,它会在运行结束时报告是否遇到任何问题制作存档。一般来说,如果没有显示任何问题,备份可能是好的。使用带有-t(tell)
选项的tar
,或者在本地或远程机器上实际提取它,也是确定存档是否成功制作的好方法。
注意
注意:在使用 tar 时一个常见的错误是将当前正在更新的文件包含在备份中。
这是一个相当明显的例子:
guest1 /home # tar cvzf guest1.gz guest1/ | tee /home/guest1/temp/mainlogs`date '+%Y%m%d'`.gz
tar
命令可能不认为这是一个错误,但通常会报告,所以一定要检查一下。
另一个常见的备份错误是不将文件复制到另一台计算机或外部设备。如果您擅长备份,但它们都在同一台机器上,最终硬盘和/或控制器将会失败。您可能能够恢复数据,但为什么要冒险呢?将文件复制到至少一个外部驱动器和/或计算机上,保险起见。
我将提到备份的最后一件事。确保您将备份发送到离岗位置,最好是在另一个城市、州、大陆或行星上。对于您宝贵的数据,您真的不能太小心。
ssh 和 scp
使用scp
到远程计算机也是一个非常好的主意,我的备份程序每天晚上也会这样做。以下是如何设置无人值守ssh
/scp
。在这种情况下,机器 1(M1)上的 root 帐户将能够将文件scp
到机器 2(M2)上的 guest1 帐户。我之所以这样做,是因为出于安全原因,我总是在所有的机器上禁用ssh
/scp
的 root 访问。
-
首先确保在每台机器上至少运行了一次
ssh
。这将设置一些必要的目录和文件。 -
在 M1 上,在
root
下,运行ssh-keygen -t rsa
命令。这将在/root/.ssh
目录中创建文件id_rsa.pub
。 -
使用
scp
将该文件复制到 M2 的/tmp
目录(或其他适当的位置)。 -
在 M2 中转到
/home/guest1/.ssh
目录。 -
如果已经有一个
authorized_keys
文件,请编辑它,否则创建它。 -
将
/tmp/id_rsa.pub
文件中的行复制到authorized_keys
文件中并保存。
通过使用scp
将文件从 M1 复制到 M2 进行测试。它应该可以在不提示输入密码的情况下工作。如果有任何问题,请记住,这必须为每个想要执行无人值守ssh
/scp
的用户设置。
如果您的互联网服务提供商(ISP)为您的帐户提供 SSH,这种方法也可以在那里使用。我一直在使用它,它真的很方便。使用这种方法,您可以让脚本生成一个 HTML 文件,然后将其直接复制到您的网站上。动态生成 HTML 页面是程序真正擅长的事情。
找到并使用一个好的文本编辑器
如果你只是偶尔写脚本或程序,那么 vi 可能对你来说已经足够了。然而,如果你进行了一些真正深入的编程,无论是在 Bash、C、Java 还是其他语言,你都应该非常确定地了解一些其他可用的 Linux 文本编辑器。你几乎肯定会变得更有生产力。
正如我之前提到的,我已经使用计算机工作了很长时间。我最开始在 DOS 上使用一个叫做 Edlin 的编辑器,它相当弱(但仍然比穿孔卡好)。我最终转而开始在 AIX(IBM 的 UNIX 版本)上使用 vi。我在使用 vi 方面变得相当擅长,因为当时我们还没有其他选择。随着时间的推移,其他选择变得可用,我开始使用 IBM 个人编辑器。这些非常容易使用,比 vi 更高效,并且具有更多功能。随着我进行了越来越多的编程,我发现这些编辑器都不能满足我想要的一切,所以我用 C 编程语言编写了自己的编辑器。这是很久以前在 DOS 下,然而,我的编辑器现在已经被修改以在 Xenix、OS/2、AIX、Solaris、UNIX、FreeBSD、NetBSD 和当然 Linux 上运行。它在 Cygwin 环境下的 Windows 上也运行良好。
任何文本编辑器都应该具有标准功能,如复制、粘贴、移动、插入、删除、拆分、合并、查找/替换等。这些应该易于使用,不需要超过两个按键。保存
命令只需要一个按键。
此外,一个好的编辑器还应该具有以下一个或多个功能:
-
能够同时编辑多个文件(文件环)
-
能够用单个按键切换到环中的下一个或上一个文件
-
能够显示环中的文件并立即切换到任何文件
-
能够将文件插入当前文件
-
能够记录和回放记住的按键序列。有时这被称为宏
-
撤销/恢复功能
-
自动保存文件选项
-
一个锁定文件的功能,以防止在编辑器的另一个实例中编辑同一个文件
-
绝对没有明显的缺陷或错误。这是强制性的
-
通过心灵感应接受输入
嗯,也许我还没有完全弄清楚最后一个。当然还有许多许多其他功能可以列出,但我觉得这些是最重要的。
这是我的编辑器的截图,显示了ring
命令可能的样子:
还有很多功能可以展示,但这应该足以表达观点。我会提到 vi 是一个很好的编辑器,可能大多数 UNIX/Linux 用户都成功地使用它。然而,根据我的经验,如果要进行大量的编程,使用具有更多功能的不同编辑器将节省大量时间。这也更容易一些,这使得整个过程更有趣。
环境变量和别名
环境变量在第二章中有介绍,变量处理。这是我多年前学到的一个很酷的技巧,可以在使用命令行时真正帮助。大多数 Linux 系统通常在$HOME
下有几个标准目录,如桌面、下载、音乐、图片等。我个人不喜欢一遍又一遍地输入相同的东西,所以这样做可以帮助更有效地使用系统。以下是我添加到/home/guest1/.bashrc
文件的一些行:
export BIN=$HOME/bin
alias bin="cd $BIN"
export DOWN=$HOME/Downloads
alias down="cd $DOWN"
export DESK=$HOME/Desktop
alias desk="cd $DESK"
export MUSIC=$HOME/Music
alias music="cd $MUSIC"
export PICTURES=$HOME/Pictures
alias pictures="cd $PICTURES"
export BOOKMARKS=$HOME/Bookmarks
alias bookmarks="cd $BOOKMARKS"
# Packt- Linux Scripting Bootcamp
export LB=$HOME/LinuxScriptingBook
alias lb="cd $LB"
# Source lbcur
. $LB/source.lbcur.txt
使用这种方法,你可以通过只输入小写别名来 cd 到上述任何一个目录。更好的是,你还可以通过使用大写导出的环境变量来复制或移动文件到目录中或从目录中。看看下面的截图:
我花了好几年的时间才开始做这件事,我仍然为自己没有早点发现而感到后悔。记住将别名设为小写,环境变量设为大写,你就可以开始了。
注意我在“书签”目录中运行的命令。我实际上输入了mv $DESK/
然后按了Tab键。这导致该行自动完成,然后我添加了句点.
字符并按下Enter。
记住尽可能使用命令自动完成,这样可以节省大量时间。
需要解释的是. $LB/source.lbcur.txt
这一行。你可以看到我有一个lbcur
别名,它让我进入我当前撰写本书的目录。由于我同时使用我的 root 和guest1
账户来写书,我只需在source.lbcur.txt
文件中更改章节号。然后我为 root 和guest1
源.bashrc
文件,就完成了。否则,我将不得不在每个.bashrc
文件中进行更改。也许只有两个文件可能不会那么糟糕,但假设你有几个用户呢?我在我的系统上经常使用这种技术,因为我是一个非常懒的打字员。
记住:当使用别名和环境变量时,需要在终端中更改之前先源用户的.bashrc
文件。
ssh 提示
当我运行 Linux 系统时,我倾向于至少打开 30 个终端窗口。其中一些登录到我家的其他机器上。在撰写本文时,我已登录到 laptop1、laptop4 和 gabi1(我女朋友运行 Fedora 20 的笔记本电脑)。我发现很久以前,如果这些终端的提示不同,我很难弄清楚并在错误的计算机上输入正确的命令。不用说,那可能是一场灾难。有一段时间我会手动更改提示,但这很快就厌倦了。有一天我几乎偶然发现了这个问题的一个非常酷的解决方案。我在 Red Hat Enterprise Linux、Fedora 和 CentOS 上使用了这种技术,所以它也应该适用于您的系统(可能需要稍微调整)。
这些行在我所有系统的$HOME/.bashrc
文件中:
# Modified 1/17/2014
set | grep XAUTHORITY
rc=$?
if [ $rc -eq 0 ] ; then
PS1="\h \w # "
else
PS1="\h \h \h \h \w # "
fi
所以这个命令使用 set 命令来 grep 字符串XAUTHORITY
。这个字符串只存在于本地机器的环境中。因此,当你在 big1 本地打开终端时,它使用正常的提示。然而,如果你ssh
到另一个系统,该字符串就不存在,因此它使用长扩展提示。
这是我系统的屏幕截图:
测试一个存档
这是我在几个计算机工作中遇到的问题。我的经理会要求我接手一个同事的项目。他会将文件zip
或tar
起来,然后给我存档。我会在我的系统上解压缩它并尝试开始工作。但总会有一个文件丢失。通常需要两次、三次或更多次尝试,我才最终拥有编译项目所需的每个文件。所以,这个故事的教训是,当制作一个要交给别人的存档时,一定要确保将其复制到另一台机器上并在那里进行测试。只有这样,你才能相对确定地包含了每个文件。
进度指示器
这是另一个光标移动脚本,它还计算了$RANDOM
Bash 变量的低和高。这可能对每个人来说看起来并不那么酷,但它确实展示了我们在本书中涵盖的更多概念。我也对那个随机数生成器的范围有些好奇。
第十章 - 脚本 1
#!/bin/sh
#
# 6/11/2017
# Chapter 10 - Script 1
#
# Subroutines
trap catchCtrlC INT # Initialize the trap
# Subroutines
catchCtrlC()
{
loop=0 # end the loop
}
cls()
{
tput clear
}
movestr() # move cursor to row, col, display string
{
tput cup $1 $2
echo -n "$3"
}
# Code
if [ "$1" = "--help" ] ; then
echo "Usage: script1 or script1 --help "
echo " Shows the low and high count of the Bash RANDOM variable."
echo " Press Ctrl-C to end."
exit 255
fi
sym[0]='|'
sym[1]='/'
sym[2]='-'
sym[3]='\'
low=99999999
high=-1
cls
echo "Chapter 10 - Script 1"
echo "Calculating RANDOM low and high ..."
loop=1
count=0
x=0
while [ $loop -eq 1 ]
do
r=$RANDOM
if [ $r -lt $low ] ; then
low=$r
elif [ $r -gt $high ] ; then
high=$r
fi
# Activity indicator
movestr 2 1 "${sym[x]}" # row 2 col 1
let x++
if [ $x -gt 3 ] ; then
x=0
fi
let count++
done
echo " " # carriage return
echo "Number of loops: $count"
echo "low: $low high: $high"
echo "End of script1"
exit 0
我系统上的输出:
从模板创建新命令
由于您正在阅读本书,可以假定您将要编写大量脚本。这是我多年来学到的另一个方便的技巧。当我需要创建一个新脚本时,我不是从头开始做,而是使用这个简单的命令:
第十章 - 脚本 2
#!/bin/sh
#
# 1/26/2014
#
# create a command script
if [ $# -eq 0 ] ; then
echo "Usage: mkcmd command"
echo " Copies mkcmd.template to command and edits it with kw"
exit 255
fi
if [ -f $1 ] ; then
echo File already exists!
exit 2
fi
cp $BIN/mkcmd.template $1
kw $1
exit 0
And here is the contents of the $BIN/mkcmd.template file:
#!/bin/sh
#
# Date
#
if [ $# -eq 0 ] ; then
echo "Usage: "
echo " "
exit 255
fi
确保在创建mkcmd.template
文件后对其运行chmod 755
。这样你就不必每次都记得这样做了。事实上,这就是我写这个脚本的主要原因。
随意修改这个脚本,当然也可以将kw
更改为您正在使用的 vi 或其他编辑器。
提醒用户
当重要任务完成并且您想立刻知道时,让您的计算机响铃是很好的。以下是我用来响铃我的计算机内部扬声器的脚本:
第十章 - 脚本 3
#!/bin/sh
#
# 5/3/2017
#
# beep the PC speaker
lsmod | grep pcspkr > /dev/null
rc=$?
if [ $rc -ne 0 ] ; then
echo "Please modprobe pcspkr and try again."
exit 255
fi
echo -e '\a' > /dev/console
这个命令会响铃 PC 扬声器(如果有的话),并且驱动程序已经加载。请注意,这个命令可能只有在以 root 用户身份运行时才能在您的系统上工作。
总结
在这最后一章中,我展示了一些我学到的编程最佳实践。讨论了一个好的文本编辑器的特性,并包括了一个$RANDOM
测试脚本。我还介绍了我多年来编写的一些脚本,以使我的系统更高效、更易于使用。