Linux-Shell-编程训练营-全-

Linux Shell 编程训练营(全)

原文:zh.annas-archive.org/md5/65C572CE82539328A9B0D1458096FD51

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在 Linux Shell Scripting Bootcamp 中,您将首先学习脚本创建的基础知识。您将学习如何验证参数,以及如何检查文件的存在。接着,您将熟悉 Linux 系统上变量的工作原理以及它们与脚本的关系。您还将学习如何创建和调用子例程以及创建交互式脚本。最后,您将学习如何调试脚本和脚本编写的最佳实践,这将使您每次都能编写出优秀的代码!通过本书,您将能够编写能够高效地从网络中获取数据并处理数据的 shell 脚本。

本书涵盖内容

第一章,开始 shell 脚本,从脚本设计的基础知识开始。展示了如何使脚本可执行,以及创建一个信息丰富的Usage消息。还介绍了返回代码的重要性,并使用和验证参数。

第二章,使用变量,讨论了如何声明和使用环境变量和本地变量。我们还讨论了如何执行数学运算以及如何使用数组。

第三章,使用循环和 sleep 命令,介绍了使用循环执行迭代操作的方法。它还展示了如何在脚本中创建延迟。读者还将学习如何在脚本中使用循环和sleep命令。

第四章,创建和调用子例程,从一些非常简单的脚本开始,然后继续介绍一些接受参数的简单子例程。

第五章,创建交互式脚本,解释了使用read内置命令来查询键盘的用法。此外,我们探讨了一些不同的读取选项,并介绍了陷阱的使用。

第六章,使用脚本自动化任务,描述了创建脚本来自动执行任务。还介绍了使用 cron 在特定时间自动运行脚本的正确方法。还讨论了执行压缩备份的存档命令ziptar

第七章,处理文件,介绍了使用重定向运算符将文件写出以及使用read命令读取文件的方法。还讨论了校验和和文件加密,以及将文件内容转换为变量的方法。

第八章,使用 wget 和 curl,讨论了在脚本中使用wgetcurl的用法。除此之外,还讨论了返回代码,并提供了一些示例脚本。

第九章,调试脚本,解释了一些防止常见语法和逻辑错误的技术。还讨论了使用重定向运算符将脚本的输出发送到另一个终端的方法。

第十章,脚本编写最佳实践,讨论了一些实践和技术,将帮助读者每次都编写出优秀的代码。

本书适用对象

任何安装了 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

新术语重要单词以粗体显示。例如,屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“点击下一步按钮将您移至下一个屏幕。”

注意

警告或重要提示会显示在这样的框中。

提示

提示和技巧会以这样的方式出现。

读者反馈

我们的读者的反馈总是受欢迎的。让我们知道您对本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它可以帮助我们开发您真正能够充分利用的书籍。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在主题中提及书籍的标题。

如果您在某个专题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的自豪所有者,我们有一些东西可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册到我们的网站。

  2. 将鼠标指针悬停在顶部的支持选项卡上。

  3. 点击代码下载和勘误

  4. 搜索框中输入书名。

  5. 选择您要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买本书的地点。

  7. 点击下载代码

您还可以通过在 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,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何形式的非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

盗版

请通过<copyright@packtpub.com>与我们联系,并附上涉嫌盗版材料的链接。

我们感谢您帮助保护我们的作者和我们为您提供有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章:开始使用 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行检查是否给出了参数。如果没有,将执行第69行。请注意,通常最好在脚本中包含一个信息性的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 等编程语言不同,那里一切都是强类型的。在前面的示例中,即使ab看起来是整数,它们也是字符串。

这是一个简短的脚本,让我们开始:

第二章-脚本 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"

当字符串包含空格时,引用字符串很重要,例如这里的变量de

注意

我发现如果我引用程序中的所有字符串,但不引用数字,那么更容易跟踪我如何使用变量(即作为字符串还是数字)。

使用条件语句验证参数

当将变量用作数字时,可以测试和比较变量与其他变量。

以下是可以使用的一些运算符的列表:

运算符 说明
-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"

输出如下:

第二章-脚本 2

为了帮助理解本章,请在您的系统上运行脚本。尝试更改变量的值,看看它如何影响输出。

我们在第一章中看到了否定运算符,开始使用 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

这是我系统的输出:

第二章-脚本 3

让我们逐行看一下这个:

  • 第 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

这是我的系统上的输出:

第二章 - 脚本 4

在你的系统上使用不同的参数运行这个脚本。在每次尝试时,尝试确定输出是什么,然后运行它。直到你每次都能做对为止,重复这个过程。现在理解这个概念将对我们在后面处理更复杂的脚本时非常有帮助。

现在让我们看一下脚本 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"

以下是输出:

第二章 - 脚本 5

如你所见,变量和以前一样设置。使用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变量非常有用。这告诉系统要使用哪个文本编辑器来编辑用户的crontabcrontab -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"

这是输出:

第二章 - 脚本 6

你也可以创建和使用自己的环境变量。这是 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
L1=guest1@192.168.1.21
L4=guest1@192.168.1.2
LBCUR=/home/guest1/LinuxScriptingBook/chapters/chap2
export BIN DOWN DESK JAVAPATH KW_WORKDIR L1 L4 LBCUR
  • BIN:这是我的可执行文件和脚本的目录在根目录下

  • DOWN:这是用于电子邮件附件下载的目录等

  • DESK:这是屏幕截图的下载目录

  • JAVAPATH:这是我编写 Java 应用程序时要使用的目录

  • KW_WORKDIR:这是我的编辑器放置其工作文件的位置

  • L1L2:这是我笔记本电脑的 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"

这是我系统上的输出:

第二章 - 脚本 7

在你的系统上运行这个脚本并尝试进行实验。如果你以前从未见过或使用过数组,不要让它们吓到你;你很快就会熟悉它们。这是另一个容易忘记${数组变量}语法的地方,所以如果脚本不按你的意愿执行(或生成错误),首先检查这个。

在下一章中,当我们讨论循环时,我们将更详细地讨论数组。

总结

在本章中,我们介绍了如何声明和使用环境变量和本地变量。我们讨论了如何进行数学运算以及如何处理数组。

我们还介绍了在脚本中使用变量。脚本 1展示了如何分配一个变量并显示其值。脚本 2展示了如何处理数字变量,脚本 3展示了如何比较字符串。脚本 4展示了逻辑运算符,脚本 5展示了如何进行数学运算。脚本 6展示了如何使用环境变量,脚本 7展示了如何使用数组。

第三章:使用循环和 sleep 命令

本章展示了如何使用循环执行迭代操作。它还展示了如何在脚本中创建延迟。读者将学习如何在脚本中使用循环和sleep命令。

本章涵盖的主题如下:

  • 标准的forwhileuntil循环。

  • 循环的嵌套,以及如何避免混淆。

  • 介绍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

以下是输出:

第三章 - 脚本 1

我们首先将变量x设置为1while语句检查x是否小于或等于10,如果是,则运行dodone语句之间的命令。它将继续这样做,直到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 1chapter 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运行时,输出如下:

第三章 - 脚本 4

好吧,我同意这可能不是非常有用,但它确实展示了一些东西。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变量,然后再次开始该过程。在最后一行结束后结束。

通过仅使用LINESCOLUMNS环境变量,可以将实际屏幕写入。您可以通过运行程序然后扩展终端来测试这一点。

在使用嵌套循环时,很容易混淆哪里放什么。这是我每次都尝试做的事情。当我第一次意识到程序(可以是脚本、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

输出:

第三章 - 脚本 6

看看这个脚本。两个循环的输出是相同的;但是,条件是相反的。第一个循环在条件为真时继续,第二个循环在条件为真时继续。这是一个不那么微妙的区别,所以要注意这一点。

使用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目录:

第三章 - 脚本 7

下一个脚本显示了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

以下是输出:

第三章 - 脚本 8

您可以使用for语句做一些其他事情,请参阅 Bash 的man页面以获取更多信息。

提前离开循环

有时在编写脚本时,您会遇到一种情况,希望在满足结束条件之前提前退出循环。可以使用breakcontinue命令来实现这一点。

这是一个显示这些命令的脚本。我还介绍了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

这是我的系统输出:

第三章 - 脚本 9

在您的系统上运行此命令,并在另一个终端中cd/tmp目录。运行命令touch continue.txt并观察发生了什么。如果愿意,您可以多次执行此操作(请记住,上箭头会调用上一个命令)。请注意,当命中continue命令时,变量x不会增加。这是因为控制立即返回到while语句。

现在运行touch break.txt命令。脚本将结束,再次,x没有被增加。这是因为break立即导致循环结束。

breakcontinue命令在脚本中经常使用,因此一定要充分尝试,真正理解发生了什么。

睡眠命令

我之前展示了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

和输出:

第三章 - 脚本 10

您可能已经注意到,我按下了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

这是我的系统输出。最终会出现不同步的情况:

第三章 - 脚本 11

对于绝大多数脚本来说,这永远不会成为一个问题。只要记住,如果您要完成的任务是时间关键的,比如每天晚上准确在 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命令来检查。

这是我系统上的一些示例输出:

第三章-脚本 13

这个脚本可以得到很大的改进。它可以被制作成适用于路径/文件,并且应该检查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

以下是输出:

第四章 - 脚本 2

接下来的部分显示文件重定向。

第四章 - 脚本 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

以下是输出:

第四章 - 脚本 4

在您的系统上尝试一下。如果您使用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

第四章-脚本 5

guest1 $ script5 move

第四章-脚本 5

guest1 $ script5 movestr

第四章-脚本 5

由于我们现在将光标放在特定位置,输出对您来说应该更有意义。请注意,命令行提示重新出现在上次光标位置的地方。

您可能已经注意到,子例程的参数与脚本的参数工作方式相同。参数 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

以下是输出

第四章-脚本 6

这里有一些新概念,所以我们会非常仔细地讲解这个。

首先,我们定义了子例程。请注意,已添加了返回代码。还包括了一个cls例程,以便显示返回代码。

我们现在开始编码。调用cls例程,然后将其返回值存储在rc变量中。然后将显示显示脚本标题的echo语句。

那么,为什么我必须将cls命令的返回代码放入rc变量中呢?我不能在显示脚本标题的echo之后直接显示它吗?不行,因为echo $?总是指的是紧随其后的命令。这很容易被忘记,所以请确保您理解这一点。

好的,现在我们将rc变量重置为0并继续。我本可以使用不同的变量,但由于rc的值不会再次需要,我选择重用rc变量。

现在,在检查参数时,如果没有三个参数,将显示Usage语句。

输入三个参数后,我们会显示它们。这总是一个好主意,特别是在首次编写脚本/程序时。如果不需要,您随时可以将其删除。

第一个子例程sub11个参数运行。这将进行检查,如果需要,将显示错误。

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

在我的系统上的输出

第四章-脚本 7

这个脚本中没有我们尚未涵盖的内容。顶部的非正式注释主要是为了我自己,这样我就不会忘记我写了什么或为什么写了。

检查参数并在不存在时创建后备子目录。我似乎总是记不住要创建它,所以我让脚本来做。

接下来,设置了主要变量,然后如果.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

在我的系统上运行时的输出如下:

第五章 - 脚本 1

这是一个在您的系统上运行的好例子。尝试几种不同的字符串、数字等。注意返回的字符串包含空格、特殊字符等。你不必引用任何东西,如果你这样做了,那些也会被返回。

您还可以使用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

在我的系统上运行时的输出如下:

第五章 - 脚本 2

我按了两次Enter,然后在最后一个上按了QEnter

让我们尝试一些更有趣的东西。下一个脚本显示了如何用从键盘获取的值填充数组:

第五章 - 脚本 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

和输出:

第五章 - 脚本 3

由于这个脚本不需要任何参数,我决定添加一个Usage语句。如果用户使用--help运行它,这将显示,并且在许多系统脚本和程序中是一个常见的特性。

这个脚本中唯一新的东西是read命令。looparray变量在之前的章节中已经讨论过。再次注意,使用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

这是一些示例输出:

第五章 - 脚本 5

这里有很多新信息,让我们来看看。首先定义了子例程,你可以看到我们从前面的脚本 4中包含了checktermsize子例程。

init例程设置了光标放置数组。将初始值放入子例程是良好的编程实践,特别是如果它将被再次调用。

drawscreen例程显示初始表单。请注意,我可以在这里使用srowscol数组中的值,但我不想让脚本看起来太乱。

非常仔细地看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

输出:

第五章-脚本 6

脚本中的注释应该使这个脚本相当容易理解。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

这是我系统上的输出:

第五章-脚本 7

在你的系统上运行这个脚本。按一些键,看看反应。也按几次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)。

这只是这个脚本可以做的一个例子:

第五章-脚本 8

好吧,我想这表明我不是一个很好的艺术家。我会继续从事编程和写书。

总结

在本章中,我们展示了如何使用read内置命令来查询键盘。我们解释了一些不同的读取选项,并介绍了陷阱的使用。还包括了一个简单的绘图游戏。

下一章将展示如何自动运行脚本,使其可以无人值守地运行。我们将解释如何使用cron在特定时间运行脚本。还将介绍归档程序ziptar,因为它们在创建自动化备份脚本时非常有用。

第六章:使用脚本自动化任务

本章介绍了如何使用脚本自动化各种任务。

本章涵盖的主题如下:

  • 如何创建一个自动化任务的脚本。

  • 使用 cron 在特定时间自动运行脚本的正确方法。

  • 如何使用ZIPTAR进行压缩备份。

  • 源代码示例。

读者将学习如何创建自动化脚本。

我们在第三章使用循环和 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

以及在我的系统上的输出:

第六章 - 脚本 3

我只运行了三次,但它可以一直运行。在你的系统上运行之前,让我们谈谈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

以下是输出(根据长度调整):

第六章 - 脚本 4

这个脚本检查 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 时在我的系统上的样子的屏幕截图:

Cron

正如你所看到的,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 和 crontabman 页面。

需要提到的一件事是用户的 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 时,我得到这个:

Cron

还有一件事需要提到的是,如果在使用 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

在我的系统上的输出:

第六章 - 脚本 5

这是一个非常简单的脚本,但它展示了使用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时,真的没有理由包括.cacheCache.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 在特定时间自动运行脚本的正确方法。我们讨论了存档命令ziptar,以展示如何执行压缩备份。我们还包括并讨论了完整的调度程序和备份脚本。

在下一章中,我们将展示如何在脚本中读写文件。

第七章:文件操作

本章将展示如何从文本文件中读取和写入。它还将涵盖文件加密和校验和。

本章涵盖的主题如下:

  • 展示如何使用重定向操作符写出文件

  • 展示如何读取文件

  • 解释如何捕获命令的输出并在脚本中使用

  • 查看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

这是一个截图:

第七章-脚本 1

这很简单。如果文件存在,它会将文件(静默地)删除,然后输出每一行到文件,每次增加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的截图:

第七章-脚本 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

以下是输出:

第七章-脚本 3

这里的结构可能看起来有点奇怪,因为它与我们以前看到的非常不同。此脚本使用read命令获取文件的每一行。在语句中:

 while IFS= read -r linevar

IFS=内部字段分隔符)防止read修剪前导和尾随的空白字符。-r参数使read忽略反斜杠转义序列。下一行使用重定向操作符,将file1.txt作为read的输入。

 done  <  $FN

这里有很多新材料,所以仔细查看,直到你对它感到舒适为止。

上面的脚本有一个小缺陷。如果文件不存在,将会出现错误。看看下面的截图:

第七章-脚本 3

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

以下是输出:

第七章-脚本 4

在使用文件时请记住这一点,并始终检查文件是否存在,然后再尝试读取它。

读写文件

下一个脚本读取一个文本文件并创建其副本:

第七章-脚本 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的截图:

第七章-脚本 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

在我的系统上是这样的:

第七章-脚本 6

这是代码的描述:

  • 在这个脚本中设置的第一件事是一个Ctrl + C的陷阱,它会导致文件被保存并且脚本结束。

  • 定义子例程。

  • 使用getdata例程读取用户输入。

  • savefile例程写出数据数组。

  • getfile例程将文件(如果存在)读入数组。

  • 检查参数,因为需要一个文件名。

  • FN变量设置为文件的名称。

  • 在使用数组时,最好有一个固定的大小,即sizeofarray

  • 检查终端的大小,确保它是 25x80(或 80x25,取决于你的 GUI)。

  • 调用init例程设置屏幕数组。

  • 调用getfiledrawscreen例程。

  • getdata例程用于移动光标并将字段中的数据放入正确的数组位置。

  • Ctrl + C用于保存文件并终止脚本。

这是一个简单的 Bash 屏幕输入/输出例程的示例。这个脚本可能需要一些改进,以下是部分列表:

  • 检查现有文件是否有特定的头。这可以帮助确保文件格式正确,避免语法错误。

  • 检查输入文件,确保它是文本而不是二进制。提示:使用filegrep命令。

  • 如果文件无法正确写出,请确保优雅地捕获错误。

文件校验和

你可能注意到了上面使用了sum命令。它显示文件的校验和和块计数,可用于确定两个或更多个文件是否是相同的文件(即具有完全相同的内容)。

这是一个真实世界的例子:

假设你正在写一本书,文件正在从作者发送到出版商进行审阅。出版商进行了一些修订,然后将修订后的文件发送回作者。有时很容易出现不同步的情况,并收到一个看起来没有任何不同的文件。如果对这两个文件运行sum命令,你可以轻松地确定它们是否相同。

看一下下面的截图:

文件校验和

第一列是校验和,第二列是块计数。如果这两者都相同,那意味着文件的内容是相同的。所以,在这个例子中,bookfiles 1、2 和 4 是相同的。Bookfiles 3 和 5 也是相同的。然而,bookfiles 6、7 和 8 与任何文件都不匹配,最后两个甚至没有相同的块计数。

提示

注意:sum命令只查看文件的内容和块计数。它不查看文件名或其他文件属性,如所有权或权限。要做到这一点,你可以使用lsstat命令。

文件加密

有时候你可能想要加密系统中一些重要和/或机密的文件。有些人把他们的密码存储在计算机的文件中,这可能没问题,但前提是要使用某种类型的文件加密。有许多加密程序可用,在这里我们将展示 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

这是屏幕截图:

第七章-脚本 7

这显然比输入(或尝试记住)openssl 的语法要容易得多。正如您所看到的,解密后的文件(file2.txt)与file1.txt文件相同。

摘要

在本章中,我们展示了如何使用重定向运算符写出文件,以及如何使用(格式正确的)read命令读取文件。涵盖了将文件内容转换为变量的内容,以及使用校验和和文件加密。

在下一章中,我们将介绍一些可以用来从互联网上的网页收集信息的实用程序。

第八章:使用 wget 和 curl

本章将展示如何使用wgetcurl直接从互联网上收集信息。

本章涵盖的主题有:

  • 展示如何使用wget获取信息。

  • 展示如何使用curl获取信息。

以这种方式收集数据的脚本可以是非常强大的工具。正如您从本章中所看到的,您可以从世界各地的网站自动获取股票报价、湖泊水位等等。

介绍 wget 程序

您可能已经听说过或者甚至使用过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 递归模式

让我们试试另一个例子:

wget 选项

在这种情况下使用了-o选项。检查了返回代码,代码0表示没有失败。没有输出,因为它被定向到log文件,然后由cat命令显示。

在这种情况下使用了-o选项,将输出写入文件。没有显示输出,因为它被定向到log文件,然后由cat命令显示。检查了wget的返回代码,代码0表示没有失败。

请注意,这次它将下载的文件命名为index.html.1。这是因为index.html是在上一个例子中创建的。这个应用程序的作者这样做是为了避免覆盖先前下载的文件。非常好!

看看下面的例子:

wget 选项

在这里,我们告诉wget下载给定的文件(shipfire.gif)。

在下一个截图中,我们展示了wget如何返回一个有用的错误代码:

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)选项下载整个网站的内容。

例如,请查看以下屏幕截图:

wget 和递归

使用无冗长(-nv)选项来限制输出。wget命令完成后,使用 more 命令来查看日志的内容。根据文件数量,输出可能会非常长。

在使用wget时,您可能会遇到意外问题。它可能不会获取任何文件,或者可能获取其中一些但不是全部。它甚至可能在没有合理错误消息的情况下失败。如果发生这种情况,请非常仔细地查看man页面(man wget)。可能有一个选项可以帮助您解决问题。特别是要查看以下内容。

在您的系统上运行wget --version。它将显示选项和功能的详细列表,以及wget的编译方式。

以下是从我运行 CentOS 6.8 64 位系统中获取的示例:

wget 和递归

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是一个非常强大的程序,有许多选项。

注意

记得小心使用wget,不要忘记在循环中至少放一个睡眠。一个小时会更好。

curl

现在让我们看一下curl程序,因为它与wget有些相似。wgetcurl之间的主要区别之一是它们如何处理输出。

wget程序默认在屏幕上显示一些进度信息,然后下载index.html文件。相比之下,curl通常在屏幕上显示文件本身。

这是curl在我的系统上运行的一个例子,使用了我最喜欢的网站(截图缩短以节省空间):

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选项将进度信息从输出中去掉,但我觉得它看起来很酷,所以留了下来:

第八章-脚本 2

你可以看到curlwget基本上是以相同的方式工作的。记住,当编写这样的脚本时,要牢记页面的格式几乎肯定会不时改变,所以要做好相应的计划。

总结

在本章中,我们展示了如何在脚本中使用wgetcurl。展示了这些程序的默认行为,以及其中的许多选项。还讨论了返回代码,并呈现了一些示例脚本。

以下章节将介绍如何更轻松地调试脚本中的语法和逻辑错误。

第九章:调试脚本

本章介绍了如何调试 Bash shell 脚本。

使用任何语言进行编程,无论是 C、Java、FORTRAN、COBOL*还是 Bash,都可能非常有趣。然而,通常不有趣的是当出现问题时,需要花费大量时间找到问题并解决问题。本章将尝试向读者展示如何避免一些常见的语法和逻辑错误,以及在出现这些错误时如何找到它们。

*COBOL:好吧,我必须说,在 COBOL 中编程从来都不是一件有趣的事情!

本章涵盖的主题是:

  • 如何防止一些常见的语法和逻辑错误。

  • shell 调试命令,如set -xset -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

以下是输出:

第九章-脚本 1

仔细看,它说错误出现在第 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

以下是输出:

第九章-脚本 2

您可以看到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

以下是输出:

第九章-脚本 3

你找到我的错误了吗?当我编写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

输出:

第九章 - 脚本 4

看一下两个循环之间的微妙差异:

  • x循环中,计数从0开始。

  • x在小于5的情况下递增。

  • 循环后x的值为5

  • 变量maxx,它应该等于迭代次数,被设置为x

  • y循环中,计数从1开始。

  • y在小于或等于5的情况下递增。

  • 循环后y的值为6

  • 变量maxy,它应该等于迭代次数,被设置为y-1

如果你已经完全理解了上面的内容,你可能永远不会遇到 1 偏差错误的问题,那太好了。

对于我们其他人,我建议你仔细看一下,直到你完全理解为止。

使用 set 调试脚本

你可以使用set命令来帮助调试你的脚本。set有两个常见的选项,xv。以下是每个选项的描述。

请注意,-激活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

输出:

第九章 - 脚本 5 和脚本 6

如果一开始看起来有点奇怪,不要担心,你看得越多就会变得更容易。实质上,以+开头的行是扩展的源代码行,而没有+的行是脚本的输出。

看一下前两行。它显示:

 + echo 'Chapter 9 - Script 5'
 Chapter 9 - Script 5

第一行显示了扩展的命令,第二行显示了输出。

您还可以使用set -v选项。这是Script 6的屏幕截图,它只是Script 5,但这次使用了set -v

第九章 - 脚本 5 和脚本 6

您可以看到输出有很大的不同。

请注意,使用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

和输出:

第九章 - 脚本 7

请注意,尽管默认情况下关闭了调试,但在脚本开头明确关闭了调试。这是一个很好的方法,可以跟踪何时关闭和何时打开调试。仔细查看输出,看看调试语句直到第二个循环与数组开始显示。然后在运行最后两行之前关闭它。

使用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

和输出:

第九章 - 脚本 8

记得引用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):

第九章 - 脚本 9

通过在另一个终端中显示调试信息,更容易看到代码中发生了什么。

您可以将p1例程放在您认为问题可能出现的任何地方。标记正在使用的子例程也可以帮助确定问题是在子例程中还是在主代码体中。

当您的脚本完成并准备好使用时,您不必删除对p1例程的调用,除非您真的想这样做。您只需在例程顶部编写return 0

我在调试 shell 脚本或 C 程序时使用这种方法,它对我来说总是非常有效。

摘要

在本章中,我们解释了如何防止一些常见的语法和逻辑错误。还描述了 shell 调试命令set -xset -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 访问。

  1. 首先确保在每台机器上至少运行了一次ssh。这将设置一些必要的目录和文件。

  2. 在 M1 上,在root下,运行ssh-keygen -t rsa命令。这将在/root/.ssh目录中创建文件id_rsa.pub

  3. 使用scp将该文件复制到 M2 的/tmp目录(或其他适当的位置)。

  4. 在 M2 中转到/home/guest1/.ssh目录。

  5. 如果已经有一个authorized_keys文件,请编辑它,否则创建它。

  6. /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到另一个系统,该字符串就不存在,因此它使用长扩展提示。

这是我系统的屏幕截图:

ssh 提示

测试一个存档

这是我在几个计算机工作中遇到的问题。我的经理会要求我接手一个同事的项目。他会将文件ziptar起来,然后给我存档。我会在我的系统上解压缩它并尝试开始工作。但总会有一个文件丢失。通常需要两次、三次或更多次尝试,我才最终拥有编译项目所需的每个文件。所以,这个故事的教训是,当制作一个要交给别人的存档时,一定要确保将其复制到另一台机器上并在那里进行测试。只有这样,你才能相对确定地包含了每个文件。

进度指示器

这是另一个光标移动脚本,它还计算了$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

我系统上的输出:

第十章 - 脚本 1

从模板创建新命令

由于您正在阅读本书,可以假定您将要编写大量脚本。这是我多年来学到的另一个方便的技巧。当我需要创建一个新脚本时,我不是从头开始做,而是使用这个简单的命令:

第十章 - 脚本 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测试脚本。我还介绍了我多年来编写的一些脚本,以使我的系统更高效、更易于使用。

posted @ 2024-05-16 19:07  绝不原创的飞龙  阅读(10)  评论(0编辑  收藏  举报