Linux-Shell-脚本编程基础知识-全-
Linux Shell 脚本编程基础知识(全)
原文:
zh.annas-archive.org/md5/0DC4966A30F44E218A64746C6792BE8D
译者:飞龙
前言
GNU/Linux 系统上的 shell 可以说是任何用户最强大的工具。一般来说,shell 充当系统用户和操作系统内核之间的接口。我们使用 shell 来运行命令以执行任务,并经常将输出保存到文件中。虽然这些简单的用例可以通过在 shell 上使用一些命令轻松实现,但有时手头的任务可能比这更复杂。
进入 shell 脚本编写,这是一种神奇的工具,它允许您向 shell 编写逐步说明,告诉它如何执行复杂的任务。然而,除非您知道可以使用的命令,否则仅仅学习编写脚本的语法是不够的。只有这样,脚本才能被重复使用,高效且易于使用。当一个人掌握了 GNU/Linux 系统上可用的命令后,接下来就是疯狂地自动化日常任务——无论是查找文档还是清理早已观看过的旧电影。无论您是其他脚本语言的专家,还是第一次尝试,本书都将向您展示如何使用 shell 脚本做魔术!
本书涵盖的内容
第一章, 脚本编写之旅的开始,告诉您编写 shell 脚本的重要性,以及一个简单的 Hello World shell 脚本程序。它还涵盖了定义变量,内置变量和运算符等基本和必要的 shell 脚本主题。它还包含了有关 shell 扩展的详细解释,这些扩展发生在字符(如~,*,?,[]和{})中。
第二章, I/O,重定向管道和过滤器的实践,讨论了命令和 shell 脚本的标准输入,输出和错误流。它还介绍了如何将它们重定向到其他流的方法。还涵盖了正则表达式等最强大的概念。它为grep
,sed
,uniq
和tail
等命令提供了过滤输入数据中有用数据的指导。
第三章, 有效的脚本编写,提供了关于构建 shell 脚本以组织任务的见解。在讨论脚本退出代码之后,它讨论了基本的编程结构,如条件和循环。然后讨论了代码组织成函数和别名的方法。最后,它详细介绍了xargs
,pushd
和popd
的工作原理。
第四章, 模块化和调试,讨论了通过使用可以被引用的通用代码使 shell 脚本模块化。它还涵盖了脚本的命令行参数的详细信息,以及当脚本出现故障时如何调试。本章还包含了用户如何实现自定义命令完成的信息。
第五章, 自定义环境,继续讨论 shell 环境-它包含的内容,其重要性,以及最终如何修改它。它还带领读者了解 bash 在启动时使用的不同初始化文件。最后,我们讨论了如何检查命令历史记录和管理运行任务。
第六章, 文件操作,讨论了文件,这是任何 UNIX 系统的主要组成部分。它涵盖了“一切皆为文件”的基本理念,并带领读者了解基本文件操作,比较文件,查找文件和创建链接。本章还解释了特殊文件和临时文件的概念,以及文件权限的详细信息。
第七章,“欢迎来到进程”,讨论了活跃的可执行文件,以及它们如何成为进程。从列出和监视运行中的进程,它继续讨论如何利用进程替换。接下来,它涵盖了进程调度优先级、信号、陷阱,以及进程如何相互通信。
第八章,“调度任务和嵌入脚本语言”,讨论了通过使用系统 Cron 在适当的时间安排任务。接下来,它涵盖了负责在大多数现代 Linux 系统中编排启动任务的系统。最后,本章包含了如何将其他脚本语言的脚本嵌入到 shell 脚本中的说明。
您需要为本书准备什么
读者不需要任何先前的知识来理解本书,尽管对 Linux 有一些熟悉会有所帮助。在软件方面,具有足够新的 Linux 发行版和 bash 4 的系统应该能够尝试本书中的所有示例。
本书适合对象
本书面向管理员和那些具有基本 shell 脚本知识并希望学习如何充分利用编写 shell 脚本的人。
约定
在本书中,您会发现许多文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“我们还可以在 shell 编程中使用printf
命令进行打印。”
代码块设置如下:
$ name=foo
$ foo="Welcome to foo world"
$ echo $name
foo
$ new_name='$'$name #new_name just stores string value $foo
$ echo $new_name
$foo
$ eval new_name='$'$name # eval processes $foo string into variable and prints # foo variable value
Welcome to foo world
任何命令行输入或输出都以以下形式编写:
$ ps -p $$
注意
警告或重要提示显示在这样的框中。
提示
技巧和窍门看起来像这样。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它可以帮助我们开发出您真正能够充分利用的标题。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>
,并在主题中提及书名。
如果您在某个专题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请参阅我们的作者指南,网址为www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的自豪所有者,我们有许多事情可以帮助您充分利用您的购买。
下载示例代码
您可以从您在www.packtpub.com
的帐户中下载您购买的所有 Packt Publishing 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support
并注册,以便直接通过电子邮件接收文件。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在我们的书中发现错误——也许是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata
,选择您的书,点击勘误提交表格链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该标题的勘误部分的任何现有勘误列表中。
要查看先前提交的勘误表,请访问www.packtpub.com/books/content/support
,并在搜索框中输入书名。所需信息将出现在勘误部分。
盗版
互联网上侵犯版权的盗版行为是跨所有媒体持续存在的问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。
请通过<copyright@packtpub.com>
与我们联系,并附上涉嫌盗版材料的链接。
我们感谢您帮助保护我们的作者和我们为您提供有价值内容的能力。
问题
如果您对本书的任何方面有问题,可以通过<questions@packtpub.com>
与我们联系,我们将尽力解决问题。
第一章 脚本之旅的开始
基于 Unix、类 Unix 或基于 Linux 的操作系统提供了许多强大的功能。其中,最强大和重要的功能是执行各种命令以快速轻松地执行任务;例如,ls
、cat
、sort
、grep
等。我们将在本书中了解一部分命令和用法。为了运行命令,我们需要一个被广泛称为shell的接口。
Shell 是一个充当用户(我们)和操作系统内核(Linux、Unix 等)之间接口的程序。就 Windows 操作系统而言,shell 的作用类似于 DOS。Unix、类 Unix 或 Linux 操作系统提供了不同的 shell。一些流行的 shell 包括 Bourne shell(sh)、C shell(csh)、Korn shell(ksh)、Bourne Again shell(bash)和 Z shell(zsh)。
在本书中,我们将使用 Linux 操作系统和 Bourne Again shell,通常简称为bash
。基于 Linux 的系统通常已经安装了bash
。如果没有安装bash
,请尝试从您的发行版软件包管理器中安装 bash 软件包。要知道当前您的 Linux 控制台正在使用哪个 shell,请在终端中运行以下命令:
$ ps -p $$
输出如下:
PID TTY TIME CMD
12578 pts/4 00:00:00 bash
在前面的输出中,我们看到CMD
列的值为bash
。这意味着我们当前在当前控制台中使用bash
shell。
如果您的控制台未使用bash
shell,则可以运行以下命令:
$ bash
另外,您的 shell 现在将是bash
。要将bash
设置为默认登录 shell,请运行以下命令:
$ chsh -s /bin/bash
获得的输出如下:
Changing shell for user.
Password:******
Shell changed.
我们现在已经设置了bash
shell,并准备详细学习 shell 脚本。Shell 脚本只是一系列按指定顺序由bash
运行的命令的纯文本文件。当您必须通过运行各种命令执行一系列任务时,编写 shell 脚本非常有用,因为bash
将从脚本文件中读取每一行并在没有用户干预的情况下运行它。用于 shell 脚本的一般文件扩展名是.sh
、.bash
、.zsh
、.ksh
等。与使用文件扩展名不同,最好将文件名保持无扩展名,并让解释器通过查看 shebang(#!
)来识别类型。Shebang 用于指示脚本的解释器以进行执行。例如,它写在脚本文件的第一行中:
#! /bin/bash
这意味着使用bash
shell 来执行给定的脚本。要运行 shell 脚本,请确保它具有执行权限。要为文件的所有者提供执行权限,请运行以下命令:
$ chmod u+x foo
在这里,foo
是 shell 脚本文件。运行此命令后,foo
将对文件的所有者具有执行权限。
现在,我们准备进一步学习 shell 脚本概念的细节。本书中涵盖的每个主题和子主题都将通过示例逐步引导我们成为优秀的 shell 脚本程序员。
在本章中,我们将广泛讨论以下主题:
-
Shell 中的 Hello World
-
定义所需的变量
-
内置 shell 变量
-
操作符
-
Shell 扩展
-
使用 eval 构建命令
-
使用 set 使 bash 行为
Shell 中的 Hello World
每当我们学习一种新的编程语言时,我们首先学习如何在其中编写 Hello World 程序。这是了解和与新语言交互的最佳方式。这也有助于确认已经设置了给定语言中程序的基本环境,并且您可以深入了解这种语言。
与 shell 交互
我们可以以交互方式在控制台中打印命令的输出。控制台也被称为标准输入和输出流。要在bash
控制台中打印任何内容,请使用echo
命令,然后跟上要打印的内容:
$ echo Hello World
Hello World
或者,将要打印的文本放在双引号中:
$ echo "Hello World"
Hello World
您还可以将要打印的文本放在单引号中:
$ echo 'Hello World'
Hello World
我们还可以在 shell 编程中使用printf
命令进行打印。printf
命令也支持格式化打印,类似于 C 编程语言中的printf()
函数:
$ printf "Hello World"
Hello World$
在这里,在输出之后,我们看到命令提示符($
),因为printf
在执行后不会添加默认换行符,而echo
会。因此,我们必须在printf
语句中显式添加换行符(\n
)以添加换行符:
$ printf "Hello World\n"
Hello World
类似于 C 中的printf()
,我们可以在bash
中指定格式化打印。bash
的printf
语法如下:
printf FORMAT [ARGUMENTS]
FORMAT
是描述格式规范的字符串,并在双引号内指定。ARGUMENTS
可以是与格式规范对应的值或变量。格式规范由百分号(%
)后跟格式说明符组成。格式说明符在下表中解释:
格式规范 | 描述 |
---|---|
%u |
这将打印一个无符号整数值 |
%i 或%d |
这将打印一个关联的参数作为有符号数 |
%f |
这将打印一个关联的参数作为浮点数 |
%o |
这将打印一个无符号八进制值 |
%s |
这将打印一个字符串值 |
%X |
这将打印一个无符号十六进制值(0 到 9 和 A 到 F) |
%x |
这将打印一个无符号十六进制值(0 到 9 和 a 到 f) |
以下示例演示了如何在 shell 中使用格式规范打印不同的数据类型格式:
$ printf "%d mul %f = %f\n" 6 6.0 36.0
6 mul 6.000000 = 36.000000
$ printf "%s Scripting\n" Shell
Shell Scripting
我们还可以在格式规范中可选地指定修饰符,以对齐输出以提供更好的格式。格式修饰符放置在%
和格式说明符字符之间。以下表格解释了格式修饰符:
格式修饰符 | 描述 |
---|---|
N | 这是指定最小字段宽度的任何数字。 |
。 | 这与字段宽度一起使用。文本变长时,字段不会扩展。 |
- | 这是字段中左边界文本打印。 |
0 | 这用于用零(0)而不是空格填充填充。默认情况下,使用空格填充。 |
以下示例演示了如何使用格式修饰符来改进打印格式:
$ printf "%d mul %.2f = %.2f\n" 6 6.0 36.0
6 mul 6.00 = 36.00
让我们把它写成脚本
如果我们需要打印一两行,交互式打印是很好的,但是对于大量打印,编写脚本文件是很好且更可取的。脚本文件将包含所有指令,我们可以运行脚本文件来执行所需的任务。
现在,我们将创建一个bash
脚本文件,利用echo
和printf
命令并打印消息:
#!/bin/bash
#Filename: print.sh
#Description: print and echo
echo "Basic mathematics"
printf "%-7d %-7s %-7.2f =\t%-7.2f\n" 23 plus 5.5 28.5
printf "%-7.2f %-7s %-7d =\t%-7.2f\n" 50.50 minus 20 30.50
printf "%-7d %-7s %-7d =\t%-7d\n" 10 mul 5 50
printf "%-7d %-7s %-7d =\t%-7.2f\n" 27 div 4 6.75
bash
脚本中的第一行表示所使用的解释器的路径。第二行是一个注释行,告诉脚本文件的文件名。在 shell 脚本中,我们使用#
添加注释。此外,echo
命令将打印在双引号内写的字符串。对于其余部分,我们使用printf
来打印格式化输出。
要运行此脚本,我们将首先为此脚本的用户/所有者提供执行权限:
$ chmod u+x print.sh
然后,在控制台中运行脚本文件如下:
$ ./print.sh
运行此脚本后的结果如下:
定义所需的变量
现在我们知道如何编写一个简单的 hello world shell 脚本。接下来,我们将熟悉 shell 中的变量以及如何定义和使用 shell 中的变量。
命名规则
变量名可以是字母数字和下划线的组合。变量的名称也不能以数字开头。shell 脚本中的变量名称是区分大小写的。特殊字符,如*,-,+,〜,。,^等,在变量名称中不使用,因为它们在 shell 中具有特殊含义。以下表格说明了命名变量的正确和不正确的方式:
正确的变量名 | 不正确的变量名 |
---|---|
variable | 2_variable |
variable1 | 2variable |
variable_2 | variable$ |
_variable3 | variable*^ |
分配一个值
我们可以使用赋值(=
)运算符为变量赋值,然后是一个值。在分配变量值时,赋值运算符前后不应有任何空格。还有,变量不能单独声明;必须跟随其初始值分配:
$ book="Linux Shell Scripting" # Stores string value
$ book = "Linux Shell Scripting" # Wrong, spaces around = operator
$ total_chapters=8 # Stores integer value
$ number_of_pages=210 # Stores integer value
$ average_pages_per_chapter=26.25 # Stores float value
因此,在 shell 脚本中声明和赋值变量非常容易。您不必担心左侧的变量的数据类型。无论您在右侧提供什么值,变量都会存储该值。
提示
下载示例代码
您可以从www.packtpub.com
的帐户中下载示例代码文件,用于您购买的所有 Packt Publishing 图书。如果您在其他地方购买了本书,可以访问www.packtpub.com/support
并注册,以便直接将文件发送到您的电子邮件。
访问值
要访问变量值,请使用美元符号($
)运算符,后跟变量名:
#!/bin/bash
#Filename: variables.sh
#Description: Basic variable definition and accessing them
book="Linux Shell Scripting"
total_chapters=8
number_of_pages=210
average_pages_per_chapter=26.25
echo "Book name - $book"
echo "Number of Chapters - $total_chapters"
printf "Total number of pages in book - $number_of_pages\n"
printf "Average pages in each chapter - %-.2f\n" $average_pages_per_chapter
此脚本的结果如下:
Book name - Linux Shell Scripting
Number of Chapters - 8
Total number of pages in book - 210
Average pages in each chapter – 26.25
我们可以使用unset
关键字在bash
中删除变量的值。使用unset
将变量删除并重置为空:
#!/bin/bash
#Filename: unset.sh
#Description: removing value of a variable
fruit="Apple"
quantity=6
echo "Fruit = $fruit , Quantity = $quantity"
unset fruit
echo "Fruit = $fruit , Quantity = $quantity"
运行此脚本后的结果如下:
Fruit = Apple , Quantity = 6
Fruit = , Quantity = 6
很明显,我们在水果变量上使用了 unset,所以当我们尝试在第 8 行取消设置变量水果后,它什么也不打印。quantity
变量仍保留其值,因为我们没有在其上使用 unset。
常量变量
我们还可以在bash
中创建constant
变量,其值无法更改。使用readonly
关键字声明常量变量。我们还可以使用declare -r
后跟变量名使其成为常量:
#!/bin/bash
#Filename: constant.sh
#Description: constant variables in shell
readonly text="Welcome to Linux Shell Scripting"
echo $text
declare -r number=27
echo $number
text="Welcome"
运行此脚本后的结果如下:
Welcome to Linux Shell Scripting
27
constant.sh: line 9: text: readonly variable
从错误消息中可以明显看出,我们无法更改常量变量的值,也无法取消常量变量的值。
从用户输入中读取变量
我们可以使用read
shell 内置命令要求用户提供输入。用户要提供的输入数量等于提供给read
的参数数量。用户插入的值存储在传递给read
的相应参数中。所有参数都充当变量,其中存储相应的用户输入值。
read
的语法如下:
read [options] var1 var2 … varN
如果未指定参数中的变量,则用户的输入值将存储在内置变量REPLY
中,并且可以使用$REPLY
进一步访问。
我们可以按如下方式在其输入变量中读取用户输入:
$ read
Hello World
$ echo $REPLY
Hello World
我们可以按如下方式从用户输入中读取值:
$ read text
Hello
$ echo $text
Hello
我们可以按如下方式从用户输入中读取多个值:
$ read name usn marks
Foo 345 78
$ echo $name $usn $marks
Foo 345 78
我们可以仅读取n
个字符,而不必等待用户输入完整行,如下所示:
$ read -n 5 # option -n number takes only 5 characters from user input
Hello$
$ echo $REPLY
Hello
我们可以在读取用户输入之前提示用户消息如下:
$ read -p "What is your name?" # -p allows to prompt user a message
What is your name?Foo
$ echo $REPLY
Foo
在控制台中读取时隐藏输入字符:
$ read -s -p "Enter your secret key:" # -s doesn't echo input in console
Enter your secret key:$ #Pressing enter key brings command prompt $
echo $REPLY
foo
以下示例显示了read
命令的用法:
#!/bin/bash
#Filename: read.sh
#Description: Find a file in a path entered by user
read -p "Enter filename to be searched:"
filename=$REPLY
read -p "Enter path for search:" path
echo "File $filename search matches to"
find $path -name $filename
在bash
中运行read.sh
脚本的结果如下:
Enter filename to be searched:read
Enter path for search:/usr/bin
File read search matches to
/usr/bin/read
在这里,find
命令已用于在指定路径中搜索文件名。命令find
的详细讨论将在第六章中进行,处理文件。
内置 shell 变量
内置 shell 变量是预定义的全局变量,我们可以在脚本的任何时间点使用它们。这些是保留的 shell 变量,其中一些可能由bash
分配默认值。某些变量的值将取决于当前的 shell 环境设置。不同类型的 shell 可能具有一些特定的保留变量。所有内置 shell 变量的名称都将是大写。
bash
shell 中可用的一些保留 shell 变量如下:
在 bash 中可用的 shell 变量 | 描述 |
---|---|
BASH |
这是当前调用的bash 的绝对路径 |
BASH_VERSION |
这是bash 的版本号 |
BASHPID |
这是当前bash 进程的进程 ID |
EUID |
这是当前用户的有效用户 ID,在启动时分配 |
HOME |
这是当前用户的主目录 |
HOSTNAME |
这是当前主机的名称 |
PATH |
这是 shell 将查找命令的以冒号分隔的目录列表 |
PPID |
这是 shell 父进程的进程 ID |
PWD |
这是当前工作目录 |
可以在man bash
中找到更多的 shell 变量。
我们将通过在 shell 脚本中打印其值来查看这些 shell 变量包含的值:
#!/bin/bash
#Filename: builtin_shell_variables.sh
#Description: Knowing about builtin shell variables
echo "My current bash path - $BASH"
echo "Bash version I am using - $BASH_VERSION"
echo "PID of bash I am running - $BASHPID"
echo "My home directory - $HOME"
echo "Where am I currently? - $PWD"
echo "My hostname - $HOSTNAME"
运行此脚本后,输出可能会有所不同,具体取决于系统中这些变量的值设置为何。示例输出如下:
My current bash path - /bin/sh
Bash version I am using – 4.3.33(1)-release
PID of bash I am running - 4549
My home directory - /home/sinny
Where am I currently? - /home/sinny/Documents/
My hostname – localhost.localdomain
shell 变量,如PWD
、PATH
、HOME
等,非常有用,可以通过简单地回显其中的值来快速获取信息。我们还可以添加或修改一些 shell 变量的值,如PATH
,以便在其中添加我们希望 shell 查找命令的自定义路径。
修改PATH
变量值的一个用例是:假设我已经编译了一个生成一些二进制文件(如foo
和bar
)的源代码。现在,如果我希望 shell 也在该特定目录中搜索命令,那么将该目录路径添加到PATH
变量中即可。以下是一个小的 shell 脚本示例,显示了如何执行此操作:
#!/bin/bash
#Filename: path_variable.sh
#Description: Playing with PATH variable
echo "Current PATH variable content - $PATH"
echo "Current directory - $PWD"
echo "Content of current directory\n`ls`"
PATH=$PATH:$PWD
echo "New PATH variable content - $PATH"
# Now execute commands available in current working diectory
运行此脚本后的输出将如下所示:
Current PATH variable content - /usr/lib64/qt-3.3/bin:/bin:/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/home/sinny/go/source_code/go/bin:/home/sinny/.local/bin:/home/sinny/bin
Current directory - /home/sinny/test_project/bin
Content of current directory – foo bar
New PATH variable content - /usr/lib64/qt-/usr/lib64/qt-3.3/bin:/bin:/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/home/sinny/go/source_code/go/bin:/home/sinny/.local/bin:/home/sinny/bin: /home/sinny/test_project/bin
从输出中我们可以看到,新的PATH
变量已经添加了我的自定义路径。从下一次开始,每当我使用设置了这个自定义PATH
变量的foo
或bar
命令时,就不需要foo
和bar
命令/二进制文件的绝对路径了。Shell 将通过查看其PATH
变量来找到这些变量。这仅在当前 shell 会话期间有效。我们将在第五章中看到这一点,自定义环境中的配方,修改 shell 环境。
操作符
与其他编程语言类似,shell 编程也支持各种类型的操作符来执行任务。操作符可以分为以下几类:
-
赋值操作符
-
算术操作符
-
逻辑操作符
-
比较操作符
赋值操作符
等于操作符(=
)是用于初始化或更改变量值的赋值操作符。此操作符适用于任何数据,如字符串、整数、浮点数、数组等。例如:
$ var=40 # Initializing variable var to integer value
$ var="Hello" # Changing value of var to string value
$ var=8.9 # Changing value of var to float value
算术操作符
算术操作符用于对整数执行算术运算。它们如下:
-
+(加)
-
-(减)
-
*(乘法)
-
/(除法)
-
**(指数)
-
%(取模)
-
+=(加等于)
-
-=(减等于)
-
*=(乘等于)
-
/=(斜杠等于)
-
%=(模等于)
要执行任何算术操作,在实际算术表达式之前,我们需要在bash
中加上expr
和let
关键字。以下示例显示了如何在bash
中执行算术操作:
#!/bin/bash
#Filename: arithmetic.sh
#Description: Arithmetic evaluation
num1=10 num2=5
echo "Numbers are num1 = $num1 and num2 = $num2"
echo "Addition = `expr $num1 + $num2`"`"
echo "Subtraction = `expr $num1 - $num2`"
echo "Multiplication = `expr $num1 \* $num2`"
echo "Division = `expr $num1 / $num2`"
let "exponent = $num1 ** num2"
echo "Exponentiation = $exponent"
echo "Modulo = `expr $num1 % $num2`"
let "num1 += $num2"
echo "New num1 = $num1"
let "num1 -= $num1"
echo "New num2 = $num2"
运行此脚本后的结果如下:
Numbers are num1 = 10 and num2 = 5
Addition = 15
Subtraction = 5
Multiplication = 50
Division = 2
Exponentiation = 100000
Modulo = 0
New num1 = 15
New num2 = 5
逻辑操作符
逻辑操作符也被称为布尔操作符。它们是:
!(非)、&&(与)和||(或)
执行逻辑操作返回一个布尔值,如true(1)
或false(0)
,具体取决于操作所涉及的变量的值。
一个有用的用例是:假设我们希望在第一个命令或操作成功返回时执行一个命令。在这种情况下,我们可以使用&&
操作符。同样,如果我们想要执行另一个命令,无论第一个命令是否执行,我们都可以在两个命令之间使用||
操作符。我们可以使用!操作符来否定真值。例如:
$ cd ~/Documents/ && ls
cd
命令用于将当前路径更改为指定的参数。在这里,cd ~/Documents/
命令将更改目录到Documents
(如果存在)。如果失败,则ls
不会被执行,但如果cd
到Documents
成功,则ls
命令将显示Documents 目录
的内容:
$ cat ~/file.txt || echo "Current Working directory $PWD"
cat: /home/skumari/file.txt: No such file or directory
Current Working directory /tmp/
cat
命令显示file.txt
的内容(如果存在)。无论cat ~/file.txt
命令是否执行,稍后将执行的命令是echo "当前工作目录 $PWD"
:
$ ! cd /tmp/foo && mkdir /tmp/foo
bash: cd: /tmp/foo: No such file or directory
通过运行上述命令,首先会尝试更改目录到/tmp/foo
。在这里,! cd /tmp/foo
表示如果更改目录到/tmp/foo
不成功,则运行第二个命令,即mkdir /tmp/foo
。mkdir
命令用于创建一个新目录。由于进行命令执行,如果目录/tmp/foo
不存在,它将被创建。
$ cd /tmp/foo
自从/tmp/foo
目录被创建后,目录的成功更改将发生。
比较运算符
比较运算符比较两个变量,并检查条件是否满足。它们对整数和字符串有所不同。
对整数变量有效的比较运算符(将a
和b
视为两个整数变量;例如,a=20, b=35
)如下:
-
-eq(等于)-
[ $a -eq $b ]
-
-ne(不等于)- [ $a -ne $b ]
-
-gt(大于)- [ $a -gt $b ]
-
-ge 或>=(大于或等于)- [ $a -ge $b ]
-
-lt(小于)- [ $a -lt $b ]
-
-le(小于或等于)- [ $a -le $b ]
-
<(小于)- (($a < $b))
-
<=(小于或等于)- (($a <= $b))
-
(is greater than) - (($a > $b))
-
=(大于或等于)- (($a >= $b))
对字符串变量有效的比较运算符(将 a 和 b 视为两个字符串变量;例如,a="Hello" b="World")如下:
-
=(等于);例如,
[ $a = $b ]
-
!=(不等于);例如,[ $a != $b ]
-
<(小于);例如,[ $a < $b ]或[[ $a < $b ]]或(( $a < $b ))
-
(大于);例如,[ $a > $b ]或[[ $a > $b ]]或(( $a > $b ))
-
-n(字符串非空);例如,[ -n $a ]
-
-z(字符串长度为零或为空);例如,[ -z $a ]
Shell 使用<
和>
操作符进行重定向,因此如果在[ … ]下使用,应该使用转义(\
)。双括号,(( ... ))或[[ … ]],不需要转义序列。使用[[ … ]]还支持模式匹配。
我们将在第三章中更详细地看到操作符的用法和示例,有效脚本编写。
Shell 扩展
在使用 shell 时,我们执行了许多类似和重复的任务。例如,在当前目录中,有 100 个文件,但我们只对文件扩展名为.sh
的 shell 脚本感兴趣。我们可以执行以下命令来查看当前目录中的 shell 脚本文件:
$ ls *.sh
这将显示所有以.sh
结尾的文件。从这里可以得到一个有趣的启示是*
通配符。它表示文件名可以是任何东西,并以.sh
结尾的文件列表。
Shell 扩展所有通配符模式。最新通配符模式列表如下:
-
~(波浪号)
-
*(星号)
-
?(问号)
-
[ ](方括号)
-
{ }(花括号)
为了解释不同通配符的 shell 扩展,我们将在我们的home
目录中使用mkdir
命令创建一个测试文件夹,其中包含如下所述的不同文件:
$ mkdir ~/test && cd ~/test
$ touch a ab foo bar hello moo foo.c bar.c moo.c hello.txt foo.txt bar.sh hello.sh moo.sh
touch
命令如果文件不存在则创建一个空文件。如果文件存在,则文件时间戳会更改:
$ ls
a ab bar bar.c bar.sh foo foo.c foo.txt hello hello.sh hello.txt moo moo.c moo.sh
运行上述命令将创建一个测试目录,并在测试目录中创建作为touch
命令参数给出的文件。
~(波浪号)
当~
出现在未引用字符串的开头时,~
会被bash
扩展。扩展取决于使用了什么tilde-prefix
。tilde-prefix
是直到第一个未引用的(/)斜杠的字符。一些bash
扩展如下:
-
~
:这是用户的主目录;该值设置在$HOME
变量中 -
~user_name
:这是用户user_name
的主目录 -
~user_name
/file_name
:这是用户user_name
主目录中的文件/目录file_name
-
~/file_name
:这是$HOME
/file_name
中的文件/目录 -
~+
:这是当前工作目录;该值设置在$PWD
变量中 -
~-
:这是旧的或上一个工作目录;该值设置在$OLDPWD
变量中 -
~+/file_name
:这是当前目录中的文件/目录file_name
,即$PWD/file_name
-
~-/file_name
:这是旧/上一个工作目录中的文件/目录file_name
,即$OLDPWD/file_name
*(星号)
它匹配零个或多个字符。以测试目录为例:
- 按如下方式显示所有文件:
$ ls *
a ab bar bar.c bar.sh foo foo.c foo.txt hello hello.sh hello.txt moo moo.c moo.sh
- 按如下方式显示 C 源文件:
$ ls *.c
bar.c foo.c moo.c
- 按如下方式显示具有
a
的文件:
$ ls *a*
a ab bar bar.c bar.sh
- 按如下方式删除具有扩展名.txt 的文件:
$ rm *.txt
$ ls
a ab bar bar.c bar.sh foo foo.c hello hello.sh moo moo.c moo.sh
?(问号)
它匹配任何单个字符:?(单个问号将匹配一个字符),??(双问号匹配任何两个字符),依此类推。以测试目录为例:
$ touch a ab foo bar hello moo foo.c bar.c moo.c hello.txt foo.txt bar.sh hello.sh moo.sh
这将重新创建在上一个示例中删除的文件,并更新现有文件的访问和修改时间:
- 获取文件名长度与扩展文件无关:
$ ls ??
ab
- 获取文件名长度为 2 或 5 的文件:
$ ls ?? ?????
ab bar.c foo.c hello moo.c
- 删除文件名为四个字符长的文件:
$ rm ????
rm: cannot remove '????': No such file or directory
This error is because there is no file name with 4 character
- 将文件移动到
/tmp
目录,文件名至少为三个字符长:
$ mv ???* /tmp
$ ls
a ab
我们只在测试目录中看到两个文件,因为其余的文件长度为 3 或更长。
[ ](方括号)
方括号匹配方括号内提到的字符集中的任何字符。字符可以指定为单词或范围。
使用 -(连字符)可以指定一系列字符。例如:
-
[a-c]
:这匹配 a、b 或 c -
[a-z]
:这匹配从 a 到 z 的任何字符 -
[A-Z]
:这匹配从 A 到 Z 的任何字符 -
[0-9]
:这匹配 0 到 9 之间的任何字符
以测试目录为例,在测试目录中重新创建文件:
$ touch a ab foo bar hello moo foo.c bar.c moo.c hello.txt foo.txt bar.sh hello.sh moo.sh
获取文件名以a
、b
、c
或d
开头的文件,使用以下命令:
$ ls [a-d]*
a ab bar bar.c bar.sh
获取文件名以任何字母开头并以字母o
或h
结尾的文件,使用以下命令:
$ ls [a-zA-Z]*[oh]
foo hello hello.sh moo moo.sh
获取文件名中至少包含两个字母o
的文件,使用以下命令:
$ ls *[o]*[o]*
foo foo.c foo.txt moo moo.c moo.sh
[!characters]
(感叹号)用于匹配不在方括号内提到的字符集中的字符。
获取文件名中不包含数字的文件,使用以下命令:
$ ls [!0-9]*
a ab bar bar.c bar.sh foo foo.c foo.txt hello hello.sh hello.txt moo moo.c moo.sh
{ }(花括号)
它创建多个通配符模式进行匹配。花括号表达式可以包含逗号分隔的字符串列表、范围或单个字符。
可以使用以下方式指定范围:
-
{a..z}
:这匹配从 a 到 z 的所有字符 -
{0..6}
:这匹配数字 0、1、2、3、4、5 和 6
以测试目录为例,重新创建测试目录中的文件:
$ touch a ab foo bar hello moo foo.c bar.c moo.c hello.txt foo.txt bar.sh hello.sh moo.sh
获取具有文件扩展名.sh
或.c
的文件,使用以下命令:
$ ls {*.sh,*.c}
bar.c bar.sh foo.c hello.sh moo.c moo.sh
使用以下命令将bar.c
复制到bar.html
:
$ cp bar{.c,.cpp} # Expands to cp bar.c bar.cpp
$ ls bar.*
bar.c bar.cpp bar.sh
使用以下命令打印从1
到50
的数字:
$ echo {1..50}
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
创建以hello
开头并具有扩展名.cpp
的 10 个文件:
$ touch hello{0..9}.cpp
$ ls *.cpp
hello0.cpp hello1.cpp hello2.cpp hello3.cpp hello4.cpp hello5.cpp hello6.cpp hello7.cpp hello8.cpp hello9.cpp
为了避免通配符的 shell 扩展,使用反斜杠(\)或在单引号(' ')中写入字符串。
使用 eval 构建命令
eval
命令是一个 shell 内置命令,用于通过连接传递给eval
的参数来构造一个命令。连接的命令进一步由 shell 执行并返回结果。如果没有给eval
传递参数,则返回0
。
eval
命令的语法如下:
eval [arg …]
以下示例显示了使用eval
将变量扩展为另一个变量的名称:
$ name=foo
$ foo="Welcome to foo world"
$ echo $name
foo
$ new_name='$'$name #new_name just stores string value $foo
$ echo $new_name
$foo
$ eval new_name='$'$name # eval processes $foo string into variable and prints # foo variable value
Welcome to foo world
eval
有用的另一个示例如下:
$ pipe="|"
$ df $pipe wc # Will give error because
df: '|': No such file or directory
df: 'wc': No such file or directory
$ eval df $pipe wc # eval executes it as shell command
12 73 705
在这里,df
命令显示了系统磁盘的使用情况:
A shell script showing the use of eval is as follows:
#!/bin/bash
#Filename: eval.sh
#Description: Evaluating string as a command using eval
cmd="ls /usr"
echo "Output of command $cmd -"
eval $cmd #eval will treat content of cmd as shell command and execute it
cmd1="ls /usr | wc -l"
echo "Line count of /usr -"
eval $cmd1
expression="expr 2 + 4 \* 6"
echo "Value of $expression"
eval $expression
运行脚本将给出以下结果:
Output of command ls /usr -
avr bin games include lib lib64 libexec local sbin share src tmp
Line count of /usr -
12
Value of expr 2 + 4 \* 6
26
使用 set 使 bash 行为
set
命令是一个 shell 内置命令,用于在 shell 中设置和取消设置本地变量的值。
使用 set 的语法如下:
set [--abBCefhHkmnpPtuvx] [-o option] [arg …]
一些选项值是allexport
、braceexpand
、history
、keyword
、verbose
和xtrace
。
使用不带任何选项的set
命令以一种格式显示所有 shell 变量和函数的名称和值,该格式可以作为设置和取消当前设置变量的输入重用。
在第一次失败时退出
在 shell 脚本中,默认情况下,如果当前行发生错误,则会执行下一行。有时,我们可能希望在遇到错误后停止运行脚本。set
的-e
选项确保一旦管道中的任何命令失败,脚本就会退出。
在以下 shell 脚本中,do_not_exit_on_failure.sh
不使用带有-e
选项的set
:
$ cat do_not_exit_on_failure.sh
#!/bin/bash
# Filename: do_not_exit_on_failure.sh
# Description: Resume script after an error
echo "Before error"
cd /root/ # Will give error
echo "After error"
运行此脚本后,输出如下:
Before error
do_not_exit_on_failure.sh: line 6: cd: /root/: Permission denied
After error
我们看到错误后的命令也被执行了。为了在遇到错误后停止执行,请在脚本中使用set -e
。以下脚本演示了相同的情况:
$ cat exit_on_failure.sh
#!/bin/bash
# Filename: exit_on_failure.sh
# Description: Exits script after an error
set -e
echo "Before error"
cd /root/ # Will give error
echo "After error"
运行上述脚本后的输出如下:
Before error
exit_on_failure.sh: line 7: cd: /root/: Permission denied
我们可以看到,在第 7 行遇到错误后,脚本已经终止。
启用/禁用符号链接的解析路径
使用带有-P
选项的set
不解析符号链接。以下示例演示了如何启用或禁用/bin
目录的符号链接解析,该目录是/usr/bin/
目录的符号链接:
$ ls -l /bin
lrwxrwxrwx. 1 root root 7 Nov 18 18:03 /bin -> usr/bin
$ set –P # -P enable symbolic link resolution
$ cd /bin
$ pwd
/usr/bin
$ set +P # Disable symbolic link resolution
$ pwd
/bin
设置/取消设置变量
我们可以使用set
命令查看当前进程可访问的所有本地变量。本地变量在子进程中不可访问。
我们可以创建自己的变量并将其设置为本地,如下所示:
$ MYVAR="Linux Shell Scripting"
$ echo $MYVAR
Linux Shell Scripting
$ set | grep MYVAR # MYVAR local variable is created
MYVAR='Linux Shell Scripting'
$ bash # Creating a new bash sub-process in current bash
$ set | grep MYVAR
$ # Blank because MYVAR is local variable
要使变量对其子进程也可访问,请使用export
命令,后跟要导出的变量:
$ MYVARIABLE="Hello World"
$ export MYVARIABLE
$ bash # Creating a new bash sub-process under bash
$ echo $MYVARIABLE
Hello World
这将把MYVARIABLE
变量导出到从该进程运行的任何子进程。要检查MYVARIABLE
是否已导出,请运行以下命令:
$ export |grep MYVARIABLE
declare -x MYVARIABLE="Hello World"
$ export | grep MYVAR
$MYVAR variable is not present in sub-process but variable MYVARIABLE is present in sub-process.
要取消本地或导出的变量,请使用unset
命令,它将将变量的值重置为 null:
$ unset MYVAR # Unsets local variable MYVAR
$ unset MYVARIABLE # Unsets exported variable MYVARIABLE
总结
阅读完本章后,您了解了如何通过打印、回显和询问用户输入来在 bash 中编写简单的 shell 脚本。您现在应该对在 shell 中定义和使用变量以及存在哪些内置 shell 变量有了很好的理解。您现在熟悉 shell 中有哪些操作符,以及它们如何创建和评估自己的表达式。有关通配符的信息在本章中可用,这使得在处理类似类型的数据或模式时,工作变得更加容易。shell 内置命令set
可以轻松修改 shell 变量。
本章为即将到来的章节奠定了基础。现在,在下一章中,您将了解有关标准输入、输出和错误的信息。此外,将详细介绍如何使用命令的输出,然后过滤/转换它们以根据您的需要显示数据。
第二章:开始使用 I/O、重定向管道和过滤器
在日常工作中,我们会遇到不同类型的文件,比如文本文件、来自不同编程语言的源代码文件(例如file.sh
、file.c
和file.cpp
)等。在工作时,我们经常对文件或目录执行各种操作,比如搜索给定的字符串或模式、替换字符串、打印文件的几行等。如果我们必须手动执行这些操作,那是很困难的。在一个包含成千上万个文件的目录中手动搜索字符串或模式可能需要几个月的时间,并且很容易出错。
Shell 提供了许多强大的命令,可以使我们的工作更轻松、更快速、更无误。Shell 命令有能力从不同的流(如标准输入、文件等)中操作和过滤文本。其中一些命令是grep
、sed
、head
、tr
、sort
等。Shell 还具有将一个命令的输出重定向到另一个命令的功能,使用管道(|
)。使用管道有助于避免创建不必要的临时文件。
这些命令中最好的一个特点是它们都有man
页面。我们可以直接转到man
页面,并通过运行man
命令查看它们提供的所有功能。大多数命令都有选项,比如--help
来查找帮助用法,以及--version
来了解命令的版本号。
本章将详细介绍以下主题:
-
标准 I/O 和错误流
-
重定向标准 I/O 和错误流
-
管道和管道——连接命令
-
正则表达式
-
使用
grep
过滤输出 -
使用
sed
编辑输出 -
使用
tee
复制流 -
排序和查找唯一文本
-
使用
tr
进行基于字符的翻译 -
基于行的过滤——
head
和tail
-
基于切割的选择
标准 I/O 和错误流
在 shell 编程中,有不同的方式来提供输入(例如,通过键盘和终端)和显示输出(例如,终端和文件)以及执行命令或程序时的错误(例如,终端)。
以下示例显示了运行命令时的输入、输出和错误:
- 通过键盘由用户输入和通过标准输入流(即终端)由程序获取的输入如下:
$ read -p "Enter your name:"
Enter your name:Foo
- 打印在标准输出流(即终端)上的输出如下:
$ echo "Linux Shell Scripting"
Linux Shell Scripting
- 打印在标准错误流(即终端)上的错误消息如下:
$ cat hello.txt
cat: hello.txt: No such file or directory
当程序执行时,默认情况下会打开三个文件,它们是stdin
、stdout
和stderr
。以下表格提供了这三个文件的简要描述:
文件描述符编号 | 文件名 | 描述 |
---|---|---|
0 |
stdin |
这是从终端读取的标准输入 |
1 |
stdout |
这是标准输出到终端 |
2 |
stderr |
这是标准错误输出到终端 |
文件描述符
文件描述符是表示操作系统中打开文件的整数编号。每个打开的文件都有唯一的文件描述符编号。文件描述符的编号从0
开始递增。
在 Linux 中创建新进程时,会为其提供标准输入、输出和错误文件,以及其他所需的打开文件。
要知道与进程相关联的所有打开文件描述符,我们将考虑以下示例:
首先运行一个应用程序并获取其进程 ID。考虑运行bash
作为一个例子来获取 bash 的 PID:
$ pidof bash
2508 2480 2464 2431 1281
我们看到有多个 bash 进程正在运行。以 bash PID 示例2508
为例,运行以下命令:
$ ls -l /proc/2508/fd
total 0
lrwx------. 1 sinny sinny 64 May 20 00:03 0 -> /dev/pts/5
lrwx------. 1 sinny sinny 64 May 20 00:03 1 -> /dev/pts/5
lrwx------. 1 sinny sinny 64 May 19 23:22 2 -> /dev/pts/5
lrwx------. 1 sinny sinny 64 May 20 00:03 255 -> /dev/pts/5
我们看到 0、1 和 2 这三个打开的文件描述符与 bash 进程相关联。目前,它们都指向/dev/pts/5
。pts
是伪终端从属。
因此,无论我们在这个 bash 中做什么,与此 PID 相关的输入、输出和错误都将被写入/dev/pts/5
文件。但是,pts
文件是伪文件,内容在内存中,因此当您打开文件时,您看不到任何内容。
重定向标准 I/O 和错误流
我们有选项可以重定向标准输入、输出和错误,例如到文件、另一个命令、预期的流等。重定向在不同方面非常有用。例如,我有一个 bash 脚本,其输出和错误显示在标准输出上,也就是终端上。我们可以通过将其中一个或两者重定向到文件来避免混合错误和输出。用于重定向的不同运算符。以下表格显示了一些用于重定向的运算符及其描述:
运算符 | 描述 |
---|---|
> |
这将标准输出重定向到文件中 |
>> |
这将标准输出附加到文件中 |
< |
这将标准输入从文件中重定向 |
>& |
这将标准输出和错误重定向到文件中 |
>>& |
这将标准输出和错误附加到文件中 |
| |
这将输出重定向到另一个命令 |
重定向标准输出
程序或命令的输出可以重定向到文件。将输出保存到文件中在将来查看输出时非常有用。对于使用不同输入运行的程序的大量输出文件,可以用于研究程序输出行为。
例如,将 echo 输出重定向到output.txt
的示例如下:
$ echo "I am redirecting output to a file" > output.txt
$
我们可以看到终端上没有显示任何输出。这是因为输出被重定向到output.txt
。运算符'>'(大于)告诉 shell 将输出重定向到运算符后面提到的任何文件名。在我们的情况下,它是output.txt
:
$ cat output.txt
I am redirecting output to a file
现在,让我们向output.txt
文件添加一些更多的输出:
$ echo "I am adding another line to file" > output.txt
$ cat output.txt
I am adding another line to file
我们注意到output.txt
文件的先前内容被擦除了,现在只有最新重定向的内容。要保留先前的内容并将最新的重定向输出附加到文件中,使用运算符'>>':
$ echo "Adding one more line" >> output.txt
$ cat output.txt
I am adding another line to file
Adding one more line
我们还可以在 bash 中使用运算符' | '(管道)将程序/命令的输出重定向到另一个命令:
$ ls /usr/lib64/ | grep libc.so
libc.so
libc.so.6
在这个例子中,我们使用' | '(管道)运算符将ls
的输出传递给grep
命令,grep
给出了libc.so
库的匹配搜索结果:
重定向标准输入
不是从标准输入获取输入到命令,而是使用<(小于)运算符从文件中重定向输入。例如,我们想要计算从重定向标准输出部分创建的output.txt
文件中的单词数:
$ cat output.txt
I am adding another line to file
Adding one more line
$ wc -w < output.txt
11
我们可以对output.txt
的内容进行排序:
$ sort < output.txt # Sorting output.txt on stdout
Adding one more line
I am adding another line to file
我们还可以将patch
文件作为patch
命令的输入,以便在源代码中应用patch.diff
。patch
命令用于应用对文件进行的额外更改。额外的更改以diff
文件的形式提供。diff
文件包含通过运行diff
命令对原始文件和修改后文件之间的更改。例如,我有一个要应用在output.txt
上的补丁文件:
$ cat patch.diff # Content of patch.diff file
2a3
> Testing patch command
$ patch output.txt < patch.diff # Applying patch.diff to output.txt
$ cat output.txt # Checking output.txt content after applying patch
I am adding another line to file
Adding one more line
Testing patch command
重定向标准错误
在 bash 中执行命令/程序时可能会出现错误,原因可能是无效输入、参数不足、文件未找到、程序中的错误等:
$ cd /root # Doing cd to root directory from a normal user
bash: cd: /root/: Permission denied
Bash prints the error on a terminal saying, permission denied.
通常,错误会打印在终端上,这样我们就可以很容易地知道错误的原因。在终端上打印错误和输出可能会很烦人,因为我们必须手动查看每一行,并检查程序是否遇到任何错误:
$ cd / ; ls; cat hello.txt; cd /bin/; ls *.{py,sh}
我们在前面的部分运行了一系列命令。首先cd
到/
,ls
查看/
的内容,cat 文件hello.txt
,cd
到/bin
并查看/bin
中匹配*.py
和*.sh
的文件。输出如下:
bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
cat: hello.txt: No such file or directory
alsa-info.sh kmail_clamav.sh sb_bnfilter.py sb_mailsort.py setup-nsssysinit.sh amuFormat.sh kmail_fprot.sh sb_bnserver.py sb_mboxtrain.py struct2osd.sh core_server.py kmail_sav.sh sb_chkopts.py sb_notesfilter.py
我们看到hello.txt
在/
目录中不存在,因此终端上打印了一个错误,以及其他输出。我们可以按如下方式重定向错误:
$ (cd / ; ls; cat hello.txt; cd /bin/; ls *.{py,sh}) 2> error.txt
bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
alsa-info.sh kmail_clamav.sh sb_bnfilter.py sb_mailsort.py setup-nsssysinit.sh amuFormat.sh kmail_fprot.sh sb_bnserver.py sb_mboxtrain.py struct2osd.sh core_server.py kmail_sav.sh sb_chkopts.py sb_notesfilter.py
我们可以看到错误已重定向到error.txt
文件。要验证,请检查error.txt
的内容:
$ cat error.txt
cat: hello.txt: No such file or directory
多重重定向
我们可以在命令或脚本中重定向stdin
,stdout
和stderr
,或者它们的一些组合。
以下命令重定向了stdout
和stder
:
$ (ls /home/ ;cat hello.txt;) > log.txt 2>&1
在这里,stdout
被重定向到log.txt
,错误消息也被重定向到log.txt
。在2>&1
中,2>
表示重定向错误,&1
表示重定向到stdout
。在我们的情况下,我们已经将stdout
重定向到log.txt
文件。因此,现在stdout
和stderr
的输出都将写入log.txt
,并且不会打印在终端上。要验证,我们将检查log.txt
的内容:
$ cat log.txt
lost+found
sinny
cat: hello.txt: No such file or directory
以下示例显示了stdin
,stdout
和stderr
的重定向:
$ cat < ~/.bashrc > out.txt 2> err.txt
在这里,home
目录中的.bashrc
文件作为cat
命令的输入,并且其输出被重定向到out.txt
文件。在中间遇到的任何错误都被重定向到err.txt
文件。
以下bash
脚本将更清楚地解释stdin
,stdout
,stderr
及其重定向:
#!/bin/bash
# Filename: redirection.sh
# Description: Illustrating standard input, output, error
# and redirecting them
ps -A -o pid -o command > p_snapshot1.txt
echo -n "Running process count at snapshot1: "
wc -l < p_snapshot1.txt
echo -n "Create a new process with pid = "
tail -f /dev/null & echo $! # Creating a new process
echo -n "Running process count at snapshot2: "
ps -A -o pid -o command > p_snapshot2.txt
wc -l < p_snapshot2.txt
echo
echo "Diff bewteen two snapshot:"
diff p_snapshot1.txt p_snapshot2.txt
此脚本保存系统中所有运行进程的两个快照,并生成diff
。运行进程后的输出将如下所示:
$ sh redirection.sh
Running process count at snapshot1: 246
Create a new process with pid = 23874
Running process count at snapshot2: 247
Diff bewteen two snapshot:
246c246,247
< 23872 ps -A -o pid -o command
---
> 23874 tail -f /dev/null
> 23875 ps -A -o pid -o command
管道和管道 - 连接命令
程序的输出通常保存在文件中以供进一步使用。有时,为了将一个程序的输出用作另一个程序的输入,会创建临时文件。我们可以使用 bash 管道和管道来避免创建临时文件,并将一个程序的输出作为另一个程序的输入。
管道
由运算符|
表示的管道将左侧进程的标准输出连接到右侧进程的标准输入,通过进程间通信机制。换句话说,|
(管道)通过将一个命令的输出作为另一个命令的输入来连接命令。
考虑以下示例:
$ cat /proc/cpuinfo | less
在这里,cat
命令不是在stdout
上显示/proc/cpuinfo
文件的内容,而是将其输出作为less
命令的输入。less
命令从cat
获取输入,并在每页上显示在stdout
上。
另一个使用管道的示例如下:
$ ps -aux | wc -l # Showing number of currently running processes in system
254
管道
管道是由运算符'|
'分隔的程序/命令序列,每个命令的执行输出都作为下一个命令的输入。管道中的每个命令都在一个新的子 shell 中执行。语法如下:
command1 | command2 | command3 …
以下是显示管道的示例:
$ ls /usr/lib64/*.so | grep libc | wc -l
13
在这里,我们首先从/usr/lib64
目录中获取具有.so
扩展名的文件列表。获得的输出被传递给下一个grep
命令,以查找libc
字符串。输出进一步传递给wc
命令以计算行数。
正则表达式
正则表达式(也称为 regex 或 regexp)提供了一种指定要在给定的大块文本数据中匹配的模式的方法。它支持一组字符来指定模式。它被广泛用于文本搜索和字符串操作。许多 shell 命令提供了指定正则表达式的选项,如grep
,sed
,find
等。
正则表达式概念也用于其他编程语言,如 C++,Python,Java,Perl 等。不同语言中都有库来支持正则表达式的特性。
正则表达式元字符
正则表达式中使用的元字符在下表中解释:
元字符 | 描述 |
---|---|
*(星号) | 这匹配前一个字符的零个或多个出现 |
+(加号) | 这匹配前一个字符的一个或多个出现 |
? | 这匹配前一个元素的零次或一次出现 |
. (Dot) | 这匹配任何一个字符 |
^ | 这匹配行的开头 |
$ | 这匹配行尾 |
[... ] | 这匹配方括号内的任何一个字符 |
[^... ] | 这匹配不在方括号内的任何一个字符 |
| (Bar) | 这匹配|的左侧或右侧元素 |
这匹配前一个元素的确切 X 次出现 | |
这匹配前一个元素的 X 次或更多出现 | |
这匹配前一个元素的 X 到 Y 次出现 | |
(...) | 这将所有元素分组 |
< | 这匹配单词的开头的空字符串 |
> | 这匹配单词的末尾的空字符串 |
\ | 这禁用下一个字符的特殊含义 |
字符范围和类
当我们查看人类可读的文件或数据时,其主要内容包含字母(a 到 z)和数字(0 到 9)。在编写用于匹配由字母或数字组成的模式的正则表达式时,我们可以使用字符范围或类。
字符范围
我们也可以在正则表达式中使用字符范围。我们可以通过一个连字符分隔的一对字符来指定范围。匹配介于该范围内的任何字符,包括在内。字符范围被包含在方括号内。
以下表格显示了一些字符范围:
字符范围 | 描述 |
---|---|
[a-z] |
这匹配 a 到 z 的任何单个小写字母 |
[A-Z] |
这匹配从 A 到 Z 的任何单个大写字母 |
[0-9] |
这匹配 0 到 9 的任何单个数字 |
[a-zA-Z0-9] |
这匹配任何单个字母或数字字符 |
[h-k] |
这匹配从 h 到 k 的任何单个字母 |
[2-46-8j-lB-M] |
这匹配从 2 到 4 或 6 到 8 的任何单个数字,或从 j 到 l 或从 B 到 M 的任何字母 |
字符类:指定一系列字符匹配的另一种方法是使用字符类。它在方括号[:class:]内指定。可能的类值在下表中提到:
字符类 | 描述 |
---|---|
[:alnum:] |
这匹配任何单个字母或数字字符;例如,[a-zA-Z0-9] |
[:alpha:] |
这匹配任何单个字母字符;例如,[a-zA-Z] |
[:digit:] |
这匹配任何单个数字;例如,[0-9] |
[:lower:] |
这匹配任何单个小写字母;例如,[a-z] |
[:upper:] |
这匹配任何单个大写字母;例如,[A-Z] |
[:blank:] |
这匹配空格或制表符 |
[:graph:] |
这匹配 ASCII 范围内的字符—例如 33-126—不包括空格字符 |
[:print:] |
这匹配 ASCII 范围内的字符—例如 32-126—包括空格字符 |
[:punct:] |
这匹配任何标点符号,如'?'、'!'、'.'、','等 |
[:xdigit:] |
这匹配任何十六进制字符;例如,[a-fA-F0-9] |
[:cntrl:] |
这匹配任何控制字符 |
创建您自己的正则表达式:在正则表达式的前几节中,我们讨论了元字符、字符范围、字符类及其用法。使用这些概念,我们可以创建强大的正则表达式,用于根据我们的需要过滤文本数据。现在,我们将使用我们学到的概念创建一些正则表达式。
匹配 mm-dd-yyyy 格式的日期
我们将考虑从 UNIX 纪元开始的有效日期—即 1970 年 1 月 1 日。在这个例子中,我们将考虑从 UNIX 纪元到 2099 年 12 月 30 日之间的所有日期都是有效日期。形成其正则表达式的解释在以下小节中给出:
匹配有效的月份
-
0[1-9] 匹配 01 到 09 月
-
1[0-2] 匹配第 10、11 和 12 个月
-
'|' 匹配左侧或右侧表达式
将所有内容放在一起,匹配日期的有效月份的正则表达式将是0[1-9]|1[0-2]。
匹配有效的日期
-
0[1-9] 匹配 01 到 09 日
-
[12][0-9] 匹配 10 到 29 日
-
3[0-1] 匹配 30 到 31 日
-
'|' 匹配左侧或右侧表达式
-
0[1-9]|[12][0-9]|3[0-1] 匹配日期中的所有有效日期
匹配日期中的有效年份
-
19[7-9][[0-9] 匹配从 1970 年到 1999 年的年份
-
20[0-9]{2} 匹配从 2000 年到 2099 年的年份
-
'|' 匹配左侧或右侧表达式
-
19[7-9][0-9]|20[0-9]{2} 匹配 1970 年到 2099 年之间的所有有效年份
将有效的月份、日期和年份正则表达式组合成有效日期
我们的日期将以 mm-dd-yyyy 格式。通过将前面部分形成的月份、日期和年份的正则表达式放在一起,我们将得到有效日期的正则表达式:
(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[0-1])-(19[7-9][0-9]|20[0-9]{2)
有一个很好的网站,regexr.com/
,您也可以验证正则表达式。以下屏幕截图显示了在给定输入中匹配有效日期:
有效 shell 变量的正则表达式
在第一章中,脚本之旅的开始,我们学习了 shell 中变量的命名规则。有效的变量名可以包含来自字母数字和下划线的字符,并且变量的第一个字母不能是数字。
牢记这些规则,可以编写有效的 shell 变量正则表达式如下:
[1][_a-zA-Z0-9]*$
这里,^(插入符号)匹配行的开头。
正则表达式[_a-zA-Z]匹配 _ 或任何大写或小写字母[_a-zA-Z0-9]*匹配 _、任何数字或大写和小写字母的零次或多次出现$(美元符号)匹配行的结尾。
在字符类格式中,我们可以将正则表达式写成[2][_[:alnum:]]*$。
以下屏幕截图显示了使用正则表达式形成的有效 shell 变量:
注意
-
将正则表达式放在单引号(')中,以避免预先 shell 扩展。
-
在字符前使用反斜杠(\)来转义元字符的特殊含义。
-
元字符,如?、+、{、|、(和)被认为是扩展的正则表达式。当在基本正则表达式中使用时,它们失去了特殊含义。为了避免这种情况,使用反斜杠'?'、'+'、'{'、'|'、'('和')'。
使用 grep 过滤输出
shell 中一个强大且广泛使用的命令是grep
。它在输入文件中搜索并匹配包含给定模式的行。默认情况下,所有匹配的模式都打印在通常是终端的stdout
上。我们还可以将匹配的输出重定向到其他流,例如文件。grep
也可以从左侧执行的命令的重定向输出中获取输入,而不是从文件中获取输入。
语法
使用grep
命令的语法如下:
grep [OPTIONS] PATTERN [FILE...]
这里,FILE
可以是搜索的多个文件。如果没有给定文件作为搜索的输入,它将搜索标准输入。
PATTERN
可以是任何有效的正则表达式。根据需要将PATTERN
放在单引号(')或双引号(")中。例如,使用单引号(')避免任何 bash 扩展,使用双引号(")进行扩展。
grep
中有很多OPTIONS
。以下表格讨论了一些重要和广泛使用的选项:
选项 | 用法 |
---|---|
-i | 这强制在模式和输入文件中进行不区分大小写的匹配 |
-v |
显示不匹配的行 |
-o |
仅显示匹配行中的匹配部分 |
-f FILE |
从文件中获取一个模式,每行一个 |
-e PATTERN |
指定多个搜索模式 |
-E |
将模式视为扩展的正则表达式(egrp) |
-r |
这会递归读取目录中的所有文件,不包括解析符号链接,除非显式指定为输入文件 |
-R |
这会递归读取目录中的所有文件,并解析任何符号链接 |
-a |
这会将二进制文件处理为文本文件 |
-n |
这会在每个匹配行前加上行号 |
-q |
不要在 stdout 上打印任何内容 |
-s |
不要打印错误消息 |
-c |
这会打印每个输入文件的匹配行数 |
-A NUM |
这会打印实际字符串匹配后的 NUM 行。(与-o 选项无效) |
-B NUM |
这会打印实际字符串匹配之前的 NUM 行。(与-o 选项无效) |
-C NUM |
这会打印实际字符串匹配前后的 NUM 行。(与-o 选项无效) |
在文件中查找模式
很多时候,我们必须在文件中搜索给定的字符串或模式。grep
命令为我们提供了在一行中执行此操作的能力。让我们看下面的例子:
我们的示例的输入文件将是input1.txt
:
$ cat input1.txt # Input file for our example
This file is a text file to show demonstration
of grep command. grep is a very important and
powerful command in shell.
This file has been used in chapter 2
我们将尝试使用grep
命令从input1.txt
文件中获取以下信息:
-
行数
-
以大写字母开头的行
-
以句点(.)结尾的行
-
句子的数量
-
搜索子字符串
sent lines
,不包含periodNumber
次使用字符串file
的行
以下 shell 脚本演示了如何执行上述任务:
#!/bin/bash
#Filename: pattern_search.sh
#Description: Searching for a pattern using input1.txt file
echo "Number of lines = `grep -c '.*' input1.txt`"
echo "Line starting with capital letter:"
grep -c ^[A-Z].* input1.txt
echo
echo "Line ending with full stop (.):"
grep '.*\.$' input1.txt
echo
echo -n "Number of sentence = "
grep -c '\.' input1.txt
echo "Strings matching sub-string sent:"
grep -o "sent" input1.txt
echo
echo "Lines not having full stop are:"
grep -v '\.' input1.txt
echo
echo -n "Number of times string file used: = "
grep -o "file" input1.txt | wc -w
运行pattern_search.sh
shell 脚本后的输出如下:
Number of lines = 4
Line starting with capital letter:
2
Line ending with full stop (.):
powerful command in shell.
Number of sentence = 2
Strings matching sub-string sent:
Lines not having full stop are:
This file is a text file to show demonstration
This file has been used in chapter 2
Number of times string file used: = 3
在多个文件中查找模式
grep
命令还允许我们在多个文件中搜索模式作为输入。为了详细解释这一点,我们将直接看以下示例:
在我们的例子中,输入文件将是input1.txt
和input2.txt
。
我们将重用先前示例中input1.txt
文件的内容:
input2.txt
的内容如下:
$ cat input2.txt
Another file for demonstrating grep CommaNd usage.
It allows us to do CASE Insensitive string test
as well.
We can also do recursive SEARCH in a directory
using -R and -r Options.
grep allows to give a regular expression to
search for a PATTERN.
Some special characters like . * ( ) { } $ ^ ?
are used to form regexp.
Range of digit can be given to regexp e.g. [3-6],
[7-9], [0-9]
我们将尝试使用grep
命令从input1.txt
和input2.txt
文件中获取以下信息:
-
搜索字符串
command
-
不区分大小写地搜索字符串
command
-
打印字符串
grep
匹配的行号 -
搜索标点符号
-
打印一个匹配行后面的一行,同时搜索字符串
important
以下 shell 脚本演示了如何执行前面的步骤:
#!/bin/bash
# Filename: multiple_file_search.sh
# Description: Demonstrating search in multiple input files
echo "This program searches in files input1.txt and input2.txt"
echo "Search result for string \"command\":"
grep "command" input1.txt input2.txt
echo
echo "Case insensitive search of string \"command\":"
# input{1,2}.txt will be expanded by bash to input1.txt input2.txt
grep -i "command" input{1,2}.txt
echo
echo "Search for string \"grep\" and print matching line too:"
grep -n "grep" input{1,2}.txt
echo
echo "Punctuation marks in files:"
grep -n [[:punct:]] input{1,2}.txt
echo
echo "Next line content whose previous line has string \"important\":"
grep -A 1 'important' input1.txt input2.txt
运行 shell 脚本pattern_search.sh
后的输出如下截图。匹配的模式字符串已被突出显示:
一些更多的grep
用法
以下小节将涵盖grep
命令的一些更多用法。
在二进制文件中搜索
到目前为止,我们已经看到所有grep
示例在文本文件上运行。我们也可以使用grep
在二进制文件中搜索模式。为此,我们必须告诉grep
命令将二进制文件也视为文本文件。选项-a
或-text
告诉grep
将二进制文件视为文本文件。
我们知道grep
命令本身是一个二进制文件,执行并给出搜索结果。
grep
中的一个选项是--text
。字符串--text
应该在grep
二进制文件中的某个地方可用。让我们按照以下方式搜索它:
$ grep --text '\-\-text' /usr/bin/grep
-a, --text equivalent to –binary-files=text
我们看到字符串--text
在搜索路径/usr/bin/grep
中找到了。反斜杠('\
')字符用于转义其特殊含义。
现在,让我们在wc
二进制文件中搜索-w
字符串。我们知道wc
命令有一个-w
选项,用于计算输入文本中的单词数。
$ grep -a '\-w' /usr/bin/wc
-w, --words print the word counts
在目录中搜索
我们还可以告诉grep
使用选项-R
递归地搜索目录中的所有文件/目录,而无需指定每个文件作为grep
的输入文本文件。
例如,我们有兴趣知道标准include
目录中#include <stdio.h>
被使用了多少次:
$ grep -R '\#include <stdio\.h>' /usr/include/ | wc -l
77
这意味着#include <stdio.h>
字符串在/usr/include
目录中有77
个匹配位置。
在另一个例子中,我们想知道/usr/lib64/python2.7/
目录中有多少个 Python 文件(扩展名为.py
)包含"import os"
。我们可以这样检查:
$ grep -R "import os" /usr/lib64/python2.7/*.py | wc -l
93
从搜索中排除文件/目录
我们还可以指定grep
命令来排除特定的目录或文件进行搜索。当我们不希望grep
查找包含一些机密信息的文件或目录时,这是很有用的。这在我们确定搜索某个目录毫无用处的情况下也很有用。因此,排除它们将减少搜索时间。
假设有一个名为s0
的源代码目录,它使用git
版本控制。现在,我们有兴趣在源文件中搜索文本或模式。在这种情况下,在.git
子目录中搜索将毫无用处。我们可以通过以下方式排除.git
进行搜索:
$ grep -R --exclude-dir=.git "search_string" s0
在这里,我们正在在s0
目录中搜索search_string
字符串,并告诉grep
不要在.git
目录中搜索。
不要排除一个目录,要排除一个文件,使用--exclude-from=FILE
选项。
显示具有匹配模式的文件名
在某些用例中,我们不关心搜索匹配的位置以及在文件中匹配了多少个位置。相反,我们只关心至少有一个搜索匹配的文件名。
例如,我想保存包含特定搜索模式的文件名,或者重定向到其他命令进行进一步处理。我们可以使用-l
选项来实现这一点:
$ grep -Rl "import os" /usr/lib64/python2.7/*.py > search_result.txt
$ wc -l search_result.txt
79
这个例子获取了写有import os
的文件的文件名,并将结果保存在文件search_result.txt
中。
匹配精确单词
也可以使用单词边界\b
来实现单词的精确匹配。
在这里,我们将重用input1.txt
文件及其内容:
$ grep -i --color "\ba\b" input1.txt
--color
选项允许匹配搜索结果进行彩色打印。
"\ba\b"
选项告诉grep
只查找独立的字符a。在搜索结果中,它不会匹配作为子字符串出现的字符a
。
以下截图显示了输出:
使用 sed 编辑输出
sed
命令是一个非交互式流编辑器,允许您修改标准输入或文件的内容。它在管道中对每一行执行一个操作。语法将是:
sed [OPTIONS]... {script} [input-file …]
默认情况下,输出显示在stdout
上,但如果指定了,可以将其重定向到文件。
input-file
是需要运行sed
的文件。如果没有指定文件,它将从stdin
读取。
script
可以是一个命令,也可以是一个包含多个命令的文件,要传递给sed
,sed
的OPTIONS
在下表中描述:
选项 | 描述 |
---|---|
-n | 这会抑制模式空间的自动打印 |
-e script | 这允许执行多个脚本 |
-r | 这在脚本中使用扩展的正则表达式 |
-l N | 这指定换行长度 |
--posix | 这将禁用所有 GNU 扩展 |
-u | 这从输入中加载最小量的数据并频繁刷新输出缓冲区 |
使用s
进行字符串替换
sed
命令广泛用于文本文件中的字符串替换。程序员经常在重命名大型源代码中的变量时使用此功能。它通过避免手动重命名节省了许多程序员的时间。
替换命令s
具有以下字段:
s/regex/replacement/
在这里,s
表示执行替换,/
充当分隔符,regex
是需要替换的正则表达式。这里也可以指定一个简单的字符串。最后一个字段replacement
是匹配结果应该被替换成什么。
默认情况下,sed
只会替换行中匹配模式的第一次出现。要替换所有出现,可以在/—
的末尾使用g
标志,即s/regex/replacement/g
。
以下表格中提到了一些可以使用的标志:
标志 | 描述 |
---|---|
g |
这将在一行中应用替换到所有匹配项 |
p |
如果发生替换,这将打印一个新的模式空间 |
w filename |
这将替换的模式空间写入文件名 |
N |
这只替换匹配行中的第 N 个匹配结果 |
我们有一个名为sed.sh
的文件作为示例。该文件的内容如下:
$ cat sed.sh
#!/bin/bash
var1="sed "
var1+="command "
var1+="usage"
echo $var1
这是一个 shell 脚本,其中变量var1
已经在四个地方使用了。现在,我们想要将变量var1
重命名为variable
。我们可以使用sed
命令很容易地做到这一点:
$ sed -i 's/var1/variable/g' sed.sh
$ cat sed.sh
#!/bin/bash
variable="sed "
variable+="command "
variable+="usage"
echo $variable
这里,-i
选项用于替换输入文件。
多个替换
我们还可以使用-e
后跟一个命令来指定要执行的多个替换命令。
例如,考虑sed.txt
文件。该文件的内容如下:
$ cat sed.txt
The sed command is widely used for string
substitution in text file. Programmers frequently
use this feature while renaming a variable in huge source code.
It saves lot of programmers time by avoiding manual renaming.
现在,我们想要将'.
'替换为',
'并删除包含字符串manual
的行:
$ sed -e 's/\./,/g' -e '/manual/d' sed.txt
The sed command is widely used for string
substitution in text file, Programmers frequently
use this feature while renaming a variable in huge source code,
在sed.txt
文件中,s/\./,/g
命令首先将'.
'替换为',
',/manual/d
删除了包含字符串manual
的行。
使用 tee 复制流
在某些情况下,有必要在stdout
上打印输出并将输出保存在文件中。一般来说,命令输出可以打印,也可以保存在文件中。为了解决这个问题,使用tee
命令。该命令从标准输入读取,并同时写入标准输出和文件。tee
的语法如下:
tee [OPTION] [FILE …]
tee
命令将输出复制到每个FILE
,并且也复制到stdout
。OPTIONS
可以如下:
选项 | 描述 |
---|---|
-a, --append |
这将附加到FILE 而不是覆盖 |
-i, --ignore-interrupts |
如果有的话,这将忽略中断信号 |
将输出写入stdout
和文件:一般来说,要将输出写入stdout
和文件,我们将调用相同的命令两次,一次进行重定向,一次不进行重定向。例如,以下命令显示了如何在stdout
上打印输出并将其保存到文件中:
$ ls /usr/bin/*.pl # Prints output on stdout
/usr/bin/rsyslog-recover-qi.pl /usr/bin/syncqt.pl
$ ls /usr/bin/*.pl> out.txt # Saves output in file out.txt
我们可以通过使用tee
命令一次运行ls
命令来完成这两个任务,如下所示:
$ ls /usr/bin/*.pl| tee out.txt # Output gets printed to stdout and saved in out.txt
/usr/bin/rsyslog-recover-qi.pl
/usr/bin/syncqt.pl
$ cat out.txt #Checking content of out.txt
/usr/bin/rsyslog-recover-qi.pl
/usr/bin/syncqt.pl
我们还可以为tee
指定多个文件名,以便将输出写入每个文件。这将复制输出到所有文件:
$ ls /usr/bin/*.pl| tee out1.txt out2.txt
/usr/bin/rsyslog-recover-qi.pl
/usr/bin/syncqt.pl
通过运行上述命令,输出也将写入out1.txt
和out2.txt
文件:
$ cat out1.txt
/usr/bin/rsyslog-recover-qi.pl
/usr/bin/syncqt.pl
$ cat out2.txt
/usr/bin/rsyslog-recover-qi.pl
/usr/bin/syncqt.pl
将输出写入 stdout 并附加到文件
tee
命令还允许您将输出附加到文件而不是覆盖文件。这可以使用tee
的-a
选项来实现。将输出附加到文件在我们想要将各种命令的输出或不同命令执行的错误日志写入单个文件时非常有用。
例如,如果我们想要将运行ls
和echo
命令的输出保留在out3.txt
文件中,并且还在stdout
上显示结果,我们可以这样做:
$ echo "List of perl file in /usr/bin/ directory" | tee out3.txt
List of perl file in /usr/bin/ directory
$ ls /usr/bin/*.pl| tee -a out3.txt
/usr/bin/rsyslog-recover-qi.pl
/usr/bin/syncqt.pl
$ cat out3.txt # Content of file
List of perl file in /usr/bin/ directory
/usr/bin/rsyslog-recover-qi.pl
/usr/bin/syncqt.pl
将输出发送到多个命令
我们还可以使用tee
命令将命令的输出作为多个命令的输入。这是通过将tee
输出发送到管道来完成的。
$ df -h | tee out4.txt | grep tmpfs | wc -l
7
在这里,df -h
命令的输出保存到out4.txt
文件中,stdout
输出被重定向到grep
命令,并且来自grep
的搜索结果的输出进一步被重定向到wc
命令。最后,wc
的结果被打印到stdout
上。
排序和查找唯一文本
Shell 提供了不同的方法来使用sort
命令对输入文本进行排序。还可以使用uniq
命令从排序/未排序的输入文本中删除重复的行。可以从文件中给出要排序和uniq
命令的输入文本,或者从另一个命令重定向。
对输入文本进行排序
输入文本中的行按以下顺序排序:
-
从 0 到 9 的数字
-
从 A 到 Z 的大写字母
-
从 a 到 z 的小写字母
语法如下:
sort [OPTION] [FILE …]
可以提供单个或多个输入文件进行排序。
sort
命令采用多个选项以提供排序的灵活性。在以下表中讨论了排序的流行和重要的OPTION
:
选项 | 描述 |
---|---|
-b |
这忽略前导空格 |
-d |
这仅考虑空格和字母数字字符 |
-f |
这忽略了大小写 |
-i |
这忽略了不可打印的字符 |
-M |
这比较未知的月份(例如,< JAN < FEB… < DEC) |
-n |
这根据数值进行排序 |
-r |
这以相反的顺序排序 |
-h |
这根据可读性强的数字进行排序;例如,9K,5M,1G 等。 |
-u |
这获取唯一行 |
-o file |
这将输出写入文件而不是 stdout |
-m |
这合并已排序的文件而不重新排序 |
-k n |
这根据给定的列 n 对数据进行排序 |
现在,我们将通过示例看看如何对输入文本数据进行不同的排序。
对单个文件进行排序
在我们的示例中,我们将考虑sort1.txt
文件进行排序。该文件的内容如下:
$ cat sort1.txt
Japan
Singapore
Germany
Italy
France
Sri Lanka
要按字母顺序对内容进行排序,可以使用没有任何选项的sort
命令:
$ sort sort1.txt
France
Germany
Italy
Japan
Singapore
Sri Lanka
要以相反的顺序对内容进行排序,我们可以使用-r
选项:
$ sort -r sort1.txt
Sri Lanka
Singapore
Japan
Italy
Germany
France
排序多个文件:我们还可以集体对多个文件进行排序,并且排序后的输出可以用于进一步的查询。
例如,考虑sort1.txt
和sort2.txt
文件。我们将重用先前示例中的sort1.txt
文件的内容。sort2.txt
的内容如下:
$ cat sort2.txt
India
USA
Canada
China
Australia
我们可以按字母顺序对两个文件一起进行排序,如下所示:
$ sort sort1.txt sort2.txt
Australia
Canada
China
France
Germany
India
Italy
Japan
Singapore
Sri Lanka
USA
我们还可以使用-o
选项将文件的排序输出保存到文件中,而不是在stdout
上显示它:
$ sort sort1.txt sort2.txt -o sorted.txt
$ cat sorted.txt
Australia
Canada
China
France
Germany
India
Italy
Japan
Singapore
Sri Lanka
USA
将输出重定向到 sort
我们可以对从另一个命令重定向的输出进行排序。以下示例显示了对df -h
命令输出进行排序:
$ df -h # Disk space usage in human readable format
以下命令按其第二列内容对df
的输出进行排序:
$ df -h | sort -hb -k2 #. Sorts by 2nd column according to size available:
我们可以根据最后修改的日期和月份对ls -l
的输出进行排序:
$ ls -l /var/cache/ # Long listing content of /var/cache
要对ls -l
的输出进行排序,首先按照第 6 个字段的月份使用-M
选项进行排序,如果两个或更多行的月份相同,则按照第 7 个字段的日期使用-n
进行排序:
$ ls -l /var/cache/ | sort -bk 6M -nk7
过滤唯一元素
在许多用例中,我们需要删除重复的项目并仅保留项目的一次出现。当命令或输入文件的输出太大并且包含大量重复行时,这非常有用。要从文件或重定向的输出中获取唯一行,使用 shell 命令uniq
。一个重要的要点是,为了获得uniq
输出,输入应该是排序的,或者首先运行 sort 命令使其排序。语法如下:
sort [OPTION] [INPUT [OUTPUT]]
uniq
的输入可以来自文件或另一个命令的输出。
如果提供了输入文件,则还可以在命令行上指定可选的输出文件。如果未指定输出文件,则输出将打印在stdout
上。
在以下表中讨论了uniq
支持的选项:
选项 | 描述 |
---|---|
-c |
这在行前加上出现次数 |
-d |
这仅打印重复行一次 |
-f N |
这跳过了前 N 个字段的比较 |
-i |
这是项目的不区分大小写比较 |
-u |
仅打印唯一行 |
-s N |
这避免比较行中的前 N 个字符 |
-w N |
仅比较行中的 N 个字符 |
文件中的唯一元素
以unique.txt
文件为例,我们将使用uniq
命令及其选项运行。unique.txt
的内容如下:
$ cat unique.txt
Welcome to Linux shell scripting
1
Welcome to LINUX shell sCripting
2
Welcome To Linux Shell Scripting
4
2
4
Welcome to Linux shell scripting
2
3
Welcome to Linux shell scripting
2
Welcome to Linux shell scripting
Welcome to LINUX shell sCripting
要从unique.txt
文件中删除重复行,我们可以执行以下操作:
- 首先对文件进行排序,然后将排序后的文本重定向到
uniq
命令:
$ sort unique.txt | uniq
- 使用
-u
选项与sort
命令:
$ sort -u unique.txt
运行任何一个命令的输出将是相同的,如下所示:
我们可以使用-c
选项来打印输入文件中每行的出现次数:
$ sort unique.txt | uniq -c
使用-c
和-i
选项将打印uniq
行以及出现次数。将进行不区分大小写的唯一行比较:
$ sort unique.txt | uniq -ci
要仅获取文件中仅出现一次的行,使用-u
选项:
$ sort unique.txt | uniq -u
1
3
Welcome To Linux Shell Scripting
类似地,要获取文件中出现多次的行,使用-d
:
$ sort unique.txt | uniq -d
2
4
Welcome to Linux shell scripting
Welcome to LINUX shell sCripting
我们还可以告诉uniq
命令根据仅比较行的前 N 个字符来查找唯一行:
$ sort unique.txt | uniq -w 10
1
2
3
4
Welcome to Linux shell scripting
Welcome To Linux Shell Scripting
注意
-
uniq
命令不会检测重复的行,除非它们是相邻的。 -
要查找唯一行,首先使用
sort
命令对输入进行排序,然后应用uniq
命令
使用tr
进行基于字符的翻译
另一个有趣的 shell 命令是tr
。它可以从标准输入中翻译、挤压或删除字符。语法如下:
tr [OPTION]... SET1 [SET2]
tr
命令的选项在下表中解释:
选项 | 描述 |
---|---|
-c, -C |
使用 SET1 的补集 |
-d |
这将删除 SET1 中指定的字符范围。 |
-s |
这将用 SET1 中字符的单个出现替换连续多次出现的字符。 |
-t |
这将 SET1 截断为 SET2 的长度。SET1 中的任何额外字符都不会被考虑进行翻译。 |
SET 是一串可以使用以下方式指定的字符:
-
字符类:
[:alnum:]
、[:digit:]
、[:alpha:]
等等 -
字符范围:
'a-z'
、'A-Z'
和'0-9'
-
转义字符:
\\
、\b
、\r
、\n
、\f
、\v
和\t
要从文件提供输入文本并将输出重定向到文件,我们可以使用文件重定向运算符:<
(输入的小于)和>
(输出的大于)。
删除输入字符
有时,从输入文本中删除一些不必要的字符是很重要的。例如,我们的输入文本在tr.txt
文件中:
$ cat tr.txt
This is a text file for demonstrating
tr command.
This input file contains digit 2 3 4 and 5
as well.
THIS IS CAPS LINE
this a lowercase line
假设我们想要从这个文件中删除所有大写字母。我们可以使用SET1
为'A-Z'
的-d
选项:
$ tr -d 'A-Z' < tr.txt
This is a text file for demonstrating
tr command.
This input file contains digit 2 3 4 and 5
as well.
this a lowercase line
我们看到输出没有任何大写字母。我们还可以从文件中删除换行符和空格如下:
$ tr -d ' \n' < tr.txt > tr_out1.txt
在这里,我们已将输出重定向到tr_out1.txt
:
$ cat tr_out1.txt
Thisisatextfilefordemonstratingtrcommand.Thisinputfileconatainsdigit234and5aswell.THISISCAPSLINEthisalowercaseline
挤压到单个出现
当我们不想在输入文本中删除字符时,而是想要将给定字符的连续多次出现挤压到单个出现时,-s
选项就很有用。
其中一个用例是当我们在两个单词之间有多个空格时,我们希望将其减少到输入文本中任意两个单词/字符串之间的单个空格。以tr1.txt
文件为例:
$ cat tr1.txt
India China Canada
USA Japan Russia
Germany France Italy
Australia Nepal
通过查看这个文件,很明显文本没有对齐。两个单词之间有多个空格。我们可以使用tr
选项和-s
将多个空格挤压为一个空格:
$ tr -s ' ' < tr1.txt
India China Canada
USA Japan Russia
Germany France Italy
Australia Nepal
反转要翻译的字符集
tr
命令还提供了-c
或-C
选项来反转要翻译的字符集。当我们知道不需要翻译什么时,这是很有用的。
例如,我们只想在文本字符串中保留字母数字、换行符和空格。输入文本中的所有内容都应该被删除。在这里,指定不删除而不是删除会更容易。
例如,考虑tr2.txt
文件,其内容如下:
$ cat tr2.txt
This is an input file.
It conatins special character like ?, ! etc
&^var is an invalid shll variable.
_var1_ is a valid shell variable
除了字母数字、换行和空格之外的字符,我们可以运行以下命令来删除:
tr -cd '[:alnum:] \n' < tr2.txt
This is an input file
It conatins special character like etc
var is an invalid shll variable
var1 is a valid shell variable
基于行的过滤-头和尾
要显示文件的内容,我们将使用cat
命令。cat
命令将整个文件内容显示在stdout
上。但是,有时我们只对查看文件的几行感兴趣。在这种情况下,使用cat
将很麻烦,因为我们必须滚动到我们感兴趣的特定行。
Shell 为我们提供了head
和tail
命令,以仅打印我们感兴趣的行。两个命令之间的主要区别是,head
从文件开头打印行,而tail
从文件末尾打印行。
使用 head 打印行
语法如下:
head [OPTION] [FILE …]
默认情况下,head
将每个文件的前 10 行打印到stdout
。如果没有提到文件或指定了'-
',则输入来自stdin
。
头部中可用的选项可用于更改要打印的内容量。可用选项在以下表中描述:
选项 | 描述 |
---|---|
-c [-] K |
这将打印文件的前 K 个字节。如果使用了-K,则可以输出除最后 K 个字节之外的所有内容。 |
-n [-]K |
这将打印每个文件的前 K 行。如果使用了-K,则可以输出除最后 n 行之外的所有行。 |
-q |
这将阻止打印输入文件的名称。 |
-v |
这总是输出每个文件的文件名标题。 |
打印前几行
让我们看看/usr/lib64/
目录包含多少个文件-
:
$ ls /usr/lib64 | wc
3954
我们看到/usr/lib64
有 3954 个文件。假设我们不想要所有的库名称,而只想要前五个库名称。我们可以使用以下命令进行头部操作:
$ ls /usr/lib64 | head -n 5
akonadi
alsa-lib
ao
apper
apr-util-1
打印前几个字节
我们使用-c
选项来打印文件的前几个字节,如下所示:
$ head -c50 /usr/share/dict/linux.words /usr/share/dict/words
==> /usr/share/dict/linux.words <==
1080
10-point
10th
11-point
12-point
16-point
18-p
==> /usr/share/dict/words <==
1080
10-point
10th
11-point
12-point
16-point
18-p
这首先打印/usr/share/dict/linux.words
和/usr/share/dict/words
文件的前 50 个字节。
我们可以使用–q
来消除具有文件名的标题的打印:
$ head -c50 -q /usr/share/dict/linux.words /usr/share/dict/words
1080
10-point
10th
11-point
12-point
16-point
18-p1080
10-point
10th
11-point
12-point
16-point
18-p
对于单个文件,head
命令不会在输出中打印文件名。要查看它,请使用–v
选项:
$ head -c50 -v /usr/share/dict/linux.words
==> /usr/share/dict/linux.words <==
1080
10-point
10th
11-point
12-point
16-point
18-p
使用 tail 打印行
tail
的语法如下:
tail [OPTION] [FILE …]
默认情况下,tail
将每个FILE
的最后 10 行打印到stdout
。如果没有提到文件或指定了'-
',则输入来自stdin
。
tail
中的可用选项可用于更改要打印的内容量。可用选项在以下表中描述:
选项 | 描述 |
---|---|
-c [+]K |
这将打印每个文件的最后K 字节。如果使用了+K ,则从每个文件的第K 字节开始打印。 |
-n [+]K |
这将打印每个文件的最后K 行。如果使用+K,则从每个文件的第K 行开始输出。 |
-f [{name|descriptor}] |
输出随着文件增长而追加的数据。 |
--retry |
如果文件无法访问,将继续尝试打开文件。 |
--max-unchanged-stats=N |
使用-f 名称,重新打开未打开的文件。这显示N 次迭代后的更改大小(默认为 5)。 |
--pid=PID |
使用-f ,如果PID 死亡,则终止。 |
-q |
不要输出每个文件的文件名的标题。 |
-F |
这与-f 名称--retry 选项相同。 |
-s N |
在迭代之间休眠N 秒。使用–pid =PID ,每隔N 秒至少检查一次进程。 |
-v |
这总是输出每个文件的文件名标题。 |
检查日志条目
tail
命令经常用于检查最近几次命令的错误或消息日志。每次新运行时,日志都会追加到行的末尾。
我们将在以下示例中看到,当添加新的 USB 驱动器和移除它时,内核日志条目被创建:
$ dmesg | tail -n7 # Log when USB was attached
[120060.536856] sd 10:0:0:0: Attached scsi generic sg1 type 0
[120060.540848] sd 10:0:0:0: [sdb] 1976320 512-byte logical blocks: (1.01 GB/965 MiB)
[120060.541989] sd 10:0:0:0: [sdb] Write Protect is off
[120060.541991] sd 10:0:0:0: [sdb] Mode Sense: 23 00 00 00
[120060.543125] sd 10:0:0:0: [sdb] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
[120060.550464] sdb: sdb1
[120060.555682] sd 10:0:0:0: [sdb] Attached SCSI removable disk
$ dmesg | tail -n7 # USB unmounted
[120060.540848] sd 10:0:0:0: [sdb] 1976320 512-byte logical blocks: (1.01 GB/965 MiB)
[120060.541989] sd 10:0:0:0: [sdb] Write Protect is off
[120060.541991] sd 10:0:0:0: [sdb] Mode Sense: 23 00 00 00
[120060.543125] sd 10:0:0:0: [sdb] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
[120060.550464] sdb: sdb1
[120060.555682] sd 10:0:0:0: [sdb] Attached SCSI removable disk
[120110.466498] sdb: detected capacity change from 1011875840 to 0
我们看到当 USB 卸载时,会添加一个新的日志条目:[120110.466498] sdb:
检测到容量从1011875840
变为0
。要在基于 RPM 的系统中检查最后 10 个 yum 日志,我们可以这样做:
# sudo tail -n4 -v /var/log/yum.log
==> /var/log/yum.log-20150320 <==
Mar 19 15:40:19 Updated: libgpg-error-1.17-2.fc21.i686
Mar 19 15:40:19 Updated: libgcrypt-1.6.3-1.fc21.i686
Mar 19 15:40:20 Updated: systemd-libs-216-21.fc21.i686
Mar 19 15:40:21 Updated: krb5-libs-1.12.2-14.fc21.i686
要查看实时日志,我们可以使用-f
选项。例如,/var/log/messages
文件显示一般系统活动。使用tail -f
,/var/log/messages
中追加的日志消息也将打印在stdout
上:
$ tail -f /var/log/messages
Jun 7 18:21:14 localhost dbus[667]: [system] Rejected send message, 10 matched rules; type="method_return", sender=":1.23" (uid=0 pid=1423 comm="/usr/lib/udisks2/udisksd --no-debug ") interface="(unset)" member="(unset)" error name="(unset)" requested_reply="0" destination=":1.355" (uid=1000 pid=25554 comm="kdeinit4: dolphin [kdeinit] --icon system-fil ")
Jun 7 18:21:14 localhost systemd-udevd: error: /dev/sdb: No medium found
Jun 7 18:21:14 localhost systemd-udevd: error: /dev/sdb: No medium found
Jun 7 18:27:10 localhost kernel: [135288.809319] usb 3-1.2: USB disconnect, device number 14
Jun 7 18:27:10 localhost kernel: usb 3-1.2: USB disconnect, device number 14
Jun 7 18:27:10 localhost systemd-udevd: error opening USB device 'descriptors' file
命令提示符不会返回。相反,每当/var/log/messages
中有新内容时,输出将持续更新。
在文件中查找任何行
我们可以使用 head 和 tail 来查找文件的任何行。
我们将考虑/usr/share/dict/words
文件作为示例。
现在,要找到这个文件的第 10 行,我们可以这样做:
$ head -10 /usr/share/dict/words | tail -n1 # 10th line
20-point
$ head -200000 /usr/share/dict/words | tail -n1 # 200000th line
intracartilaginous
基于 Cut 的选择
我们还可以使用cut
命令从单个/多个文件的每一行中选择文本。cut
命令允许我们基于分隔符选择列。默认情况下,使用 TAB 作为分隔符。我们还可以通过指定字符或范围来选择行中的一部分文本。语法如下:
cut OPTION [FILE …]
cut
命令适用于单个和多个文件。默认情况下,输出打印在stdout
上。
cut
命令的选项在下表中解释:
选项 | 描述 |
---|---|
-b LIST |
这会选择列表中指定的字节。 |
-c LIST |
这会选择列表中指定的字符。 |
-d DELIM |
这使用 DELIM 作为分隔符,而不是 TAB。它还打印没有分隔符的行。 |
-f LIST |
这只选择列表中指定的字段。 |
--complement |
这是对所选字节、字符或字段集的补集。 |
-s |
不打印没有分隔符的行。 |
--output-delimiter=STRING |
这使用 STRING 作为输出分隔符。默认情况下,使用输入分隔符。 |
LIST 由一个范围或多个由逗号分隔的范围组成。范围的指定方式如下:
范围 | 含义 |
---|---|
N |
这是第 N 个字节、字符或字段,从 1 开始计数 |
N- |
这是从第 N 个字节、字符或字段到行尾 |
N-M |
这是从第 N 到第 M 个字节(包括 M 和 N)、字符或字段。 |
-M |
这是从第一个到第 M 个(包括)字节、字符或字段。 |
跨列切割
许多 Linux 命令的输出格式都是这样的,结果有多个字段,每个字段由空格或制表符分隔。可以通过查看特定字段列来查看每个字段的输出。
执行ls -l ~
命令并观察以下输出:
$ ls -l ~
现在,我们只对修改时间和文件名感兴趣。为了实现这一点,我们将需要列6
到9
:
$ ls -l ~ | tr -s ' ' |cut -f 6-9 -d ' '
默认情况下,使用 TAB 作为分隔符。在ls -l
输出中,任何两列之间有多个空格。因此,首先使用tr -s
,我们将多个空格压缩为单个空格,然后我们将使用空格作为分隔符切割列字段范围6-9
。
文件中的文本选择
以cut1.txt
文件为例。文件的内容如下:
$ cat cut1.txt
输出将是:
现在,我们对学生的姓名感兴趣。我们可以通过获取第一列来获得这个。在这里,每一列都是由Tab分隔的。因此,在我们的命令中,我们不必指定分隔符:
$ cut -f1 cut1.txt
Name
Foo
Bar
Moo
Bleh
Worm
Lew
另一件有趣的事情是获取唯一的部门名称。我们可以通过在cut1.txt
文件上使用以下一组命令来实现这一点:
$ cut -f4 cut1.txt | tail -n +2 | sort -u
Civil
CSE
ECE
Mechanical
我们可以看到在cut1.txt
文件中提到了四个唯一的部门。
我们还可以做的另一件有趣的事情是找出谁获得了最高分,如下所示:
$ cut -f1,3 cut1.txt | tail -n +2 | sort -k2 -nr | head -n1
Worm 99
要找出谁得分最高,我们首先从cut1.txt
文件中选择第一列和第三列。然后,我们使用tail -n +2
排除第一行,这告诉我们这个文件是关于什么的,因为我们不需要这个。之后,我们对第二列进行数字排序,以逆序排列,其中包含所有学生的分数。现在,我们知道第一列包含得分最高的人的详细信息。
了解系统处理器的速度是有趣的,以便了解系统的各种细节。其中之一就是了解处理器的速度。首先要知道的是,所有处理器的详细信息都在/proc/cpuinfo
文件中。你可以打开这个文件,看看都有哪些详细信息。例如,我们知道处理器的速度在"model name"
字段中提到。
以下 shell 脚本将显示处理器的速度:
#!/bin/bash
#Filename: process_speed.sh
#Description: Demonstrating how to find processor speed ofrunning system
grep -R "model name" /proc/cpuinfo | sort -u > /tmp/tmp1.txt
tr -d ' ' </tmp/tmp1.txt > /tmp/tmp2.txt
cut -d '@' -f2 /tmp/tmp2.txt
运行这个脚本将输出你系统的处理器速度:
$ sh processor_speed.sh
2.80GHz
我们也可以不使用临时文件:
$ grep -R "model name" /proc/cpuinfo | sort -u | cut -d '@' -f2
2.80GHz
总结
阅读完本章后,你应该知道如何向命令提供输入并打印或保存其结果。你还应该熟悉将一个命令的输出和输入重定向到另一个命令。现在,你可以轻松地在文件中搜索、替换字符串或模式,并根据需要过滤数据。
从这一章中,我们现在可以很好地控制文本数据的转换/过滤。在下一章中,我们将学习如何通过学习循环、条件、开关和 shell 中最重要的函数来编写更强大和有用的 shell 脚本。我们还将了解知道命令的退出状态有多重要。在下一章中,我们还将看到本章中学到的命令的更高级的例子。
第三章:有效的脚本编写
要在 shell 中编写有效的脚本,非常重要的是要了解 shell 提供的不同实用工具。与其他编程语言类似,shell 编程也需要一种在特定条件下指定跳过或运行某些命令的方法。在 shell 中也需要循环结构来执行元素列表上的某些任务。
在本章中,我们将涵盖诸如if
、else
、case
和select
之类的主题,这些主题可根据条件运行一定的命令块。我们将看到for
、while
和until
结构,用于在脚本中循环执行一定的命令块。我们将看到在命令或脚本执行后,退出代码如何在了解命令是否成功执行方面发挥重要作用。我们还将看到如何在 shell 中定义函数,从而使我们能够从现在开始编写模块化和可重用的代码。
本章将详细介绍以下主题:
-
退出脚本和退出代码
-
使用测试测试表达式
-
使用
if
和else
的条件语句 -
索引数组和关联数组
-
使用
for
循环 -
select
、while
和until
循环 -
切换到您的选择
-
使用函数和位置参数
-
使用
xargs
将stdout
作为参数传递 -
别名
-
pushd
和popd
退出脚本和退出代码
我们现在对 shell 脚本文件、命令以及在bash
中运行它们以获得所需的输出非常熟悉。到目前为止,我们所见过的 shell 脚本示例都是按行运行直到文件末尾。在编写真实世界的 shell 脚本时,情况可能并非总是如此。例如,当发生错误时,不满足某些条件时等等,我们可能需要在脚本中间退出。要退出脚本,使用带有可选返回值的exit
shell 内置命令。返回值告诉退出代码,也称为返回状态或退出状态。
退出代码
每个命令在执行时都会返回一个退出代码。退出代码是了解命令是否成功执行或是否发生了错误的一种方式。根据POSIX(可移植操作系统接口)标准约定,成功执行的命令或程序返回0
,而失败执行返回1
或更高的值。
在 bash 中,要查看上一个命令的退出状态,可以使用“$?
”。
以下示例显示了成功执行命令的退出代码:
$ ls /home # viewing Content of directory /home
foo
现在,要查看上一个执行的命令的退出代码,即ls /home
,我们将运行以下命令:
$ echo $?
0
我们看到ls
命令执行的退出状态为0
,这意味着它已成功执行。
另一个示例显示了不成功执行命令的退出代码如下:
$ ls /root/
ls: cannot open directory /root/: Permission deniedWe see that the ls command execution was unsuccessful with the Permission denied error. To see the exit status, run the following command:
$ echo $?
2
退出状态代码为2
,高于0
,表示执行不成功。
具有特殊含义的退出代码
在不同的情况下,脚本或命令返回不同的退出代码。在调试脚本或命令时,了解退出代码的含义是有用的。以下表格解释了在命令或脚本执行的不同条件下惯例返回哪个退出代码:
退出代码 | 描述 |
---|---|
0 | 成功执行 |
1 | 一般错误 |
2 | 使用 shell 内置命令时出错 |
126 | 在执行命令时出现权限问题;我们无法调用请求的命令 |
127 | 无法调用请求的命令 |
128 | 在脚本中指定无效参数退出。只有 0 到 255 之间的值是有效的退出代码 |
128+n | 信号'n'的致命错误 |
130 | 使用 Ctl + C 终止脚本 |
255* | 超出范围的退出代码 |
保留退出代码 0、1、126-165 和 255,我们在脚本文件中返回退出代码时应使用除这些数字之外的其他数字。
以下示例显示命令返回的不同退出代码:
- 退出代码 0:以下是
echo
命令的成功执行:
$ echo "Successful Exit code check"
Successful Exit code check
$ echo $?
0
- 退出代码 1:从
/root
复制文件没有权限,如下所示:
$ cp -r /root/ .
cp: cannot access '/root/': Permission denied
$ echo $?
1
- 退出代码 2:使用无效参数读取 shell 内置如下:
$ echo ;;
bash: syntax error near unexpected token ';;'
$ echo $?
2
- 退出代码 126:将
/usr/bin
目录作为实际上不是命令的命令运行:
$ /usr/bin
bash: /usr/bin: Is a directory
$ echo $?
126
- 退出代码 127:运行一个名为
foo
的命令,实际上并不存在于系统中:
$ foo
bash: foo: command not found
$ echo $?
127
- 退出代码 128+n:通过按Ctrl + C终止脚本:
$ read
^C
$ echo $?
130
在这里,Ctrl + C发送SIGQUIT
信号,其值为2
。因此,退出代码为130
(128 + 2)。
具有退出代码的脚本
我们还可以退出 shell 内置命令,并附带退出代码,以了解脚本是否成功运行或遇到任何错误。在调试自己的脚本时,可以使用不同的错误代码来了解错误的实际原因。
当我们在脚本中不提供任何退出代码时,脚本的退出代码由最后执行的命令决定:
#!/bin/bash
# Filename: without_exit_code.sh
# Description: Exit code of script when no exit code is mentioned in script
var="Without exit code in script"
echo $var
cd /root
上述脚本没有指定任何退出代码;运行此脚本将得到以下输出:
$ sh without_exit_code.sh
Without exit code in script
without_exit_code.sh: line 8: cd: /root: Permission denied
$ echo $? # checking exit code of script
1
此脚本的退出代码为1
,因为我们没有指定任何退出代码,最后执行的命令是cd /root
,由于权限问题而失败。
接下来的示例返回退出代码0
,无论发生任何错误,即脚本成功运行:
#!/bin/bash
# Filename: with_exit_code.sh
# Description: Exit code of script when exit code is mentioned in scr# ipt
var="Without exit code in script"
echo $var
cd /root
exit 0
运行此脚本将得到以下结果:
$ sh with_exit_code.sh
Without exit code in script
with_exit_code.sh: line 8: cd: /root: Permission denied
echo $?
0
现在,脚本文件返回退出代码为0
。我们现在知道在脚本中添加退出代码会有什么不同。
另一个具有退出状态代码的示例如下:
#!/bin/bash
# Filename: exit_code.sh
# Description: Exit code of script
cmd_foo # running command not installed in system
echo $?
cd /root # Permission problem
echo $?
echo "Hello World!" # Successful echo print
echo $?
exit 200 # Returning script's exit code as 200
运行此脚本后的输出如下:
$ sh exit_status.sh
exit_code.sh: line 5: cmd_foo: command not found
127
exit_code.sh: line 8: cd: /root: Permission denied
1
Hello World!
0
$ echo $? # Exit code of script
200
如果在脚本中未指定退出代码,则退出代码将是脚本中运行的最后一个命令的退出状态。
使用测试检查测试表达式
shell 内置命令test
可用于检查文件类型和比较表达式的值。语法为test EXPRESSION
或test
命令也等同于[ EXPRESSION ]。
如果EXPRESSION
结果为0
,则返回退出代码1
(false
),对于非零的EXPRESSION
结果,返回0
(true
)。
如果未提供EXPRESSION
,则退出状态设置为1
(false)。
文件检查
可以使用test
命令对文件进行不同类型的检查;例如,文件存在性检查,目录检查,常规文件检查,符号链接检查等。
可以使用以下表格中的选项对文件进行各种检查:
选项 | 描述 |
---|---|
-e | fileChecks 文件是否存在 |
-f file | 文件是常规文件 |
-d file | 文件存在且为目录 |
-h,-L file | 文件是符号链接 |
-b file | 文件是块特殊文件 |
-c file | 文件是字符特殊文件 |
-S file | 文件是套接字 |
-p file | 文件是命名管道 |
-k file | 文件的粘着位已设置 |
-g file | 文件的设置组 ID(sgid)位已设置 |
-u file | 文件的设置用户 ID(suid)位已设置 |
-r file | 文件具有读权限 |
-w file | 文件具有写权限 |
-x file | 文件具有执行权限 |
-t fd | 文件描述符 fd 在终端上打开 |
file1 -ef file2 | file1 是 file2 的硬链接 |
file1 -nt file2 | file1 比 file2 更近 |
file1 -ot file2 | file1 的修改时间早于 file2 |
Shell 脚本对文件执行不同的检查,如下所示:
#!/bin/bash
# Filename: file_checks.sh
# Description: Performing different check on and between files
# Checking existence of /tmp/file1
echo -n "Does File /tmp/file1 exist? "
test -e /tmp/file1
echo $?
# Create /tmp/file1
touch /tmp/file1 /tmp/file2
echo -n "Does file /tmp/file1 exist now? "
test -e /tmp/file1
echo $?
# Check whether /tmp is a directory or not
echo -n "Is /tmp a directory? "
test -d /tmp
echo $?
# Checking if sticky bit set on /tmp"
echo -n "Is sticky bit set on /tmp ? "
test -k /tmp
echo $?
# Checking if /tmp has execute permission
echo -n "Does /tmp/ has execute permission ? "
test -x /tmp
echo $?
# Creating another file /tmp/file2
touch /tmp/file2
# Check modification time of /tmp/file1 and /tmp/file2
echo -n "Does /tmp/file1 modified more recently than /tmp/file2 ? "
test /tmp/file1 -nt /tmp/file2
echo $?
运行此脚本的输出如下:
Does File /tmp/file1 exist? 1
Does file /tmp/file1 exist now? 0
Is /tmp a directory? 0
Is sticky bit set on /tmp ? 0
Does /tmp/ has execute permission? 0
Does /tmp/file1 modified more recently than /tmp/file2 ? 1
在我们的输出中,0
和1
是在文件上运行测试命令后的存在
状态。输出1
表示测试失败,0
表示测试成功通过。
算术检查
我们还可以在整数之间执行算术检查。可以在整数上进行的比较在以下表中解释:
比较 | 描述 |
---|---|
INTEGER1 -eq INTEGER2 |
INTEGER1 等于 INTEGER2 |
INTEGER1 -ne INTEGER2 |
INTEGER1 不等于 INTEGER2 |
INTEGER1 -gt INTEGER2 |
INTEGER1 大于 INTEGER2 |
INTEGER1 -ge INTEGER2 |
INTEGER1 大于或等于 INTEGER2 |
INTEGER1 -lt INTEGER2 |
INTEGER1 小于 INTEGER2 |
INTEGER1 -le INTEGER2 |
INTEGER1 小于或等于 INTEGER2 |
Shell 脚本显示了两个整数之间的各种算术检查,如下所示:
#!/bin/bash
# Filename: integer_checks.sh
# Description: Performing different arithmetic checks between integers
a=12 b=24 c=78 d=24
echo "a = $a , b = $b , c = $c , d = $d"
echo -n "Is a greater than b ? "
test $a -gt $b
echo $?
echo -n "Is b equal to d ? "
test $b -eq $d
echo $?
echo -n "Is c not equal to d ? "
test $c -ne $d
echo $?
运行脚本后的输出如下:
a = 12 , b = 24 , c = 78 , d = 24
Is a greater than b ? 1
Is b equal to d ? 0
Is c not equal to d ? 0
此外,此处的测试在整数之间运行比较测试后返回退出状态,并在成功时返回0
(true),在测试失败时返回1
(false)。
字符串检查
命令测试还允许您对字符串进行检查。可能的检查在下表中描述:
比较 | 描述 |
---|---|
-z STRING |
字符串的长度为零 |
-n STRING |
字符串的长度不为零 |
STRING1 = STRING2 |
STRING1 和 STRING2 相等 |
SRING1 != STRING2 |
STRING1 和 STRING2 不相等 |
Shell 脚本显示了字符串之间的各种字符串检查,如下所示:
#!/bin/bash
# Filename: string_checks.sh
# Description: Performing checks on and between strings
str1="Hello" str2="Hell" str3="" str4="Hello"
echo "str1 = $str1 , str2 = $str2 , str3 = $str3 , str4 = $str4"
echo -n "Is str3 empty ? "
test -z $str3
echo $?
echo -n "Is str2 not empty? "
test -n $str2
echo $?
echo -n "Are str1 and str4 equal? "
test $str1 = $str4
echo $?
echo -n "Are str1 and str2 different? "
test $str1 != $str2
echo $?
运行脚本后的输出如下:
str1 = Hello , str2 = Hell , str3 = , str4 = Hello
Is str3 empty ? 0
Is str2 not empty? 0
Are str1 and str4 equal? 0
Are str1 and str2 different? 0
在这里,如果字符串检查为真,则测试返回0
退出状态,否则返回1
。
表达式检查
test
命令还允许您对表达式进行检查。表达式本身也可以包含多个要评估的表达式。可能的检查如下表所示:
比较 | 描述 |
---|---|
( EXPRESSION ) |
此表达式为真 |
! EXPRESSION |
此表达式为假 |
EXPRESSION1 -a EXPRESSION2 |
两个表达式都为真(AND 操作) |
EXPRESSION1 -o EXPRESSION2 |
两个表达式中的一个为真(OR 操作) |
Shell 脚本显示了字符串之间的各种字符串检查,如下所示:
#!/bin/bash
# Filename: expression_checks.sh
# Description: Performing checks on and between expressions
a=5 b=56
str1="Hello" str2="Hello"
echo "a = $a , b = $b , str1 = $str1 , str2 = $str2"
echo -n "Is a and b are not equal, and str1 and str2 are equal? "
test ! $a -eq $b -a $str1 = $str2
echo $?
echo -n "Is a and b are equal, and str1 and str2 are equal? "
test $a -eq $b -a $str1 = $str2
echo $?
echo -n "Does /tmp is a sirectory and execute permission exists? "
test -d /tmp -a -x /tmp
echo $?
echo -n "Is /tmp file is a block file or write permission exists? "
test -b /tmp -o -w /tmp
echo $?
运行此脚本的输出如下:
a = 5 , b = 56 , str1 = Hello , str2 = Hello
Is a and b are not equal, and str1 and str2 are equal? 0
Is a and b are equal, and str1 and str2 are equal? 1
Does /tmp is a sirectory and execute permission exists? 0
Is /tmp file is a block file or write permission exists? 0
与test
命令的其他检查类似,0
退出代码表示表达式评估为真,1
表示评估为假。
使用 if 和 else 的条件语句
Shell 提供了if
和else
,根据评估是true
还是false
来运行条件语句。如果我们只想在某个条件为true
时执行某些任务,这将非常有用。
if 的测试条件可以使用测试条件或[条件]给出。我们已经在上一节使用测试测试表达式中学习了多个用例和示例。
简单的 if 和 else
if
条件的语法如下:
if [ conditional_expression ]
then
statements
fi
如果conditional_expression
为true
——也就是说,退出状态为0
——那么其中的语句将被执行。如果不是,则它将被忽略,fi
后的下一行将被执行。
if
和else
的语法如下:
if [ conditional_expression ]
then
statements
else
statements
fi
有时,当条件不成立时,我们可能希望执行一些语句。在这种情况下,使用if
和else
。在这里,如果conditional_statement
为真,则 if 内的语句将被执行。否则,else 内的语句将被执行。
以下 shell 脚本在文件存在时打印消息:
#!/bin/bash
# Filename: file_exist.sh
# Description: Print message if file exists
if [ -e /usr/bin/ls ]
then
echo "File /usr/bin/ls exists"
fi
运行脚本后的输出如下:
File /usr/bin/ls exists
另一个示例显示了两个整数中的较大者,如下所示:
#!/bin/bash
# Filename: greater_integer.sh
# Description: Determining greater among two integers
echo "Enter two integers a and b"
read a b # Reading input from stdin
echo "a = $a , b = $b"
# Finding greater integer
if test $a -gt $b
then
echo "a is greater than b"
else
echo "b is greater than a"
fi
运行脚本后的输出如下:
$ sh greater_integer.sh
Enter two integers a and b
56 8
a = 56 , b = 8
a is greater than b
if、elif 和 else 语句
在某些情况下,存在超过两个选择,其中只有一个需要执行。elif
允许您在条件不成立时使用另一个if
条件,而不是使用else
。语法如下:
if [ conditional_expression1 ]
then
statements
elif [ conditional_expression2 ]
then
statements
elif [ conditional_expression3 ]
then
statements
# More elif conditions
else
statements
以下 shell 脚本将使elif
的用法更清晰。此脚本要求用户输入带有绝对路径的有效文件或目录名称。对于有效的常规文件或目录,它显示以下内容:
#!/bin/bash
# Filename: elif_usage.sh
# Description: Display content if user input is a regular file or a directoy
echo "Enter a valid file or directory path"
read path
echo "Entered path is $path"
if [ -f $path ]
then
echo "File is a regular file and its content is:"
cat $path
elif [ -d $path ]
then
echo "File is a directory and its content is:"
ls $path
else
echo "Not a valid regular file or directory"
fi
运行脚本后的输出如下:
Enter a valid file or directory path
/home/
Entered path is /home/
File is a directory and its content is:
lost+found sinny
嵌套 if
在许多情况下,需要多个if
条件,因为条件的执行取决于另一个条件的结果。 语法如下:
if [ conditional_expression1 ]
then
if [ conditional_expression2 ]
then
statements
if [conditional_expression3 ]
then
statements
fi
fi
fi
以下脚本示例更详细地解释了嵌套的if
。 在此脚本中,我们将看到如何找到三个整数值中的最大值:
#!/bin/bash
# Filename: nested_if.sh
# Description: Finding greatest integer among 3 by making use of nested if
echo "Enter three integer value"
read a b c
echo "a = $a , b = $b, c = $c"
if [ $a -gt $b ]
then
if [ $a -gt $c ]
then
echo "a is the greatest integer"
else
echo "c is the greatest integer"
fi
else
if [ $b -gt $c ]
then
echo "b is the greatest integer"
else
echo "c is the greatest integer"
fi
fi
运行脚本后的输出如下:
Enter three integer value
78 110 7
a = 78 , b = 110, c = 7
b is the greatest integer
索引数组和关联数组
Bash 提供了一个声明变量列表(或数组)的功能,可以是索引数组或关联数组的一维数组。 数组的大小可以是0
或更多。
索引数组
索引数组包含可能已初始化或未初始化的变量。 索引数组的索引从0
开始。 这意味着数组的第一个元素将从索引0
开始。
数组声明和赋值
可以通过初始化任何索引来声明索引数组,如下所示:
array_name[index]=value
在这里,索引可以是任何正整数,或者表达式必须评估为正整数。
另一种声明方式是使用内置的declare
shell,如下所示:
declare -a array_name
我们还可以在声明时使用值初始化数组。 值用括号括起来,每个值用空格分隔,如下所示:
declare -a array_name=(value1 value2 value3 …)
数组的操作
初始化和声明变量的值是不够的。 当我们对其执行不同的操作以获得所需的结果时,数组的实际用法才体现出来。
可以对索引数组执行以下操作:
- 通过索引访问数组元素:可以通过引用其索引值来访问数组的元素:
echo ${array_name[index]}
- 打印数组的内容:如果给出数组的索引为
@
或*
,则可以打印数组的内容:
echo ${array_name[*]}
echo ${array_name[@]}
- 获取数组的长度:可以使用带有数组变量的
$#
获取数组的长度:
echo ${#array_name[@]}
echo ${#array_name[*]}
- 获取数组元素的长度:可以使用
$#
获取第 n 个索引的数组元素的长度:
echo ${#array_name[n]}
- 删除元素或整个数组:可以使用
unset
关键字从数组中删除元素:
unset array_name[index] # Removes value at index
unset array_name # Deletes entire array
以下 shell 脚本演示了对索引数组的不同操作:
#!/bin/bash
# Filename: indexed_array.sh
# Description: Demonstrating different operations on indexed array
#Declaring an array conutries and intializing it
declare -a countries=(India Japan Indonesia 'Sri Lanka' USA Canada)
# Printing Length and elements of countries array
echo "Length of array countries = ${#countries[@]}"
echo ${countries[@]}
# Deleting 2nd element of array
unset countries[1]
echo "Updated length and content of countries array"
echo "Length = ${#countries[@]}"
echo ${countries[@]}
# Adding two more countries to array
countries=("${countries[@]}" "Indonesia" "England")
echo "Updated length and content of countries array"
echo "Length = ${#countries[@]}"
echo ${countries[@]}
执行此脚本后的输出如下:
Length of array countries = 6
India Japan Indonesia Sri Lanka USA Canada
Updated length and content of countries array
Length = 5
India Indonesia Sri Lanka USA Canada
Updated length and content of countries array
Length = 7
India Indonesia Sri Lanka USA Canada Indonesia England
关联数组
关联数组包含一个元素列表,其中每个元素都有一个键值对。 关联数组的元素不是通过使用整数值0
到N
来引用的。 它是通过提供包含相应值的键名来引用的。 每个键名都应该是唯一的。
声明和赋值
使用declare
shell 内置的-A
选项进行关联数组的声明如下:
declare -A array_name
关联数组使用键而不是索引在方括号中初始化值,如下所示:
array_name[key]=value
可以以以下方式初始化多个值:
array_name=([key1]=value1 [key2]=value2 ...)
数组的操作
关联数组的一些操作与索引数组类似,例如打印数组的长度和内容。 操作如下:
- 通过键名访问数组元素;要访问关联数组的元素,请使用唯一键,如下所示:
echo ${array_name[key]}
- 打印关联数组内容:使用以下语法打印关联数组:
echo ${array_name[*]}
echo ${array_name[@]}
Obtaining the length of an array:
echo ${#array_name[@]}
echo ${#array_name[*]}
- 获取给定键的值和长度:
echo ${array_name[k]} # Value of key k
echo ${#array_name[k]} # Length of value of key k
- 添加新元素;要在关联数组中添加新元素,请使用
+=
运算符,如下所示:
array_name+=([key]=value)
- 使用
k
键删除关联数组的元素如下:
unset array_name[k]
- 删除关联数组
array_name
如下:
unset array_name
以下 shell 脚本演示了关联数组的不同操作:
#!/bin/bash
# Filename: associative_array.sh
# Description: Demonstrating different operations on associative array
# Declaring a new associative array
declare -A student
# Assigning different fields in student array
student=([name]=Foo [usn]=2D [subject]=maths [marks]=67)
# Printing length and content of array student
echo "Length of student array = ${#student[@]}"
echo ${student[@]}
# deleting element with key marks
unset student[marks]
echo "Updated array content:"
echo ${student[@]}
# Adding department in student array
student+=([department]=Electronics)
echo "Updated array content:"
echo ${student[@]}
执行此脚本后的输出如下:
Length of student array = 4
Foo 67 maths 2D
Updated array content:
Foo maths 2D
Updated array content:
Foo maths Electronics 2D
使用 for 循环
for
循环可用于遍历列表中的项目或直到条件为真。
在 bash 中使用for
循环的语法如下:
for item in [list]
do
#Tasks
done
另一种编写for
循环的方式是 C 的方式,如下所示:
for (( expr1; expr2; expr3 ))
# Tasks
done
在这里,expr1
是初始化,expr2
是条件,expr3
是增量。
简单迭代
以下 shell 脚本解释了如何使用for
循环打印列表的值:
#!/bin/bash
# Filename: for_loop.sh
# Description: Basic for loop in bash
declare -a names=(Foo Bar Tom Jerry)
echo "Content of names array is:"
for name in ${names[@]}
do
echo -n "$name "
done
echo
脚本的输出如下:
Content of names array is:
Foo Bar Tom Jerry
迭代命令输出
我们知道很多命令会给出多行输出,比如ls
、cat
、grep
等。在许多情况下,循环遍历每行输出并对其进行进一步处理是有意义的。
以下示例循环遍历'/
'的内容并打印目录:
#!/bin/bash
# Filename: finding_directories.sh
# Description: Print which all files in / are directories
echo "Directories in / :"
for file in 'ls /'
do
if [ -d "/"$file ]
then
echo -n "/$file "
fi
done
echo
运行此脚本后的输出如下:
Directories in / :
/bin /boot /dev /etc /home /lib /lib64 /lost+found /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var
为 for 循环指定范围
我们还可以在for
循环中指定整数范围,并为其指定可选的增量值:
#!/bin/bash
# Filename: range_in_for.sh
# Description: Specifying range of numbers to for loop
echo "Numbers between 5 to 10 -"
for num in {5..10}
do
echo -n "$num "
done
echo
echo "Odd numbers between 1 to 10 -"
for num in {1..10..2}
do
echo -n "$num "
done
echo
运行此脚本后的输出如下:
Numbers between 5 to 10 -
5 6 7 8 9 10
Odd numbers between 1 to 10 -
1 3 5 7 9
小巧的 for 循环
在某些情况下,我们不想编写脚本然后执行它;相反,我们更喜欢在 shell 中完成工作。在这种情况下,将完整的 for 循环写在一行中非常有用和方便,而不是将其变成多行。
例如,打印 3 到 20 之间 3 的倍数可以使用以下代码完成:
$ for num in {3..20..3}; do echo -n "$num " ; done
3 6 9 12 15 18
选择、while 和 until 循环
select
、while
和until
循环也用于循环和迭代列表中的每个项目,或者在条件为真时进行轻微变化的语法。
使用 select 循环
选择循环有助于以简单格式创建带编号的菜单,用户可以从中选择一个或多个选项。
select
循环的语法如下:
select var in list
do
# Tasks to perform
done
list
可以在使用select
循环时预先生成或指定为[item1 item2 item3 …]
的形式。
例如,考虑一个简单的菜单,列出'/
'的内容,并要求用户输入一个选项,以便知道它是否是一个目录:
#!/bin/bash
# Filename: select.sh
# Description: Giving user choice using select to choose
select file in 'ls /'
do
if [ -d "/"$file ]
then
echo "$file is a directory"
else
echo "$file is not a directory"
fi
done
运行脚本后的输出如下:
要退出脚本,请按Ctrl + C。
while 循环
while
循环允许您重复任务,直到条件为真。语法与 C 和 C++编程语言中的语法非常相似,如下所示:
while [ condition ]
do
# Task to perform
done
例如,读取应用程序的名称并显示该应用程序所有运行实例的 pids,如下所示:
#!/bin/bash
# Filename: while_loop.sh
# Description: Using while loop to read user input
echo "Enter application name"
while read line
do
echo -n "Running PID of application $line :"
pidof $line
done
运行此脚本后的输出如下:
Enter application name
firefox
Running PID of application firefox : 1771
bash
Running PID of application bash : 9876 9646 5333 4388 3970 2090 2079 2012 1683 1336
ls
Running PID of application ls:
systemd
Running PID of application systemd : 1330 1026 1
要退出脚本,请按Ctrl + C。
直到循环
until
循环与while
循环非常相似,但唯一的区别是它执行代码块,直到条件执行为 false。until
的语法如下:
until condition
do
# Task to be executed
done
例如,假设我们有兴趣知道应用程序的pid
,每当它的任何实例正在运行时。为此,我们可以使用until
并使用sleep
在一定间隔内检查应用程序的pidof
。当我们找到pid
时,我们可以退出until
循环并打印应用程序运行实例的pid
。
以下 shell 脚本演示了相同的内容:
#!/bin/bash
# Filename: until_loop.sh
# Description: Using until loop to read user input
echo "Enter application name"
read app
until pidof $app
do
sleep 5
done
echo "$app is running now with pid 'pidof $app'"
执行此脚本后的输出如下:
Enter application name
firefox
1867
firefox is running now with pid 1867
切换到我的选择
Switch 用于根据条件或表达式的结果跳转和运行特定的 case。它作为在 bash 中使用多个if的替代方案,并使 bash 脚本更清晰和可读。
switch
的语法如下:
case $variable in
pattern1)
# Tasks to be executed
;;
pattern2)
# Tasks to be executed
;;
…
pattern n)
# Tasks to be executed
;;
*)
esac
在语法中,$variable
是需要在提供的选择列表中匹配的表达式或值。
在每个选择中,可以指定一个模式或模式的组合。;;
告诉 bash 给定选择块的结束。esac
关键字指定 case 块的结束。
以下是一个示例,用于计算给定路径中文件和目录的数量:
#!/bin/bash
# Filename: switch_case.sh
# Description: Using case to find count of directories and files in a # path
echo "Enter target path"
read path
files_count=0
dirs_count=0
for file in 'ls -l $path | cut -d ' ' -f1'
do
case "$file" in
d*)
dirs_count='expr $dirs_count + 1 '
;;
-*)
files_count='expr $files_count + 1'
;;
*)
esac
done
echo "Directories count = $dirs_count"
echo "Regular file count = $files_count"
运行此脚本后的输出如下:
Enter target path
/usr/lib64
Directories count = 134
Regular file count = 1563
在这个例子中,我们首先使用read
shell 内置命令从用户那里读取输入路径。然后,我们将文件和目录计数的计数变量初始化为0
。此外,我们使用ls -l $path | cut -d ' ' -f1
来获取路径内容的文件属性的长列表,然后检索其第一列。我们知道ls -l
的第一列的第一个字符表示文件的类型。如果是d
,那么它是一个目录,-
表示一个常规文件。dirs_count
或files_count
变量相应地递增。
使用 xargs 传递 stdout 作为参数
xargs
命令用于从标准输入构建和执行命令行。诸如cp
、echo
、rm
、wc
等命令不从标准输入获取输入,也不从另一个命令的重定向输出获取输入。在这样的命令中,我们可以使用xargs
将输入作为另一个命令的输出。语法如下:
xargs [option]
以下表格解释了一些选项:
选项 | 描述 |
---|---|
-a file |
这从文件中读取项目,而不是从 stdin 中读取 |
-0 , --null |
输入以空字符而不是空格终止 |
-t , --verbose |
在执行之前在标准输出上打印命令行 |
--show-limits |
这显示操作系统强加的命令行长度限制 |
-P max-procs |
一次运行最多 max-procs 个进程 |
-n max-args |
最多使用每个命令行的 max-args 参数 |
使用 xargs 的基本操作
xargs
命令可以不带任何选项。它允许您从 stdin 输入,并在调用ctrl + d
时打印输入的任何内容:
$ xargs
Linux shell
scripting
ctrl + d
Linux shell scripting
--show-limits
选项可用于了解命令行长度的限制:
$ xargs --show-limits
Your environment variables take up 4017 bytes
POSIX upper limit on argument length (this system): 2091087
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2087070
Size of command buffer we are actually using: 131072
使用 xargs 查找具有最大大小的文件
以下 shell 脚本将解释如何使用xargs
递归地获取给定目录中具有最大大小的文件:
#!/bin/bash
# Filename: max_file_size.sh
# Description: File with maximum size in a directory recursively
echo "Enter path of directory"
read path
echo "File with maximum size:"
find $path -type f | xargs du -h | sort -h | tail -1
运行此脚本后的输出如下:
Enter path of directory
/usr/bin
File with maximum size:
12M /usr/bin/doxygen
在这个例子中,我们使用xargs
将从find
命令获取的每个常规文件传递给大小计算。此外,du
的输出被重定向到sort
命令进行人类数字排序,然后我们可以打印最后一行或排序以获得具有最大大小的文件。
使用给定模式归档文件
使用xargs
的另一个有用的例子是归档我们感兴趣的所有文件,并将这些文件作为备份文件保留。
以下 shell 脚本在指定目录中查找所有的 shell 脚本,并为进一步参考创建tar
文件:
#!/bin/bash
# Filename: tar_creation.sh
# Description: Create tar of all shell scripts in a directory
echo "Specify directory path"
read path
find $path -name "*.sh" | xargs tar cvf scripts.tar
运行脚本后的输出如下:
Specify directory path
/usr/lib64
/usr/lib64/nspluginwrapper/npviewer.sh
/usr/lib64/xml2Conf.sh
/usr/lib64/firefox/run-mozilla.sh
/usr/lib64/libreoffice/ure/bin/startup.sh
在这个例子中,搜索所有扩展名为.sh
的文件,并将其作为参数传递给tar
命令以创建一个归档。文件scripts.tar
被创建在调用脚本的目录中。
使用函数和位置参数
与其他编程语言类似,函数是一种编写一组操作一次并多次使用的方法。它使代码模块化和可重用。
编写函数的语法如下:
function function_name
{
# Common set of action to be done
}
这里,function
是一个关键字,用于指定一个函数,function_name
是函数的名称;我们也可以以下列方式定义一个函数:
function_name()
{
# Common set of action to be done
}
在花括号内编写的操作在调用特定函数时执行。
在 bash 中调用函数
考虑以下定义my_func()
函数的 shell 脚本:
#!/bin/bash
# Filename: function_call.sh
# Description: Shows how function is defined and called in bash
# Defining my_func function
my_func()
{
echo "Function my_func is called"
return 3
}
my_func # Calling my_func function
return_value=$?
echo "Return value of function = $return_value"
要在 shell 脚本中调用my_func()
,我们只需写出函数的名称:
my_func
my_func
函数的返回值为 3。函数的返回值是函数的退出状态。在前面的例子中,my_func
函数的退出状态被赋给return_value
变量。
运行上述脚本的结果如下:
Function my_func is called
Return value of function = 3
函数的返回值是其参数中指定的返回 shell 内置命令。如果没有使用return
,则函数中执行最后一个命令的退出代码。在这个例子中,退出代码将是echo
命令的退出代码。
向函数传递参数
通过指定函数的第一个名称,后跟以空格分隔的参数,可以为函数提供参数。shell 中的函数不是通过名称而是通过位置来使用参数;我们也可以说 shell 函数使用位置参数。在函数内部,通过变量名$1
、$2
、$3
、$n
等访问位置参数。
可以使用$#
获取参数的长度,使用$@
或$*
一起获取传递的参数列表。
以下 shell 脚本解释了如何在 bash 中传递参数给函数:
#!/bin/bash
# Filename: func_param.sh
# Description: How parameters to function is passed and accessed in bash
upper_case()
{
if [ $# -eq 1 ]
then
echo $1 | tr '[a-z]' '[A-Z]'
fi
}
upper_case hello
upper_case "Linux shell scripting"
上述脚本的输出如下:
HELLO
LINUX SHELL SCRIPTING
在上面的 shell 脚本示例中,我们两次使用upper_case()
方法,参数分别为hello
和Linux shell scripting
。它们都被转换为大写。类似地,其他函数也可以编写,以避免重复编写工作。
别名
shell 中的别名指的是给命令或一组命令取另一个名称。当命令的名称很长时,它非常有用。借助别名,我们可以避免输入更长的名称,并根据自己的方便性来调用命令。
要创建别名,使用别名 shell 内置命令。语法如下:
alias alias_name="要别名的命令"
创建别名
要以人类可读的格式打印磁盘空间,我们使用带有-h
选项的df
命令。通过将df -h
的别名设置为df
,我们可以避免反复输入df -h
。
在将其别名设置为df -h
之前,df
命令的输出如下所示:
$ df
现在,要将df -h
的别名设置为df
,我们将执行以下命令:
$ alias df="df -h" # Creating alias
$ df
获得的输出如下:
我们看到,在将df -h
的别名设置为df
后,以人类可读的格式打印了默认磁盘空间。
另一个有用的例子是将rm
命令别名设置为rm -i
。使用带有-i
选项的rm
会在删除文件之前要求用户确认:
#!/bin/bash
# Filename: alias.sh
# Description: Creating alias of rm -i
touch /tmp/file.txt
rm /tmp/file.txt # File gets deleted silently
touch /tmp/file.txt # Creating again a file
alias rm="rm -i" # Creating alias of rm -i
rm /tmp/file.txt
执行上述脚本后的输出如下:
rm: remove regular empty file '/tmp/file.txt'? Y
我们可以看到,在创建别名后,rm
在删除/tmp/file.txt
文件之前要求确认。
列出所有别名
要查看当前 shell 已设置的别名,可以使用不带任何参数或带-p
选项的别名:
$ alias
alias df='df -h'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias vi='vim'
我们可以看到,我们创建的df
别名仍然存在,并且还有其他已存在的别名。
删除别名
要删除已经存在的别名,可以使用unalias
shell 内置命令:
$ unalias df # Deletes df alias
$ alias -p # Printing existing aliases
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias vi='vim'
我们看到df
别名已被移除。要删除所有别名,请使用unalias
和a
选项:
$ unalias -a # Delets all aliases for current shell
$ alias -p
我们可以看到所有的别名现在都已经被删除。
pushd 和 popd
pushd
和popd
都是 shell 内置命令。pushd
命令用于将当前目录保存到堆栈中并移动到新目录。此外,popd
可用于返回到堆栈顶部的上一个目录。
当我们需要频繁在两个目录之间切换时,它非常有用。
使用pushd
的语法如下:
pushd [目录]
如果未指定目录,pushd
会将目录更改为堆栈顶部的目录。
使用popd
的语法如下:
popd
使用popd
开关,我们可以返回到堆栈顶部的上一个目录并弹出该目录。
以下示例计算指定目录中文件或目录的数量,直到一个级别为止:
#!/bin/bash
# Filename: pushd_popd.sh
# Description: Count number of files and directories
echo "Enter a directory path"
read path
if [ -d $path ]
then
pushd $path > /dev/null
echo "File count in $path directory = 'ls | wc -l'"
for f in 'ls'
do
if [ -d $f ]
then
pushd $f > /dev/null
echo "File count in sub-directory $f = 'ls | wc -l'"
popd > /dev/null
fi
done
popd > /dev/null
else
echo "$path is not a directory"
fi
运行上述脚本后的输出如下:
Enter a directory path
/usr/local
File count in /usr/local directory = 10
File count in sub-directory bin = 0
File count in sub-directory etc = 0
File count in sub-directory games = 0
File count in sub-directory include = 0
File count in sub-directory lib = 0
File count in sub-directory lib64 = 0
File count in sub-directory libexec = 0
File count in sub-directory sbin = 0
File count in sub-directory share = 3
File count in sub-directory src = 0
总结
阅读完本章后,你现在应该有足够的信心来使用条件语句、循环等编写有效的 shell 脚本。现在,你也可以使用 shell 中的函数来编写模块化和可重用的代码。了解退出代码的知识将有助于知道命令是否成功执行。你还应该了解一些更有用的 shell 内建命令,比如alias
、pushd
和popd
。
在下一章中,我们将通过了解如何编写可重用的 shell 脚本本身来学习如何模块化我们的脚本,这些脚本可以在 shell 脚本中使用。我们还将看到如何调试我们的 shell 脚本以解决问题。
第四章:模块化和调试
在现实世界中,当你编写代码时,你要么永远维护它,要么以后有人接管它并对其进行更改。非常重要的是,您编写一个质量良好的 shell 脚本,以便更容易进一步维护它。同样重要的是,shell 脚本没有错误,以便按预期完成工作。在生产系统上运行的脚本非常关键,因为脚本的任何错误或错误行为可能会造成轻微或重大的损害。为了解决这些关键问题,重要的是尽快解决问题。
在本章中,我们将看到如何编写模块化和可重用的代码,以便快速和无需任何麻烦地维护和更新我们的 shell 脚本应用程序。我们还将看到如何使用不同的调试技术快速轻松地解决 shell 脚本中的错误。我们将看到如何通过在脚本中提供命令行选项的支持为不同的任务提供不同的选择。了解如何在脚本中提供命令行完成甚至会增加使用脚本的便利性。
本章将详细介绍以下主题:
-
将你的脚本模块化
-
将命令行参数传递给脚本
-
调试您的脚本
-
命令完成
将你的脚本模块化
在编写 shell 脚本时,有一个阶段我们会觉得一个 shell 脚本文件变得太大,难以阅读和管理。为了避免这种情况发生在我们的 shell 脚本中,保持脚本模块化非常重要。
为了保持脚本的模块化和可维护性,您可以执行以下操作:
-
创建函数而不是一遍又一遍地写相同的代码
-
在一个单独的脚本中编写一组通用的函数和变量,然后源来使用它
我们已经看到如何在第三章 有效脚本编写中定义和使用函数。在这里,我们将看到如何将一个更大的脚本分成更小的 shell 脚本模块,然后通过源使用它们。换句话说,我们可以说在bash
中创建库。
源到脚本文件
源是一个 shell 内置命令,它在当前 shell 环境中读取并执行脚本文件。如果一个脚本调用另一个脚本文件的源,那么该文件中可用的所有函数和变量将被加载以供调用脚本使用。
语法
使用源的语法如下:
source <script filename> [arguments]
或:
. <script filename> [arguments]
脚本文件名
可以带有或不带有路径名。如果提供了绝对或相对路径,它将仅在该路径中查找。否则,将在PATH
变量中指定的目录中搜索文件名。
arguments
被视为脚本文件名的位置参数。
source
命令的退出状态将是在脚本文件中执行的最后一个命令的退出代码。如果脚本文件不存在或没有权限,则退出状态将为1
。
创建一个 shell 脚本库
库提供了一个功能集合,可以被另一个应用程序重用,而无需从头开始重写。我们可以通过将我们的函数和变量放入一个 shell 脚本文件中来创建一个 shell 库,以便重用。
以下的shell_library.sh
脚本是一个 shell 库的例子:
#!/bin/bash
# Filename: shell_library.sh
# Description: Demonstrating creation of library in shell
# Declare global variables
declare is_regular_file
declare is_directory_file
# Function to check file type
function file_type()
{
is_regular_file=0
is_directory_file=0
if [ -f $1 ]
then
is_regular_file=1
elif [ -d $1 ]
then
is_directory_file=1
fi
}
# Printing regular file detail
function print_file_details()
{
echo "Filename - $1"
echo "Line count - `cat $1 | wc -l`"
echo "Size - `du -h $1 | cut -f1`"
echo "Owner - `ls -l $1 | tr -s ' '|cut -d ' ' -f3`"
echo "Last modified date - `ls -l $1 | tr -s ' '|cut -d ' ' -f6,7`"
}
# Printing directory details
function print_directory_details()
{
echo "Directory Name - $1"
echo "File Count in directory - `ls $1|wc -l`"
echo "Owner - `ls -ld $1 | tr -s ' '|cut -d ' ' -f3`"
echo "Last modified date - `ls -ld $1 | tr -s ' '|cut -d ' ' -f6,7`"
}
前面的shell_library.sh
shell 脚本包含了is_regular_file
和is_directory_file
全局变量,可以在调用file_type()
函数后用于知道给定的文件是普通文件还是目录。此外,根据文件的类型,可以打印有用的详细信息。
加载一个 shell 脚本库
创建 shell 库是没有用的,除非它在另一个 shell 脚本中使用。我们可以直接在 shell 中使用 shell 脚本库,也可以在另一个脚本文件中使用。要加载 shell 脚本库,我们将使用 source 命令或.(句点字符),然后是 shell 脚本库。
在 bash 中调用 shell 库
要在 shell 中使用shell_library.sh
脚本文件,我们可以这样做:
$ source shell_library.sh
或:
$ . shell_library.sh
调用它们中的任何一个将使函数和变量可用于当前 shell 中使用:
$ file_type /usr/bin
$ echo $is_directory_file
1
$ echo $is_regular_file
0
$ if [ $is_directory_file -eq 1 ]; then print_directory_details /usr/bin; fi
Directory Name - /usr/bin
File Count in directory - 2336
Owner - root
Last modified date - Jul 12
当执行file_type /usr/bin
命令时,将调用带有/usr/bin
参数的file_type()
函数。结果是,全局变量is_directory_file
或is_regular_file
将设置为1
(true
),取决于/usr/bin
路径的类型。使用 shell 的if
条件,我们测试is_directory_file
变量是否设置为1
。如果设置为1
,则调用print_directory_details()
函数,参数为/usr/bin
,以打印其详细信息。
在另一个 shell 脚本中调用 shell 库
以下示例解释了在 shell 脚本文件中使用 shell 库的用法:
#!/bin/bash
# Filename: shell_library_usage.sh
# Description: Demonstrating shell library usage in shell script
# Print details of all files/directories in a directory
echo "Enter path of directory"
read dir
# Loading shell_library.sh module
. $PWD/shell_library.sh
# Check if entered pathname is a directory
# If directory, then print files/directories details inside it
file_type $dir
if [ $is_directory_file -eq 1 ]
then
pushd $dir > /dev/null # Save current directory and cd to $dir
for file in `ls`
do
file_type $file
if [ $is_directory_file -eq 1 ]
then
print_directory_details $file
echo
elif [ $is_regular_file -eq 1 ]
then
print_file_details $file
echo
fi
done
fi
在运行shell_library_usage.sh
脚本后,得到以下输出:
$ sh shell_library_usage.sh # Few outputs from /usr directory
Enter path of directory
/usr
Directory Name - bin
File Count in directory - 2336
Owner - root
Last modified date - Jul 12
Directory Name - games
File Count in directory - 0
Owner - root
Last modified date - Aug 16
Directory Name - include
File Count in directory - 172
Owner - root
Last modified date - Jul 12
Directory Name - lib
File Count in directory - 603
Owner - root
Last modified date - Jul 12
Directory Name - lib64
File Count in directory - 3380
Owner - root
Last modified date - Jul 12
Directory Name - libexec
File Count in directory - 170
Owner - root
Last modified date - Jul 7
注意
要加载 shell 脚本库,使用source
或.
,然后是script_filename
。
source
和.
(句点字符)都在当前 shell 中执行脚本。./script
与. script
不同,因为./script
在子 shell 中执行脚本,而. script
在调用它的 shell 中执行。
将命令行参数传递给脚本
到目前为止,我们已经看到了诸如grep
、head
、ls
、cat
等命令的用法。这些命令还支持通过命令行传递参数给命令。一些命令行参数是输入文件、输出文件和选项。根据输出的需要提供参数。例如,执行ls -l filename
以获得长列表输出,而使用ls -R filename
用于递归显示目录的内容。
Shell 脚本还支持提供命令行参数,我们可以通过 shell 脚本进一步处理。
命令行参数可以如下给出:
<script_file> arg1 arg2 arg3 … argN
这里,script_file
是要执行的 shell 脚本文件,arg1
、arg2
、arg3
、argN
等是命令行参数。
在脚本中读取参数
命令行参数作为位置参数传递给 shell 脚本。因此,arg1
在脚本中将被访问为$1
,arg2
为$2
,依此类推。
以下 shell 演示了命令行参数的用法:
#!/bin/bash
# Filename: command_line_arg.sh
# Description: Accessing command line parameters in shell script
# Printing first, second and third command line parameters"
echo "First command line parameter = $1"
echo "Second command line parameter = $2"
echo "Third command line parameter = $3"
在带有参数运行command_line_arg.sh
脚本后,得到以下输出:
$ sh command_line_arg.sh Linux Shell Scripting
First command line parameter = Linux
Second command line parameter = Shell
Third command line parameter = Scripting
以下表格显示了有用的特殊变量,用于获取有关命令行参数的更多信息:
特殊变量 | 描述 |
---|---|
$# |
命令行参数的数量 |
$* | 以单个字符串的形式包含所有命令行参数的完整集合,即'$1 $2 … $n' |
|
$@ | 完整的命令行参数集合,但每个参数都用单独的引号括起来,即'$1' '$2' … '$n' |
|
$0 |
shell 脚本本身的名称 |
$1, $1, … $N |
分别指代参数 1、参数 2、…、参数 N |
在脚本中使用$#
来检查命令行参数的数量将非常有助于进一步处理参数。
以下是另一个接受命令行参数的 shell 脚本示例:
#!/bin/bash
# Filename: command_line_arg2.sh
# Description: Creating directories in /tmp
# Check if at least 1 argument is passed in command line
if [ $# -lt 1 ]
then
echo "Specify minimum one argument to create directory"
exit 1
else
pushd /tmp > /dev/null
echo "Directory to be created are: $@"
mkdir $@ # Accessing all command line arguments
fi
在执行command_line_arg2.sh
脚本后,得到以下输出:
$ sh command_line_arg2.sh a b
Directory to be created are: a b
$ sh command_line_arg2.sh
Specify minimum one argument to create directory
移动命令行参数
要将命令行参数向左移动,可以使用shift
内置命令。语法如下:
shift N
这里,N
是它可以向左移动的参数个数。
例如,假设当前的命令行参数是arg1
,arg2
,arg3
,arg4
和arg5
。它们可以在 shell 脚本中分别作为$1
,$2
,$3
,$4
和$5
访问;$#
的值为5
。当我们调用shift 3
时,参数会被移动3
个位置。现在,$1
包含arg4
,$2
包含arg5
。此外,$#
的值现在是2
。
以下 shell 脚本演示了shift
的用法:
#!/bin/bash
# Filename: shift_argument.sh
# Description: Usage of shift shell builtin
echo "Length of command line arguments = $#"
echo "Arguments are:"
echo "\$1 = $1, \$2 = $2, \$3 = $3, \$4 = $4, \$5 = $5, \$6 = $6"
echo "Shifting arguments by 3"
shift 3
echo "Length of command line arguments after 3 shift = $#"
echo "Arguments after 3 shifts are"
echo "\$1 = $1, \$2 = $2, \$3 = $3, \$4 = $4, \$5 = $5, \$6 = $6"
使用参数a b c d e f
运行shift_argument.sh
脚本后获得以下输出:
$ sh shift_argument.sh a b c d e f
Length of command line arguments = 6
Arguments are:
$1 = a, $2 = b, $3 = c, $4 = d, $5 = e, $6 = f
Shifting arguments by 3
Length of command line arguments after 3 shift = 3
Arguments after 3 shifts are
$1 = d, $2 = e, $3 = f, $4 = , $5 = , $6 =
在脚本中处理命令行选项
提供命令行选项使 shell 脚本更具交互性。从命令行参数中,我们还可以解析选项以供 shell 脚本进一步处理。
以下 shell 脚本显示了带有选项的命令行用法:
#!/bin/bash
# Filename: myprint.sh
# Description: Showing how to create command line options in shell script
function display_help()
{
echo "Usage: myprint [OPTIONS] [arg ...]"
echo "--help Display help"
echo "--version Display version of script"
echo "--print Print arguments"
}
function display_version()
{
echo "Version of shell script application is 0.1"
}
function myprint()
{
echo "Arguments are: $*"
}
# Parsing command line arguments
if [ "$1" != "" ]
then
case $1 in
--help )
display_help
exit 1
;;
--version )
display_version
exit 1
;;
--print )
shift
myprint $@
exit 1
;;
*)
display_help
exit 1
esac
fi
执行myprint.sh
脚本后获得以下输出:
$ sh myprint.sh --help
Usage: myprint [OPTIONS] [arg ...]
--help Display help
--version Display version of script
--print Print arguments
$ sh myprint.sh --version
Version of shell script application is 0.1
$ sh myprint.sh --print Linux Shell Scripting
Arguments are: Linux Shell Scripting
调试您的脚本
我们编写不同的 shell 脚本来执行不同的任务。在执行 shell 脚本时,您是否曾遇到过任何错误?答案很可能是肯定的!这是可以预料的,因为几乎不可能总是编写完美的 shell 脚本,没有错误或漏洞。
例如,以下 shell 脚本在执行时是有错误的:
#!/bin/bash
# Filename: buggy_script.sh
# Description: Demonstrating a buggy script
a=12 b=8
if [ a -gt $b ]
then
echo "a is greater than b"
else
echo "b is greater than a"
fi
执行buggy_script.sh
后获得以下输出:
$ sh buggy_script.sh
buggy_script.sh: line 6: [: a: integer expression expected
b is greater than a
从输出中,我们看到错误[: a: integer expression expected
发生在第 6 行。仅仅通过查看错误消息,通常不可能知道错误的原因,特别是第一次看到错误时。此外,在处理冗长的 shell 脚本时,手动查看代码并纠正错误是困难的。
为了克服在解决 shell 脚本中的错误或漏洞时遇到的各种麻烦,最好调试代码。调试 shell 脚本的方法如下:
-
在脚本的预期错误区域使用
echo
打印变量或要执行的命令的内容。 -
在运行脚本时使用
-x
调试整个脚本 -
使用 set 内置命令在脚本内部使用
-x
和+x
选项调试脚本的一部分
使用 echo 进行调试
echo
命令非常有用,因为它打印提供给它的任何参数。当我们在执行脚本时遇到错误时,我们知道带有错误消息的行号。在这种情况下,我们可以使用echo
在实际执行之前打印将要执行的内容。
在我们之前的例子buggy_script.sh
中,我们在第 6 行得到了一个错误——即if [ a -gt $b ]
——在执行时。我们可以使用echo
语句打印实际将在第 6 行执行的内容。以下 shell 脚本在第 6 行添加了echo
,以查看最终将在第 6 行执行的内容:
#!/bin/bash
# Filename: debugging_using_echo.sh
# Description: Debugging using echo
a=12 b=8
echo "if [ a -gt $b ]"
exit
if [ a -gt $b ]
then
echo "a is greater than b"
else
echo "b is greater than a"
fi
我们现在将按以下方式执行debugging_using_echo.sh
脚本:
$ sh debugging_using_echo.sh
if [ a -gt 8 ]
我们可以看到字符a
正在与8
进行比较,而我们期望的是变量a
的值。这意味着我们错误地忘记了在a
中使用$
来提取变量a
的值。
使用-x 调试整个脚本
使用echo
进行调试很容易,如果脚本很小,或者我们知道问题出在哪里。使用echo
的另一个缺点是,每次我们进行更改,都必须打开一个 shell 脚本,并相应地修改echo
命令。调试后,我们必须记住删除为调试目的添加的额外echo
行。
为了克服这些问题,bash 提供了-x
选项,可以在执行 shell 脚本时使用。使用-x
选项运行脚本会以调试模式运行脚本。这会打印所有要执行的命令以及脚本的输出。
以以下 shell 脚本为例:
#!/bin/bash
# Filename : debug_entire_script.sh
# Description: Debugging entire shell script using -x
# Creating diretcories in /tmp
dir1=/tmp/$1
dir2=/tmp/$2
mkdir $dir1 $dir2
ls -ld $dir1
ls -ld $dir2
rmdir $dir1
rmdir $dir2
现在,我们将按以下方式运行前述脚本:
$ sh debug_entire_script.sh pkg1
mkdir: cannot create directory '/tmp/': File exists
drwxrwxr-x. 2 skumari skumari 40 Jul 14 01:47 /tmp/pkg1
drwxrwxrwt. 23 root root 640 Jul 14 01:47 /tmp/
rmdir: failed to remove '/tmp/': Permission denied
它会给出/tmp/
目录已经存在的错误。通过查看错误,我们无法知道为什么它要创建/tmp
目录。为了跟踪整个代码,我们可以使用带有-x
选项运行debug_entire_script.sh
脚本:
$ sh -x debug_entire_script.sh pkg1
+ dir1=/tmp/pkg1
+ dir2=/tmp/
+ mkdir /tmp/pkg1 /tmp/
mkdir: cannot create directory '/tmp/': File exists
+ ls -ld /tmp/pkg1
drwxrwxr-x. 2 skumari skumari 40 Jul 14 01:47 /tmp/pkg1
+ ls -ld /tmp/
drwxrwxrwt. 23 root root 640 Jul 14 01:47 /tmp/
+ rmdir /tmp/pkg1
+ rmdir /tmp/
rmdir: failed to remove '/tmp/': Permission denied
我们可以看到dir2
是/tmp/
。这意味着没有输入来创建第二个目录。
使用-v
选项以及-x
使得调试更加详细,因为-v
会显示输入行:
$ sh -xv debug_entire_script.sh pkg1
#!/bin/bash
# Filename : debug_entire_script.sh
# Description: Debugging entire shell script using -x
# Creating diretcories in /tmp
dir1=/tmp/$1
+ dir1=/tmp/pkg1
dir2=/tmp/$2
+ dir2=/tmp/
mkdir $dir1 $dir2
+ mkdir /tmp/pkg1 /tmp/
mkdir: cannot create directory '/tmp/': File exists
ls -ld $dir1
+ ls -ld /tmp/pkg1
drwxrwxr-x. 2 skumari skumari 40 Jul 14 01:47 /tmp/pkg1
ls -ld $dir2
+ ls -ld /tmp/
drwxrwxrwt. 23 root root 640 Jul 14 01:47 /tmp/
rmdir $dir1
+ rmdir /tmp/pkg1
rmdir $dir2
+ rmdir /tmp/
rmdir: failed to remove '/tmp/': Permission denied
通过详细输出,很明显dir1
和dir2
变量期望从命令行参数中提供两个参数。因此,必须从命令行提供两个参数:
$ sh debug_entire_script.sh pkg1 pkg2
drwxrwxr-x. 2 skumari skumari 40 Jul 14 01:50 /tmp/pkg1
drwxrwxr-x. 2 skumari skumari 40 Jul 14 01:50 /tmp/pkg2
现在,脚本可以正常运行而不会出现任何错误。
注意
不再需要从命令行传递-xv
选项给 bash,我们可以在脚本文件的shebang
行中添加它,即#!/bin/bash -xv
。
使用设置选项调试脚本的部分
调试 shell 脚本时,并不总是需要一直调试整个脚本。有时,调试部分脚本更有用且节省时间。我们可以使用set
内置命令在 shell 脚本中实现部分调试:
set -x (Start debugging from here)
set +x (End debugging here)
我们可以在 shell 脚本的多个位置使用set +x
和set -x
,具体取决于需要。当执行脚本时,它们之间的命令将与输出一起打印出来。
考虑以下 shell 脚本作为示例:
#!/bin/bash
# Filename: eval.sh
# Description: Evaluating arithmetic expression
a=23
b=6
expr $a + $b
expr $a - $b
expr $a * $b
执行此脚本会得到以下输出:
$ sh eval.sh
29
17
expr: syntax error
我们得到了一个语法错误,最有可能是第三个表达式,即expr $a * $b
。
为了调试,在expr $a * $b
之前使用set -x
,之后使用set +x
。
另一个带有部分调试的脚本partial_debugging.sh
如下:
#!/bin/bash
# Filename: partial_debugging.sh
# Description: Debugging part of script of eval.sh
a=23
b=6
expr $a + $b
expr $a - $b
set -x
expr $a * $b
set +x
执行partial_debugging.sh
脚本后得到以下输出:
$ sh partial_debugging.sh
29
17
+ expr 23 eval.sh partial_debugging.sh 6
expr: syntax error
+ set +x
从前面的输出中,我们可以看到expr $a * $b
被执行为expr 23 eval.sh partial_debugging.sh 6
。这意味着,bash 在执行乘法时,扩展了*
作为当前目录中的任何内容的行为。因此,我们需要转义字符*
的行为,以防止其被扩展,即expr $a \* $b
。
脚本eval_modified.sh
是eval.sh
脚本的修改版本:
#!/bin/bash
# Filename: eval_modified.sh
# Description: Evaluating arithmetic expression
a=23
b=6
expr $a + $b
expr $a - $b
expr $a \* $b
现在,运行eval_modified.sh
的输出将如下所示:
$ sh eval_modified.sh
29
17
138
脚本现在可以完美运行而不会出现任何错误。
除了我们在调试中学到的内容,您还可以使用bashdb
调试器来更好地调试 shell 脚本。bashdb
的源代码和文档可以在bashdb.sourceforge.net/
找到。
命令完成
在命令行上工作时,每个人都必须执行一些常见任务,比如输入命令、选项、输入/输出文件路径和其他参数。有时,由于命令名称中的拼写错误,我们会写错命令名称。此外,输入一个很长的文件路径将很难记住。例如,如果我们想要递归查看路径为/dir1/dir2/dir3/dir4/dir5/dir6
的目录的内容,我们将不得不运行以下命令:
$ ls -R /dir1/dir2/dir3/dir4/dir5/dir6
我们可以看到这个目录的路径非常长,很容易在输入完整路径时出错。由于这些问题,使用命令行将花费比预期更长的时间。
为了解决所有这些问题,shell 支持一个非常好的功能,称为命令完成。除了其他 shell 外,bash 也非常好地支持命令完成。
大多数 Linux 发行版,例如 Fedora、Ubuntu、Debian 和 CentOS,都预先安装了核心命令的 bash 完成。如果没有可用,可以使用相应的发行版软件包管理器下载,软件包名称为bash-completion
。
shell 中的命令完成允许您自动完成部分输入的命令的其余字符,提供与给定命令相关的可能选项。它还建议并自动完成部分输入的文件路径。
要在 bash 中启用自动完成功能,使用Tab键。在输入命令时,如果单个命令匹配,单个TAB
将自动完成命令,双[TAB]将列出所有以部分输入的命令开头的可能命令。
例如:
$ gr[TAB] # Nothing happens
$ gre[TAB] # Autocompletes to grep
$ grep[TAB][TAB] # Lists commands installed in system and starts with grep
grep grep-changelog grepdiff
现在,假设我们想要查看/usr/share/man/
目录的内容,我们将不得不输入ls /usr/share/man/
。使用 bash 完成,输入以下命令:
$ ls /u[TAB]/sh[TAB]/man
Bash 完成将自动完成缺少的部分路径,命令将变为:
$ ls /usr/share/man
使用 complete 管理 bash 完成
complete
是一个内置的 shell,可用于查看系统中可用命令的 bash 完成规范。它还用于修改、删除和创建 bash 完成。
查看现有的 bash 完成
要了解现有的 bash 完成,请使用complete
命令,带有或不带-p
选项:
$ complete -p
以下是前述命令的一些输出:
complete cat # No completion output
complete -F _longopt grep # Completion as files from current directory
complete -d pushd # Completion as directories from current directory
complete -c which # Completion as list of all available commands
要在这些命令上看到 bash 完成,输入以下命令:
这将列出所有文件/目录,包括隐藏的文件/目录:
$ grep [TAB][TAB]
这将列出所有文件/目录,包括隐藏的文件/目录:
$ 猫[TAB][TAB]
这尝试列出系统中所有可用的命令。按下y将显示命令,按下n将不显示任何内容。
$ complete -c which [TAB][TAB]
Display all 3205 possibilities? (y or n)
修改默认的 bash 完成行为
我们还可以使用 complete shell 内置命令修改给定命令的现有 bash 完成行为。
以下命令用于更改which
命令的行为,不显示任何选项:
$ complete which
$ which [TAB][TAB] # No auto completion option will be shown
以下命令用于更改ls
命令的标签行为,仅显示目录列表作为 bash 完成:
$ ls ~/[TAB][TAB] # Displays directories and file as auto-completion
file1.sh file2.txt dir1/ dir2/ dir3/
$ complete -d ls
$ ls ~/[TAB][TAB] # Displays only directory name as auto-completion
dir1/ dir2/ dir3/
删除 bash 完成规范
我们可以使用 shell 内置的complete
命令和-r
选项删除命令的 bash 完成规范。
语法如下:
complete -r command_name
将以下内容视为示例:
$ complete | grep which # Viewing bash completion specification for which
complete -c which
$ complete -r which # Removed bash completion specification for which
$ complete | grep which # No output
如果没有给出command_name
作为complete -r
的参数,所有完成规范都将被删除:
$ complete -r
$ complete
为自己的应用程序编写 bash 完成
bash-completion 包不为任何外部工具提供自动完成功能。假设我们想创建一个具有多个选项和参数的工具。要为其选项添加 bash 完成功能,我们必须创建自己的 bash 完成文件并将其源化。
例如,软件包管理器如dnf
和apt-get
都有自己的 bash 完成文件,以支持其选项的自动完成:
$ dnf up[TAB][TAB]
update updateinfo update-to upgrade upgrade-to
$ apt-get up[TAB][TAB]
update upgrade
将以下 shell 脚本视为示例:
#!/bin/bash
# Filename: bash_completion_example.sh
# Description: Example demonstrating bash completion feature for command options
function help()
{
echo "Usage: print [OPTIONS] [arg ...]"
echo "-h|--help Display help"
echo "-v|--version Display version of script"
echo "-p|--print Print arguments"
}
function version()
{
echo "Version of shell script application is 0.1"
}
function print()
{
echo "Arguments are: $*"
}
# Parsing command line arguments
while [ "$1" != "" ]
do
case $1 in
-h | --help )
help
exit 1
;;
-v | --version )
version
exit 1
;;
-p | --print )
shift
print $@
exit 1
;;
*)
help
exit 1
esac
done
要了解bash_completion_example.sh
中支持的选项,我们将运行--help
选项:
$ chmod +x bash_completion_example.sh # Adding execute permission to script
$ ./bash_completion_example.sh --help
Usage: print [OPTIONS] [arg ...]
-h|--help Display help
-v|--version Display version of script
-p|--print Print arguments
所以,支持的选项是-h
,--help
,-v
,--version
,-p
和--print
。
要编写 bash 完成,需要以下 bash 内部变量的信息:
Bash 变量 | 描述 |
---|---|
COMP_WORDS |
在命令行上键入的单词数组 |
COMP_CWORD |
包含当前光标位置的单词的索引。 |
COMPREPLY |
一个数组,它保存在按下[TAB][TAB]后显示的完成结果 |
compgen
是一个内置的 shell 命令,根据选项显示可能的完成。它用于在 shell 函数中生成可能的完成。
bash 完成的示例
我们的 shell 脚本bash_completion_example
的 bash 完成文件将如下所示:
# Filename: bash_completion_example
# Description: Bash completion for bash_completion_example.sh
_bash_completion_example()
{
# Declaring local variables
local cur prev opts
# An array variable storing the possible completions
COMPREPLY=()
# Save current word typed on command line in cur variable
cur="${COMP_WORDS[COMP_CWORD]}"
# Saving previous word typed on command line in prev variable
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Save all options provided by application in variable opts
opts="-h -v -p --help --verbose --print"
# Checking "${cur} == -*" means that perform completion only if current
# word starts with a dash (-), which suggest that user is trying to complete an option.
# Variable COMPREPLY contains the match of the current word "${cur}" against the list
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
# Register _bash_completion_example to provide completion
# on running script bash_completion_example.sh
complete -F _bash_completion_example ./bash_completion_example.sh
根据惯例,bash 完成函数名称应以下划线(_)开头,后跟应用程序的名称,即_bash_completion_example
。此外,我们将 bash 变量COMPREPLY
重置为清除任何先前遗留的数据。然后,我们声明并设置cur
变量为命令行的当前单词,prev
变量为命令行中的前一个单词。另一个变量opts
被声明并初始化为应用程序识别的所有选项;在我们的情况下,它们是-h -v -p --help --verbose –print
。条件if [[ ${cur} == -* ]]
检查当前单词是否等于-*
,因为我们的选项以-
开头,后跟任何其他字符。如果为true
,则使用compgen
shell 内置和-W
选项显示所有匹配的选项。
运行创建的 bash 完成。
为了运行创建的 bash 完成,最简单的方法是将其源到source bash_completion_example shell script
,然后运行脚本或命令:
$ source ./bash_completion_example
Now, execute shell script:
$ ./bash_completion_example.sh -[TAB][TAB]
-h --help -p --print -v --verbose
$ ./bash_completion_example.sh --[TAB][TAB]
--help --print --verbose
$ ./bash_completion_example.sh –-p[TAB]
在这里,--p[TAB]
会自动完成为-–print
。
总结
阅读完本章后,你现在应该能够编写一个易于维护和修改的 shell 脚本。现在,你知道如何在自己的脚本中使用现有的 shell 脚本库,使用source
命令。你还熟悉了使用不同的调试技术来修复 shell 脚本中的错误和 bug。你还应该知道如何通过接受命令行参数并为其提供 bash 完成功能来编写脚本。
在下一章中,我们将看到如何查看、更改、创建和删除环境变量,以满足运行我们的应用程序的要求。
第五章:自定义环境
在默认系统中,我们会得到预配置的某些设置。随着时间的推移,我们经常感到需要修改一些默认设置。当我们在 shell 中工作以完成任务时,例如根据应用程序的需求修改环境时,会出现类似的需求。有些功能是如此令人难以抗拒,以至于我们可能每次都需要它们,例如应用程序使用的我们选择的编辑器。在处理重要任务时,可能会忘记几天前使用的命令。在这种情况下,我们会尽快回忆起该命令,以完成工作。如果我们记不起来,就会花费时间和精力在互联网或教科书中搜索确切的命令和语法。
在本章中,我们将看到如何通过添加或更改现有环境变量来修改环境,以满足我们的应用需求。我们还将看到用户如何修改.bashrc
、.bash_profile
和.bash_logout
文件,以使设置更改永久生效。我们将看到如何搜索和修改先前执行的命令的历史记录。我们还将看到如何从单个 shell 运行多个任务并一起管理它们。
本章将详细介绍以下主题:
-
了解默认环境
-
修改 shell 环境
-
使用 bash 启动文件
-
了解你的历史
-
管理任务
了解默认环境
设置适当的环境对于运行进程非常重要。环境由环境变量组成,这些变量可能具有默认值或未设置默认值。通过修改现有环境变量或创建新的环境变量来设置所需的环境。环境变量是导出的变量,可用于当前进程及其子进程。在第一章, 脚本之旅的开始中,我们了解了一些内置 shell 变量,可以将其用作环境变量来设置环境。
查看 shell 环境
要查看 shell 中的当前环境,可以使用printenv
或env
命令。环境变量可能没有值,有单个值,或者有多个值设置。如果存在多个值,每个值都用冒号(:)分隔。
printenv
我们可以使用printenv
来打印与给定环境变量相关联的值。语法如下:
$ printenv [VARIABLE]
考虑以下示例:
$ printenv SHELL # Prints which shell is being used
/bin/bash
$ printenv PWD # Present working directory
/home/foo/Documents
$ printenv HOME # Prints user's home directory
/home/foo
$ printenv PATH # Path where command to be executed is searched
/usr/lib64/qt-3.3/bin:/usr/lib64/ccache:/bin:/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/home/foo
$ printenv USER HOSTNAME # Prints value of both environment variables
foo
localhost
如果未指定VARIABLE
,printenv
将打印所有环境变量,如下所示:
$ printenv # Prints all environment variables available to current shell
环境
我们也可以使用env
命令来查看环境变量,如下所示:
$ env
这将显示为给定 shell 定义的所有环境变量。
注意
要查看特定环境变量的值,也可以使用echo
命令,后跟以美元符号($
)为前缀的环境变量名称。例如,echo $SHELL
。
shell 和环境变量之间的区别
shell 变量和环境变量都是可访问和设置的变量,用于给定的 shell,可能被在该 shell 中运行的应用程序或命令使用。但是,它们之间有一些区别,如下表所示:
Shell 变量 | 环境变量 |
---|---|
本地和导出的变量都是 shell 变量 | 导出的 shell 变量是环境变量 |
使用set builtin 命令可查看 shell 变量的名称和相应值 |
使用env 或printenv 命令可查看环境变量的名称和相应值 |
本地 shell 变量不可供子 shell 使用 | 子 shell 继承父 shell 中存在的所有环境变量 |
通过在等号(=)的右侧用冒号(:)分隔的值在左侧指定变量名称来创建 shell 变量 | 可以通过在现有 shell 变量前加上 export shell 内置命令的前缀,或者在创建新的 shell 变量时创建环境变量 |
修改 shell 环境
当启动新的 shell 时,它具有初始环境设置,将被任何在给定 shell 中执行的应用程序或命令使用。我们现在知道,env
或setenv
shell 内置命令可用于查看为该 shell 设置了哪些环境变量。shell 还提供了修改当前环境的功能。我们还可以通过创建、修改或删除环境变量来修改当前的 bash 环境。
创建环境变量
要在 shell 中创建一个新的环境变量,使用export
shell 内置命令。
例如,我们将创建一个新的环境变量ENV_VAR1
:
$ env | grep ENV_VAR1 # Verifying that ENV_VAR1 doesn't exist
$ export ENV_VAR1='New environment variable'
创建了一个名为ENV_VAR1
的新环境变量。要查看新环境变量,可以调用printenv
或env
命令:
$ env | grep ENV_VAR1
ENV_VAR1=New environment variable
$ printenv ENV_VAR1 # Viewing value of ENV_VAR1 environment variable
New environment variable
我们还可以使用echo
命令来打印环境变量的值:
$ echo $ENV_VAR1 # Printing value of ENV_VAR1 environment variable
New environment variable
本地 shell 变量也可以进一步导出为环境变量。例如,我们将创建ENV_VAR2
和LOCAL_VAR1
变量:
$ ENV_VAR2='Another environment variable'
$ LOCAL_VAR1='Local variable'
$ env | grep ENV_VAR2 # Verifying if ENV_VAR2 is an environment variable
找不到名为ENV_VAR2
的环境变量。这是因为在创建ENV_VAR2
时,它没有被导出。因此,它将被创建为 shell 的本地变量:
$ set | grep ENV_VAR2
ENV_VAR2='Another environment variable'
$ set | grep LOCAL_VAR1
LOCAL_VAR1='Local variable'
现在,要将ENV_VAR2
shell 变量作为环境变量,可以使用 export 命令:
$ export ENV_VAR2 # Becomes environment variable
$ printenv ENV_VAR2 # Checking of ENV_VAR2 is an environment variable
Another environment variable
$ printenv LOCAL_VAR1
变量LOCAL_VAR1
不是环境变量。
环境变量的一个重要特点是它对所有子 shell 都可用。我们可以在以下示例中看到这一点:
$ bash # creating a new bash shell
$ env | grep ENV_VAR2 # Checking if ENV_VAR2 is available in child shell
ENV_VAR2=Another environment variable
$ env | grep ENV_VAR1
ENV_VAR1=New environment variable
$ env | grep LOCAL_VAR1
我们可以看到,从父 shell 继承的环境变量被子 shell 继承,例如ENV_VAR1
,ENV_VAR2
,而本地变量,如LOCAL_VAR1
,仅对创建变量的 shell 可用。
修改环境变量
Shell 提供了灵活性,可以修改任何现有的环境变量。例如,考虑HOME
环境变量。默认情况下,HOME
环境变量包含当前登录用户的主目录的路径:
$ printenv HOME
/home/foo
$ pwd # Checking current working directory
/tmp
$ cd $HOME # Should change directory to /home/foo
$ pwd # Check now current working directory
/home/foo
现在,我们将修改HOME
环境变量的值为/tmp
:
$ HOME=/tmp # Modifying HOME environment variable
$ printenv HOME # Checking value of HOME environment variable
/tmp
$ cd $HOME # Changing directory to what $HOME contains
$ pwd # Checking current working directory
/tmp
我们还可以向环境变量附加一个值。为此,请确保新值用冒号(:)分隔。例如,考虑PATH
环境变量:
$ printenv PATH
usr/lib64/ccache:/bin:/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/home/foo/.local/bin:/home/foo/bin
现在,我们想要将一个新路径添加到PATH
变量中,例如/home/foo/projects/bin
,这样,在查找程序或命令时,shell 也可以搜索指定的路径。要将路径追加到PATH
环境变量中,使用冒号(:)后跟新路径名称:
$ PATH=$PATH:/home/foo/projects/bin # Appends new path
$ printenv PATH
usr/lib64/ccache:/bin:/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/home/foo/.local/bin:/home/foo/bin:/home/foo/projects/bin
我们可以看到新路径已附加到PATH
变量的现有值上。
我们还可以将多个值附加到环境变量;为此,每个值应该用冒号(:)分隔。
例如,我们将向PATH
变量添加两个应用程序路径:
$ PATH=$PATH:/home/foo/project1/bin:PATH:/home/foo/project2/bin
$ printenv PATH
usr/lib64/ccache:/bin:/usr/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/home/foo/.local/bin:/home/foo/bin:/home/foo/projects/bin:/home/foo/project1/bin:PATH:/home/foo/project2/bin
两个新路径/home/foo/project1/bin
和/home/foo/project2/bin
已添加到PATH
变量中。
删除环境变量
我们可以使用unset
shell 内置命令删除或重置环境变量的值。
例如,我们将创建一个名为ENV1
的环境变量:
$ export ENV1='My environment variable'
$ env | grep ENV1 # Checking if ENV1 environment variable exist
ENV1=My environment variable
$ unset ENV1 # Deleting ENV1 environment variable
$ env | grep ENV1
环境变量ENV1
被unset
命令删除。现在,要重置环境变量,将其赋予空值:
$ export ENV2='Another environment variable'
$ env | grep ENV2
ENV2=Another environment variable
$ ENV2='' # Reset ENV2 to blank
$ env | grep ENV2
ENV2=
使用 bash 启动文件
到目前为止,要执行任务或为给定的 shell 设置任何内容,我们必须在 shell 中执行所需的命令。这种方法的主要局限性之一是相同的配置不会在新的 shell 中可用。在许多情况下,用户可能希望每当启动新的 shell 时,而不是使用新的自定义配置,而是使用默认配置之上的新的自定义配置。对于自定义 bash,用户的主目录中默认执行的三个文件是bashrc
、.bash_profile
和.bash_logout
。
.bashrc
在图形系统中,用户主要使用非登录 shell。要运行非登录 shell,我们不需要登录凭据。在图形系统中启动 shell 提供了一个非登录 shell。当 bash 以非登录模式调用时,会调用~/.bashrc
文件,并执行其中可用的配置,并将其应用于任何启动的 bash shell。需要在登录和非登录 shell 中都需要的设置保存在~/.bashrc
文件中。
例如,在 Fedora 22 系统上,默认的~/.bashrc
文件如下:
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=
# User specific aliases and functions
在~/.bashrc
中进行的任何添加只会反映到当前用户的 bash shell。我们可以看到.bashrc
文件还检查etc/bashrc
文件是否可用。如果可用,也会执行该文件。/etc/bashrc
文件包含应用于所有用户的 bash shell 的系统范围配置。如果需要应用到所有用户的 bash shell 的任何配置,系统管理员可以修改/etc/bashrc
文件。
/etc/bashrc
文件还查看了/etc/profile.d
中可用的脚本文件,可以通过/etc/bashrc
文件中的以下代码片段确认:
for i in /etc/profile.d/*.sh; do
if [ -r "$i" ]; then
if [ "$PS1" ]; then
. "$i"
以下示例显示了修改后的.bashrc
文件。将此文件命名为custom_bashrc
:
# custom_bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=
# User added settings
# Adding aliases
alias rm='rm -i' # Prompt before every removal
alias cp='cp -i' # Prompts before overwrite
alias df='df -h' # Prints size in human readable format
alias ll='ls -l' # Long listing of file
# Exporting environment variables
# Setting and exporting LD_LIBRARY_PATH variable
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/libs
# Setting number of commands saved in history file to 10000
export HISTFILESIZE=10000
# Defining functions
# Function to calculate size of current directory
function current_directory_size()
{
echo -n "Current directory is $PWD with total used space "
du -chs $PWD 2> /dev/null | grep total | cut -f1
}
LD_LIBRARY_PATH
环境变量用于为运行时共享库加载器(ld.so
)提供额外的目录,以便在搜索共享库时查找。您可以在tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html
了解更多关于共享库的信息。
在修改之前,请备份您的原始~/.bashrc
文件:
$ cp ~/.bashrc ~/.bashrc.bak
现在,将custom_bashrc
文件复制到~/.bashrc
中:
$ cp custom_bashrc ~/.bashrc
要应用修改后的设置,请打开一个新的 bash shell。要在相同的 bash shell 中应用新的.bashrc
,您可以将其源到新的~/.bashrc
文件中:
$ source ~/.bashrc
我们可以检查新的设置是否可用:
$ ll /home # Using alias ll which we created
total 24
drwx------. 2 root root 16384 Jun 11 00:46 lost+found
drwx--x---+ 41 foo foo 4096 Aug 3 12:57 foo
$ alias # To view aliases
alias cp='cp -i'
alias df='df -h'
alias ll='ls -l'
alias ls='ls --color=auto'
alias rm='rm -i'
alias vi='vim'
alias
命令显示我们在.bashrc
中添加的别名,即rm
、cp
、df
和ll
。
现在,调用我们在.bashrc
中添加的current_directory_size()
函数:
$ cd ~ # cd to user's home directory
$ current_directory_size
Current directory is /home/foo with total used space 97G
$ cd /tmp
$ current_directory_size
Current directory is /tmp with total used space 48K
确保将我们在本示例开始时创建的原始.bashrc
文件移回去,并将其源到其中,以便在当前 shell 会话中反映设置。如果您不希望在执行前面示例时进行的任何配置更改,则需要这样做:
$ mv ~/.bashrc.bak ~/.bashrc
$ source ~/.bashrc
注意
当 bash 作为非登录 shell 调用时,它会加载~/.bashrc
、/etc/bashrc
和/etc/profile.d/*.sh
文件中可用的配置。
.bash_profile
在非图形系统中,成功登录后,用户会获得一个 shell。这样的 shell 称为登录 shell。当 bash 作为登录 shell 调用时,首先执行/etc/profile
文件;这会运行/etc/profile.d/
中可用的脚本。/etc/profile
中的以下代码片段也提到了这一点:
for i in /etc/profile.d/*.sh ; do
if [ -r "$i" ]; then
if [ "${-#*i}" != "$-" ]; then
. "$i"
else
这些是应用于任何用户登录 shell 的全局设置。此外,~/.bash_profile
会为登录 shell 执行。在 Fedora 22 系统上,默认的~/.bash_profile
文件内容如下:
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH
从内容中,我们可以看到它在用户的主目录中查找.bashrc
文件。如果主目录中有.bashrc
文件,则会执行它。我们还知道~/.bashrc
文件也会执行/etc/bashrc
文件。接下来,我们看到.bash_profile
将PATH
变量附加到$HOME/.local/bin
和$HOME/bin
值。此外,修改后的PATH
变量被导出为环境变量。
用户可以根据自己的定制配置需求修改~/.bash_profile
文件,例如默认 shell、登录 shell 的编辑器等。
以下示例包含了.bash_profile
中的修改配置。我们将使用bash_profile
作为文件名:
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH
# Added configuration by us
# Setting user's default editor
EDITOR=/usr/bin/vim
# Show a welcome message to user with some useful information
echo "Welcome 'whoami'"
echo "You are using $SHELL as your shell"
echo "You are running 'uname ' release 'uname -r'"
echo "The machine architecture is 'uname -m'"
echo "$EDITOR will be used as default editor"
echo "Have a great time here!"
在我们添加的配置注释之后进行更改。在应用新配置到~/.bash_profile
之前,我们将首先备份原始文件。这将帮助我们恢复.bash_profile
文件的原始内容:
$ cp ~/.bash_profile ~/.bash_profile.bak
在home
目录中将创建一个新文件.bash_profile.bak
。现在,我们将复制我们的新配置到~/.bash_profile
:
$ cp bash_profile ~/.bash_profile
要在登录 shell 中看到反映的更改,我们可以以非图形界面登录,或者只需执行ssh
到同一台机器上运行登录 shell。SSH(Secure Shell)是一种加密网络协议,用于以安全方式在远程计算机上启动基于文本的 shell 会话。在 UNIX 和基于 Linux 的系统中,可以使用ssh
命令进行对本地或远程机器的 SSH。ssh
的man
页面(man ssh
)显示了它提供的所有功能。要在同一台机器上进行远程登录,我们可以运行ssh username@localhost
:
$ ssh foo@localhost # foo is the username of user
Last login: Sun Aug 2 20:47:46 2015 from 127.0.0.1
Welcome foo
You are using /bin/bash as your shell
You are running Linux release 4.1.3-200.fc22.x86_64
The machine architecture is x86_64
/usr/bin/vim will be used as default editor
Have a great time here!
我们可以看到我们添加的所有细节都打印在登录 shell 中。快速测试我们的新.bash_profile
的另一种方法是通过对其进行源操作:
$ source ~/.bash_profile
Welcome foo
You are using /bin/bash as your shell
You are running Linux release 4.1.3-200.fc22.x86_64
The machine architecture is x86_64
/usr/bin/vim will be used as default editor
Have a great time here!
要重置~/.bash_profile
文件中的更改,从我们在本示例开始时创建的~/.bash_profile.bak
文件中复制,并对其进行源操作,以便在当前 shell 中反映更改:
$ mv ~/.bash_profile.bak ~/.bash_profile
$ source ~/.bash_profile
注意
当 bash 作为登录 shell 调用时,它会加载/etc/profile
、/etc/profile.d/*.sh
、~/.bash_profile
、.~/.bashrc
和~/etc/bashrc
文件中可用的配置。
.bash_logout
在用户的主目录中存在的.bash_logout
文件在每次登录 shell 退出时都会执行。当用户远程登录或使用非图形界面时,这很有用。用户可以添加在从系统注销之前执行的清理任务。清理任务可能包括删除创建的临时文件、清除环境变量、注销重要数据、存档或加密某些任务、上传到 Web 等。
了解您的历史记录
Shell 提供了一个有趣的功能,允许您查看以前在 shell 中执行的所有命令的历史记录。经常发生我们忘记了前一天键入的命令来执行任务。我们可能能够回忆起确切的语法,也可能不行,但很方便的是我们可以参考 shell 保存的历史记录。
控制历史记录的 shell 变量
有一些 shell 变量可以更改用户可以看到的历史记录的内容和数量。这些 shell 变量在下表中提到:
名称 | 值 |
---|---|
HISTFILE | 默认情况下历史记录将保存在的文件名 |
HISTFILESIZE | 历史文件中要保留的命令数 |
HISTSIZE | 当前会话中要存储的历史记录数量 |
HISTCONTROL | 以冒号分隔的值列表,控制如何保存命令在历史列表中 |
HISTCONTROL
shell 变量的值可以是:
值 | 描述 |
---|---|
ignorespace | 以空格开头的行,不保存在历史记录列表中 |
ignoredups | 不保存与先前保存的历史记录列表匹配的行 |
ignoreboth | 应用 ignorespace 和 ignoredups |
erasedups | 在将其保存到历史文件之前,删除与当前行匹配的历史中的所有先前行 |
让我们看看这些 shell 变量可能包含什么值:
$ echo $HISTFILE
/home/foo/.bash_history
$ echo $HISTFILESIZE
1000
$ echo $HISTSIZE
1000
$ echo $HISTCONTROL
ignoredups
从获得的值中,我们可以看到默认历史记录保存在用户home
目录的.bash_history
文件中,最大历史命令行保存为 1000。此外,已经存在于先前历史行中的任何重复历史都不会保存。
history 内置命令
Shell 提供了history
内置命令,以便用户了解到目前为止执行的命令历史。
在没有任何选项的情况下运行历史记录,会将所有先前输入的命令打印到stdout
。命令序列按从顶部到底部的顺序提供,从最旧到最新:
$ history # Prints all commands typed previously on stdout
$ history | tail -n10 # Prints last 10 commands executed
以下表格解释了history
shell 内置命令的可用选项:
选项 | 描述 |
---|---|
-a | 立即将新的历史行追加到历史记录中 |
-c | 清除当前列表中的历史记录 |
-d offset | 从指定的偏移量删除历史记录 |
-r | 将保存的历史内容追加到当前列表 |
-w | 在覆盖现有保存的历史内容后,将当前历史列表写入历史文件 |
要查看最后执行的五个命令,我们还可以执行以下命令:
$ history 5
769 cd /tmp/
770 vi hello
771 cd ~
772 vi .bashrc
773 history 5
我们将发现,所有执行的命令都与历史文件中的给定字符串匹配。例如,搜索其中包含set
字符串的命令:
$ history | grep set
555 man setenv
600 set | grep ENV_VAR2
601 unset ENV_VAR2
602 set | grep ENV_VAR2
603 unset -u ENV_VAR2
604 set -u ENV_VAR2
605 set | grep ENV_VAR2
737 set |grep HIST
778 history | grep set
要清除所有保存的命令历史记录并将当前列表中的历史追加到历史中,我们可以执行以下操作(如果不想丢失保存的命令历史,请不要运行以下命令):
$ history -c # Clears history from current list
$ history -w # Overwrite history file and writes current list which is empty
修改默认历史记录行为
默认情况下,shell 为管理历史记录设置了一些值。在前一节中,我们看到历史文件中将存储最多 1000 行历史记录。如果用户大部分时间都在 shell 中工作,他可能在一两天内使用了 1000 条或更多命令。在这种情况下,如果他十天前输入了一个命令,他将无法查看历史记录。根据个人用例,用户可以修改要存储在历史文件中的行数。
执行以下命令将将历史文件的最大行数设置为100000
:
$ HISTFILESIZE=100000
同样,我们可以更改历史文件应保存的位置。我们看到,默认情况下,它保存在home
目录中的.bash_history
文件中。我们可以修改HISTFILE
shell 变量,并将其设置为我们想要保存命令历史的任何位置:
$ HISTFILE=~/customized_history_path
现在,执行的命令历史将保存在home
目录中的customized_history_path
文件中,而不是~/.bash_history
文件中。
要使这些更改反映到用户启动的所有 shell 和所有会话中,将这些修改添加到~/.bashrc
文件中。
查看历史记录的便捷快捷键
根据用户的历史记录大小设置,历史记录中可用的命令数量可能很大。如果用户想要查找特定命令,他或她将不得不查看整个历史记录,这有时可能会很麻烦。Shell 提供了一些快捷方式,以帮助我们在历史记录中找到先前执行的特定命令。了解这些快捷方式可以节省在历史记录中查找先前执行的命令的时间。
[Ctrl + r]
在 shell 中工作时,[Ctrl + r]快捷键允许您在历史记录中搜索命令。按下[Ctrl + r]后开始输入命令;shell 会显示与输入的命令子字符串匹配的完整命令。要向前移动到下一个匹配项,再次在键盘上输入[Ctrl + r],依此类推:
$ [ctrl + r]
(reverse-i-search)'his': man history
我们可以看到,从历史记录man history
中建议输入his
。
上下箭头键
键盘上的上下箭头键可用于在用户先前执行的命令历史记录中后退和前进。例如,要获取上一个命令,请按一次上箭头键。要进一步后退,请再次按上箭头键,依此类推。此外,要在历史记录中前进,请使用下箭头键。
!!
快捷方式!!
可用于重新执行 shell 中执行的最后一个命令:
$ ls /home/
lost+found foo
$ !!
ls /home/
lost+found foo
!(search_string)
这个快捷方式执行最后一个以search_string
开头的命令:
$ !l
ls /home/
lost+found skumari
$ !his
history 12
!?(search_string)
这个快捷方式执行最后一个包含子字符串search_string
的命令:
$ !?h
ls /home/
lost+found skumari
任务管理
当应用程序运行时,可能会长时间运行,或者一直运行直到计算机关闭。在 shell 中运行应用程序时,我们知道只有当在 shell 中运行的程序成功完成或由于某些错误终止时,shell 提示符才会返回。除非我们得到 shell 提示符返回,否则我们无法在同一个 shell 中运行另一个命令。我们甚至不能关闭该 shell,因为这将关闭正在运行的进程。
此外,要运行另一个应用程序,我们将不得不在新的终端中打开另一个 shell,然后运行它。如果我们必须运行很多任务,管理起来可能会变得困难和繁琐。Shell 提供了在后台运行、挂起、终止或移回前台的方法。
在后台运行任务
可以通过在命令末尾添加&来在 shell 中将任务作为后台启动。
例如,我们想在整个文件系统中搜索一个字符串。根据文件系统的大小和文件数量,可能需要很长时间。我们可以调用grep
命令来搜索字符串并将结果保存在文件中。Linux 中的文件系统层次结构从根目录('/')开始。
$ grep -R "search Text" / 2>/dev/null > out1.txt &
[1] 8871
$
在这里,grep
在整个文件系统中搜索字符串,将任何错误消息发送到/dev/null
,并将搜索结果保存到out1.txt
文件中。在末尾的&将整个作业发送到后台,打印启动任务的 PID,并返回 shell 提示符。
现在,我们可以在同一个打开的 shell 中做其他工作并执行其他任务。
将正在运行的任务发送到后台
通常我们在 shell 中正常运行任务,即作为前台任务,但后来我们想将其移至后台。首先通过[Ctrl + z]暂停当前任务,然后使用bg
将任务移至后台。
考虑最后一次文本搜索作为一个例子。我们正常地开始搜索如下:
$ grep -R "search Text" / 2>/dev/null > out2.txt
我们不会看到 shell 上发生任何事情,我们只会等待 shell 提示符返回。或者,我们可以使用[Ctrl + z]暂停运行的作业:
[ctrl + z]
[2]+ Stopped grep -R "search Text" / 2> /dev/null > out2.txt
然后,要将挂起的任务发送到后台继续运行,请使用bg
命令:
$ bg
[2]+ grep -R "search Text" / 2> /dev/null > out2.txt
列出后台任务
要查看当前 shell 中正在后台运行或挂起的任务,使用内置jobs
shell 如下:
$ jobs
[1]- Running grep -R "search Text" / 2> /dev/null > out1.txt &
[2]+ Running grep -R "search Text" / 2> /dev/null > out2.txt &
这里,索引[1]和[2]是作业编号。
字符'+'标识将由fg
或bg
命令用作默认值的作业,字符'-'标识当前默认作业退出或终止后将成为默认作业的作业。
创建另一个任务并使用以下命令将其挂起:
$ grep -R "search Text" / 2>/dev/null > out3.txt
[ctrl + z]
[3]+ Stopped grep -R "search Text" / 2> /dev/null > out3.txt
$ jobs
[1] Running grep -R "search Text" / 2> /dev/null > out1.txt &
[2]- Running grep -R "search Text" / 2> /dev/null > out2.txt &
[3]+ Stopped grep-R "search Text" / 2> /dev/null > out3.txt
要查看所有后台和挂起任务的 PID,我们可以使用-p
选项:
$ jobs -p
8871
8873
8874
作业的 PID 是按顺序排列的。要查看只在后台运行的任务,使用-r
选项如下:
$ jobs -r
[1] Running grep -R "search Text" / 2> /dev/null > out1.txt &
[2]- Running grep -R "search Text" / 2> /dev/null > out2.txt &
要查看只挂起的任务,使用-s
选项如下:
$ jobs -s
[3]+ Stopped grep-R "search Text" / 2> /dev/null > out3.txt
要查看特定索引作业,请使用带有jobs
命令的索引号:
$ jobs 2
[2]- Running grep -R "search Text" / 2> /dev/null > out2.txt &
将任务移动到前台
我们可以使用 shell 内置命令fg
将后台或挂起的任务移动到前台:
$ jobs # Listing background and suspended tasks
[1] Running grep -R "search Text" / 2> /dev/null > out1.txt &
[2]- Running grep -R "search Text" / 2> /dev/null > out2.txt &
[3]+ Stopped grep-R "search Text" / 2> /dev/null > out3.txt
字符'+'在作业索引3
中被提到。这意味着运行fg
命令将在前台运行第三个作业:
$ fg
$ grep -R "search Text" / 2> /dev/null > out3.txt
[ctrl + z]
[3]+ Stopped grep -R "search Text" / 2> /dev/null > out3.txt
以下命令暂停第三个任务:
$ jobs
[1] Running grep -R "search Text" / 2> /dev/null > out1.txt &
[2]- Running grep -R "search Text" / 2> /dev/null > out2.txt &
[3]+ Stopped grep-R "search Text" / 2> /dev/null > out3.txt
要将特定作业移到前台,请使用带有任务索引号的fg
:
$ fg 1 # Moving first tasks to foreground
$ grep -R "search Text" / 2> /dev/null > out1.txt
[ctrl + z]
[1]+ Stopped grep -R "search Text" / 2> /dev/null > out1.txt
终止任务
如果不再需要,我们也可以删除运行中或挂起的任务。这可以通过使用disown
shell 内置命令来完成:
$ jobs # List running or suspended tasks in current shell
[1]+ Stopped grep -R "search Text" / 2> /dev/null > out1.txt
[2] Running grep -R "search Text" / 2> /dev/null > out2.txt &
[3]- Stopped grep -R "search Text" / 2> /dev/null > out3.txt
使用disown
而不带任何选项,会删除具有字符'+
'的任务:
$ disown
bash: warning: deleting stopped job 1 with process group 8871
$ jobs # Listing available jobs
[2]- Running grep -R "search Text" / 2> /dev/null > out2.txt &
[3]+ Stopped grep -R "search Text" / 2> /dev/null > out3.txt
要删除运行中的任务,使用-r
选项:
$ disown -r
jobs
[3]- Stopped grep -R "search Text" / 2> /dev/null > out3.txt
要删除所有任务,使用-a
选项如下:
$ disown -a # Gives warning for deleting a suspended task
bash: warning: deleting stopped job 3 with process group 8874
$ jobs
jobs
的输出什么也不显示,因为所有挂起和运行中的任务都被-a
选项删除了。
总结
阅读完本章后,您现在知道如何在 shell 中创建和修改环境变量。您还知道.bashrc
和.bash_profile
如何帮助永久地为用户的所有会话进行更改。您学会了如何搜索我们先前执行的命令的历史记录,以及如何使用fg
和bg
shell 内置命令在 shell 中运行和管理不同的任务。
在下一章中,我们将看到在基于 Linux 的系统上有哪些重要类型的文件,以及可以对它们执行哪些操作以获得有意义的结果。
第六章:处理文件
为了简单起见,UNIX 和基于 Linux 的操作系统中的所有内容都被视为文件。文件系统中的文件以分层树状结构排列,树的根由'/'(斜杠)表示。树的节点可以是目录或文件,其中目录也是一种特殊类型的文件,其中包含 inode 号和相应的文件名条目列表。inode 号是 inode 表中的条目,包含与文件相关的元数据信息。
在本章中,我们将更详细地了解重要和常用的文件类型。我们将看到如何创建、修改和执行文件的其他有用操作。我们还将看到如何监视进程或用户打开的文件列表。
本章将详细介绍以下主题:
-
执行基本文件操作
-
移动和复制文件
-
比较文件
-
查找文件
-
文件的链接
-
特殊文件
-
临时文件
-
权限和所有权
-
获取打开文件的列表
-
配置文件
执行基本文件操作
最常用的文件是常规文件和目录。在以下子节中,我们将看到基本文件操作。
创建文件
我们可以使用不同的 shell 命令在 shell 中创建常规文件和目录。
目录文件
目录是一种特殊类型的文件,其中包含文件名列表和相应的 inode 号。它充当容器或文件夹,用于保存文件和目录。
要通过 shell 创建新目录,我们可以使用mkdir
命令:
$ mkdir dir1
我们还可以将多个目录名称作为参数提供给mkdir
命令,如下所示:
$ mkdir dir2 dir3 dir4 # Creates multiple directories
如果指定的路径名不存在,我们可以使用mkdir
中的-p
选项创建父目录。这是通过mkdir
中的-p
选项完成的:
$ mkdir -p /tmp/dir1/dir2/dir3
在这里,如果dir1
和dir2
是dir3
的父目录且尚不存在,则-p
选项将首先创建dir1
目录,然后在dir1
内创建dir2
子目录,最后在dir2
内创建dir3
子目录。
常规文件
一般来说,文本和二进制文件被称为常规文件。在 shell 中,可以通过多种方式创建常规文件。以下部分提到了其中一些。
Touch 命令
也可以使用touch
命令创建新的常规文件。它主要用于修改现有文件的时间戳,但如果文件不存在,将创建一个新文件:
$ touch newfile.txt # A new empty file newfile.txt gets created
$ test -f newfile.txt && echo File exists # Check if file exists
File exists
使用命令行编辑器
我们可以打开任何命令行编辑器;例如,在 shell 中使用vi/vim
、emacs、nano,编写内容,并将内容保存在文件中。
现在,我们将使用vi
编辑器创建并编写文本:
$ vi foo.txt # Opens vi editor to write content
按下I键进入 vi 的INSERT
模式,然后按照以下截图中显示的文本输入:
在写完文本后,按下Esc键,然后输入:wq
命令保存并退出 vi 编辑器。要详细了解vi/vim
,请参考其man
页面或在线文档(www.vim.org/docs.php
):
使用 cat 命令
我们甚至可以使用cat
命令将内容写入现有或新的常规文件,如下所示:
$ cat > newfile1.txt
We are using cat command
to create a new file and write into
it
[Ctrl + d] # Press Ctrl + d to save and exit
$ cat newfile1.txt # See content of file
We are using cat command
to create a new file and write into
it
通过使用>>
运算符而不是>
,我们可以追加而不是覆盖文件的内容。
重定向命令的输出
在 bash 或脚本中执行命令时,我们可以将结果重定向到现有文件或新文件中:
$ ls -l /home > newfile2.txt #File gets created containing command output
$ cat newfile2.txt
total 24
drwx------. 2 root root 16384 Jun 11 00:46 lost+found
drwx—x---+ 41 foo foo 4096 Aug 22 12:19 foo
修改文件
要在 shell 中修改常规文件的内容,打开编辑器中的文件,进行所需的更改,然后保存并退出。我们还可以使用>>
运算符将命令的输出追加到指定的文件中:
Command >> file.txt
例如,我们将保存/home
的ls
输出到ls_output.txt
文件中:
$ ls /home/ >> ls_output.txt
$ cat ls_output.txt # Viewing content of file
lost+found
foo
现在,我们将追加另一个目录/home/foo/
的ls
输出如下:
$ ls /home/foo >> ls_output.txt
lost+found
foo
Desktop
Documents
Downloads
Pictures
我们看到ls_output.txt
文件通过追加ls
命令的内容而被修改。
查看文件
要查看常规文件的内容,我们可以简单地在编辑器中打开文件,如 vi/vim,emacs 和 nano。我们还可以使用cat
,less
和more
命令来查看文件的内容。
要查看目录的内容,我们使用ls
命令:
$ ls /home/
lost+found foo
要递归查看目录的内容,请使用带有-R
或--recursive
选项的ls
。
使用 cat 查看内容
我们可以使用cat
命令查看文件的内容如下:
$ cat newfile1.txt
We are using cat command
to create a new file and write into
it
$ cat -n newfile1.txt # Display line number as well
1 We are using cat command
2 to create a new file and write into
3 it
more 和 less
more
和less
命令非常有用,方便查看当前终端上无法容纳的大文件。
more
命令以页面格式显示文件的内容,我们可以向上和向下滚动以查看文件的其余内容:
$ more /usr/share/dict/words
将文件路径作为参数传递给more
命令。在上面的示例中,它将显示/usr/share/dict/
目录中可用的文件单词的内容。
键s用于向前跳过k
行文本。键f用于向前跳过 k 屏幕文本。键b用于向后跳过 k 屏幕文本。
less
命令更受欢迎,被广泛用于查看大文件的内容。使用less
命令的优点之一是它不会在开始时加载整个文件,因此查看大文件的内容更快。
使用less
的用法与more
命令非常相似:
$ less /usr/share/dict/words
导航使用less
命令要容易得多。它还有更多选项来自定义文件内容的过滤视图。
如果没有提供输入文件,more
和less
命令可以从stdin
接收输入。使用管道('|
')从stdin
提供输入:
$ cat /usr/share/dict/words | more # cat output redirected to more
$ grep ^.{3}$ /usr/share/dict/words | less # Matches all 3 character words
查看more
和less
的man
页面以获取详细用法。
注意
由于不同的实现,more
命令的行为可能因不同系统而异。
删除文件
如果不再需要,我们也可以删除常规文件和目录。
删除常规文件
要删除常规文件,我们在 shell 中使用rm
命令。
如果文件存在,则rm
命令删除文件,否则会在stdout
上打印错误:
$ rm newfile1.txt # Deletes if file exists
$ rm newfile1.txt # Prints error message if file doesn't exist
rm: cannot remove 'newfile1.txt': No such file or directory
要忽略错误消息,可以使用rm
与-f
选项:
$ rm -f newfile1.txt
$ rm -i newfile.txt # Interactive deletion of file
rm: remove regular empty file 'newfile.txt'?
输入键y删除文件,n跳过删除文件。
删除目录
要删除目录,我们可以使用rmdir
和rm
命令。我们将考虑在文件
创建子主题下创建的目录
文件中创建的目录:
$ rmdir dir2/ # Deletes directory dir2
$ rmdir dir1/ # Fails to delete because of non-empty directory
rmdir: failed to remove 'dir1/': Directory not empty
要删除非空目录,首先删除内容,然后删除目录。我们还可以使用rm
来删除空目录或非空目录。
-d
选项如下删除空目录:
$ ls dir3/ # Directory dir3 is empty
$ rm -d dir3/ # Empty diretcory dir3 gets deleted
$ ls dir1/ # Diretcory dir1 is not empty
dir2
$ rm -d dir1/ # Fails to delete non-empty directory dir1
rm: cannot remove 'dir1': Directory not empty
选项-r
,-R
或--recursive
递归地删除目录及其内容:
$ rm -ri dir1/ # Asks to remove directory dir1 recursively
rm: descend into directory 'dir1'? Y
输入y确认应删除dir1
。
注意
小心使用rm
选项-r
。如果可能的话,使用-i
选项以避免意外删除整个目录的内容。
移动和复制文件
我们经常需要复制或移动文件到另一个位置,以便根据需要整理文件。我们还可以将计算机数据复制到本地或远程可用的外部驱动器或另一台计算机,以便备份重要数据。
移动文件
移动常规文件和目录在我们想要在新位置保留数据的确切副本时非常有用。mv
命令用于将文件从一个位置移动到另一个位置。
使用mv
命令的语法如下:
mv [option] source... destination
这里,source
是要移动的文件或目录。可以指定多个源文件,destination
是应将文件和目录移动到的位置。
mv
命令的一些重要选项在下表中解释:
选项 | 描述 |
---|---|
-n |
不覆盖现有文件 |
-i |
在覆盖现有文件之前提示 |
-f |
在覆盖现有文件时不提示 |
-u |
仅在源文件较新或目标文件丢失时才移动源文件 |
-v |
打印正在移动的文件的名称 |
将目录移动到新位置
要将目录从一个位置移动到另一个位置,请执行以下命令:
$ mkdir ~/test_dir1 # Directory test_dir1 created in home directory
$ mv ~/test_dir1/ /tmp # moving directory to /tmp
test_dir1
目录已经移动到了/tmp
,现在主目录中没有test_dir1
的副本了。
现在,我们将在用户的主目录中再次创建一个名为test_dir1
的目录:
$ mkdir ~/test_dir1 # Directory test_dir1 created in home directory
尝试使用-i
选项再次将test_dir1
移动到/tmp
:
$ mv -i ~/test_dir1/ /tmp
mv: overwrite '/tmp/test_dir1'?
我们可以看到-i
选项明确询问用户是否要用新目录覆盖现有目录。
注意
使用mv
命令和-i
选项来避免意外覆盖文件。
重命名文件
我们也可以使用mv
命令来重命名文件。例如,我们在/tmp
目录中有test_dir1
目录。现在,我们想将其重命名为test_dir
。我们可以执行以下命令:
$ mv /tmp/test_dir1/ /tmp/test_dir # directory got renamed to test_dir
复制文件
创建文件的副本是一个非常常见的操作,可以在本地或远程系统上执行。
在本地复制文件
要在本地机器上复制文件,使用cp
命令。
使用cp
命令的语法如下:
cp [option] source … destination
在这里,source
可以是单个文件、多个文件或目录,而destination
如果source
是单个文件,则可以是文件。否则,destination
将是一个目录。
cp
命令的一些重要选项如下:
选项 | 描述 |
---|---|
-f |
在覆盖现有文件时不提示 |
-i |
在覆盖现有文件之前提示 |
-R |
递归复制目录 |
-u |
仅在源文件较新或目标文件丢失时才复制源文件 |
-p |
保留原始文件的属性 |
-v |
显示正在复制的文件的详细信息 |
将文件复制到另一个位置
要将文件复制到另一个位置,请执行以下命令:
$ touch ~/copy_file.txt # Creating a file
$ cp ~/copy_file.txt /tmp/ # Copying file to /tmp
现在,copy_file.txt
文件有两个副本,一个在用户的主目录,一个在/tmp
目录。
要复制目录,我们使用带有-R
选项的cp
:
$ mkdir ~/test_dir2 # Creating a test diretcory
$
cp -R ~/test_dir2 /tmp/
test_dir2
目录以及目录中的所有内容都被复制到了/tmp
。
远程复制文件
要在远程机器上复制文件,使用scp
命令。它在网络上的主机之间复制文件。scp
命令使用ssh
来验证目标主机并传输数据。
scp
的简单语法如下:
scp [option] user1@host1:source user2@host2:destination
在user1@host1:source
中,user1
是要复制文件的源用户名,host1
是主机名或 IP 地址;source
可以是要复制的文件或目录。
在user2@host2:destination
中,user2
是目标主机的用户名,文件应该被复制到该主机,host2
是主机名或 IP 地址;destination
可以是要复制到的文件或目录。如果没有指定目的地,将在目标主机的主目录中进行复制。
如果没有提供远程源和目的地,将在本地进行复制。
讨论了scp
的一些重要选项如下表所示:
选项 | 描述 |
---|---|
-C |
在网络上传输数据时启用压缩 |
-l limit |
限制以 Kbit/s 指定的带宽使用 |
-p |
保留原始文件的属性 |
-q |
不在stdout 上打印任何进度输出 |
-r |
递归复制目录 |
-v |
复制过程中显示详细信息 |
将文件复制到远程服务器
要将文件复制到远程服务器,非常重要的是服务器上已经运行了ssh
服务器。如果没有,请确保启动ssh
服务器。要复制文件,请使用以下scp
命令:
$ scp -r ~/test_dir2/ foo@localhost:/tmp/test_dir2/
在这里,我们已经将一个副本复制到了本地机器。所以使用的主机名是localhost
。现在,在/tmp/test_dir2/
内有另一个目录test_dir2
:
$ ls -l /tmp/test_dir2
total 0
drwxrwxr-x. 2 foo foo 40 Aug 25 00:44 test_dir2
比较文件
比较两个相似文件之间的差异是有意义的,以了解这两个文件之间存在哪些差异。例如,比较在两组数据上运行的命令获得的结果。另一个例子可以是比较脚本文件的旧版本和新版本,以了解脚本中进行了哪些修改。Shell 提供了用于文件比较的diff
命令。
使用 diff 进行文件比较
diff
命令用于逐行比较文件。使用diff
命令的语法如下:
diff [option] file1 file2
其中,file1
和file2
是要比较的文件。
diff
命令的选项在下表中解释:
选项 | 描述 |
---|---|
-q |
仅在文件不同时打印 |
-s |
如果两个文件相同,则在stdout 上打印消息 |
-y |
侧边显示diff 结果 |
-i |
对文件内容进行不区分大小写的比较 |
-b |
忽略空格数的更改 |
-u NUM |
输出NUM (默认 3)行统一上下文 |
-a |
在比较时将文件视为文本文件 |
例子
diff
命令显示了两个文件之间添加、删除和修改行的比较结果。
我们将以comparison_file1.txt
和comparison_file2.txt
文本文件为例:
$ cat comparison_file1.txt # Viewing content of file
This is a comparison example.
This line should be removed.
We have added multiple consecutive blank spaces.
THIS line CONTAINS both CAPITAL and small letters
$ cat comparison_file2.txt # Viewing content of file
This is a comparison example.
We have added multiple consecutive blank spaces.
this line contains both CAPITAL and small letters
Addition of a line
现在,我们将比较comparison_file1.txt
和comparison_file2.txt
文件:
$ diff comparison_file1.txt comparison_file2.txt
2,5c2,4
<
< This line should be removed.
< We have added multiple consecutive blank spaces.
< THIS line CONTAINS both CAPITAL and small letters
---
> We have added multiple consecutive blank spaces.
> this line contains both CAPITAL and small letters
> Addition of a line
在这里,<
(小于)表示删除的行,>
(大于)表示添加的行。
使用-u
选项使diff
输出更易读,如下所示:
$ diff -u comparison_file1.txt comparison_file2.txt
--- comparison_file1.txt 2015-08-23 16:47:28.360766660 +0530
+++ comparison_file2.txt 2015-08-23 16:40:01.629441762 +0530
@@ -1,6 +1,5 @@
This is a comparison example.
-
-This line should be removed.
-We have added multiple consecutive blank spaces.
-THIS line CONTAINS both CAPITAL and small letters
+We have added multiple consecutive blank spaces.
+this line contains both CAPITAL and small letters
+Addition of a line
在这里,'-
'告诉旧文件(comparison_file1.txt
)中可用的行,但在新文件(comparison_file2.txt
)中不再存在。
'+
'表示在新文件(comparison_file2.txt
)中添加的行。
我们甚至可以使用–i
选项对内容进行不区分大小写的比较:
$ diff -i comparison_file1.txt comparison_file2.txt
2,4c2
<
< This line should be removed.
< We have added multiple consecutive blank spaces.
---
> We have added multiple consecutive blank spaces.
5a4
> Addition of a line
要忽略多个空格,请使用diff
并使用-b
选项:
$ diff -bi comparison_file1.txt comparison_file2.txt
2,3d1
<
< This line should be removed.
5a4
> Addition of a line
查找文件
在文件系统中,有大量的文件可用。有时,还会连接外部设备,这些设备可能也包含大量的文件。想象一下系统中有数百万甚至数十亿个文件,我们需要在其中搜索特定的文件或文件模式。如果文件数量在 10 到 100 之间,手动搜索文件是可能的,但在数百万个文件中几乎是不可能的。为了解决这个问题,UNIX 和 Linux 提供了find
命令。这是一个非常有用的用于在计算机中搜索文件的命令。
使用find
命令的语法如下:
find search_path [option]
在search_path
中,指定find
应搜索file_search_pattern
的路径。
以下表中提到了一些重要的选项:
选项 | 描述 |
---|---|
-P | 不要遵循符号链接。这是默认行为 |
-L | 在搜索时遵循符号链接 |
-exec cmd ; | 执行作为-exec 参数传递的命令 cmd |
-mount | 不在其他文件系统中搜索 |
-可执行 | 匹配可执行文件 |
-group gname | 文件属于组 gname |
-user uname | 属于用户 uname 的文件 |
-名称模式 | 搜索文件以获取给定模式 |
-iname 模式 | 对给定模式的文件进行不区分大小写的搜索 |
-inum N | 搜索具有索引号 N 的文件 |
-samefile name | 具有与名称相同的索引号的文件 |
-regex 模式 | 匹配给定正则表达式模式的文件。匹配整个路径。 |
-iregex 模式 | 对给定正则表达式模式的文件进行不区分大小写的匹配。匹配整个路径。 |
根据用例搜索文件
以下 shell 脚本显示了如何使用find
命令的一些用例:
#!/bin/bash
# Filename: finding_files.sh
# Description: Searching different types of file in system
echo -n "Number of C/C++ header files in system: "
find / -name "*.h" 2>/dev/null |wc -l
echo -n "Number of shell script files in system: "
find / -name "*.sh" 2>/dev/null |wc -l
echo "Files owned by user who is running the script ..."
echo -n "Number of files owned by user $USER :"
find / -user $USER 2>/dev/null |wc -l
echo -n "Number of executable files in system: "
find / -executable 2>/dev/null | wc -l
在执行上述finding_files.sh
脚本后,以下是示例输出:
Number of C/C++ header files in system: 73950
Number of shell script files in system: 2023
Files owned by user who is running the script ...
Number of files owned by user foo :341726
Number of executable files in system: 127602
根据索引号查找并删除文件
find
命令可用于根据其索引号查找文件。
$ find ~/ -inum 8142358
/home/foo/Documents
-inum
选项可以与exec
一起使用,用于删除无法通过文件名删除的文件。例如,名为-test.txt
的文件无法使用rm
命令删除:
$ ls -i ~ |grep test # Viewing file with its inode number
8159146 -test.txt
要删除-test.txt
文件,执行以下命令:
$ find ~/ -inum 8159146 -exec rm -i {} \; # Interactive deletion
rm: remove regular file '/home/skumari/-test.txt?' y
链接到一个文件
文件的链接意味着用不同的文件名引用相同的文件。在 Linux 和基于 Unix 的系统中,存在以下两种类型的链接:
-
软链接或符号链接
-
硬链接
要创建文件之间的链接,可以使用ln
命令。语法如下:
ln [option] target link_name
在这里,target
是要创建链接的文件名,link_name
是要创建链接的名称。
软链接
软链接是一种特殊类型的文件,它只是指向另一个文件。这使得更容易创建文件的快捷方式,并且可以更容易地在文件系统中的不同位置访问文件。
要创建文件的符号链接,使用ln
命令带有-s
选项。例如,我们将在我们的主目录中创建/tmp
目录的符号链接:
$ ln -s /tmp ~/local_tmp
现在,我们在我们的主目录中有一个对/tmp
目录的符号链接,名为local_tmp
。要访问/tmp
数据,我们也可以cd
到~/local_tmp
目录。要知道一个文件是否是符号链接,运行ls -l
命令:
$ ls -l ~/local_tmp
lrwxrwxrwx. 1 foo foo 5 Aug 23 23:31 /home/foo/local_tmp -> /tmp/
如果第一列的第一个字符是l
,那么它意味着它是一个符号链接。同时,最后一列显示/home/foo/local_tmp -> /tmp/
,这意味着local_tmp
指向/tmp
。
硬链接
硬链接是一种用不同名称引用文件的方式。所有这些文件都将具有相同的索引节点号。索引节点号是索引表中的索引号,包含有关文件的元数据。
要创建文件的硬链接,使用ln
命令而不带任何选项。在我们的情况下,我们将首先创建一个名为file.txt
的常规文件:
$ touch file.txt
$ ls -l file.txt
-rw-rw-r--. 1 foo foo 0 Aug 24 00:13 file.txt
ls
的第二列显示链接计数。我们可以看到当前是1
。
现在,要创建file.txt
的硬链接,我们将使用ln
命令:
$ ln file.txt hard_link_file.txt
要检查是否为file.txt
创建了硬链接,我们将查看其链接计数:
$ ls -l file.txt
-rw-rw-r--. 2 foo foo 0 Aug 24 00:13 file.txt
现在,链接计数为2
,因为使用名称hard_link_file.txt
创建了一个硬链接。
我们还可以看到file.txt
和hard_link_file.txt
文件的索引节点号是相同的:
$ ls -i file.txt hard_link_file.txt
96844 file.txt
96844 hard_link_file.txt
硬链接和软链接之间的区别
以下表格显示了硬链接和软链接之间的一些重要区别:
软链接 | 硬链接 |
---|---|
实际文件和软链接文件的索引节点号是不同的。 | 实际文件和硬链接文件的索引节点号是相同的。 |
可以在不同的文件系统之间创建软链接。 | 只能在相同的文件系统中创建硬链接。 |
软链接可以链接到常规文件和目录。 | 硬链接不能链接到目录。 |
如果实际文件被删除,软链接不会更新。它将继续指向一个不存在的文件。 | 如果实际文件被移动或删除,硬链接总是会更新。 |
特殊文件
除了常规文件、目录和链接文件之外的文件都是特殊文件。它们如下:
-
块设备文件
-
字符设备文件
-
命名管道文件
-
套接字文件
块设备文件
块设备文件是以块形式读写数据的文件。这种文件在需要大量写入数据时非常有用。诸如硬盘驱动器、USB 驱动器和 CD-ROM 之类的设备被视为块设备文件。数据是异步写入的,因此其他用户不会被阻止执行写操作。
要创建块设备文件,使用mknod
命令,带有b
选项以及提供主要和次要编号。主要编号选择调用哪个设备驱动程序执行输入和输出操作。次要编号用于识别子设备:
$ sudo mknod block_device b 0X7 0X6
在这里,0X7
是十六进制格式的主要编号,0X6
是次要编号:
$ ls -l block_device
brw-r--r--. 1 root root 7, 6 Aug 24 12:21 block_device
第一列的第一个字符是b
,这意味着它是一个块设备文件。
ls
输出的第五列是7
和6
。这里,7
是一个主要号,6
是一个次要号,以十进制格式表示。
字符设备文件是以逐个字符的方式读取和写入数据的文件。这些设备是同步的,一次只能有一个用户进行写操作。键盘、打印机和鼠标等设备被称为字符设备文件。
以下命令将创建一个字符特殊文件:
$ sudo mknod character_device c 0X78 0X60
这里,0X78
是一个主要号,0X60
是一个次要号,以十六进制格式表示。
$ ls -l character_device # viewing attribute of character_device file
crw-r--r--. 1 root root 120, 96 Aug 24 12:21 character_device
第一列的第一个字符是c
,表示它是一个字符设备文件。ls
输出的第五列是120
和96
。这里,120
是一个主要号,96
是一个次要号,以十进制格式表示。
命名管道文件
命名管道文件被不同的系统进程用于相互通信。这种通信也被称为进程间通信。
要创建这样一个文件,我们使用mkfifo
命令:
$ mkfifo pipe_file # Pipe file created
$ ls pipe_file # Viewing file content
prw-rw-r--. 1 foo foo 0 Aug 24 01:41 pipe_file
这里,第一列的第一个字符是p
,表示它是一个管道文件。/dev
目录中有很多管道文件。
我们还可以使用mknod
命令的p
选项创建一个命名管道:
$ mknod named_pipe_file p
$ ls -l named_pipe_file
prw-rw-r--. 1 foo foo 0 Aug 24 12:33 named_pipe_file
以下 shell 脚本演示了从命名管道中读取消息。send.sh
脚本创建一个名为named_pipe
的命名管道,如果它不存在的话,然后在其中发送一条消息:
#!/bin/bash
# Filename: send.sh
# Description: Script which sends message over pipe
pipe=/tmp/named_pipe
if [[ ! -p $pipe ]]
then
mkfifo $pipe
fi
echo "Hello message from Sender">$pipe
receive.sh
脚本检查名为named_pipe
的命名管道是否存在,从管道中读取消息,并显示在stdout
上:
#!/bin/bash
#Filename: receive.sh
# Description: Script receiving message from sender from pipe file
pipe=/tmp/named_pipe
if [[ ! -p $pipe ]]
then
echo "Reader is not running"
fi
while read line
do
echo "Message from Sender:"
echo $line
done < $pipe
要执行它,在一个终端中运行send.sh
,在另一个终端中运行receive.sh
:
$ sh send.sh # In first terminal
$ sh receive.sh # In second terminal
Message from Sender:
Hello message from Sender
套接字文件
套接字文件用于从一个应用程序传递信息到另一个应用程序。例如,如果通用 UNIX 打印系统(CUPS)守护程序正在运行,我的打印应用程序想要与它通信,那么我的打印应用程序将向套接字文件写入一个请求,CUPS 守护程序会监听即将到来的请求。一旦请求被写入套接字文件,守护程序将处理请求:
$ ls -l /run/cups/cups.sock # Viewing socket file attributes
srw-rw-rw-. 1 root root 0 Aug 23 15:39 /run/cups/cups.sock
第一列中的第一个字符是s
,表示它是一个套接字文件。
临时文件
临时文件是在应用程序运行时需要的一段时间内的文件。这些文件被用来保存运行程序的中间结果,在程序执行完成后就不再需要了。在 shell 中,我们可以使用mktemp
命令创建临时文件。
使用mktemp
创建临时文件
mktemp
命令创建一个临时文件,并在stdout
上打印其名称。临时文件默认创建在/tmp
目录中。
创建临时文件的语法如下:
$ mktmp
/tmp/tmp.xEXXxYeRcF
一个名为tmp.xEXXxYeRcF
的文件被创建到/tmp
目录中。我们可以在应用程序中进一步读写这个文件以供临时使用。使用mktemp
命令而不是使用一个随机名称来创建临时文件名,可以避免意外覆盖现有的临时文件。
要创建临时目录,我们可以使用mktemp
的-d
选项:
$ temp_dir=mktemp -d
$ echo $temp_dir
/tmp/tmp.Y6WMZrkcj4
此外,我们也可以明确地删除它:
$ rm -r /tmp/tmp.Y6WMZrkcj4
我们甚至可以通过提供一个参数作为name.XXXX
来指定一个模板用于临时文件。这里,name
可以是临时文件应该以哪个名称开头,XXXX
表示在点(.)后使用随机字符的长度。通常,在编写应用程序时,如果需要临时文件,应用程序名称将作为临时文件名。
例如,一个测试应用程序需要创建一个临时文件。为了创建一个临时文件,我们将使用以下命令:
$ mktemp test.XXXXX
test.q2GEI
我们可以看到临时文件名以test
开头,后面正好包含五个随机字母。
注意
临时文件将被清理的时间是与发行版相关的。
权限和所有权
作为 Linux 和 UNIX 系统的用户,重要的是用户对特定文件或目录具有所需的权限。例如,作为普通用户,执行cd
进入/root
:
$ cd /root
bash: cd: /root/: Permission denied
由于权限被拒绝,我们无法这样做:
$ cd ~/
我们成功地能够进入用户的主目录,因为用户有权限访问自己的主目录。
UNIX 或 Linux 中的每个文件都有一个所有者和一个关联的组。它还具有相对于用户、组和其他人的一组权限(读取、写入和执行)。
查看文件的所有权和权限
使用ls -l
选项的ls
命令用于查看文件的所有权和权限:
$ touch permission_test_file.txt # Creating a file
$ ls -l permission_test_file.txt # Seeing files' attributes
-rw-rw-r-- 1 foo foo 0 Aug 24 16:59 permission_test_file.txt
在这里,ls
的第一列包含权限信息,即-rw-rw-r--
。
第一个字符指定文件的类型,在这个例子中是短横线(-)。短横线表示这是一个常规文件。它可以有其他字符,如下所示:
-
p:这意味着这是一个命名管道文件
-
d:这意味着这是一个目录文件
-
s:这意味着这是一个套接字文件
-
c:这意味着这是一个字符设备文件
-
b:这意味着这是一个块设备文件
接下来的三个字符属于用户或所有者的权限。它可以是rwx
或-
中的任何一个。权限r
表示读权限可用,w
表示写权限可用,x
表示给定文件上的执行权限可用。如果存在短横线,则相应的权限缺失。在上面的例子中,所有者的权限是rw-
,这意味着所有者对permission_test_file.txt
文件具有读和写权限,但没有执行权限。
接下来的三个字符属于组的权限。如果相应的权限缺失,则在这些位置中可以是rwx
或-
。在前面的例子中,授予组的权限是rw-
,这意味着读取和写入权限存在,但执行权限缺失。
接下来的三个字符属于其他人的权限。在前面的例子中,授予其他人的权限是r--
,这意味着其他用户可以读取permission_test_file.txt
文件的内容,但不能修改或执行它。
ls -l
输出中的下一列,即第二列指定文件的所有者是谁。在我们的例子中,第二列的值是foo
,这意味着foo
拥有该文件。默认情况下,文件的所有权归创建该文件的人。
ls -l
输出中的第三列指定文件所属的组。在我们的例子中,permission_test_file.txt
文件的组是foo
。
更改权限
要更改文件的权限,使用chmod
命令。使用chmod
的语法如下:
chmod [option] mode[,mode] file
或者,
chmod [option] octal-mode file
chmod
的一个重要选项是-R
,它表示递归更改文件和目录的权限。
mode
可以是[ugoa][-+][rwx]
。
在这里,u
是所有者,g
是组,o
是其他,a
是所有用户,即ugo
。
指定-(减号)会移除指定的权限,指定+
(加号)会添加指定的权限。
字母r
(读取)、w
(写入)和x
(执行)指定权限。
八进制模式
以八进制格式指定用户的rwx
权限,可以是0 到 7
。以下表格解释了特定用户权限的八进制表示:
八进制值 | 二进制表示 | 意义 |
---|---|---|
0 | 000 | 没有读取、写入和执行权限(---) |
1 | 001 | 只有执行权限(--x) |
2 | 010 | 只有写权限(-w-) |
3 | 011 | 写和执行权限(-wx) |
4 | 100 | 只有读权限(r--) |
5 | 101 | 读取和执行权限(r-x) |
6 | 110 | 读取和写入权限(rw-) |
7 | 111 | 读取、写入和执行权限(rwx) |
为了演示对文件进行权限更改,我们将创建一个文件如下:
$ touch test_file.txt
$ ls -l test_file.txt # Checking permission of file
-rw-rw-r--. 1 foo foo 0 Aug 24 18:59 test_file.txt
对于普通文件,默认权限是所有者、组和其他人都有“读”权限。所有者和组有“写”权限。没有人被赋予执行权限。
现在,我们想以只有所有者可以拥有“写”权限的方式修改权限,并保持其他权限不变。我们可以这样做:
$ chmod 644 test_file.txt
$ ls -l tst_file.txt
-rw-r--r--. 1 foo foo 0 Aug 24 19:03 test_file.txt
现在,我们可以看到只有所有者可以修改test_file
。在使用八进制模式时,我们必须指定我们希望进一步查看的确切权限。在chmod
中,我们将octal_mode
设置为644
;这里的第一个八进制数字,即6
表示所有者的读、写和执行权限。同样,第二个八进制数字4
指定了组的权限,第三个数字指定了其他人的权限。
还有另一种修改权限的方法,即使用模式。模式被指定为[ugoa][-+][rwx]
。在这里,我们只需要指定要添加或删除的权限。
例如,我们想要从所有者那里删除写权限,并向所有人添加执行权限。我们可以这样做:
$ chmod u-w,a+x test_file.txt
$ ls -l test_file.txt
-r-xr-xr-x. 1 foo foo 0 Aug 24 19:03 test_file.txt
更改所有者和组
我们还可以更改文件的所有者和组所有权。这允许进一步修改文件的组和所有者。
更改文件的所有者
要更改命令的所有者,使用chown
。这对于系统管理员在不同情况下非常有用。例如,用户正在进行一个项目,现在用户将要停止在该项目上的工作。在这种情况下,系统管理员可以将所有权修改为负责继续该项目的新用户。系统管理员可以将文件的所有权更改为项目中所有相关文件的新用户。
在我们之前的例子中,foo
是test_file.txt
文件的所有者。现在,我们想把文件的所有权转移到用户bar
。
如果系统中不存在用户bar
,可以使用useradd
命令创建一个名为 bar 的新用户。需要 root 访问权限。
以下命令将创建一个名为bar
的新用户:
$ sudo useradd bar # New user bar will be created
我们可以通过以下命令将test_file.txt
文件的所有权更改为用户bar
:
$ sudo chown bar test_file.txt # Changing ownership of file to user bar
$ ls -l test_file.txt
-r-xr-xr-x. 1 bar foo 0 Aug 24 19:03 test_file.txt
我们可以看到文件的所有权已更改为 bar。
更改组所有权
要修改文件的组所有权,可以使用chown
或chgrp
命令。要创建一个新组,使用groupadd
命令作为sudo
或root
。例如,我们想创建一个名为test_group
的新组:
$ sudo groupadd test_group
现在,我们将使用chown
命令将示例文件test_file.txt
的组更改为。可以通过执行以下命令来完成这个操作:
$ sudo chown :test_group test_file.txt # Modifying group ownership
$ ls -l test_file.txt
-r-xr-xr-x. 1 bar test_group 0 Aug 24 19:03 test_file.txt
我们可以看到组已经修改为test_group
。要使用chgrp
命令更改组,可以执行以下命令:
$ sudo chgrp bar test_file.txt # Changing group ownership to bar
$ ls -l test_file.txt
-r-xr-xr-x. 1 bar bar 0 Aug 24 19:03 test_file.txt
现在,我们将把test_file.txt
文件的所有者和组还原为foo
:
$ sudo chown foo:foo test_file.txt
$ ls -l test_file.txt
-r-xr-xr-x. 1 foo foo 0 Aug 24 19:03 test_file.txt
在使用chown
命令修改所有者和组所有权时,新的所有者名称在:
(冒号)之前提供,组名称在:
之后提供。
获取打开文件的列表
我们知道系统中可能有数百万个文件,可以是二进制文件、文本文件、目录等。当文件没有被使用时,它们只是作为“0 和 1”存储在存储设备上。要查看或处理文件,需要打开它。正在执行的应用程序可能会打开多个文件。知道运行应用程序打开了哪些文件非常有用。要知道已打开文件的列表,使用lsof
命令。
执行以下命令会列出所有打开的文件:
$ lsof
这会给出所有打开文件的大量输出。
知道特定应用程序打开的文件
要知道特定应用程序打开的文件列表,首先获取正在运行应用程序的进程 ID(PID):
$ pidof application_name
例如,让我们不带任何参数运行cat
:
$ cat
在另一个终端中,运行以下命令:
$ pidof cat
15913
$ lsof -p 15913
或者,我们可以直接输入以下命令:
$ lsof -p 'pidof cat'
以下是lsof
输出的示例截图:
在输出中,我们看到了各种结果的列。第一列是COMMAND
,即打开此文件的应用程序,PID 列指定了打开文件的 PID,USER 指示打开文件的用户,FD 是文件描述符,TYPE 指定文件类型,DEVICE 指定设备号,值用逗号分隔,SIZE/OFF 指定文件大小或字节偏移量,NAME 是带有绝对路径的文件名。
在输出中,我们可以看到应用程序已经从/usr/bin
打开了cat binary
。它还加载了共享库文件,如libc-2.21.so
和ld-2.21.so
,这些文件位于/usr/lib64/
中。此外,还有一个字符设备dev/pts/2
被打开。
列出打开文件的应用程序
我们还可以找出哪些应用程序打开了一个文件。可以通过执行以下命令来实现:
$ lsof /usr/bin/bash
以下是示例输出:
从输出中,我们可以看到bash
文件已被六个运行的应用程序打开。
了解用户打开的文件
要了解特定用户打开的文件列表,请使用lsof
命令和-u
选项。语法如下:
lsof -u user_name
例如,考虑以下命令:
$ lsof -u foo | wc -l
525
这意味着当前有525
个文件由用户 root 打开。
配置文件
配置文件是包含应用程序设置的常规文件。在 Linux 和 UNIX 的执行初始阶段,许多应用程序从配置文件中读取设置,并相应地配置应用程序。
查看和修改配置文件
配置文件通常位于/etc/
目录中,可以使用cat
命令查看。
例如,考虑查看resolv.conf
配置文件:
$ cat /etc/resolv.conf
# Generated by NetworkManager
search WirelessAP
nameserver 192.168.1.1
resolv.conf
文件包含联系 DNS 服务器的顺序。
我们还可以修改配置文件以满足我们的需求。例如,如果一些网络 URL 可以通过192.168.1.1
访问,我们可以在/etc/resolv.conf
文件中添加另一个 DNS 条目,DNS 值为8.8.8.8
。修改后的cat /etc/resolv.conf
将如下所示:
$ cat /etc/resolv.conf
# Generated by NetworkManager
search WirelessAP
nameserver 192.168.1.1
nameserver 8.8.8.8
系统中还有许多其他配置文件,例如ssh
、passwd
、profile
、sysconfig
、crontab
、inittab
等,位于/etc/
目录中。
总结
阅读本章后,您现在应该知道 UNIX 和基于 Linux 的操作系统将一切视为文件,可以进一步分类为常规、目录、链接、块设备、字符设备、套接字和管道文件。您还应该知道如何对这些文件中的任何一个执行基本操作。现在,您应该对如何查看和修改文件的权限和所有权有很好的了解。您还应该知道如何使用lsof
命令监视和管理系统中打开文件的列表。
在下一章中,您将学习系统中如何创建进程以及如何监视和管理所有运行中的进程。我们还将看到两个或更多进程如何使用进程间通信(IPC)机制相互通信。
第七章:欢迎来到进程
正在执行的程序称为进程。当操作系统启动时,多个进程会启动,以提供各种功能和用户界面,以便用户可以轻松执行所需的任务。例如,当我们启动命令行服务器时,我们将看到一个带有 bash 或任何其他已启动的 shell 进程的终端。
在 Linux 中,我们对进程有完全控制权。它允许我们创建、停止和终止进程。在本章中,我们将看到如何使用诸如top
、ps
和kill
之类的命令以及通过更改其调度优先级来创建和管理进程。我们还将看到信号如何导致进程突然终止,以及使用命令 trap 在脚本中处理信号的方法。我们还将看到进程的一个美妙特性,即进程间通信,它允许它们相互通信。
本章将详细介绍以下主题:
-
进程管理
-
列出和监视进程
-
进程替换
-
进程调度优先级
-
信号
-
陷阱
-
进程间通信
进程管理
管理进程非常重要,因为进程是消耗系统资源的主要因素。系统用户应该注意他们正在创建的进程,以确保进程不会影响任何其他关键进程。
进程创建和执行
在 bash 中,创建进程非常容易。执行程序时,会创建一个新进程。在 Linux 或基于 Unix 的系统中,创建新进程时会为其分配一个唯一的 ID,称为 PID。PID 值始终是从1
开始的正数。根据系统是否具有init
或systemd
,它们始终获得 PID 值 1,因为这将是系统中的第一个进程,它是所有其他进程的祖先。
PID 的最大值在pid_max
文件中定义,该文件应该位于/proc/sys/kernel/
目录中。默认情况下,pid_max
文件包含值32768
(最大 PID + 1),这意味着系统中最多可以同时存在32767
个进程。我们可以根据需要更改pid_max
文件的值。
为了更好地理解进程创建,我们将从 bash 创建一个新进程vi
:
$ vi hello.txt
在这里,我们创建了一个新进程vi
,它打开编辑器中的hello.txt
文件以读写文本。调用vi
命令会导致二进制文件/usr/bin/vi
执行并执行所需的任务。创建另一个进程的进程称为该进程的父进程。在本例中,vi
是从 bash 创建的,因此 bash 是进程vi
的父进程。创建子进程的方法称为 forking。在 fork 过程中,子进程继承其父进程的属性,如 GID、真实和有效的 UID 和 GID、环境变量、共享内存和资源限制。
要知道在前一节中创建的vi
进程的 PID,我们可以使用诸如pidof
和ps
之类的命令。例如,在新终端中运行以下命令以了解vi
进程的 pid:
$ pidof vi # Process ID of vi process
21552
$ ps -o ppid= -p 21552 # Knowing parent PID of vi process
1785
任务完成后,进程终止并且 PID 可根据需要自由分配给新进程。
有关每个进程的详细信息可在/proc/
目录中找到。对于/proc/
中的每个进程,都会创建一个名为 PID 的目录,其中包含其详细信息。
进程在其生命周期中可以处于以下任何状态之一:
-
运行:在此状态下,进程正在运行或准备运行
-
等待:进程正在等待资源
-
停止:进程已停止;例如,收到信号后
-
僵尸:进程已成功退出,但其状态变化尚未被父进程确认
进程终止
在正常情况下,完成任务后,进程会终止并释放分配的资源。如果 shell 已经派生了任何子进程,那么它将等待它们完成任务(而不是后台进程)。在某些情况下,进程可能不会正常工作,可能会等待或消耗比预期更长的时间。在其他一些情况下,可能会发生进程现在不再需要的情况。在这种情况下,我们可以从终端杀死进程并释放资源。
要终止一个进程,我们可以使用kill
命令。如果系统上有的话,也可以使用killall
和pkill
命令。
使用 kill 命令
kill
命令向指定的进程发送指定的信号。如果没有提供信号,则发送默认的SIGTERM
信号。我们将在本章后面更多地了解有关信号的信息。
以下是使用kill
命令的语法:
kill PID
AND
kill -signal PID
要杀死一个进程,首先获取该进程的PID
如下:
$ pidof firefox # Getting PID of firefox process if running
1663
$ kill 1663 # Firefox will be terminated
$ vi hello.txt # Starting a vi process
$ pidof vi
22715
$ kill -SIGSTOP 22715 # Sending signal to stop vi process
[1]+ Stopped vi
在这里,我们使用SIGSTOP
信号来停止进程而不是杀死它。要杀死,我们可以使用SIGKILL
信号或与此信号相关的值,即9
。
$ kill -9 22715 # Killing vi process
OR
$ kill -SIGKILL 22715 # Killing vi process
使用 killall 命令
按名称而不是 PID 来记住一个进程更容易。killall
命令使得杀死一个进程更容易,因为它将命令名称作为参数来杀死一个进程。
以下是killall
命令的语法:
killall process_name
AND
killall -signal process_name
例如,我们可以按名称杀死firefox
进程,如下所示:
$ killall firefox # Firefox application gets terminated
使用 pkill 命令
pkill
命令也可以用来按名称杀死一个进程。与killall
命令不同,默认情况下,pkill
命令会找到所有以其参数中指定的名称开头的进程。
例如,以下命令演示了pkill
如何根据参数中指定的部分名称杀死firefox
进程:
$ pkill firef # Kills processes beginning with name firef and hence firefox
pkill
命令应该谨慎使用,因为它会杀死所有匹配的进程,这可能不是我们的意图。我们可以使用pgrep
命令和-l
选项来确定将要被pkill
杀死的进程。pgrep
命令根据其名称和属性找到进程。运行以下命令来列出所有以firef
和fire
字符串开头的进程名称及其 PID:
$ pgrep firef
8168 firefox
这里,firefox
是匹配的进程名称,其 PID 是8168
:
$ pgrep fire
747 firewalld
8168 firefox
我们还可以告诉pkill
使用--exact
或-x
选项来精确匹配进程名称杀死进程,如下所示:
$ pgrep -x -l firef # No match found
$ pkill -x fire # Nothing gets killed
$ pgrep --exact -l firefox # Process firefox found
8168 firefox
$ pkill --exact firefox # Process firefox will be killed
pkill 命令还可以使用-signal_name
选项向所有匹配的进程发送特定信号,如下所示:
$ pkill -SIGKILL firef
上述命令向所有以firef
开头的进程发送SIGKILL
信号。
列出和监视进程
在运行中的系统中,我们经常会注意到突然系统反应缓慢。这可能是因为运行的应用程序消耗了大量内存,或者进程正在进行 CPU 密集型工作。很难预测哪个应用程序导致系统反应变慢。为了知道原因,了解正在运行的所有进程以及了解进程的监视行为(例如消耗的 CPU 或内存量)是很有帮助的。
列出进程
要知道系统中运行的进程列表,我们可以使用ps
命令。
语法
ps
命令的语法如下:
ps [option]
有很多选项可以使用ps
命令。常用选项在下表中有解释。
简单的进程选择
以下表格显示了可以组合在一起使用以获得更好结果选择的多个选项:
选项 | 描述 |
---|---|
-A , -e |
选择所有进程 |
-N |
选择不满足条件的所有进程,即否定选择 |
T |
选择与当前终端相关的进程 |
r |
限制选择只有运行中的进程 |
x |
选择没有控制终端的进程,例如在引导过程中启动的守护进程 |
a |
选择终端上的进程,包括所有用户 |
按列表选择进程
以下选项接受以空格分隔或逗号分隔的列表形式的单个参数;它们可以多次使用:
选项 | 描述 |
---|---|
-C cmdlist |
通过名称选择进程。提供在cmdlist 中选择的名称列表。 |
-g grplist |
通过grplist 参数列表中提供的有效组名选择进程。 |
-G grplist |
通过grplist 参数列表中提供的真实组名选择进程。 |
-p pidlist |
通过pidlist 中提到的 PID 选择进程。 |
-t ttylist |
通过ttylist 中提到的终端选择进程。 |
-U userlist |
通过userlist 中提到的真实用户 ID 或名称选择进程。 |
-u userlist |
通过userlist 中提到的有效用户 ID 或名称选择进程。 |
输出格式控制
以下选项用于选择如何显示ps
命令的输出:
选项 | 描述 |
---|---|
显示作业格式。 | |
-f |
用于完整格式列表。它还打印传递给命令的参数。 |
u |
显示面向用户的格式。 |
-l |
显示长格式。 |
v |
显示虚拟内存格式。 |
列出所有带有详细信息的进程
要了解系统上的所有进程,可以使用-e
选项。要获得更详细的输出,请与u
选项一起使用:
$ ps -e u | wc -l # Total number of processes in system
211
$ ps -e u | tail -n5 # Display only last 5 line of result
我们可以从输出中看到所有用户的进程。实际显示输出的命令——即ps -e u | tail -n5——也作为两个单独的运行进程在ps
输出中提到。
在 BSD 风格中,使用aux
选项可以获得与-e u
相同的结果:
$ ps aux
在基于 Linux 的操作系统上,aux
以及-e u
选项都可以正常工作。
列出特定用户运行的所有进程
要了解特定用户正在运行哪些进程,可以使用-u
选项,后面跟着用户名。也可以提供多个用户名,用逗号(,)分隔。
$ ps u -u root | wc -l
130
$ ps u -u root | tail -n5 # Display last 5 results
前面的命令显示以下结果:
我们看到所有进程都是以 root 用户身份运行的。其他用户的进程已被过滤掉。
在当前终端中运行的进程
了解当前终端中运行哪些进程很有用。这有助于决定是否终止运行中的终端。我们可以使用T
或t
选项制作当前终端中运行的进程列表。
$ ps ut
以下命令的输出如下:
我们可以从输出中看到,bash
和ps uT
命令(我们刚刚执行以显示结果)是当前终端中唯一运行的进程。
按命令名称列出进程
我们还可以使用-C
选项按名称了解进程的详细信息,后面跟着命令名称。多个命令名称可以用逗号(,
)分隔:
$ ps u -C firefox,bash
获得以下输出:
进程的树形格式显示
pstree
命令以树形结构显示运行中的进程,这样很容易理解进程的父子关系。
使用-p
选项运行pstree
命令,以树形格式显示进程及其 PID 号,如下所示:
$ pstree -p
从 pstree
输出中,我们可以看到所有进程的父进程是 systemd
。这是作为负责执行其余进程的第一个进程启动的。在括号中,提到了每个进程的 PID 号码。我们可以看到 systemd
进程得到了 PID 1,这是固定的。在基于 init
的操作系统上,init
将是所有进程的父进程,并且具有 PID 1。
要查看特定 PID 的进程树,我们可以使用 pstree
并将 PID 号码作为参数:
$ pstree -p 1627 # Displays process tree of PID 1627 with PID number
使用 pstree
命令并带有 -u
选项来查看进程的 UID 和父进程不同时:
$ pstree -pu 1627
我们可以看到最初,bash
由用户 skumari
以 PID 1627
运行。在树的下方,sudo
命令以 root 用户身份运行。
监视进程
在运行时了解进程消耗了多少内存和 CPU 是非常重要的,以确保没有内存泄漏和过度 CPU 计算的发生。有一些命令,如 top
、htop
和 vmstat
,可以用来监视每个进程消耗的内存和 CPU。在这里,我们将讨论 top
命令,因为它是预装在基于 Linux 的操作系统中的。
top
命令显示 CPU、内存、交换和当前正在运行的任务数量的动态实时使用情况。
运行 top
而不带任何选项会给出以下结果:
$ top
在 top
命令输出中,第一行告诉我们系统自上次启动以来的时间长度、用户数量和平均负载。
第二行告诉我们任务的数量及其状态 - 运行、睡眠、停止和僵尸。
第三行给出了 CPU 使用情况的详细信息。不同的 CPU 使用情况显示在下表中:
值 | 描述 |
---|---|
us |
在运行非优先用户进程中花费的 CPU 时间百分比 |
sy |
在内核空间中花费的 CPU 时间百分比 - 即运行内核进程 |
ni |
运行优先用户进程的 CPU 时间百分比 |
id |
空闲时间百分比 |
wa |
等待 I/O 完成所花费的时间百分比 |
hi |
服务硬件中断所花费的时间百分比 |
si |
服务软件中断所花费的时间百分比 |
st |
虚拟机消耗的时间百分比 |
第四行告诉我们关于总、空闲、已使用和缓冲的 RAM 内存使用情况。
第五行告诉我们关于总交换内存、空闲和已使用的交换内存。
其余行提供了关于运行进程的详细信息。每列的含义在下表中描述:
列 | 描述 |
---|---|
PID | 进程 ID |
USER | 任务所有者的有效用户名 |
PR | 任务的优先级(值越低,优先级越高) |
NI | 任务的优先级。负的优先级值意味着更高的优先级,正的意味着较低的优先级 |
VIRT | 进程使用的虚拟内存大小 |
RES | 未交换的物理内存进程 |
SHR | 进程可用的共享内存量 |
S | 进程状态 - D(不可中断的睡眠),R(运行),S(睡眠),T(被作业控制信号停止),t(被调试器停止),Z(僵尸) |
%CPU | 进程当前使用的 CPU 百分比 |
%MEM | 进程当前使用的物理内存百分比 |
TIME+ | CPU 时间,百分之一秒 |
COMMAND | 命令名称 |
当 top 在运行时,我们也可以重新排序和修改输出。要查看帮助,请使用 ? 或 h 键,将显示帮助窗口,其中包含以下详细信息:
要根据特定字段进行排序,最简单的方法是在 top
运行时按下 f 键。一个新窗口会打开,显示所有列。打开的窗口如下所示:
使用上下箭头导航并选择列。要根据特定字段进行排序,请按下s键,然后按q键切换回顶部输出窗口。
在这里,我们选择了 NI,然后按下了s键和q键。现在,top
输出将按nice
数字排序。排序后的top
输出如下所示:
进程替换
我们知道可以使用管道将命令的输出作为另一个命令的输入。例如:
$ cat file.txt | less
在这里,cat
命令的输出——即file.txt
的内容——作为输入传递给了 less 命令。我们可以将仅一个进程的输出(在本例中为 cat 进程)重定向为另一个进程的输入。
我们可能需要将多个进程的输出作为另一个进程的输入。在这种情况下,使用进程替换。进程替换允许进程从一个或多个进程的输出中获取输入,而不是文件。
使用进程替换的语法如下:
将输入文件替换为列表
<(list)
或者
通过列表替换输出文件(s)
>(list)
在这里,list
是一个命令或一系列命令。进程替换使列表的行为类似于文件,方法是给列表命名,然后在命令行中替换该名称。
比较两个进程的输出
要比较两组数据,我们使用diff
命令。但是,我们知道diff
命令需要两个文件作为输入来生成差异。因此,我们必须首先将两组数据保存到两个单独的文件中,然后运行diff
。保存差异内容会增加额外的步骤,这是不好的。为了解决这个问题,我们可以在执行diff
时使用进程替换功能。
例如,我们想要知道目录中的隐藏文件。在 Linux 和基于 Unix 的系统中,以。
(点)开头的文件称为隐藏文件。要查看隐藏文件,可以使用ls
命令的-a
选项:
$ ls -l ~ # Long list home directory content excluding hidden files
$ ls -al ~ # Long list home directory content including hidden files
要仅获取目录中的隐藏文件,请对从前两个命令获得的排序输出运行diff
命令:
$ diff <(ls -l ~ | tr -s " " | sort -k9) <(ls -al ~ | tr -s " " | sort -k9)
在这里,我们将ls -l ~ | tr -s " " | sort -k9
和ls -al ~ | tr -s " " | sort -k9
命令作为输入数据提供给diff
命令,而不是传递两个文件。
进程调度优先级
在进程的生命周期中,它可能需要 CPU 和其他资源来保持正常执行。我们知道系统中同时运行多个进程,并且它们可能需要 CPU 来完成操作。为了共享可用的 CPU 和资源,进行进程调度,以便每个进程有机会利用 CPU。创建进程时,会设置初始优先级值。根据优先级值,进程获得 CPU 时间。
进程调度优先级范围是从-20
到19
。这个值也被称为 nice 值。nice 值越低,进程的调度优先级就越高。因此,具有-20
的进程将具有最高的调度优先级,而具有 nice 值19
的进程将具有最低的调度优先级。
要查看进程的 nice 值,可以使用ps
或top
命令。进程的相应 nice 值在 NI 列中可用:
$ ps -l
在ps
输出中,我们可以看到 bash 和ps
进程的NI
列中的 nice 值为0
。
更改调度优先级
系统中的每个进程都分配了一些优先级,这取决于它的 nice 值。根据优先级,进程获得 CPU 时间和其他资源来使用。有时,可能会发生进程需要快速执行,但由于较低的调度优先级而等待释放 CPU 资源很长时间。在这种情况下,我们可能希望增加其调度优先级以更快地完成任务。我们可以使用nice
和renice
命令来更改进程的调度优先级。
使用 nice
nice
命令以用户定义的调度优先级启动进程。默认情况下,用户创建的进程的 nice 值为0
。要验证这一点,请运行不带任何选项的nice
命令:
$ nice
0
让我们创建一个实际消耗 CPU 和资源的新firefox
进程:
$ killall firefox # Terminate any firefox if already running
$ firefox & # Firefox launched in background
$ top
我们可以看到firefox
的 nice 值为0
,CPU 使用率为 8.7%。
现在,我们将终止当前的firefox
并启动另一个firefox
,其 nice 值为10
。这意味着firefox
的优先级将低于其他用户创建的进程。
要创建一个具有不同 nice 值的进程,可以使用nice
的-n
选项:
$ killall firefox
$ nice -n 10 firefox &
或者
$ nice -10 firefox &
要查看firefox
现在的 nice 值,请检查top
输出:
$ top
我们可以看到firefox
进程的 nice 值为10
。要提供更多的调度优先级——即为进程设置负的 nice 值——需要 root 权限。
以下示例将设置firefox
进程为更高的调度优先级:
$ nice -n -10 firefox
或者
$ sudo nice --10 firefox
使用 renice
nice
命令只能在启动进程时修改 nice 值。但是,如果我们想要更改正在运行的进程的调度优先级,则应使用renice
命令。renice
命令改变一个或多个正在运行的进程的调度优先级。
使用renice
的语法如下:
renice [-n] priority [-g|-p|-u] identifier
在这里,-g
选项考虑后续参数——即 GID 作为标识符。
-p
选项考虑后续参数——即 PID 作为标识符。
-u
选项考虑后续参数——即用户名或 UID 作为标识符。
如果没有提供-g
、-p
或-u
选项,则将标识符视为 PID。
例如,我们将更改属于某个用户的所有进程的优先级。首先,查看由用户拥有的进程的当前优先级:
$ top -u skumari # User is skumari
现在,我们将使用renice
和-u
选项修改所有进程的优先级:
$ sudo renice -n -5 -u skumari
让我们查看由用户skumari
拥有的进程的新的 nice 值:
$ top -u skumari
要修改几个进程的调度优先级,请使用进程的 PID 进行修改。以下示例修改了 PID 分别为1505
和5969
的进程 plasmashell 和 Firefox:
$ sudo renice -n 2 -p 1505 5969
$ top -u skumari
现在,我们可以看到进程 plasmashell 和 Firefox 的 nice 值为2
。
信号
信号是一种软件中断,用于通知进程发生外部事件。在正常执行中,进程按预期继续运行。现在,由于某种原因,用户可能希望取消正在运行的进程
。当进程从终端启动时,当我们按下Ctrl + c键或运行kill
命令时,它将终止。
当我们在终端中运行进程时按下Ctrl + c键时,会生成信号SIGINT
并发送到前台运行的进程。此外,当对进程调用kill
命令时,会生成SIGKILL
信号并终止进程。
可用信号
在所有可用的信号中,我们将在这里讨论经常使用的信号:
信号名称 | 值 | 默认操作 | 描述 |
---|---|---|---|
SIGHUP | 1 | Term | 此信号用于挂起或控制进程的死亡 |
SIGINT | 2 | Term | 此信号用于从键盘中断,如 ctrl + c,ctrl + z |
SIGQUIT | 3 | 核心 | 此信号用于从键盘退出 |
SIGILL | 4 | Core | 用于非法指令 |
SIGTRAP | 5 | Core | 此信号用于跟踪或断点陷阱 |
SIGABRT | 6 | Core | 用于中止信号 |
SIGFPE | 8 | Core | 浮点异常 |
SIGKILL | 9 | Term | 进程立即终止 |
SIGSEGV | 11 | Core | 无效内存引用 |
SIGPIPE | 13 | Term | 管道破裂 |
SIGALRM | 14 | Term | 警报信号 |
SIGTERM | 15 | Term | 终止进程 |
SIGCHLD | 17 | Ign | 子进程停止或终止 |
SIGSTOP | 19 | Stop | 此信号用于停止进程 |
SIGPWR | 30 | Term | 电源故障 |
在上表中,我们提到了信号名称和值。在默认操作部分中使用的术语的含义如下:
-
Term: 终止
-
Core: 终止进程并转储核心
-
Ign: 忽略信号
-
Stop: 停止进程
根据信号的类型,可以采取以下任何一种操作:
-
进程可以忽略信号,这意味着不会采取任何操作。除了
SIGKILL
和SIGSTOP
之外,大多数信号都可以被忽略。SIGKILL
和SIGSTOP
信号无法被捕获、阻止或忽略。这允许内核在任何时间点杀死或停止任何进程。 -
可以通过编写信号处理程序代码来处理信号,指定接收到特定信号后要采取的必要操作。
-
每个信号都有一个默认操作,因此让信号执行默认操作;例如,如果发送
SIGKILL
信号,则终止进程。
要了解所有信号及其相应的值,请使用kill
命令和-l
选项:
$ kill -l
kill
命令还提供了一种在以下方式中将信号编号转换为名称的方法:
kill -l signal_number
$ kill -l 9
KILL
$ kill -l 29
IO
$ kill -l 100 # invalid signal number gives error
bash: kill: 100: invalid signal specification
要向进程发送信号,可以使用kill
、pkill
和kilall
命令:
$ kill -9 6758 # Sends SIGKILL process to PID 6758
$ killall -1 foo # Sends SIGHUP signal to process foo
$ pkill -19 firef # Sends SIGSTOP signal to processes' name beginning with firef
陷阱
当一个进程正在运行时,我们在中间杀死这个进程,进程会立即终止而不再执行任何操作。编写程序的程序员可能希望在程序实际终止之前执行一些任务;例如,清理创建的临时目录,保存应用程序状态,保存日志等。在这种情况下,程序员希望监听信号并在允许终止进程之前执行所需的任务。
考虑以下 shell 脚本示例:
#!/bin/bash
# Filename: my_app.sh
# Description: Reverse a file
echo "Enter file to be reversed"
read filename
tmpfile="/tmp/tmpfile.txt"
# tac command is used to print a file in reverse order
tac $filename > $tmpfile
cp $tmpfile $filename
rm $tmpfile
该程序从用户文件中获取输入,然后反转文件内容。此脚本创建一个临时文件来保存文件的反转内容,然后将其复制到原始文件。最后,它删除临时文件。
当我们执行此脚本时,可能正在等待用户输入文本文件名,或者在反转文件时(大文件需要更多时间来反转内容)。在此期间,如果进程被终止,那么临时文件可能不会被删除。程序员的任务是确保删除临时文件。
为了解决这样的问题,我们可以处理信号,执行必要的任务,然后终止进程。这可以通过使用trap
命令来实现。该命令允许您在脚本接收到信号时执行命令。
使用trap
的语法如下:
$ trap action signals
在这里,我们可以提供要执行的trap
操作。操作可以是一个或多个执行命令。
在trap
的上述语法中,signals
指的是要执行操作的一个或多个信号名称。
以下 shell 脚本演示了trap
如何在接收到信号后执行任务以防止进程突然退出:
#!/bin/bash
# Filename: my_app_with_trap.sh
# Description: Reverse a file and perform action on receiving signals
echo "Enter file to be reversed"
read filename
tmpfile="/tmp/tmpfile.txt"
# Delete temporary file on receiving any of signals
# SIGHUP SIGINT SIGABRT SIGTERM SIGQUIT and then exit from script
trap "rm $tmpfile; exit" SIGHUP SIGINT SIGABRT SIGTERM SIGQUIT
# tac command is used to print a file in reverse order
tac $filename > $tmpfile
cp $tmpfile $filename
rm $tmpfile
在这个修改后的脚本中,当接收到SIGHUP
、SIGINT
、SIGABRT
、SIGTERM
或SIGQUIT
等信号时,将执行rm
$tmpfile; exit
。这意味着首先删除临时文件,然后可以退出脚本。
进程间通信
一个进程可以单独完成某些事情,但不是所有事情。如果两个或更多进程可以以共享结果、发送或接收消息等形式相互通信,那将是非常有用和良好的资源利用。在基于 Linux 或 Unix 的操作系统中,两个或更多进程可以使用 IPC 相互通信。
IPC 是进程之间通信并由内核管理的技术。
IPC 可以通过以下任一方式进行:
-
命名管道:这允许进程从中读取和写入。
-
共享内存:这是由一个进程创建的,并且可以被多个进程读取和写入。
-
消息队列:这是一个结构化和有序的内存段列表,进程可以以队列方式存储或检索数据。
-
信号量:这为访问相同资源的进程提供了同步机制。它具有用于控制多个进程对共享资源访问的计数器。
在讨论命名管道时,在第六章中,处理文件,我们学习了进程如何使用命名管道进行通信。
使用 ipcs 查看 IPC 的信息
ipcs
命令提供了有关 IPC 设施的信息,对于这些设施,调用进程具有读取访问权限。它可以提供有关三种资源的信息:共享内存、消息队列和信号量。
使用ipcs
的语法如下:
ipcs option
选项如下:
选项 | 描述 |
---|---|
-a |
显示所有资源的信息—共享内存、消息队列和信号量 |
-q |
显示有关活动消息队列的信息 |
-m |
显示有关活动共享内存段的信息 |
-s |
显示有关活动信号量集的信息 |
-i ID |
显示 ID 的详细信息。与-q 、-m 或-s 选项一起使用。 |
-l |
显示资源限制 |
-p |
显示资源创建者和最后操作者的 PID |
-b |
以字节打印大小 |
--human |
以人类可读的格式打印大小 |
IPC 提供的信息列表
我们可以使用ipcs
命令不带选项或带-a
:
$ ipcs
或
$ ipcs -a
要仅查看共享内存段,我们可以使用带有-m
选项的ipcs
:
$ ipcs -m --human
在这里,--human
选项通过以 KB 和 MB 的大小而不是以字节的方式提供大小,使大小列以更可读的格式显示。
要查找有关资源 ID 的详细信息,请使用ipcs
命令,后跟-i
选项和资源 ID:
$ ipcs -m -i 393217
知道最近进行 IPC 的进程的 PID
我们可以使用-p
选项知道最近访问特定 IPC 资源的进程的 PID:
$ ipcs -m -p
在这里,cpid
列显示创建共享内存资源的进程的pid
,而lpid
指的是最后访问共享内存资源的进程的 PID。
摘要
阅读完本章后,您将了解 Linux 和基于 UNIX 的系统中的进程是什么。您现在应该知道如何创建、停止、终止和监视进程。您还应该知道如何向进程发送信号,并使用trap
命令在 shell 脚本中管理接收到的信号。您还学会了不同进程如何使用 IPC 进行通信以共享资源或发送和接收消息。
在下一章中,您将了解任务可以自动化的不同方式以及它们如何在指定时间运行而无需进一步人工干预。您还将学习如何以及为什么创建启动文件,并如何在 shell 脚本中嵌入其他编程语言,如 Python。
第八章:安排任务和在脚本中嵌入语言
到目前为止,我们已经了解了各种有用的 shell 实用程序以及如何将它们写入 shell 脚本,以避免一遍又一遍地编写相同的指令。通过编写脚本自动化任务可以减少任务的数量,但是我们仍然需要在需要时运行这些脚本。有时,我们希望在特定时间运行命令或脚本,例如,系统管理员必须在凌晨 12:30 对数据中心中可用的系统进行清理和维护。为了执行所需的操作,系统管理员将在凌晨 12:30 左右登录到计算机并进行必要的工作。但是如果他或她的家庭网络出现故障,数据中心又很远怎么办?在那一刻执行任务将会很不方便和困难。还有一些需要每天或每小时执行的任务,例如监视每个用户的网络使用情况,进行系统备份等。一遍又一遍地执行重复的任务将会非常无聊。
在本章中,我们将看到如何通过使用at
和crontab
实用程序在特定时间或时间间隔内安排任务来解决这些问题。我们还将看到 systemd(系统启动后启动的第一个进程,PID 1)如何管理系统启动后需要的进程。我们还将看到 systemd 如何管理不同的服务和系统日志。最后,我们将学习如何在 shell 脚本中嵌入其他脚本语言,以获得 shell 脚本中的额外功能。
本章将详细介绍以下主题:
-
在特定时间运行任务
-
Cron 作业
-
管理 Crontab 条目
-
systemd
-
嵌入语言
在特定时间运行任务
通常,当我们运行命令或脚本时,它会立即开始执行。但是,如果我们希望在特定时间后运行它呢?例如,我想从互联网上下载大量数据,但不想在工作时减慢我的互联网带宽。因此,我想在凌晨 1:00 运行我的下载脚本,因为在凌晨 1:00 之后我不会使用互联网进行任何工作。使用at
命令可以在指定的时间后安排下载脚本或命令。我们还可以使用atq
命令列出已安排的任务,或使用atrm
命令删除任何已安排的任务。
使用at
执行脚本
我们将使用at
命令在指定时间运行任务。使用at
命令的语法如下:
at [Option] specified_time
在前面的语法中,specified_time
指的是命令或脚本应该运行的时间。时间可以采用以下格式:
时间格式 | 描述 |
---|---|
HH:MM | 一天中特定的时间,以小时(HH)和分钟(MM)表示。如果时间已经过去,则假定为第二天。时间以 24 小时制表示。 |
noon | 白天 12:00。 |
teatime | 下午 4 点或下午 4 点。 |
midnight | 凌晨 12:00。 |
today | 指的是同一天的当前时间。 |
tomorrow | 指的是第二天的当前时间。 |
AM 或 PM | 用于在时间后缀中指定 12 小时制的时间,例如 4:00PM。 |
now + count time-units | 在一定时间后以相同时间运行脚本。计数可以是整数。时间单位可以是分钟,小时,天,周,月或年。 |
日期 | 日期可以以月份-日期和可选年份的形式给出。日期可以采用以下格式之一:MMDD[CC]YY,MM/DD/[CC]YY,DD.MM.[CC]YY,或[CC]YY-MM-DD。 |
at
命令的选项在以下表中解释:
选项 | 描述 |
---|---|
-f FILE |
指定要执行的脚本文件。 |
-l |
atq 命令的别名。 |
-m |
在作业完成时向用户发送电子邮件。 |
-M |
不向用户发送电子邮件。 |
-r |
atrm 命令的别名。 |
-t time |
在指定时间运行作业。时间的格式为[[CC]YY]MMDDhhmm[.ss]。 |
-c job_number |
在标准输出上打印与job_number 相关的作业。 |
-v |
打印作业将被执行的时间。 |
安排命令
以下命令被安排在 14:00 运行,它将文件系统的使用情况存储在一个名为file_system_usage.log
的文件中,存储在用户的主目录中:
$ at 14:00
warning: commands will be executed using /bin/sh
at> df > ~/file_system_usage.log
at> <EOT>
job 33 at Mon Sep 21 14:00:00 2015
当我们像上面那样运行at
命令时,会打印一个警告消息warning: commands will be executed using /bin/sh,指定将使用哪个 shell 来执行命令。在下一行,我们将看到at prompt
,在那里我们可以指定要在 14:00 执行的命令列表。在我们的情况下,我们输入了df > ~/file_system_usage.log
命令,这意味着运行df
命令并将其结果保存在file_system_usage.log
文件中。
一旦输入要输入的命令列表完成,按下Enter键,然后在下一行使用Ctrl + d键从at
提示中退出。在获得正常的 shell 提示之前,我们将看到消息,显示创建的作业编号和作业将被执行的时间戳。在我们的情况下,作业编号是33
,时间戳是Mon Sep 21 14:00:00 2015
。
一旦我们指定的时间戳结束,我们可以检查file_system_usage.log
文件的内容。
当特定的预定作业运行时,我们可以在stdout
上打印将要执行的内容:
$ at -c 33 # Lists content of job 33
我们可以看到df > ~/file_system_usage.log
命令将被执行。其余的行指定了任务将在什么环境中执行。
现在,考虑一个由 root 用户安排的作业:
# at -v 4am
Mon Sep 21 04:00:00 2015
warning: commands will be executed using /bin/sh
at> reboot
at> <EOT>
job 34 at Mon Sep 21 04:00:00 2015
编号为34
的作业是由用户 root 安排的。这个作业系统将在凌晨 4 点重启。
安排脚本文件
我们可以使用at
命令的-f
选项来安排脚本文件在特定时间执行。
例如,我们想要在下周下午 4 点运行loggedin_user_detail.sh
脚本。这个脚本列出了登录的用户以及在脚本在预定时间执行时他们正在运行的进程。脚本的内容如下:
$ cat loggedin_user_detail.sh
#!/bin/bash
# Filename: loggedin_user_detail.sh
# Description: Collecting information of loggedin users
users_log_file=~/users_log_file.log
echo "List of logged in users list at time 'date'" > $users_log_file
users=('who | cut -d' ' -f1 | sort | uniq')
echo ${users[*]} >> $users_log_file
for i in ${users[*]}
do
echo "Processes owned by user $i" >> $users_log_file
ps u -u $i >> $users_log_file
echo
done
$ chmod +x loggedin_user_detail.sh # Provide execute permission
现在,要在下周下午 4 点运行上述脚本,我们将运行以下命令:
$at -f loggedin_user_detail.sh 4pm + 1 week
warning: commands will be executed using /bin/sh
job 42 at Sun Sep 27 16:00:00 2015
我们可以看到这个作业已经被安排在一周后运行。
列出预定的任务
有时候,一个任务被安排在特定的时间运行,但我们忘记了任务应该在什么时间运行。我们可以使用atq
或at
命令的-l
选项来查看已经安排的任务:
$ atq
33 Mon Sep 21 14:00:00 2015 a skumari
42 Sun Sep 27 16:00:00 2015 a skumari
atq
命令显示了当前用户安排的作业,包括作业编号、时间和用户名:
$ sudo atq
34 Mon Sep 21 04:00:00 2015 a root
33 Mon Sep 21 14:00:00 2015 a skumari
42 Sun Sep 27 16:00:00 2015 a skumari
使用sudo
运行atq
命令,列出所有用户安排的作业。
删除预定的任务
如果不再需要执行某个预定的任务,我们也可以删除该任务。当我们想要修改任务执行的时间时,删除任务也是有用的。要修改时间,首先删除预定的任务,然后再用新的时间创建相同的任务。
例如,我们不想在凌晨 1 点而不是凌晨 4 点重启系统。为此,root 用户将首先使用atrm
命令删除作业34
:
# atrm 34
$ sudo atq # Updated lists of tasks
33 Mon Sep 21 14:00:00 2015 a skumari
42 Sun Sep 27 16:00:00 2015 a skumari
# at 1am
warning: commands will be executed using /bin/sh
at> reboot
at> <EOT>
job 47 at Mon Sep 21 01:00:00 2015
$ sudo atq
33 Mon Sep 21 14:00:00 2015 a skumari
42 Sun Sep 27 16:00:00 2015 a skumari
47 Mon Sep 21 01:00:00 2015 a root
我们可以看到,由 root 用户安排的任务现在将在凌晨 1 点而不是凌晨 4 点运行。
定时任务
Cron 作业是定期运行的任务或作业,与at
命令不同。例如,在办公室,我的工作是保持公司员工的详细信息是保密的。为了确保信息安全和更新,而不会丢失任何信息,我将不得不在外部设备上备份最新数据,如硬盘或闪存驱动器。根据员工人数,我可能需要每分钟、每小时、每天或每周备份一次。手动备份每次都是困难、繁琐且浪费时间的。通过了解如何安排 cron 作业,可以很容易地实现。系统管理员经常创建 Cron 作业来安排定期执行的任务,例如备份系统、保存每个登录用户的日志、监视和报告每个用户的网络使用情况、执行系统清理、安排系统更新等。
Cron 由两部分组成:cron 守护进程和 cron 配置。
Cron 守护进程
当系统启动时,cron 守护进程会自动启动并在后台持续运行。守护进程被称为 crond,并由 systemd 或 init 进程启动,这取决于您的系统。它的任务是以一分钟的间隔定期检查配置文件,并检查是否有任何任务需要完成。
Cron 配置
Cron 配置包含 Cron 作业的文件和目录。它们位于/etc/
目录中。与 cron 配置相关的最重要的文件是crontab
。在 Linux 系统中,与 cron 相关的配置文件如下:
-
/etc/cron.hourly/
:其中包含每小时运行的脚本 -
/etc/cron.daily/
:其中包含每天运行的脚本 -
/etc/cron.weekly/
:其中包含每周运行的脚本 -
/etc/cron.monthly/
:其中包含每月运行的脚本 -
/etc/crontab
:其中包含命令以及它们应该运行的间隔 -
/etc/cron.d/
:其中包含命令以及它们应该运行的间隔的文件目录
脚本可以直接添加到cron.hourly/
、cron.daily/
、cron.weekly/
或cron.monthly/
中的任何一个目录中,以便按小时、每天、每周或每月的基础运行它们。
以下是一个简单的 shell 脚本firefox_memcheck.sh
,它检查 Firefox 进程是否正在运行。如果 Firefox 正在运行,并且其内存使用大于 30%,则重新启动 Firefox:
#!/bin/sh
# Filename: firefox_memcheck.sh
# Desription: Resatrts application firefix if memory usage is more than 30%
pid='pidof firefox' # Get pid of firefox
if [ $pid -gt 1 ]
then
# Get current memory usage of firefox
current_mem_usage='ps -u --pid $pid| tail -n1 | tr -s ' ' | cut -d ' ' -f 4'
# Check if firefox memory usage is more than 30% or not
if [ $(echo "$current_mem_usage > 30" | bc) -eq 1 ]
then
kill $pid # Kill firefox if memory usage is > 30%
firefox & # Launch firefox
fi
fi
我们可以将此脚本添加到系统的/etc/cron.hourly/
目录中,它将持续检查我们的 Firefox 内存使用情况。此脚本可以修改为监视其他进程的内存使用情况。
crontab 条目
通过将脚本放入cron.{hourly, daily, weekly, monthly}
中,我们只能设置每小时、每天、每周和每月的间隔任务。如果一个任务需要以 2 天间隔、10 天间隔、90 分钟间隔等运行,该怎么办?为了实现这一点,我们可以将任务添加到/etc/crontab
文件或/etc/cron.d/
目录中。每个用户可能都有自己的 crontab 条目,与每个用户相关的文件位于/var/spool/
中。
crontab 条目如下所示:
我们可以从上述截图中看到,crontab 条目有五个星号。每个星号定义了一个特定的持续时间。我们可以用建议的值替换,或者保持不变。如果在字段中提到,那么它意味着考虑该字段的所有实例。
时间语法也可以描述如下:
-
指定分钟值介于 0 到 59 之间
-
指定小时范围从 0 到 23
-
指定天数范围从 1 到 31
-
指定月份范围从 1 到 12,或者我们可以写 Jan,Feb,... Dec
-
指定一周中的某一天范围从 0 到 6,或者我们可以写 sun(0),mon(1),...,sat(6)
所有五个字段由空格分隔。然后是一个用户名,指定命令将由哪个用户执行。指定用户名是可选的,默认情况下会作为 root 运行。最后一个字段是计划执行的命令。
演示如何编写 crontab 条目的示例如下:
20 7 * * 0 foo command
每个字段的解释如下:
-
20
:第 20 分钟 -
7
:上午 7 点 -
*
:每天 -
*
:每个月 -
0
:星期日 -
foo
:此命令将作为 foo 用户运行 -
command
:要执行的指定命令
因此,命令将在每个星期日的上午 7:20 作为 root 运行。
我们可以使用逗号(,)指定字段的多个实例:
30 20,22 * * * command
在这里,command
将在每天的 8:30 PM 和 10:30 PM 运行。
我们还可以使用连字符(-
)在字段中指定一段时间的范围:
35 7-11 * * 0-3 command
这意味着在星期日、星期一、星期二和星期三的 7:35、8:35、9:35、10:35 和 11:35 运行命令。
要在特定间隔运行脚本,我们可以使用正斜杠(/)指定如下:
20-45/4 8 9 4 * command
该命令将在 4 月 9 日的 8:20 AM 至 8:45 AM 之间以 4 分钟的间隔运行。
Crontab 中的特殊字符串
Crontab 还可以指定以下字符串:
字符串 | 描述 |
---|---|
@hourly |
每小时运行一次,相当于 0 * * * * |
@daily 或@midnight |
每天运行一次,相当于 0 0 * * * |
@weekly |
每周运行一次,相当于 0 0 * * 0 |
@monthly |
每月运行一次,相当于 0 0 1 * * |
@yearly 或@annually |
每年运行一次,相当于 0 0 1 1 * |
@reboot |
在系统启动时运行 |
管理 crontab 条目
我们不直接添加或修改 crontab 的条目。可以使用crontab
命令来添加、修改和列出 crontab 的条目。每个用户都可以有自己的 crontab,可以在其中添加、删除或修改任务。默认情况下,对所有用户启用,但如果系统管理员想要限制某些用户,可以将该用户添加到/etc/cron.deny
文件中。
使用crontab
命令的语法如下:
crontab [-u user] file
crontab [-u user] [option]
crontab 的选项在下表中解释:
选项 | 描述 |
---|---|
-u user |
追加要修改其crontab 的用户的名称 |
-l |
在stdout 上显示当前的 crontab |
-e |
使用EDITOR env 指定的编辑器编辑当前的crontab |
-r |
删除当前的crontab |
-i |
与-r 选项一起使用时,交互式删除当前的crontab |
列出 crontab 条目
要列出crontab
条目,我们使用当前用户的-l
选项:
$ crontab -l
no crontab for foo
输出显示用户foo
没有crontab
条目。这意味着用户foo
尚未在其crontab
中添加任何任务。
要以 root 用户身份查看crontab
,请输入以下命令:
# crontab -l
no crontab for root
或者,使用以下命令:
$ sudo crontab -l
编辑 crontab 条目
当前用户的 crontab 可以使用-e
选项与 crontab 进行编辑或修改:
$ crontab -e
执行上述命令后,将打开一个编辑器,用户可以在其中将任务添加到crontab
文件中。在我们的情况下,启动了vi
编辑器。以下条目已添加到用户foo crontab
条目中:
从编辑器保存并退出后,获得的输出如下:
no crontab for foo - using an empty one
crontab: installing new crontab
要查看用户foo
的修改后的crontab
条目,再次运行-l
选项:
$ crontab -l
要创建用户 root 的crontab
条目,我们可以作为 root 使用-e
选项运行crontab
:
# crontab -e
或者
$ sudo crontab -e
运行上述命令后,编辑器将打开以修改用户 root 的crontab
,在添加条目后如下所示:
要查看 root 的crontab
条目,我们可以使用crontab -l
作为 root 用户:
# crontab -l
root 用户还可以查看和修改另一个用户的crontab
条目。这是通过指定-u
选项,后跟用户名来完成的:
# crontab -u foo -e # Modifying crontab of user foo as root
用户foo
的 crontab 将如下所示打开以进行修改:
要查看另一个用户的crontab
条目,运行以下命令:
# crontab -u foo -l
我们可以如下显示用户foo
的crontab
:
使用crontab
命令创建 crontab 条目,并将其存储在/var/spool/cron/
目录中。文件以用户名命名:
# ls /var/spool/cron
root foo
我们可以看到为用户root
和foo
创建了一个文件。
删除 crontab 条目
我们还可以使用crontab
命令的-r
选项来删除crontab
。默认情况下,将删除当前用户的crontab
。使用-i
选项允许交互式删除crontab
:
# crontab -i -r
crontab: really delete root's crontab? Y
通过运行上述命令,已删除了用户 root 的crontab
条目。我们可以通过运行-l
选项来验证这一点:
# crontab -l
no crontab for root
# ls /var/spool/cron
foo
用户 root 还可以通过在-u
选项中指定用户来删除其他用户的crontab
:
# crontab -r -i -u foo
crontab: really delete foo's crontab? n
我们指定了n
(否)而不是y
(是),因此将中止删除用户foo crontab
。
现在让我们删除它:
# crontab -r -i -u foo
crontab: really delete foo's crontab? Y
现在,用户foo
的crontab
条目已被删除。要验证,请运行以下命令:
$ crontab -l
no crontab for foo
systemd
如今,大多数 Linux 发行版系统,如 Fedora、Ubuntu、Arch Linux、Debian、openSUSE 等,已经从init
切换到了 systemd。systemd 是系统启动后第一个启动的进程,具有 PID 1。它控制和管理其他应该在系统启动后启动的进程。它也被称为操作系统的基本构建块。要了解基于 init 的系统,请参考维基百科链接en.wikipedia.org/wiki/Init
。
systemd 单元
systemd 有几个单元,每个单元包含一个关于服务、套接字、设备、挂载点、交换文件或分区、启动目标等的配置文件。
以下表格解释了一些单元文件:
单元类型 | 文件扩展名 | 描述 |
---|---|---|
服务单元 | .service |
系统服务 |
设备单元 | .device |
内核识别的设备文件 |
挂载单元 | .mount |
文件系统挂载点 |
定时器单元 | .timer |
一个 systemd 定时器 |
交换单元 | .swap |
交换文件 |
要列出系统中安装的所有单元文件,请使用systemctl
命令和list-unit-files
选项:
$ systemctl list-unit-files | head -n 12
要列出单元类型的单元文件,请使用list-unit-files
和--type
选项。运行以下命令将只显示系统中可用的服务单元:
$ systemctl list-unit-files --type=service | head -n 10
管理服务
systemd 管理系统中所有可用的服务,从 Linux 内核启动到系统关闭的时间。Linux 系统中的服务是在后台运行或等待使用的应用程序。服务管理文件的文件名后缀为.service
。
在基于 systemd 的 Linux 系统中,用户或管理员可以使用systemctl
命令管理服务。
服务状态
要列出当前服务的状态并检查它是否正在运行,使用systemctl status
:
例如,要查看我的NetworkManager
服务的状态,请运行以下命令:
$ systemctl status -l NetworkManager.service
我们可以看到NetworkManager
服务正在运行并处于活动状态。它还提供了与当前NetworkManager
服务相关的详细信息。
让我们看看另一个名为sshd
的服务的状态。sshd
服务控制是否可以对系统进行ssh
连接:
$ systemctl status sshd.service
这表明服务sshd
目前处于非活动状态。
如果不需要详细的输出,那么我们可以只使用is-active
选项来查看服务状态:
$ systemctl is-active sshd.service
unknown
$ systemctl is-active NetworkManager.service
active
这里,active
表示服务正在运行,unknown
表示服务未运行。
启用和禁用服务
当系统启动时,systemd 会自动启动一些服务。也可能有一些服务没有运行。要在系统启动后启用服务运行,使用systemctl enable
,要在系统启动时停止系统运行的服务,使用systemctl disable
。
执行以下命令将允许 systemd 在系统启动后运行sshd
服务:
# systemctl enable sshd.service
执行以下命令将允许 systemd 在系统启动时不运行sshd.service
:
# systemctl disable sshd.service
要检查服务是否已启用,请运行systemctl is-enabled
命令:
$ systemctl is-enabled sshd.service
disabled
$ systemctl is-enabled NetworkManager.service
enabled
这意味着sshd
服务当前在系统启动时被禁用,而NetworkManager
服务在启动时由systemd
启用。
启动和停止服务
当系统运行时,有时我们可能需要一些服务在运行。例如,要在我的当前系统中从另一台系统进行ssh
,sshd
服务必须在运行。
例如,让我们看看sshd
服务的当前状态:
$ systemctl is-active sshd.service
unknown
sshd
服务当前未运行。让我们尝试在系统中进行ssh
:
$ ssh foo@localhost # Doing ssh to same machine # Doing ssh to same machine
ssh: connect to host localhost port 22: Connection refused
我们可以看到ssh
连接已被拒绝。
现在,让我们开始运行sshd
服务。我们可以使用以下命令systemctl start
来启动服务:
# systemctl start sshd.service
$ systemctl is-active sshd.service
active
现在,sshd
服务正在运行。再次尝试从另一台机器进行ssh
:
$ ssh foo@localhost
Last login: Fri Sep 25 23:10:21 2015 from 192.168.1.101
现在,登录已成功。
我们甚至可以使用systemctl restart
命令重新启动正在运行的服务。当服务已被修改时,这是必需的。然后,要启用修改的设置,我们只需重新启动它。
# systemctl restart sshd.service
上述命令将重新启动sshd
服务。
当不再需要ssh
时,停止运行它是安全的。这可以避免对机器的匿名访问。要停止运行服务,请运行systemctl stop
命令:
# systemctl stop sshd.service
$ systemctl is-active sshd.service
unknown
查看系统日志
要检查用户是在个人还是企业机器上工作,查看系统日志对于追踪问题和获取系统中发生的活动的详细信息非常重要。查看系统日志在监视和确保网络流量不易受攻击方面起着重要作用。在基于 systemd 的系统上,系统日志由其一个组件journald
收集和管理。它的任务是收集应用程序和内核的日志。日志文件位于/var/log/journal/
目录中。
要查看journald
收集的日志,使用journalctl
命令:
# journalctl
运行上述命令会显示所有收集的系统日志,从旧的开始,逐渐增加到新的日志。
查看最新的日志条目
要查看最新的日志条目并持续打印追加到日志中的新条目,请使用-f
选项:
$ journalctl -f
要查看自系统上次启动以来捕获的日志条目,请使用-b
选项:
$ journalctl -b
查看特定时间间隔的日志
我们还可以查看特定时间间隔的日志。例如,要查看最近 1 小时的日志,我们可以运行以下命令:
$ journalctl --since "1 hour ago" --until now
要查看自 2015 年 7 月 1 日至今的日志条目,我们可以运行以下命令:
$ journalctl --since 2015-07-01
要查看从 2015 年 8 月 7 日下午 7:23 到 2015 年 8 月 9 日上午 7 点的日志,我们可以运行以下命令:
$ journalctl --since "2015-08-07 19:23:00" --until "2015-08-09 7:00:00"
嵌入语言
与其他脚本编程语言(如 Python、Ruby、Perl 和 AWK)相比,Shell 脚本提供了一定的功能集。这些语言提供了与 Shell 脚本语言相比的附加功能。在 Linux 和基于 UNIX 的系统上,要使用这些语言,如果它们没有预装,我们必须单独安装它们。
考虑一个简单的例子:有一个 json 或 XML 文件,我们想解析它并检索其中存储的数据。使用 shell 及其命令来做这件事非常困难且容易出错,但如果我们了解 Python 或 Ruby 语言,我们可以很容易地做到这一点,然后将其嵌入到 shell 脚本中。应该嵌入 shell 脚本中的另一种语言以减少工作量并实现更好的性能。
在 shell 脚本中嵌入其他语言的语法如下:
脚本语言 | 嵌入到 shell 脚本中的语法 |
---|---|
Python(Python 版本 2) | python -c ' '。在单引号中编写要处理的 Python 代码 |
Python3 | python3 -c ' '。在单引号中编写要处理的 Python 版本 3 代码 |
Perl | perl -e ' '。在单引号中编写 Perl 代码。 |
Ruby | ruby -e ' '。在单引号中编写 Ruby 代码。 |
AWK | 这可以用作命令实用程序。有关可用选项,请参阅 awk man 页面。 |
嵌入 Python 语言
要在 shell 脚本中嵌入 Python 语言,我们将使用python -c " Python Code"
。要了解 Python,请参阅官方网站www.python.org/
。
一个简单的 Python 示例是在 Python 中打印Hello World
,如下所示:
print "Hello World"
将此嵌入到 shell 脚本中,我们可以编写以下代码
#!/bin/bash
# Filename: python_print.sh
# Description: Embeding python in shell script
# Printing using Python
python -c 'print "Hello World"'
我们现在将执行python_print.sh
脚本如下:
$ sh python_print.sh
Hello World
要在 shell 脚本中嵌入多行 Python 代码,请使用以下代码:
python - <<EOF
# Python code
EOF
这里,python -指示 python 命令从 stdin 获取输入,EOF
是一个标签,指示获取 stdin 输入直到遇到EOF
文本。
以下示例在 shell 脚本中嵌入 Python 语言,并从用户的 Gmail 帐户中获取未读邮件:
#!/bin/bash
# Filename: mail_fetch.sh
# Description: Fetching unread email from gmail by embedding python in shell script
# Enter username and password of your gmail account
echo Enter your gmail username:
read USER
echo Enter password:
read -s PASSWD
echo Running python code
python - <<CODE
# Importing required Python module
import urllib2
import getpass
import xml.etree.ElementTree as ET
# Function to get unread messages in XML format
def get_unread_msgs(user, passwd):
auth_handler = urllib2.HTTPBasicAuthHandler()
auth_handler.add_password(
realm='mail.google.com',
uri='https://mail.google.com',
user=user,
passwd=passwd
)
opener = urllib2.build_opener(auth_handler)
urllib2.install_opener(opener)
feed = urllib2.urlopen('https://mail.google.com/mail/feed/atom')
return feed.read()
xml_data = get_unread_msgs("$USER", "$PASSWD")
root = ET.fromstring(xml_data)
# Getting Title of unread emails
print "Title of unread messages:"
print "........................"
count=0
for e in root.iter('{http://purl.org/atom/ns#}title'):
print e.text
CODE
echo "Done!"
执行此脚本后,示例输出如下:
$ sh mail_fetch.sh
Enter your gmail username:
foo@gmail.com
Enter password:
Running python code
Title of unread messages:
.....................……………..
Gmail - Inbox for foo@gmail.com
Unread message1
unread message2
Unread message3
Done!
嵌入 AWK 语言
Awk 是一种用于文本处理的编程语言,主要用于获取相关数据和报告工具。要了解更多关于 AWK 编程语言的信息,请参阅其 man 页面或访问网站www.gnu.org/software/gawk/manual/gawk.html
。
Awk 语言可以很容易地在 shell 脚本中使用。例如,考虑在运行系统上执行df
命令的输出:
$ df -h
要使用awk
获取第四列,即Avail
字段,我们可以编写一个使用awk
的 shell 脚本如下:
#!/bin/bash
# Filename: awk_embed.sh
# Description: Demonstrating using awk in shell script
# Fetching 4th column of command df output
df -h |awk '{ print $4 }'
考虑另一个例子,我们将使用一个输入文件,该文件将是系统的/etc/passwd
文件。该文件包含有关 Linux 或基于 UNIX 的系统上每个用户或帐户的基本信息。
/etc/passwd
文件的每一行如下所示:
root:x:0:0:root:/root:/bin/bash
有七个字段,每个字段由冒号(:)分隔。要了解每个字段的详细含义,请参阅en.wikipedia.org/wiki/Passwd
上的维基百科链接。
以下 shell 脚本利用 awk 功能并从/etc/passwd
文件中显示一些有用的信息。例如,我们将考虑以下作为passwd
文件的内容:
$ cat passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
$ cat passwd_file_info.sh # Shell script content
#!/bin/bash
# Filename: passwd_file_info.sh
# Desciption: Fetches useful information from /etc/passwd file using awk
# Fetching 1st and 3rd field i.e. Username and UID and separate them with blank space
awk -F":" '{ print "Username: " $1 "\tUID:" $3 }' passwd
# Searching line whose user is root
echo "User root information"
awk '$1 ~ /^root/' passwd
运行此脚本会得到以下结果:
$ sh passwd_file_info.sh
Username: root UID:0
Username: bin UID:1
Username: daemon UID:2
Username: adm UID:3
Username: lp UID:4
Username: sync UID:5
Username: shutdown UID:6
Username: halt UID:7
User root information
root:x:0:0:root:/root:/bin/bash
注意
还可以在 shell 脚本中使用编译语言,如 C、C++和 Java。为此,编写命令来编译和执行代码。
摘要
阅读完本章后,你现在应该知道如何使用at
命令安排任务在特定时间执行。你还应该知道创建 Cron 作业的好处,这些作业需要多次执行。你还应该学会如何使用crontab
命令来添加、修改、列出和删除 crontab 条目。你还应该对systemd
有很好的理解——这是系统上创建的第一个进程,它管理其他系统进程、服务和日志。你还应该知道如何在 shell 脚本中嵌入其他脚本语言,比如 Python、AWK、Ruby 等。
阅读完所有这些章节并练习了例子后,你现在应该对 shell 脚本有信心了。作为命令行的大师,你现在能够编写自己的 shell 脚本来解决日常任务。最后,如果这本书中没有涵盖的内容,你知道应该查看任何命令的 man 页面以获取帮助。