Bash 脚本编程入门教程

发布于 2025-09-27 分类: Linux

Bash 脚本编程:程序员的“偷懒”艺术

欢迎来到本系列教程的最后一部分。在这里,我们将一起探索一个强大的工具——脚本编程

本篇教程旨在为你提供一个 Bash 脚本的快速入门,让你掌握足以应对日常工作的实用技能。内容整合了前面章节的许多知识点,如果你对某些命令感到陌生,不妨回顾一下之前的章节。


究竟什么是脚本?

在计算机领域,“脚本”(Script)与戏剧中的“剧本”非常相似。剧本规定了演员的台词和动作;而 Bash 脚本则是一份写给计算机的“剧本”,它包含了一系列需要执行的命令。

通过编写脚本,我们可以让计算机自动完成一系列操作,而无需我们手动逐条输入命令。这对于处理频繁或重复性的任务特别有用。

Bash 脚本由一个叫做“解释器”(Interpreter)的程序来读取和执行。Linux 系统中有多种解释器,但我们学习的是 Bash Shell,所以这里我们专注于 Bash 脚本。

请记住一个核心原则:

任何你能在命令行中运行的命令,都可以放进脚本里,其行为完全一致。反之亦然,脚本中的任何命令,也可以直接在命令行中运行,效果也完全相同。

理解这一点至关重要。当你在构建脚本、测试某个部分的功能时,最简单的方法就是直接在命令行里运行对应的命令。

脚本本质上只是一个纯文本文件,你可以用任何文本编辑器(例如我们学过的 vi)来创建它。


一个简单的例子

让我们从一个简单的脚本开始。我建议你亲手创建这个文件并运行它,以感受其工作方式。这个脚本会先在屏幕上打印一条消息,然后列出当前目录下的所有文件。

1. 创建脚本文件 myscript.sh

#!/bin/bash
# 这是一个简单的演示脚本
# 作者: Ryan, 日期: 2025-09-04

echo "以下是您当前目录中的文件:"
ls

2. 赋予脚本执行权限

一个脚本文件必须拥有“执行权限”才能运行。我们使用 chmod 命令来添加权限。

chmod 755 myscript.sh

755 是一个常用的权限设置,表示文件所有者拥有读、写、执行权限,而其他用户只有读和执行权限。

3. 运行脚本

./myscript.sh

4. 查看输出

运行后,你将看到类似下面的输出:

以下是您当前目录中的文件:
barry.txt   bob   example.png   firstfile   foo1   myoutput   video.mpeg

脚本构成要素详解

上面的例子虽然简单,但包含了很多关键点。让我们来逐一解析。

1. Shebang (#!)

脚本的第一行必须是 #!/bin/bash
这一行被称为 Shebang,它的作用是告诉系统应该使用哪个解释器来执行这个文件。

  • #! 是固定格式,必须出现在文件的最开头,前面不能有任何空格或空行。
  • /bin/bash 是 Bash 解释器的路径。

如果你不确定解释器的路径,可以使用 which 命令查找:

$ which bash
/bin/bash

$ which ls
/usr/bin/ls

虽然省略 Shebang 有时也能正常运行(因为当前 Shell 可能会默认自己为解释器),但为了保证脚本在任何环境下都能被正确解释,强烈建议始终添加 Shebang

2. 脚本命名

Linux 是一个“无扩展名”的系统,脚本文件的名称不会影响其运行。虽然我们通常会给 Bash 脚本加上 .sh 后缀以方便识别,但这纯粹是习惯,并非强制要求。你可以将脚本命名为 myscript 甚至 myscript.jpg,它依然能正常运行。

3. 注释 (#)

在脚本中,以 # 开头的任何内容都会被解释器忽略,它们是写给人看的注释

# 这是一个占据整行的注释
ls -l # 这是跟在命令后面的注释

一个好的编程习惯是在脚本开头写明脚本的功能、作者和创建日期,并在代码中对复杂的或关键的步骤添加注释。

4. 为什么需要 ./ 来执行?

你可能注意到我们使用 ./myscript.sh 而不是直接用 myscript.sh 来运行脚本。这其实是 Linux 的一种安全机制。

当你输入一个命令(如 ls)时,系统并不会搜索整个硬盘,而是会查找一个名为 PATH 的环境变量所定义的目录列表。

你可以用 echo 查看你的 PATH 变量:

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/games

系统会依次在这些目录(以冒号分隔)中寻找你输入的命令。它不会默认在你的当前目录中寻找。

通过提供路径(./),我们明确地告诉系统:“不要去 PATH 里找了,就在我指定的这个位置执行文件”。其中:

  • . 代表当前目录。
  • ./myscript.sh 就表示“当前目录下的 myscript.sh 文件”。

这种机制可以防止恶意行为。例如,如果有人在一个公共目录里创建了一个名为 ls 的恶意脚本,而你恰好在该目录想用 ls 命令查看文件,如果没有 ./ 的保护机制,你可能会无意中运行那个恶意脚本。


核心利器:变量

变量是用来存储数据的容器,在脚本中非常有用。

基本变量

  • 定义变量变量名=值 (注意:等号两边不能有空格)
  • 使用变量$变量名
# 脚本: variable_example.sh
#!/bin/bash
# 变量使用示例

name='Ryan'
echo "你好, $name"

运行 ./variable_example.sh 会输出 你好, Ryan

特殊变量 (预设变量)

运行脚本时,系统会自动为我们设置一些特殊的变量:

  • $0:脚本本身的名称。
  • $1 - $9:传递给脚本的命令行参数。$1 是第一个参数,$2 是第二个,以此类推。
  • $#:传递给脚本的命令行参数的总个数。
  • $*:所有命令行参数组成的单个字符串。

示例脚本 arguments.sh:

#!/bin/bash
# 特殊变量演示

echo "脚本名称是: $0"
echo "共传入了 $# 个参数"
echo "所有参数是: $*"
echo "第二个参数是: $2"

运行与输出:

$ ./arguments.sh bob fred sally
脚本名称是: ./arguments.sh
共传入了 3 个参数
所有参数是: bob fred sally
第二个参数是: fred

命令替换 (反引号或 $())

我们可以将一个命令的输出结果存入一个变量。这可以通过反引号 (`) 或更现代的 $(...) 语法实现。

示例脚本 backticks.sh:

#!/bin/bash
# 命令替换演示

# 统计 testfile.txt 文件的行数并存入变量 lines
# $1 代表传入的第一个参数,这里即 'testfile.txt'
lines=`cat $1 | wc -l`
# 推荐使用 $(...) 写法,更清晰且支持嵌套: lines=$(wc -l < $1)

echo "文件 $1 中共有 $lines 行。"

运行与输出: (假设 testfile.txt 有12行)

$ ./backticks.sh testfile.txt
文件 testfile.txt 中共有 12 行。

实战演练:一个简单的备份脚本

现在,让我们把所学知识应用到一个有用的脚本中。假设我需要定期备份 ~/projects 目录下的某个项目文件夹到 ~/projectbackups 目录。

#!/bin/bash
# 备份单个项目目录
# 作者: Ryan, 日期: 2025-09-04

# 格式化当前日期为 YYYY-MM-DD
todays_date=$(date +%F)

# 创建备份目录,格式为:项目名_日期
mkdir ~/projectbackups/$1_$todays_date

# 递归复制项目目录到备份目录
cp -R ~/projects/$1 ~/projectbackups/$1_$todays_date

echo "项目 $1 的备份已完成!"

运行与输出:

$ ./projectbackup.sh ocelot
项目 ocelot 的备份已完成!

这个脚本通过使用 $1 作为项目名,变得非常通用。我的同事也可以直接使用它来备份他们自己的项目,而无需修改脚本代码。编写灵活、通用的脚本能极大地提高效率。


让脚本更智能:If 条件判断

上面的备份脚本虽然能用,但如果使用者输错了参数,或者目录不存在,脚本就会报错。我们可以使用 if 语句来增加判断逻辑,让脚本更健壮。

(这部分内容相对进阶,如果觉得困惑可以先跳过。即使只掌握以上知识,也足以编写出许多实用的脚本。)

增强版备份脚本 projectbackup_v2.sh:

#!/bin/bash
# 更健壮的项目备份脚本
# 作者: Ryan, 日期: 2025-09-04

# 检查参数数量是否不为1
if [ $# != 1 ]; then
    echo "用法: $0 <要备份的目录名>"
    exit 1
fi

# 检查源项目目录是否存在
if [ ! -d ~/projects/$1 ]; then
    echo "错误: 目录 ~/projects/$1 不存在,请检查拼写。"
    exit 1
fi

todays_date=$(date +%F)
backup_dir=~/projectbackups/$1_$todays_date

# 检查今天是否已经备份过
if [ -d $backup_dir ]; then
    echo "今天似乎已经备份过该项目,是否覆盖? (y/n)"
    read answer # 读取用户输入
    if [ "$answer" != "y" ]; then
        echo "操作取消。"
        exit 0
    fi
else
    mkdir -p $backup_dir # 如果不存在,则创建
fi

cp -R ~/projects/$1 $backup_dir
echo "项目 $1 的备份已完成!"

代码解析:

  • if [ ... ]; then ... fiif 语句的基本结构。fiif 的倒写,表示条件块结束。
  • [ $# != 1 ]: [test 命令的简写。$# 是参数个数,!= 是不等于。这行代码判断“参数个数是否不等于1”。

    注意[] 与其中的表达式之间必须有空格!

  • exit: 立即退出脚本。通常 exit 0 表示成功,非0值表示错误。
  • [ ! -d ~/projects/$1 ]: -d 用来测试一个路径是否存在且为目录,! 表示“非”(not)。所以这行代码判断“如果 ~/projects/$1 不是一个目录”。
  • read answer: 读取用户的键盘输入,并将其存入 answer 变量。

为了代码的可读性,if 块内的代码通常会进行缩进,虽然这不是强制的,但这是一个非常好的编程习惯。


知识点总结

  • #!
    : Shebang。指定脚本的解释器。
  • echo
    : 在屏幕上打印消息。
  • which
    : 查找一个命令的完整路径。
  • $
    : 引用变量值的前缀。
  • ``$( )
    : 命令替换。用于将命令的输出赋值给变量。
  • date
    : 显示或格式化日期时间。
  • if [ ] then else fi
    : 实现基本的条件逻辑判断。
  • 核心原则
    : 命令行能做的,脚本就能做,反之亦然。
  • 格式要求
    : Bash 脚本对空格非常敏感,尤其是在变量赋值和 if 判断中,要特别注意。

课后练习

现在,运用你学到的所有知识,来自动化一些任务吧!

  1. 编写你自己的备份脚本。可以从一个最简单的版本开始,然后逐步增加功能,比如添加日期、检查目录是否存在、压缩备份文件等。
  2. 编写一个目录报告脚本。这个脚本接受一个目录名作为参数,然后输出关于该目录的报告,可以包括:
    • 目录中有多少个文件?
    • 有多少个子目录?
    • 哪个文件最大?
    • 哪个文件是最新修改的?
    • 统计目录中文件分属哪些不同用户。
    • 任何你能想到的其他信息...

-- 感谢阅读 --