【编程语言】从0到1:带你轻松玩转Bash

目录

一、什么是 Bash

(一)Bash 的定义与作用

(二)与其他 Shell 的比较

二、Bash 学习前的准备

(一)环境搭建

(二)必备工具

三、Bash 基础语法学习

(一)脚本基础

(二)变量

(三)条件判断

(四)循环结构

四、Bash 进阶知识

(一)函数

(二)输入输出重定向

(三)管道与过滤

(四)数组

五、Bash 实战应用

(一)自动化脚本编写

(二)分析开源 Bash 脚本

六、总结与拓展

(一)学习总结

(二)进一步学习建议


一、什么是 Bash

(一)Bash 的定义与作用

在 Linux 和 Unix 系统的世界里,Bash 可是个相当重要的角色。Bash,全称 Bourne Again SHell,是一种命令行解释器,也是一种脚本语言 ,在操作系统中承担着用户与内核之间沟通桥梁的关键角色。用户在终端中输入的各种命令,都由 Bash 来解析和执行,比如常见的文件操作命令ls(列出目录内容)、cd(切换目录) ,进程管理命令ps(显示当前进程状态)、kill(终止一个进程)等,Bash 都能准确无误地将这些命令传达给系统内核,并把内核处理后的结果反馈给用户。

同时,Bash 还是一个强大的脚本语言。你可以把一系列相关的命令按照一定逻辑组合成一个脚本文件,通常以.sh为扩展名,然后让系统自动依次执行这些命令,实现任务的自动化。比如,系统管理员可以编写一个 Bash 脚本来定期备份重要文件,开发人员可以用 Bash 脚本自动化软件的编译和部署过程 ,运维人员可以利用 Bash 脚本监控服务器的性能指标,当指标超出正常范围时自动报警。

(二)与其他 Shell 的比较

在 Shell 的大家族里,除了 Bash,还有一些其他常见的成员,如 Bourne Shell(sh)、C Shell(csh)、Korn Shell(ksh)、Z Shell(zsh)和 Fish Shell 等。

Bourne Shell(sh)是 Unix 系统中最早的 Shell 之一,由 Stephen Bourne 开发,它是许多其他 Shell 的基础,语法相对简洁,但功能也比较基础,在处理复杂任务和用户交互方面稍显不足。

C Shell(csh)的语法与 C 语言相似,对于熟悉 C 语言的程序员来说可能比较亲切,它提供了命令补全、命令别名、历史命令替换等功能,增强了用户交互体验,但与 Bourne Shell 并不兼容。

Korn Shell(ksh)集合了 C Shell 和 Bourne Shell 的优点,既具有强大的编程能力,又与 Bourne Shell 向下完全兼容,在效率和交互界面方面表现都不错,不过它是 AT&T 的商业产品。

Z Shell(zsh)是一种功能非常强大且可高度定制的 Shell,拥有庞大的插件库,通过像 Oh My Zsh 这样的框架,安装插件和主题变得轻松简单,能让用户打造出符合自己工作习惯和审美的终端环境。它具有出色的通配符匹配、更强大的自动补全以及内置的语法高亮等功能,但默认配置不太友好,新手可能会被大量的自定义选项搞得不知所措,而且安装过多插件和主题可能会导致运行速度下降。

Fish Shell 是一个面向用户的友好型 Shell,注重现代且用户友好的体验,语法高亮、自动补全和选项卡补全等功能都内置其中,脚本语法也清晰直观,容易理解 。但它不兼容 POSIX,用 Fish 编写的脚本在其他 Shell 中无法运行,插件生态系统相对有限。

相比之下,Bash 有着自己独特的优势。它是 GNU 项目的一部分,是完全免费且开源的,这使得它在 Linux 和 Unix 系统中广泛普及,几乎所有的 Linux 发行版都将 Bash 作为默认的 Shell。Bash 具有广泛的兼容性,能运行大多数 Bourne Shell 脚本,同时还继承了 C Shell 和 Korn Shell 的一些优点,功能丰富且强大。它拥有庞大的用户群体和丰富的文档资料,当你在使用过程中遇到问题时,很容易在网上找到相关的教程、指南和论坛帖子来帮助你解决问题。

二、Bash 学习前的准备

在正式踏上 Bash 学习之旅前,我们需要做好一些准备工作,就像出发前要准备好行囊一样,这些准备工作能为我们后续的学习提供良好的基础和便利的条件。

(一)环境搭建

安装 Linux 系统或 WSL

如果你有一台闲置的计算机,或者不介意在当前计算机上替换操作系统,那么直接安装 Linux 系统是个不错的选择。主流的 Linux 发行版有 Ubuntu、CentOS、Debian、Fedora 等 ,它们各有特点。Ubuntu 以其友好的用户界面和丰富的软件源受到初学者的喜爱;CentOS 稳定性高,常用于企业服务器环境;Debian 注重系统的稳定性和开源原则;Fedora 则是新技术的试验田,更新速度快。

以 Ubuntu 为例,安装步骤如下:首先,前往 Ubuntu 官方网站(https://ubuntu.com/download)下载最新版本的 Ubuntu 镜像文件,通常是一个 ISO 文件。下载完成后,准备一个容量不小于 2GB 的空白 U 盘,使用专门的 U 盘刻录工具,如 Rufus(Windows 系统)或 Etcher(跨平台),将下载好的 ISO 文件写入 U 盘中,使其成为可引导的安装介质。接下来,将制作好的 U 盘插入需要安装 Ubuntu 的计算机,重启计算机,并在启动过程中按下对应的快捷键(常见的有 F2、F10、Del 等,具体按键因计算机品牌和型号而异)进入 BIOS 或 UEFI 设置界面。在启动选项中,选择从 U 盘启动。进入 Ubuntu 安装界面后,按照提示进行操作,选择安装语言、键盘布局、安装类型(如全新安装或与现有系统共存进行双系统安装),设置分区(对于新手,可选择默认分区设置),创建用户名和密码等。完成这些步骤后,耐心等待安装过程结束,安装完成后,取出 U 盘,重启计算机,就可以进入全新的 Ubuntu 系统了。

如果你不想在计算机上直接安装 Linux 系统,或者你的工作环境主要是 Windows 系统,那么 Windows Subsystem for Linux(WSL)是一个很好的解决方案。WSL 允许你在 Windows 系统上直接运行 Linux 二进制可执行文件(ELF 格式),无需使用虚拟机,性能损耗小。

在 Windows 10 及以上版本中安装 WSL 的步骤如下:首先,打开 PowerShell(以管理员身份运行),在 PowerShell 中输入以下命令并回车,以启用适用于 Linux 的 Windows 子系统功能:


dism.exe /online /enable - feature /featurename:Microsoft - Windows - Subsystem - Linux /all /norestart

然后,再次在 PowerShell 中输入以下命令,启用虚拟机平台功能:


dism.exe /online /enable - feature /featurename:VirtualMachinePlatform /all /norestart

完成上述操作后,重启计算机。计算机重启后,前往 Microsoft Store(微软应用商店),搜索 “Linux”,你会看到各种 Linux 发行版可供选择,如 Ubuntu、Debian、Kali Linux 等 ,选择你想要安装的发行版,点击 “获取” 按钮进行安装。安装完成后,在开始菜单中找到并打开安装好的 Linux 发行版应用,首次启动时,会提示你进行一些初始化设置,如创建用户名和密码,设置完成后,就可以在 Windows 系统中愉快地使用 Linux 环境了。

熟悉终端

无论是在 Linux 系统还是 WSL 中,终端都是我们与系统交互的重要工具,就像是我们进入 Bash 世界的大门。在 Linux 系统中,大多数桌面环境都提供了便捷的方式来打开终端。例如,在 Ubuntu 的 GNOME 桌面环境中,可以通过按下 “Ctrl + Alt + T” 组合键快速打开终端;也可以在应用程序菜单中找到 “终端” 图标并点击打开。在 Windows 系统的 WSL 中,安装好的 Linux 发行版应用本身就是一个终端入口,直接从开始菜单中启动即可。

打开终端后,我们先来熟悉一些基本的命令行操作,这些操作就像是我们在 Bash 世界中的基础 “行走” 技能。

cd命令:用于切换目录。例如,要切换到系统根目录,可以在终端中输入 “cd /” 并回车;要切换到当前用户的主目录,输入 “cd ~” ;要切换到上一级目录,输入 “cd ..”。假设当前在 “/home/user/Documents” 目录下,输入 “cd ..” 后,就会切换到 “/home/user” 目录。

ls命令:用于列出目录内容。直接输入 “ls”,会列出当前目录下的文件和子目录;如果要列出详细信息,使用 “ls -l” 命令,它会显示文件的权限、所有者、大小、修改时间等信息;若要显示所有文件,包括隐藏文件,使用 “ls -a” 命令 。比如在 “/home/user” 目录下执行 “ls -l”,会看到类似如下的结果:


total 40

drwxr - xr - x 2 user user 4096 Jun 5 10:30 Desktop

drwxr - xr - x 2 user user 4096 Jun 5 10:30 Documents

drwxr - xr - x 2 user user 4096 Jun 5 10:30 Downloads

-rw - r--r-- 1 user user 123 Jun 5 10:35 example.txt

mkdir命令:用于创建新目录。例如,要在当前目录下创建一个名为 “test” 的新目录,输入 “mkdir test” 并回车;如果要创建多级目录,比如在 “/home/user” 目录下创建 “projects/python_project” 这样的多级目录,可以使用 “mkdir -p /home/user/projects/python_project” 命令 ,“-p” 选项表示递归创建,即会自动创建父目录 “projects”。

(二)必备工具

在学习 Bash 的过程中,一款好用的文本编辑器是必不可少的,它就像是我们在 Bash 世界中书写代码和脚本的 “笔”。以下为你推荐几款常用的文本编辑器,并说明它们各自的特点。

nano:这是一款非常适合初学者的文本编辑器,它的界面简洁直观,操作简单易懂。在 nano 编辑器中,所有的快捷键都显示在屏幕底部,比如按下 “Ctrl + O” 组合键保存文件,按下 “Ctrl + X” 组合键退出编辑器 ,按下 “Ctrl + G” 组合键获取帮助信息。即使是没有任何文本编辑经验的新手,也能快速上手。而且 nano 是轻量级的,占用系统资源少,启动速度快,非常适合简单的文本编辑任务,如修改配置文件、编写简短的 Bash 脚本等。例如,要使用 nano 编辑一个名为 “script.sh” 的 Bash 脚本文件,在终端中输入 “nano script.sh” 即可打开文件进行编辑。

vim:vim 是一款功能极其强大且高度可定制的文本编辑器,在开发者和系统管理员中广受欢迎。它采用模式编辑方式,有正常模式、插入模式、可视模式和命令行模式等 。在正常模式下,可以使用各种快捷键进行高效的文本操作,如 “dd” 删除整行,“yy” 复制整行,“p” 粘贴文本 ;按下 “i” 键进入插入模式,就可以像普通文本编辑器一样输入文本;可视模式则方便进行文本块的选择和操作。vim 支持大量的插件和扩展,用户可以通过安装插件来增强其功能,如支持不同编程语言的语法高亮、代码自动补全、版本控制集成等。不过,vim 的学习曲线相对较陡峭,需要花费一定的时间来熟悉它的各种模式和快捷键,但一旦熟练掌握,它将极大地提高你的工作效率。比如,在编写一个复杂的 Bash 脚本时,利用 vim 的语法高亮和代码折叠功能,可以让代码结构更加清晰,便于阅读和修改。

gedit:gedit 是 GNOME 桌面环境下的默认文本编辑器,它具有简洁易用的图形界面,提供了标准的文本编辑功能,如复制、粘贴、查找、替换等,同时支持多种编程语言的语法高亮显示,能帮助你在编写代码时更清晰地分辨不同的代码元素。gedit 还支持插件扩展,用户可以根据自己的需求安装插件来增加额外的功能。对于习惯使用图形界面进行操作的用户来说,gedit 是一个不错的选择,它在处理日常的文本编辑任务和简单的代码编写时都能表现得游刃有余。比如,当你需要快速编辑一个配置文件,并且更倾向于使用图形化的操作方式时,gedit 就能很好地满足你的需求。

三、Bash 基础语法学习

现在,我们已经搭建好了 Bash 的学习环境,就像是为即将开启的 Bash 编程之旅准备好了坚固的 “战车”,接下来,让我们深入学习 Bash 的基础语法,这是我们在 Bash 世界中驰骋的 “驾驶技巧”。

(一)脚本基础

创建与运行脚本

在 Bash 中,脚本文件通常以.sh作为扩展名,这就像是给脚本贴上了一个独特的 “身份标签”,方便我们识别和管理。比如,我们要创建一个名为hello_world.sh的脚本文件,可以使用之前介绍的文本编辑器,如nano 。在终端中输入命令 “nano hello_world.sh”,即可打开nano编辑器开始编辑脚本内容。

在脚本文件的开头,我们通常会添加一行 “#!/bin/bash”,这一行被称为 “shebang”,它的作用至关重要,就像是给脚本指定了一个专属的 “翻译官”,告诉系统该脚本需要使用 Bash 解释器来执行。如果没有这一行,系统可能会使用默认的 Shell 来解释脚本,这可能会导致脚本无法正常运行,就像找错了翻译,交流就会出现障碍一样。

编写好脚本后,我们可以通过以下两种常见的方式来运行它:

使用bash命令:在终端中输入 “bash script.sh”(将script.sh替换为实际的脚本文件名),这种方式会直接调用 Bash 解释器来执行脚本中的命令。例如,对于我们刚刚创建的hello_world.sh脚本,如果它的内容是 “echo “Hello, World!””,那么在终端中执行 “bash hello_world.sh”,就会在屏幕上输出 “Hello, World!” ,这是最直接的执行方式,适用于快速测试脚本。

赋予脚本执行权限后直接运行:首先,使用chmod命令给脚本文件赋予执行权限,命令为 “chmod +x script.sh” 。“chmod” 是 “change mode” 的缩写,用于改变文件的权限,“+x” 表示给文件添加可执行权限。赋予权限后,就可以在终端中直接输入脚本文件名(前面加上 “./” 表示当前目录)来执行脚本,即 “./script.sh” 。这种方式更加灵活,适用于需要经常运行的脚本,因为只需要赋予一次执行权限,之后就可以方便地直接运行。

注释

在编写脚本时,注释是非常重要的部分,它就像是脚本中的 “说明书”,帮助我们和他人更好地理解脚本的功能和逻辑。在 Bash 中,有单行注释和多行注释两种方式。

单行注释:使用 “#” 符号来表示单行注释。在 “#” 后面的内容,直到该行结束,都不会被 Bash 解释器执行,只是用于对脚本进行说明和解释。例如:


#!/bin/bash

# 这是一个打印问候语的脚本

echo "Hello, Everyone!" # 输出问候语

在这个例子中,“# 这是一个打印问候语的脚本” 和 “# 输出问候语” 这两行都是单行注释,它们分别对脚本的功能和具体命令进行了说明,使代码的可读性大大提高。当我们日后查看或修改这个脚本时,通过这些注释就能快速了解脚本的用途和每一行代码的作用。

多行注释:Bash 本身没有专门的多行注释语法,但我们可以利用一些特殊的技巧来实现多行注释。其中一种常用的方法是使用 “:” 和 “<<'COMMENT'” 的组合。例如:


#!/bin/bash

: <<'COMMENT'

这是一个多行注释的示例

这里可以写多行内容

用于对脚本的某个部分进行详细解释

COMMENT

echo "This is a normal command."

在这个例子中,从 “<<'COMMENT'” 开始到 “COMMENT” 结束之间的内容都是注释,不会被执行。这种方式适合对脚本中较大篇幅的代码进行注释说明,比如对一段复杂的逻辑或一个功能模块进行解释。需要注意的是,在 “<<” 和分隔符(这里是 “COMMENT”)之间不能有空格,否则可能会导致注释无法正常生效。另外,这种多行注释方式在一些较老版本的 Bash 中可能存在兼容性问题,不过在大多数现代的 Linux 系统中都能正常使用。

(二)变量

变量定义与访问

在 Bash 中,变量是存储数据的 “容器”,它可以存储各种信息,如文本、数字、文件路径等。变量的定义非常简单,使用 “变量名=值” 的格式,等号两边不能有空格。例如,我们定义一个名为name的变量,并给它赋值为 “Alice” ,可以这样写:


name="Alice"

这里的 “name” 就是变量名,“Alice” 就是赋给变量的值,用双引号括起来是为了防止值中包含空格或特殊字符时出现问题。如果值是纯数字,也可以不用引号。

定义好变量后,我们就可以通过 “$变量名” 或 “${变量名}” 的方式来访问变量的值。例如,要输出name变量的值,可以使用以下命令:


echo $name

# 或者

echo ${name}

这两种方式在大多数情况下效果是一样的,但当变量名与其他字符连在一起时,使用 “${变量名}” 可以更清晰地界定变量名的范围,避免歧义。比如,我们有一个变量num,值为 “5” ,如果我们想输出 “The number is 5”,可以这样写:


num=5

echo "The number is ${num}"

如果写成 “echo “The number is $num”” ,虽然在这个简单的例子中也能得到正确结果,但如果我们想在变量名后面接着输出其他字符,如 “echo “The number is $num apples”” ,就可能会出现问题,Bash 可能会尝试寻找名为 “numapples” 的变量,而不是我们期望的 “num” 变量。

变量命名需要遵循一定的规则:变量名只能由字母、数字和下划线组成,并且不能以数字开头,同时变量名是区分大小写的。例如,“myVar”、“VAR1”、“_var2” 都是合法的变量名,而 “1var”、“my-var” 是不合法的变量名。在实际编程中,为了提高代码的可读性,建议使用有意义的变量名,比如用 “file_path” 表示文件路径,“total_count” 表示总数等,这样能让代码更容易理解和维护。

变量类型与特性

与一些编程语言不同,Bash 中的变量默认都是以字符串形式存储的,即使你赋给变量的是数字,它也会被当作字符串处理。例如:


num="10"

result=$((num + 5))

echo $result

在这个例子中,虽然我们定义的num变量值是数字 “10”,但在进行数学运算 “$((num + 5))” 时,Bash 会自动尝试将num转换为数字进行计算,结果是 “15” 。如果变量值不能被正确转换为数字,就会导致运算错误。

Bash 还提供了一种特殊的变量类型 —— 只读变量,使用readonly关键字来声明。一旦将一个变量声明为只读变量,就不能再修改它的值。例如:


readonly PI=3.14159

# 尝试修改PI的值会报错

PI=3.14

在上述代码中,我们将PI声明为只读变量并赋值为 “3.14159” ,之后如果尝试修改PI的值,如 “PI=3.14” ,Bash 会报错,提示 “bash: PI: readonly variable” ,这就保证了PI的值在脚本执行过程中不会被意外修改,对于一些常量值的定义非常有用,比如数学常量、系统配置参数等,能增强脚本的稳定性和安全性。

(三)条件判断

if 语句

if语句是 Bash 中用于条件判断的重要工具,它允许我们根据不同的条件执行不同的代码块,就像是在岔路口根据指示牌选择不同的道路一样。if语句的基本语法如下:


if [ 条件 ]; then

命令1

命令2

...

elif [ 其他条件 ]; then

命令3

命令4

...

else

命令5

命令6

...

fi

其中,“if” 关键字后面的 “[ 条件 ]” 是判断条件,“[” 和 “]” 之间必须有空格,条件成立时,执行 “then” 后面的命令块;如果条件不成立,且存在 “elif” 部分(可选),则继续判断 “elif” 后面的条件,条件成立时执行相应的命令块;如果所有条件都不成立,就执行 “else” 后面的命令块(可选),“fi” 表示if语句的结束。

例如,我们根据用户输入的数字判断其是否大于 10:


#!/bin/bash

read -p "请输入一个数字: " num

if [ $num -gt 10 ]; then

echo "你输入的数字大于10"

elif [ $num -eq 10 ]; then

echo "你输入的数字等于10"

else

echo "你输入的数字小于10"

fi

在这个脚本中,首先使用 “read -p” 命令读取用户输入的数字,并将其存储在num变量中。然后通过 “if” 语句判断num是否大于 10,如果是,输出 “你输入的数字大于10”;如果不是,再通过 “elif” 判断num是否等于 10,如果是,输出 “你输入的数字等于10”;如果都不满足,即num小于 10,执行 “else” 部分,输出 “你输入的数字小于10”。

逻辑运算符

在条件判断中,我们常常需要使用逻辑运算符来组合多个条件,使判断更加灵活和强大。Bash 中常用的逻辑运算符有 “&&”(与)、“||”(或)、“!”(非) 。

&&”(与)运算符:只有当 “&&” 两边的条件都为真时,整个表达式才为真。例如:


if [ $num -gt 5 ] && [ $num -lt 15 ]; then

echo "数字在5和15之间"

fi

在这个例子中,只有当num大于 5 并且小于 15 时,条件才成立,才会执行 “echo” 命令输出 “数字在5和15之间” 。

||”(或)运算符:只要 “||” 两边的条件中有一个为真,整个表达式就为真。例如:


if [ $num -eq 10 ] || [ $num -eq 20 ]; then

echo "数字是10或者20"

fi

这里,只要num等于 10 或者等于 20,条件就成立,会输出 “数字是10或者20” 。

!”(非)运算符:用于对一个条件取反,即原来条件为真,取反后为假;原来条件为假,取反后为真。例如:


if [ $num -ne 10 ]; then

echo "数字不等于10"

fi

# 等价于

if! [ $num -eq 10 ]; then

echo "数字不等于10"

fi

在这两个例子中,第一个 “if” 语句直接判断num是否不等于 10;第二个 “if” 语句使用 “!” 运算符对 “$num -eq 10” 这个条件取反,效果是一样的,都是当num不等于 10 时,输出 “数字不等于10” 。

(四)循环结构

for 循环

for循环是 Bash 中用于重复执行一段代码的常用结构,它可以遍历一个列表或数字序列,就像我们沿着一条街道依次访问每一个店铺一样。for循环的基本语法有两种形式:

遍历列表形式


for 变量 in 列表; do

命令1

命令2

...

done

在这种形式中,“for” 关键字后面的 “变量” 会依次取 “列表” 中的每一个值,然后执行 “do” 和 “done” 之间的命令块。例如,我们遍历一个水果列表并输出每个水果的名称:


#!/bin/bash

fruits=("apple" "banana" "cherry")

for fruit in ${fruits[@]}; do

echo "我喜欢吃 $fruit"

done

在这个脚本中,首先定义了一个包含三种水果的数组fruits,然后使用 “for” 循环遍历fruits数组,“${fruits[@]}” 表示获取数组fruits中的所有元素。每次循环时,fruit变量会依次取数组中的一个水果值,然后执行 “echo” 命令输出相应的信息。

遍历数字序列形式


for (( 初始值; 条件; 增量 )); do

命令1

命令2

...

done

这种形式类似于 C 语言中的for循环,“初始值” 用于初始化循环变量,“条件” 用于判断是否继续循环,只要条件为真就会继续循环,“增量” 用于在每次循环结束后更新循环变量的值。例如,我们使用这种形式的for循环输出 1 到 5 的数字:


#!/bin/bash

for ((i = 1; i <= 5; i++)); do

echo $i

done

在这个例子中,循环变量i初始值为 1,当i小于等于 5 时,循环继续执行,每次循环结束后i的值增加 1。在循环体中,使用 “echo” 命令输出i的值,最终会依次输出 1 到 5 这五个数字。

while 循环

while循环也是一种常用的循环结构,它会在条件为真的情况下不断重复执行一段代码,就像只要条件满足,就一直重复做某件事情一样。while循环的基本语法如下:


while [ 条件 ]; do

命令1

命令2

...

done

其中,“while” 关键字后面的 “[ 条件 ]” 是循环条件,只要条件为真,就会执行 “do” 和 “done” 之间的命令块,每次循环结束后都会重新判断条件,直到条件为假时才退出循环。例如,我们使用while循环计算 1 到 10 的累加和:


#!/bin/bash

sum=0

count=1

while [ $count -le 10 ]; do

sum=$((sum + count))

count=$((count + 1))

done

echo "1到10的累加和是: $sum"

在这个脚本中,首先初始化变量sum为 0(用于存储累加和),count为 1(用于表示当前要累加的数字)。然后进入while循环,判断count是否小于等于 10,如果是,就将count累加到sum中,并将count的值增加 1,每次循环结束后重新判断条件。当count大于 10 时,条件为假,退出循环,最后输出累加和。

循环控制语句

在循环过程中,有时我们需要根据某些条件提前结束循环或者跳过当前循环的剩余部分,直接进入下一次循环,这就需要用到循环控制语句。Bash 中常用的循环控制语句有break和continue 。

break语句:用于立即终止当前循环,跳出循环体,继续执行循环后面的代码。例如:


#!/bin/bash

for num in 1 2 3 4 5 6 7 8 9 10; do

if [ $num -eq 5 ]; then

break

fi

echo $num

done

echo "循环结束"

在这个例子中,当num等于 5 时,执行 “break” 语句,立即终止循环,所以只会输出 1 到 4 这四个数字,然后输出 “循环结束” 。

continue语句:用于跳过当前循环中 “continue” 语句后面的代码,直接进入下一次循环。例如:


#!/bin/bash

for num in 1 2 3 4 5 6 7 8 9 10; do

if [ $num -eq 5 ]; then

continue

fi

echo $num

done

在这个例子中,当num等于 5 时,执行 “continue” 语句,跳过本次循环中 “echo $num” 这一行代码,直接

四、Bash 进阶知识

(一)函数

函数定义与调用

在 Bash 中,函数是一段可重复使用的代码块,它能将一系列相关的命令组合在一起,提高代码的可读性和可维护性,就像是把一些常用工具放在一个工具箱里,需要时随时取用。函数的定义有两种常见的语法形式:


# 第一种形式

function 函数名 {

命令1

命令2

...

}

# 第二种形式

函数名() {

命令1

命令2

...

}

这两种形式的功能是一样的,你可以根据自己的喜好选择使用。例如,我们定义一个简单的函数greet,用于打印问候语:


function greet {

echo "Hello, $1!"

}

在这个函数中,$1是函数的参数,它代表调用函数时传递的第一个参数。当我们调用这个函数并传递一个名字作为参数时,函数就会打印出相应的问候语。比如:


greet "Tom"

执行上述代码,终端会输出Hello, Tom! ,这就是函数定义和调用的基本过程。

函数参数与返回值

函数可以接受参数,通过参数传递不同的数据,函数就能根据这些数据执行不同的操作,实现更灵活的功能。在函数内部,可以使用$1、$2、$3…… 来依次获取传递进来的参数,$0表示脚本本身的名称。例如,我们定义一个函数add_numbers,用于计算两个数的和:


add_numbers() {

sum=$(( $1 + $2 ))

echo "The sum is: $sum"

}

add_numbers 3 5

在这个例子中,调用add_numbers函数时传递了两个参数3和5,函数内部通过$1获取到3,通过$2获取到5,然后计算它们的和并输出结果,终端会输出The sum is: 8 。

关于函数的返回值,在 Bash 中,函数默认的返回值是函数中最后一条命令的退出状态码,0 表示成功,非 0 表示失败 。如果我们想要自定义函数的返回值,可以使用return语句,不过return语句只能返回一个 0 到 255 之间的整数。例如,我们定义一个函数check_file,用于检查文件是否存在:


check_file() {

if [ -f "$1" ]; then

return 0 # 文件存在,返回0表示成功

else

return 1 # 文件不存在,返回1表示失败

fi

}

check_file "example.txt"

if [ $? -eq 0 ]; then

echo "File exists."

else

echo "File does not exist."

fi

在这个例子中,check_file函数接受一个文件名作为参数,通过[ -f “$1” ]判断文件是否存在,存在则返回 0,不存在返回 1。调用函数后,通过$?获取函数的返回值,$?表示上一个命令的退出状态码,然后根据返回值进行相应的处理 。如果想要返回其他类型的值,比如字符串或复杂的数据结构,可以通过命令替换或输出重定向的方式来实现。例如,我们定义一个函数get_date,用于返回当前日期:


get_date() {

date "+%Y-%m-%d"

}

current_date=$(get_date)

echo "Today's date is: $current_date"

在这个例子中,get_date函数内部使用date “+%Y-%m-%d”命令获取当前日期并输出,调用函数时使用$(get_date)进行命令替换,将函数的输出赋值给current_date变量,从而实现了返回字符串类型的值 。

(二)输入输出重定向

输出重定向

在 Bash 中,输出重定向是一种非常有用的功能,它允许我们将命令的输出结果写入到文件中,而不是显示在终端上,就像是把打印机的输出纸张换成了指定的文件。输出重定向主要有两种操作符:>(覆盖写入)和>>(追加重定向) 。

>(覆盖写入):使用>操作符时,如果目标文件不存在,会创建一个新文件;如果目标文件已经存在,会覆盖文件的原有内容。例如,我们使用date命令获取当前日期和时间,并将其输出重定向到date.txt文件中:


date > date.txt

执行这个命令后,date.txt文件中会写入当前的日期和时间信息,如果date.txt文件原本有内容,会被新的日期时间信息完全覆盖。

>>(追加重定向):>>操作符则是将命令的输出追加到目标文件的末尾,如果文件不存在,同样会创建新文件。比如,我们有一个log.txt文件,用于记录系统操作日志,每次执行某个操作后,都将操作信息追加到log.txt文件中:


echo "Performed some operation at $(date)" >> log.txt

这样,每次执行这个命令,都会在log.txt文件末尾添加一条新的日志记录,不会覆盖之前的内容,方便我们查看操作的历史记录。

输入重定向

输入重定向与输出重定向相反,它是让命令从文件中读取输入,而不是从键盘读取,就像是给命令指定了一个特定的 “输入菜单”。常见的输入重定向操作符是< 。例如,我们有一个input.txt文件,内容如下:


apple

banana

cherry

然后我们使用cat命令结合输入重定向,将input.txt文件的内容输出到终端:


cat < input.txt

执行这个命令,终端会输出input.txt文件中的内容,即:


apple

banana

cherry

这在处理一些需要大量输入的命令时非常方便,比如批量处理数据文件,我们可以将数据事先整理到文件中,然后通过输入重定向让命令直接从文件读取数据进行处理,而不需要手动在终端逐行输入。

错误输出重定向

在执行命令的过程中,有时会出现错误,这些错误信息默认会输出到标准错误输出(stderr),也就是终端上。错误输出重定向可以让我们将这些错误信息重定向到文件中,方便我们查看和分析错误原因,避免错误信息在终端中与正常输出混杂,影响我们对结果的判断。错误输出重定向使用2>操作符,其中2表示标准错误输出的文件描述符。例如,我们执行一个错误的命令ls non_existent_directory(试图列出一个不存在的目录内容),并将错误输出重定向到error.log文件中:


ls non_existent_directory 2> error.log

执行这个命令后,终端上不会显示错误信息,而是将错误信息写入到error.log文件中,我们可以查看error.log文件来了解错误详情,比如文件中可能会记录类似ls: cannot access 'non_existent_directory': No such file or directory的错误信息 。

有时,我们希望将标准错误输出和标准输出合并,一起重定向到同一个文件中,这时可以使用2>&1 。例如:


ls -l non_existent_file 2>&1 > output.log

在这个命令中,2>&1表示将标准错误输出重定向到标准输出,然后>将标准输出(此时包含了标准错误输出)重定向到output.log文件中,这样output.log文件就会包含命令执行过程中的所有输出和错误信息 。

(三)管道与过滤

管道操作

管道操作是 Bash 中一个非常强大且灵活的功能,它就像是一条数据 “传送带”,可以将一个命令的输出作为另一个命令的输入,让多个命令协同工作,完成复杂的数据处理任务。管道操作使用|符号来实现 。例如,我们想要在当前目录下查找所有文件名包含.txt的文件,就可以使用ls -l命令列出当前目录下的文件详细信息,然后通过管道将其输出传递给grep命令进行过滤:


ls -l | grep ".txt"

在这个例子中,ls -l命令会列出当前目录下所有文件和目录的详细信息,包括文件权限、所有者、大小、修改时间等 ,这些信息会通过管道|传递给grep “.txt”命令,grep命令会在接收到的输入中搜索包含.txt的行,并将这些行输出到终端,这样我们就能快速找到所有文本文件。

管道操作可以将多个命令连接起来,形成一个复杂的处理流程。比如,我们想要统计当前目录下所有文件的行数,可以先使用ls命令列出所有文件,然后通过管道将文件名传递给xargs命令,xargs命令会将接收到的文件名作为参数传递给wc -l命令(用于统计行数):


ls | xargs wc -l

这样,就可以一次性统计出当前目录下所有文件的行数。

文本处理工具

在 Bash 中,有一些非常强大的文本处理工具,它们与管道操作结合使用,可以对文本数据进行高效的处理和分析。

grep(搜索文本):grep是一个用于搜索文本的工具,它可以在文件或命令输出中查找包含指定模式的行。grep的基本语法是grep [选项] 模式 [文件] 。例如,我们想要在example.txt文件中查找包含 “hello” 的行,可以使用以下命令:


grep "hello" example.txt

如果不指定文件,而是通过管道接收其他命令的输出,就可以对输出内容进行搜索。比如前面提到的ls -l | grep “.txt” ,就是在ls -l的输出中搜索文件名包含.txt的行。grep还支持一些常用的选项,如-i表示忽略大小写搜索,-r表示递归搜索目录下的所有文件。例如,要在当前目录及其子目录下的所有文件中忽略大小写地搜索 “python”,可以使用:


grep -ir "python" .

这里的 “.” 表示当前目录。

awk(提取字段):awk是一个强大的文本处理工具,它可以根据指定的规则对文本进行逐行处理,常用于提取文本中的特定字段。awk的基本语法是awk [选项] '模式{动作}' [文件] 。例如,我们有一个students.txt文件,内容如下:


Alice 20 Math

Bob 22 English

Charlie 19 Science

如果我们想要提取每个学生的名字和年龄,可以使用以下awk命令:


awk '{print $1, $2}' students.txt

在这个命令中,{print $1, $2}是动作部分,表示打印每行的第一个字段($1)和第二个字段($2) ,执行结果会输出:


Alice 20

Bob 22

Charlie 19

awk还支持更复杂的模式匹配和条件判断,比如我们想要只打印年龄大于 20 的学生信息,可以使用:


awk '$2 > 20 {print $1, $2, $3}' students.txt

这里$2 > 20是模式部分,表示只有当第二个字段(年龄)大于 20 时,才执行后面的动作(打印该行的所有字段) 。

sed(查找替换):sed是一个流编辑器,主要用于对文本进行查找和替换操作。sed的基本语法是sed [选项] 's/查找内容/替换内容/' [文件] 。例如,我们想要将example.txt文件中的所有 “apple” 替换为 “orange”,可以使用以下命令:


sed 's/apple/orange/' example.txt

执行这个命令后,会在终端输出替换后的文本内容。如果想要直接修改example.txt文件的内容,可以使用-i选项:


sed -i 's/apple/orange/' example.txt

这样,example.txt文件中的所有 “apple” 就会被替换为 “orange” 。sed还支持正则表达式进行查找替换,以及更复杂的文本处理操作,如删除行、插入行等 。例如,要删除example.txt文件中所有空行,可以使用:


sed '/^$/d' example.txt

这里/^$/d表示匹配空行(^$表示行首和行尾之间没有任何字符,即空行),并删除这些行 。

(四)数组

数组定义与访问

在 Bash 中,数组是一种数据结构,它可以在一个变量中存储多个值,就像是一个可以装多个物品的容器。数组的定义方式有多种,常见的是使用括号来初始化数组,每个元素之间用空格分隔。例如:


array=(one two three)

这样就定义了一个名为array的数组,它包含三个元素:one、two和three 。数组的下标从 0 开始,我们可以通过下标来访问数组中的元素,使用${数组名[下标]}的格式 。例如,要访问array数组的第一个元素,可以使用:


echo ${array[0]}

执行这个命令,终端会输出one 。

我们也可以在定义数组时指定下标,例如:


array=( [2]=three [0]=one [1]=two )

这种方式可以不按照顺序定义数组元素,通过指定下标来确定元素的位置 。

数组操作

在实际使用数组时,我们常常需要进行一些常见的操作,比如获取数组长度、遍历数组元素等 。

获取数组长度:使用${#数组名[@]}或${#数组名[*]}可以获取数组的长度,即数组中元素的个数。例如:


echo ${#array[@]}

对于前面定义的包含三个元素的array数组,执行这个命令会输出3 。

遍历数组元素:通常使用for循环来遍历数组中的所有元素。例如,要遍历array数组并输出每个元素,可以使用以下代码:


for element in ${array[@]}; do

echo $element

done

在这个for循环中,${array[@]}表示获取数组array中的所有元素,element变量会依次取数组中的每个元素,然后执行循环体中的echo $element命令,将元素输出到终端 ,最终会依次输出one、two、three 。

除了上述基本操作,还可以对数组进行添加元素、删除元素、连接数组等操作 。例如,要向array数组中添加一个元素four,可以使用以下方式:


array=(${array[@]} four)

这里先通过${array[@]}获取原数组的所有元素,然后在后面添加新元素four,重新赋值给array数组 。如果要删除数组中的某个元素,可以使用unset命令,例如删除array数组中第二个元素(下标为 1):


unset array[1]

执行这个命令后,array数组中第二个元素就被删除了 。如果想要连接两个数组,比如有另一个数组array2=(four five) ,要将array2连接到array后面,可以使用:


new_array=(${array[@]} ${array2[@]})

这样就创建了一个新数组new_array,它包含了array和array2的所有元素 。

五、Bash 实战应用

(一)自动化脚本编写

备份脚本

在日常的系统管理和数据处理中,备份是一项至关重要的任务。使用 Bash 编写备份脚本可以实现自动化备份,大大节省时间和精力。下面是一个简单的备份脚本示例:


#!/bin/bash

# 备份脚本

# 定义源目录和目标目录

source_dir="/home/user/data"

backup_dir="/backup/data_backup"

# 创建备份目录(如果不存在)

if [ ! -d $backup_dir ]; then

mkdir -p $backup_dir

fi

# 生成备份文件名,使用当前日期作为后缀

backup_file="$backup_dir/data_$(date +%Y%m%d).tar.gz"

# 使用tar命令进行备份,将源目录打包并压缩成备份文件

tar -zcvf $backup_file $source_dir

echo "Backup completed. Backup file: $backup_file"

这个脚本的实现思路如下:

首先,定义了两个变量source_dir和backup_dir,分别表示需要备份的源目录和存放备份文件的目标目录。你可以根据实际情况修改这两个变量的值,以适应不同的备份需求。

接着,使用if语句判断目标备份目录是否存在。如果不存在,使用mkdir -p命令创建该目录,-p选项表示递归创建,即如果父目录不存在也会一并创建,确保备份目录结构完整。

然后,通过date +%Y%m%d获取当前日期,并将其拼接到备份文件名中,生成一个具有日期标识的备份文件名backup_file,这样可以方便区分不同时间的备份文件,避免文件覆盖,便于管理和追溯历史备份。

最后,使用tar -zcvf命令对源目录进行打包和压缩操作。-z选项表示使用 gzip 压缩,-c选项表示创建新的归档文件,-v选项表示显示详细的处理过程,-f选项后面接备份文件名,指定生成的备份文件路径和名称。执行该命令后,源目录及其所有子目录和文件都会被打包压缩成一个tar.gz格式的备份文件存放在指定的备份目录中。

文件整理脚本

随着时间的推移,我们的文件目录可能会变得杂乱无章,不同类型的文件混在一起,查找和管理都很不方便。这时,一个文件整理脚本就能派上用场。下面是一个按文件类型移动文件的脚本示例:


#!/bin/bash

# 文件整理脚本

# 定义源目录

source_dir="/home/user/Downloads"

# 遍历源目录下的所有文件

for file in $source_dir/*; do

# 获取文件的扩展名

extension="${file##*.}"

# 根据文件扩展名创建对应的目标目录(如果不存在)

case $extension in

"jpg" | "jpeg" | "png" | "gif")

target_dir="$source_dir/Images"

;;

"pdf" | "doc" | "docx" | "txt")

target_dir="$source_dir/Documents"

;;

"mp3" | "wav" | "flac")

target_dir="$source_dir/Audio"

;;

"mp4" | "avi" | "mkv")

target_dir="$source_dir/Videos"

;;

*)

target_dir="$source_dir/Others"

;;

esac

if [ ! -d $target_dir ]; then

mkdir -p $target_dir

fi

# 将文件移动到对应的目标目录

mv $file $target_dir

done

echo "File organization completed."

这个脚本的逻辑和实现步骤如下:

首先,定义了源目录source_dir,这里以/home/user/Downloads为例,实际使用时可根据自己的下载目录或需要整理的目录进行修改。

然后,使用for循环遍历源目录下的所有文件,$source_dir/*表示源目录下的所有文件和子目录。

在循环内部,通过${file##*.}获取文件的扩展名。这里使用了 Bash 的字符串操作语法,##表示从字符串的末尾开始删除匹配的字符,*.表示匹配任意字符和一个点号,所以${file##*.}就得到了文件扩展名。

接着,使用case语句根据文件扩展名判断文件类型,并为每种文件类型定义对应的目标目录target_dir。例如,对于图片文件(jpg、jpeg、png、gif),目标目录是Images;对于文档文件(pdf、doc、docx、txt),目标目录是Documents等。如果文件扩展名不在预设的类型中,则将其归类到Others目录。

再使用if语句判断目标目录是否存在,如果不存在,使用mkdir -p命令创建该目录。

最后,使用mv命令将文件移动到对应的目标目录中,实现文件按类型整理的功能。当循环结束后,源目录下的文件就会被分类整理到各自对应的子目录中,使文件目录结构更加清晰有序,方便查找和管理 。

(二)分析开源 Bash 脚本

学习他人的优秀代码是提升编程能力的有效途径之一,在 Bash 编程领域也不例外。我们可以通过分析一些优秀的开源 Bash 脚本,来学习它们的结构、功能和设计思路,了解如何更好地组织代码,提高代码的可读性、可维护性和可扩展性。

以Bash Script Template这个开源项目为例,它为我们提供了一个非常实用的 Bash 脚本模板,包含了许多最佳实践和有用的函数,很适合拿来分析学习。

这个项目的核心文件主要有template.sh、source.sh和script.sh ,以及一个辅助生成工具build.sh 。

template.sh是一个完全自包含的脚本,它整合了其他两个关键文件的功能。打开template.sh,可以看到它首先设置了一些脚本执行的基本选项,比如:


#!/bin/bash

# 确保脚本在遇到错误时立即退出

set -o errexit

# 确保脚本在使用未定义变量时抛出错误

set -o nounset

# 确保脚本在管道命令中的任何一个命令失败时,整个管道都返回错误

set -o pipefail

这些选项的设置是非常重要的最佳实践,set -o errexit能让脚本在执行过程中一旦遇到任何错误(命令返回非零退出状态码)就立即停止执行,避免错误的进一步扩散,防止脚本继续执行可能导致的更多问题 。set -o nounset可以帮助我们在脚本中尽早发现使用未定义变量的情况,避免因为变量拼写错误或未初始化而导致的难以排查的问题 。set -o pipefail则保证了在使用管道连接多个命令时,只要其中一个命令执行失败,整个管道操作就会返回错误,而不是像默认情况下,即使管道中的某个命令失败,后续命令仍可能继续执行,导致结果不符合预期 。

source.sh包含了一系列不太需要修改的基础函数,例如用于解析命令行参数的函数:


parse_args() {

while [[ $# -gt 0 ]]; do

key="$1"

case $key in

-h|--help)

show_help

exit 0

;;

-v|--version)

show_version

exit 0

;;

*)

# 处理其他参数

;;

esac

shift

done

}

这个函数通过while循环遍历命令行参数,使用case语句根据不同的参数选项执行相应的操作。当遇到-h或–help参数时,调用show_help函数显示帮助信息并退出脚本;当遇到-v或–version参数时,调用show_version函数显示脚本版本信息并退出 。对于其他未处理的参数,可以在*)分支中进行相应的处理,这种命令行参数解析的方式清晰明了,易于维护和扩展,当需要添加新的参数选项时,只需要在case语句中增加一个分支即可。

script.sh提供了示例脚本,演示如何引入这些函数并进行定制化扩展。比如在script.sh中可能会有这样的代码:


#!/bin/bash

# 引入source.sh中的函数

source source.sh

# 调用解析命令行参数的函数

parse_args "$@"

# 这里可以添加自定义的脚本逻辑

echo "Custom script logic starts here..."

这段代码首先通过source source.sh引入了source.sh中定义的函数,使得在script.sh中可以使用这些基础函数 。然后调用parse_args “$@”函数来解析命令行参数,”$@”表示传递给脚本的所有参数 。之后就可以在脚本中添加自定义的逻辑,实现具体的功能需求,这种模块化的设计方式使得代码结构清晰,各个部分的职责明确,基础函数在source.sh中定义和维护,而具体的业务逻辑在script.sh中进行扩展和定制,提高了代码的可维护性和可复用性 。

通过分析这个开源 Bash 脚本项目,我们可以学习到如何合理设置脚本的执行选项,如何设计和实现通用的基础函数,以及如何通过模块化的方式组织代码,使脚本更易于理解、维护和扩展 。在实际的 Bash 脚本编写中,我们可以借鉴这些思路和方法,提升自己的脚本编程水平 。

六、总结与拓展

(一)学习总结

通过前面的学习,我们对 Bash 有了全面而深入的了解。从基础的定义和作用,到与其他 Shell 的比较,我们认识到 Bash 在 Linux 和 Unix 系统中的重要地位以及它的独特优势。在环境搭建和准备工作中,掌握了安装 Linux 系统或 WSL,熟悉终端操作,以及选择合适文本编辑器的方法,为后续学习打下了坚实基础。

在 Bash 基础语法学习中,了解了脚本的创建与运行、注释的使用、变量的定义与访问、条件判断语句和循环结构的运用,这些都是 Bash 编程的基石。例如,if语句让我们能够根据不同条件执行不同操作,for循环和while循环则实现了代码的重复执行,大大提高了编程的灵活性和效率。

Bash 进阶知识部分,学习了函数的定义与调用、输入输出重定向、管道与过滤以及数组的操作。函数使代码具有更好的可复用性,输入输出重定向让我们能够灵活地处理命令的输入和输出,管道与过滤实现了多个命令的协同工作,数组则方便了数据的存储和管理 。

在实战应用中,通过编写自动化脚本和分析开源 Bash 脚本,将所学知识应用到实际场景中。备份脚本和文件整理脚本展示了如何利用 Bash 解决日常工作中的实际问题,而分析开源 Bash 脚本则让我们学习到了优秀的代码结构和设计思路,拓宽了编程视野 。

(二)进一步学习建议

学习正则表达式:正则表达式是一种强大的文本模式匹配工具,在 Bash 中与grep、sed、awk等文本处理工具结合使用,可以实现更复杂的文本处理任务。例如,使用正则表达式可以在大量文本中精确地搜索特定模式的字符串,对文本进行高效的查找、替换、提取等操作。推荐阅读《正则表达式必知必会》《精通正则表达式》等书籍,系统地学习正则表达式的语法和应用技巧,通过实际练习加深对正则表达式的理解和掌握。

学习 Bash 脚本优化技巧:随着脚本复杂度的增加和处理数据量的增大,脚本的执行效率变得尤为重要。可以学习一些优化 Bash 脚本的方法,如减少命令执行次数、使用内建命令、避免不必要的子进程、合理使用并行处理等。例如,尽量使用内建的cd命令代替外部的cd命令,因为内建命令不需要启动新的进程,执行速度更快;在处理独立任务时,使用 Bash 的并行处理功能可以提高执行效率 。可以参考相关的技术文章和博客,了解实际项目中的优化经验和案例,不断提升自己编写高效 Bash 脚本的能力。

与其他编程语言结合使用:Bash 虽然功能强大,但在处理一些复杂的业务逻辑和大规模数据处理时,可能存在一定的局限性。将 Bash 与其他编程语言,如 Python、C 等结合使用,可以发挥各自的优势,实现更强大的功能。例如,Bash 擅长系统管理和脚本自动化,而 Python 具有丰富的库和强大的数据处理能力,将两者结合,可以先用 Bash 完成系统相关的操作,再利用 Python 进行复杂的数据处理和分析。可以通过实际项目来学习如何在不同编程语言之间进行协作,提高自己解决复杂问题的能力 。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容