云计算百科
云计算领域专业知识百科平台

第七章 Shell脚本编程

第07章 Shell脚本编程

本章是Linux学习的重要转折点。前面几章你学的都是"手动操作"——一行一行敲命令,就像用筷子一口一口吃饭。而这一章,你要学的是"自动化"——写一个脚本,让电脑自己干活,就像给厨师一份菜谱,他照着做就行。

学完本章,你就能让Linux替你完成重复性的工作,从此告别机械劳动,真正体验到"懒人改变世界"的乐趣。


7.1 什么是 Shell

7.1.1 先讲个故事:你、翻译官和外国人

想象一下这样一个场景:

你只懂中文,现在面前站着一个只懂英语的外国人。你想让他帮你搬个箱子,但你说的中文他听不懂,他说的英语你也听不懂。怎么办?

这时候你需要一个"翻译官"。你对翻译官说中文"请帮我搬一下箱子",翻译官把它翻成英语"Please help me move the box",外国人听懂了,就把箱子搬走了。

在Linux的世界里,你就是那个"只懂中文的人",Linux内核就是那个"只懂英语的外国人",而Shell就是你中间的翻译官。

  • 你:通过键盘输入命令,比如 ls、cd、mkdir
  • Shell(翻译官):接收你输入的命令,翻译成内核能听懂的"机器语言",交给内核去执行
  • Linux内核(外国人):真正干活的,帮你创建文件夹、读取文件、管理内存

7.1.2 Shell 的准确定义

用稍微正式一点的话来说:

Shell 是一个命令行解释器(Command Interpreter),它位于用户和操作系统内核之间,负责接收用户输入的命令,将其解释并传递给内核执行,然后把执行结果返回给用户。

打个比方:

  • 内核就像是汽车的发动机,是真正的动力来源,但它不会自己运转
  • Shell就像是方向盘和油门,你通过它来控制发动机
  • 你就是驾驶员,决定要去哪里

7.1.3 Shell 和图形界面有什么区别

你可能会问:那我平时用鼠标点击文件夹、双击打开文件,那不也是在操作电脑吗?这跟Shell有什么关系?

好问题!这里有两种"翻译官":

  • 图形界面(GUI):就是你平时看到的窗口、图标、按钮。你用鼠标点击,它帮你翻译成命令。就像一个会手语的翻译官,用手势交流。

  • 命令行界面(CLI / Shell):就是你敲字的那种黑底白字的窗口。你用键盘输入文字命令。就像一个用嘴巴说话的翻译官,用语言交流。

  • 两种方式都能达到目的,但Shell有几个巨大的优势:

    • 效率高:一行命令能完成图形界面要点十几次的操作
    • 可自动化:可以把很多命令写成一个文件,让电脑自动执行
    • 远程管理方便:通过SSH连接服务器时,通常只能用命令行
    • 功能更强大:很多高级操作图形界面根本做不到

    7.1.4 常见的 Shell 有哪些

    就像翻译官有很多种(英语翻译、法语翻译、日语翻译),Shell也有不同的"品种":

    Shell名称全称特点
    sh Bourne Shell 最古老的Shell,几乎所有Unix系统都有
    bash Bourne Again Shell sh的升级版,Linux默认Shell,最常用
    csh C Shell 语法类似C语言,BSD系统常用
    tcsh TENEX C Shell csh的增强版
    zsh Z Shell 功能最强,支持主题和插件,Mac默认
    fish Friendly Interactive Shell 对新手最友好,开箱即用

    其中,bash 是最重要的,因为:

  • 几乎所有Linux发行版默认都用bash
  • 大部分教程和文档都基于bash
  • 你面试时被问的也是bash
  • 所以本章我们重点学习bash。

    7.1.5 查看你的 Shell

    打开终端,输入以下命令可以查看你当前使用的Shell:

    echo $SHELL

    输出可能类似这样:

    /bin/bash

    这说明你当前用的是bash。

    如果你想知道系统里装了哪些Shell,可以输入:

    cat /etc/shells

    输出类似:

    /bin/sh
    /bin/bash
    /usr/bin/sh
    /usr/bin/bash
    /bin/zsh

    如果你想临时切换到别的Shell试试,直接输入它的名字就行,比如输入 zsh 就切换到zsh,输入 exit 退回原来的Shell。

    7.1.6 Shell 脚本是什么

    前面我们说的都是"交互式Shell"——你输一条命令,它执行一条。这就像你口述菜谱,厨师做一道菜。

    但如果你想一次做一桌菜呢?难道要一道一道口述?当然不用,你把菜谱写在纸上,交给厨师,他照着纸一张张做就行。

    Shell脚本就是这个"写在纸上的菜谱"。

    把一系列命令写到一个文本文件里,然后让Shell一次性执行这个文件里的所有命令,这个文件就叫Shell脚本(也叫Shell脚本程序)。

    比如下面就是一个最简单的Shell脚本:

    #!/bin/bash
    # 这是一个打招呼的脚本
    echo "你好,世界!"
    echo "今天是:"
    date
    echo "当前登录用户是:"
    whoami

    把上面这段代码保存到一个文件里(比如 hello.sh),然后运行它,它就会依次执行里面的每一条命令。

    这就是Shell脚本的魔力——你写一次,可以运行无数次。


    7.2 Bash Shell 基础

    7.2.1 创建你的第一个脚本

    纸上得来终觉浅,我们马上动手写第一个脚本。

    第一步:创建脚本文件

    打开终端,输入:

    vim first.sh

    这会用vim编辑器打开(或创建)一个名为 first.sh 的文件。文件名你可以随便起,但习惯上Shell脚本用 .sh 后缀,就像Word文档用 .docx,图片用 .jpg 一样,方便识别。

    第二步:输入内容

    按 i 进入插入模式,输入以下内容:

    #!/bin/bash
    # 这是我的第一个Shell脚本
    echo "Hello World!"
    echo "我的第一个脚本运行成功了!"

    输入完后,按 Esc,然后输入 :wq 保存退出。

    第三步:给脚本加执行权限

    在Linux里,新建的文件默认是没有"执行权限"的。就像你写了一份菜谱,但厨师不让你贴在厨房墙上,你得先获得许可。

    输入以下命令赋予执行权限:

    chmod +x first.sh

    chmod 是"change mode"(改变模式)的缩写,+x 表示"加上执行(eXecute)权限"。

    第四步:运行脚本

    ./first.sh

    如果你看到下面的输出,恭喜你,你的第一个脚本运行成功了!

    Hello World!
    我的第一个脚本运行成功了!

    7.2.2 理解脚本的结构

    我们来逐行分析刚才那个脚本:

    #!/bin/bash

    这一行叫Shebang(读作"希邦"),它是 # 和 ! 两个符号的组合。它的作用是告诉系统:“请用 /bin/bash 这个程序来解释执行本文件”。

    就像你写了一份法语菜谱,在最上面标注"本菜谱用法语阅读",这样厨师一看就知道该怎么读。

    注意:Shebang必须是文件的第一行,而且 # 和 ! 之间不能有空格。

    # 这是我的第一个Shell脚本

    这一行是注释。以 # 开头的行会被Shell忽略,不执行。它的作用是给人看的,用来解释代码的作用。写注释是一个好习惯,就像在书本上做批注,帮自己(和同事)理解代码。

    echo "Hello World!"

    echo 是"回声"的意思,它的作用是把后面的内容"喊"出来——也就是输出到屏幕上。就像你在山谷里喊"你好",山谷回应你"你好"一样。

    7.2.3 脚本的几种运行方式

    除了刚才的 ./first.sh,还有其他几种运行脚本的方式,各有用途:

    方式一:相对路径运行(最常用)

    ./first.sh

    需要先 chmod +x 加执行权限。./ 表示"当前目录下"。

    方式二:绝对路径运行

    /home/user/first.sh

    同样需要执行权限。好处是不管你在哪个目录都能运行。

    方式三:用bash命令运行(不需要执行权限)

    bash first.sh

    这种方式直接告诉系统"用bash来运行这个文件",所以不需要加执行权限,也不需要Shebang行。适合调试的时候用。

    方式四:用source或点号运行

    source first.sh
    # 或者
    . first.sh

    这种方式的特殊之处在于:它在当前Shell环境中执行脚本,而不是开一个新的子Shell。

    什么意思呢?打个比方:

    • 前三种方式就像你去朋友家做饭——用的是朋友家的厨房(子Shell),你做完了出来,朋友家的厨房有什么变化你带走不了。
    • source方式就像在你自己家做饭——用的是你自己的厨房(当前Shell),做的菜和产生的影响都在你自己家。

    这种区别在后面讲变量的时候会很重要。

    7.2.4 脚本的基本书写规范

    写Shell脚本就像写作文,有一些格式上的"规矩":

    #!/bin/bash
    #=============================================================
    # 脚本名称:backup.sh
    # 脚本功能:备份重要文件
    # 作者:张三
    # 创建日期:2024-01-15
    # 修改记录:无
    #=============================================================

    # ——– 第一部分:定义变量 ——–
    BACKUP_DIR="/backup"
    SOURCE_DIR="/home/user/data"

    # ——– 第二部分:开始备份 ——–
    echo "开始备份…"
    mkdir -p $BACKUP_DIR
    cp -r $SOURCE_DIR $BACKUP_DIR/
    echo "备份完成!"

    注意几个要点:

  • 第一行一定是Shebang:#!/bin/bash
  • 多写注释:解释代码的作用,方便日后维护
  • 适当留空行:让代码看起来不拥挤,有层次感
  • 变量名用大写:这是约定俗成的习惯,方便区分变量和命令
  • 代码对齐:提高可读性
  • 7.2.5 一个稍微复杂一点的示例

    来感受一下Shell脚本能做什么。下面这个脚本可以显示系统的基本信息:

    #!/bin/bash
    # 显示系统基本信息

    echo "========== 系统信息报告 =========="
    echo ""

    echo "【主机名】"
    hostname
    echo ""

    echo "【系统版本】"
    cat /etc/os-release | head -n 5
    echo ""

    echo "【内核版本】"
    uname -r
    echo ""

    echo "【CPU信息】"
    lscpu | grep "Model name"
    echo ""

    echo "【内存使用情况】"
    free -h
    echo ""

    echo "【磁盘使用情况】"
    df -h
    echo ""

    echo "【当前时间】"
    date
    echo ""
    echo "========== 报告结束 =========="

    你看,只要运行这一个脚本,就能一次性看到主机名、系统版本、内核版本、CPU、内存、磁盘、时间等信息,再也不用一条一条命令去敲了。这就是脚本的魅力!


    7.3 变量定义与使用

    7.3.1 什么是变量——用盒子来理解

    还记得小学数学吗?老师会说:“设 x = 5”,然后问 “x + 3 等于多少?”

    这里的 x 就是一个变量——它就像一个贴了标签的盒子,你可以往里面放东西(赋值),也可以拿出来用(引用)。

    在Shell中,变量也是这个概念:

    name="张三"

    这行代码做了什么?它创建了一个名叫 name 的盒子,在里面放了"张三"这个字符串。

    以后你想用"张三"的时候,不用每次都写一遍,只要说"把name盒子里的东西给我"就行:

    echo $name

    输出:

    张三

    7.3.2 变量定义的规则——最容易踩的坑

    Shell里定义变量有一个非常奇葩的规则,很多新手都会踩坑:

    等号两边绝对不能有空格!

    # 正确写法(等号两边没空格)
    name="张三"

    # 错误写法1(等号左边有空格)
    name ="张三"

    # 错误写法2(等号右边有空格)
    name= "张三"

    # 错误写法3(两边都有空格)
    name = "张三"

    为什么Shell对空格这么敏感?因为在Shell里,空格是"命令和参数的分隔符"。

    • name="张三" 被理解为:给变量name赋值"张三"
    • name = "张三" 被理解为:运行一个名叫 name 的命令,参数是 = 和 "张三"

    这跟其他编程语言(如Python、Java)完全不一样,所以从其他语言转过来的同学特别容易出错。

    记住这个口诀:Shell赋值,等号贴脸,不贴就报错。

    7.3.3 变量的命名规则

    变量名不能随便起,有规矩:

  • 只能用字母、数字、下划线,不能有其他符号
  • 不能以数字开头
  • 区分大小写(Name和name是两个不同的变量)
  • 不能使用Shell关键字(如if、for、while等)
  • # 合法的变量名
    name="张三"
    age=25
    _user="admin"
    my_home="/home/user"
    URL="https://www.example.com"

    # 非法的变量名
    2name="李四" # 不能以数字开头
    my-name="王五" # 不能用减号
    my name="赵六" # 不能有空格
    if="测试" # 不能用关键字

    7.3.4 变量的赋值方式

    方式一:直接赋值

    name="张三"
    age=25
    height=175

    注意:等号右边的值如果有空格,必须用引号包起来:

    # 正确
    greeting="你好 世界"

    # 错误(Shell会以为 greeting="你好" 然后世界是另一个命令)
    greeting=你好 世界

    方式二:将命令结果赋给变量

    用反引号 `命令` 或者 $() 包裹命令:

    # 两种写法效果一样
    current_date=`date +%Y-%m-%d`
    current_date=$(date +%Y-%m-%d)

    echo $current_date

    推荐用 $() 这种写法,因为它更清晰,而且支持嵌套。

    # 获取当前目录下的文件数量
    file_count=$(ls | wc -l)
    echo "当前目录有 $file_count 个文件"

    # 获取当前用户名
    user=$(whoami)
    echo "你好,$user!"

    方式三:交互式赋值(用read命令,后面会详细讲)

    echo "请输入你的名字:"
    read name
    echo "你好,$name!"

    7.3.5 使用变量——美元符号$

    定义变量时不用 $,但使用变量时必须加 $:

    name="张三"

    # 定义时不用$
    # 使用时必须用$
    echo $name # 输出:张三
    echo "我的名字是$name" # 输出:我的名字是张三

    这就像:你往盒子里放东西的时候不用说"这是个盒子"(定义),但你想从盒子里拿东西的时候,得指明"我要从那个盒子里拿"(引用)。

    花括号的作用

    有时候变量名会和后面的文字"粘"在一起,Shell分不清哪里是变量名:

    fruit="苹果"
    echo "我喜欢吃$fruit汁" # Shell可能以为变量名是fruit汁

    这时候用花括号 {} 明确告诉Shell变量名的范围:

    fruit="苹果"
    echo "我喜欢吃${fruit}汁" # 输出:我喜欢吃苹果汁

    养成好习惯:在字符串中使用变量时,都用 ${变量名} 这种写法。

    7.3.6 只读变量

    有些变量你不想让别人修改,可以用 readonly 声明为只读:

    PI=3.14159
    readonly PI

    PI=3.14 # 会报错:bash: PI: readonly variable

    就像图书馆的书,你可以看,但不能在上面写字。

    7.3.7 删除变量

    用 unset 可以删除变量:

    name="张三"
    echo $name # 输出:张三

    unset name
    echo $name # 输出:空(什么都没有)

    注意:只读变量不能被unset删除。

    7.3.8 变量的类型——Shell没有真正的类型

    在其他语言里(如Java、C),变量有明确的类型:整数就是整数,字符串就是字符串,不能混。

    但在Shell里,所有变量的值都是字符串!

    age=25

    你以为 age 是个数字25?不,Shell觉得它是字符串"25"。只不过在你需要做数学运算的时候,Shell会临时把它当数字处理。

    a=10
    b=20

    # 这不是数学加法,是字符串拼接
    c=$a+$b
    echo $c # 输出:10+20(不是30)

    要做真正的数学运算,需要用特殊的方法,我们后面7.6节会讲。

    7.3.9 完整示例:个人信息卡片

    #!/bin/bash
    # 个人信息卡片生成器

    # 定义变量
    name="李明"
    age=22
    gender="男"
    city="北京"
    job="程序员"

    # 输出信息卡片
    echo "========================="
    echo " 个人信息卡片"
    echo "========================="
    echo "姓名:${name}"
    echo "年龄:${age}岁"
    echo "性别:${gender}"
    echo "城市:${city}"
    echo "职业:${job}"
    echo "========================="
    echo "生成时间:$(date '+%Y-%m-%d %H:%M:%S')"

    运行结果:

    =========================
    个人信息卡片
    =========================
    姓名:李明
    年龄:22岁
    性别:男
    城市:北京
    职业:程序员
    =========================
    生成时间:2024-01-15 14:30:25


    7.4 环境变量与位置参数

    7.4.1 局部变量 vs 环境变量——房间里的灯vs楼道里的灯

    前面我们定义的变量都是局部变量(也叫自定义变量)。它们有个特点:只在当前Shell中有效,子进程看不到。

    打个比方:

    • 你在自己房间里开了一盏灯(定义局部变量),只有你在房间里能看到,隔壁房间的人看不到。
    • 如果你在楼道里开了一盏灯(环境变量),所有房间的人都能看到。

    看这个例子:

    # 定义局部变量
    my_var="你好"

    # 开一个子Shell
    bash

    # 在子Shell里尝试访问
    echo $my_var # 什么都不会输出!

    因为 my_var 是局部变量,子Shell看不到。

    如果想让所有子进程都能看到,就要用 export 把它变成环境变量:

    # 定义并导出为环境变量
    export my_var="你好"

    # 开一个子Shell
    bash

    # 在子Shell里访问
    echo $my_var # 输出:你好

    7.4.2 export 命令详解

    export 的作用就是把局部变量"升级"为环境变量。有两种写法:

    # 写法一:先定义,再导出
    NAME="张三"
    export NAME

    # 写法二:定义的同时导出(推荐)
    export NAME="张三"

    环境变量的典型用途是配置程序的行为。比如:

    # 设置语言环境
    export LANG="zh_CN.UTF-8"

    # 设置Java路径
    export JAVA_HOME="/usr/lib/jvm/java-11"

    # 设置PATH(让系统知道去哪里找程序)
    export PATH=$PATH:/usr/local/myapp/bin

    7.4.3 查看环境变量

    查看所有环境变量:

    env
    # 或者
    printenv

    查看单个环境变量:

    echo $PATH
    echo $HOME
    echo $USER

    查看所有变量(包括局部变量和环境变量):

    set

    7.4.4 常见系统环境变量

    系统预先定义好了很多环境变量,常用的有:

    变量名含义示例值
    $HOME 用户主目录 /home/user
    $USER 当前用户名 user
    $PWD 当前工作目录 /home/user/docs
    $SHELL 当前Shell路径 /bin/bash
    $PATH 命令搜索路径 /usr/bin:/bin:…
    $HOSTNAME 主机名 myserver
    $LANG 语言设置 zh_CN.UTF-8
    $UID 用户ID 1000
    $RANDOM 随机数(0-32767) 29384
    $? 上一条命令的返回值 0

    其中 $PATH 非常重要,它决定了Shell去哪些目录找可执行命令。

    举个例子:

    echo $PATH
    # 输出:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

    当你输入 ls 的时候,Shell会依次去这些目录里找有没有叫 ls 的程序,找到就执行。如果这些目录里都没有 ls,就会报"command not found"。

    7.4.5 让环境变量永久生效

    用 export 设置的环境变量,关闭终端后就没了。要想永久生效,要写到配置文件里:

    对当前用户生效:

    # 编辑 ~/.bashrc 文件
    vim ~/.bashrc

    # 在文件末尾加上
    export MY_VAR="你好"

    # 保存后让配置立即生效
    source ~/.bashrc

    对所有用户生效:

    # 编辑 /etc/environment 或 /etc/profile
    sudo vim /etc/profile

    # 在文件末尾加上
    export MY_VAR="你好"

    7.4.6 位置参数——给脚本传参数

    还记得怎么运行脚本吗?./script.sh

    如果我想在运行脚本的时候传一些数据进去呢?比如:

    ./greet.sh 张三 25

    这里 张三 和 25 就是传给脚本的参数。在脚本内部,用位置参数来接收它们:

    符号含义
    $0 脚本本身的文件名
    $1 第1个参数
    $2 第2个参数
    $3 第3个参数
    以此类推
    ${10} 第10个参数(两位数要加花括号)
    $# 参数的总个数
    $* 所有参数(作为一个整体字符串)
    $@ 所有参数(每个参数独立)
    $$ 当前脚本的进程号(PID)

    来个例子:

    #!/bin/bash
    # 参数演示脚本 param.sh

    echo "脚本名称:$0"
    echo "第一个参数:$1"
    echo "第二个参数:$2"
    echo "参数总个数:$#"
    echo "所有参数(\\$*):$*"
    echo "所有参数(\\$@):$@"
    echo "当前进程号:$$"

    运行:

    chmod +x param.sh
    ./param.sh 张三 李四 王五

    输出:

    脚本名称:./param.sh
    第一个参数:张三
    第二个参数:李四
    参数总个数:3
    所有参数($*):张三 李四 王五
    所有参数($@):张三 李四 王五
    当前进程号:12345

    7.4.7 $* 和 $@ 的区别

    这两个看起来一样,但在被引号包裹时有区别:

    #!/bin/bash
    # 区别演示 diff.sh

    echo "— 使用 \\$* —"
    for arg in "$*"
    do
    echo $arg
    done

    echo "— 使用 \\$@ —"
    for arg in "$@"
    do
    echo $arg
    done

    运行:

    ./diff.sh aaa bbb ccc

    输出:

    — 使用 $* —
    aaa bbb ccc # 所有参数合成了一个整体
    — 使用 $@ —
    aaa # 每个参数独立
    bbb
    ccc

    记住这个区别:"$*" 把所有参数当成一个整体,"$@" 把每个参数当成独立的个体。通常用 "$@" 更多。

    7.4.8 位置参数的移位:shift

    shift 命令可以把所有位置参数左移一位:$2变成$1,$3变成$2,原来的$1被丢弃。

    #!/bin/bash
    # shift演示

    echo "移位前:\\$1=$1, \\$2=$2, \\$3=$3"
    shift
    echo "移位一次:\\$1=$1, \\$2=$2, \\$3=$3"
    shift 2
    echo "移位两次:\\$1=$1, \\$2=$2"

    运行 ./shift.sh A B C D E,输出:

    移位前:$1=A, $2=B, $3=C
    移位一次:$1=B, $2=C, $3=D
    移位两次:$1=E, $2=

    shift 常用在while循环中逐个处理参数:

    #!/bin/bash
    # 逐个处理参数

    while [ $# -gt 0 ]
    do
    echo "当前参数:$1"
    shift
    done

    7.4.9 实战:计算器脚本

    #!/bin/bash
    # 简单计算器 calc.sh

    if [ $# -ne 3 ]; then
    echo "用法:$0 数字 运算符 数字"
    echo "示例:$0 10 + 20"
    exit 1
    fi

    num1=$1
    operator=$2
    num2=$3

    case $operator in
    +) result=$(echo "$num1 + $num2" | bc) ;;
    ) result=$(echo "$num1$num2" | bc) ;;
    \\*) result=$(echo "$num1 * $num2" | bc) ;;
    /) result=$(echo "scale=2; $num1 / $num2" | bc) ;;
    *) echo "不支持的运算符:$operator"; exit 1 ;;
    esac

    echo "$num1 $operator $num2 = $result"


    7.5 字符串操作与数组

    7.5.1 字符串的两种表示方式

    在Shell中,字符串可以用单引号或双引号包裹,但两者有重要区别:

    单引号:原样输出,不解析变量

    name="张三"
    echo '你好,$name' # 输出:你好,$name(原样输出)

    双引号:会解析变量和命令

    name="张三"
    echo "你好,$name" # 输出:你好,张三(变量被解析了)

    打个比方:

    • 单引号就像一个"密不透风的盒子",里面是什么就是什么,不解释
    • 双引号就像一个"半透明的盒子",里面的变量和命令能被"看穿"并替换

    什么时候用单引号,什么时候用双引号?

    • 需要解析变量时用双引号:"你好,$name"
    • 不想被解析时用单引号:'$符号表示美元'
    • 不确定的时候,默认用双引号(更安全,避免空格问题)

    7.5.2 字符串的拼接

    Shell拼接字符串特别简单——直接挨着写就行:

    # 方式一:直接相连
    greeting="你好,""世界"
    echo $greeting # 输出:你好,世界

    # 方式二:用变量拼接
    part1="你好,"
    part2="
    世界"
    greeting=$part1$part2
    echo $greeting # 输出:你好,世界

    # 方式三:在双引号中拼接
    name="张三"
    greeting="
    你好,${name}!欢迎!"
    echo $greeting # 输出:你好,张三!欢迎!

    7.5.3 获取字符串长度

    用 ${#变量名} 获取字符串长度:

    str="Hello World"
    echo ${#str} # 输出:11

    name="张三"
    echo ${#name} # 输出:2(中文字符也算1个)

    7.5.4 截取子字符串

    截取字符串有好几种方式,记住下面这几种就够了:

    方式一:从第几个字符开始,截取几个字符

    语法:${变量名:起始位置:截取长度}

    注意:位置从0开始算。

    str="Hello World"

    # 从第0个位置截取5个字符
    echo ${str:0:5} # 输出:Hello

    # 从第6个位置截取5个字符
    echo ${str:6:5} # 输出:World

    # 从第6个位置截取到末尾(不写长度)
    echo ${str:6} # 输出:World

    方式二:从右边开始截取(用#和##)

    • ${变量名#匹配模式}:从左边删除最短匹配
    • ${变量名##匹配模式}:从左边删除最长匹配

    file="photo.tar.gz"

    # 删掉第一个.及左边所有内容
    echo ${file#*.} # 输出:tar.gz

    # 删掉最后一个.及左边所有内容
    echo ${file##*.} # 输出:gz

    方式三:从右边开始截取(用%和%%)

    • ${变量名%匹配模式}:从右边删除最短匹配
    • ${变量名%%匹配模式}:从右边删除最长匹配

    file="photo.tar.gz"

    # 删掉最后一个.及右边所有内容
    echo ${file%.*} # 输出:photo.tar

    # 删掉第一个.及右边所有内容
    echo ${file%%.*} # 输出:photo

    这些操作在处理文件名和扩展名时特别有用。

    7.5.5 字符串替换

    用 / 可以替换字符串中的内容:

    str="Hello World World"

    # 替换第一个匹配
    echo ${str/World/Linux} # 输出:Hello Linux World

    # 替换所有匹配(用两个/)
    echo ${str//World/Linux} # 输出:Hello Linux Linux

    # 从开头匹配替换(用#)
    str2="Hello World"
    echo ${str2/#Hello/Hi} # 输出:Hi World

    # 从末尾匹配替换(用%)
    echo ${str2/%World/Earth} # 输出:Hello Earth

    7.5.6 字符串综合示例

    #!/bin/bash
    # 字符串操作演示 string_ops.sh

    filename="report.2024.01.15.pdf"

    echo "原始字符串:$filename"
    echo "字符串长度:${#filename}"

    # 提取各部分
    echo "文件名(不含扩展名):${filename%.*}"
    echo "主扩展名:${filename##*.}"
    echo "日期部分:${filename#*.}" # 2024.01.15.pdf
    echo "去掉.pdf后:${filename%.pdf}" # report.2024.01.15

    # 替换
    echo "替换.为_:${filename//./_}" # report_2024_01_15_pdf

    # 截取
    echo "截取前6个字符:${filename:0:6}" # report

    7.5.7 数组——一排带编号的盒子

    变量是一个盒子,数组就是一排带编号的盒子。

    想象一下超市的储物柜,一排柜子,每个柜子都有编号(0号、1号、2号…),每个柜子里可以放不同的东西。

    在Shell中定义数组:

    # 方式一:用括号定义
    fruits=("苹果" "香蕉" "橘子" "葡萄")

    # 方式二:逐个赋值
    fruits[0]="苹果"
    fruits[1]="香蕉"
    fruits[2]="橘子"
    fruits[3]="葡萄"

    # 方式三:用declare声明
    declare -a colors
    colors[0]="红"
    colors[1]="绿"
    colors[2]="蓝"

    注意:数组的编号(索引)从0开始,不是从1开始!这是大部分编程语言的惯例。

    7.5.8 读取数组元素

    fruits=("苹果" "香蕉" "橘子" "葡萄")

    # 读取单个元素
    echo ${fruits[0]} # 输出:苹果
    echo ${fruits[1]} # 输出:香蕉
    echo ${fruits[2]} # 输出:橘子

    # 读取所有元素
    echo ${fruits[@]} # 输出:苹果 香蕉 橘子 葡萄
    echo ${fruits[*]} # 输出:苹果 香蕉 橘子 葡萄

    # 获取数组长度
    echo ${#fruits[@]} # 输出:4
    echo ${#fruits[*]} # 输出:4

    # 获取单个元素的长度
    echo ${#fruits[0]} # 输出:2("苹果"有2个字符)

    7.5.9 遍历数组

    #!/bin/bash
    # 遍历数组 array_loop.sh

    students=("张三" "李四" "王五" "赵六" "钱七")

    # 方式一:用for遍历值
    echo "=== 方式一 ==="
    for student in "${students[@]}"
    do
    echo "学生:$student"
    done

    # 方式二:用索引遍历
    echo "=== 方式二 ==="
    for i in "${!students[@]}"
    do
    echo "第$((i+1))个学生:${students[$i]}"
    done

    # 方式三:用while遍历
    echo "=== 方式三 ==="
    i=0
    while [ $i -lt ${#students[@]} ]
    do
    echo "索引$i${students[$i]}"
    let i++
    done

    7.5.10 数组的增删改

    #!/bin/bash
    # 数组增删改 array_ops.sh

    # 定义数组
    nums=(10 20 30 40 50)
    echo "原始数组:${nums[@]}"

    # 修改元素
    nums[2]=300
    echo "修改后:${nums[@]}" # 10 20 300 40 50

    # 在末尾添加元素
    nums+=(60 70)
    echo "添加后:${nums[@]}" # 10 20 300 40 50 60 70

    # 删除元素
    unset nums[1]
    echo "删除索引1后:${nums[@]}" # 10 300 40 50 60 70
    echo "索引1的值:${nums[1]}" # 空(被删除了)

    # 注意:删除后数组索引不连续!
    echo "数组长度:${#nums[@]}"

    7.5.11 关联数组(字典)

    Bash 4.0以上版本支持关联数组,就是可以用字符串做索引的数组(类似Python的字典):

    #!/bin/bash
    # 关联数组 assoc.sh

    # 声明关联数组
    declare -A person

    # 赋值(用字符串做索引)
    person[name]="张三"
    person[age]=25
    person[city]="北京"
    person[job]="程序员"

    # 读取
    echo "姓名:${person[name]}"
    echo "年龄:${person[age]}"
    echo "城市:${person[city]}"

    # 遍历所有值
    echo "所有信息:"
    for key in "${!person[@]}"
    do
    echo " $key = ${person[$key]}"
    done

    输出:

    姓名:张三
    年龄:25
    城市:北京
    所有信息:
    name = 张三
    age = 25
    city = 北京
    job = 程序员


    7.6 算术运算与expr

    7.6.1 Shell算术运算的痛点

    在其他语言里,做加减乘除特别简单:

    # Python
    a = 10
    b = 20
    c = a + b # c就是30

    但在Shell里,因为所有变量都是字符串,直接用 + 是不行的:

    a=10
    b=20
    c=$a+$b
    echo $c # 输出:10+20(不是30!)

    Shell觉得 $a 和 $b 是字符串"10"和"20",+ 只是它们中间的一个字符,拼在一起就是"10+20"。

    要告诉Shell"我要做数学运算,不是字符串拼接",需要用特殊的方式。

    7.6.2 方法一:expr命令

    expr 是"expression"(表达式)的缩写,专门用来做数学运算:

    a=10
    b=20

    # 加法
    sum=`expr $a + $b`
    echo $sum # 输出:30

    # 减法
    diff=$(expr $a – $b)
    echo $diff # 输出:-10

    # 乘法(注意:乘号要转义!)
    product=$(expr $a \\* $b)
    echo $product # 输出:200

    # 除法
    quotient=$(expr $b / $a)
    echo $quotient # 输出:2

    # 取余
    remainder=$(expr $b % 3)
    echo $remainder # 输出:2

    注意几个坑:

  • 运算符两边必须有空格:expr $a + $b 对,expr $a+$b 错
  • 乘号 * 必须转义写成 \\*:因为 * 在Shell里是通配符
  • 只支持整数运算:expr 10 / 3 结果是3,不是3.33
  • 7.6.3 方法二:双小括号 $(( ))(推荐)

    这是最常用、最方便的方式:

    a=10
    b=20

    # 加法
    echo $((a + b)) # 输出:30

    # 减法
    echo $((a b)) # 输出:-10

    # 乘法(不需要转义!)
    echo $((a * b)) # 输出:200

    # 除法
    echo $((b / a)) # 输出:2

    # 取余
    echo $((b % 3)) # 输出:2

    # 幂运算(expr不支持,但双括号支持)
    echo $((2 ** 10)) # 输出:1024

    双小括号的优点:

  • 不需要在变量前加 $(加了也行):$((a+b)) 和 $((\\$a+\\$b)) 都对
  • 乘号不需要转义
  • 支持幂运算 **
  • 写起来更简洁
  • 赋值给变量:

    result=$((a + b))
    echo $result # 30

    # 也可以直接修改变量自身
    x=5
    x=$((x + 1)) # x变成6
    echo $x

    7.6.4 方法三:let命令

    a=10
    b=20

    let sum=a+b
    echo $sum # 30

    let "product = a * b"
    echo $product # 200

    # 自增自减
    let a++
    echo $a # 11

    let b–
    echo $b # 19

    # 复合赋值
    let a+=5
    echo $a # 16

    let 和双括号 $(()) 功能类似,但 let 后面不需要 $。

    7.6.5 方法四:bc命令(支持小数)

    前面三种方法都只支持整数运算。如果要做小数运算,需要用 bc(basic calculator):

    # 基本用法
    echo "10 + 20" | bc # 30
    echo "10.5 + 20.3" | bc # 30.8

    # 除法保留小数
    echo "scale=2; 10 / 3" | bc # 3.33(scale=2表示保留2位小数)

    # 乘方
    echo "2 ^ 10" | bc # 1024

    # 求平方根
    echo "sqrt(144)" | bc # 12

    在脚本中使用bc:

    #!/bin/bash
    # 计算圆的面积 circle.sh

    read -p "请输入圆的半径:" radius

    # 用bc计算(保留2位小数)
    area=$(echo "scale=2; 3.14 * $radius * $radius" | bc)

    echo "半径为 $radius 的圆,面积是 $area"

    7.6.6 自增自减运算

    #!/bin/bash
    # 自增自减演示

    x=10

    # 方式一:用双括号
    ((x++)) # x = 11
    echo $x

    ((x)) # x = 10
    echo $x

    # 方式二:用let
    let x++ # x = 11
    echo $x

    let x+=5 # x = 16
    echo $x

    # 方式三:传统方式
    x=$((x + 1))
    echo $x

    7.6.7 算术运算综合示例

    #!/bin/bash
    # 简易成绩计算器 grade.sh

    read -p "请输入语文成绩:" chinese
    read -p "请输入数学成绩:" math
    read -p "请输入英语成绩:" english

    # 计算总分(整数运算)
    total=$((chinese + math + english))

    # 计算平均分(需要小数,用bc)
    average=$(echo "scale=2; $total / 3" | bc)

    echo ""
    echo "========== 成绩单 =========="
    echo "语文:$chinese"
    echo "数学:$math"
    echo "英语:$english"
    echo "总分:$total"
    echo "平均分:$average"
    echo "============================="

    7.6.8 数值比较

    在条件判断中经常需要比较数字大小,Shell提供了这些运算符:

    运算符含义示例
    -eq 等于 (equal) [ $a -eq $b ]
    -ne 不等于 (not equal) [ $a -ne $b ]
    -gt 大于 (greater than) [ $a -gt $b ]
    -lt 小于 (less than) [ $a -lt $b ]
    -ge 大于等于 [ $a -ge $b ]
    -le 小于等于 [ $a -le $b ]

    在双括号中也可以用数学符号:

    a=10
    b=20

    # 传统写法
    if [ $a -lt $b ]; then
    echo "$a 小于 $b"
    fi

    # 双括号写法(更直观)
    if ((a < b)); then
    echo "$a 小于 $b"
    fi


    7.7 条件判断 if 语句

    7.7.1 如果…就…——生活中的条件判断

    我们每天都在做条件判断:

    • 如果下雨,就带伞
    • 如果红灯,就停下来;否则继续走
    • 如果成绩大于90,就评优;如果大于60,就及格;否则不及格

    Shell的 if 语句就是干这个的,让程序根据不同的情况做不同的事。

    7.7.2 if 语句的三种形式

    形式一:单分支 if

    语法:

    if [ 条件 ]; then
    命令
    fi

    示例:

    #!/bin/bash
    # 判断是否成年

    age=20

    if [ $age -ge 18 ]; then
    echo "你已经成年了!"
    fi

    注意几个细节:

  • if 后面有空格
  • [ 后面要有空格,] 前面也要有空格
  • ; then 可以换行写成:if [ 条件 ]
    then
    命令
    fi

  • fi 是 if 反过来写,表示条件判断结束
  • 形式二:双分支 if-else

    语法:

    if [ 条件 ]; then
    命令1
    else
    命令2
    fi

    示例:

    #!/bin/bash
    # 判断成绩是否及格

    score=55

    if [ $score -ge 60 ]; then
    echo "恭喜,及格了!"
    else
    echo "抱歉,不及格!"
    fi

    形式三:多分支 if-elif-else

    语法:

    if [ 条件1 ]; then
    命令1
    elif [ 条件2 ]; then
    命令2
    elif [ 条件3 ]; then
    命令3
    else
    命令n
    fi

    示例:

    #!/bin/bash
    # 成绩等级评定

    score=85

    if [ $score -ge 90 ]; then
    echo "优秀!"
    elif [ $score -ge 80 ]; then
    echo "良好!"
    elif [ $score -ge 70 ]; then
    echo "中等!"
    elif [ $score -ge 60 ]; then
    echo "及格!"
    else
    echo "不及格!"
    fi

    7.7.3 test命令和方括号[]

    其实 [ ] 是 test 命令的简写,下面两种写法完全等价:

    # 写法一:用test
    if test $a -eq $b; then
    echo "相等"
    fi

    # 写法二:用方括号(更常用)
    if [ $a -eq $b ]; then
    echo "相等"
    fi

    [ ] 更常用,因为更简洁。但要注意 [ 后面和 ] 前面必须有空格!

    7.7.4 条件判断的种类

    一、数字比较

    运算符含义
    -eq 等于
    -ne 不等于
    -gt 大于
    -lt 小于
    -ge 大于等于
    -le 小于等于

    a=10
    b=20

    if [ $a -lt $b ]; then
    echo "$a 小于 $b"
    fi

    二、字符串比较

    运算符含义
    = 相等
    != 不相等
    -z 字符串为空(长度为0)
    -n 字符串非空

    str1="hello"
    str2="world"

    if [ $str1 = $str2 ]; then
    echo "相等"
    else
    echo "不相等"
    fi

    # 判断字符串是否为空
    name=""
    if [ -z $name ]; then
    echo "name是空的"
    fi

    三、文件判断

    这是Shell特有的,用来判断文件的各种状态:

    运算符含义
    -e 文件 文件存在
    -f 文件 存在且是普通文件
    -d 文件 存在且是目录
    -r 文件 存在且可读
    -w 文件 存在且可写
    -x 文件 存在且可执行
    -s 文件 存在且非空
    文件1 -nt 文件2 文件1比文件2新
    文件1 -ot 文件2 文件1比文件2旧

    #!/bin/bash
    # 文件检查 file_check.sh

    file="/etc/passwd"

    if [ -e $file ]; then
    echo "文件存在"

    if [ -f $file ]; then
    echo "是普通文件"
    elif [ -d $file ]; then
    echo "是目录"
    fi

    if [ -r $file ]; then
    echo "可读"
    fi

    if [ -w $file ]; then
    echo "可写"
    fi

    if [ -x $file ]; then
    echo "可执行"
    fi
    else
    echo "文件不存在"
    fi

    7.7.5 逻辑运算符

    就像数学里有"且"、“或”、“非”,Shell也有:

    运算符含义示例
    -a 或 && 逻辑与(且) [ $a -gt 0 -a $a -lt 10 ]
    -o 或 || 逻辑或 [ $a -lt 0 -o $a -gt 100 ]
    ! 逻辑非 [ ! -e $file ]

    #!/bin/bash
    # 逻辑运算演示

    age=25
    gender="男"

    # 逻辑与:两个条件都满足
    if [ $age -ge 22 -a $gender = "男" ]; then
    echo "达到法定结婚年龄"
    fi

    # 逻辑或:满足一个即可
    temperature=38
    if [ $temperature -lt 0 -o $temperature -gt 37 ]; then
    echo "体温异常!"
    fi

    # 逻辑非
    file="/not_exist"
    if [ ! -e $file ]; then
    echo "文件不存在"
    fi

    用&&和||的简写形式:

    # 如果命令1成功,就执行命令2
    command1 && command2

    # 如果命令1失败,就执行命令2
    command1 || command2

    # 实际例子
    [ -d /tmp/test ] || mkdir -p /tmp/test
    # 如果目录不存在,就创建它

    [ -f config.txt ] && echo "配置文件存在"
    # 如果文件存在,就输出提示

    7.7.6 双括号的高级条件判断

    用 (( )) 做数学比较更直观:

    a=10
    b=20

    # 传统写法
    if [ $a -lt $b ]; then
    echo "$a < $b"
    fi

    # 双括号写法(可以用数学符号了!)
    if ((a < b)); then
    echo "$a < $b"
    fi

    # 也支持 && 和 ||
    if ((a > 0 && a < 100)); then
    echo "a在0到100之间"
    fi

    用 [[ ]] 做字符串比较更强大(支持模式匹配):

    str="hello world"

    # 支持*通配符
    if [[ $str == hello* ]]; then
    echo "以hello开头"
    fi

    # 支持正则匹配(用=~)
    if [[ $str =~ ^[0-9]+$ ]]; then
    echo "全是数字"
    else
    echo "不全是数字"
    fi

    7.7.7 if嵌套

    if里面可以再套if:

    #!/bin/bash
    # 嵌套if示例

    read -p "请输入年龄:" age
    read -p "请输入是否有驾照(y/n):" has_license

    if [ $age -ge 18 ]; then
    echo "你已成年"

    if [ "$has_license" = "y" ]; then
    echo "你可以开车了!"
    else
    echo "你需要先考驾照"
    fi
    else
    echo "你还未成年,不能开车"
    fi

    7.7.8 综合示例:判断闰年

    #!/bin/bash
    # 判断闰年 leap.sh

    read -p "请输入年份:" year

    # 闰年规则:
    # 1. 能被4整除但不能被100整除,是闰年
    # 2. 能被400整除,是闰年

    if [ $(($year % 400)) -eq 0 ]; then
    echo "$year 年是闰年"
    elif [ $(($year % 4)) -eq 0 ] && [ $(($year % 100)) -ne 0 ]; then
    echo "$year 年是闰年"
    else
    echo "$year 年不是闰年"
    fi


    7.8 循环结构 for、while、until

    7.8.1 为什么要循环——懒人的福音

    假设你要打印1到100,如果没有循环,你得写100行 echo:

    echo 1
    echo 2
    echo 3
    # … 97行省略
    echo 100

    太蠢了!有循环的话,只需要几行:

    for i in {1..100}; do
    echo $i
    done

    这就是循环的意义:让电脑替你做重复的事情。

    7.8.2 for 循环

    基本语法:

    for 变量 in 列表; do
    命令
    done

    示例一:遍历列表

    #!/bin/bash
    for fruit in 苹果 香蕉 橘子 葡萄; do
    echo "我喜欢吃 $fruit"
    done

    输出:

    我喜欢吃 苹果
    我喜欢吃 香蕉
    我喜欢吃 橘子
    我喜欢吃 葡萄

    执行过程:

  • fruit 先取"苹果",执行 echo
  • fruit 再取"香蕉",执行 echo
  • 依此类推,直到列表遍历完
  • 示例二:遍历数字范围

    # 方法一:用花括号
    for i in {1..5}; do
    echo "第 $i 次"
    done

    # 方法二:用seq命令
    for i in $(seq 1 5); do
    echo "第 $i 次"
    done

    # 带步长(每次加2)
    for i in {1..10..2}; do
    echo $i # 输出:1 3 5 7 9
    done

    # 倒序
    for i in {5..1}; do
    echo $i # 输出:5 4 3 2 1
    done

    示例三:C语言风格的for

    如果你学过C/Java,这种写法更亲切:

    for ((i=1; i<=5; i++)); do
    echo "第 $i 次循环"
    done

    执行过程:

  • 初始化 i=1
  • 判断 i<=5,成立就执行循环体
  • 执行完循环体后 i++
  • 回到第2步,直到 i>5 结束
  • 示例四:遍历命令结果

    # 遍历当前目录所有.txt文件
    for file in *.txt; do
    echo "找到文件:$file"
    done

    # 遍历/etc目录下的内容
    for item in $(ls /etc); do
    echo "$item"
    done

    示例五:遍历数组

    colors=("红" "橙" "黄" "绿" "蓝")

    for color in "${colors[@]}"; do
    echo "颜色:$color"
    done

    示例六:遍历位置参数

    #!/bin/bash
    # 遍历所有传入的参数
    for arg in "$@"; do
    echo "参数:$arg"
    done

    7.8.3 while 循环

    while是"当…的时候"的意思,只要条件成立,就一直循环。

    语法:

    while [ 条件 ]; do
    命令
    done

    示例一:计数器

    #!/bin/bash
    # 打印1到5

    i=1
    while [ $i -le 5 ]; do
    echo "第 $i 次"
    i=$((i + 1)) # 别忘了让计数器增加,否则死循环!
    done

    注意:while循环中最容易犯的错误就是忘记更新计数器,导致无限循环。就像跑步机,如果你不按停止键,它会一直转。

    示例二:逐行读取文件

    #!/bin/bash
    # 逐行读取文件内容

    file="/etc/passwd"
    while read line; do
    echo "行内容:$line"
    done < $file

    这个用法非常实用,< $file 把文件内容输入给while循环,每次读取一行赋给 line 变量。

    示例三:交互式菜单

    #!/bin/bash
    # 交互式菜单 menu.sh

    while true; do
    echo ""
    echo "===== 系统管理菜单 ====="
    echo "1. 查看磁盘使用情况"
    echo "2. 查看内存使用情况"
    echo "3. 查看当前登录用户"
    echo "4. 退出"
    echo "======================="

    read -p "请选择 [1-4]:" choice

    case $choice in
    1) df -h ;;
    2) free -h ;;
    3) who ;;
    4) echo "再见!"; break ;;
    *) echo "无效选择,请重试" ;;
    esac
    done

    7.8.4 until 循环

    until是"直到…才"的意思,和while正好相反:while是条件成立就循环,until是条件不成立就循环(直到条件成立才停止)。

    语法:

    until [ 条件 ]; do
    命令
    done

    示例:

    #!/bin/bash
    # 等待某个用户登录

    echo "等待用户admin登录…"

    until who | grep -q "admin"; do
    echo "admin还没登录,继续等待…"
    sleep 5 # 等5秒再检查
    done

    echo "admin登录了!"

    while和until的关系:

    # while写法:条件为真时循环
    while [ 条件为假 ]; do
    命令
    done

    # 等价的until写法:条件为假时循环(条件为真时停止)
    until [ 条件为真 ]; do
    命令
    done

    7.8.5 break 和 continue

    这两个关键字用来控制循环的流程:

    • break:跳出整个循环(“我不干了,走人”)
    • continue:跳过本次循环,进入下一次(“这次算了,下一个”)

    break示例:

    #!/bin/bash
    # 找到第一个大于5的数就停止

    for i in 1 2 3 4 5 6 7 8 9 10; do
    if [ $i -gt 5 ]; then
    echo "找到第一个大于5的数:$i"
    break # 跳出循环
    fi
    echo "检查:$i"
    done

    echo "循环结束"

    输出:

    检查:1
    检查:2
    检查:3
    检查:4
    检查:5
    找到第一个大于5的数:6
    循环结束

    continue示例:

    #!/bin/bash
    # 打印1到10,但跳过偶数

    for i in {1..10}; do
    if ((i % 2 == 0)); then
    continue # 跳过偶数
    fi
    echo $i # 只打印奇数
    done

    输出:

    1
    3
    5
    7
    9

    7.8.6 嵌套循环

    循环里面可以再套循环,最典型的例子是打印乘法表:

    #!/bin/bash
    # 九九乘法表 multiplication.sh

    for i in {1..9}; do
    for j in {1..9}; do
    # 只打印 j <= i 的部分(形成三角形)
    if [ $j -le $i ]; then
    printf "%d×%d=%-4d" $j $i $((i * j))
    fi
    done
    echo "" # 每行结束后换行
    done

    输出:

    1×1=1
    1×2=2 2×2=4
    1×3=3 2×3=6 3×3=9
    1×4=4 2×4=8 3×4=12 4×4=16
    …(省略)
    1×9=9 2×9=18 3×9=27 4×9=36 5×9=45 6×9=54 7×9=63 8×9=72 9×9=81

    7.8.7 select 循环(菜单选择)

    select 是一个专门用来做菜单的循环:

    #!/bin/bash
    # select菜单 select_menu.sh

    echo "请选择你喜欢的编程语言:"
    select lang in "Python" "Java" "C++" "Shell" "退出"; do
    case $lang in
    "退出")
    echo "再见!"
    break
    ;;
    *)
    echo "你选择了 $lang"
    ;;
    esac
    done

    运行后效果:

    请选择你喜欢的编程语言:
    1) Python
    2) Java
    3) C++
    4) Shell
    5) 退出
    #?

    用户输入数字选择,非常方便。

    7.8.8 循环综合示例:猜数字游戏

    #!/bin/bash
    # 猜数字游戏 guess.sh

    # 生成1-100的随机数
    target=$((RANDOM % 100 + 1))
    count=0

    echo "============================="
    echo " 欢迎来到猜数字游戏!"
    echo " 我已经想好了一个1-100的数字"
    echo " 看你能猜几次猜中!"
    echo "============================="
    echo ""

    while true; do
    read -p "请输入你猜的数字(1-100):" guess
    count=$((count + 1))

    if [ $guess -lt $target ]; then
    echo "太小了,往大猜!"
    elif [ $guess -gt $target ]; then
    echo "太大了,往小猜!"
    else
    echo ""
    echo "恭喜你,猜中了!"
    echo "正确答案是 $target"
    echo "你一共猜了 $count 次"
    break
    fi
    echo ""
    done


    7.9 case 多分支选择

    7.9.1 为什么需要case——if-elif太多了

    假设你要做一个菜单程序,有很多选项:

    if [ $choice = "1" ]; then
    ...
    elif [ $choice = "2" ]; then
    ...
    elif [ $choice = "3" ]; then
    ...
    elif [ $choice = "4" ]; then
    ...
    elif [ $choice = "5" ]; then
    ...
    else
    ...
    fi

    if-elif一多,代码就变得很长、很难看。这时候用 case 就优雅多了。

    7.9.2 case 的语法

    case 变量 in
    模式1)
    命令1
    ;;
    模式2)
    命令2
    ;;
    模式3)
    命令3
    ;;
    *)
    默认命令
    ;;
    esac

    解释一下:

    • case 变量 in:开始case语句,检查"变量"的值
    • 模式):每个分支以 ) 结尾(不是括号配对,就是单个右括号)
    • ;;:每个分支结束的标志(相当于C语言里的break)
    • *):默认分支(相当于else),* 匹配所有
    • esac:case 反过来写,表示case语句结束

    7.9.3 case 基础示例

    #!/bin/bash
    # 星期判断 day.sh

    read -p "请输入数字(1-7):" day

    case $day in
    1) echo "星期一" ;;
    2) echo "星期二" ;;
    3) echo "星期三" ;;
    4) echo "星期四" ;;
    5) echo "星期五" ;;
    6) echo "星期六" ;;
    7) echo "星期日" ;;
    *) echo "输入无效!" ;;
    esac

    是不是比用一堆if-elif清爽多了?

    7.9.4 case 中的模式匹配

    case的强大之处在于支持模式匹配:

    #!/bin/bash
    # 文件类型判断 filetype.sh

    read -p "请输入文件名:" filename

    case $filename in
    *.txt)
    echo "这是一个文本文件"
    ;;
    *.sh)
    echo "这是一个Shell脚本"
    ;;
    *.jpg|*.jpeg|*.png|*.gif)
    echo "这是一个图片文件"
    ;;
    *.mp3|*.wav)
    echo "这是一个音频文件"
    ;;
    *.mp4|*.avi|*.mkv)
    echo "这是一个视频文件"
    ;;
    *)
    echo "未知文件类型"
    ;;
    esac

    注意:

    • | 表示"或",可以匹配多个模式
    • * 是通配符,匹配任意内容

    7.9.5 case 中的字符范围

    可以用 [ ] 匹配字符范围:

    #!/bin/bash
    # 成绩等级 case_grade.sh

    read -p "请输入成绩(0-100):" score

    # 确保输入的是数字
    if ! [[ $score =~ ^[0-9]+$ ]]; then
    echo "请输入有效的数字!"
    exit 1
    fi

    # 取十位数判断等级
    level=$((score / 10))

    case $level in
    9|10)
    echo "成绩 $score:优秀!"
    ;;
    8)
    echo "成绩 $score:良好!"
    ;;
    7)
    echo "成绩 $score:中等!"
    ;;
    6)
    echo "成绩 $score:及格!"
    ;;
    *)
    echo "成绩 $score:不及格!"
    ;;
    esac

    7.9.6 case 实战:简易计算器

    #!/bin/bash
    # 简易计算器 calculator.sh

    echo "===== 简易计算器 ====="
    echo ""

    read -p "请输入第一个数字:" num1
    read -p "请输入运算符(+,-,*,/):" op
    read -p "请输入第二个数字:" num2

    case $op in
    +)
    result=$((num1 + num2))
    echo "$num1 + $num2 = $result"
    ;;
    )
    result=$((num1 num2))
    echo "$num1$num2 = $result"
    ;;
    \\*|x|X)
    result=$((num1 * num2))
    echo "$num1 × $num2 = $result"
    ;;
    /)
    if [ $num2 -eq 0 ]; then
    echo "错误:除数不能为0!"
    else
    result=$(echo "scale=2; $num1 / $num2" | bc)
    echo "$num1 ÷ $num2 = $result"
    fi
    ;;
    *)
    echo "不支持的运算符:$op"
    ;;
    esac

    7.9.7 case 嵌套

    case里面也可以嵌套case:

    #!/bin/bash
    # 系统管理菜单 sysmenu.sh

    while true; do
    echo ""
    echo "===== 系统管理工具 ====="
    echo "1. 磁盘管理"
    echo "2. 内存管理"
    echo "3. 网络管理"
    echo "4. 退出"
    echo "======================="

    read -p "请选择:" main_choice

    case $main_choice in
    1)
    echo ""
    echo "– 磁盘管理 –"
    echo "a. 查看磁盘使用情况"
    echo "b. 查看挂载信息"
    read -p "请选择:" sub_choice
    case $sub_choice in
    a|A) df -h ;;
    b|B) mount ;;
    *) echo "无效选择" ;;
    esac
    ;;
    2)
    echo ""
    echo "– 内存管理 –"
    echo "a. 查看内存使用"
    echo "b. 查看内存详情"
    read -p "请选择:" sub_choice
    case $sub_choice in
    a|A) free -h ;;
    b|B) cat /proc/meminfo ;;
    *) echo "无效选择" ;;
    esac
    ;;
    3)
    echo ""
    echo "– 网络管理 –"
    echo "a. 查看IP地址"
    echo "b. 查看网络连接"
    read -p "请选择:" sub_choice
    case $sub_choice in
    a|A) ip addr ;;
    b|B) netstat -tlnp 2>/dev/null || ss -tlnp ;;
    *) echo "无效选择" ;;
    esac
    ;;
    4)
    echo "再见!"
    break
    ;;
    *)
    echo "无效选择,请重试"
    ;;
    esac
    done


    7.10 函数定义与调用

    7.10.1 为什么需要函数——避免重复劳动

    假设你在脚本中多次需要计算圆的面积,如果没有函数:

    # 计算第一个圆
    r1=5
    area1=$(echo "scale=2; 3.14 * $r1 * $r1" | bc)
    echo $area1

    # 计算第二个圆
    r2=10
    area2=$(echo "scale=2; 3.14 * $r2 * $r2" | bc)
    echo $area2

    # 计算第三个圆
    r3=15
    area3=$(echo "scale=2; 3.14 * $r3 * $r3" | bc)
    echo $area3

    同样的代码写了三遍,太累了。

    函数的作用就是把重复的代码打包,取个名字,用的时候叫名字就行。就像你教孩子系鞋带,教一次,以后他自己会系,不用每次都教。

    7.10.2 定义函数

    Shell中定义函数有两种语法:

    语法一:

    function 函数名() {
    命令
    ...
    }

    语法二(省略function关键字):

    函数名() {
    命令
    ...
    }

    两种都可以,推荐用第二种(更简洁)。

    7.10.3 调用函数

    直接写函数名就能调用:

    #!/bin/bash
    # 函数基础 func_basic.sh

    # 定义函数
    say_hello() {
    echo "你好,世界!"
    }

    # 调用函数
    say_hello
    say_hello
    say_hello

    输出:

    你好,世界!
    你好,世界!
    你好,世界!

    调用函数就像调用命令一样,直接写函数名即可,不需要加括号。

    7.10.4 函数参数

    函数内部可以用 $1, $2, $3… 接收参数(跟脚本的位置参数类似,但这是函数的参数,不是脚本的):

    #!/bin/bash
    # 函数参数 func_param.sh

    # 定义带参数的函数
    greet() {
    echo "你好,$1!"
    echo "你今年 $2 岁。"
    echo "你住在 $3。"
    }

    # 调用并传参
    greet "张三" 25 "北京"
    echo "—"
    greet "李四" 30 "上海"

    输出:

    你好,张三!
    你今年 25 岁。
    你住在 北京。

    你好,李四!
    你今年 30 岁。
    你住在 上海。

    注意函数内的 $1 和脚本外的 $1 是不同的——函数内的 $1 指的是调用函数时传的第一个参数,不是运行脚本时传的参数。

    7.10.5 函数返回值

    函数的返回值有两种方式:

    方式一:用return返回状态码

    #!/bin/bash
    # return返回值

    is_even() {
    if [ $(($1 % 2)) -eq 0 ]; then
    return 0 # 返回0表示成功(偶数)
    else
    return 1 # 返回非0表示失败(奇数)
    fi
    }

    # 调用
    is_even 4
    if [ $? -eq 0 ]; then
    echo "4是偶数"
    fi

    is_even 7
    if [ $? -eq 0 ]; then
    echo "7是偶数"
    else
    echo "7是奇数"
    fi

    注意:return 只能返回0-255的整数,通常用来表示成功(0)或失败(非0)。$? 用来获取上一条命令(或函数)的返回值。

    方式二:用echo输出结果(更常用)

    #!/bin/bash
    # echo返回值

    # 计算两个数之和
    add() {
    echo $(($1 + $2))
    }

    # 计算圆面积
    circle_area() {
    echo $(echo "scale=2; 3.14 * $1 * $1" | bc)
    }

    # 调用并接收返回值
    result=$(add 10 20)
    echo "10 + 20 = $result"

    area=$(circle_area 5)
    echo "半径5的圆面积 = $area"

    这种方式更灵活,可以返回任意字符串。用 $(函数名 参数) 来捕获函数的输出。

    7.10.6 局部变量 local

    默认情况下,函数内定义的变量是全局的,会影响函数外:

    #!/bin/bash
    # 全局变量的问题

    modify_var() {
    x=100 # 这会修改全局的x!
    }

    x=1
    echo "调用前:x=$x" # 输出:1
    modify_var
    echo "调用后:x=$x" # 输出:100(被函数修改了!)

    如果你不想函数内的变量影响外面,用 local 声明局部变量:

    #!/bin/bash
    # local局部变量

    safe_function() {
    local x=100 # 局部变量,只在函数内有效
    echo "函数内:x=$x"
    }

    x=1
    echo "调用前:x=$x"
    safe_function
    echo "调用后:x=$x" # 还是1,没被修改

    养成好习惯:函数内部用的临时变量都用 local 声明,避免意外修改全局变量。

    7.10.7 函数的递归调用

    函数可以调用自己,这叫递归。经典例子是计算阶乘:

    #!/bin/bash
    # 递归计算阶乘 factorial.sh

    factorial() {
    local n=$1
    if [ $n -le 1 ]; then
    echo 1
    else
    local prev=$(factorial $((n – 1)))
    echo $((n * prev))
    fi
    }

    read -p "请输入一个正整数:" num
    result=$(factorial $num)
    echo "$num! = $result"

    执行过程(以 factorial 4 为例):

    factorial(4) = 4 * factorial(3)
    = 4 * (3 * factorial(2))
    = 4 * (3 * (2 * factorial(1)))
    = 4 * (3 * (2 * 1))
    = 4 * (3 * 2)
    = 4 * 6
    = 24

    7.10.8 在单独文件中定义函数

    当函数多了,可以把函数放到单独的文件中,然后在主脚本中引用:

    文件 lib.sh(函数库):

    #!/bin/bash
    # 函数库

    # 打印分隔线
    print_line() {
    echo "================================"
    }

    # 打印信息
    print_info() {
    print_line
    echo " $1"
    print_line
    }

    # 检查文件是否存在
    check_file() {
    if [ -e "$1" ]; then
    return 0
    else
    return 1
    fi
    }

    主脚本 main.sh:

    #!/bin/bash
    # 引入函数库
    source ./lib.sh
    # 或者用 . ./lib.sh

    # 使用库中的函数
    print_info "系统检查开始"

    if check_file "/etc/passwd"; then
    echo "/etc/passwd 存在"
    fi

    print_info "系统检查结束"

    7.10.9 综合示例:学生成绩管理

    #!/bin/bash
    # 学生成绩管理 grade_manager.sh

    # 定义数组存储学生信息
    declare -a students
    declare -a scores
    count=0

    # 添加学生
    add_student() {
    local name=$1
    local score=$2

    students[$count]=$name
    scores[$count]=$score
    count=$((count + 1))

    echo "添加成功:$name 的成绩是 $score"
    }

    # 显示所有学生
    show_all() {
    if [ $count -eq 0 ]; then
    echo "暂无学生信息"
    return
    fi

    echo "序号 姓名 成绩 等级"
    echo "—————————-"
    local i=0
    while [ $i -lt $count ]; do
    local grade
    if [ ${scores[$i]} -ge 90 ]; then
    grade="优秀"
    elif [ ${scores[$i]} -ge 80 ]; then
    grade="良好"
    elif [ ${scores[$i]} -ge 60 ]; then
    grade="及格"
    else
    grade="不及格"
    fi
    printf "%-4d %-6s %-4d %s\\n" $((i+1)) "${students[$i]}" "${scores[$i]}" "$grade"
    i=$((i + 1))
    done
    echo "—————————-"
    }

    # 计算平均分
    calc_average() {
    if [ $count -eq 0 ]; then
    echo "暂无数据"
    return
    fi

    local total=0
    local i=0
    while [ $i -lt $count ]; do
    total=$((total + scores[$i]))
    i=$((i + 1))
    done

    local avg=$(echo "scale=2; $total / $count" | bc)
    echo "平均分:$avg"
    }

    # 查找学生
    find_student() {
    local name=$1
    local i=0
    while [ $i -lt $count ]; do
    if [ "${students[$i]}" = "$name" ]; then
    echo "找到:$name 的成绩是 ${scores[$i]}"
    return 0
    fi
    i=$((i + 1))
    done
    echo "未找到学生:$name"
    return 1
    }

    # 主程序
    add_student "张三" 95
    add_student "李四" 82
    add_student "王五" 58
    add_student "赵六" 76

    echo ""
    show_all
    echo ""
    calc_average
    echo ""
    find_student "李四"
    find_student "钱七"


    7.11 输入输出与read命令

    7.11.1 echo 命令详解

    echo 是最常用的输出命令,前面已经用了很多次了。这里补充一些高级用法:

    # 基本输出
    echo "Hello"

    # 输出不换行(用-n)
    echo -n "请稍候"
    echo -n "…"
    echo "完成"
    # 输出在一行:请稍候…完成

    # 启用转义字符(用-e)
    echo -e "第一行\\n第二行" # \\n换行
    echo -e "制表符\\t这里" # \\t制表符
    echo -e "这是\\b退格" # \\b退格
    echo -e "\\033[31m红色文字\\033[0m" # 红色文字
    echo -e "\\033[32m绿色文字\\033[0m" # 绿色文字
    echo -e "\\033[33m黄色文字\\033[0m" # 黄色文字

    颜色输出的格式:

    \\033[颜色代码m 内容 \\033[0m

    常见颜色代码:

    代码颜色
    30 黑色
    31 红色
    32 绿色
    33 黄色
    34 蓝色
    35 紫色
    36 青色
    37 白色

    封装成函数更方便:

    #!/bin/bash
    # 彩色输出 color.sh

    red() { echo -e "\\033[31m$1\\033[0m"; }
    green() { echo -e "\\033[32m$1\\033[0m"; }
    yellow() { echo -e "\\033[33m$1\\033[0m"; }
    blue() { echo -e "\\033[34m$1\\033[0m"; }

    red "这是红色文字"
    green "这是绿色文字"
    yellow "这是黄色文字"
    blue "这是蓝色文字"

    7.11.2 printf 命令

    printf 比echo更强大,支持格式化输出(跟C语言的printf类似):

    # 基本用法(不会自动换行,需要手动加\\n)
    printf "Hello\\n" # 输出:Hello(换行)

    # 格式化输出
    printf "%s今年%d岁\\n" "张三" 25 # 输出:张三今年25岁

    # 对齐输出
    printf "%-10s %-10s %5d\\n" "姓名" "城市" "年龄"
    printf "%-10s %-10s %5d\\n" "张三" "北京" 25
    printf "%-10s %-10s %5d\\n" "李四" "上海" 30

    # 浮点数
    printf "圆周率是:%.2f\\n" 3.14159 # 输出:圆周率是:3.14

    常用格式符:

    格式符含义
    %s 字符串
    %d 整数
    %f 浮点数
    %-10s 左对齐,占10个字符宽度
    %10s 右对齐,占10个字符宽度
    %.2f 保留2位小数

    用printf输出表格:

    #!/bin/bash
    # 表格输出 table.sh

    printf "%-8s %-8s %-6s %-8s\\n" "姓名" "语文" "数学" "英语"
    printf "%-8s %-8s %-8s %-8s\\n" "—-" "—-" "—-" "—-"
    printf "%-8s %-8d %-8d %-8d\\n" "张三" 90 85 92
    printf "%-8s %-8d %-8d %-8d\\n" "李四" 78 95 88
    printf "%-8s %-8d %-8d %-8d\\n" "王五" 88 76 90

    输出:

    姓名 语文 数学 英语
    —- —- —- —-
    张三 90 85 92
    李四 78 95 88
    王五 88 76 90

    7.11.3 read 命令详解

    read 用来从标准输入读取数据,通常用于和用户交互。

    基本用法:

    #!/bin/bash
    # read基础

    echo "请输入你的名字:"
    read name
    echo "你好,$name!"

    加提示信息(-p):

    read -p "请输入你的名字:" name
    echo "你好,$name!"

    -p 后面的提示信息会显示在用户输入之前,不需要单独用echo。

    输入密码时隐藏内容(-s):

    read -s -p "请输入密码:" password
    echo ""
    echo "密码已接收(我不会告诉别人的)"

    -s 表示silent(静默模式),输入的内容不会显示在屏幕上。

    限制输入长度(-n):

    read -n 1 -p "继续吗?(y/n):" choice
    echo ""
    echo "你选择了:$choice"

    -n 1 表示只读1个字符,用户输入一个字符后自动结束,不需要按回车。

    设置超时(-t):

    # 5秒内不输入就自动跳过
    if read -t 5 -p "请输入你的名字(5秒内):" name; then
    echo "你好,$name"
    else
    echo ""
    echo "超时了,你太慢了!"
    fi

    读取多个变量:

    read -p "请输入姓名和年龄(用空格分开):" name age
    echo "姓名:$name,年龄:$age"

    输入"张三 25",name就是"张三",age就是"25"。

    读取整行到数组(-a):

    read -a nums -p "请输入几个数字(空格分开):"
    echo "你输入了 ${#nums[@]} 个数字"
    echo "分别是:${nums[@]}"
    echo "第一个是:${nums[0]}"

    7.11.4 重定向

    重定向就是改变输入输出的方向。

    输出重定向(> 和 >>):

    # > 覆盖写入(原来的内容没了)
    echo "第一行" > file.txt
    echo "第二行" > file.txt # file.txt里只剩"第二行"

    # >> 追加写入(在原来的基础上添加)
    echo "第一行" > file.txt
    echo "第二行" >> file.txt # file.txt里有"第一行"和"第二行"

    打个比方:

    • > 就像擦黑板重新写——原来的内容被擦掉
    • >> 就像在纸上接着写——原来的内容还在

    错误重定向(2>):

    # 把错误信息写入文件
    ls /nonexistent 2> error.log

    # 丢弃错误信息(扔到黑洞/dev/null)
    ls /nonexistent 2> /dev/null

    在Linux中,每个进程有三个标准流:

    编号名称说明
    0 stdin 标准输入(键盘)
    1 stdout 标准输出(屏幕)
    2 stderr 标准错误(屏幕)

    所以 2> 就是重定向标准错误。

    同时重定向正确和错误输出(&>):

    # 把正确和错误输出都写入同一个文件
    command &> output.log

    # 等价写法
    command > output.log 2>&1

    输入重定向(<):

    # 从文件读取内容(而不是从键盘)
    wc -l < /etc/passwd # 统计文件的行数

    # 在脚本中
    while read line; do
    echo "处理:$line"
    done < input.txt

    Here Document(<<):

    Here Document是一种特殊的输入方式,可以在脚本中直接写多行文本:

    #!/bin/bash
    # here文档示例

    cat << EOF
    ============================
    欢迎使用本系统
    版本:1.0
    作者:张三
    ============================
    EOF

    << 后面跟一个标记(通常是EOF,表示End Of File),之间的内容会被当作输入。当再次遇到这个标记时,输入结束。

    实际用途——生成配置文件:

    #!/bin/bash
    # 自动生成配置文件

    cat > /tmp/myapp.conf << EOF
    # 应用配置文件
    APP_NAME=MyApp
    APP_PORT=8080
    APP_DEBUG=true
    APP_LOG=/var/log/myapp.log
    EOF

    echo "配置文件已生成"
    cat /tmp/myapp.conf

    7.11.5 管道 |

    管道 | 把前一个命令的输出当作后一个命令的输入:

    # 查看系统中运行的所有进程,然后只看包含nginx的
    ps aux | grep nginx

    # 统计当前目录有多少个文件
    ls | wc -l

    # 查看文件内容并分页显示
    cat /etc/passwd | less

    # 多级管道
    cat /etc/passwd | grep bash | wc -l
    # 找出使用bash的用户数量

    管道就像流水线——前一个工序的产品,直接传给下一个工序继续加工。

    7.11.6 综合示例:用户注册系统

    #!/bin/bash
    # 用户注册系统 register.sh

    # 存储用户信息的文件
    USER_FILE="users.txt"

    # 如果文件不存在就创建
    if [ ! -f "$USER_FILE" ]; then
    touch "$USER_FILE"
    fi

    # 注册函数
    register() {
    echo ""
    echo "===== 用户注册 ====="

    read -p "请输入用户名:" username

    # 检查用户名是否已存在
    if grep -q "^${username}|" "$USER_FILE"; then
    echo "错误:用户名 $username 已存在!"
    return 1
    fi

    read -s -p "请输入密码:" password
    echo ""
    read -s -p "请再次输入密码:" password2
    echo ""

    if [ "$password" != "$password2" ]; then
    echo "错误:两次密码不一致!"
    return 1
    fi

    read -p "请输入邮箱:" email

    # 保存到文件(用|分隔)
    echo "${username}|${password}|${email}" >> "$USER_FILE"
    echo "注册成功!欢迎 $username"
    }

    # 登录函数
    login() {
    echo ""
    echo "===== 用户登录 ====="

    read -p "请输入用户名:" username
    read -s -p "请输入密码:" password
    echo ""

    # 验证
    if grep -q "^${username}|${password}|" "$USER_FILE"; then
    echo "登录成功!欢迎回来,$username"
    else
    echo "登录失败:用户名或密码错误!"
    fi
    }

    # 主菜单
    while true; do
    echo ""
    echo "=========================="
    echo " 欢迎使用用户系统"
    echo "=========================="
    echo "1. 注册"
    echo "2. 登录"
    echo "3. 查看用户列表"
    echo "4. 退出"
    echo "=========================="

    read -p "请选择 [1-4]:" choice

    case $choice in
    1) register ;;
    2) login ;;
    3)
    echo ""
    echo "已注册用户:"
    if [ -s "$USER_FILE" ]; then
    cut -d'|' -f1 "$USER_FILE"
    else
    echo " 暂无用户"
    fi
    ;;
    4) echo "再见!"; break ;;
    *) echo "无效选择" ;;
    esac
    done


    7.12 脚本调试与错误处理

    7.12.1 常见错误类型

    写Shell脚本时,常见的错误有:

    错误一:语法错误

    # if后面忘了then
    if [ $a -gt 10 ]
    echo "大于10"
    fi
    # 报错:syntax error near unexpected token `fi'

    错误二:变量未定义

    # 变量名打错了
    username="张三"
    echo $usernam # 少了个e,输出为空

    错误三:权限问题

    # 没有执行权限就运行
    ./script.sh
    # 报错:Permission denied

    错误四:路径错误

    # 文件路径不存在
    cat /etc/passwdd # 多了个d
    # 报错:No such file or directory

    7.12.2 调试模式 -x

    在运行脚本时加 -x 参数,可以查看每一步执行了什么:

    bash -x script.sh

    或者在脚本开头加上:

    #!/bin/bash -x

    效果:

    #!/bin/bash
    # 调试示例 debug.sh

    name="张三"
    age=25
    echo "$name 今年 $age 岁"

    正常运行:

    张三 今年 25 岁

    用 -x 运行 bash -x debug.sh:

    + name=张三
    + age=25
    + echo '张三 今年 25 岁'
    张三 今年 25 岁

    每行前面的 + 表示这是Shell执行的命令,你能清楚地看到每一步做了什么。当脚本有bug时,用这个方法能快速定位问题。

    7.12.3 调试模式 -v

    -v 模式会显示脚本中每一行的原始内容(执行前):

    bash -v script.sh

    -v 和 -x 的区别:

    • -v:显示要执行的行(原文)
    • -x:显示执行后的命令(变量已替换)

    通常两个一起用:bash -vx script.sh

    7.12.4 在脚本中局部开启调试

    你可以在脚本的某一部分开启调试,不需要全程开启:

    #!/bin/bash

    echo "这部分不调试"

    set -x # 开启调试
    name="张三"
    age=25
    echo "$name $age"
    set +x # 关闭调试

    echo "这部分也不调试"

    7.12.5 set命令的常用选项

    选项含义作用
    set -e 遇到错误就退出 任何命令返回非0,脚本立即停止
    set -u 使用未定义变量就报错 防止变量名打错
    set -x 打印执行的命令 调试用
    set -o pipefail 管道中任一命令失败就失败 默认只看管道最后命令的返回值

    推荐在脚本开头加上:

    #!/bin/bash
    set -euo pipefail

    这三个选项能让你的脚本更健壮:

    • -e:出错就停,不让错误蔓延
    • -u:变量未定义就报错,避免空变量导致的bug
    • -o pipefail:管道命令出错也能捕获

    7.12.6 错误处理:检查命令返回值

    每条命令执行后都有一个返回值(退出状态码 $?):

    • 0:成功
    • 非0:失败(不同的数字代表不同的错误)

    #!/bin/bash
    # 检查命令是否成功 check_cmd.sh

    mkdir /tmp/test_dir

    if [ $? -eq 0 ]; then
    echo "目录创建成功"
    else
    echo "目录创建失败"
    fi

    简写形式(用&&和||):

    # 成功时做A,失败时做B
    mkdir /tmp/test_dir && echo "成功" || echo "失败"

    7.12.7 trap命令:捕获信号

    trap 可以在脚本收到特定信号时执行指定的命令。最常用的场景是:脚本中途被Ctrl+C中断时,清理临时文件。

    #!/bin/bash
    # trap演示 trap.sh

    # 定义清理函数
    cleanup() {
    echo ""
    echo "正在清理临时文件…"
    rm -f /tmp/my_temp_*
    echo "清理完成,再见!"
    exit 0
    }

    # 捕获Ctrl+C(SIGINT)和退出信号
    trap cleanup SIGINT SIGTERM

    echo "脚本开始运行,按Ctrl+C可以中断"
    echo "正在处理…"

    # 模拟长时间运行
    for i in {1..100}; do
    echo "进度:$i%"
    sleep 0.5
    done

    echo "处理完成!"

    常用信号:

    信号编号含义
    SIGINT 2 Ctrl+C中断
    SIGTERM 15 终止信号
    SIGKILL 9 强制杀死(不能被trap捕获)
    EXIT 0 脚本退出

    7.12.8 实用的错误处理模板

    #!/bin/bash
    # 带完整错误处理的脚本模板 template.sh

    set -euo pipefail

    # 定义日志函数
    log_info() { echo "[INFO] $(date '+%H:%M:%S') $*"; }
    log_warn() { echo "[WARN] $(date '+%H:%M:%S') $*" >&2; }
    log_error() { echo "[ERROR] $(date '+%H:%M:%S') $*" >&2; }

    # 定义错误处理函数
    on_error() {
    log_error "脚本在行 $1 发生错误"
    # 这里可以做一些清理工作
    exit 1
    }

    # 设置错误陷阱(ERR信号带行号)
    trap 'on_error $LINENO' ERR

    # 定义清理函数
    cleanup() {
    log_info "脚本结束,执行清理…"
    # rm -f /tmp/temp_*
    }

    trap cleanup EXIT

    # ===== 主程序开始 =====
    log_info "脚本开始执行"

    # 检查必要的目录
    DATA_DIR="/tmp/myapp_data"
    if [ ! -d "$DATA_DIR" ]; then
    log_info "创建数据目录: $DATA_DIR"
    mkdir -p "$DATA_DIR" || { log_error "无法创建目录"; exit 1; }
    fi

    # 检查必要的文件
    CONFIG_FILE="$DATA_DIR/config.txt"
    if [ ! -f "$CONFIG_FILE" ]; then
    log_warn "配置文件不存在,创建默认配置"
    echo "default=true" > "$CONFIG_FILE"
    fi

    log_info "读取配置…"
    while read line; do
    log_info "配置项: $line"
    done < "$CONFIG_FILE"

    log_info "所有操作完成"
    # ===== 主程序结束 =====

    这个模板包含了:

  • set -euo pipefail 严格模式
  • 日志函数(带时间戳和级别)
  • 错误捕获(记录出错行号)
  • 退出清理机制
  • 7.12.9 调试技巧总结

  • 用 bash -n 检查语法(不执行,只检查语法对不对):
  • bash -n script.sh

  • 用 shellcheck 工具(如果装了的话):
  • shellcheck script.sh

  • 在关键位置加echo,打印变量值:
  • echo "DEBUG: var_name = $var_name"

  • 用 set -x 局部调试:
  • set -x
    # 可能有问题的代码
    set +x

  • 分段测试:先写一小段,测试没问题再加下一段

  • 7.13 实战脚本编写案例

    7.13.1 案例:自动备份脚本

    这是一个非常实用的脚本,自动备份指定目录:

    #!/bin/bash
    # 自动备份脚本 backup.sh
    # 用法:./backup.sh 源目录 备份目录

    set -euo pipefail

    # ===== 配置 =====
    SOURCE_DIR="${1:-/home/user/data}" # 源目录,默认/home/user/data
    BACKUP_DIR="${2:-/backup}" # 备份目录,默认/backup
    KEEP_DAYS=7 # 保留几天的备份
    DATE=$(date '+%Y%m%d_%H%M%S')
    BACKUP_FILE="${BACKUP_DIR}/backup_${DATE}.tar.gz"
    LOG_FILE="${BACKUP_DIR}/backup.log"

    # ===== 函数定义 =====
    log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
    }

    error_exit() {
    log "ERROR: $1"
    exit 1
    }

    cleanup_old_backups() {
    log "清理 ${KEEP_DAYS} 天前的旧备份…"
    local deleted=0
    for file in "${BACKUP_DIR}"/backup_*.tar.gz; do
    if [ -f "$file" ]; then
    local file_date=$(echo "$file" | grep -o '[0-9]\\{8\\}')
    local diff_days=$(( ($(date +%s) $(date d "$file_date" +%s)) / 86400 ))
    if [ $diff_days -gt $KEEP_DAYS ]; then
    rm -f "$file"
    log "已删除旧备份: $(basename $file)"
    deleted=$((deleted + 1))
    fi
    fi
    done
    log "共删除 $deleted 个旧备份"
    }

    show_summary() {
    local size=$(du -h "$BACKUP_FILE" | cut -f1)
    local files=$(tar -tzf "$BACKUP_FILE" | wc -l)

    echo ""
    echo "========== 备份报告 =========="
    echo "备份时间:$(date '+%Y-%m-%d %H:%M:%S')"
    echo "源目录: $SOURCE_DIR"
    echo "备份到: $BACKUP_FILE"
    echo "文件大小:$size"
    echo "文件数量:$files 个"
    echo "=============================="
    }

    # ===== 主程序 =====

    # 检查源目录
    if [ ! -d "$SOURCE_DIR" ]; then
    error_exit "源目录不存在: $SOURCE_DIR"
    fi

    # 创建备份目录
    mkdir -p "$BACKUP_DIR" || error_exit "无法创建备份目录"

    log "========== 开始备份 =========="
    log "源目录: $SOURCE_DIR"
    log "备份到: $BACKUP_FILE"

    # 执行备份
    log "正在压缩打包…"
    tar -czf "$BACKUP_FILE" -C "$(dirname $SOURCE_DIR)" "$(basename $SOURCE_DIR)" 2>/dev/null || \\
    error_exit "备份失败"

    log "备份完成!"

    # 清理旧备份
    cleanup_old_backups

    # 显示摘要
    show_summary

    log "========== 备份结束 =========="

    7.13.2 案例:系统监控脚本

    监控系统状态,异常时报警:

    #!/bin/bash
    # 系统监控脚本 monitor.sh

    # ===== 阈值配置 =====
    CPU_WARN=80 # CPU使用率警告阈值(%)
    MEM_WARN=80 # 内存使用率警告阈值(%)
    DISK_WARN=90 # 磁盘使用率警告阈值(%)
    CHECK_INTERVAL=60 # 检查间隔(秒)
    LOG_FILE="/var/log/sys_monitor.log"

    # ===== 函数 =====
    log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
    }

    alert() {
    local msg="[ALERT] $1"
    echo "$msg" | tee -a "$LOG_FILE"
    # 这里可以加发邮件、发短信等通知逻辑
    }

    get_cpu_usage() {
    # 获取CPU使用率
    local idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}')
    local usage=$(echo "100 – $idle" | bc)
    echo $usage | cut -d'.' -f1
    }

    get_mem_usage() {
    local total=$(free | grep Mem | awk '{print $2}')
    local used=$(free | grep Mem | awk '{print $3}')
    echo $((used * 100 / total))
    }

    check_disk() {
    local alerts=""
    df -h | awk 'NR>1' | while read line; do
    local partition=$(echo $line | awk '{print $1}')
    local usage=$(echo $line | awk '{print $5}' | cut -d'%' -f1)
    local mount=$(echo $line | awk '{print $6}')

    if [ "$usage" -gt "$DISK_WARN" ]; then
    alert "磁盘 $mount ($partition) 使用率 ${usage}%,超过阈值 ${DISK_WARN}%"
    fi
    done
    }

    check_system() {
    log "— 开始检查 —"

    # 检查CPU
    local cpu=$(get_cpu_usage)
    log "CPU使用率: ${cpu}%"
    if [ "$cpu" -gt "$CPU_WARN" ]; then
    alert "CPU使用率 ${cpu}%,超过阈值 ${CPU_WARN}%"
    fi

    # 检查内存
    local mem=$(get_mem_usage)
    log "内存使用率: ${mem}%"
    if [ "$mem" -gt "$MEM_WARN" ]; then
    alert "内存使用率 ${mem}%,超过阈值 ${MEM_WARN}%"
    fi

    # 检查磁盘
    check_disk

    # 检查僵尸进程
    local zombies=$(ps aux | awk '{print $8}' | grep -c Z)
    if [ "$zombies" -gt 0 ]; then
    alert "发现 $zombies 个僵尸进程!"
    fi

    # 检查负载
    local load=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | cut -d',' -f1)
    local cores=$(nproc)
    log "系统负载: ${load} (核心数: ${cores})"

    log "— 检查结束 —"
    log ""
    }

    # ===== 主程序 =====
    echo "系统监控脚本启动"
    echo "检查间隔: ${CHECK_INTERVAL}秒"
    echo "CPU阈值: ${CPU_WARN}% 内存阈值: ${MEM_WARN}% 磁盘阈值: ${DISK_WARN}%"
    echo "日志文件: ${LOG_FILE}"
    echo "按Ctrl+C停止…"
    echo ""

    while true; do
    check_system
    sleep $CHECK_INTERVAL
    done

    7.13.3 案例:批量重命名文件

    #!/bin/bash
    # 批量重命名文件 rename.sh

    show_help() {
    echo "用法:$0 <目录> <模式>"
    echo ""
    echo "模式:"
    echo " upper – 文件名转大写"
    echo " lower – 文件名转小写"
    echo " prefix:X – 添加前缀X"
    echo " suffix:X – 添加后缀X"
    echo " replace:OLD:NEW – 替换文件名中的OLD为NEW"
    echo " number – 按序号重命名"
    echo ""
    echo "示例:"
    echo " $0 ./photos lower"
    echo " $0 ./docs prefix:2024_"
    echo " $0 ./files replace:IMG:photo"
    }

    if [ $# -lt 2 ]; then
    show_help
    exit 1
    fi

    TARGET_DIR="$1"
    MODE="$2"

    if [ ! -d "$TARGET_DIR" ]; then
    echo "错误:目录 $TARGET_DIR 不存在"
    exit 1
    fi

    count=0
    cd "$TARGET_DIR" || exit 1

    for file in *; do
    # 跳过目录
    [ -f "$file" ] || continue

    # 获取文件名和扩展名
    filename="${file%.*}"
    extension="${file##*.}"

    # 如果没有扩展名
    if [ "$filename" = "$file" ]; then
    extension=""
    else
    extension=".$extension"
    fi

    newname=""

    case "$MODE" in
    upper)
    newname=$(echo "$filename" | tr 'a-z' 'A-Z')"$extension"
    ;;
    lower)
    newname=$(echo "$filename" | tr 'A-Z' 'a-z')"$extension"
    ;;
    prefix:*)
    prefix="${MODE#prefix:}"
    newname="${prefix}${filename}${extension}"
    ;;
    suffix:*)
    suffix="${MODE#suffix:}"
    newname="${filename}${suffix}${extension}"
    ;;
    replace:*)
    OLD="${MODE#replace:}"
    OLD="${OLD%%:*}"
    NEW="${MODE#*:}"
    NEW="${NEW#*:}"
    newname=$(echo "$filename" | sed "s/$OLD/$NEW/g")"$extension"
    ;;
    number)
    count=$((count + 1))
    newname=$(printf "%03d%s" $count "$extension")
    ;;
    *)
    echo "未知模式: $MODE"
    show_help
    exit 1
    ;;
    esac

    if [ "$newname" != "$file" ] && [ -n "$newname" ]; then
    mv "$file" "$newname" 2>/dev/null
    if [ $? -eq 0 ]; then
    echo "重命名: $file -> $newname"
    else
    echo "失败: $file -> $newname (可能已存在)"
    fi
    fi
    done

    echo "完成!"

    7.13.4 案例:一键部署Web环境

    #!/bin/bash
    # 一键部署LNMP环境 deploy_lnmp.sh

    set -euo pipefail

    # ===== 颜色定义 =====
    RED='\\033[31m'
    GREEN='\\033[32m'
    YELLOW='\\033[33m'
    BLUE='\\033[34m'
    NC='\\033[0m' # 恢复默认颜色

    info() { echo -e "${GREEN}[INFO]${NC} $*"; }
    warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
    error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

    # ===== 配置 =====
    WEB_ROOT="/var/www/html"
    MYSQL_ROOT_PASSWORD="MySecure@Pass123"
    PHP_VERSION="8.1"

    # ===== 检查root权限 =====
    check_root() {
    if [ "$(id -u)" -ne 0 ]; then
    error "请使用root用户运行此脚本"
    fi
    }

    # ===== 检测系统类型 =====
    detect_os() {
    if [ -f /etc/os-release ]; then
    . /etc/os-release
    OS=$ID
    info "检测到系统: $NAME $VERSION"
    else
    error "无法检测系统类型"
    fi
    }

    # ===== 安装Nginx =====
    install_nginx() {
    info "开始安装 Nginx…"

    if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
    apt-get update -y
    apt-get install -y nginx
    elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "rocky" ]; then
    yum install -y nginx
    fi

    systemctl start nginx
    systemctl enable nginx

    info "Nginx 安装完成"
    nginx -v
    }

    # ===== 安装MySQL =====
    install_mysql() {
    info "开始安装 MySQL…"

    if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
    apt-get install -y mysql-server
    elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ]; then
    yum install -y mariadb-server
    fi

    systemctl start mysqld 2>/dev/null || systemctl start mysql 2>/dev/null || systemctl start mariadb
    systemctl enable mysqld 2>/dev/null || systemctl enable mysql 2>/dev/null || systemctl enable mariadb

    info "设置MySQL root密码…"
    mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}';" 2>/dev/null || true

    info "MySQL 安装完成"
    }

    # ===== 安装PHP =====
    install_php() {
    info "开始安装 PHP…"

    if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
    apt-get install -y php-fpm php-mysql php-cli php-curl php-gd php-mbstring php-xml
    elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ]; then
    yum install -y php-fpm php-mysqlnd php-cli php-curl php-gd php-mbstring php-xml
    fi

    systemctl start php-fpm 2>/dev/null || systemctl start php${PHP_VERSION}-fpm
    systemctl enable php-fpm 2>/dev/null || systemctl enable php${PHP_VERSION}-fpm

    info "PHP 安装完成"
    php -v
    }

    # ===== 配置Nginx支持PHP =====
    config_nginx_php() {
    info "配置Nginx支持PHP…"

    mkdir -p "$WEB_ROOT"

    # 创建测试页面
    cat > "$WEB_ROOT/index.php" << EOF
    <?php
    phpinfo();
    ?>
    EOF

    # 配置Nginx
    cat > /etc/nginx/conf.d/default.conf << 'EOF'
    server {
    listen 80;
    server_name localhost;
    root /var/www/html;
    index index.php index.html;

    location / {
    try_files \\$uri \\$uri/ =404;
    }

    location ~ \\.php$ {
    fastcgi_pass unix:/run/php/php-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME \\$document_root\\$fastcgi_script_name;
    include fastcgi_params;
    }
    }
    EOF

    # 检查配置并重启
    nginx -t && systemctl restart nginx

    info "Nginx配置完成"
    }

    # ===== 配置防火墙 =====
    config_firewall() {
    info "配置防火墙…"

    if command -v ufw &>/dev/null; then
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw allow 3306/tcp
    info "ufw防火墙已配置"
    elif command -v firewall-cmd &>/dev/null; then
    firewall-cmd –permanent –add-service=http
    firewall-cmd –permanent –add-service=https
    firewall-cmd –permanent –add-port=3306/tcp
    firewall-cmd –reload
    info "firewalld已配置"
    fi
    }

    # ===== 显示安装结果 =====
    show_result() {
    echo ""
    echo -e "${GREEN}========================================${NC}"
    echo -e "${GREEN} LNMP环境部署完成!${NC}"
    echo -e "${GREEN}========================================${NC}"
    echo ""
    echo " Nginx: 已启动,端口80"
    echo " MySQL: 已启动,端口3306"
    echo " root密码: $MYSQL_ROOT_PASSWORD"
    echo " PHP: 已启动"
    echo ""
    echo " 网站根目录: $WEB_ROOT"
    echo ""
    echo " 测试页面: http://localhost/index.php"
    echo ""
    echo -e "${YELLOW} 请及时修改MySQL密码并删除测试页面!${NC}"
    echo ""
    }

    # ===== 主程序 =====
    info "===== LNMP一键部署脚本 ====="
    echo ""

    check_root
    detect_os

    install_nginx
    install_mysql
    install_php
    config_nginx_php
    config_firewall

    show_result

    info "部署完成!"

    7.13.5 案例:日志分析脚本

    #!/bin/bash
    # Nginx日志分析脚本 log_analyze.sh

    LOG_FILE="${1:-/var/log/nginx/access.log}"

    if [ ! -f "$LOG_FILE" ]; then
    echo "错误:日志文件 $LOG_FILE 不存在"
    exit 1
    fi

    echo ""
    echo "================================================"
    echo " Nginx访问日志分析报告"
    echo " 日志文件: $LOG_FILE"
    echo " 分析时间: $(date '+%Y-%m-%d %H:%M:%S')"
    echo "================================================"
    echo ""

    # 总请求数
    total=$(wc -l < "$LOG_FILE")
    echo "【总请求数】"
    echo " $total 条"
    echo ""

    # 今日请求数
    today=$(date '+%d/%b/%Y')
    today_count=$(grep "$today" "$LOG_FILE" | wc -l)
    echo "【今日请求数】"
    echo " $today_count 条"
    echo ""

    # 独立IP数(Top 10)
    echo "【访问量Top 10 IP】"
    echo " 排名 IP地址 访问次数"
    echo " —- ————– ——–"
    awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10 | \\
    awk '{printf(" %-6s %-15s %d\\n", NR".", $2, $1)}'
    echo ""

    # 被访问最多的页面(Top 10)
    echo "【被访问最多的页面 Top 10】"
    echo " 排名 访问次数 页面"
    echo " —- ——– —-"
    awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10 | \\
    awk '{printf(" %-6s %-8d %s\\n", NR".", $1, $2)}'
    echo ""

    # HTTP状态码统计
    echo "【HTTP状态码统计】"
    awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn | \\
    while read count code; do
    case $code in
    200) desc="OK 成功" ;;
    301) desc="永久重定向" ;;
    302) desc="临时重定向" ;;
    304) desc="未修改" ;;
    403) desc="禁止访问" ;;
    404) desc="未找到" ;;
    500) desc="服务器内部错误" ;;
    502) desc="网关错误" ;;
    503) desc="服务不可用" ;;
    *) desc="其他" ;;
    esac
    printf(" %s %d 次 (%s)\\n" "$code" "$count" "$desc")
    done
    echo ""

    # 404错误统计
    not_found=$(grep ' 404 ' "$LOG_FILE" | wc -l)
    echo "【404错误】"
    echo " 总计: $not_found 次"
    if [ $not_found -gt 0 ]; then
    echo " 缺失页面Top 5:"
    grep ' 404 ' "$LOG_FILE" | awk '{print $7}' | sort | uniq -c | sort -rn | head -5 | \\
    awk '{printf(" %d次 %s\\n", $1, $2)}'
    fi
    echo ""

    # 访问时间分布(按小时)
    echo "【访问时间分布(按小时)】"
    awk '{print $4}' "$LOG_FILE" | cut -c 14-15 | sort | uniq -c | \\
    awk '{
    bar=""
    for(i=0; i<int($1/total*50); i++) bar=bar"█"
    printf(" %s时 %5d %s\\n", $2, $1, bar)
    }'
    total=$(wc -l < "$LOG_FILE") | sort -t'时' -k1 -n
    echo ""

    # 来源统计
    echo "【来源统计 Top 5】"
    awk -F'"' '{print $6}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -5 | \\
    awk '{printf(" %d次 %s\\n", $1, substr($0, index($0,$2)))}'
    echo ""

    # 流量统计
    echo "【流量统计】"
    total_size=$(awk '{sum+=$10} END {printf("%.2f", sum/1024/1024)}' "$LOG_FILE" 2>/dev/null)
    echo " 总传输流量: ${total_size} MB"
    echo ""

    echo "================================================"
    echo " 分析报告结束"
    echo "================================================"

    7.13.6 案例:批量创建用户

    #!/bin/bash
    # 批量创建用户脚本 batch_user.sh

    # 用户列表文件格式:用户名:密码:备注
    # 示例:
    # zhangsan:Pass@123:研发部
    # lisi:Pass@456:测试部

    USER_LIST="${1:-users.txt}"
    DEFAULT_PASSWORD="Default@123"
    DEFAULT_SHELL="/bin/bash"

    # 检查是否root
    if [ "$(id -u)" -ne 0 ]; then
    echo "错误:请使用root用户运行"
    exit 1
    fi

    # 检查用户列表文件
    if [ ! -f "$USER_LIST" ]; then
    echo "用户列表文件 $USER_LIST 不存在"
    echo "正在创建示例文件…"
    cat > "$USER_LIST" << EOF
    zhangsan:Zhang@123:研发部
    lisi:Li@456:测试部
    wangwu:Wang@789:运维部
    EOF

    echo "已创建 $USER_LIST,请修改后重新运行"
    exit 0
    fi

    # 统计
    success=0
    fail=0
    exist=0

    echo "==============================="
    echo " 批量创建用户脚本"
    echo " 用户列表: $USER_LIST"
    echo "==============================="
    echo ""

    # 读取用户列表
    while IFS=: read -r username password comment; do
    # 跳过空行和注释
    [[ -z "$username" || "$username" == \\#* ]] && continue

    # 设置默认密码
    [ -z "$password" ] && password="$DEFAULT_PASSWORD"
    [ -z "$comment" ] && comment="批量创建用户"

    # 检查用户是否已存在
    if id "$username" &>/dev/null; then
    echo "[跳过] 用户 $username 已存在 ($comment)"
    exist=$((exist + 1))
    continue
    fi

    # 创建用户
    useradd -m -s "$DEFAULT_SHELL" -c "$comment" "$username"

    if [ $? -eq 0 ]; then
    # 设置密码
    echo "$username:$password" | chpasswd

    # 强制首次登录修改密码
    chage -d 0 "$username"

    echo "[成功] 创建用户 $username ($comment) 密码: $password"
    success=$((success + 1))
    else
    echo "[失败] 创建用户 $username 失败"
    fail=$((fail + 1))
    fi
    done < "$USER_LIST"

    echo ""
    echo "==============================="
    echo " 执行结果统计"
    echo "==============================="
    echo " 成功创建: $success 个"
    echo " 已存在: $exist 个"
    echo " 失败: $fail 个"
    echo "==============================="

    7.13.7 学习建议

    恭喜你看完了这一整章!Shell脚本编程内容很多,不要期望一次就全部记住。给你几个学习建议:

  • 不要死记硬背:语法记不住很正常,需要用的时候查文档就行
  • 多动手实践:把本章的每个示例都自己运行一遍,改改参数看看效果
  • 从简单开始:先写简单的脚本(几行代码),再慢慢写复杂的
  • 读别人的脚本:网上有很多优秀的Shell脚本,多读多学
  • 善用 man 和 –help:每个命令都有详细文档,养成查文档的习惯
  • 遇到报错别慌:看错误信息,用 bash -x 调试,一步步排查
  • Shell脚本是Linux运维的必备技能,掌握它,你的工作效率会大幅提升。从今天开始,试着把日常重复的操作写成脚本吧!


    本章总结

    知识点核心内容
    Shell概念 命令解释器,用户与内核的桥梁
    脚本基础 Shebang、注释、执行方式
    变量 定义不用,使用要加,使用要加,使用要加,等号不空格
    环境变量 export导出,子进程可见
    位置参数 $1 $2 $@ $# 等
    字符串操作 长度、截取、替换
    数组 普通数组和关联数组
    算术运算 $(( )) 最方便,bc支持小数
    条件判断 if-elif-else、test、[ ]、[[ ]]
    循环 for、while、until、break、continue
    case 多分支选择,支持模式匹配
    函数 封装代码,参数用$1 $2,返回用echo
    输入输出 echo、printf、read、重定向、管道
    调试 bash -x、set -euo pipefail、trap
    实战案例 备份、监控、重命名、部署、日志分析

    下一章我们将学习更多Linux进阶知识,继续加油!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 第七章 Shell脚本编程
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!