期目录:
上期教程我们学习了Shell编程中的控制语句,这期教程我们来学习函数和Shell编程的其它知识点。
一、函数
函数(Function)是一段可以重复使用的代码,它实现了某种特定的功能或计算。通过调用函数,程序员可以将代码模块化、简化复杂性、提高代码的复用性和可维护性。
1.1 函数创建方式一
我们可以使用function字段完成函数的创建。
function 函数名 {
函数代码体
}
1.2 函数创建方式二
对于函数的创建我们也可以不使用function字段。
函数名() {
函数代码体
}
1.3 函数的调用
定义函数后,我们可以直接通过函数名来调用。
1.4 有参函数
Shell 函数可以接受参数,参数在函数体内通过$1,$2... 来引用,$1表示第一个参数,$2表示第二个参数,依此类推。
当然如果我们需要访问所有参数,可以使用$@来引用所有传入的参数。
1.5 函数返回值
Shell 函数通常没有返回值,但可以通过return关键字返回一个数字状态码。
如果我们想要查看函数返回的数值,可以使用$?,该变量会显示上一个命令的返回值(0表示上一条命令执行成功,否则就是执行失败)。
如果我们非要将函数结果进行返回,那么我们可以将函数的结果通过echo输出,然后在调用函数时捕获这个输出,然后结合$(command)或反引号`command`来捕获echo输出的结果。
1.6 local变量
现在我们已经学会了如何去创建函数以及调用,下面我们就来探讨一下函数里面的变量。
假设我们定义一个函数my_fn(),并在里面定义一个变量var1,我们在函数里面和外面分别调用var1这个变量。
从上述结果中我们可以看出var变量在函数内和函数外面都可以调用,也就是说var1此时是一个全局变量,而外面前面说过变量分为全局变量和局部变量,那么局部变量我们应该如何定义呢?
在Shell编程中,local关键字用于限定变量的作用域为当前函数。也就是说,local定义的变量只能在当前函数内部使用,函数执行结束后,这个变量就会被销毁,外部无法访问该变量。
1.7 递归函数
递归函数是一个函数,它在自己的定义中调用自己。递归通常包括两个重要的部分:
- 基准条件(Base Case):当满足某些条件时,递归停止,不再调用自己。
- 递归条件(Recursive Case):当不满足基准条件时,函数调用自己。
Shell编程中递归函数的基本结构与其他编程语言相似。我们可以定义一个函数,并在该函数内调用自己,直到达到基准条件。
下面我们根据递归函数的定义去实现数学中的阶乘。
递归的注意事项
-
栈溢出:
-
在 Bash 脚本中,递归调用的深度有限制,过深的递归可能导致栈溢出。
-
为避免这种情况,通常会限制递归深度或使用循环来代替递归。
-
-
效率:
-
递归函数在某些情况下会造成性能问题,因为每次递归调用都要保存函数的上下文。如果递归深度很大,可能会导致性能下降。
-
对于递归问题,可以考虑使用尾递归优化(在某些语言中尾递归可以提高效率),但在 Bash 中,由于其不支持尾递归优化,递归深度过深时要小心。
-
-
参数传递:
-
在 Shell 脚本中,递归调用时需要将必要的参数传递给每次调用。注意 Bash 中参数是通过位置变量传递的,例如
$1
、$2
等。
-
-
退出条件:
-
每个递归函数必须有明确的退出条件,否则会陷入无限递归,导致程序崩溃。
-
1.8 错误处理
Shell 编程时,错误处理是一个非常重要的方面。Shell 函数通常会依赖于命令的执行结果来决定接下来的操作,而错误处理的好坏直接影响到脚本的健壮性和可靠性。
在 Shell 脚本中,错误的处理方式通常依赖于退出状态码。每个命令在执行后都会返回一个退出状态码,表示命令的执行结果:
- 退出状态码 0:表示命令成功执行,没有错误。
- 退出状态码 非0:表示命令执行失败或发生了某种错误。
这个状态码我们上述已经接触过了,我们可以使用$?来查看上一个命令的状态码的值。
在函数中我们应该如何处理错误呢?
下面我们来介绍在Shell编程中的函数里面关于函数错误处理的几种方式。
1、检查命令的返回值
每个命令执行后,Shell 都会通过特殊变量$?来存储该命令的退出状态码。通常,如果$?的值为 0,表示命令执行成功;如果为非零值,表示命令执行失败。
2、set -e自动退出脚本
set -e是一个非常有用的选项。当启用set -e后,如果脚本中任何命令的返回值为非零,脚本会立即退出,后续的命令将不会被执行。
我们通过上图中的结果可以知道当ls命令出现错误时,程序还是继续往下在执行,下面我们开启set -e选项。
从图中我们可以知道开启set -e选项后ls命令出现错误就不在继续执行后续的命令。
3、trap捕获错误信号
trap命令用于捕获和处理信号。信号通常是系统或用户发出的通知,表明某种事件已经发生。常见的信号包括中断信号(如Ctrl+C)、退出信号、错误信号等。trap可以用来捕获这些信号并在发生时执行指定的命令或脚本。
trap '对错误执行的命令' SIGNAL
SIGNAL是要捕获的信号类型。常见的信号包括:
- EXIT:脚本或 shell 退出时触发。
- ERR:当命令执行返回非零状态时触发(发生错误时)。
- SIGINT:由Ctrl+C发送的中断信号。
- SIGTERM:终止信号,通常用于优雅地关闭程序。
- SIGKILL:无法捕获的信号,用于强制终止进程。
上图中实现了捕获ERR信号,当然trap可以一次捕获多个信号。
1.9 管道机制
Shell中的管道(|)是将一个命令的标准输出传递到另一个命令的标准输入。
如果我们想将函数的输出通过管道传输到另一个命令,可以像使用普通命令一样使用管道。
上图中my_fn1函数输出的“哈哈哈哈哈哈哈”会将命令传递给cat命令。
当然我们也可以使用多个管道进行数据传递。
1.10 命令替换
命令替换(Command Substitution)是一个非常有用的功能,它可以将命令的输出作为字符串插入到另一个命令或脚本中。通过命令替换,我们可以将一个函数的输出赋值给变量,或将其传递给其他命令进行处理。
在Shell编程中命令替换有两种形式即反引号"``"和“$()”。
1.10.1 反引号形式
反引号命令替换是较老的方式,用于将一个命令的输出作为另一个命令的参数。
command1 `command2`
在上述命令中command1命令使用command2命令的输出结果作为输入。
注意:反引号可以多层嵌套,但是极其不好维护。
我们可以使用反引号实现接收函数通过echo返回的值。
1.10.2 $()形式
为了提高可读性并解决反引号的嵌套问题,Shell 也支持$()作为命令替换的方式。这是更现代的做法,推荐使用。
command1 $(command2)
上述命令中command1使用command2命令的输出值作为输入值。
注意:$()可以进行多层嵌套。
当然$()也可以实现接收函数通过echo返回的值。
二、输入输出
Shell 脚本的输入和输出主要是通过三个标准流来管理的:
- 标准输入(stdin):默认情况下,来自键盘的输入。
- 标准输出(stdout):输出到屏幕。
- 标准错误(stderr):输出错误信息,默认也会输出到屏幕。
Shell 提供了丰富的命令和语法来控制这些流,处理用户输入以及输出结果。
2.1 echo命令
echo命令其实我们已经很熟悉了,我们在Shell编程教程中一直在接触,echo命令是Shell编程中的输出。
对于echo我们不去过多讲解,这里只是简单讲解一下它常见的两个选项:
- -n:禁止换行
- -e:启用转义字符
2.2 read命令
read命令在Shell编程中可以用于输入。
read命令可以允许用户输入一个或多个参数,参数与参数之间使用空格相隔开。
对于read命令我们也可以加上一些选项,例如-r和-t。其中-r选项可以防止反斜杠(\)被转义,-t选项可以设置超时来限制用户输入的时间。如果用户在指定时间内没有输入,read命令将自动终止。
2.3 输入输出重定向
Shell编程中的输入输出重定向允许我们将命令的标准输入、标准输出或标准错误输出重定向到文件、设备或其他命令。这使得 Shell 编程不仅仅限于屏幕交互,还能进行文件操作、日志记录等。
Linux 中的重定向是通过文件描述符来实现的。每个进程都会有一个标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。文件描述符的默认值如下:
- 0:标准输入(stdin)
- 1:标准输出(stdout)
- 2:标准错误输出(stderr)
在重定向中,我们实际上是通过文件描述符来控制数据流向的。Shell 提供了一些符号和语法来控制这些输入输出流。
2.3.1 “>”实现输出重定向
“>”符号将命令的标准输出重定向到文件。如果文件已存在,重定向会覆盖文件内容。
如上图所示,“Hello World”字符串被重定向输出到t1.log文件中,若t1.log文件不存在,那么就会先创建出一个t1.log文件然后再进行内容的填充,若t1.log文件存在,那么t1.log文件里面的内容就会被覆盖。
2.3.2 “>>”实现输出内容追加
“>>”会将命令的标准输出追加到文件的末尾,而不是覆盖文件内容。
2.3.3 “<”实现输入重定向
“<”符号用于从文件中读取输入,而不是从标准输入(键盘)读取数据。它将文件的内容作为输入传递给命令。
上图中的“< t1.log”将t1.log文件里面的内容传递给了cat命令。
2.3.4 标准错误输出重定向
上述我们学习了如何将内容输出或者追加到指定文件,下面我们来了解一下如何将标准错误输出到指定文件。
前面我们说过Shell编程中重定向是通过文件描述符来决定的,假设我们想要输出或者追加标准错误,那么我们只需要在“>”或“>>”命令前面加上描述符:“2”。
2.3.5 标准输出和标准错误同时重定向
我们可以将标准输出和标准错误同时重定向到同一个文件。
command > output_and_error.log 2>&1
上述命令中执行完了command里面的命令以后将内容输出到output_and_error.log文件里面,若command中的命令在执行的时候出现了错误,也会将错误输出到output_and_error.log文件里面。
上述命令中的2>&1指的是将标准错误输出(文件描述符 2)重定向到标准输出(文件描述符 1)。
2.3.6 输入输出的组合使用
我们可以将标准输出和标准错误输出分别重定向到不同的文件。
command > output.txt 2> error.txt
上述命令执行以后会将command的输出内容重定向到ouput.txt文件里面,而command命令执行出现错误的时候会将错误重定向到error.txt文件里面。
2.4 printf命令
printf命令在 Shell 中提供了更强大和灵活的格式化输出功能。它与 C 语言中的printf函数类似,能够格式化文本并输出。
上图中的命令实现换行输出“Hello World”。
printf允许使用格式化字符串(类似于 C 语言中的格式化输出),比如:
- %s:字符串
- %d:数字
- %f:浮点数
- %x:16进制数
- %c:字符
同时printf还支持设置输出的宽度、对齐方式和精度。
注意:上图中“%-5.1f”指的是浮点数所占的宽度是5,数字左对齐,并且浮点数的精度是0.1。
三、文件描述符
3.1 基本概念
文件描述符(File Descriptor,简称 FD)是操作系统内核为每个打开的文件、设备或输入/输出流(包括管道和套接字)分配的一个非负整数。文件描述符使得进程能够通过该数字来访问和操作文件、设备或流。
在 Linux 和类 Unix 系统中,文件描述符是一个由操作系统内核维护的整数,用于标识进程打开的文件或设备。每当一个进程打开文件或设备时,操作系统都会为该文件或设备分配一个文件描述符。
每个进程都有自己的文件描述符表,文件描述符表保存了与文件或设备的映射信息(如文件偏移量、访问模式等)。
3.2 文件描述符的标准值
在 Linux 中,系统预定义了三个标准文件描述符,它们代表了不同的输入输出流:
-
标准输入:文件描述符 0,用于接收用户输入的数据(默认从键盘读取)。
-
标准输出:文件描述符 1,用于向终端或屏幕输出数据。
-
标准错误:文件描述符 2,用于输出错误信息,通常也显示在终端。
这些文件描述符对应的设备或流是由操作系统默认打开的,可以直接通过 Shell 脚本或程序进行读取和写入操作。
3.3 文件描述符的使用
在 Shell 脚本中,我们可以通过以下方式显式地使用和操作文件描述符。
3.3.1 打开文件描述符
Shell 提供了用于将文件或设备与特定文件描述符关联的语法。例如,打开文件并将其与文件描述符 3 关联。
exec 3<> myfile.txt
上述命令打开了myfile.txt文件,并将其关联到文件描述符 3。exec命令用于管理文件描述符。
3.3.2 读写文件描述符
如果我们想要将文件输入或者输出重定向到指定文件描述所指定的文件里面只需要在>或者<命令后面加上“&文件描述符数值”。
例如这里我们将“Hello World”输出重定向到上述定义的文件描述符3里面。
3.3.3 关闭文件描述符
文件描述符使用完毕后,需要关闭它们。可以使用exec关闭特定文件描述符。
exec 文件描述符数值>&-
假设这里我们想要关闭文件描述符3。
3.3.4 复制文件描述符
我们可以使用exec命令来复制文件描述符。这样可以将一个文件描述符的输出复制到另一个文件描述符。
例如上述命令将文件描述符3复制为文件描述符1即标准输出。
通过这种方法,我们可以让不同的命令或进程同时输出到多个地方。