这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

编写shell脚本

这里是乐趣开始的地方 Here is Where the Fun Begins

https://linuxcommand.org/lc3_writing_shell_scripts.php

With the thousands of commands available to the command line user, how can we remember them all? The answer is, we don’t. The real power of the computer is its ability to do the work for us. To get it to do that, we use the power of the shell to automate things. We write shell scripts.

​ 对于命令行用户来说,有成千上万个可用命令,我们如何记住它们呢?答案是,我们不需要记住它们全部。计算机的真正力量在于它能够替我们完成工作。为了实现这一点,我们利用 shell 的力量来自动化任务。我们编写shell 脚本

什么是 Shell 脚本? What are Shell Scripts?

In the simplest terms, a shell script is a file containing a series of commands. The shell reads this file and carries out the commands as though they have been entered directly on the command line.

​ 简而言之,shell 脚本是包含一系列命令的文件。Shell 会读取该文件,并像直接在命令行上输入这些命令一样执行它们。

The shell is somewhat unique, in that it is both a powerful command line interface to the system and a scripting language interpreter. As we will see, most of the things that can be done on the command line can be done in scripts, and most of the things that can be done in scripts can be done on the command line.

​ Shell 在某种程度上是独特的,它既是一个强大的系统命令行接口,又是一种脚本语言解释器。正如我们将看到的,大多数可以在命令行上完成的事情也可以在脚本中完成,而大多数可以在脚本中完成的事情也可以在命令行上完成。

We have already covered many shell features, but we have focused on those features most often used directly on the command line. The shell also provides a set of features usually (but not always) used when writing programs.

​ 我们已经介绍了许多 shell 特性,但我们侧重于那些在命令行上直接使用的特性。Shell 还提供了一套通常(但并不总是)在编写程序时使用的特性。

Scripts unlock the power of our Linux machine. So let’s have some fun!

​ 脚本释放了我们 Linux 机器的力量。所以让我们来玩一玩吧!

1 - 编写我们的第一个脚本并使其正常工作

编写我们的第一个脚本并使其正常工作 Writing Our First Script and Getting It to Work

https://linuxcommand.org/lc3_wss0010.php

To successfully write a shell script, we have to do three things:

​ 要成功编写一个 shell 脚本,我们需要完成三件事情:

  1. Write a script
  2. Give the shell permission to execute it
  3. Put it somewhere the shell can find it
  4. 编写一个脚本
  5. 给 shell 赋予执行权限
  6. 将脚本放在 shell 能够找到的地方

编写脚本 Writing a Script

A shell script is a file that contains ASCII text. To create a shell script, we use a text editor. A text editor is a program, like a word processor, that reads and writes ASCII text files. There are many, many text editors available for Linux systems, both for the command line and GUI environments. Here is a list of some common ones:

​ Shell 脚本是一个包含 ASCII 文本的文件。为了创建一个 shell 脚本,我们使用一个文本编辑器。文本编辑器是一个程序,类似于文字处理器,用于读写 ASCII 文本文件。对于 Linux 系统,有许多文本编辑器可供选择,包括命令行和图形界面环境。以下是一些常见的文本编辑器列表:

名称 Name描述 Description界面 Interface
vi, vimUnix 文本编辑器的鼻祖 vi 以其晦涩难懂的用户界面而闻名。好处是,vi 强大、轻巧且快速。学习 vi 是 Unix 系统使用的基本技能,因为它在类 Unix 系统上通用。在大多数 Linux 发行版中,vi 的增强版本称为 vim,取代了 vi。vim 是一款出色的编辑器,值得花时间学习它。
The granddaddy of Unix text editors, vi, is infamous for its obtuse user interface. On the bright side, vi is powerful, lightweight, and fast. Learning vi is a Unix rite of passage, since it is universally available on Unix-like systems. On most Linux distributions, an enhanced version of vi called vim is provided in place of vi. vim is a remarkable editor and well worth taking the time to learn it.
命令行
command line
Emacs文本编辑器领域的真正巨头是 Emacs,最初由Richard Stallman编写。Emacs 包含(或可以包含)为文本编辑器构想的所有功能。值得注意的是,vi 和 Emacs 的拥护者之间存在激烈的宗教战争,争论哪个更好。
The true giant in the world of text editors is Emacs originally written by Richard Stallman. Emacs contains (or can be made to contain) every feature ever conceived of for a text editor. It should be noted that vi and Emacs fans fight bitter religious wars over which is better.
命令行
command line
nanonanopine 邮件程序提供的文本编辑器的免费克隆版本。nano 非常易于使用,但与 vimemacs 相比,功能较少。nano 推荐给需要命令行编辑器的初学者。
nano is a free clone of the text editor supplied with the pine email program. nano is very easy to use but is very short on features compared to vim and emacs. nano is recommended for first-time users who need a command line editor.
命令行
command line
geditgedit 是 GNOME 桌面环境附带的编辑器。gedit 易于使用,并包含足够的功能作为初学者级别的编辑器。
gedit is the editor supplied with the GNOME desktop environment. gedit is easy to use and contains enough features to be a good beginners-level editor.
图形界面
graphical
kwritekwrite 是 KDE 附带的“高级编辑器”。它具有语法高亮功能,对于程序员和脚本编写者来说非常有用。
kwrite is the “advanced editor” supplied with KDE. It has syntax highlighting, a helpful feature for programmers and script writers.
图形界面
graphical

Let’s fire up our text editor and type in our first script as follows:

​ 让我们启动我们的文本编辑器,并按照以下方式输入我们的第一个脚本:

1
2
3
4
5
#!/bin/bash

# My first script

echo "Hello World!"

Clever readers will have figured out how to copy and paste the text into the text editor ;-)

​ 聪明的读者已经知道如何将文本复制并粘贴到文本编辑器中 ;-)

This is a traditional “Hello World” program. Forms of this program appear in almost every introductory programming book. We’ll save the file with some descriptive name. How about hello_world?

​ 这是一个传统的“Hello World”程序。这种程序的形式几乎出现在每本入门编程书籍中。我们将文件保存为一些描述性的名称。比如 hello_world

The first line of the script is important. It is a special construct, called a shebang, given to the system indicating what program is to be used to interpret the script. In this case, /bin/bash. Other scripting languages such as Perl, awk, tcl, Tk, and python also use this mechanism.

​ 脚本的第一行很重要。它是一个特殊的结构,称为shebang,告诉系统要使用哪个程序来解释脚本。在本例中是 /bin/bash。其他脚本语言如 Perl, awk, tcl, Tkpython 也使用这种机制。

The second line is a comment. Everything that appears after a “#” symbol is ignored by bash. As our scripts become bigger and more complicated, comments become vital. They are used by programmers to explain what is going on so that others can figure it out. The last line is the echo command. This command simply prints its arguments on the display.

​ 第二行是一个注释bash 会忽略 “#” 符号后面的所有内容。随着我们的脚本变得越来越大和复杂,注释变得至关重要。程序员使用注释来解释正在发生的事情,以便其他人可以理解。最后一行是 echo 命令。该命令简单地将其参数打印到显示器上。

设置权限 Setting Permissions

The next thing we have to do is give the shell permission to execute our script. This is done with the chmod command as follows:

​ 接下来,我们需要给 shell 赋予执行脚本的权限。可以使用 chmod 命令执行以下操作:

1
[me@linuxbox me]$ chmod 755 hello_world

The “755” will give us read, write, and execute permission. Everybody else will get only read and execute permission. To make the script private, (i.e., only we can read and execute), use “700” instead.

​ “755” 将赋予我们读取、写入和执行权限,其他用户只能获取读取和执行权限。如果要使脚本私有(即只有我们自己能读取和执行),可以使用 “700”。

将其放在路径中 Putting It in Our Path

At this point, our script will run. Try this:

​ 此时,我们的脚本将可以运行。尝试运行以下命令:

1
[me@linuxbox me]$ ./hello_world

We should see “Hello World!” displayed.

​ 我们应该会看到 “Hello World!” 显示出来。

Before we go any further, we need to talk about paths. When we type the name of a command, the system does not search the entire computer to find where the program is located. That would take a long time. We see that we don’t usually have to specify a complete path name to the program we want to run, the shell just seems to know.

​ 在继续之前,我们需要讨论一下路径。当我们键入一个命令的名称时,系统不会搜索整个计算机来找到程序所在的位置。那样会花费很长时间。我们注意到,通常我们不需要指定要运行的程序的完整路径名称,shell 似乎自己就知道。

Well, that’s correct. The shell does know. Here’s how: the shell maintains a list of directories where executable files (programs) are kept, and only searches the directories on that list. If it does not find the program after searching each directory on the list, it will issue the famous command not found error message.

​ 是的,这是正确的。shell 确实知道。原因如下:shell 维护着一个包含可执行文件(程序)所在目录的列表,并且只搜索该列表中的目录。如果在列表中的每个目录中都找不到该程序,它将显示著名的command not found错误消息。

This list of directories is called our path. We can view the list of directories with the following command:

​ 这个目录列表被称为我们的路径。我们可以使用以下命令查看目录列表:

1
[me@linuxbox me]$ echo $PATH

This will return a colon separated list of directories that will be searched if a specific path name is not given when a command is entered. In our first attempt to execute our new script, we specified a pathname ("./") to the file.

​ 这将返回一个以冒号分隔的目录列表,如果在输入命令时没有指定特定路径名,系统将在这些目录中进行搜索。在我们尝试执行新脚本时,我们指定了一个路径名("./")。

We can add directories to our path with the following command, where directory is the name of the directory we want to add:

​ 我们可以使用以下命令将目录添加到我们的路径中,其中 directory 是要添加的目录名称:

1
[me@linuxbox me]$ export PATH=$PATH:directory

A better way would be to edit our .bash_profile file to include the above command. That way, it would be done automatically every time we log in.

​ 更好的方法是编辑我们的 .bash_profile 文件,将上述命令包含其中。这样,每次登录时都会自动执行该命令。

Most Linux distributions encourage a practice in which each user has a specific directory for the programs they personally use. This directory is called bin and is a subdirectory of our home directory. If we do not already have one, we can create it with the following command:

​ 大多数 Linux 发行版都鼓励每个用户为他们个人使用的程序创建一个特定的目录。这个目录称为 bin,是我们家目录的子目录。如果我们还没有这个目录,可以使用以下命令创建它:

1
[me@linuxbox me]$ mkdir ~/bin

If we move or copy our script into our new bin directory we’ll be all set. Now we just have to type:

​ 如果我们将脚本移动或复制到新的 bin 目录中,我们就准备好了。现在我们只需要输入:

1
[me@linuxbox me]$ hello_world

and our script will run. Most distributions will have the ~/bin directory already in the PATH, but on some distributions, most notably Ubuntu (and other Debian-based distributions), we may need to restart our terminal session before our newly created bin directory is added to our PATH.

​ 我们的脚本将运行。大多数发行版都已经将 ~/bin 目录添加到 PATH 中,但在某些发行版中,特别是 Ubuntu(和其他基于 Debian 的发行版),我们可能需要重新启动终端会话,以便我们新创建的 bin 目录被添加到 PATH 中。

2 - 编辑我们已有的脚本

编辑我们已有的脚本 Editing the Scripts We Already Have

https://linuxcommand.org/lc3_wss0020.php

Before we start writing new scripts, We’ll take a look at some scripts we already have. These scripts were put into our home directory when our account was created, and are used to configure the behavior of our sessions on the computer. We can edit these scripts to change things.

​ 在我们开始编写新脚本之前,我们先来看一下我们已经有的一些脚本。这些脚本在我们的账户创建时被放置在我们的主目录中,并用于配置我们在计算机上的会话行为。我们可以编辑这些脚本来进行更改。

In this lesson, we will look at a couple of these scripts and learn a few important new concepts about the shell.

​ 在本课程中,我们将查看其中一些脚本,并学习关于 shell 的一些重要的新概念。

During our shell session, the system is holding a number of facts about the world in its memory. This information is called the environment. The environment contains such things as our path, our user name, and much more. We can examine a complete list of what is in the environment with the set command.

​ 在我们的 shell 会话期间,系统会在内存中保存关于该世界的一些事实。这些信息被称为环境。环境包含诸如路径、用户名等等。我们可以使用 set 命令查看环境中的完整列表。

Two types of commands are often contained in the environment. They are aliases and shell functions.

​ 环境中经常包含两种类型的命令:别名shell 函数

环境是如何建立的? How is the Environment Established?

When we log on to the system, the bash program starts, and reads a series of configuration scripts called startup files. These define the default environment shared by all users. This is followed by more startup files in our home directory that define our personal environment. The exact sequence depends on the type of shell session being started. There are two kinds: a login shell session and a non-login shell session. A login shell session is one in which we are prompted for our user name and password; when we start a virtual console session, for example. A non-login shell session typically occurs when we launch a terminal session in the GUI.

​ 当我们登录到系统时,bash 程序启动,并读取一系列的配置脚本,称为启动文件。这些启动文件定义了所有用户共享的默认环境。接下来是我们主目录中的更多启动文件,定义了我们个人的环境。确切的顺序取决于所启动的 shell 会话类型。有两种类型:登录 shell 会话非登录 shell 会话。登录 shell 会话是指我们被提示输入用户名和密码的会话;例如,当我们启动一个虚拟控制台会话时。非登录 shell 会话通常发生在图形界面中启动终端会话时。

Login shells read one or more startup files as shown below:

​ 登录 shell 会读取一个或多个启动文件,如下所示:

文件 File内容 Contents
/etc/profile适用于所有用户的全局配置脚本。
A global configuration script that applies to all users.
~/.bash_profile用户个人的启动文件。可以用来扩展或覆盖全局配置脚本中的设置。
A user’s personal startup file. Can be used to extend or override settings in the global configuration script.
~/.bash_login如果找不到 ~/.bash_profile,bash 尝试读取此脚本。
If ~/.bash_profile is not found, bash attempts to read this script.
~/.profile如果既找不到 ~/.bash_profile 也找不到 ~/.bash_login,bash 尝试读取此文件。这是基于 Debian 的发行版(如 Ubuntu)的默认设置。
If neither ~/.bash_profile nor ~/.bash_login is found, bash attempts to read this file. This is the default in Debian-based distributions, such as Ubuntu.

Non-login shell sessions read the following startup files:

​ 非登录 shell 会话读取以下启动文件:

文件 File内容 Contents
/etc/bash.bashrc适用于所有用户的全局配置脚本。
A global configuration script that applies to all users.
~/.bashrc用户个人的启动文件。可以用来扩展或覆盖全局配置脚本中的设置。
A user’s personal startup file. Can be used to extend or override settings in the global configuration script.

In addition to reading the startup files above, non-login shells also inherit the environment from their parent process, usually a login shell.

​ 除了读取上述启动文件,非登录 shell 会话还会从其父进程继承环境,通常是登录 shell。

Take a look at your system and see which of these startup files you have. Remember— since most of the file names listed above start with a period (meaning that they are hidden), you will need to use the “-a” option when using ls.

​ 查看系统,看看你有哪些启动文件。记住,由于上面列出的大多数文件名以句点开头(表示它们是隐藏文件),使用 ls 命令时需要使用 -a 选项。

The ~/.bashrc file is probably the most important startup file from the ordinary user’s point of view, since it is almost always read. Non-login shells read it by default and most startup files for login shells are written in such a way as to read the ~/.bashrc file as well.

~/.bashrc 文件可能是普通用户来说最重要的启动文件,因为它几乎总是会被读取。非登录 shell 会话默认读取它,而大多数登录 shell 的启动文件也以这种方式编写,以便读取 ~/.bashrc 文件。

If we take a look inside a typical .bash_profile (this one taken from a CentOS system), it looks something like this:

​ 如果我们查看一个典型的 .bash_profile(这是从 CentOS 系统中获取的一个示例),它的内容如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# .bash_profile
# Get the aliases and functions 获取别名和函数 
if [ -f ~/.bashrc ]; then
  . ~/.bashrc
fi

# User specific environment and startup programs
# 用户特定的环境和启动程序
PATH=$PATH:$HOME/bin
export PATH

Lines that begin with a “#” are comments and are not read by the shell. These are there for human readability. The first interesting thing occurs on the fourth line, with the following code:

​ 以“#”开头的行是注释,不会被 shell 读取。这些注释是为了人类可读性。有趣的事情发生在第四行,具有以下代码:

1
2
3
if [ -f ~/.bashrc ]; then
  . ~/.bashrc
fi

This is called an if compound command, which we will cover fully in a later lesson, but for now we will translate:

​ 这被称为if 复合命令,我们将在后面的课程中详细介绍,但现在我们先翻译一下:

If the file "~/.bashrc" exists, then read the "~/.bashrc" file.

​ 如果文件 "~/.bashrc" 存在,则读取 "~/.bashrc" 文件。

We can see that this bit of code is how a login shell gets the contents of .bashrc. The next thing in our startup file does is set the PATH variable to add the ~/bin directory to the path.

​ 我们可以看到,这段代码是登录 shell 获取 .bashrc 的内容的方式。我们的启动文件中的下一个部分是将 PATH 变量设置为将 ~/bin 目录添加到路径中。

Lastly, we have:

​ 最后,我们有:

1
export PATH

The export command tells the shell to make the contents of the PATH variable available to child processes of this shell.

export 命令告诉 shell 将 PATH 变量的内容提供给该 shell 的子进程。

别名 Aliases

An alias is an easy way to create a new command which acts as an abbreviation for a longer one. It has the following syntax:

​ 别名是创建一个新命令的简单方式,该命令作为一个较长命令的缩写。它具有以下语法:

1
alias name=value

where name is the name of the new command and value is the text to be executed whenever name is entered on the command line.

​ 其中 name 是新命令的名称,value 是在命令行输入 name 时要执行的文本。

Let’s create an alias called “l” and make it an abbreviation for the command “ls -l”. We’ll move to our home directory and using our favorite text editor, open the file .bashrc and add this line to the end of the file:

​ 让我们创建一个名为 “l” 的别名,将其作为命令 “ls -l” 的缩写。我们切换到我们的主目录,并使用我们喜欢的文本编辑器打开 .bashrc 文件,并在文件末尾添加以下行:

1
alias l='ls -l'

By adding the alias command to the file, we have created a new command called “l” which will perform “ls -l”. To try out our new command, close the terminal session and start a new one. This will reload the .bashrc file. Using this technique, we can create any number of custom commands for ourselves. Here is another one to try:

​ 通过将 alias 命令添加到文件中,我们创建了一个名为 “l” 的新命令,该命令将执行 “ls -l”。要尝试我们的新命令,关闭终端会话并启动一个新的会话。这将重新加载 .bashrc 文件。使用这种技术,我们可以为自己创建任意数量的自定义命令。下面是另一个要尝试的示例:

1
alias today='date +"%A, %B %-d, %Y"'

This alias creates a new command called “today” that will display today’s date with nice formatting.

​ 该别名创建了一个名为 “today” 的新命令,它将以漂亮的格式显示今天的日期。

By the way, the alias command is just another shell builtin. We can create our aliases directly at the command prompt; however they will only remain in effect during the current shell session. For example:

​ 顺便说一下,alias 命令只是另一个 shell 内置命令。我们可以直接在命令提示符下创建别名;但是,它们只在当前的 shell 会话中有效。例如:

1
[me@linuxbox me]$ alias l='ls -l'

Shell 函数 Shell Functions

Aliases are good for very simple commands, but to create something more complex, we need shell functions. Shell functions can be thought of as “scripts within scripts” or little sub-scripts. Let’s try one. Open .bashrc with our text editor again and replace the alias for “today” with the following:

​ 别名适用于非常简单的命令,但是要创建更复杂的内容,我们需要使用shell 函数。可以将 shell 函数视为“脚本中的脚本”或小的子脚本。让我们尝试一个例子。再次用我们的文本编辑器打开 .bashrc,并用以下内容替换 “today” 的别名:

1
2
3
4
today() {
    echo -n "Today's date is: "
    date +"%A, %B %-d, %Y"
}

Believe it or not, () is a shell builtin too, and as with alias, we can enter shell functions directly at the command prompt.

​ 你可能不会相信,() 也是一个 shell 内置命令,就像 alias 一样,我们可以直接在命令提示符下输入 shell 函数。

1
2
3
4
5
[me@linuxbox me]$ today() {
> echo -n "Today's date is: "
> date +"%A, %B %-d, %Y"
> }
[me@linuxbox me]$

However, like alias, shell functions defined directly on the command line only last as long as the current shell session.

​ 然而,与 alias 类似,直接在命令行定义的 shell 函数只在当前 shell 会话中有效,持续的时间与当前 shell 会话相同。

3 - here 脚本

here 脚本 - Here Scripts

https://linuxcommand.org/lc3_wss0030.php

Beginning with this lesson, we will construct a useful application. This application will produce an HTML document that contains information about our system. As we construct our script, we will discover step by step the tools needed to solve the problem at hand.

​ 从这一课开始,我们将创建一个有用的应用程序。这个应用程序将生成一个包含关于我们系统信息的 HTML 文档。在编写脚本的过程中,我们将逐步发现解决问题所需的工具。

使用脚本编写 HTML 文件 Writing an HTML File with a Script

As we may be aware, a well formed HTML file contains the following content:

​ 如我们所知,一个格式正确的 HTML 文件包含以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<html>
<head>
    <title>
    The title of your page
    </title>
</head>

<body>
    Your page content goes here.
</body>
</html>

Now, with what we already know, we could write a script to produce the above content:

​ 现在,根据我们已经了解的内容,我们可以编写一个脚本来生成上述内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

# sysinfo_page - A script to produce an html file

echo "<html>"
echo "<head>"
echo "  <title>"
echo "  The title of your page"
echo "  </title>"
echo "</head>"
echo ""
echo "<body>"
echo "  Your page content goes here."
echo "</body>"
echo "</html>"

This script can be used as follows:

​ 这个脚本可以按以下方式使用:

1
[me@linuxbox me]$ sysinfo_page > sysinfo_page.html

It has been said that the greatest programmers are also the laziest. They write programs to save themselves work. Likewise, when clever programmers write programs, they try to save themselves typing.

​ 有人说最优秀的程序员也是最懒惰的。他们编写程序来减少工作量。同样地,聪明的程序员编写程序时,会尽量减少键入的次数。

The first improvement to this script will be to replace the repeated use of the echo command with a single instance by using quotation more efficiently:

​ 对于这个脚本的第一个改进是将重复使用的 echo 命令替换为一个单独的实例,通过更有效地使用引号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

echo "<html>
 <head>
   <title>
   The title of your page
   </title>
 </head>
 
 <body>
   Your page content goes here.
 </body>
 </html>"

Using quotation, it is possible to embed carriage returns in our text and have the echo command’s argument span multiple lines.

​ 使用引号,我们可以在文本中嵌入换行符,并且 echo 命令的参数可以跨多行。

While this is certainly an improvement, it does have a limitation. Since many types of markup used in HTML incorporate quotation marks themselves, it makes using a quoted string a little awkward. A quoted string can be used but each embedded quotation mark will need to be escaped with a backslash character.

​ 尽管这无疑是一个改进,但它也有局限性。由于在 HTML 中使用了许多引号,因此使用引号字符串会有些不方便。可以使用引号字符串,但是每个嵌入的引号都需要用反斜杠字符进行转义。

In order to avoid the additional typing, we need to look for a better way to produce our text. Fortunately, the shell provides one. It’s called a here script.

​ 为了避免额外的键入,我们需要寻找一种更好的方式来生成我们的文本。幸运的是,shell 提供了一种方式,它被称为here 脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

cat << _EOF_
<html>
<head>
    <title>
    The title of your page
    </title>
</head>

<body>
    Your page content goes here.
</body>
</html>
_EOF_

A here script (also sometimes called a here document) is an additional form of I/O redirection. It provides a way to include content that will be given to the standard input of a command. In the case of the script above, the standard input of the cat command was given a stream of text from our script.

​ here 脚本(有时也称为 here document)是一种附加的输入/输出重定向形式。它提供了一种将内容提供给命令的标准输入的方法。在上面的脚本中,cat 命令的标准输入接收了来自我们脚本的文本流。

A here script is constructed like this:

​ here 脚本的结构如下:

1
2
3
command << token
content to be used as command's standard input
token

token can be any string of characters. “_EOF_” (EOF is short for “End Of File”) is traditional, but we can use anything as long as it does not conflict with a bash reserved word. The token that ends the here script must exactly match the one that starts it, or else the remainder of our script will be interpreted as more standard input to the command which can lead to some really exciting script failures.

token 可以是任意字符字符串。"_EOF_"(EOF 是 “End Of File” 的缩写)是传统用法,但我们可以使用任何字符串,只要它不与 bash 的保留词冲突即可。结束 here 文档的标记必须与开始的标记完全匹配,否则我们脚本的其余部分将被解释为更多的命令标准输入,这可能导致一些令人激动的脚本失败。

There is one additional trick that can be used with a here script. Often, we might want to indent the content portion of the here script to improve the readability of the script. We can do this if we change the script as follows:

​ 还有一种可以与 here 脚本一起使用的额外技巧。通常,我们可能希望对 here 脚本的内容进行缩进,以提高脚本的可读性。如果我们将脚本更改如下所示,就可以实现这一点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

cat <<- _EOF_
    <html>
    <head>
        <title>
        The title of your page
        </title>
    </head>

    <body>
        Your page content goes here.
    </body>
    </html>
_EOF_

Changing the “<<” to “<<-” causes bash to ignore the leading tabs (but not spaces) in the here script. The output from the cat command will not contain any of the leading tab characters. This technique is a bit problematic, as many text editors are configured (and desirably so) to use sequences of spaces rather than tab characters.

​ 将 “<<” 改为 “<<-",会让 bash 忽略 here 脚本中的前导制表符(但不包括空格)。cat 命令的输出将不包含任何前导制表符。这种技巧有些问题,因为许多文本编辑器被配置为(并且应该如此)使用一系列空格而不是制表符。

O.k., let’s make our page. We will edit our page to get it to say something:

​ 好的,让我们制作我们的页面。我们将编辑我们的页面,使其显示一些信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

cat <<- _EOF_
    <html>
    <head>
        <title>
        My System Information
        </title>
    </head>

    <body>
    <h1>My System Information</h1>
    </body>
    </html>
_EOF_

In our next lesson, we will make our script produce some real information about the system.

​ 在我们的下一课中,我们将让我们的脚本生成一些关于系统的真实信息。

4 - 变量

变量 Variables

https://linuxcommand.org/lc3_wss0040.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

cat <<- _EOF_
    <html>
    <head>
        <title>
        My System Information
        </title>
    </head>

    <body>
    <h1>My System Information</h1>
    </body>
    </html>
_EOF_

Now that we have our script working, let’s improve it. First off, we’ll make some changes because we want to be lazy. In the script above, we see that the phrase “My System Information” is repeated. This is wasted typing (and extra work!) so we’ll improve it like this:

​ 现在我们的脚本可以工作了,让我们对它进行改进。首先,我们想偷点懒。在上面的脚本中,我们发现短语 “My System Information” 是重复的。这是多余的键入(和额外的工作!),所以我们将对其进行改进:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

title="My System Information"

cat <<- _EOF_
    <html>
    <head>
        <title>
        $title
        </title>
    </head>

    <body>
    <h1>$title</h1>
    </body>
    </html>
_EOF_

We added a line to the beginning of the script and replaced the two occurrences of the phrase “My System Information” with $title.

​ 我们在脚本的开头添加了一行,并将短语 “My System Information” 的两个出现替换为 $title

变量 Variables

What we have done is to introduce a fundamental concept that appears in every programming language, variables. Variables are areas of memory that can be used to store information and are referred to by a name. In the case of our script, we created a variable called “title” and placed the phrase “My System Information” into memory. Inside the here script that contains our HTML, we use “$title” to tell the shell to perform parameter expansion and replace the name of the variable with the variable’s contents.

​ 我们所做的是引入了一种在每种编程语言中都出现的基本概念,变量。变量是用于存储信息的内存区域,并通过名称进行引用。在我们的脚本中,我们创建了一个名为 “title” 的变量,并将短语 “My System Information” 存储到内存中。在包含我们的 HTML 的 here 脚本中,我们使用 “$title” 来告诉 shell 执行参数展开,将变量的名称替换为变量的内容。

Whenever the shell sees a word that begins with a “$”, it tries to find out what was assigned to the variable and substitutes it.

​ 当 shell 遇到以 “$” 开头的单词时,它会尝试查找所分配给变量的内容并进行替换。

如何创建变量 How to Create a Variable

To create a variable, put a line in the script that contains the name of the variable followed immediately by an equal sign ("="). No spaces are allowed. After the equal sign, assign the information to store.

​ 要创建一个变量,在脚本中放置一行,其中包含变量名,紧跟着一个等号("=")。不允许有空格。在等号后面,分配要存储的信息。

变量名从何而来? Where Do Variable Names Come From?

We just make them up. That’s right; we get to choose the names for our variables. There are a few rules.

​ 我们只是自己编写。没错,我们可以选择变量的名称。有一些规则。

  1. Names must start with a letter.
  2. A name must not contain embedded spaces. Use underscores instead.
  3. Punctuation marks are not permitted.
  4. 名称必须以字母开头。
  5. 名称不能包含空格。使用下划线代替。
  6. 不允许使用标点符号。

这如何增加我们的懒惰程度? How Does This Increase Our Laziness?

The addition of the title variable made our life easier in two ways. First, it reduced the amount of typing we had to do. Second and more importantly, it made our script easier to maintain.

​ 添加 title 变量以两种方式简化了我们的生活。首先,它减少了我们需要键入的量。更重要的是,它使我们的脚本更易于维护。

As we write more and more scripts (or do any other kind of programming), we will see that programs are rarely ever finished. They are modified and improved by their creators and others. After all, that’s what open source development is all about. Let’s say that we wanted to change the phrase “My System Information” to “Linuxbox System Information.” In the previous version of the script, we would have had to change this in two locations. In the new version with the title variable, we only have to change it in one place. Since our script is so small, this might seem like a trivial matter, but as scripts get larger and more complicated, it becomes very important.

​ 当我们编写越来越多的脚本(或进行任何其他类型的编程)时,我们会发现程序很少是完成的。它们会被其创建者和其他人进行修改和改进。毕竟,这就是开源开发的目的。假设我们想要将短语 “My System Information” 更改为 “Linuxbox System Information."。在以前版本的脚本中,我们需要在两个位置进行更改。而在具有 title 变量的新版本中,我们只需要在一个位置进行更改。由于我们的脚本非常小,这可能看起来像是一个琐碎的事情,但是随着脚本变得越来越大和更复杂,这变得非常重要。

环境变量 Environment Variables

When we start our shell session, some variables are already set by the startup files we looked at earlier. To see all the variables that are in the environment, use the printenv command. One variable in our environment contains the host name for the system. We will add this variable to our script like so:

​ 当我们启动 shell 会话时,一些变量已经由我们之前查看过的启动文件设置好了。要查看环境中的所有变量,请使用 printenv 命令。我们的环境中的一个变量包含系统的主机名。我们将像这样将此变量添加到我们的脚本中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

title="System Information for"

cat <<- _EOF_
    <html>
    <head>
        <title>
        $title $HOSTNAME
        </title>
    </head>

    <body>
    <h1>$title $HOSTNAME</h1>
    </body>
    </html>
_EOF_

Now our script will always include the name of the machine on which we are running. Note that, by convention, environment variables names are uppercase.

​ 现在我们的脚本将始终包含正在运行的计算机的名称。请注意,按照约定,环境变量名称是大写的。

5 - 命令替换和常量

命令替换和常量 Command Substitution and Constants

https://linuxcommand.org/lc3_wss0050.php

In the previous lesson, we learned how to create variables and perform parameter expansions with them. In this lesson, we will extend this idea to show how we can substitute the results from commands.

​ 在上一课中,我们学习了如何创建变量并对其进行参数展开。在本课中,我们将扩展这个概念,展示如何替换命令的结果。

When we last left our script, it could create an HTML page that contained a few simple lines of text, including the host name of the machine which we obtained from the environment variable HOSTNAME. Now, we will add a time stamp to the page to indicate when it was last updated, along with the user that did it.

​ 上次我们离开时,我们的脚本可以创建一个包含一些简单文本的 HTML 页面,其中包括从环境变量 HOSTNAME 获取的计算机的主机名。现在,我们将在页面上添加一个时间戳,以指示上次更新的时间,以及执行更新的用户。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

title="System Information for"

cat <<- _EOF_
    <html>
    <head>
        <title>
        $title $HOSTNAME
        </title>
    </head>

    <body>
    <h1>$title $HOSTNAME</h1>
    <p>Updated on $(date +"%x %r %Z") by $USER</p>
    </body>
    </html>
_EOF_

As we can see, another environment variable, USER, is used to get the user name. In addition, we used this strange looking thing:

​ 正如我们所看到的,另一个环境变量 USER 用于获取用户名。此外,我们使用了这样一个看起来奇怪的东西:

1
$(date +"%x %r %Z")

The characters “$( )” tell the shell, “substitute the results of the enclosed command,” a technique known as command substitution. In our script, we want the shell to insert the results of the command date +"%x %r %Z" which outputs the current date and time. The date command has many features and formatting options. To look at them all, try this:

​ 字符 “$( )” 告诉 shell,“替换封闭命令的结果”,这是一种称为命令替换的技术。在我们的脚本中,我们希望 shell 插入命令 date +"%x %r %Z" 的结果,该命令输出当前日期和时间。date 命令有许多功能和格式选项。要查看所有选项,请尝试执行以下命令:

1
[me@linuxbox me]$ date --help | less

Be aware that there is an older, alternate syntax for “$(command)” that uses the backtick character " ` “. This older form is compatible with the original Bourne shell (sh) but its use is discouraged in favor of the modern syntax. The bash shell fully supports scripts written for sh, so the following forms are equivalent:

​ 请注意,有一种较旧的,替代 “$(command)” 语法,它使用反引号字符 " `"。这种较旧的形式与原始 Bourne shell (sh) 兼容,但建议使用现代语法而不是它。Bash shell 完全支持为 sh 编写的脚本,因此以下形式是等价的:

1
2
$(command)
`command`

将命令的结果赋给变量 Assigning a Command’s Result to a Variable

We can also assign the results of a command to a variable:

​ 我们还可以将命令的结果赋给变量:

1
right_now="$(date +"%x %r %Z")"

We can even nest the variables (place one inside another), like this:

​ 我们甚至可以嵌套变量(将一个变量放在另一个变量内),如下所示:

1
2
right_now="$(date +"%x %r %Z")"
time_stamp="Updated on $right_now by $USER"

An important safety tip: when performing parameter expansions or command substitutions, it is good practice to surround them in double quotes to prevent unwanted word splitting in case the result of the expansion contains whitespace characters.

​ **一个重要的安全提示:**在进行参数展开或命令替换时,最好将其用双引号括起来,以防止展开的结果包含空格字符时发生意外的单词分割。

常量 Constants

As the name variable suggests, the content of a variable is subject to change. This means that it is expected that during the execution of our script, a variable may have its content modified by something the script does.

​ 正如变量的名称所暗示的,变量的内容是可变的。这意味着在脚本执行期间,变量的内容可能会被脚本执行的某些操作修改。

On the other hand, there may be values that, once set, should never be changed. These are called constants. This is a common idea in programming. Most programming languages have special facilities to support values that are not allowed to change. Bash also has this facility but it is rarely used. Instead, if a value is intended to be a constant, it is given an uppercase name to remind the programmer that it should be considered a constant even if it’s not being enforced.

​ 另一方面,可能存在一些一旦设置就不应该更改的值。这些被称为常量。这是编程中的一个常见概念。大多数编程语言都有特殊的功能来支持不允许更改的值。Bash 也有这样的功能,但很少被使用。相反,如果一个值被认为是常量,它会被赋予一个大写的名称,以提醒程序员它应该被视为常量,即使它没有被强制执行。

Environment variables are usually considered constants since they are rarely changed. Like constants, environment variables are given uppercase names by convention. In the scripts that follow, we will use this convention - uppercase names for constants and lowercase names for variables.

​ 环境变量通常被认为是常量,因为它们很少改变。与常量一样,按照约定,环境变量使用大写名称。在接下来的脚本中,我们将使用这个约定 - 常量使用大写名称,变量使用小写名称。

So, applying everything we know, our program looks like this:

​ 因此,根据我们所知,我们的程序如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# sysinfo_page - A script to produce an HTML file

title="System Information for $HOSTNAME"
RIGHT_NOW="$(date +"%x %r %Z")"
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

cat <<- _EOF_
    <html>
    <head>
        <title>
        $title
        </title>
    </head>

    <body>
    <h1>$title</h1>
    <p>$TIME_STAMP</p>
    </body>
    </html>
_EOF_

6 - shell函数

shell函数 Shell Functions

https://linuxcommand.org/lc3_wss0060.php

As programs get longer and more complex, they become more difficult to design, code, and maintain. As with any large endeavor, it is often useful to break a single, large task into a series of smaller tasks.

​ 随着程序变得越来越长和复杂,设计、编码和维护就变得更加困难。与任何大型任务一样,将一个单一的、庞大的任务拆分为一系列较小的任务通常是有用的。

In this lesson, we will begin to break our single monolithic script into a number of separate functions.

​ 在本课中,我们将开始将我们的单一的庞大脚本拆分为多个独立的函数。

To get familiar with this idea, let’s consider the description of an everyday task – going to the market to buy food. Imagine that we were going to describe the task to a man from Mars.

​ 为了熟悉这个概念,让我们考虑一个日常任务的描述 - 去市场买食物。想象一下,我们要将这个任务描述给来自火星的人。

Our first top-level description might look like this:

​ 我们的第一个顶层描述可能是这样的:

  1. Leave house
  2. Drive to market
  3. Park car
  4. Enter market
  5. Purchase food
  6. Drive home
  7. Park car
  8. Enter house
  9. 离开房子
  10. 开车去市场
  11. 停车
  12. 进入市场
  13. 购买食物
  14. 开车回家
  15. 停车
  16. 进入房子

This description covers the overall process of going to the market; however a man from Mars will probably require additional detail. For example, the “Park car” sub task could be described as follows:

​ 这个描述涵盖了去市场的整个过程;然而,来自火星的人可能需要额外的细节。例如,“停车”子任务可以描述如下:

  1. Find parking space
  2. Drive car into space
  3. Turn off motor
  4. Set parking brake
  5. Exit car
  6. Lock car
  7. 寻找停车位
  8. 将车开入停车位
  9. 关闭发动机
  10. 设置驻车制动器
  11. 离开车辆
  12. 锁车

Of course the task “Turn off motor” has a number of steps such as “turn off ignition” and “remove key from ignition switch,” and so on.

​ 当然,“关闭发动机”这个任务还有许多步骤,比如“关闭点火开关”和“取下点火开关上的钥匙”,等等。

This process of identifying the top-level steps and developing increasingly detailed views of those steps is called top-down design. This technique allows us to break large complex tasks into many small, simple tasks.

​ 这个确定顶层步骤并开发逐渐详细的视图的过程被称为自顶向下设计。这种技术使我们能够将大型复杂任务拆分为许多小而简单的任务。

As our script continues to grow, we will use top down design to help us plan and code our script.

​ 随着我们的脚本不断增长,我们将使用自顶向下设计来帮助我们规划和编码脚本。

If we look at our script’s top-level tasks, we find the following list:

​ 如果我们查看脚本的顶层任务,我们会发现以下列表:

  1. Open page
  2. Open head section
  3. Write title
  4. Close head section
  5. Open body section
  6. Write title
  7. Write time stamp
  8. Close body section
  9. Close page
  10. 打开页面
  11. 打开头部部分
  12. 写标题
  13. 关闭头部部分
  14. 打开正文部分
  15. 写标题
  16. 写时间戳
  17. 关闭正文部分
  18. 关闭页面

All of these tasks are implemented, but we want to add more. Let’s insert some additional tasks after task 7:

​ 所有这些任务都已经实现,但我们想要添加更多。让我们在任务 7 之后插入一些额外的任务:

  1. Write time stamp
  2. Write system release info
  3. Write up-time
  4. Write drive space
  5. Write home space
  6. Close body section
  7. Close page
  8. 写时间戳
  9. 写系统发布信息
  10. 写运行时间
  11. 写驱动器空间
  12. 写主目录空间
  13. 关闭正文部分
  14. 关闭页面

It would be great if there were commands that performed these additional tasks. If there were, we could use command substitution to place them in our script like so:

​ 如果有能够执行这些额外任务的命令就好了。如果有的话,我们可以使用命令替换将它们放在我们的脚本中,像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash

# sysinfo_page - A script to produce a system information HTML file

##### Constants

TITLE="System Information for $HOSTNAME"
RIGHT_NOW="$(date +"%x %r %Z")"
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

##### Main

cat <<- _EOF_
  <html>
  <head>
      <title>$TITLE</title>
  </head>

  <body>
      <h1>$TITLE</h1>
      <p>$TIME_STAMP</p>
      $(system_info)
      $(show_uptime)
      $(drive_space)
      $(home_space)
  </body>
  </html>
_EOF_

While there are no commands that do exactly what we need, we can create them using shell functions.

​ 虽然没有确切满足我们需求的命令,但我们可以使用shell 函数来创建它们。

As we learned in lesson 2, shell functions act as “little programs within programs” and allow us to follow top-down design principles. To add the shell functions to our script, we’ll change it so:

​ 正如我们在第二课中学到的那样,shell 函数充当“程序中的小程序”,使我们能够遵循自顶向下的设计原则。为了将 shell 函数添加到我们的脚本中,我们将对它进行如下更改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/bin/bash

# sysinfo_page - A script to produce an system information HTML file

##### Constants

TITLE="System Information for $HOSTNAME"
RIGHT_NOW="$(date +"%x %r %Z")"
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

##### Functions

system_info()
{

}


show_uptime()
{

}


drive_space()
{

}


home_space()
{

}

##### Main

cat <<- _EOF_
  <html>
  <head>
      <title>$TITLE</title>
  </head>

  <body>
      <h1>$TITLE</h1>
      <p>$TIME_STAMP</p>
      $(system_info)
      $(show_uptime)
      $(drive_space)
      $(home_space)
  </body>
  </html>
_EOF_

A couple of important points about functions: First, they must appear before we attempt to use them. Second, the function body (the portions of the function between the { and } characters) must contain at least one valid command. As written, the script will not execute without error, because the function bodies are empty. The simple way to fix this is to place a return statement in each function body. After we do this, our script will execute successfully again.

​ 关于函数有几点重要的要点:首先,它们必须出现在我们尝试使用它们之前。其次,函数体(花括号 { 和 } 之间的部分)必须包含至少一条有效命令。按照当前的编写方式,脚本将无法执行而出现错误,因为函数体是空的。修复这个问题的简单方法是在每个函数体中放置一个 return 语句。在我们这样做之后,脚本将再次成功执行。

保持脚本工作正常 Keep Your Scripts Working

When we develop a program, it is is often a good practice to add a small amount of code, run the script, add some more code, run the script, and so on. This way, if we introduce a mistake into the code, it will be easier to find and correct.

​ 当我们开发一个程序时,通常最好的做法是添加一小段代码,运行脚本,再添加一些代码,运行脚本,依此类推。这样,如果我们在代码中引入了错误,就会更容易找到和纠正它。

As we add functions to your script, we can also use a technique called stubbing to help watch the logic of our script develop. Stubbing works like this: imagine that we are going to create a function called “system_info” but we haven’t figured out all of the details of its code yet. Rather than hold up the development of the script until we are finished with system_info, we just add an echo command like this:

​ 当我们向脚本添加函数时,我们还可以使用一种叫做存根的技术来帮助观察脚本逻辑的发展。存根的工作方式如下:假设我们要创建一个名为 “system_info” 的函数,但我们还没有弄清楚它的所有代码细节。与其等到我们完成 system_info 后再继续脚本的开发,我们只需添加一个 echo 命令,像这样:

1
2
3
4
5
system_info()
{
    # Temporary function stub
    echo "function system_info"
}

This way, our script will still execute successfully, even though we do not yet have a finished system_info function. We will later replace the temporary stubbing code with the complete working version.

​ 这样,即使我们还没有一个完整的 system_info 函数,我们的脚本仍然可以成功执行。稍后,我们将用完整的工作版本替换临时的存根代码。

The reason we use an echo command is so we get some feedback from the script to indicate that the functions are being executed.

​ 我们使用 echo 命令的原因是为了从脚本中获得一些反馈,以指示函数正在被执行。

Let’s go ahead and write stubs for our new functions and keep the script working.

​ 让我们继续为我们的新函数编写存根,保持脚本的正常工作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/bin/bash

# sysinfo_page - A script to produce an system information HTML file

##### Constants

TITLE="System Information for $HOSTNAME"
RIGHT_NOW="$(date +"%x %r %Z")"
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

##### Functions

system_info()
{
    # Temporary function stub
    echo "function system_info"
}


show_uptime()
{
    # Temporary function stub
    echo "function show_uptime"
}


drive_space()
{
    # Temporary function stub
    echo "function drive_space"
}


home_space()
{
    # Temporary function stub
    echo "function home_space"
}


##### Main

cat <<- _EOF_
  <html>
  <head>
      <title>$TITLE</title>
  </head>

  <body>
      <h1>$TITLE</h1>
      <p>$TIME_STAMP</p>
      $(system_info)
      $(show_uptime)
      $(drive_space)
      $(home_space)
  </body>
  </html>
_EOF_

7 - 一些真正的工作

一些真正的工作 Some Real Work

https://linuxcommand.org/lc3_wss0070.php

In this lesson, we will develop some of our shell functions and get our script to produce some useful information.

​ 在本课中,我们将开发一些 shell 函数,并使我们的脚本生成一些有用的信息。

show_uptime

The show_uptime function will display the output of the uptime command. The uptime command outputs several interesting facts about the system, including the length of time the system has been “up” (running) since its last re-boot, the number of users and recent system load.

show_uptime 函数将显示 uptime 命令的输出结果。uptime 命令输出了系统的一些有趣信息,包括系统自上次重新启动以来的运行时间,用户数量以及最近的系统负载。

1
2
[me@linuxbox me]$ uptime
9:15pm up 2 days, 2:32, 2 users, load average: 0.00, 0.00, 0.00

To get the output of the uptime command into our HTML page, we will code our shell function like this, replacing our temporary stubbing code with the finished version:

​ 为了将 uptime 命令的输出添加到我们的 HTML 页面中,我们将编写如下的 shell 函数,将临时的存根代码替换为最终版本:

1
2
3
4
5
6
7
show_uptime()
{
    echo "<h2>System uptime</h2>"
    echo "<pre>"
    uptime
    echo "</pre>"
}

As we can see, this function outputs a stream of text containing a mixture of HTML tags and command output. When the command substitution takes place in the main body of the our program, the output from our function becomes part of the here script.

​ 正如我们所看到的,该函数输出了一个包含 HTML 标记和命令输出混合的文本流。当命令替换发生在我们程序的主体部分时,函数的输出将成为 here 脚本的一部分。

drive_space

The drive_space function will use the df command to provide a summary of the space used by all of the mounted file systems.

drive_space 函数将使用 df 命令提供所有已挂载文件系统使用空间的摘要信息。

1
2
3
4
5
6
[me@linuxbox me]$ df
Filesystem   1k-blocks      Used Available Use% Mounted on
/dev/hda2       509992    225772    279080  45% /
/dev/hda1        23324      1796     21288   8% /boot
/dev/hda3     15739176   1748176  13832360  12% /home
/dev/hda5      3123888   3039584     52820  99% /usr

In terms of structure, the drive_space function is very similar to the show_uptime function:

​ 在结构上,drive_space 函数与 show_uptime 函数非常相似:

1
2
3
4
5
6
7
drive_space()
{
    echo "<h2>Filesystem space</h2>"
    echo "<pre>"
    df
    echo "</pre>"
}

home_space

The home_space function will display the amount of space each user is using in his/her home directory. It will display this as a list, sorted in descending order by the amount of space used.

home_space 函数将显示每个用户在他/她的主目录中使用的空间量。它将按照使用空间的大小降序排列,以列表形式显示。

1
2
3
4
5
6
7
8
home_space()
{
    echo "<h2>Home directory space by user</h2>"
    echo "<pre>"
    echo "Bytes Directory"
    du -s /home/* | sort -nr
    echo "</pre>"
}

Note that in order for this function to successfully execute, the script must be run by the superuser, since the du command requires superuser privileges to examine the contents of the /home directory.

​ 请注意,为了使该函数成功执行,脚本必须以超级用户身份运行,因为 du 命令需要超级用户权限来检查 /home 目录的内容。

system_info

We’re not ready to finish the system_info function yet. In the meantime, we will improve the stubbing code so it produces valid HTML:

​ 我们还没有准备好完成 system_info 函数。与此同时,我们将改进存根代码,以生成有效的 HTML:

1
2
3
4
5
system_info()
{
    echo "<h2>System release info</h2>"
    echo "<p>Function not yet implemented</p>"
}

8 - 流程控制 - 第一部分

流程控制 - 第一部分 Flow Control - Part 1

https://linuxcommand.org/lc3_wss0080.php

In this lesson, we will look at how to add intelligence to our scripts. So far, our project script has only consisted of a sequence of commands that starts at the first line and continues line by line until it reaches the end. Most programs do much more than this. They make decisions and perform different actions depending on conditions.

​ 在这节课中,我们将学习如何在脚本中添加智能功能。到目前为止,我们的项目脚本只是由一系列命令组成,从第一行开始顺序执行,直到结束。大多数程序要做的事情比这要复杂得多。它们根据条件进行决策并执行不同的操作。

The shell provides several commands that we can use to control the flow of execution in our program. In this lesson, we will look at the following:

​ Shell 提供了几个命令,我们可以用它们来控制程序的执行流程。在本课中,我们将学习以下内容:

  • if
  • test
  • exit

if

The first command we will look at is if. The if command is fairly simple on the surface; it makes a decision based on the exit status of a command. The if command’s syntax looks like this:

​ 我们首先学习的是 if 命令。if 命令在表面上很简单,它根据一个命令的 退出状态 做出决策。if 命令的语法如下:

1
2
3
4
5
6
7
if commands; then
    commands
[elif commands; then
    commands...]
[else
    commands]
fi

where commands is a list of commands. This is a little confusing at first glance. But before we can clear this up, we have to look at how the shell evaluates the success or failure of a command.

​ 其中 commands 是一系列命令。乍一看可能有点困惑。但在我们澄清这一点之前,我们必须了解 Shell 如何评估命令的成功或失败。

退出状态 Exit Status

Commands (including the scripts and shell functions we write) issue a value to the system when they terminate, called an exit status. This value, which is an integer in the range of 0 to 255, indicates the success or failure of the command’s execution. By convention, a value of zero indicates success and any other value indicates failure. The shell provides a parameter that we can use to examine the exit status. Here we see it in action:

​ 命令(包括我们编写的脚本和 Shell 函数)在终止时向系统发出一个值,称为退出状态。这个值是一个在 0 到 255 范围内的整数,表示命令执行的成功或失败。按照惯例,零表示成功,任何其他值表示失败。Shell 提供了一个参数,我们可以使用它来检查退出状态。下面是一个示例:

1
2
3
4
5
6
7
8
[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2

In this example, we execute the ls command twice. The first time, the command executes successfully. If we display the value of the parameter $?, we see that it is zero. We execute the ls command a second time, producing an error and examine the parameter $? again. This time it contains a 2, indicating that the command encountered an error. Some commands use different exit status values to provide diagnostics for errors, while many commands simply exit with a value of one when they fail. Man pages often include a section entitled “Exit Status,” describing what codes are used. However, a zero always indicates success.

​ 在这个示例中,我们执行了两次 ls 命令。第一次,命令成功执行。如果我们显示参数 $? 的值,会发现它是零。我们第二次执行 ls 命令,产生一个错误,并再次检查参数 $?。这次它包含了 2,表示命令遇到了一个错误。有些命令使用不同的退出状态值来提供错误的诊断信息,而许多命令在失败时只是退出状态为 1。手册页面通常包含一个名为“Exit Status”的部分,描述了使用了哪些代码。然而,零总是表示成功。

The shell provides two extremely simple builtin commands that do nothing except terminate with either a zero or one exit status. The true command always executes successfully and the false command always executes unsuccessfully:

​ Shell 提供了两个非常简单的内置命令,它们除了以零或一的退出状态终止外什么都不做。true 命令始终成功执行,而 false 命令始终执行失败:

1
2
3
4
5
6
[me@linuxbox~]$ true
[me@linuxbox~]$ echo $?
0
[me@linuxbox~]$ false
[me@linuxbox~]$ echo $?
1

We can use these commands to see how the if statement works. What the if statement really does is evaluate the success or failure of commands:

​ 我们可以使用这些命令来了解 if 语句的工作原理。if 语句的真正作用是评估命令的成功或失败:

1
2
3
4
[me@linuxbox ~]$ if true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if false; then echo "It's true."; fi
[me@linuxbox ~]$

The command echo "It's true." is executed when the command following if executes successfully, and is not executed when the command following if does not execute successfully.

​ 当 if 后面的命令成功执行时,会执行 echo "It's true." 命令,当 if 后面的命令执行失败时,不会执行 echo "It's true." 命令。

test

The test command is used most often with the if command to perform true/false decisions. The command is unusual in that it has two different syntactic forms:

test 命令最常与 if 命令一起使用以进行真/假判断。该命令有两种不同的语法形式:

1
2
3
4
5
6
7
# First form

test expression

# Second form

[ expression ]

The test command works simply. If the given expression is true, test exits with a status of zero; otherwise it exits with a status of 1.

test 命令的工作很简单。如果给定的表达式为真,则 test 以零状态退出;否则,它以 1 的状态退出。

The neat feature of test is the variety of expressions we can create. Here is an example:

test 的好处是我们可以创建多种表达式。以下是一个示例:

1
2
3
4
5
if [ -f .bash_profile ]; then
    echo "You have a .bash_profile. Things are fine."
else
    echo "Yikes! You have no .bash_profile!"
fi

In this example, we use the expression “-f .bash_profile “. This expression asks, “Is .bash_profile a file?” If the expression is true, then test exits with a zero (indicating true) and the if command executes the command(s) following the word then. If the expression is false, then test exits with a status of one and the if command executes the command(s) following the word else.

​ 在这个示例中,我们使用表达式 “-f .bash_profile"。该表达式询问:“.bash_profile 是否是一个文件?”如果表达式为真,则 test 以零状态退出(表示为真),并且 if 命令执行紧随 then 之后的命令。如果表达式为假,则 test 以状态 1 退出,并且 if 命令执行紧随 else 之后的命令。

Here is a partial list of the conditions that test can evaluate. Since test is a shell builtin, use “help test” to see a complete list.

​ 以下是 test 命令可以评估的一部分条件列表。由于 test 是一个内置命令,使用 “help test” 可以查看完整列表。

表达式 Expression描述 Description
-d file如果 file 是一个目录,则为真。
True if file is a directory.
-e file如果 file 存在,则为真。
True if file exists.
-f file如果 file 存在且是一个普通文件,则为真。
True if file exists and is a regular file.
-L file如果 file 是一个符号链接,则为真。
True if file is a symbolic link.
-r file如果 file 是一个你可读取的文件,则为真。
True if file is a file readable by you.
-w file如果 file 是一个你可写入的文件,则为真。
True if file is a file writable by you.
-x file如果 file 是一个你可执行的文件,则为真。
True if file is a file executable by you.
file1 -nt file2如果 file1 比(根据修改时间)file2 新,则为真。
True if file1 is newer than (according to modification time) file2.
file1 -ot file2如果 file1file2 旧,则为真。
True if file1 is older than file2.
-z string如果 string 为空,则为真。
True if string is empty.
-n string如果 string 不为空,则为真。
True if string is not empty.
string1 = string2如果 string1 等于 string2,则为真。
True if string1 equals string2.
string1 != string2如果 string1 不等于 string2,则为真。
True if string1 does not equal string2.

Before we go on, We need to explain the rest of the example above, since it also reveals more important ideas.

​ 在我们继续之前,我们需要解释上面示例的剩余部分,因为它还揭示了更重要的思想。

In the first line of the script, we see the if command followed by the test command, followed by a semicolon, and finally the word then. Most people choose to use the [ *expression* ] form of the test command since it’s easier to read. Notice that the spaces required between the “[” and the beginning of the expression are required. Likewise, the space between the end of the expression and the trailing “]”.

​ 在脚本的第一行中,我们看到 if 命令后面跟着 test 命令,后面是一个分号,最后是单词 then。大多数人选择使用 [ *expression* ] 形式的 test 命令,因为它更容易阅读。请注意,在 “[” 与表达式开头之间需要的空格。同样,表达式的结尾和尾随的 “]” 之间的空格也是必需的。

The semicolon is a command separator. Using it allows us to put more than one command on a line. For example:

​ 分号是一个命令分隔符。使用分号允许我们在一行上放置多个命令。例如:

1
[me@linuxbox me]$ clear; ls

will clear the screen and execute the ls command.

将清除屏幕并执行 ls 命令。

We use the semicolon as we did to allow us to put the word then on the same line as the if command, because it’s easier to read that way.

​ 我们像这样使用分号是为了让 then 这个词与 if 命令在同一行上,因为这样更容易阅读。

On the second line, there is our old friend echo. The only thing of note on this line is the indentation. Again for the benefit of readability, it is traditional to indent all blocks of conditional code; that is, any code that will only be executed if certain conditions are met. The shell does not require this; it is done to make the code easier to read.

​ 在第二行中,我们看到了我们的老朋友 echo。这行中值得注意的是缩进。为了可读性,传统上对所有的条件代码块进行缩进,也就是只有在满足特定条件时才会执行的代码块。Shell 并不要求这样做,但这样做是为了让代码更容易阅读。

In other words, we could write the following and get the same results:

​ 换句话说,我们可以编写以下代码并获得相同的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Preferred form

if [ -f .bash_profile ]; then
    echo "You have a .bash_profile. Things are fine."
else
    echo "Yikes! You have no .bash_profile!"
fi

# Another alternate form

if [ -f .bash_profile ]
then echo "You have a .bash_profile. Things are fine."
else echo "Yikes! You have no .bash_profile!"
fi

exit

In order to be good script writers, we must set the exit status when our scripts finish. To do this, use the exit command. The exit command causes the script to terminate immediately and set the exit status to whatever value is given as an argument. For example:

​ 为了成为良好的脚本编写者,我们必须在脚本完成时设置退出状态。要做到这一点,请使用 exit 命令。exit 命令会立即终止脚本,并将退出状态设置为作为参数给出的值。例如:

1
exit 0

exits our script and sets the exit status to 0 (success), whereas

退出我们的脚本,并将退出状态设置为 0(成功),而

1
exit 1

exits your script and sets the exit status to 1 (failure).

退出你的脚本,并将退出状态设置为 1(失败)。

测试是否为 Root 用户 Testing for Root

When we last left our script, we required that it be run with superuser privileges. This is because the home_space function needs to examine the size of each user’s home directory, and only the superuser is allowed to do that.

​ 当我们上次离开我们的脚本时,我们要求它在超级用户权限下运行。这是因为 home_space 函数需要检查每个用户的主目录大小,而只有超级用户才允许这样做。

But what happens if a regular user runs our script? It produces a lot of ugly error messages. What if we could put something in the script to stop it if a regular user attempts to run it?

​ 但是如果普通用户运行我们的脚本会发生什么?它会产生很多丑陋的错误消息。如果我们能在脚本中添加一些内容,以便在普通用户尝试运行它时停止它会怎么样?

The id command can tell us who the current user is. When executed with the “-u” option, it prints the numeric user id of the current user.

id 命令可以告诉我们当前用户是谁。当使用 “-u” 选项执行时,它会打印当前用户的数字用户 ID。

1
2
3
4
5
6
7
[me@linuxbox me]$ id -u
501
[me@linuxbox me]$ sudo -i
Password for me:
[root@linuxbox ~]# id -u
0
[root@linuxbox ~]# exit

If the superuser executes id -u, the command will output “0.” This fact can be the basis of our test:

​ 如果超级用户执行 id -u,该命令将输出 “0”。这个事实可以成为我们的测试基础:

1
2
3
if [ "$(id -u)" = "0" ]; then
    echo "superuser"
fi

In this example, if the output of the command id -u is equal to the string “0”, then print the string “superuser.”

​ 在这个示例中,如果 id -u 命令的输出等于字符串 “0”,那么打印字符串 “superuser”。

While this code will detect if the user is the superuser, it does not really solve the problem yet. We want to stop the script if the user is not the superuser, so we will code it like so:

​ 虽然这段代码会检测用户是否是超级用户,但它还没有真正解决问题。我们想要在用户不是超级用户时停止脚本,所以我们将对其进行编码:

1
2
3
4
if [ "$(id -u)" != "0" ]; then
    echo "You must be the superuser to run this script" >&2
    exit 1
fi

With this code, if the output of the id -u command is not equal to “0”, then the script prints a descriptive error message, exits, and sets the exit status to 1, indicating to the operating system that the script executed unsuccessfully.

​ 使用这段代码,如果 id -u 命令的输出不等于 “0”,则脚本会打印一个描述性的错误消息,退出并将退出状态设置为 1,表示脚本执行失败,以通知操作系统。

Notice the “>&2” at the end of the echo command. This is another form of I/O direction. We will often see this in routines that display error messages. If this redirection were not done, the error message would go to standard output. With this redirection, the message is sent to standard error. Since we are executing our script and redirecting its standard output to a file, we want the error messages separated from the normal output.

​ 请注意 echo 命令末尾的 “>&2"。这是另一种 I/O 重定向形式。我们经常在显示错误消息的程序中看到这种形式。如果没有进行此重定向,错误消息将被发送到标准输出。通过进行此重定向,消息将被发送到标准错误。由于我们正在执行脚本并将其标准输出重定向到文件,我们希望错误消息与正常输出分开。

We could put this routine near the beginning of our script so it has a chance to detect a possible error before things get under way, but in order to run this script as an ordinary user, we will use the same idea and modify the home_space function to test for proper privileges instead, like so:

​ 我们可以将此代码放在脚本的开头附近,以便在开始之前有机会检测可能的错误,但为了以普通用户身份运行此脚本,我们将使用相同的思路,并修改 home_space 函数以测试适当的权限,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function home_space {
    # Only the superuser can get this information

    if [ "$(id -u)" = "0" ]; then
        echo "<h2>Home directory space by user</h2>"
        echo "<pre>"
        echo "Bytes Directory"
            du -s /home/* | sort -nr
        echo "</pre>"
    fi

}   # end of home_space

This way, if an ordinary user runs the script, the troublesome code will be passed over, rather than executed and the problem will be solved.

​ 这样,如果普通用户运行脚本,有问题的代码将被跳过,而不是执行,问题就得到解决了。

进一步阅读 Further Reading

  • Chapter 27 in The Linux Command Line covers this topic in more detail including the [[ ]] construct, the modern replacement for the test command, and the (( )) construct for performing arithmetic truth tests.
  • 《Linux 命令行》的第 27 章详细介绍了这个主题,包括 [[ ]] 结构,作为 test 命令的现代替代品,以及用于执行算术真值测试的 (( )) 结构。
  • To learn more about stylistic conventions and best coding practices for bash scripts, see the Coding Standards adventure.
  • 要了解有关 bash 脚本的样式约定和最佳编码实践的更多信息,请参阅 Coding Standards 探险。

9 - 远离麻烦

远离麻烦 - Stay Out of Trouble

https://linuxcommand.org/lc3_wss0090.php

Now that our scripts are getting a little more complicated, Let’s look at some common mistakes that we might run into. To do this, we’ll create the following script called trouble.bash. Be sure to enter it exactly as written.

现在我们的脚本变得有点复杂了,让我们看看可能遇到的一些常见错误。为此,我们将创建以下名为 trouble.bash 的脚本。请确保按照原样输入。

1
2
3
4
5
6
7
8
9
#!/bin/bash

number=1

if [ $number = "1" ]; then
    echo "Number equals 1"
else
    echo "Number does not equal 1"
fi

When we run this script, it should output the line “Number equals 1” because, well, number equals 1. If we don’t get the expected output, we need to check our typing; we’ve made a mistake.

​ 当我们运行这个脚本时,它应该输出 “Number equals 1”,因为,嗯,number 等于 1。如果我们没有得到预期的输出,我们需要检查我们的输入,我们肯定犯了个错误。

空变量 Empty Variables

Let’s edit the script to change line 3 from:

​ 让我们编辑脚本,将第 3 行从:

1
number=1

to:

改为:

1
number=

and run the script again. This time we should get the following:

​ 然后再次运行脚本。这次我们应该得到以下结果:

1
2
3
[me@linuxbox me]$ ./trouble.bash
/trouble.bash: [: =: unary operator expected.
Number does not equal 1

As we can see, bash displayed an error message when we ran the script. We might think that by removing the “1” on line 3 it created a syntax error on line 3, but it didn’t. Let’s look at the error message again:

​ 如我们所见,当我们运行脚本时,bash 显示了一个错误消息。我们可能认为,通过在第 3 行删除 “1”,它在第 3 行创建了一个语法错误,但实际上并非如此。让我们再次查看错误消息:

1
./trouble.bash: [: =: unary operator expected

We can see that ./trouble.bash is reporting the error and the error has to do with “[”. Remember that “[” is an abbreviation for the test shell builtin. From this we can determine that the error is occurring on line 5 not line 3.

​ 我们可以看到 ./trouble.bash 报告了错误,并且该错误与 “[” 有关。请记住,"[" 是 test 内置命令的缩写。从这里我们可以确定错误发生在第 5 行而不是第 3 行。

First, to be clear, there is nothing wrong with line 3. number= is perfectly good syntax. We sometimes want to set a variable’s value to nothing. We can confirm the validity of this by trying it on the command line:

​ 首先,为了明确,第 3 行没有问题。number= 是完全正确的语法。有时我们想将变量的值设置为空。我们可以通过在命令行上尝试来确认其有效性:

1
2
[me@linuxbox me]$ number=
[me@linuxbox me]$

See, no error message. So what’s wrong with line 5? It worked before.

​ 看,没有错误消息。那么第 5 行有什么问题?它之前是有效的。

To understand this error, we have to see what the shell sees. Remember that the shell spends a lot of its life expanding text. In line 5, the shell expands the value of number where it sees $number. In our first try (when number=1), the shell substituted 1 for $number like so:

​ 要理解这个错误,我们必须看到 shell 看到的内容。请记住,shell 在其生命周期中花费了很多时间来展开文本。在第 5 行,shell 在 $number 处展开了 number 的值。在我们第一次尝试(number=1)中,shell 将 1 替换为 $number,如下所示:

1
if [ 1 = "1" ]; then

However, when we set number to nothing (number=), the shell saw this after the expansion:

​ 然而,当我们将 number 设置为空(number=)时,shell 在展开之后看到了这个:

1
if [ = "1" ]; then

which is an error. It also explains the rest of the error message we received. The “=” is a binary operator; that is, it expects two items to operate upon - one on each side. What the shell is trying to tell us is that there is only one item and there should be a unary operator (like “!”) that only operates on a single item.

这是一个错误。这也解释了我们收到的错误消息的其他部分。"=" 是一个二元运算符;也就是说,它期望两个操作数 - 每个操作数在一边。Shell 试图告诉我们的是,只有一个操作数,并且应该有一个仅对单个操作数操作的一元运算符(如 “!")。

To fix this problem, change line 5 to read:

​ 要解决这个问题,将第 5 行改为以下内容:

1
if [ "$number" = "1" ]; then

Now when the shell performs the expansion it will see:

​ 现在当 shell 执行展开时,将看到以下内容:

1
if [ "" = "1" ]; then

which correctly expresses our intent.

这样正确地表达了我们的意图。

This brings up two important things to remember when we are writing scripts. We need to consider what happens if a variable is set to equal nothing and we should always put double quotes around parameters that undergo expansion.

​ 这带来了两个重要的事情,我们在编写脚本时需要记住。我们需要考虑如果一个变量被设置为空会发生什么,而且我们应该始终在经过展开的参数周围加上双引号。

缺少引号 Missing Quotes

Edit line 6 to remove the trailing quote from the end of the line:

​ 编辑第 6 行,将行末的引号删除:

1
 echo "Number equals 1

and run the script again. We should get this:

然后再次运行脚本。我们应该得到以下结果:

1
2
3
4
[me@linuxbox me]$ ./trouble.bash
./trouble.bash: line 8:
unexpected EOF while looking for matching "
./trouble.bash: line 10 syntax error: unexpected end of file

Here we have another instance of a mistake in one line causing a problem later in the script. What happened in this case was that the shell kept looking for the closing quotation mark to determine where the end of the string is, but ran off the end of the file before it found it.

​ 这里我们又遇到了一行中的错误导致脚本后面出现问题的情况。在这种情况下,发生的情况是 shell 一直在寻找结束引号以确定字符串的结尾位置,但在找到之前已经到达了文件末尾。

These errors can be a real pain to track down in a long script. This is one reason we should test our scripts frequently while we are writing so there is less new code to test. Also, using a text editor with syntax highlighting makes these bugs easier to find.

​ 在一个很长的脚本中追踪这些错误可能非常困难和令人沮丧。这就是为什么在编写过程中我们应该经常测试我们的脚本,这样就会有较少的新代码需要测试。此外,使用带有语法高亮的文本编辑器可以更容易地找到这些错误。

隔离问题 Isolating Problems

Finding bugs in scripts can sometimes be very difficult and frustrating. Here are a couple of techniques that are useful:

​ 在脚本中找到错误有时可能非常困难和令人沮丧。以下是两种有用的技巧:

Isolate blocks of code by “commenting them out.” This trick involves putting comment characters at the beginning of lines of code to stop the shell from reading them. We can do this to a block of code to see if a particular problem goes away. By doing this, we can isolate which part of a program is causing (or not causing) a problem.

通过"注释掉"来隔离代码块。 这个技巧涉及将注释字符放在代码行的开头,以阻止 shell 读取它们。我们可以这样做来对一块代码进行注释,以查看特定的问题是否消失。通过这样做,我们可以确定程序的哪一部分导致(或不导致)问题。

For example, when we were looking for our missing quotation we could have done this:

​ 例如,在查找缺少引号时,我们可以这样做:

1
2
3
4
5
6
7
8
9
#!/bin/bash

number=1

if [ $number = "1" ]; then
    echo "Number equals 1
#else
#   echo "Number does not equal 1"
fi

By commenting out the else clause and running the script, we could show that the problem was not in the else clause even though the error message suggested that it was.

​ 通过注释掉 else 语句并运行脚本,我们可以证明问题不在 else 语句中,即使错误消息暗示问题在其中。

Use echo commands to verify assumptions. As we gain experience tracking down bugs, we will discover that bugs are often not where we first expect to find them. A common problem will be that we will make a false assumption about the performance of our program. A problem will develop at a certain point in the program and we assume the problem is there. This is often incorrect. To combat this, we can place echo commands in the code while we are debugging, to produce messages that confirm the program is doing what is expected. There are two kinds of messages that we can insert.

使用 echo 命令验证假设。 随着我们在调试中积累经验,我们会发现错误通常不在我们最初期望的地方。一个常见的问题是我们对程序的性能做出了错误的假设。问题将在程序的某个特定点出现,并且我们会认为问题就在那里。这通常是不正确的。为了解决这个问题,我们可以在调试过程中在代码中插入 echo 命令,以生成确认程序按预期运行的消息。我们可以插入两种类型的消息。

The first type simply announces that we have reached a certain point in the program. We saw this in our earlier discussion on stubbing. It is useful to know that program flow is happening the way we expect.

​ 第一种类型只是宣布我们已经到达程序的某个特定点。我们在之前讨论的占位符中看到过这一点。了解程序流程是否按我们的预期进行非常有用。

The second type displays the value of a variable (or variables) used in a calculation or test. We will often find that a portion of a program will fail because something that we assumed was correct earlier in the program is, in fact, incorrect and is causing our program to fail later on.

​ 第二种类型显示在计算或测试中使用的变量(或变量)的值。我们经常发现,程序的一部分将因为我们在程序早期假设正确的东西实际上是不正确的而失败,并且导致我们的程序在后面失败。

观察脚本运行 Watching Our Script Run

It is possible to have bash show us what it is doing when we run our script. To do this, add a “-x” to the first line of the script, like this:

​ 在运行脚本时,可以让 bash 显示它在做什么。要做到这一点,将 “-x” 添加到脚本的第一行,如下所示:

1
#!/bin/bash -x

Now, when we run the script, bash will display each line (with expansions performed) as it executes it. This technique is called tracing. Here is what it looks like:

​ 现在,当我们运行脚本时,bash 将显示每行执行时的文本(包括展开)。这个技术被称为跟踪。下面是它的样子:

1
2
3
4
5
[me@linuxbox me]$ ./trouble.bash
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number equals 1'
Number equals 1

Alternately, we can use the set command within the script to turn tracing on and off. Use set -x to turn tracing on and set +x to turn tracing off. For example.:

​ 或者,我们可以在脚本中使用 set 命令来打开和关闭跟踪。使用 set -x 打开跟踪,使用 set +x 关闭跟踪。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash

number=1

set -x
if [ $number = "1" ]; then
    echo "Number equals 1"
else
    echo "Number does not equal 1"
fi
set +x

10 - 键盘输入和算术

键盘输入和算术 Keyboard Input and Arithmetic

https://linuxcommand.org/lc3_wss0100.php

Up to now, our scripts have not been interactive. That is, they did not accept any input from the user. In this lesson, we will see how our scripts can ask questions, and get and use responses.

​ 到目前为止,我们的脚本还没有交互功能。也就是说,它们没有接受用户的任何输入。在本课中,我们将看到如何让我们的脚本提出问题,并获取和使用回答。

read

To get input from the keyboard, we use the read command. The read command takes input from the keyboard and assigns it to a variable. Here is an example:

​ 要从键盘获取输入,我们使用 read 命令。read 命令从键盘获取输入并将其赋值给一个变量。以下是一个示例:

1
2
3
4
5
#!/bin/bash

echo -n "Enter some text > "
read text
echo "You entered: $text"

As we can see, we displayed a prompt on line 3. Note that “-n” given to the echo command causes it to keep the cursor on the same line; i.e., it does not output a linefeed at the end of the prompt.

​ 正如我们所看到的,我们在第 3 行显示了一个提示符。注意,echo 命令后面的 “-n” 使其保持在同一行上;即不在提示符的末尾输出换行符。

Next, we invoke the read command with “text” as its argument. What this does is wait for the user to type something followed by the Enter key and then assign whatever was typed to the variable text.

​ 接下来,我们使用 “text” 作为 read 命令的参数来调用它。这样做的作用是等待用户输入一些内容,然后按回车键,然后将输入的内容分配给变量 text

Here is the script in action:

​ 以下是脚本的实际运行情况:

1
2
3
[me@linuxbox me]$ read_demo.bash
Enter some text > this is some text
You entered: this is some text

If we don’t give the read command the name of a variable to assign its input, it will use the environment variable REPLY.

​ 如果我们没有给 read 命令指定要分配其输入的变量的名称,它将使用环境变量 REPLY

The read command has several command line options. The three most interesting ones are -p, -t and -s.

read 命令有几个命令行选项。其中三个最有趣的选项是 -p-t-s

The -p option allows us to specify a prompt to precede the user’s input. This saves the extra step of using an echo to prompt the user. Here is the earlier example rewritten to use the -p option:

-p 选项允许我们指定一个提示符,在用户的输入之前显示。这样可以省去使用 echo 提示用户的额外步骤。以下是重写为使用 -p 选项的早期示例:

1
2
3
4
#!/bin/bash

read -p "Enter some text > " text
echo "You entered: $text"

The -t option followed by a number of seconds provides an automatic timeout for the read command. This means that the read command will give up after the specified number of seconds if no response has been received from the user. This option could be used in the case of a script that must continue (perhaps resorting to a default response) even if the user does not answer the prompts. Here is the -t option in action:

-t 选项后面跟一个秒数,为 read 命令提供了自动超时功能。这意味着如果在指定的秒数内没有从用户那里收到响应,read 命令将放弃等待。在脚本必须继续执行的情况下(可能会采用默认响应),即使用户没有回答提示,也可以使用此选项。以下是 -t 选项的示例:

1
2
3
4
5
6
7
8
#!/bin/bash

echo -n "Hurry up and type something! > "
if read -t 3 response; then
    echo "Great, you made it in time!"
else
    echo "Sorry, you are too slow!"
fi

The -s option causes the user’s typing not to be displayed. This is useful when we are asking the user to type in a password or other confidential information.

-s 选项使用户的输入不显示在屏幕上。这在要求用户输入密码或其他机密信息时非常有用。

算术 Arithmetic

Since we are working on a computer, it is natural to expect that it can perform some simple arithmetic. The shell provides features for integer arithmetic.

​ 由于我们在使用计算机,自然可以期望它能执行一些简单的算术运算。Shell 提供了整数算术的功能。

What’s an integer? That means whole numbers like 1, 2, 458, -2859. It does not mean fractional numbers like 0.5, .333, or 3.1415. To deal with fractional numbers, there is a separate program called bc which provides an arbitrary precision calculator language. It can be used in shell scripts, but is beyond the scope of this tutorial.

​ 什么是整数?那意味着像 1、2、458、-2859 这样的整数。它不包括像 0.5、.333 或 3.1415 这样的小数。为了处理小数,有一个名为 bc 的单独程序,它提供了一个任意精度的计算器语言。它可以在 shell 脚本中使用,但超出了本教程的范围。

Let’s say we want to use the command line as a primitive calculator. We can do it like this:

​ 假设我们想要将命令行用作基本计算器。我们可以这样做:

1
[me@linuxbox me]$ echo $((2+2))

When we surround an arithmetic expression with the double parentheses, the shell will perform arithmetic expansion.

​ 当我们用双括号括起算术表达式时,Shell 将执行算术展开。

Notice that whitespace is ignored:

​ 注意,空格被忽略:

1
2
3
4
5
6
[me@linuxbox me]$ echo $((2+2))
4
[me@linuxbox me]$ echo $(( 2+2 ))
4
[me@linuxbox me]$ echo $(( 2 + 2 ))
4

The shell can perform a variety of common (and not so common) arithmetic operations. Here is an example:

​ Shell 可以执行各种常见(和不常见)的算术运算。以下是一个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

first_num=0
second_num=0

read -p "Enter the first number --> " first_num
read -p "Enter the second number -> " second_num

echo "first number + second number = $((first_num + second_num))"
echo "first number - second number = $((first_num - second_num))"
echo "first number * second number = $((first_num * second_num))"
echo "first number / second number = $((first_num / second_num))"
echo "first number % second number = $((first_num % second_num))"
echo "first number raised to the"
echo "power of the second number   = $((first_num ** second_num))"

Notice how the leading “$” is not needed to reference variables inside the arithmetic expression such as “first_num + second_num”.

​ 注意在算术表达式中不需要前导的 “$” 来引用变量,例如 “first_num + second_num"。

Try this program out and watch how it handles division (remember, this is integer division) and how it handles large numbers. Numbers that get too large overflow like the odometer in a car when it exceeds the number of miles it was designed to count. It starts over but first it goes through all the negative numbers because of how integers are represented in memory. Division by zero (which is mathematically invalid) does cause an error.

​ 尝试运行此程序,观察它如何处理除法(记住,这是整数除法)以及如何处理大数。当数字变得太大时,它们会溢出,就像汽车上的里程表超过设计时的里程数时一样。它会重新开始,但首先会经过所有的负数,这是由于整数在内存中的表示方式。除零(在数学上是无效的)会导致错误。

The first four operations, addition, subtraction, multiplication and division, are easily recognized but the fifth one may be unfamiliar. The “%” symbol represents remainder (also known as modulo). This operation performs division but instead of returning a quotient like division, it returns the remainder. While this might not seem very useful, it does, in fact, provide great utility when writing programs. For example, when a remainder operation returns zero, it indicates that the first number is an exact multiple of the second. This can be very handy:

​ 前四个操作,加法、减法、乘法和除法,很容易理解,但第五个可能不太熟悉。"%” 符号表示余数(也称为模运算)。此操作执行除法,但与除法返回商不同,它返回余数。虽然这可能看起来并不非常有用,但实际上在编写程序时提供了很大的实用性。例如,当余数操作返回零时,它表示第一个数字是第二个数字的精确倍数。这非常方便:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

number=0

read -p "Enter a number > " number

echo "Number is $number"
if [ $((number % 2)) -eq 0 ]; then
    echo "Number is even"
else
    echo "Number is odd"
fi 

Or, in this program that formats an arbitrary number of seconds into hours and minutes:

​ 或者,在这个程序中,将任意秒数格式化为小时和分钟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

seconds=0

read -p "Enter number of seconds > " seconds

hours=$((seconds / 3600))
seconds=$((seconds % 3600))
minutes=$((seconds / 60))
seconds=$((seconds % 60))

echo "$hours hour(s) $minutes minute(s) $seconds second(s)"

11 - 流程控制 - 第二部分

流程控制 - 第二部分 Flow Control - Part 2

https://linuxcommand.org/lc3_wss0110.php

Hold on to your hats. This lesson is going to be a big one!

​ 准备好了吗?这一课将是一大篇章!

更多的分支 More Branching

In the previous lesson on flow control we learned about the if command and how it is used to alter program flow based on a command’s exit status. In programming terms, this type of program flow is called branching because it is like traversing a tree. We come to a fork in the tree and the evaluation of a condition determines which branch we take.

​ 在前一节的流程控制课程中,我们学习了 if 命令以及如何根据命令的退出状态来改变程序流程。从编程的角度来说,这种类型的程序流程被称为分支,因为它类似于遍历树。我们来到树中的一个分叉,条件的评估决定了我们走哪条分支。

There is a second and more complex kind of branching called a case. A case is multiple-choice branch. Unlike the simple branch, where we take one of two possible paths, a case supports several possible outcomes based on the evaluation of a value.

​ 还有一种更复杂的分支叫做case。Case 是多选分支。与简单的分支不同,我们可以基于某个值的评估来支持多种可能的结果。

We can construct this type of branch with multiple if statements. In the example below, we evaluate some input from the user:

​ 我们可以使用多个 if 语句构建这种类型的分支。在下面的示例中,我们评估用户输入的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

read -p "Enter a number between 1 and 3 inclusive > " character
if [ "$character" = "1" ]; then
    echo "You entered one."
elif [ "$character" = "2" ]; then
    echo "You entered two."
elif [ "$character" = "3" ]; then
    echo "You entered three."
else
    echo "You did not enter a number between 1 and 3."
fi

Not very pretty.

​ 不是很漂亮。

Fortunately, the shell provides a more elegant solution to this problem. It provides a built-in command called case, which can be used to construct an equivalent program:

​ 幸运的是,Shell 提供了一个更优雅的解决方案。它提供了一个内置命令叫做 case,可以用来构建等效的程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

read -p "Enter a number between 1 and 3 inclusive > " character
case $character in
    1 ) echo "You entered one."
        ;;
    2 ) echo "You entered two."
        ;;
    3 ) echo "You entered three."
        ;;
    * ) echo "You did not enter a number between 1 and 3."
esac

The case command has the following form:

case 命令的形式如下:

1
2
3
case word in
    patterns ) commands ;;
esac

case selectively executes statements if word matches a pattern. We can have any number of patterns and statements. Patterns can be literal text or wildcards. We can have multiple patterns separated by the “|” character. Here is a more advanced example to show how this works:

case 命令根据 word 是否匹配某个模式来选择性地执行语句。我们可以有任意数量的模式和语句。模式可以是文字字面值或通配符。我们可以用 “|” 字符分隔多个模式。下面是一个更高级的示例,展示了它是如何工作的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

read -p "Type a digit or a letter > " character
case $character in
                                # Check for letters
    [[:lower:]] | [[:upper:]] ) echo "You typed the letter $character"
                                ;;

                                # Check for digits
    [0-9] )                     echo "You typed the digit $character"
                                ;;

                                # Check for anything else
    * )                         echo "You did not type a letter or a digit"
esac

Notice the special pattern “*”. This pattern will match anything, so it is used to catch cases that did not match previous patterns. Inclusion of this pattern at the end is wise, as it can be used to detect invalid input.

​ 注意特殊的模式 “*"。这个模式将匹配任何内容,因此用于捕捉未匹配前面模式的情况。在最后加入这个模式是明智的,因为它可以用来检测无效的输入。

循环 Loops

The final type of program flow control we will discuss is called looping. Looping is repeatedly executing a section of a program based on the exit status of a command. The shell provides three commands for looping: while, until and for. We are going to cover while and until in this lesson and for in a upcoming lesson.

​ 我们将讨论的最后一种程序流程控制类型称为循环。循环是根据命令的退出状态重复执行程序的一部分。Shell 提供了三个用于循环的命令:whileuntilfor。我们将在本课程中讨论 whileuntil,而将 for 留在即将到来的课程中讲解。

The while command causes a block of code to be executed over and over, as long as the exit status of a specified command is true. Here is a simple example of a program that counts from zero to nine:

while 命令使得一段代码块根据指定命令的退出状态反复执行。以下是一个简单的示例,该程序从零计数到九:

1
2
3
4
5
6
7
#!/bin/bash

number=0
while [ "$number" -lt 10 ]; do
    echo "Number = $number"
    number=$((number + 1))
done

On line 3, we create a variable called number and initialize its value to 0. Next, we start the while loop. As we can see, we have specified a command that tests the value of number. In our example, we test to see if number has a value less than 10.

​ 在第3行,我们创建一个名为 number 的变量,并将其初始值设置为0。接下来,我们开始 while 循环。正如我们所见,我们指定了一个命令来测试 number 的值。在我们的示例中,我们测试 number 是否小于10。

Notice the word do on line 4 and the word done on line 7. These enclose the block of code that will be repeated as long as the exit status remains zero.

​ 注意第4行的 do 和第7行的 done。它们包围了将重复的代码块,只要退出状态保持为零,就会重复执行。

In most cases, the block of code that repeats must do something that will eventually change the exit status, otherwise we will have what is called an endless loop; that is, a loop that never ends.

​ 在大多数情况下,重复执行的代码块必须执行一些最终会改变退出状态的操作,否则我们将会得到所谓的无限循环;也就是说,一个永远不会结束的循环。

In the example, the repeating block of code outputs the value of number (the echo command on line 5) and increments number by one on line 6. Each time the block of code is completed, the test command’s exit status is evaluated again. After the tenth iteration of the loop, number has been incremented ten times and the test command will terminate with a non-zero exit status. At that point, the program flow resumes with the statement following the word done. Since done is the last line of our example, the program ends.

​ 在这个示例中,重复执行的代码块输出 number 的值(第5行的 echo 命令),并在第6行将 number 增加一。每次完成代码块后,测试命令的退出状态会再次评估。在循环的第十次迭代之后,number 已经增加了十次,test 命令将以非零退出状态终止。此时,程序流程将恢复到单词 done 后面的语句。由于 done 是我们示例的最后一行,程序结束了。

The until command works exactly the same way, except the block of code is repeated as long as the specified command’s exit status is false. In the example below, notice how the expression given to the test command has been changed from the while example to achieve the same result:

until 命令的工作方式完全相同,只是重复执行的代码块是在指定命令的退出状态为假时重复执行。在下面的示例中,注意与 while 示例相比,给 test 命令指定的表达式已经改变,但结果是相同的:

1
2
3
4
5
6
7
#!/bin/bash

number=0
until [ "$number" -ge 10 ]; do
    echo "Number = $number"
    number=$((number + 1))
done

构建菜单 Building a Menu

A common user interface for text-based programs is a menu. A menu is a list of choices from which the user can pick.

​ 文本界面程序中常见的用户界面是菜单。菜单是一个列表,用户可以从中选择。

In the example below, we use our new knowledge of loops and cases to build a simple menu driven application:

​ 在下面的示例中,我们利用循环和 case 的新知识构建一个简单的菜单驱动应用程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

selection=
until [ "$selection" = "0" ]; do
    echo "
    PROGRAM MENU
    1 - Display free disk space
    2 - Display free memory

    0 - exit program
"
    echo -n "Enter selection: "
    read selection
    echo ""
    case $selection in
        1 ) df ;;
        2 ) free ;;
        0 ) exit ;;
        * ) echo "Please enter 1, 2, or 0"
    esac
done

The purpose of the until loop in this program is to re-display the menu each time a selection has been completed. The loop will continue until selection is equal to 0, the “exit” choice. Notice how we defend against entries from the user that are not valid choices.

​ 这个程序中的 until 循环的目的是在完成选择后重新显示菜单。循环将继续,直到选择等于0,即"退出"选项。注意我们如何防止用户输入无效的选择。

To make this program better looking when it runs, we can enhance it by adding a function that asks the user to press the Enter key after each selection has been completed, and clears the screen before the menu is displayed again. Here is the enhanced example:

​ 为了使程序在运行时更美观,我们可以增强它,添加一个函数,在每次选择完成后要求用户按下回车键,并在显示菜单之前清屏。以下是增强后的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash

press_enter()
{
    echo -en "\nPress Enter to continue"
    read
    clear
}

selection=
until [ "$selection" = "0" ]; do
    echo "
    PROGRAM MENU
    1 - display free disk space
    2 - display free memory

    0 - exit program
"
    echo -n "Enter selection: "
    read selection
    echo ""
    case $selection in
        1 ) df ; press_enter ;;
        2 ) free ; press_enter ;;
        0 ) exit ;;
        * ) echo "Please enter 1, 2, or 0"; press_enter
    esac
done

当你的计算机卡住时… When your computer hangs…

We have all had the experience of an application hanging. Hanging is when a program suddenly seems to stop and become unresponsive. While you might think that the program has stopped, in most cases, the program is still running but its program logic is stuck in an endless loop.

​ 我们都曾经有过应用程序卡住的经历。卡住是指一个程序突然停止并变得无响应。虽然你可能认为程序已经停止了,但在大多数情况下,程序仍在运行,只是它的程序逻辑陷入了一个无限循环中。

Imagine this situation: you have an external device attached to your computer, such as a USB disk drive but you forgot to turn it on. You try and use the device but the application hangs instead. When this happens, you could picture the following dialog going on between the application and the interface for the device:

​ 想象一下这种情况:你的计算机连接着一个外部设备,比如一个USB磁盘驱动器,但你忘了将其打开。你尝试使用设备,但应用程序却卡住了。当这种情况发生时,你可以想象应用程序和设备接口之间发生了以下对话:

Application:    Are you ready?
Interface:  Device not ready.

Application:    Are you ready?
Interface:  Device not ready.

Application:    Are you ready?
Interface:  Device not ready.

and so on, forever.

以此类推,一直无限循环。

Well-written software tries to avoid this situation by instituting a timeout. This means that the loop is also counting the number of attempts or calculating the amount of time it has waited for something to happen. If the number of tries or the amount of time allowed is exceeded, the loop exits and the program generates an error and exits.

​ 良好编写的软件会通过引入超时机制来避免这种情况。这意味着循环同时计算尝试次数或等待发生某事的时间。如果超过了允许的尝试次数或时间限制,循环将退出,程序生成一个错误并退出。

12 - 位置参数

位置参数 Positional Parameters

https://linuxcommand.org/lc3_wss0120.php

When we last left our script, it looked something like this:

​ 当我们上次离开我们的脚本时,它看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/bin/bash

# sysinfo_page - A script to produce a system information HTML file

##### Constants

TITLE="System Information for $HOSTNAME"
RIGHT_NOW="$(date +"%x %r %Z")"
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

##### Functions

system_info()
{
    echo "<h2>System release info</h2>"
    echo "<p>Function not yet implemented</p>"

}   # end of system_info


show_uptime()
{
    echo "<h2>System uptime</h2>"
    echo "<pre>"
    uptime
    echo "</pre>"

}   # end of show_uptime


drive_space()
{
    echo "<h2>Filesystem space</h2>"
    echo "<pre>"
    df
    echo "</pre>"

}   # end of drive_space


home_space()
{
    # Only the superuser can get this information

    if [ "$(id -u)" = "0" ]; then
        echo "<h2>Home directory space by user</h2>"
        echo "<pre>"
        echo "Bytes Directory"
        du -s /home/* | sort -nr
        echo "</pre>"
    fi

}   # end of home_space



##### Main

cat <<- _EOF_
  <html>
  <head>
      <title>$TITLE</title>
  </head>
  <body>
      <h1>$TITLE</h1>
      <p>$TIME_STAMP</p>
      $(system_info)
      $(show_uptime)
      $(drive_space)
      $(home_space)
  </body>
  </html>
_EOF_

We have most things working, but there are several more features we can add:

​ 我们已经完成了大部分工作,但还有几个功能可以添加:

  1. We should be able to specify the name of the output file on the command line, as well as set a default output file name if no name is specified.

  2. 我们应该能够在命令行上指定输出文件的名称,如果没有指定名称,还应设置默认输出文件名。

  3. Let’s offer an interactive mode that will prompt for a file name and warn the user if the file exists and prompt the user to overwrite it.

  4. 让我们提供一个交互模式,提示用户输入文件名,并在文件存在时警告用户并询问是否覆盖它。

  5. Naturally, we want to have a help option that will display a usage message.

  6. 当然,我们希望有一个帮助选项,显示用法信息。

All of these features involve using command line options and arguments. To handle options on the command line, we use a facility in the shell called positional parameters. Positional parameters are a series of special variables ($0 through $9) that contain the contents of the command line.

​ 所有这些功能都涉及使用命令行选项和参数。为了处理命令行上的选项,我们使用Shell中的一个功能,称为位置参数。位置参数是一系列特殊变量($0$9),它们包含命令行的内容。

Let’s imagine the following command line:

​ 让我们想象以下命令行:

1
[me@linuxbox me]$ some_program word1 word2 word3

If some_program were a bash shell script, we could read each item on the command line because the positional parameters contain the following:

​ 如果some_program是一个bash shell脚本,我们可以读取命令行上的每个项目,因为位置参数包含以下内容:

  • $0 would contain “some_program”
  • $1 would contain “word1”
  • $2 would contain “word2”
  • $3 would contain “word3”
  • $0将包含"some_program"
  • $1将包含"word1"
  • $2将包含"word2"
  • $3将包含"word3"

Here is a script we can use to try this out:

​ 以下是我们可以用来尝试的脚本:

1
2
3
4
5
6
7
#!/bin/bash

echo "Positional Parameters"
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3

检测命令行参数 Detecting Command Line Arguments

Often, we will want to check to see if we have comand line arguments on which to act. There are a couple of ways to do this. First, we could simply check to see if $1 contains anything like so:

​ 通常,我们需要检查是否有命令行参数可供操作。有几种方法可以做到这一点。首先,我们可以简单地检查$1是否包含任何内容,例如:

1
2
3
4
5
6
7
#!/bin/bash

if [ "$1" != "" ]; then
    echo "Positional parameter 1 contains something"
else
    echo "Positional parameter 1 is empty"
fi

Second, the shell maintains a variable called $# that contains the number of items on the command line in addition to the name of the command ($0).

​ 其次,Shell维护一个名为$#的变量,其中包含命令行上的项目数,以及命令的名称($0)。

1
2
3
4
5
6
7
#!/bin/bash

if [ $# -gt 0 ]; then
    echo "Your command line contains $# arguments"
else
    echo "Your command line contains no arguments"
fi

命令行选项 Command Line Options

As we discussed before, many programs, particularly ones from the GNU Project, support both short and long command line options. For example, to display a help message for many of these programs, we may use either the “-h” option or the longer “--help” option. Long option names are typically preceded by a double dash. We will adopt this convention for our scripts.

​ 如前所述,许多程序,特别是来自GNU项目的程序,支持短选项和长选项。例如,要显示许多这些程序的帮助消息,我们可以使用"-h“选项或更长的”--help“选项。长选项名称通常以两个连字符开头。我们将采用这种约定来编写我们的脚本。

Here is the code we will use to process our command line:

​ 以下是我们将用于处理命令行的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interactive=
filename=~/sysinfo_page.html

while [ "$1" != "" ]; do
    case $1 in
        -f | --file )           shift
                                filename="$1"
                                ;;
        -i | --interactive )    interactive=1
                                ;;
        -h | --help )           usage
                                exit
                                ;;
        * )                     usage
                                exit 1
    esac
    shift
done

This code is a little tricky, so we need to explain it.

​ 这段代码有点复杂,所以我们需要解释一下。

The first two lines are pretty easy. We set the variable interactive to be empty. This will indicate that the interactive mode has not been requested. Then we set the variable filename to contain a default file name. If nothing else is specified on the command line, this file name will be used.

​ 前两行很简单。我们将变量interactive设置为空。这表示未请求交互模式。然后我们设置变量filename包含默认文件名。如果在命令行上没有指定其他内容,将使用此文件名。

After these two variables are set, we have default settings, in case the user does not specify any options.

​ 设置这两个变量后,我们有了默认设置,以防用户未指定任何选项。

Next, we construct a while loop that will cycle through all the items on the command line and process each one with case. The case will detect each possible option and process it accordingly.

​ 接下来,我们构建一个while循环,将循环遍历命令行上的所有项目,并使用case处理每个项目。case将检测每个可能的选项并相应地处理它。

Now the tricky part. How does that loop work? It relies on the magic of shift.

​ 现在是棘手的部分。这个循环是如何工作的?它依赖于shift的魔力。

shift is a shell builtin that operates on the positional parameters. Each time we invoke shift, it “shifts” all the positional parameters down by one. $2 becomes $1, $3 becomes $2, $4 becomes $3, and so on. Try this:

shift是一个Shell内置命令,用于操作位置参数。每次我们调用shift时,它会将所有位置参数向下“移动”一位。$2变为$1$3变为$2$4变为$3,依此类推。请试试这个:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash

echo "You start with $# positional parameters"

# Loop until all parameters are used up
while [ "$1" != "" ]; do
    echo "Parameter 1 equals $1"
    echo "You now have $# positional parameters"

    # Shift all the parameters down by one
    shift

done

获取选项的参数 Getting an Option’s Argument

Our “-f” option requires a valid file name as an argument. We use shift again to get the next item from the command line and assign it to filename. Later we will have to check the content of filename to make sure it is valid.

​ 我们的”-f“选项需要一个有效的文件名作为参数。我们再次使用shift从命令行中获取下一个项目,并将其赋给filename。稍后,我们将需要检查filename的内容,以确保它是有效的。

将命令行处理器集成到脚本中 Integrating the Command Line Processor into the Script

We will have to move a few things around and add a usage function to get this new routine integrated into our script. We’ll also add some test code to verify that the command line processor is working correctly. Our script now looks like this:

​ 我们需要移动一些内容并添加一个usage函数来将这个新例程集成到我们的脚本中。我们还将添加一些测试代码来验证命令行处理器是否正常工作。我们的脚本现在如下所示:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/bin/bash

# sysinfo_page - A script to produce a system information HTML file

##### Constants

TITLE="System Information for $HOSTNAME"
RIGHT_NOW="$(date +"%x %r %Z")"
TIME_STAMP="Updated on $RIGHT_NOW by $USER"

##### Functions

system_info()
{
    echo "<h2>System release info</h2>"
    echo "<p>Function not yet implemented</p>"

}   # end of system_info


show_uptime()
{
    echo "<h2>System uptime</h2>"
    echo "<pre>"
    uptime
    echo "</pre>"

}   # end of show_uptime


drive_space()
{
    echo "<h2>Filesystem space</h2>"
    echo "<pre>"
    df
    echo "</pre>"

}   # end of drive_space


home_space()
{
    # Only the superuser can get this information

    if [ "$(id -u)" = "0" ]; then
        echo "<h2>Home directory space by user</h2>"
        echo "<pre>"
        echo "Bytes Directory"
        du -s /home/* | sort -nr
        echo "</pre>"
    fi

}   # end of home_space


write_page()
{
    cat <<- _EOF_
    <html>
        <head>
        <title>$TITLE</title>
        </head>
        <body>
        <h1>$TITLE</h1>
        <p>$TIME_STAMP</p>
        $(system_info)
        $(show_uptime)
        $(drive_space)
        $(home_space)
        </body>
    </html>
_EOF_

}

usage()
{
    echo "usage: sysinfo_page [[[-f file ] [-i]] | [-h]]"
}


##### Main

interactive=
filename=~/sysinfo_page.html

while [ "$1" != "" ]; do
    case $1 in
        -f | --file )           shift
                                filename=$1
                                ;;
        -i | --interactive )    interactive=1
                                ;;
        -h | --help )           usage
                                exit
                                ;;
        * )                     usage
                                exit 1
    esac
    shift
done


# Test code to verify command line processing

if [ "$interactive" = "1" ]; then
  echo "interactive is on"
else
  echo "interactive is off"
fi
echo "output file = $filename"


# Write page (comment out until testing is complete)

# write_page > $filename

添加交互模式 Adding Interactive Mode

The interactive mode is implemented with the following code:

​ 交互模式使用以下代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
if [ "$interactive" = "1" ]; then

    response=

    read -p "Enter name of output file [$filename] > " response
    if [ -n "$response" ]; then
        filename="$response"
    fi

    if [ -f $filename ]; then
        echo -n "Output file exists. Overwrite? (y/n) > "
        read response
        if [ "$response" != "y" ]; then
            echo "Exiting program."
            exit 1
        fi
    fi
fi

First, we check if the interactive mode is on, otherwise we don’t have anything to do. Next, we ask the user for the file name. Notice the way the prompt is worded:

​ 首先,我们检查交互模式是否打开,否则我们没有任何事情要做。接下来,我们要求用户输入文件名。注意提示的方式:

1
"Enter name of output file [$filename] > "

We display the current value of filename since, the way this routine is coded, if the user just presses the enter key, the default value of filename will be used. This is accomplished in the next two lines where the value of response is checked. If response is not empty, then filename is assigned the value of response. Otherwise, filename is left unchanged, preserving its default value.

​ 我们显示filename的当前值,因为按下回车键时,如果用户没有输入任何内容,则将使用filename的默认值。在下面两行中,检查response的值。如果response不为空,则将filename赋值为response的值。否则,保持filename不变,保留其默认值。

After we have the name of the output file, we check if it already exists. If it does, we prompt the user. If the user response is not “y,” we give up and exit, otherwise we can proceed.

​ 在获得输出文件的名称后,我们检查它是否已经存在。如果存在,我们会提示用户。如果用户响应不是"y”,我们放弃并退出,否则我们可以继续。

13 - 流程控制 - 第三部分

流程控制 - 第三部分 Flow Control - Part 3

https://linuxcommand.org/lc3_wss0130.php

Now that we have learned about positional parameters, it’s time to cover the remaining flow control statement, for. Like while and until, for is used to construct loops. for works like this:

​ 现在我们已经学习了关于位置参数的知识,是时候介绍剩下的流程控制语句了,即for循环。和whileuntil一样,for用于构建循环。for的使用方式如下:

1
2
3
for variable in words; do
    commands
done

In essence, for assigns a word from the list of words to the specified variable, executes the commands, and repeats this over and over until all the words have been used up. Here is an example:

​ 简而言之,for将列表中的单词赋值给指定的变量,执行命令,然后重复这个过程,直到所有单词都被用完。下面是一个示例:

1
2
3
4
5
#!/bin/bash

for i in word1 word2 word3; do
    echo "$i"
done

`

In this example, the variable i is assigned the string “word1”, then the statement echo "$i" is executed, then the variable i is assigned the string “word2”, and the statement echo "$i" is executed, and so on, until all the words in the list of words have been assigned.

​ 在这个示例中,变量i首先被赋值为字符串"word1",然后执行语句echo "$i",然后变量i被赋值为字符串"word2",执行语句echo "$i",依此类推,直到列表中的所有单词都被赋值。

The interesting thing about for is the many ways we can construct the list of words. All kinds of expansions can be used. In the next example, we will construct the list of words using command substitution:

for的有趣之处在于我们可以以多种方式构建单词列表。可以使用各种扩展。在下一个示例中,我们将使用命令替换来构建单词列表:

1
2
3
4
5
6
7
#!/bin/bash

count=0
for i in $(cat ~/.bash_profile); do
    count=$((count + 1))
    echo "Word $count ($i) contains $(echo -n $i | wc -c) characters"
done

Here we take the file .bash_profile and count the number of words in the file and the number of characters in each word.

​ 在这里,我们读取.bash_profile文件,计算文件中的单词数以及每个单词中的字符数。

So what’s this got to do with positional parameters? Well, one of the features of for is that it can use the positional parameters as the list of words:

​ 那么这与位置参数有什么关系呢?好吧,for的一个特性是可以使用位置参数作为单词列表:

1
2
3
4
5
#!/bin/bash

for i in "$@"; do
    echo $i
done

The shell variable "$@" contains the list of command line arguments. This technique is often used to process a list of files on the command line. Here is a another example:

​ Shell变量"$@"包含命令行参数的列表。这种技巧通常用于处理命令行上的文件列表。下面是另一个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/bin/bash

for filename in "$@"; do
    result=
    if [ -f "$filename" ]; then
        result="$filename is a regular file"
    else
        if [ -d "$filename" ]; then
            result="$filename is a directory"
        fi
    fi
    if [ -w "$filename" ]; then
        result="$result and it is writable"
    else
        result="$result and it is not writable"
    fi
    echo "$result"
done

Try this script. Give it a list of files or a wildcard like “*” to see it work.

​ 尝试运行这个脚本。给它一个文件列表或通配符"*",看看它的工作原理。

The use of in "$@" is so common that it is assumed if the in words clause is ommited.

​ 使用in "$@"的方式非常常见,如果省略了in words子句,就默认使用它。

Here is another example script. This one compares the files in two directories and lists which files in the first directory are missing from the second.

​ 下面是另一个示例脚本。这个脚本比较两个目录中的文件,并列出第一个目录中缺失于第二个目录中的文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash

# cmp_dir - program to compare two directories

# Check for required arguments
if [ $# -ne 2 ]; then
    echo "usage: $0 directory_1 directory_2" 1>&2
    exit 1
fi

# Make sure both arguments are directories
if [ ! -d "$1" ]; then
    echo "$1 is not a directory!" 1>&2
    exit 1
fi

if [ ! -d "$2" ]; then
    echo "$2 is not a directory!" 1>&2
    exit 1
fi

# Process each file in directory_1, comparing it to directory_2
missing=0
for filename in "$1"/*; do
    fn=$(basename "$filename")
    if [ -f "$filename" ]; then
        if [ ! -f "$2/$fn" ]; then
            echo "$fn is missing from $2"
            missing=$((missing + 1))
        fi
    fi
done
echo "$missing files missing"

Now on to the real work. We are going to improve the home_space function to output more information. Recall that our previous version looked like this:

​ 现在让我们进入真正的工作内容。我们将改进home_space函数以输出更多信息。回顾一下,我们之前的版本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
home_space()
{
    # Only the superuser can get this information

    if [ "$(id -u)" = "0" ]; then
        echo "<h2>Home directory space by user</h2>"
        echo "<pre>"
        echo "Bytes Directory"
        du -s /home/* | sort -nr
        echo "</pre>"
    fi

}   # end of home_space

Here is the new version:

​ 下面是改进后的版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
home_space() {
    echo "<h2>Home directory space by user</h2>"
    echo "<pre>"
    format="%8s%10s%10s   %-s\n"
    printf "$format" "Dirs" "Files" "Blocks" "Directory"
    printf "$format" "----" "-----" "------" "---------"
    if [ $(id -u) = "0" ]; then
        dir_list="/home/*"
    else
        dir_list=$HOME
    fi
    for home_dir in $dir_list; do
        total_dirs=$(find $home_dir -type d | wc -l)
        total_files=$(find $home_dir -type f | wc -l)
        total_blocks=$(du -s $home_dir)
        printf "$format" "$total_dirs" "$total_files" "$total_blocks"
    done
    echo "</pre>"

}   # end of home_space

This improved version introduces a new command printf, which is used to produce formatted output according to the contents of a format string. printf comes from the C programming language and has been implemented in many other programming languages including C++, Perl, awk, java, PHP, and of course, bash. More information about printf format strings can be found at:

​ 这个改进的版本引入了一个新命令printf,它根据格式字符串的内容生成格式化输出。printf源自C编程语言,并已在许多其他编程语言中实现,包括C++、Perl、awk、Java、PHP和当然还有bash。关于printf格式字符串的更多信息可以在以下链接中找到:

We also introduce the find command. find is used to search for files or directories that meet specific criteria. In the home_space function, we use find to list the directories and regular files in each home directory. Using the wc command, we count the number of files and directories found.

​ 我们还引入了find命令。find用于搜索符合特定条件的文件或目录。在home_space函数中,我们使用find列出每个主目录中的目录和普通文件。使用wc命令,我们计算找到的文件和目录的数量。

The really interesting thing about home_space is how we deal with the problem of superuser access. Notice that we test for the superuser with id and, according to the outcome of the test, we assign different strings to the variable dir_list, which becomes the list of words for the for loop that follows. This way, if an ordinary user runs the script, only his/her home directory will be listed.

home_space真正有趣的地方在于我们如何处理超级用户访问的问题。请注意,我们使用id测试超级用户,并根据测试的结果将不同的字符串赋给变量dir_list,它成为接下来的for循环的单词列表。这样,如果普通用户运行脚本,只有他/她的主目录会被列出。

Another function that can use a for loop is our unfinished system_info function. We can build it like this:

​ 还有一个可以使用for循环的函数是我们未完成的system_info函数。我们可以这样构建它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
system_info() {
    # Find any release files in /etc

    if ls /etc/*release 1>/dev/null 2>&1; then
        echo "<h2>System release info</h2>"
        echo "<pre>"
        for i in /etc/*release; do

            # Since we can't be sure of the
            # length of the file, only
            # display the first line.

            head -n 1 "$i"
        done
        uname -orp
        echo "</pre>"
    fi

}   # end of system_info

In this function, we first determine if there are any release files to process. The release files contain the name of the vendor and the version of the distribution. They are located in the /etc directory. To detect them, we perform an ls command and throw away all of its output. We are only interested in the exit status. It will be true if any files are found.

​ 在这个函数中,我们首先确定是否有任何需要处理的 release 文件。release 文件包含供应商的名称和发行版的版本。它们位于 /etc 目录中。为了检测它们,我们执行一个 ls 命令并丢弃其所有输出。我们只对退出状态感兴趣。如果找到任何文件,退出状态将为真。

Next, we output the HTML for this section of the page, since we now know that there are release files to process. To process the files, we start a for loop to act on each one. Inside the loop, we use the head command to return the first line of each file.

​ 接下来,我们输出此页面部分的 HTML,因为现在我们知道有 release 文件要处理。要处理这些文件,我们开始一个 for 循环,对每个文件执行操作。在循环内部,我们使用 head 命令返回每个文件的第一行。

Finally, we use the uname command with the “o”, “r”, and “p” options to obtain some additional information from the system.

​ 最后,我们使用 uname 命令和 “o”、“r” 和 “p” 选项从系统获取一些额外的信息。

14 - 错误、信号和陷阱(噢,我的天!)- 第一部分

错误、信号和陷阱(噢,我的天!)- 第一部分 Errors and Signals and Traps (Oh My!) - Part 1

https://linuxcommand.org/lc3_wss0140.php

In this lesson, we’re going to look at handling errors during script execution.

​ 在本课程中,我们将讨论处理脚本执行过程中的错误。

The difference between a poor program and a good one is often measured in terms of the program’s robustness. That is, the program’s ability to handle situations in which something goes wrong.

​ 一个糟糕的程序和一个好的程序之间的区别通常是以程序的鲁棒性来衡量的。也就是说,程序处理出现问题的情况的能力。

退出状态 Exit Status

As we recall from previous lessons, every well-written program returns an exit status when it finishes. If a program finishes successfully, the exit status will be zero. If the exit status is anything other than zero, then the program failed in some way.

​ 回顾一下之前的课程,每个编写良好的程序在完成时都会返回一个退出状态。如果程序成功完成,退出状态将为零。如果退出状态不是零,那么程序以某种方式失败了。

It is very important to check the exit status of programs we call in our scripts. It is also important that our scripts return a meaningful exit status when they finish. There was once a Unix system administrator who wrote a script for a production system containing the following 2 lines of code:

​ 检查我们在脚本中调用的程序的退出状态非常重要。同样重要的是,我们的脚本在完成时返回一个有意义的退出状态。曾经有一个 Unix 系统管理员为一个生产系统编写了一个包含以下两行代码的脚本:

1
2
3
4
# Example of a really bad idea

cd "$some_directory"
rm *

Why is this such a bad way of doing it? It’s not, if nothing goes wrong. The two lines change the working directory to the name contained in $some_directory and delete the files in that directory. That’s the intended behavior. But what happens if the directory named in $some_directory doesn’t exist? In that case, the cd command will fail and the script executes the rm command on the current working directory. Not the intended behavior!

​ 为什么这样做是错误的?如果一切顺利的话,就不是错误。这两行代码将工作目录更改为$some_directory中包含的名称,并删除该目录中的文件。这是预期的行为。但是,如果$some_directory中指定的目录不存在会发生什么?在这种情况下,cd命令将失败,脚本会在当前工作目录上执行rm命令。这不是预期的行为!

By the way, the hapless system administrator’s script suffered this very failure and it destroyed a large portion of an important production system. Don’t let this happen to you!

​ 顺便说一下,这个倒霉的系统管理员的脚本遭遇了这个错误,并摧毁了一个重要的生产系统的大部分内容。不要让这种情况发生在你身上!

The problem with the script was that it did not check the exit status of the cd command before proceeding with the rm command.

​ 该脚本的问题在于它在继续执行rm命令之前没有检查cd命令的退出状态。

检查退出状态 Checking the Exit Status

There are several ways we can get and respond to the exit status of a program. First, we can examine the contents of the $? environment variable. $? will contain the exit status of the last command executed. We can see this work with the following:

​ 我们有几种方法可以获取和响应程序的退出状态。首先,我们可以检查$?环境变量的内容。$?将包含上一个执行的命令的退出状态。我们可以通过以下方式查看它的工作原理:

1
2
3
4
[me@linuxbox]$  true; echo $?
0
[me@linuxbox]$ false; echo $?
1

The true and false commands are programs that do nothing except return an exit status of zero and one, respectively. Using them, we can see how the $? environment variable contains the exit status of the previous program.

truefalse命令是什么都不做,只返回零和非零退出状态的程序。通过使用它们,我们可以看到$?环境变量包含了先前程序的退出状态。

So to check the exit status, we could write the script this way:

​ 因此,要检查退出状态,我们可以这样编写脚本:

1
2
3
4
5
6
7
8
9
# Check the exit status

cd "$some_directory"
if [ "$?" = "0" ]; then
  rm *
else
  echo "Cannot change directory!" 1>&2
  exit 1
fi

In this version, we examine the exit status of the cd command and if it’s not zero, we print an error message on standard error and terminate the script with an exit status of 1.

​ 在这个版本中,我们检查cd命令的退出状态,如果不是零,我们在标准错误上打印出一个错误消息,并以退出状态1终止脚本。

While this is a working solution to the problem, there are more clever methods that will save us some typing. The next approach we can try is to use the if statement directly, since it evaluates the exit status of commands it is given.

​ 虽然这是解决问题的一种方法,但还有更聪明的方法可以节省我们的打字。下一个我们可以尝试的方法是直接使用if语句,因为它评估给定的命令的退出状态。

Using if, we could write it this way:

​ 使用if,我们可以这样编写脚本:

1
2
3
4
5
6
7
8
# A better way

if cd "$some_directory"; then
  rm ./*
else
  echo "Could not change directory! Aborting." 1>&2
  exit 1
fi

Here we check to see if the cd command is successful. Only then does rm get executed; otherwise an error message is output and the program exits with a code of 1, indicating that an error has occurred.

​ 在这里,我们检查cd命令是否成功。只有在成功的情况下才会执行rm命令;否则,输出一个错误消息,并以代码1退出,表示发生了错误。

Notice too how we changed the target of the rm command from “” to “./”. This is a safety precaution. The reason is a little subtle and has to do with the lax way Unix-like systems name files. Since it is possible to include almost any character in a file name, we must card against file names that begin with hyphens as thy might be interpreted as command options after the wildcard is expanded. For example, if there was a file named -rf in the directory, it might cause rm to do unpleasant things. It’s a good idea to always include “./” ahead of leading asterisks in scripts.

​ 还要注意,我们将rm命令的目标从*更改为./。这是一种安全措施。原因有点微妙,与类 Unix 系统命名文件的方式有关。由于文件名几乎可以包含任何字符,我们必须对以连字符开头的文件名进行保护,因为在通配符展开后,它们可能被解释为命令选项。例如,如果目录中有一个名为-rf的文件,它可能导致rm执行一些不好的操作。在脚本中,始终在前导星号之前包含./是一个好习惯。

错误退出函数 An Error Exit Function

Since we will be checking for errors often in our programs, it makes sense to write a function that will display error messages. This will save more typing and promote laziness.

​ 由于我们经常会在程序中检查错误,因此编写一个显示错误消息的函数是有意义的。这样可以节省更多的打字并鼓励懒惰。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# An error exit function

error_exit()
{
  echo "$1" 1>&2
  exit 1
}

# Using error_exit

if cd "$some_directory"; then
  rm ./*
else
  error_exit "Cannot change directory! Aborting."
fi

AND 和 OR 列表 AND and OR Lists

Finally, we can further simplify our script by using the AND and OR control operators. To explain how they work, here is a quote from the bash man page:

​ 最后,我们可以通过使用 AND 和 OR 控制运算符来进一步简化脚本。为了解释它们的工作原理,这里是来自bash手册页的引用:

“The control operators && and || denote AND lists and OR lists, respectively. An AND list has the form

​ “控制运算符&&||分别表示 AND 列表和 OR 列表。AND 列表的形式为

1
command1 && command2

command2 is executed if, and only if, command1 returns an exit status of zero.

当且仅当command1返回零的退出状态时,执行command2

An OR list has the form

​ OR 列表的形式为

1
command1 || command2

command2 is executed if, and only if, command1 returns a non-zero exit status. The exit status of AND and OR lists is the exit status of the last command executed in the list.”

当且仅当command1返回非零的退出状态时,执行command2。AND 列表和 OR 列表的退出状态是列表中最后一个执行的命令的退出状态。”

Again, we can use the true and false commands to see this work:

​ 同样,我们可以使用truefalse命令来查看这个工作方式:

1
2
3
4
5
6
7
[me@linuxbox]$ true || echo "echo executed"
[me@linuxbox]$ false || echo "echo executed"
echo executed
[me@linuxbox]$ true && echo "echo executed"
echo executed
[me@linuxbox]$ false && echo "echo executed"
[me@linuxbox]$

Using this technique, we can write an even simpler version:

​ 使用这种技术,我们可以编写一个更简单的版本:

1
2
3
4
# Simplest of all

cd "$some_directory" || error_exit "Cannot change directory! Aborting"
rm *

If an exit is not required in case of error, then we can even do this:

​ 如果在出现错误的情况下不需要退出,那么我们甚至可以这样做:

1
2
3
# Another way to do it if exiting is not desired

cd "$some_directory" && rm ./*

We need to point out that even with the defense against errors we have introduced in our example for the use of cd, this code is still vulnerable to a common programming error, namely, what happens if the name of the variable containing the name of the directory is misspelled? In that case, the shell will interpret the variable as empty and the cd succeed, but it will change directories to the user’s home directory, so beware!

​ 我们需要指出的是,即使在我们的示例中为了防止错误而引入了对cd的防御,该代码仍然容易受到一种常见的编程错误的攻击,即如果包含目录名称的变量的名称拼写错误会发生什么?在这种情况下,shell 将将变量解释为空,并且cd成功,但它会更改到用户的主目录,所以要小心!

改进错误退出函数 Improving the Error Exit Function

There are a number of improvements that we can make to the error_exit function. It is useful to include the name of the program in the error message to make clear where the error is coming from. This becomes more important as our programs get more complex and we start having scripts launching other scripts, etc. Also, note the inclusion of the LINENO environment variable which will help identify the exact line within a script where the error occurred.

​ 对于error_exit函数,我们可以进行一些改进。在错误消息中包含程序的名称是很有用的,以清楚地表明错误的来源。随着我们的程序变得更复杂,我们开始启动其他脚本等等,这一点变得更加重要。还要注意包含LINENO环境变量,它将帮助识别发生错误的脚本中的确切行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash

# A slicker error handling routine

# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run.  You can get this
# value from the first item on the command line ($0).

PROGNAME="$(basename $0)"

error_exit()
{

# ----------------------------------------------------------------
# Function for exit due to fatal program error
#   Accepts 1 argument:
#     string containing descriptive error message
# ----------------------------------------------------------------


  echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
  exit 1
}

# Example call of the error_exit function.  Note the inclusion
# of the LINENO environment variable.  It contains the current
# line number.

echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."

The use of the curly braces within the error_exit function is an example of parameter expansion. We can surround a variable name with curly braces (as with ${PROGNAME}) if we need to be sure it is separated from surrounding text. Some people just put them around every variable out of habit. That usage is simply a style thing. The second use, ${1:-"Unknown Error"} means that if parameter 1 ($1) is undefined, substitute the string “Unknown Error” in its place. Using parameter expansion, it is possible to perform a number of useful string manipulations. More information about parameter expansion can be found in the bash man page under the topic “EXPANSIONS”.

​ 在error_exit函数中使用大括号是参数扩展的一个示例。如果我们需要确保变量与周围的文本分开,可以使用大括号将变量名称括起来(例如${PROGNAME})。有些人习惯于在每个变量周围都加上大括号。这种用法只是一种风格问题。第二个用法${1:-"Unknown Error"}的意思是,如果参数1($1)未定义,则用字符串"Unknown Error"替代它。使用参数扩展,可以进行许多有用的字符串操作。有关参数扩展的更多信息,请参阅bash手册页中的"EXPANSIONS"主题。

15 - 错误、信号和陷阱(哦,我的天!)- 第二部分

错误、信号和陷阱(哦,我的天!)- 第二部分 Errors and Signals and Traps (Oh, My!) - Part 2

https://linuxcommand.org/lc3_wss0150.php

Errors are not the only way that a script can terminate unexpectedly. We also have to be concerned with signals. Consider the following program:

​ 错误并不是脚本意外终止的唯一方式。我们还需要关注信号。考虑以下程序:

1
2
3
4
5
6
#!/bin/bash

echo "this script will endlessly loop until you stop it"
while true; do
  : # Do nothing
done

After we launch this script it will appear to hang. Actually, like most programs that appear to hang, it is really stuck inside a loop. In this case, it is waiting for the true command to return a non-zero exit status, which it never does. Once started, the script will continue until bash receives a signal that will stop it. We can send such a signal by typing Ctrl-c, the signal called SIGINT (short for SIGnal INTerrupt).

​ 启动这个脚本后,它看起来会一直挂起。实际上,像大多数看起来挂起的程序一样,它实际上是陷入了一个循环中。在这种情况下,它正在等待true命令返回一个非零的退出状态,但实际上它永远不会返回。一旦启动,脚本将一直运行,直到bash接收到一个停止它的信号。我们可以通过键入Ctrl-c来发送这样的信号,这个信号被称为SIGINT(代表SIGnal INTerrupt)。

自我清理 Cleaning Up After Ourselves

Okay, so a signal can come along and make our script terminate. Why does it matter? Well, in many cases it doesn’t matter and we can safely ignore signals, but in some cases it will matter.

​ 好的,信号可以让我们的脚本终止。这有什么关系呢?嗯,在许多情况下,这并不重要,我们可以安全地忽略信号,但在某些情况下,它确实很重要。

Let’s take a look at another script:

​ 让我们看看另一个脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

# Program to print a text file with headers and footers

TEMP_FILE=/tmp/printfile.txt

pr $1 > "$TEMP_FILE"

read -p "Print file? [y/n]: "
if [ "$REPLY" = "y" ]; then
  lpr "$TEMP_FILE"
fi

This script processes the text file specified on the command line with the pr command and stores the result in a temporary file. Next, it asks the user if they want to print the file. If the user types “y”, then the temporary file is passed to the lpr program for printing (substitute less for lpr if there isn’t a printer attached to the system.)

​ 这个脚本会使用pr命令处理命令行上指定的文本文件,并将结果存储在临时文件中。然后,它会询问用户是否要打印该文件。如果用户输入"y",则将临时文件传递给lpr程序进行打印(如果系统上没有连接打印机,则替换为less)。

Admittedly, this script has a lot of design problems. While it needs a file name passed on the command line, it doesn’t check that it received one, and it doesn’t check that the file actually exists. But the problem we want to focus on here is that when the script terminates, it leaves behind the temporary file.

​ 诚然,这个脚本存在很多设计问题。虽然它需要在命令行上传递一个文件名,但它没有检查是否接收到文件名,也没有检查文件是否实际存在。但我们想要关注的问题是,当脚本终止时,它会留下临时文件。

Good practice dictates that we delete the temporary file $TEMP_FILE when the script terminates. This is easily accomplished by adding the following to the end of the script:

​ 良好的实践规定,在脚本终止时删除临时文件$TEMP_FILE。我们可以通过在脚本末尾添加以下内容来轻松实现:

1
rm "$TEMP_FILE"

This would seem to solve the problem, but what happens if the user types ctrl-c when the “Print file? [y/n]:” prompt appears? The script will terminate at the read command and the rm command is never executed. Clearly, we need a way to respond to signals such as SIGINT when the Ctrl-c key is typed.

​ 这似乎解决了问题,但是如果用户在出现"打印文件?[y/n]:“提示时键入ctrl-c会发生什么?脚本将在read命令处终止,并且rm命令将不会执行。显然,我们需要一种方法来响应诸如SIGINT的信号,即键入Ctrl-c键时的信号。

Fortunately, bash provides a method to perform commands if and when signals are received.

​ 幸运的是,bash提供了一种在接收到信号时执行命令的方法。

trap

The trap command allows us to execute a command when our script receives a signal. It works like this:

trap命令允许我们在脚本接收到信号时执行命令。它的用法如下:

1
trap arg signals

“signals” is a list of signals to intercept and “arg” is a command to execute when one of the signals is received. For our printing script, we might handle the signal problem this way:

​ “signals"是要拦截的信号列表,“arg"是接收到其中一个信号时要执行的命令。对于我们的打印脚本,我们可以通过以下方式处理信号问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

# Program to print a text file with headers and footers

TEMP_FILE=/tmp/printfile.txt

trap "rm $TEMP_FILE; exit" SIGHUP SIGINT SIGTERM

pr $1 > "$TEMP_FILE"

read -p "Print file? [y/n]: "
if [ "$REPLY" = "y" ]; then
  lpr "$TEMP_FILE"
fi
rm "$TEMP_FILE"

Here we have added a trap command that will execute “rm $TEMP_FILE” if any of the listed signals is received. The three signals listed are the most common ones that most scripts are likely to encounter, but there are many more that can be specified. For a complete list, type “trap -l”. In addition to listing the signals by name, you may alternately specify them by number.

​ 在这里,我们添加了一个trap命令,如果接收到列出的任何信号,则执行”rm $TEMP_FILE"。列出的三个信号是大多数脚本可能遇到的最常见的信号,但还有许多其他信号可以指定。要获取完整列表,请输入”trap -l"。除了按名称列出信号外,您还可以用数字指定信号。

来自外部空间的信号9 Signal 9 from Outer Space

There is one signal that you cannot trap: SIGKILL or signal 9. The kernel immediately terminates any process sent this signal and no signal handling is performed. Since it will always terminate a program that is stuck, hung, or otherwise screwed up, it is tempting to think that it’s the easy way out when we have to get something to stop and go away. There are often references to the following command which sends the SIGKILL signal:

​ 有一个信号是无法捕获的:SIGKILL或信号9。内核立即终止接收到此信号的任何进程,并且不执行任何信号处理。由于它始终终止程序的运行(无论是卡住、挂起还是其他故障),我们可能会觉得这是一种简便的方式来停止和结束某些东西。通常会提到以下命令发送SIGKILL信号:

kill -9

However, despite its apparent ease, we must remember that when we send this signal, no processing is done by the application. Often this is OK, but with many programs it’s not. In particular, many complex programs (and some not-so-complex) create lock files to prevent multiple copies of the program from running at the same time. When a program that uses a lock file is sent a SIGKILL, it doesn’t get the chance to remove the lock file when it terminates. The presence of the lock file will prevent the program from restarting until the lock file is manually removed.

​ 然而,尽管它看起来很简单,但我们必须记住,当我们发送此信号时,应用程序不会执行任何处理。这在许多程序中可能是可以接受的,但在许多复杂程序(以及一些不那么复杂的程序)中,它是不可接受的。特别是,许多复杂程序(以及一些不那么复杂的程序)会创建锁文件,以防止同时运行多个程序副本。当发送SIGKILL给使用锁文件的程序时,它无法在终止时删除锁文件。锁文件的存在将阻止程序在手动删除锁文件之前重新启动。

Be warned. Use SIGKILL as a last resort.

​ 请注意,SIGKILL只在万不得已时使用。

一个clean_up函数 A clean_up Function

While the trap command has solved the problem, we can see that it has some limitations. Most importantly, it will only accept a single string containing the command to be performed when the signal is received. We could get clever and use “;” and put multiple commands in the string to get more complex behavior, but frankly, it’s ugly. A better way would be to create a function that is called when we want to perform any actions at the end of a script. For our purposes, we will call this function clean_up.

​ 虽然trap命令解决了问题,但我们可以看到它有一些限制。最重要的是,它只接受包含在接收到信号时要执行的命令的单个字符串。我们可以巧妙地使用”;“并将多个命令放在字符串中以获得更复杂的行为,但老实说,这样做并不美观。一个更好的方法是创建一个在我们希望在脚本末尾执行任何操作时调用的函数。对于我们的目的,我们将称此函数为clean_up

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# Program to print a text file with headers and footers

TEMP_FILE=/tmp/printfile.txt

clean_up() {

  # Perform program exit housekeeping
  rm "$TEMP_FILE"
  exit
}

trap clean_up SIGHUP SIGINT SIGTERM

pr $1 > "$TEMP_FILE"

read -p "Print file? [y/n]: "
if [ "$REPLY" = "y" ]; then
  lpr "$TEMP_FILE"
fi
clean_up

The use of a clean up function is a good idea for our error handling routines too. After all, when a program terminates (for whatever reason), we should clean up after ourselves. Here is finished version of our program with improved error and signal handling:

​ 使用清理函数对于我们的错误处理例程也是一个好主意。毕竟,当程序终止时(无论出于何种原因),我们应该在自己之后进行清理。这是改进后的程序版本,包括了更好的错误和信号处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/bin/bash

# Program to print a text file with headers and footers

# Usage: printfile file

PROGNAME="$(basename $0)"

# Create a temporary file name that gives preference
# to the user's local tmp directory and has a name
# that is resistant to tmp race attacks

if [ -d "~/tmp" ]; then
  TEMP_DIR=~/tmp
else
  TEMP_DIR=/tmp
fi
TEMP_FILE="$TEMP_DIR/$PROGNAME.$$.$RANDOM"

usage() {

  # Display usage message on standard error
  echo "Usage: $PROGNAME file" 1>&2
}

clean_up() {

  # Perform program exit housekeeping
  # Optionally accepts an exit status
  rm -f "$TEMP_FILE"
  exit $1
}

error_exit() {

  # Display error message and exit
  echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
  clean_up 1
}

trap clean_up SIGHUP SIGINT SIGTERM

if [ $# != "1" ]; then
  usage
  error_exit "one file to print must be specified"
fi
if [ ! -f "$1" ]; then
  error_exit "file $1 cannot be read"
fi

pr $1 > "$TEMP_FILE" || error_exit "cannot format file"

read -p "Print file? [y/n]: "
if [ "$REPLY" = "y" ]; then
  lpr "$TEMP_FILE" || error_exit "cannot print file"
fi
clean_up

创建安全的临时文件 Creating Safe Temporary Files

In the program above, there a number of steps taken to help secure the temporary file used by this script. It is a Unix tradition to use a directory called /tmp to place temporary files used by programs. Everyone may write files into this directory. This naturally leads to some security concerns. If possible, avoid writing files in the /tmp directory. The preferred technique is to write them in a local directory such as ~/tmp (a tmp subdirectory in the user’s home directory.) If files must be written in /tmp, we must take steps to make sure the file names are not predictable. Predictable file names may allow an attacker to create symbolic links to other files the attacker wants the user to overwrite.

​ 在上面的程序中,采取了一些步骤来帮助保护该脚本使用的临时文件。在Unix中,惯例是使用一个名为/tmp的目录来存放程序使用的临时文件。任何人都可以将文件写入此目录。这自然引起了一些安全问题。如果可能的话,避免在/tmp目录中写入文件。首选的技术是将它们写入一个本地目录,例如~/tmp(用户主目录中的tmp子目录)。如果必须在/tmp中写入文件,我们必须采取措施确保文件名不可预测。可预测的文件名可能会允许攻击者创建符号链接到攻击者希望用户覆盖的其他文件。

A good file name will help identify what wrote the file, but will not be entirely predictable. In the script above, the following line of code created the temporary file $TEMP_FILE:

​ 一个好的文件名将有助于识别写入文件的程序,但不会完全可预测。在上面的脚本中,以下代码行创建了临时文件$TEMP_FILE

1
TEMP_FILE="$TEMP_DIR/$PROGNAME.$$.$RANDOM"

The $TEMP_DIR variable contains either /tmp or ~/tmp depending on the availability of the directory. It is common practice to embed the name of the program into the file name. We have done that with the constant $PROGNAME constructed at the beginning of the sectipt. Next, we use the $$ shell variable to embed the process id (pid) of the program. This further helps identify what process is responsible for the file. Surprisingly, the process id alone is not unpredictable enough to make the file safe, so we add the $RANDOM shell variable to append a random number to the file name. With this technique, we create a file name that is both easily identifiable and unpredictable.

$TEMP_DIR变量包含/tmp~/tmp,具体取决于目录的可用性。通常的做法是将程序的名称嵌入到文件名中。我们在脚本开头构造的常量$PROGNAME已经实现了这一点。接下来,我们使用$$ shell变量将程序的进程ID(PID)嵌入到其中。这进一步有助于识别哪个进程负责该文件。令人惊讶的是,仅有进程ID还不足以使文件变得安全,因此我们使用$RANDOM shell变量将一个随机数附加到文件名上。通过这种技术,我们创建了一个既容易识别又不可预测的文件名。

就是这样 There You Have It

This concludes the LinuxCommand.org tutorials. I sincerely hope you found them both useful and enjoyable. If you did, complete your command line education by downloading my book.

​ 这就是LinuxCommand.org教程的全部内容。我真诚希望您觉得它们既有用又有趣。如果是这样的话,通过下载我的书来完善您的命令行教育。