Skip to content

Latest commit

 

History

History
595 lines (423 loc) · 35.1 KB

File metadata and controls

595 lines (423 loc) · 35.1 KB

八、变量和用户输入

在这一章中,我们将首先描述什么是变量,以及为什么我们想要和需要它们。我们将解释变量和常数之间的区别。接下来,我们将提供一些关于变量命名的可能性,并介绍一些关于命名约定的最佳实践。最后,我们将讨论用户输入以及如何正确处理它:要么使用位置参数,要么使用交互式脚本。我们将以if-then构造和退出代码的介绍来结束这一章,我们将使用它们来组合位置参数和交互式提示。

本章将介绍以下命令:readtestif

本章将涵盖以下主题:

  • 什么是变量?
  • 变量命名
  • 处理用户输入
  • 交互式和非交互式脚本

技术要求

除了带有前几章文件的 Ubuntu 虚拟机,不需要其他资源。

本章所有脚本均可在 GitHub:https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter08 上找到。对于 name-improved.sh 脚本,只有最终版本可以在网上找到。在您的系统上执行之前,请确保验证标题中的脚本版本。

什么是变量?

变量是许多(如果不是全部的话)编程和脚本语言中使用的标准构造块。变量允许我们存储信息,因此我们可以在以后引用和使用它,通常是多次。例如,我们可以使用textvariable变量来存储句子This text is contained in the variable。在这种情况下,textvariable的变量名被称为键,变量的内容(文本)被称为值,在组成变量的键-值对中。

在我们的程序中,当我们需要文本时,我们总是引用textvariable变量。现在这可能有点抽象,但我们有信心在看到本章其余部分的例子后,变量的有用性将变得清晰。

我们实际上已经看到了正在使用的 Bash 变量。请记住,在第 4 章Linux 文件系统中,我们同时考虑了BASH_VERSIONPATH变量。让我们看看如何在 shell 脚本中使用变量。我们将采用我们的hello-world-improved.sh脚本,而不是直接使用Hello world文本,我们将首先把它放在一个变量中并引用它:

reader@ubuntu:~/scripts/chapter_08$ cp ../chapter_07/hello-world-improved.sh hello-world-variable.sh
reader@ubuntu:~/scripts/chapter_08$ ls -l
total 4
-rwxrwxr-x 1 reader reader 277 Sep  1 10:35 hello-world-variable.sh
reader@ubuntu:~/scripts/chapter_08$ vim hello-world-variable.sh

首先,我们将chapter_07目录中的hello-world-improved.sh脚本复制到新创建的chapter_08目录中,名称为hello-world-variable.sh。然后,我们用vim来编辑它。给它以下内容:

#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-01
# Description: Our first script using variables!
# Usage: ./hello-world-variable.sh
#####################################

hello_text="Hello World!"

# Print the text to the terminal.
echo ${hello_text}

reader@ubuntu:~/scripts/chapter_08$ ./hello-world-variable.sh 
Hello World!
reader@ubuntu:~/scripts/chapter_08$

恭喜,您刚刚在脚本中使用了第一个变量!如您所见,您可以通过将变量的名称包装在${...}语法中来使用变量的内容。从技术上来说,只要在名字前面加上$就够了(比如echo $hello_text)。然而,在这种情况下,很难区分变量名的结尾和程序的其余部分的开头——例如,如果您在一个句子的中间使用变量(或者,更好的是,在一个单词的中间!).如果使用${..},很明显变量名以}结尾。

在运行时,我们定义的变量将被实际内容代替变量名:这个过程称为变量插值,在所有脚本/编程语言中都使用。我们永远不会在脚本中看到或直接使用变量值,因为在大多数情况下,该值取决于运行时配置。

您还会看到我们编辑了标题中的信息。虽然很容易忘记,但如果标题不包含正确的信息,就会降低可读性。一定要确保你有一个最新的标题!

如果我们进一步解剖脚本,可以看到hello_text变量是表头后的第一个函数行。我们称之为给变量赋值。在某些编程/脚本语言中,您必须首先声明一个变量,然后才能分配(大多数情况下,这些语言都有简写,您可以将它声明和分配为单个动作)。

需要声明的原因是有些语言是静态类型的(变量类型——例如,字符串或整数——应该在赋值之前声明,编译器会检查你做的是否正确——例如,没有给整数类型的变量赋值),而其他语言是动态类型的。对于动态类型的语言,语言只是假设变量的类型来自于分配给它的类型。如果给它赋值,它将是一个整数;如果它被分配了文本,它将是一个字符串,以此类推。

Basically, variables can be assigned a value, declared, or initialized. While, technically, these are different things, you will often see the terms being used interchangeably. Do not get too hung up on this; the most important thing to remember is you're creating the variable and its content!

Bash 并不真正遵循这两种方法。Bash 的简单变量(不包括数组,我们将在后面解释)总是被认为是字符串,除非操作明确指定我们应该做算术。看看下面的脚本和结果(为了简洁起见,我们省略了标题):

reader@ubuntu:~/scripts/chapter_08$ vim hello-int.sh 
reader@ubuntu:~/scripts/chapter_08$ cat hello-int.sh 
#/bin/bash

# Assign a number to the variable.
hello_int=1

echo ${hello_int} + 1
reader@ubuntu:~/scripts/chapter_08$ bash hello-int.sh 
1 + 1

你可能以为我们会把 2 号印出来。然而,如上所述,Bash 认为一切都是字符串;它只打印变量值,后跟空格、加号、另一个空格和数字 1。如果我们想要执行实际的算术运算,我们需要一个专门的语法,以便 Bash 知道它正在处理数字:

reader@ubuntu:~/scripts/chapter_08$ vim hello-int.sh 
reader@ubuntu:~/scripts/chapter_08$ cat hello-int.sh 
#/bin/bash

# Assign a number to the variable.
hello_int=1

echo $(( ${hello_int} + 1 ))

reader@ubuntu:~/scripts/chapter_08$ bash hello-int.sh 
2

通过将variable + 1包含在$((...))中,我们告诉 Bash 将其作为算术来计算。

为什么我们需要变量?

希望你现在明白如何使用变量了。然而,你可能还不明白为什么我们会想要或者 T2 需要使用变量。这看起来像是为了小回报而做的额外工作,对吗?考虑下一个例子:

reader@ubuntu:~/scripts/chapter_08$ vim name.sh 
reader@ubuntu:~/scripts/chapter_08$ cat name.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-01
# Description: Script to show why we need variables.
# Usage: ./name.sh
#####################################

# Assign the name to a variable.
name="Sebastiaan"

# Print the story.
echo "There once was a guy named ${name}. ${name} enjoyed Linux and Bash so much that he wrote a book about it! ${name} really hopes everyone enjoys his book."

reader@ubuntu:~/scripts/chapter_08$ bash name.sh 
There once was a guy named Sebastiaan. Sebastiaan enjoyed Linux and Bash so much that he wrote a book about it! Sebastiaan really hopes everyone enjoys his book.
reader@ubuntu:~/scripts/chapter_08$

如你所见,我们使用name变量不是一次,而是三次。如果我们没有适当的变量,并且我们需要编辑名称,我们将需要搜索文本中使用该名称的每个地方。

此外,如果我们在其中一个地方犯了拼写错误,写 Sebastian 而不是 Sebastiaan (如果你感兴趣的话,这种情况经常发生*),阅读文本和编辑文本都需要付出更多的努力。此外,这是一个简单的例子:变量经常被使用多次(至少三次以上)。*

此外,变量通常用于存储程序的状态*。对于 Bash 脚本,您可以想象创建一个临时目录,您将在其中执行一些操作。我们可以将这个临时目录的位置存储在一个变量中,我们在临时目录中需要做的任何事情都会利用这个变量来找到位置。程序完成后,应该清理临时目录,不再需要该变量。对于程序的每次运行,临时目录将被不同地命名,因此变量的内容将不同,或者变量也不同。

变量的另一个优点是它们有名字。正因为如此,如果我们创建一个描述性的名称,我们可以使应用更容易阅读和使用。我们已经确定可读性始终是 shell 脚本的必备条件,使用正确命名的变量有助于我们做到这一点。

变量还是常量?

在到目前为止的例子中,我们实际上使用了变量作为常数。术语变量意味着它可以改变,而我们的例子总是在脚本的开始分配一个变量,并一直使用它。虽然这有其自身的优点(如前所述,为了一致性或更容易编辑),但它还没有充分利用变量的力量。

常数是一个变量,但是一种特殊的类型。简单来说,一个常量就是在脚本开始时定义的一个变量,不受用户输入的影响,在执行过程中不改变值。

在本章的后面,当我们讨论处理用户输入时,我们将看到真正的变量。在那里,变量的内容是由脚本的调用者提供的,这意味着每次调用脚本时,脚本的输出都会不同,或者说是变化的。在本书的后面,当我们描述条件测试时,我们甚至会根据同一个脚本中的逻辑,在脚本本身的过程中更改变量值。

变量命名

关于命名的问题。您可能已经注意到了我们到目前为止所看到的变量的一些情况:Bash 变量PATHBASH_VERSION是完全大写的,但是在我们的示例中,我们使用了小写,单词用下划线(hello_text)分隔。考虑以下示例:

#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-08
# Description: Showing off different styles of variable naming.
# Usage: ./variable-naming.sh
#####################################

# Assign the variables.
name="Sebastiaan"
home_type="house"
LOCATION="Utrecht"
_partner_name="Sanne"
animalTypes="gecko and hamster"

# Print the story.
echo "${name} lives in a ${home_type} in ${LOCATION}, together with ${_partner_name} and their two pets: a ${animalTypes}."

如果我们运行这个,我们会得到一个很好的小故事:

reader@ubuntu:~/scripts/chapter_08$ bash variable-naming.sh 
Sebastiaan lives in a house in Utrecht, together with Sanne and their two pets: a gecko and hamster.

所以,我们的变量很有效!从技术上讲,我们在这个例子中所做的一切都很好。然而,他们看起来一团糟。我们使用了四种不同的命名约定:小写带下划线,大写,小写,最后是 camelCase。虽然这些在技术上是有效的,但请记住可读性很重要:最好选择一种命名变量的方式,并坚持使用这种方式。

正如你所料,关于这一点有很多观点(可能和标签和空格的争论一样多!).显然,我们也有一个观点,想分享一下:正则变量使用小写 _ 分隔 _ by _ 下划线,常量使用大写。从现在开始,您将在所有进一步的脚本中看到这种做法。

前面的示例如下所示:

reader@ubuntu:~/scripts/chapter_08$ cp variable-naming.sh variable-naming-proper.sh
reader@ubuntu:~/scripts/chapter_08$ vim variable-naming-proper.sh
vim variable-naming-proper.sh
reader@ubuntu:~/scripts/chapter_08$ cat variable-naming-proper.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-08
# Description: Showing off uniform variable name styling.
# Usage: ./variable-naming-proper.sh
#####################################

NAME="Sebastiaan"
HOME_TYPE="house"
LOCATION="Utrecht"
PARTNER_NAME="Sanne"
ANIMAL_TYPES="gecko and hamster"

# Print the story.
echo "${NAME} lives in a ${HOME_TYPE} in ${LOCATION}, together with ${PARTNER_NAME} and their two pets: a ${ANIMAL_TYPES}."

我们希望你同意这个看起来更好。在本章的后面,当我们介绍用户输入时,我们也将使用普通变量,这与我们目前使用的常量相反。

Whatever you decide upon when naming your variables, there is only one thing in the end that really matters: consistency. Whether you prefer lowercase, camelCase, or UPPERCASE, it has no impact on the script itself (except for certain readability pros and cons, as discussed). However, using multiple naming conventions at the same time greatly confuses things. Always make sure to pick a convention wisely, and then stick to it!

为了保持整洁,我们通常避免使用大写变量,常量除外。这主要是因为(几乎)Bash 中所有环境变量都是大写的。如果您在脚本中使用大写变量,有一件重要的事情要记住:确保您选择的名称不会与预先存在的 Bash 变量冲突。这些包括PATHUSERLANGSHELLHOME等等。如果您在脚本中使用相同的名称,您可能会得到一些意想不到的行为。

避免这些冲突并为变量选择唯一的名称是一个更好的主意。例如,你可以选择SCRIPT_PATH变量而不是PATH

处理用户输入

到目前为止,我们一直在处理真正静态的脚本。虽然有一个故事可供每个人打印出来很有趣,但它很难称得上是一个功能性的 shell 脚本。至少,这不是你会经常使用的东西!所以,我们想在 shell 脚本中引入一个非常重要的概念:用户输入

基本输入

在一个非常基本的层次上,调用脚本后放在命令行上的所有内容都可以用作输入。但是,使用它取决于脚本!例如,考虑以下情况:

reader@ubuntu:~/scripts/chapter_08$ ls
hello-int.sh hello-world-variable.sh name.sh variable-naming-proper.sh variable-naming.sh
reader@ubuntu:~/scripts/chapter_08$ bash name.sh 
There once was a guy named Sebastiaan. Sebastiaan enjoyed Linux and Bash so much that he wrote a book about it! Sebastiaan really hopes everyone enjoys his book.
reader@ubuntu:~/scripts/chapter_08$ bash name.sh Sanne
There once was a guy named Sebastiaan. Sebastiaan enjoyed Linux and Bash so much that he wrote a book about it! Sebastiaan really hopes everyone enjoys his book

当我们第一次调用name.sh时,我们使用了最初打算的功能。第二次调用时,我们提供了一个额外的参数:Sanne。然而,因为脚本根本不解析用户输入,所以我们看到的输出完全相同。

让我们修改name.sh脚本,以便它实际上使用我们在调用脚本时指定的额外输入:

reader@ubuntu:~/scripts/chapter_08$ cp name.sh name-improved.sh
reader@ubuntu:~/scripts/chapter_08$ vim name-improved.sh
reader@ubuntu:~/scripts/chapter_08$ cat name-improved.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-08
# Description: Script to show why we need variables; now with user input!
# Usage: ./name-improved.sh <name>
#####################################

# Assign the name to a variable.
name=${1}

# Print the story.
echo "There once was a guy named ${name}. ${name} enjoyed Linux and Bash so much that he wrote a book about it! ${name} really hopes everyone enjoys his book."

reader@ubuntu:~/scripts/chapter_08$ bash name-improved.sh Sanne
There once was a guy named Sanne. Sanne enjoyed Linux and Bash so much that he wrote a book about it! Sanne really hopes everyone enjoys his book.

现在,看起来好多了!脚本现在接受用户输入;具体来说,就是这个人的名字。它通过使用$1构造来实现:这是第一个位置参数。我们称这些论点为位置性的,因为位置很重要:第一个永远写给$1,第二个写给$2,等等。我们没有办法交换这些。只有当我们开始考虑让我们的脚本与 flags 兼容时,我们才会获得更多的灵活性。如果我们为脚本提供更多的参数,我们可以使用$3$4等等来获取它们。

您可以提供的参数数量是有限制的。然而,它足够高,你永远不必真正担心它。如果你做到了这一点,你的脚本将会非常笨拙,以至于没有人会使用它!

You might want to pass a sentence to a Bash script, as one argument. In this case, you need to enclose the entire sentence in single or double quotes if you want to have it interpreted as a single positional argument. If you do not, Bash will consider each space in your sentence the delimiter between the arguments; passing the sentence This Is Cool will result in three arguments to the script: This, Is, and Cool.

请注意,我们再次更新了标题,在用法下包含了新的输入。然而,从功能上来说,这个脚本并没有那么好;我们用了带有女性名字的男性代词!让我们快速解决这个问题,看看如果我们现在忽略用户输入会发生什么:

reader@ubuntu:~/scripts/chapter_08$ vim name-improved.sh 
reader@ubuntu:~/scripts/chapter_08$ tail name-improved.sh 
# Date: 2018-09-08
# Description: Script to show why we need variables; now with user input!
# Usage: ./name-improved.sh
#####################################

# Assign the name to a variable.
name=${1}

# Print the story.
echo "There once was a person named ${name}. ${name} enjoyed Linux and Bash so much that he/she wrote a book about it! ${name} really hopes everyone enjoys his/her book."

reader@ubuntu:~/scripts/chapter_08$ bash name-improved.sh 
There once was a person named .  enjoyed Linux and Bash so much that he/she wrote a book about it!  really hopes everyone enjoys his/her book.

因此,我们使文本更加中性。然而,当我们在没有提供名称作为参数的情况下调用脚本时,我们打乱了输出。在下一章中,我们将深入探讨错误检查和输入验证,但是现在请记住,如果变量缺失/为空,Bash 将不会提供错误;你完全有责任处理这件事。我们将在下一章中进一步讨论这一点,因为这是 shell 脚本中另一个非常重要的主题。

参数和自变量

我们需要后退一小步,讨论一些术语——参数和参数。这并不是非常复杂,但可能会有点混乱,而且它们有时会被错误地使用。

基本上,争论是你传递给剧本的东西。你在脚本中定义的被认为是参数*。*看看下面的例子,看看这是如何工作的:

reader@ubuntu:~/scripts/chapter_08$ vim arguments-parameters.sh
reader@ubuntu:~/scripts/chapter_08$ cat arguments-parameters.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-08
# Description: Explaining the difference between argument and parameter.
# Usage: ./arguments-parameters.sh <argument1> <argument2>
#####################################

parameter_1=${1}
parameter_2=${2}

# Print the passed arguments:
echo "This is the first parameter, passed as an argument: ${parameter_1}"
echo "This is the second parameter, also passed as an argument: ${parameter_2}"

reader@ubuntu:~/scripts/chapter_08$ bash arguments-parameters.sh 'first-arg' 'second-argument'
This is the first parameter, passed as an argument: first-arg
This is the second parameter, also passed as an argument: second-argument

我们以这种方式使用的变量在脚本中被称为参数,但是在将它们传递给脚本时被称为参数。在我们的name-improved.sh脚本中,参数是name变量。这是静态的,并且绑定到脚本版本。然而,每次运行脚本时,争论都是不同的:它可以是Sebastiaan,或Sanne,或任何其他名称。

Remember, when we are talking about an argument, you can read that as a runtime argument; something that can be different each run. If we're talking about a parameter of the script, we're referring to the static piece of information expected by a script (which is often provided by a runtime argument, or some logic in the script).

交互式和非交互式脚本

到目前为止,我们创建的脚本使用用户输入,但它不能真正称为交互式。一旦脚本被触发,无论参数是否有参数,脚本都会运行并完成。

但是,如果我们不想使用一长串参数,而是提示用户输入所需的信息呢?

进入read命令。read的基本用法是查看来自命令行的输入,并将其存储在REPLY变量中。自己试试吧:

reader@ubuntu:~$ read
This is a random sentence!
reader@ubuntu:~$ echo $REPLY
This is a random sentence!
reader@ubuntu:~$

在您启动read命令后,您的终端将进入一行,并允许您键入您想要的任何内容。一旦点击进入(或者,实际上,直到 Bash 遇到换行符键),输入将被保存到REPLY变量中。然后,您可以回显该变量,以验证它是否实际存储了您的文本。

read有几个有趣的标志,这使得它在 shell 脚本中更有用。我们可以使用带有参数的-p标志(要显示的文本,用引号括起来)向用户显示提示,并且我们可以提供变量的名称作为最后一个参数,我们希望在该变量中存储响应:

reader@ubuntu:~$ read -p "What day is it? "
What day is it? Sunday
reader@ubuntu:~$ echo ${REPLY}
Sunday
reader@ubuntu:~$ read -p "What day is it? " day_of_week
What day is it? Sunday
reader@ubuntu:~$ echo ${day_of_week}
Sunday

在前面的例子中,我们首先使用read -p而没有指定一个变量来保存我们的响应。在这种情况下,read的默认行为将其置于REPLY变量中。一行之后,我们用文字day_of_week结束了read命令。在这种情况下,完整的响应被保存到一个同名的变量中,如后面的echo ${day_of_week}所示。

现在让我们在一个实际的脚本中使用read。我们将首先使用read创建脚本,然后使用到目前为止的位置参数:

reader@ubuntu:~/scripts/chapter_08$ vim interactive.sh
reader@ubuntu:~/scripts/chapter_08$ cat interactive.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-09
# Description: Show of the capabilities of an interactive script.
# Usage: ./interactive.sh
#####################################

# Prompt the user for information.
read -p "Name a fictional character: " character_name
read -p "Name an actual location: " location
read -p "What's your favorite food? " food

# Compose the story.
echo "Recently, ${character_name} was seen in ${location} eating ${food}!

reader@ubuntu:~/scripts/chapter_08$ bash interactive.sh
Name a fictional character: Donald Duck
Name an actual location: London
What's your favorite food? pizza
Recently, Donald Duck was seen in London eating pizza!

结果很好。用户可以直接调用脚本,而不用考虑如何使用它,并进一步得到信息提示。现在,让我们复制并编辑这个脚本,并使用位置参数来提供信息:

reader@ubuntu:~/scripts/chapter_08$ cp interactive.sh interactive-arguments.sh
reader@ubuntu:~/scripts/chapter_08$ vim interactive-arguments.sh 
reader@ubuntu:~/scripts/chapter_08$ cat interactive-arguments.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-09
# Description: Show of the capabilities of an interactive script, 
# using positional arguments.
# Usage: ./interactive-arguments.sh <fictional character name> 
# <actual location name> <your favorite food>
#####################################

# Initialize the variables from passed arguments.
character_name=${1}
location=${2}
food=${3}

# Compose the story.
echo "Recently, ${character_name} was seen in ${location} eating ${food}!"

reader@ubuntu:~/scripts/chapter_08$ bash interactive-arguments.sh "Mickey Mouse" "Paris" "a hamburger"
Recently, Mickey Mouse was seen in Paris eating a hamburger!

首先,我们将interactive.sh脚本复制到interactive-arguments.sh。我们编辑了这个脚本,不再使用read,而是从传递给脚本的参数中获取值。我们用新名称和新用法编辑了标题,并通过提供另一组参数来运行它。又一次,我们被呈现了一个美好的小故事。

所以,你可能会想,什么时候应该用哪种方法?两种方法都以相同的结果结束。然而,就我们而言,这两个脚本的可读性和易用性并不相同。查看下表,了解每种方法的优缺点:

| | 优点 | cons | | 阅读 |

  • Users do not need to know the parameters to be provided; They just need to run the script and be prompted with any information they need.
  • It is impossible to forget to provide information.

|

  • If you want to repeat the script several times, you need to enter the response each time.
  • Cannot run interactively; For example, in a predetermined job

| | 争论 |

  • Can be easily repeated.
  • It can also run interactively.

|

  • User is trying to run script.
  • Before, I needed to know the arguments for providing and forgot to provide some required information

要容易得多 |

基本上,一种方法的优点就是另一种方法的缺点,反之亦然。似乎用这两种方法我们都赢不了。那么,我们如何创建一个健壮的交互式脚本,我们也可以非交互式地运行它呢?

组合位置参数和读取

当然是通过两种方法的结合!在我们开始执行脚本的实际功能之前,我们需要验证是否已经提供了所有必要的信息。如果没有,我们可以提示用户输入缺失的信息。

我们将稍微展望一下第 11 章条件测试和脚本循环,并解释if-then逻辑的基本用法。我们将把这个和test命令结合起来,我们可以用它来检查一个变量是包含一个值还是空的。如果是这样的话,那么我们可以用read提示用户提供缺失的信息。

从本质上来说,if-then逻辑无非是说if <something>, then do <something>。在我们的示例中,if的变量character_name为空,then使用read提示该信息。我们将对脚本中的所有三个参数执行此操作。

Because the arguments we're supplying are positional, we cannot supply the first and the third only; the script would interpret that as the first and second argument, with a missing third argument. With our current knowledge, we're limited by this. In Chapter 15, Parsing Bash Script Arguments with getopts, we'll explore how to supply information using flags. In this case, we can supply all information separately, without worrying about the order. For now, however, we'll have to live with the limitation!

在我们解释test命令之前,我们需要稍微返回并解释一下退出代码。基本上,每个运行和退出的程序都会向最初启动它的父进程返回一个代码。正常情况下,如果一个过程已经完成并且执行成功,它会以代码 0 退出。如果程序执行不成功,则退出任何其他代码;但是,这通常是代码 1 。虽然退出代码有约定,但通常你会遇到好退出为 0,坏退出为 1 的情况。

当我们使用test命令时,它也会生成符合指导原则的退出代码:如果测试成功,我们会看到退出代码 0。如果不是,我们会看到另一个代码(可能是 1)。用echo $?命令可以看到上一条命令的退出代码。

让我们看一个例子:

reader@ubuntu:~/scripts/chapter_08$ cd
reader@ubuntu:~$ ls -l
total 8
-rw-rw-r-- 1 reader reader    0 Aug 19 11:54 emptyfile
drwxrwxr-x 4 reader reader 4096 Sep  1 09:51 scripts
-rwxrwxr-x 1 reader reader   23 Aug 19 11:54 textfile.txt
reader@ubuntu:~$ mkdir scripts
mkdir: cannot create directory ‘scripts’: File exists
reader@ubuntu:~$ echo $?
1
reader@ubuntu:~$ mkdir testdir
reader@ubuntu:~$ echo $?
0
reader@ubuntu:~$ rmdir testdir/
reader@ubuntu:~$ echo $?
0
reader@ubuntu:~$ rmdir scripts/
rmdir: failed to remove 'scripts/': Directory not empty
reader@ubuntu:~$ echo $?
1

在前面的例子中发生了很多事情。首先,我们尝试创建一个已经存在的目录。由于我们不能有两个同名的目录(在同一个位置),因此mkdir命令失败。当我们使用$?打印出口代码时,我们被退回1

接下来,我们成功创建了一个新目录testdir。当我们在该命令后打印退出代码时,我们看到了成功的数字:0。成功清空testdir后,我们再次看到0的退出代码。当我们试图用rmdir删除非空的scripts目录时(这是不允许的),我们收到一条错误消息,看到退出代码又是1

让我们回到test上来。我们需要做的是验证一个变量是否为空。如果是,我们想启动read提示,让用户输入填写。首先我们将在${PATH}变量上尝试这个(它永远不会是空的),然后在empty_variable上尝试,它确实会是空的。为了测试变量是否为空,我们使用test -z <variable name>:

reader@ubuntu:~$ test -z ${PATH}
reader@ubuntu:~$ echo $?
1
reader@ubuntu:~$ test -z ${empty_variable}
reader@ubuntu:~$ echo $?
0

虽然一开始这看起来可能是错误的方式,但是想想看。我们正在测试变量是否为空。由于$PATH不为空,测试失败并产生退出代码 1。对于${empty_variable}(我们从来没有创建过),我们确定它确实是空的,退出代码 0 证实了这一点。

如果我们想将 Bash iftest结合起来,我们需要知道if期望测试以退出代码 0 结束。所以,如果测试成功,我们可以做点什么。这完全符合我们的例子,因为我们测试的是空变量。如果你想反过来测试,你需要测试一个非零长度变量,这是test-n标志。

我们先来看看if语法。本质上看起来是这样的:if <exit code 0>; then <do something>; fi。您可以选择在多行上有这个,但是使用;在线上也终止它。让我们看看我们是否可以根据自己的需要来操纵它:

reader@ubuntu:~$ if test -z ${PATH}; then read -p "Type something: " PATH; fi
reader@ubuntu:~$ if test -z ${empty_variable}; then read -p "Type something: " empty_variable; fi
Type something: Yay!
reader@ubuntu:~$ echo ${empty_variable} 
Yay!
reader@ubuntu:~$ if test -z ${empty_variable}; then read -p "Type something: " empty_variable; fi
reader@ubuntu:~

首先,我们在PATH变量上使用了我们构造的if-then子句。既然不是空的,我们也没想到会有提示:好事我们没有得到!我们使用了相同的构造,但是现在使用了empty_variable。看,由于test -z返回退出代码 0,if-then子句的then部分被执行,并提示我们输入一个值。输入值后,我们可以将其回显出来。再次运行if-then子句没有给我们read提示,因为此时变量empty_variable不再为空!

最后,让我们将这个if-then逻辑合并到我们的new interactive-ultimate.sh脚本中:

reader@ubuntu:~/scripts/chapter_08$ cp interactive.sh interactive-ultimate.sh
reader@ubuntu:~/scripts/chapter_08$ vim interactive-ultimate.sh 
reader@ubuntu:~/scripts/chapter_08$ cat interactive-ultimate.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-09-09
# Description: Show the best of both worlds!
# Usage: ./interactive-ultimate.sh [fictional-character-name] [actual-
# location] [favorite-food]
#####################################

# Grab arguments.
character_name=$1
location=$2
food=$3

# Prompt the user for information, if it was not passed as arguments.
if test -z ${character_name}; then read -p "Name a fictional character: " character_name; fi
if test -z ${location}; then read -p "Name an actual location: " location; fi
if test -z ${food}; then read -p "What's your favorite food? " food; fi

# Compose the story.
echo "Recently, ${character_name} was seen in ${location} eating ${food}!"

reader@ubuntu:~/scripts/chapter_08$ bash interactive-ultimate.sh 
"Goofy"

Name an actual location: Barcelona
What's your favorite food? a hotdog
Recently, Goofy was seen in Barcelona eating a hotdog!

成功!我们被提示输入locationfood,但是character_name成功地从我们通过的争论中解决了。我们已经创建了一个脚本,既可以完全交互使用,无需提供参数,也可以不交互使用参数。

While this script is informative, it is not really efficient. It would be better to combine the test looking directly at the passed arguments ($1, $2, $3), so we only need one line. Later on in the book, we will start using such optimizations, but for now it is more important to write things out in full, so you can more easily understand them!

摘要

在这一章的开始,我们解释了什么是变量:一个允许我们存储信息的标准构建块,我们可以稍后引用。我们更喜欢使用变量的原因有很多:我们可以一次存储一个值并多次引用它,如果我们需要更改该值,我们只需更改一次,新值就会在任何地方使用。

我们解释了常量是一种特殊类型的变量:它在脚本的开头只定义一次,不受用户输入的影响,并且在脚本执行过程中不会改变。

我们继续讨论变量命名的一些注意事项。我们证明了 Bash 在变量方面非常灵活:它允许许多不同风格的变量命名。然而,我们解释说,如果在同一个脚本中或者在多个脚本之间使用多个不同的命名约定,可读性会受到影响。最好的办法是选择一种命名变量的方式,并坚持下去。我们建议常量使用大写,所有其他变量使用小写下划线。这将减少局部变量和环境变量之间冲突的机会。

接下来,我们探讨了用户输入以及如何处理它。我们给了脚本用户改变脚本结果的能力,这个功能对于大多数现实生活中的功能脚本来说几乎是强制性的。我们描述了两种不同的用户交互方法:使用位置参数的基本输入和使用read构造的交互输入。

本章最后我们简单介绍了 if**–**然后是逻辑和test命令。在介绍了单独使用的每种方法的优缺点后,我们使用这些概念创建了一种健壮的方法来处理用户输入,将位置参数与缺失信息的read提示相结合。这创建了一个可以交互和非交互使用的脚本,具体取决于用例。

本章介绍了以下命令:readtestif

问题

  1. 什么是变量?
  2. 为什么我们需要变量?
  3. 什么是常数?
  4. 为什么命名约定对变量特别重要?
  5. 什么是位置论点?
  6. 参数和参数有什么区别?
  7. 怎样才能让一个剧本互动起来?
  8. 我们如何创建一个既可以非交互使用又可以交互使用的脚本?

进一步阅读

如果您想深入了解本章的主题,以下资源可能会很有意思: