Skip to content

Latest commit

 

History

History
1054 lines (812 loc) · 56.3 KB

File metadata and controls

1054 lines (812 loc) · 56.3 KB

十三、函数

在本章中,我们将解释 Bash 脚本的一个非常实用的概念:函数。我们将展示它们是什么,我们如何使用它们,以及我们为什么想要使用它们。

在介绍了函数的基础之后,我们将更进一步,我们将展示函数如何拥有自己的输入和输出。

将描述函数库的概念,我们将开始构建我们自己的包含各种实用函数的个人函数库。

本章将介绍以下命令:topfreedeclarecaserevreturn

本章将涵盖以下主题:

  • 功能解释
  • 用参数扩充函数
  • 函数库

技术要求

本章所有脚本均可在 GitHub:https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter13 上找到。除了您的 Ubuntu Linux 虚拟机之外,不需要其他资源来完成本章中的示例。对于参数-checker.sh、函数和变量. sh、库-重定向到文件. sh 脚本,只有最终版本是在线的。在您的系统上执行之前,请确保验证标题中的脚本版本。

功能解释

在本章中,我们将研究函数,以及这些函数如何增强您的脚本。函数的理论并不太复杂:函数是组合在一起的一组命令,可以多次调用(执行),而不必再次编写整个命令集。一如既往,一个好的例子胜过千言万语,所以让我们来看看我们最喜欢的一个例子:打印Hello world!

你好世界!

我们现在知道让Hello world!这个词出现在我们的终端上是相对容易的。一个简单的echo "Hello world!"就可以了。然而,如果我们想多次这样做,我们会怎么做呢?您可以建议使用任何类型的循环,这确实允许我们打印多次。然而,这个循环也需要一些额外的代码和预先的计划。正如您将注意到的,实际上循环非常适合迭代项目,但不完全适合以可预测的方式重用代码。让我们看看如何使用函数来实现这一点:

reader@ubuntu:~/scripts/chapter_13$ vim hello-world-function.sh
reader@ubuntu:~/scripts/chapter_13$ cat hello-world-function.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-11
# Description: Prints "Hello world!" using a function.
# Usage: ./hello-world-function.sh
#####################################

# Define the function before we call it.
hello_world() {
  echo "Hello world!"
}

# Call the function we defined earlier:
hello_world

reader@ubuntu:~/scripts/chapter_13$ bash hello-world-function.sh 
Hello world!

如您所见,我们首先定义了函数,这无非是编写一旦调用函数就应该执行的命令。在脚本的最后,您可以看到我们通过输入函数名来执行函数,就像我们执行任何其他命令一样。需要注意的是,如果您之前已经定义了函数,则只能调用该函数*。这意味着整个函数定义在脚本中需要高于它的调用。现在,我们将把所有函数作为脚本中的第一项。在本章的后面,我们将向您展示如何更有效地利用这一点。*

您在前面的示例中看到的是 Bash 中函数定义的两种可能语法中的第一种。如果我们只提取函数,语法如下:

function_name() {
   indented-commands
   further-indented-commands-as-needed
 }

第二种可能的语法是这样的,我们不太喜欢前一种语法:

function function_name {
   indented-commands
   further-indented-commands-as-needed
 }

这两种语法的区别在于,要么在开头没有单词function,要么在函数名后面没有单词()。我们更喜欢第一种语法,它使用了()符号,因为它更接近于其他脚本/编程语言的符号,因此对大多数人来说应该更容易识别。另外,它比第二种符号更短更简单。正如您所料,我们将在本书的其余部分继续只使用第一个符号;另一个是为了完整性而提出的(如果您在研究脚本时在网上遇到它,理解它总是很方便的!).

Remember, we use indentation to relay information about where commands are nested to the reader of a script. In this case, since all commands within a function are only run when the function is called, we indent them with two spaces so it's clear we're inside the function.

更复杂

一个函数可以有任意多的命令。在我们简单的例子中,我们只添加了一个echo,然后我们只调用了一次。虽然这对于抽象来说很好,但它并不能真正保证创建一个函数。让我们看一个更复杂的例子,它会让您更好地理解为什么在函数中抽象命令是一个好主意:

reader@ubuntu:~/scripts/chapter_13$ vim complex-function.sh 
reader@ubuntu:~/scripts/chapter_13$ cat complex-function.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-11
# Description: A more complex function that shows why functions exist.
# Usage: ./complex-function.sh
#####################################

# Used to print some current data on the system.
print_system_status() {
  date # Print the current datetime.
  echo "CPU in use: $(top -bn1 | grep Cpu | awk '{print $2}')"
  echo "Memory in use: $(free -h | grep Mem | awk '{print $3}')"
  echo "Disk space available on /: $(df -k / | grep / | awk '{print $4}')" 
  echo # Extra newline for readability.
}

# Print the system status a few times.
for ((i=0; i<5; i++)); do
  print_system_status
  sleep 5
done

现在我们说话了!这个函数有五个命令,其中三个包括用链式管道替换命令。现在,我们的脚本开始变得复杂而强大。如您所见,我们使用()符号定义函数。然后我们在一个 C 风格的for循环中调用这个函数,这使得脚本打印系统状态五次,中间有五秒钟的停顿(由于sleep,我们在前面的第 11 章条件测试和脚本循环中看到了这一点)。运行此程序时,它应该如下所示:

reader@ubuntu:~/scripts/chapter_13$ bash complex-function.sh 
Sun Nov 11 13:40:17 UTC 2018
CPU in use: 0.1
Memory in use: 85M
Disk space available on /: 4679156

Sun Nov 11 13:40:22 UTC 2018
CPU in use: 0.2
Memory in use: 84M
Disk space available on /: 4679156

除了日期,其他输出发生显著变化的可能性很小,除非您有其他进程正在运行。然而,功能的目的应该是明确的:以透明的方式定义和抽象一组功能。

While not the topic of this chapter, we used a few new commands here. The top and free commands are often used to check how the system is performing, and can be used without any arguments (top opens full screen, which you can exit with *Ctrl *+ C). In the Further reading section of this chapter, you can find more on these (and other) performance monitoring tools in Linux. We've also included a primer on awk there.

使用函数有很多好处;这些包括但不限于以下内容:

  • 易于重用的代码
  • 允许共享代码(例如,通过库)
  • 将混乱的代码抽象成简单的函数调用

函数中很重要的一点就是命名。函数名应该尽可能简洁,但是仍然需要告诉用户它是做什么的。例如,如果你把一个函数叫做非描述性的东西,比如function1,怎么会有人知道它是干什么的呢?将其与我们在示例中看到的名称进行比较:print_system_status。虽然可能不完美(什么是系统状态?),它至少为我们指明了正确的方向(如果您同意 CPU、内存和磁盘使用被视为系统状态的一部分,也就是说)。或许这个功能更好的名字是print_cpu_mem_disk。由你决定!做这个选择的时候一定要考虑到目标受众是谁;这往往影响最大。

虽然描述性在函数命名中非常重要,但遵守命名约定也很重要。我们已经在第 8 章变量和用户输入中提出了同样的考虑,当我们处理变量命名时。重申一下:最重要的规则就是要一致。如果你想要我们对函数命名约定的建议,坚持我们为变量设计的命名约定:小写,用下划线分隔。这是我们在前面的例子中使用的,也是我们将在本书的其余部分中继续展示的。

可变范围

虽然函数很棒,但有些东西我们之前已经学过,我们需要在函数的范围内重新考虑,最明显的是变量。我们知道变量存储的信息可以在脚本中的多个点被多次访问或变异。然而,我们还没有了解到的是,变量总是有范围的。默认情况下,变量的范围是全局,这意味着它们可以在整个脚本中的任何点使用。功能的引入也带来了新的范围:本地。局部变量在函数中定义,并随着函数调用而生存和消亡。让我们来看看这是怎么回事:

reader@ubuntu:~/scripts/chapter_13$ vim functions-and-variables.sh
reader@ubuntu:~/scripts/chapter_13$ cat functions-and-variables.sh 
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-11
# Description: Show different variable scopes.
# Usage: ./functions-and-variables.sh <input>
#####################################

# Check if the user supplied at least one argument.
if [[ $# -eq 0 ]]; then
  echo "Missing an argument!"
  echo "Usage: $0 <input>"
  exit 1
fi

# Assign the input to a variable.
input_variable=$1
# Create a CONSTANT, which never changes.
CONSTANT_VARIABLE="constant"

# Define the function.
hello_variable() {
  echo "This is the input variable: ${input_variable}"
  echo "This is the constant: ${CONSTANT_VARIABLE}"
}

# Call the function.
hello_variable
reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh teststring
This is the input variable: teststring
This is the constant: constant

目前为止,一切顺利。我们可以在函数中使用我们的全局常数。这并不奇怪,因为它不被轻称为全局变量;它可以在脚本中的任何地方使用。现在,让我们看看当我们在函数中添加一些额外的变量时会发生什么:

#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.1.0
# Date: 2018-11-11
# Description: Show different variable scopes.
# Usage: ./functions-and-variables.sh <input>
#####################################
<SNIPPED>
# Define the function.
hello_variable() {
 FUNCTION_VARIABLE="function variable text!"
  echo "This is the input variable: ${input_variable}"
  echo "This is the constant: ${CONSTANT_VARIABLE}"
 echo "This is the function variable: ${FUNCTION_VARIABLE}"
}

# Call the function.
hello_variable

# Try to call the function variable outside the function.
echo "Function variable outside function: ${FUNCTION_VARIABLE}"

你认为现在会发生什么?试一试:

reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh input
This is the input variable: input
This is the constant: constant
This is the function variable: function variable text!
Function variable outside function: function variable text!

与您可能怀疑的相反,我们在函数内部定义的变量实际上仍然是一个全局变量(抱歉欺骗您!).如果我们想使用本地范围的变量,我们需要添加内置的本地 shell:

#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.2.0
# Date: 2018-11-11
# Description: Show different variable scopes.
# Usage: ./functions-and-variables.sh <input>
#####################################
<SNIPPED>
# Define the function.
hello_variable() {
 local FUNCTION_VARIABLE="function variable text!"
  echo "This is the input variable: ${input_variable}"
  echo "This is the constant: ${CONSTANT_VARIABLE}"
  echo "This is the function variable: ${FUNCTION_VARIABLE}"
}
<SNIPPED>

现在,如果我们这次执行它,我们实际上会看到脚本在最后一个命令中表现不佳:

reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh more-input
This is the input variable: more-input
This is the constant: constant
This is the function variable: function variable text!
Function variable outside function: 

由于局部加法,我们现在只能在函数内部使用变量及其内容。所以,当我们调用hello_variable函数时,我们看到了变量的内容,但是当我们试图在echo "Function variable outside function: ${FUNCTION_VARIABLE}"中的函数外打印它时,我们看到它是空的。这是期望和可取的行为。你能做的,有时真的很方便的是:

#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.3.0
# Date: 2018-11-11
# Description: Show different variable scopes.
# Usage: ./functions-and-variables.sh <input>
#####################################
<SNIPPED>
# Define the function.
hello_variable() {
 local CONSTANT_VARIABLE="maybe not so constant?"
  echo "This is the input variable: ${input_variable}"
  echo "This is the constant: ${CONSTANT_VARIABLE}"
}

# Call the function.
hello_variable

# Try to call the function variable outside the function.
echo "Function variable outside function: ${CONSTANT_VARIABLE}"

现在,我们已经定义了一个局部范围的变量*,其名称与我们已经初始化的全局范围的变量*相同!您可能知道接下来会发生什么,但请务必运行脚本并了解发生这种情况的原因:

reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh last-input
This is the input variable: last-input
This is the constant: maybe not so constant?
Function variable outside function: constant

因此,当我们在函数中使用CONSTANT_VARIABLE变量(记住,常数仍然被认为是变量,尽管是特殊的变量)时,它打印了局部作用域变量的值:maybe not so constant?。当在函数之外时,在脚本的主体中,我们再次打印变量的值,并且我们得到了最初定义的值:constant

你可能很难想象这个用例。虽然我们同意您可能不会经常使用它,但它确实有它的位置。例如,想象一个复杂的脚本,其中一个全局变量被多个函数和命令顺序使用。现在,您可能会遇到这样一种情况,您需要变量的值,但需要稍加修改才能在函数中正确使用它。您还知道函数/命令需要原始值。现在,您可以将内容复制到一个新的变量中并使用它,但是通过在一个函数中覆盖变量,您可以让读者/用户更清楚地知道您有这样做的目的;这是一个明智的决定,你知道你需要那个例外来完成那个功能。使用局部变量(最好像往常一样带有注释)将确保可读性!

Variables can be set read-only by using the declare built-in shell. If you check the help, with help declare, you'll see it described as 'Set variable values and attributes'. A read-only variable such as a constant can be created by replacing CONSTANT=VALUE with declare -r CONSTANT=VALUE. If you do this, you can no longer (temporarily) override a variable with a local instance; Bash will give you an error. In practice, the declare command is not used too much as far as we have encountered, but it can serve useful purposes besides read-only declarations, so be sure to give it a look!

实例

在本章的下一部分介绍函数参数之前,我们将首先研究一个不需要参数的函数的实际例子。我们将回到之前创建的脚本,看看是否有一些功能可以抽象为函数。剧透提醒:有一个很棒的,它处理一个叫做错误处理的小东西!

错误处理

第 9 章错误检查和处理中,我们创建了以下结构:command || { echo "Something went wrong."; exit 1; }。正如您(希望)记得的那样,||语法意味着只有当左侧的命令具有非0的退出状态时,右侧的所有内容才会被执行。虽然这种设置运行良好,但并没有增加可读性。如果我们能把错误处理抽象成一个函数,并调用那个函数,那就更好了!就这么办吧:

reader@ubuntu:~/scripts/chapter_13$ vim error-functions.sh
reader@ubuntu:~/scripts/chapter_13$ cat error-functions.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-11
# Description: Functions to handle errors.
# Usage: ./error-functions.sh
#####################################

# Define a function that handles minor errors.
handle_minor_error() {
 echo "A minor error has occured, please check the output."
}

# Define a function that handles fatal errors.
handle_fatal_error() {
 echo "A critical error has occured, stopping script."
 exit 1
}

# Minor failures.
ls -l /tmp/ || handle_minor_error
ls -l /root/ || handle_minor_error 

# Fatal failures.
cat /etc/shadow || handle_fatal_error
cat /etc/passwd || handle_fatal_error

这个脚本定义了两个函数:handle_minor_errorhandle_fatal_error。对于一个小错误,我们将打印一条消息,但是脚本执行不会停止。然而,一个致命的错误被认为是如此严重,以至于脚本的流程预计会被中断;在这种情况下,继续脚本是没有用的,所以我们将确保函数停止它。通过使用结合了||构造的函数,我们不需要检查函数内部的退出代码;只有当退出代码不是0时,我们才会在函数中结束,所以我们已经知道我们处于错误的情况。在我们执行这个脚本之前,花点时间来思考一下我们用这些功能提高了多少可读性。完成后,运行带有调试输出的脚本,这样您就可以遵循整个流程:

reader@ubuntu:~/scripts/chapter_13$ bash -x error-functions.sh 
+ ls -l /tmp/
total 8
drwx------ 3 root root 4096 Nov 11 11:07 systemd-private-869037dc...
drwx------ 3 root root 4096 Nov 11 11:07 systemd-private-869037dc...
+ ls -l /root/
ls: cannot open directory '/root/': Permission denied
+ handle_minor_error
+ echo 'A minor error has occured, please check the output.'
A minor error has occured, please check the output.
+ cat /etc/shadow
cat: /etc/shadow: Permission denied
+ handle_fatal_error
+ echo 'A critical error has occured, stopping script.'
A critical error has occured, stopping script.
+ exit 1

如您所见,第一个命令ls -l /tmp/成功了,我们看到了它的输出;我们不进入handle_minor_error功能。下一个命令,我们确实预计会失败,确实如此。我们看到,我们现在进入函数,并打印了我们在那里指定的错误消息。但是,因为这只是一个小错误,我们继续脚本。然而,当我们到达cat /etc/shadow,我们认为这是一个重要的组成部分,我们遇到一个Permission denied消息,导致脚本执行handle_fatal_error。因为这个函数有一个exit 1,所以脚本被终止,第四个命令永远不会执行。这应该说明另一点:一个exit,即使从一个函数内部,也是全局的,并且终止脚本(不仅仅是函数)。如果您希望看到此脚本成功,请使用sudo bash error-functions.sh运行它。您将看到两个错误函数都没有执行。

用参数扩充函数

正如脚本可以接受参数形式的输入一样,函数也可以。实际上,大多数函数都将使用参数。静态函数,如早期的错误处理示例,在接受参数方面不如它们的对应函数强大或灵活。

富有色彩的

在下一个示例中,我们将创建一个脚本,允许我们以几种不同的颜色向终端打印文本。它是基于一个有两个参数的函数来实现的:stringcolor。看看下面的命令:

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

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-17
# Description: Some printed text, now with colors!
# Usage: ./colorful.sh
#####################################

print_colored() {
  # Check if the function was called with the correct arguments.
  if [[ $# -ne 2 ]]; then
    echo "print_colored needs two arguments, exiting."
    exit 1
  fi

  # Grab both arguments.
  local string=$1
  local color=$2

  # Use a case-statement to determine the color code.
  case ${color} in
  red)
    local color_code="\e[31m";;
  blue)
    local color_code="\e[34m";;
  green)
    local color_code="\e[32m";;
  *)
    local color_code="\e[39m";; # Wrong color, use default.
  esac

  # Perform the echo, and reset color to default with [39m.
  echo -e ${color_code}${string}"\e[39m"
}

# Print the text in different colors.
print_colored "Hello world!" "red"
print_colored "Hello world!" "blue"
print_colored "Hello world!" "green"
print_colored "Hello world!" "magenta"

这个剧本发生了很多事情。为了帮助您理解,我们将从函数定义的第一部分开始,一点一点地进行分析:

print_colored() {
  # Check if the function was called with the correct arguments.
  if [[ $# -ne 2 ]]; then
    echo "print_colored needs two arguments, exiting."
    exit 1
  fi

  # Grab both arguments.
  local string=$1
  local color=$2

我们在函数体中做的第一件事是检查参数的数量。语法与我们通常对传递给整个脚本的参数进行的检查相同,这可能会有所帮助,也可能会令人困惑。要意识到的一件好事是$#构造适用于它被使用的范围;如果在主脚本中使用它,它会检查传递到那里的参数。如果在函数中使用它,就像这里一样,它会检查传递给函数的参数数量。$1$2等等也是如此:如果在函数中使用,它们指的是传递给函数的有序参数,而不是一般的脚本。当我们抓住论点时,我们把它们写到局部变量;在这个简单的脚本中,我们并不严格需要这样做,但是当您只在本地范围内使用变量时,将变量标记为本地总是一个很好的做法。您可能会想象,在更大、更复杂的脚本中,许多函数使用的变量可能会意外地被称为相同的东西(在这种情况下,string是一个非常常见的词)。通过将它们标记为本地,您不仅提高了可读性,还防止了由同名变量引起的错误;总而言之,这是一个非常好的主意。让我们回到脚本的下一部分,案例陈述:

  # Use a case-statement to determine the color code.
  case ${color} in
  red)
    color_code="\e[31m";;
  blue)
    color_code="\e[34m";;
  green)
    color_code="\e[32m";;
  *)
    color_code="\e[39m";; # Wrong color, use default.
  esac

现在是介绍case的绝佳时机。案例陈述基本上是一条很长的if-then-elif-then-elif-then...链。变量的选择越多,链就会变得越长。有了case,你可以直接说for certain values in ${variable}, do <something>。在我们的例子中,这意味着如果${color}变量是red,我们将另一个color_code变量设置为\e[31m(稍后将详细介绍)。如果是blue,我们就做点别的,同样的道理也适用于green。最后,我们将定义一个通配符;没有指定的变量的任何值都将通过那里,作为一种包罗万象的结构。如果指定的颜色是不兼容的东西,比如,我们就设置默认颜色。另一种选择是中断脚本,这有点对错误颜色的过度反应。要终止一个case,你将使用esac关键字(它是case的反义词),类似于if,它由它的反义词fi终止。

现在,进入终端上颜色的技术方面。虽然我们一直在学习的大多数东西都是特定于 Bash 或 Linux 的,但打印的颜色实际上是由您的终端仿真器定义的。我们使用的颜色代码非常标准,应该由您的终端解释为不按字面打印该字符,而是将color改为<color>* 。终端看到一个转义序列\e,后面跟着一个颜色代码[31m,并且知道您正在指示它打印一种不同于先前定义的颜色(通常是该终端仿真器的默认值,除非您自己更改了配色方案)。您可以使用转义序列做更多的事情(当然,只要您的终端仿真器支持这一点),例如创建粗体文本、闪烁文本和文本的另一种背景色。现在,记住不是打印而是解释\ e[31m]序列。对于case中的综合选项,您不希望显式设置颜色,而是向终端发送信号以默认的颜色打印。这意味着,对于每个兼容的终端仿真器,文本都以用户选择的颜色打印(或者默认分配)。*

现在是脚本的最后一部分:

  # Perform the echo, and reset color to default with [39m.
  echo -e ${color_code}${string}"\e[39m"
}

# Print the text in different colors.
print_colored "Hello world!" "red"
print_colored "Hello world!" "blue"
print_colored "Hello world!" "green"
print_colored "Hello world!" "magenta"

print_colored函数的最后一部分实际打印彩色文本。它通过使用带有-e旗帜的好旧echo来做到这一点。man echo显示-e 启用反斜杠转义。如果不指定此选项,您的输出将类似于\e[31mHello world!\e[39m。在这种情况下要知道的一件好事是,只要你的终端遇到一个色码转义序列,*所有后续的文字都会以该颜色打印!*因此,我们用"\e[39m"结束回声,这将所有后续文本的颜色重置为默认值。

最后,我们多次调用该函数,第一个参数相同,但第二个参数(颜色)不同。如果您运行该脚本,输出应该如下所示:

在前面的截图中,我的配色设置为黑上绿,这就是为什么最后的Hello world!是亮绿色。你可以看到它和bash colorful.sh是同一个颜色,这应该是你需要确认的所有确认,以确保[39m颜色代码实际上是默认的。

返回值

有些函数遵循处理器原型:它们接受输入,用它做一些事情,并将结果返回给调用者。这是一个经典的功能:根据输入,产生不同的输出。我们将通过一个示例来展示这一点,该示例将用户指定给脚本的输入反转。这通常是通过rev命令来完成的(实际上在我们的函数中也会通过rev来完成),但是我们正在围绕这个命令创建一个带有一些额外功能的包装函数:

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

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-17
# Description: Reverse the input for the user.
# Usage: ./reverser.sh <input-to-be-reversed>
#####################################

# Check if the user supplied one argument.
if [[ $# -ne 1 ]]; then
  echo "Incorrect number of arguments!"
  echo "Usage: $0 <input-to-be-reversed>"
  exit 1
fi

# Capture the user input in a variable.
user_input="_${1}_" # Add _ for readability.

# Define the reverser function.
reverser() {
  # Check if input is correctly passed.
  if [[ $# -ne 1 ]]; then
    echo "Supply one argument to reverser()!" && exit 1
  fi

  # Return the reversed input to stdout (default for rev).
  rev <<< ${1}
}

# Capture the function output via command substitution.
reversed_input=$(reverser ${user_input})

# Show the reversed input to the user.
echo "Your reversed input is: ${reversed_input}"

由于这又是一个更长、更复杂的脚本,我们将一点一点地查看它,以确保您完全理解它。我们甚至在里面偷偷放了一个小惊喜,这证明了我们之前的一个说法,但是我们稍后会讲到。我们将跳过标题和输入检查,转到捕获变量:

# Capture the user input in a variable.
user_input="_${1}_" # Add _ for readability.

在前面的大多数例子中,我们总是直接将输入映射到一个变量。然而,这一次我们展示了你也可以添加一些额外的文本。在本例中,我们接受用户的输入,并在前后添加下划线。如果用户输入rain,变量实际上会包含_rain_。这将在以后被证明是有见地的。现在,对于函数定义,我们使用以下代码:

# Define the reverser function.
reverser() {
  # Check if input is correctly passed.
  if [[ $# -ne 1 ]]; then
    echo "Supply one argument to reverser()!" && exit 1
  fi

  # Return the reversed input to stdout (default for rev).
  rev <<< ${1}
}

reverser函数需要一个参数:要反转的输入。像往常一样,在我们实际做任何事情之前,我们首先检查输入是否正确。接下来,我们使用rev反转输入。然而,rev通常期望从文件或stdin输入,而不是变量作为参数。因为我们不想添加额外的回声和管道,所以我们使用一个这里的字符串(如第 12 章使用脚本中的管道和重定向中所解释的),这允许我们直接使用变量内容作为stdin。由于rev已经将结果输出到stdout,我们不需要在这一点上提供任何东西,比如回声。

我们告诉过你我们会证明一个先前的陈述,在这种情况下与先前片段中的$1相关。如果函数中的$1与脚本的第一个参数相关,而不是函数的第一个参数相关,我们就看不到我们在编写user_input变量时添加的下划线了。对于脚本来说,$1可以等于rain,在函数中,$1等于_rain_。当您运行脚本时,您肯定会看到下划线,这意味着每个函数都有自己的参数集!

将这一切联系在一起是剧本的最后一部分:

# Capture the function output via command substitution.
reversed_input=$(reverser ${user_input})

# Show the reversed input to the user.
echo "Your reversed input is: ${reversed_input}"

由于reverser函数将反向输入发送到stdout,我们将使用命令替换将其捕获在变量中。最后,我们用echo打印一些澄清文本和反向输入给用户。结果将如下所示:

reader@ubuntu:~/scripts/chapter_13$ bash reverser.sh rain
Your reversed input is: _niar_

下划线等等,我们得到的是rain: _nair_的反义词。很好!

To avoid too much complexity, we split the final part of this script in two lines. However, once you feel comfortable with command substitutions, you could save yourself the intermediate variable and use the command substitution directly within the echo, like so: echo "Your reversed input is: $(reverser ${user_input})". We would recommend not making it much more complex than this, however, since that will start to affect the readability.

函数库

当你读到这本书的这一部分时,你会看到 50 多个示例脚本。这些脚本中的许多都有一些共享组件:输入检查、错误处理和设置当前工作目录已经在多个脚本中使用。这段代码并没有真正改变;也许评论或回声略有不同,但实际上这只是重复的代码。将此与必须在脚本顶部定义函数的问题(或者至少在开始使用它们之前)联系起来,您的可维护性开始受到影响。我们都很幸运,对此有一个很好的解决方案:创建自己的函数库!

来源

函数库的思想是定义在不同脚本之间共享的函数。这些是可重复的通用函数,不太关心要运行的特定脚本。当你创建一个新的脚本时,你要做的第一件事,就在标题之后,是*包含来自库的函数定义。*这个库无非是另一个 shell 脚本:不过,它只是用来定义函数的,所以它从来不调用任何东西。如果您要运行它,最终结果将与您运行一个空脚本一样。我们将首先创建我们自己的函数库,然后再考虑如何包含它。

创建函数库只有一个真正的考虑:放在哪里。您希望它在您的文件系统中只出现一次,最好是在一个可预测的位置。个人比较喜欢/opt/目录。但是,默认情况下/opt/仅对root用户可写。在多用户系统中,把它放在那里可能不是一个坏主意,它归root所有,每个人都可以阅读,但是由于这是单用户的情况,我们将把它直接放在我们的主目录中。让我们从图书馆开始:

reader@ubuntu:~$ vim bash-function-library.sh 
reader@ubuntu:~$ cat bash-function-library.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-17
# Description: Bash function library.
# Usage: source ~/bash-function-library.sh
#####################################

# Check if the number of arguments supplied is exactly correct.
check_arguments() {
  # We need at least one argument.
  if [[ $# -lt 1 ]]; then
    echo "Less than 1 argument received, exiting."
    exit 1
  fi  

  # Deal with arguments
  expected_arguments=$1
  shift 1 # Removes the first argument.

  if [[ ${expected_arguments} -ne $# ]]; then
    return 1 # Return exit status 1.
  fi
}

因为这是一个泛型函数,我们需要首先提供我们期望的参数数量,然后是实际的参数。保存预期的参数个数后,用shift所有参数左移一位:$2变为$1$3变为$2,将$1全部去掉。这样做,只剩下要检查的参数数量,预期数量安全地存储在变量中。然后我们比较这两个值,如果它们不相同,我们返回一个退出代码1returnexit类似,但它并不停止脚本的执行:如果我们想这样做,调用函数的脚本应该处理好这一点。

要在另一个脚本中使用这个库函数,我们需要包含它。在 Bash 中,这被称为采购。采购是通过source命令实现的:

source <file-name>

语法很简单。一旦你source一个文件,它的所有内容都会被处理。在我们的库案例中,当我们只定义函数时,什么都不会执行,但是我们会有可用的函数。如果您正在获取包含实际命令的文件,如echocatmkdir,这些命令*将被执行。*一如既往,一个例子抵得上千言万语,所以让我们看看如何使用source来包含库函数:

reader@ubuntu:~/scripts/chapter_13$ vim argument-checker.sh
reader@ubuntu:~/scripts/chapter_13$ cat argument-checker.sh
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-17
# Description: Validates the check_arguments library function
# Usage: ./argument-checker.sh
#####################################

source ~/bash-function-library.sh

check_arguments 3 "one" "two" "three" # Correct.
check_arguments 2 "one" "two" "three" # Incorrect.
check_arguments 1 "one two three" # Correct.

很简单,对吧?我们使用完全限定的路径来获取文件(是的,即使~是速记,这仍然是完全限定的!)并继续使用另一个脚本中定义的函数。如果您使用 debug 运行这个函数,您将会看到该函数如我们所期望的那样工作:

reader@ubuntu:~/scripts/chapter_13$ bash -x argument-checker.sh 
+ source /home/reader/bash-function-library.sh
+ check_arguments 3 one two three
+ [[ 4 -lt 1 ]]
+ expected_arguments=3
+ shift 1
+ [[ 3 -ne 3 ]]
+ check_arguments 2 one two three
+ [[ 4 -lt 1 ]]
+ expected_arguments=2
+ shift 1
+ [[ 2 -ne 3 ]]
+ return 1
+ check_arguments 1 'one two three'
+ [[ 2 -lt 1 ]]
+ expected_arguments=1
+ shift 1
+ [[ 1 -ne 1 ]]

第一个和第三个函数调用应该是正确的,而第二个应该会失败。因为我们在函数中使用了return而不是exit,所以即使在第二次函数调用返回1退出状态后,脚本仍会继续。如调试输出所示,第二次调用函数时,执行评估2 not equals 3并成功,这导致了return 1。对于其他调用,参数是正确的,返回0的默认返回代码(输出中未显示,但这确实是发生的情况;如果要自己验证,添加echo $?)。

现在,为了在实际的脚本中使用它,我们需要将用户给我们的所有参数传递给我们的函数。这可以使用$@语法来完成:其中$#对应于参数的数量,$@简单地打印所有参数。我们还将更新argument-checker.sh来检查脚本的参数:

reader@ubuntu:~/scripts/chapter_13$ vim argument-checker.sh 
reader@ubuntu:~/scripts/chapter_13$ cat argument-checker.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.1.0
# Date: 2018-11-17
# Description: Validates the check_arguments library function
# Usage: ./argument-checker.sh <argument1> <argument2>
#####################################

source ~/bash-function-library.sh

# Check user input. 
# Use double quotes around $@ to prevent word splitting.
check_arguments 2 "$@"
echo $?

我们将预期数量的参数2和脚本接收的所有参数$@传递给我们的源函数。用一些不同的输入运行它,看看会发生什么:

reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh 
1
reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh 1
1
reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh 1 2
0
reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh "1 2"
1
reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh "1 2" 3
0

太好了,一切似乎都在运转!最有趣的尝试可能是后两种,因为它们说明了分词经常带来的问题。默认情况下,Bash 会将每一段空白解释为一个分隔符。在第四个例子中,我们传递"1 2"字符串,由于引用,它实际上是单个参数。如果我们不在$@周围使用双引号,就会出现这种情况:

reader@ubuntu:~/scripts/chapter_13$ tail -3 argument-checker.sh 
check_arguments 2 $@
echo $?

reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh "1 2"
0

在这个例子中,Bash 将参数传递给函数,而不保留引号。该函数将接收"1""2",而不是"1 2"。要时刻注意的事情!

现在,我们可以使用预定义的函数来检查参数的数量是否正确。然而,目前我们不使用返回代码做任何事情。我们将对我们的argument-checker.sh脚本进行最后一次调整,如果参数数量不正确,将停止脚本执行:

reader@ubuntu:~/scripts/chapter_13$ vim argument-checker.sh 
reader@ubuntu:~/scripts/chapter_13$ cat argument-checker.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.2.0
# Date: 2018-11-17
# Description: Validates the check_arguments library function
# Usage: ./argument-checker.sh <argument1> <argument2>
#####################################

source ~/bash-function-library.sh

# Check user input. 
# Use double quotes around $@ to prevent word splitting.
check_arguments 2 "$@" || \
{ echo "Incorrect usage! Usage: $0 <argument1> <argument2>"; exit 1; }

# Arguments are correct, print them.
echo "Your arguments are: $1 and $2"

因为这本书的页面宽度,我们用\check_arguments一分为二:这表示 Bash 继续下一行。如果您愿意,可以省略它,将完整的命令放在一行中。如果我们现在运行脚本,我们将看到理想的脚本执行:

reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh 
Incorrect usage! Usage: argument-checker.sh <argument1> <argument2>
reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh dog cat
Your arguments are: dog and cat
reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh dog cat mouse
Incorrect usage! Usage: argument-checker.sh <argument1> <argument2>

恭喜,我们已经开始创建函数库,并成功地在我们的一个脚本中使用了它!

There is a somewhat confusing shorthand syntax for source: a single dot (.). If we wanted to use that shorthand in our scripts, it would simply be . ~/bash-function-library.sh. We are, however, not big fans of this syntax: the source command is not long or complicated, while a single . can easily be missed or misused if you forget a space after it (which can be hard to see!). Our advice: know the shorthand exists if you encounter it somewhere in the wild, but use the full built-in source when writing scripts.

更多实际例子

我们将在本章的最后一部分用早期脚本中常用的操作来扩展函数库。我们将从前面的章节中复制一个脚本,并使用我们的函数库来替换可以用我们库中的函数处理的功能。

当前工作目录

包含在我们自己的私有函数库中的第一个候选项是正确设置当前的工作目录。这是一个非常简单的函数,所以我们将在不做过多解释的情况下添加它:

reader@ubuntu:~/scripts/chapter_13$ vim ~/bash-function-library.sh 
reader@ubuntu:~/scripts/chapter_13$ cat ~/bash-function-library.sh 
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.1.0
# Date: 2018-11-17
# Description: Bash function library.
# Usage: source ~/bash-function-library.sh
#####################################
<SNIPPED>
# Set the current working directory to the script location.
set_cwd() {
  cd $(dirname $0)
}

因为函数库可能会频繁更新,所以正确更新标题中的信息非常重要。最好(最有可能是在企业环境中),将函数库的新版本提交给版本控制系统。在标题中使用适当的语义版本将有助于你保持一个干净的历史。特别是,如果您将此与配置管理工具(如 Chef.io、Puppet 和 Ansible)结合起来,您将很好地概括您所做的更改和部署。

现在,我们将更新上一章redirect-to-file.sh中的脚本,包括库包含和函数调用。最终结果应该如下:

reader@ubuntu:~/scripts/chapter_13$ cp ../chapter_12/redirect-to-file.sh library-redirect-to-file.sh
reader@ubuntu:~/scripts/chapter_13$ vim library-redirect-to-file.sh 
reader@ubuntu:~/scripts/chapter_13$ cat library-redirect-to-file.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-17
# Description: Redirect user input to file.
# Usage: ./library-redirect-to-file.sh
#####################################

# Load our Bash function library.
source ~/bash-function-library.sh

# Since we're dealing with paths, set current working directory.
set_cwd

# Capture the users' input.
read -p "Type anything you like: " user_input

# Save the users' input to a file. > for overwrite, >> for append.
echo ${user_input} >> redirect-to-file.txt

出于教学目的,我们将文件复制到了当前章节的目录中;通常,我们只更新原始文件。我们只增加了函数库的包含,并用我们的set_cwd函数调用替换了神奇的cd $(dirname $0)。让我们从没有脚本的位置运行它,看看目录是否设置正确:

reader@ubuntu:/tmp$ bash ~/scripts/chapter_13/library-redirect-to-file.sh
Type anything you like: I like ice cream, I guess
reader@ubuntu:/tmp$ ls -l
drwx------ 3 root root 4096 Nov 17 11:20 systemd-private-af82e37c...
drwx------ 3 root root 4096 Nov 17 11:20 systemd-private-af82e37c...
reader@ubuntu:/tmp$ cd ~/scripts/chapter_13
reader@ubuntu:~/scripts/chapter_13$ ls -l
<SNIPPED>
-rw-rw-r-- 1 reader reader 567 Nov 17 19:32 library-redirect-to-file.sh
-rw-rw-r-- 1 reader reader 26 Nov 17 19:35 redirect-to-file.txt
-rw-rw-r-- 1 reader reader 933 Nov 17 15:18 reverser.sh
reader@ubuntu:~/scripts/chapter_13$ cat redirect-to-file.txt 
I like ice cream, I guess

因此,即使我们使用了$0语法(如您所记得的,它打印了脚本的完全限定路径),我们在这里看到它指的是library-redirect-to-file.sh的路径,而不是您可能合理假设的bash-function-library.sh脚本的位置。这应该证实了我们的解释,即只包含函数定义,并且当函数在运行时被调用时,它们呈现包含它们的脚本环境。

类型检查

我们在许多脚本中做的事情是检查参数。我们从一个允许检查用户输入的参数数量的函数开始我们的库。我们经常对用户输入执行的另一个操作是验证输入类型。例如,如果我们的脚本需要一个数字,我们希望用户实际输入一个数字,而不是一个单词(或者一个写出来的数字,如“11”)。您可能还记得大概的语法,但我相信如果您现在再次需要它,您会在我们的旧脚本中找到它。这听起来不像是库函数的理想候选吗?我们创建并彻底测试我们的功能一次,然后我们可以感到安全,只是采购和使用它!让我们创建一个函数来检查传递的参数是否真的是整数:

reader@ubuntu:~/scripts/chapter_13$ vim ~/bash-function-library.sh
reader@ubuntu:~/scripts/chapter_13$ cat ~/bash-function-library.sh 
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.2.0
# Date: 2018-11-17
# Description: Bash function library.
# Usage: source ~/bash-function-library.sh
#####################################
<SNIPPED>

# Checks if the argument is an integer.
check_integer() {
  # Input validation.
  if [[ $# -ne 1 ]]; then
    echo "Need exactly one argument, exiting."
    exit 1 # No validation done, exit script.
  fi

  # Check if the input is an integer.
  if [[ $1 =~ ^[[:digit:]]+$ ]]; then
    return 0 # Is an integer.
  else
    return 1 # Is not an integer.
  fi
}

因为我们处理的是库函数,为了可读性,我们可以稍微详细一点。常规脚本中过多的冗长会降低可读性,但是一旦有人查看函数库进行理解,你可以假设他们会喜欢一些更冗长的脚本。毕竟,当我们在脚本中调用函数时,我们只会看到check_integer ${variable}

转到函数。我们首先检查是否收到一个单独的参数。如果我们没有收到,我们退出而不是返回。我们为什么要这么做?调用的脚本不要混淆1的返回代码是什么意思;如果这可能意味着我们要么没有检查任何东西,而且检查本身失败了,我们就在我们不想要的地方带来了模糊性。简单地说,return 总是告诉调用者一些关于传递的参数的信息,如果脚本错误地调用了函数,它将看到完整的脚本退出并显示一条错误消息。

接下来,我们使用我们在第 10 章正则表达式中构造的正则表达式来检查参数是否实际上是整数。如果是,我们返回0。如果不是,我们会打到else街区1会被退回。为了向阅读图书馆的人强调这一点,我们加入了# Is an integer# Is not an integer的评论。为什么不让他们轻松一点呢?请记住,您并不总是为他人编写代码,但是如果您在一年后查看自己的代码,您肯定还会觉得自己是他人(同样,您可以在这一点上信任我们!).

我们将进行另一次搜索——从我们早期的脚本中替换。上一章password-generator.sh中合适的一个将很好地服务于这个目的。将它复制到一个新文件中,用源代码加载函数库,并替换参数检查(是的,两者都有!):

reader@ubuntu:~/scripts/chapter_13$ vim library-password-generator.sh 
reader@ubuntu:~/scripts/chapter_13$ cat library-password-generator.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-17
# Description: Generate a password.
# Usage: ./library-password-generator.sh <length>
#####################################

# Load our Bash function library.
source ~/bash-function-library.sh

# Check for the correct number of arguments.
check_arguments 1 "$@" || \
{ echo "Incorrect usage! Usage: $0 <length>"; exit 1; }

# Verify the length argument.
check_integer $1 || { echo "Argument must be an integer!"; exit 1; }

# tr grabs readable characters from input, deletes the rest.
# Input for tr comes from /dev/urandom, via input redirection.
# echo makes sure a newline is printed.
tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c $1
echo

我们用库函数替换了参数数量检查和整数检查。我们还删除了变量声明,直接在脚本的函数部分使用$1;这并不总是最好的做法。然而,当输入只使用一次时,首先将其存储在一个命名变量中会产生一些开销,我们可能会跳过这些开销。即使有了所有的空白和注释,我们仍然设法通过使用函数调用将脚本行数从 31 减少到 26。当我们调用新的和改进的脚本时,我们会看到以下内容:

reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh
Incorrect usage! Usage: library-password-generator.sh <length>
reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh 10
50BCuB835l
reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh 10 20
Incorrect usage! Usage: library-password-generator.sh <length>
reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh bob
Argument must be an integer!

太好了,我们的支票如预期般有效。看起来也好多了,不是吗?

是-否检查

在我们完成这一章之前,我们将再出示一张支票。本书进行到一半时,在第 9 章错误检查和 处理、中,我们展示了一个脚本,该脚本处理一个可以提供“是”或“否”的用户。但是,正如我们在那里解释的那样,用户也可能使用“y”或“n”,甚至可能在那里的某个地方使用大写字母。通过偷偷使用一点 Bash 扩展,您会看到在第 16 章Bash 参数替换和扩展中有适当的解释,我们能够对用户输入进行相对清晰的检查。我们去图书馆拿那个东西吧!

reader@ubuntu:~/scripts/chapter_13$ vim ~/bash-function-library.sh 
reader@ubuntu:~/scripts/chapter_13$ cat ~/bash-function-library.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.3.0
# Date: 2018-11-17
# Description: Bash function library.
# Usage: source ~/bash-function-library.sh
#####################################
<SNIPPED>

# Checks if the user answered yes or no.
check_yes_no() {
  # Input validation.
  if [[ $# -ne 1 ]]; then
    echo "Need exactly one argument, exiting."
    exit 1 # No validation done, exit script.
  fi

  # Return 0 for yes, 1 for no, exit 2 for neither.
  if [[ ${1,,} = 'y' || ${1,,} = 'yes' ]]; then
    return 0
  elif [[ ${1,,} = 'n' || ${1,,} = 'no' ]]; then
    return 1
  else
    echo "Neither yes or no, exiting."
    exit 2
  fi
}

通过这个例子,我们为您设计了一个高级脚本。我们现在有四种可能的结果,而不是二元返回:

  • 函数被错误调用:exit 1
  • 函数找到一个是:return 0
  • 函数找到一个编号:return 1
  • 函数未找到:exit 2

使用我们新的库函数,我们将采用yes-no-optimized.sh脚本,并用(几乎)单个函数调用替换复杂的逻辑:

reader@ubuntu:~/scripts/chapter_13$ cp ../chapter_09/yes-no-optimized.sh library-yes-no.sh
reader@ubuntu:~/scripts/chapter_13$ vim library-yes-no.sh
reader@ubuntu:~/scripts/chapter_13$ cat library-yes-no.sh 
#!/bin/bash

#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-11-17
# Description: Doing yes-no questions from our library.
# Usage: ./library-yes-no.sh
#####################################

# Load our Bash function library.
source ~/bash-function-library.sh

read -p "Do you like this question? " reply_variable

check_yes_no ${reply_variable} && \
echo "Great, I worked really hard on it!" || \
echo "You did not? But I worked so hard on it!"

花一分钟看看前面的脚本。刚开始可能会有点混乱,但是尽量记住&&||是做什么的。由于我们应用了一些智能排序,我们可以依次使用&&||来实现我们的结果。这样看:

  1. 如果check_yes_no返回退出状态 0(当发现时),则执行& &后的命令。因为这与成功相呼应,并且echo的退出代码为 0,所以下一个||之后的失败echo不会执行。
  2. 如果check_yes_no返回退出状态 1(当发现没有时),则不执行& &后的命令。但是,它会一直持续到到达||,由于返回代码仍然是而不是 0,因此会继续出现故障回声。
  3. 如果check_yes_no因缺少参数或缺少是/否而退出,则不执行&&||之后的命令(因为脚本被赋予了exit而不是return,所以代码执行立即停止)。

很聪明吧?然而,我们必须承认,这有点违背我们一直在教你的关于可读性的大多数东西。将此视为链接&&||的教学练习。如果你想自己实现“是-否”检查,最好创建专用的check_yes()check_no()功能。无论如何,让我们看看我们精心设计的脚本是否真的像我们希望的那样工作:

reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh 
Do you like this question? Yes
Great, I worked really hard on it!
reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh 
Do you like this question? n
You did not? But I worked so hard on it!
reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh 
Do you like this question? MAYBE 
Neither yes or no, exiting.
reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh 
Do you like this question?
Need exactly one argument, exiting.

我们在检验工作中定义的所有场景。大获成功!

Normally, you do not want to mix exit and return codes too much. Also, using a return code to convey anything other than pass or fail is also pretty uncommon. However, since you can return 256 different codes (from 0 up to 255), this is at least possible by design. Our yes-no example was a good candidate for showing how this could be used. However, as a general tip, you're probably better off by using it in a pass/fail way, as currently you place the burden of knowing the different return codes on the caller. Which is, to say the least, not always a fair thing to ask of them.

我们想用一个小练习来结束这一章。在本章中,在介绍函数库之前,我们已经创建了几个函数:两个用于错误处理,一个用于彩色打印,一个用于反转文本。你的练习很简单:抓住那些函数,并把它们添加到你的个人函数库。请务必记住以下事项:

  • 这些函数是否足够冗长,可以原样包含在库中,或者它们可以使用更多?
  • 我们能调用函数并按原样处理输出吗,还是编辑更好?
  • 返回和退出是否正确实现,或者它们是否需要调整以作为通用库函数工作?

这里没有对错答案,只是需要考虑的事情。祝你好运!

摘要

在本章中,我们介绍了 Bash 函数。函数是通用的命令链,可以定义一次,然后调用多次。函数是可重用的,可以在多个脚本之间共享。

引入了可变范围。到目前为止,我们看到的变量一直是全球范围的:它们对整个脚本都是可用的。然而,随着函数的引入,我们遇到了局部范围的变量。这些只能在一个函数中访问,并用local关键字标记。

我们了解到函数可以有自己独立的参数集,当调用函数时,这些参数可以作为参数传递。我们证明了这些实际上不同于传递给脚本的全局参数(当然,除非所有参数都传递给函数)。我们给出了一个使用stdout从函数返回输出的例子,我们可以通过将函数调用封装在命令替换中来捕获它。

在本章的后半部分,我们将注意力转向创建函数库:一个没有实际命令的独立脚本,它可以(通过source命令)包含在另一个脚本中。一旦库来源于另一个脚本,该脚本就可以使用库中定义的所有函数。我们在本章的剩余部分展示了如何做到这一点,同时用一些实用函数扩展了我们的函数库。

我们在这一章的最后为读者做了一个练习,以确保本章中定义的所有函数都包含在自己的个人函数库中。

本章介绍了以下命令:topfreedeclarecaserevreturn

问题

  1. 我们可以用哪两种方法定义函数?
  2. 函数有哪些优点?
  3. 全局范围的变量和局部范围的变量有什么区别?
  4. 我们如何为变量设置值和属性?
  5. 函数如何使用传递给它的参数?
  6. 我们如何从函数中返回值?
  7. source命令是做什么的?
  8. 为什么我们要创建一个函数库?

进一步阅读