Linux专题十三:shell脚本编程

一、Shell 脚本基础认知

1. 解释型 vs 编译型语言

类型 代表语言 执行原理 优缺点
解释型 Shell、Python、Perl 无需编译,由解释器逐行解析执行 (如 Bash 解释器解析 .sh 脚本) 优点:开发效率高、修改后可直接执行;缺点:运行效率低于编译型,依赖解释器
编译型 C/C++、Go、Java(半编译) 先通过编译器将源代码转换为机器可识别的二进制指令,再执行生成的可执行文件 优点:运行效率高、不依赖编译器(仅需运行可执行文件);缺点:开发效率低,修改后需重新编译

2.Shell 脚本的两种执行方式(核心区别:权限与子 Shell)

bash 复制代码
# 格式:解释器名称 脚本文件路径
bash a.sh
sh a.sh  # sh 是 bash 的简化版,部分语法不兼容
  • 无需为脚本授予执行权限x 权限);
  • 会启动子 Shell 进程执行脚本,脚本内的变量、环境修改仅在子 Shell 中有效,执行完毕后子 Shell 退出,修改不影响当前终端环境。
(2)脚本自身作为可执行文件执行
bash 复制代码
# 第一步:授予执行权限(仅需执行一次)
chmod +x a.sh

# 第二步:执行脚本(需指定路径,./ 表示当前目录)
./a.sh  # 相对路径执行
/home/user/a.sh  # 绝对路径执行
  • 必须先通过 chmod +x 授予执行权限;
  • 脚本开头需指定解释器(#!/bin/bash#!/bin/sh),否则系统会使用默认解释器;
  • 默认同样启动子 Shell 进程 执行,若需在当前 Shell 执行,需使用 source. 命令。
(3)在当前 Shell 环境执行(保留脚本修改)
bash 复制代码
# 两种格式等价,均在当前 Shell 中执行,不启动子进程
source a.sh
. a.sh  # 注意:. 与脚本路径之间有空格
  • 无需授予执行权限;
  • 脚本内的变量赋值、环境变量修改、函数定义等会直接作用于当前终端环境,执行后永久保留(直到终端关闭)。

3. 脚本头部指定解释器

bash 复制代码
#!/bin/bash  # 指定该脚本由 Bash 解释器解析执行(推荐,功能更完善)
#!/bin/sh    # 指定该脚本由 sh 解释器解析执行(兼容性强,功能有限)
  • 该语句必须位于脚本第一行第一列 ,以 #! 开头(称为 Shebang 符号);
  • 若省略该语句,系统会使用当前终端的默认 Shell 解释器执行脚本。

二、Shell 变量(无类型,核心注意格式)

1. 变量的定义与赋值(严格遵循格式)

(1)核心规则
  1. 变量名由字母、数字、下划线组成,不能以数字开头;
  2. 赋值符号 = 左右无任何空格(Shell 语法的严格要求,多空格会被解析为命令);
  3. 变量无类型区分,默认存储为字符串,数值运算需额外处理;
  4. 字符串值包含空格、特殊字符 时,必须用双引号 "单引号 ' 包裹。
(2)示例
bash 复制代码
# 合法定义
val=100                # 纯数字(无空格,无需引号)
str="hello world"      # 包含空格,用双引号包裹
str2='hello $val'      # 包含特殊字符($),用单引号包裹
name=zhangsan          # 纯字符串(无空格,无需引号)

# 非法定义(会报错)
val = 100  # = 左右有空格
str=hello world  # 包含空格,未加引号
1name=zhangsan  # 以数字开头

2. 变量的取值与输出

(1)取值语法:$变量名${变量名}
  • $变量名:简单取值,适用于无歧义场景;
  • ${变量名}:明确变量边界,避免与后续字符混淆(推荐,更严谨)。
(2)输出命令:echo(核心)
bash 复制代码
val=100
# 简单输出变量
echo $val  # 输出:100
echo ${val}  # 输出:100,与上面等价

# 变量嵌入字符串输出(推荐用双引号,可解析变量)
echo "val的取值是:$val"  # 输出:val的取值是:100
echo "val+10的结果是:$((val+10))"  # 输出:val+10的结果是:110(简单数值运算)

# 避免变量解析(单引号,原样输出)
echo 'val的取值是:$val'  # 输出:val的取值是:$val

3. 变量的作用域(全局变量 vs 局部变量)

(1)全局变量(默认)
  • 定义:直接赋值,无 local 关键字修饰;
  • 作用域:整个脚本有效,若在当前 Shell 执行脚本(source/.),则终端环境中也有效;
  • 注意:父子 Shell 中,子 Shell 会继承父 Shell 的全局变量,但子 Shell 对变量的修改不会影响父 Shell。
(2)局部变量(仅函数内有效)
  • 定义:在函数体内local 关键字修饰;
  • 作用域:仅当前函数内部有效,函数执行完毕后自动销毁,不会污染全局变量;
  • 示例:
bash 复制代码
#!/bin/bash
# 全局变量
global_val=10

# 定义函数
test_fun() {
    # 局部变量(仅函数内有效)
    local local_val=20
    echo "函数内:local_val=$local_val"
    echo "函数内:global_val=$global_val"  # 函数内可访问全局变量
}

# 调用函数
test_fun

# 函数外访问局部变量(无输出,变量不存在)
echo "函数外:local_val=$local_val"
echo "函数外:global_val=$global_val"

4. 特殊变量(系统预定义,无需手动赋值)

Shell 提供了一系列预定义的特殊变量,用于获取脚本执行时的命令行参数、进程信息等,核心常用如下:

特殊变量 含义说明
$0 脚本的文件名(包含执行路径,如 ./a.sh/home/user/a.sh
$1~$n 脚本的命令行参数,$1 是第一个参数,$2 是第二个参数,以此类推($10 需写为 ${10}
$# 脚本的命令行参数总个数
$* 所有命令行参数的集合,将所有参数视为一个整体(用双引号包裹时:"$*"等价于"1 2 ... $n")
$@ 所有命令行参数的集合,将每个参数视为独立个体(用双引号包裹时:"$@"等价于"1" "2"..."$n",推荐使用)
$$ 当前脚本进程的 PID(进程 ID)
$? 上一条命令 / 脚本的执行返回状态码(0 表示执行成功,非 0 表示执行失败,具体值对应错误类型)
$_ 上一条命令的最后一个参数
示例:获取命令行参数
bash 复制代码
#!/bin/bash
echo "脚本文件名:$0"
echo "第一个参数:$1"
echo "第二个参数:$2"
echo "参数总个数:$#"
echo "所有参数(整体):$*"
echo "所有参数(独立):$@"

执行命令:./a.sh hello world,输出结果:

cpp 复制代码
脚本文件名:./a.sh
第一个参数:hello
第二个参数:world
参数总个数:2
所有参数(整体):hello world
所有参数(独立):hello world

5. 环境变量(全局有效,继承性)

(1)核心特性
  • 环境变量是全局变量,可在当前 Shell 及所有子 Shell 中生效;
  • 父子 Shell 之间存在继承关系:子 Shell 会继承父 Shell 的环境变量,但子 Shell 无法修改父 Shell 的环境变量。
(2)定义环境变量:export 关键字
bash 复制代码
# 格式1:先定义变量,再导出为环境变量
val=300
export val

# 格式2:定义与导出一步完成(推荐)
export str="global environment variable"
3)常用系统环境变量
  • PATH:系统命令查找路径,新增自定义命令路径可追加:export PATH=$PATH:/home/user/bin
  • HOME:当前用户的主目录路径(如 /home/user);
  • USER:当前登录用户的用户名;
  • PWD:当前工作目录的路径;
  • SHELL:当前使用的 Shell 解释器路径(如 /bin/bash)。

6. 命令替换(获取命令执行结果赋值给变量)

两种格式等价,推荐使用 $(命令)(可读性更强,支持嵌套):

bash 复制代码
# 格式1:反引号 ``(注意:不是单引号,位于键盘 Tab 键上方)
str=`ls`  # 将 ls 命令的执行结果(当前目录文件列表)赋值给 str

# 格式2:$(命令)(推荐,支持嵌套)
file_count=$(ls | wc -l)  # 嵌套:先执行 ls,再通过管道传递给 wc -l 统计文件个数
dir_path=$(cd $(dirname $0) && pwd)  # 获取脚本所在目录的绝对路径

三、Shell 流程控制(选择、循环)

1. 条件判断:if 语句(单分支 / 双分支 / 多分支)

(1)核心语法规则
  1. 条件判断表达式必须放在中括号 []双中括号 [[]] 中;
  2. [ ] 左右必须保留空格if [ 条件 ]; then),这是 Shell 语法的严格要求,缺少空格会报错;
  3. 语句结构:if/elif 后必须跟 thenelse 后无需 then,整个 if 语句以 fiif 倒写)结尾;
  4. 多条件组合:[ ] 中用 -a(与)、-o(或),[[]] 中用 &&(与)、||(或)(推荐 [[]],可读性更强);
  5. 数值比较、字符串比较、文件判断有固定的运算符,不可混用。
(2)常用判断运算符
判断类型 运算符([ ] 中使用) 含义说明
数值比较 num1 -eq num2 等于(equal)
num1 -ne num2 不等于(not equal)
num1 -gt num2 大于(greater than)
num1 -lt num2 小于(less than)
num1 -ge num2 大于等于(greater or equal)
num1 -le num2 小于等于(less or equal)
字符串比较 str1 = str2 相等(注意:= 左右有空格)
str1 != str2 不相等
-z str 字符串为空(长度为 0)
-n str 字符串非空(长度大于 0)
文件判断 -f file 文件存在且为普通文件
-d dir 目录存在且为目录
-e path 文件 / 目录存在
-r file 文件存在且有读取权限
-w file 文件存在且有写入权限
-x file 文件存在且有执行权限
(3)示例

单分支 if

bash 复制代码
#!/bin/bash
read -p "请输入一个数字:" num
if [ $num -ge 60 ]; then
    echo "及格"
fi
双分支 if-else
bash 复制代码
#!/bin/bash
read -p "请输入一个数字:" num
if [[ $num -ge 60 && $num -le 100 ]]; then
    echo "及格"
else
    echo "不及格"
fi
多分支 if-elif-else
bash 复制代码
#!/bin/bash
read -p "请输入一个数字(0-100):" num
if [[ $num -ge 90 && $num -le 100 ]]; then
    echo "优秀"
elif [[ $num -ge 80 && $num -lt 90 ]]; then
    echo "良好"
elif [[ $num -ge 60 && $num -lt 80 ]]; then
    echo "及格"
else
    echo "不及格"
fi

2. 多分支选择:case 语句(等价于 C 语言 switch

(1)核心语法规则
  1. 结构:case 变量名 in 开头,esaccase 倒写)结尾;
  2. 模式匹配:模式) 结尾,支持通配符(* 匹配任意内容,| 匹配多个模式);
  3. 分支结束:每个分支的命令执行完毕后,用 ;; 结尾(等价于 C 语言 break,防止穿透);
  4. 默认分支:用 *) 匹配所有未定义的模式(等价于 C 语言 default)。
(2)示例
bash 复制代码
#!/bin/bash
read -p "请输入一个字符(a/b/c/其他):" char
case $char in
    a|A)  # 匹配小写 a 或大写 A
        echo "你输入的是字母 A"
        ;;
    b|B)
        echo "你输入的是字母 B"
        ;;
    c|C)
        echo "你输入的是字母 C"
        ;;
    *)  # 默认分支,匹配所有其他字符
        echo "你输入的不是 a/b/c 中的字符"
        ;;
esac

3. 循环语句:for / while

(1)for 循环(两种常用格式)
格式 1:遍历列表元素(常用,适用于已知循环次数)
bash 复制代码
#!/bin/bash
# 格式:for 变量名 in 列表元素1 列表元素2 ... 列表元素n
for i in {1..10}  # {1..10} 表示生成 1 到 10 的整数列表
do
    # 循环体:通过 $i 获取当前遍历的元素
    touch "file${i}.txt"  # 创建 file1.txt 到 file10.txt
    echo "已创建:file${i}.txt"
done

# 遍历自定义列表
for fruit in apple banana orange
do
    echo "水果:$fruit"
done
格式 2:C 语言风格(适用于复杂循环条件)
bash 复制代码
#!/bin/bash
# 格式:for (( 初始化表达式; 条件判断表达式; 增量/减量表达式 ))
sum=0
for (( i=1; i<=100; i++ ))
do
    sum=$((sum + i))  # 累加 1 到 100
done
echo "1+2+...+100=$sum"
(2)while 循环(适用于未知循环次数,满足条件则循环)
bash 复制代码
#!/bin/bash
# 格式:while [ 条件判断表达式 ]
num=1
sum=0
while [ $num -le 100 ]
do
    sum=$((sum + num))
    num=$((num + 1))  # 更新循环变量,避免无限循环
done
echo "1+2+...+100=$sum"

# 无限循环(两种格式等价)
while true
do
    echo "无限循环中,按 Ctrl+C 退出"
    sleep 1  # 休眠 1 秒
done

# 简化版无限循环
while :
do
    echo "无限循环中,按 Ctrl+C 退出"
    sleep 1
done
(3)循环控制:break / continue
  • break:跳出当前整个循环,不再执行后续循环体;

  • continue:跳过当前循环的剩余内容,直接进入下一次循环。

    bash 复制代码
    #!/bin/bash
    # 示例:打印 1 到 10,跳过 5,遇到 8 跳出循环
    for i in {1..10}
    do
        if [ $i -eq 5 ]; then
            continue  # 跳过 5,直接进入下一次循环
        fi
        if [ $i -eq 8 ]; then
            break  # 跳出整个循环,不再执行后续内容
        fi
        echo "当前数字:$i"
    done

    四、Shell 函数(封装可复用逻辑)

    1. 函数的定义(两种格式,推荐格式 2)

    格式 1:简化格式(无 function 关键字)
    bash 复制代码
    fun_name() {
        # 函数体:可包含任意 Shell 命令、变量、流程控制
        echo "函数 $fun_name 正在执行"
        echo "第一个参数:$1"
        echo "第二个参数:$2"
    }
    格式 2:标准格式(带 function 关键字,可读性更强)
    bash 复制代码
    function fun_name() {
        local num1=$1  # 局部变量,仅函数内有效
        local num2=$2
        local sum=$((num1 + num2))
        echo "两数之和:$sum"
    }

    2. 函数的调用(无需括号,直接传参)

    bash 复制代码
    #!/bin/bash
    # 先定义函数(Shell 函数必须先定义后调用,无声明机制)
    function add() {
        local num1=$1
        local num2=$2
        echo $((num1 + num2))
    }
    
    # 调用函数:格式为「函数名 参数1 参数2 ...」,无需加括号
    result=$(add 10 20)  # 捕获函数输出结果
    echo "10+20=$result"
    
    add 5 8  # 直接调用,输出结果

    3. 函数的返回值(两种方式)

    (1)return 语句(仅返回 0~255 的状态码)
  • return 0 表示函数执行成功,return 非0 表示执行失败;

  • 调用后通过 $? 获取返回值。

    bash 复制代码
    #!/bin/bash
    function check_file() {
        if [ -f "$1" ]; then
            return 0  # 文件存在,返回成功
        else
            return 1  # 文件不存在,返回失败
        fi
    }
    
    # 调用函数
    check_file "test.txt"
    if [ $? -eq 0 ]; then
        echo "文件存在"
    else
        echo "文件不存在"
    fi
    (2)echo 输出(返回任意数据,推荐)
  • 函数内通过 echo 输出需要返回的数据,调用时通过命令替换 $(函数名) 捕获返回值;

  • 支持返回字符串、数值、复杂结果,灵活性更高。

    bash 复制代码
    #!/bin/bash
    function get_info() {
        local name=$1
        local age=$2
        echo "姓名:$name,年龄:$age"  # 输出返回值
    }
    
    # 捕获函数返回值
    info=$(get_info "zhangsan" 20)
    echo $info

    五、Shell 文本处理三剑客(grep/awk/sed)

    你已经梳理了 awksed 的核心内容,这里提炼三者的核心定位和常用用法,形成清晰对比:

    1. 三剑客核心定位对比

    工具 核心定位 典型用途
    grep 文本查找 / 匹配(过滤符合条件的行),基于正则表达式 查找日志中的错误信息、筛选包含指定关键字的行、统计匹配行数
    awk 文本分析 / 统计 / 格式化(处理结构化文本,按列提取、计算),自带编程语言特性 统计文件行数 / 列数、提取 CSV 文件的指定列、计算数值总和、格式化文本输出
    sed 文本编辑 / 修改(逐行替换、删除、插入、追加),基于流编辑 批量替换配置文件中的参数、删除文本中的注释行、在指定行插入内容、修改文件名

    2. 核心用法示例(快速上手)

    (1)grep(查找核心)
    bash 复制代码
    # 基础用法:查找包含 "error" 的行
    grep "error" log.txt
    
    # 忽略大小写 + 显示行号 + 递归查找(查找目录下所有文件)
    grep -i -n -r "warning" /home/user/logs
    
    # 反向匹配:查找不包含 "info" 的行
    grep -v "info" log.txt
    (2)awk(分析核心,结构化文本处理)
    bash 复制代码
    # 基础用法:打印文件的第 1 列和第 3 列(默认分隔符:空格/制表符)
    awk '{print $1, $3}' data.txt
    
    # 指定分隔符(逗号),处理 CSV 文件
    awk -F "," '{print $2, $4}' test.csv
    
    # 统计文件总行数(NR 为内置变量,标识当前行号)
    awk 'END {print NR}' data.txt
    
    # 计算第 2 列的总和
    awk '{sum += $2} END {print "总和:", sum}' data.txt
    (3)sed(编辑核心,批量修改文本)
    bash 复制代码
    # 基础用法:全局替换 "old" 为 "new"(仅输出,不修改原文件)
    sed 's/old/new/g' test.txt
    
    # 直接修改原文件(-i,慎用,建议先备份)
    sed -i.bak 's/old/new/g' test.txt  # -i.bak 生成备份文件 test.txt.bak
    
    # 删除第 2 到第 8 行
    sed '2,8d' test.txt
    
    # 在第 3 行上方插入内容
    sed '3i\===== 插入的表头 =====' test.txt
    
    # 替换第 5 行的整行内容
    sed '5c\这是替换后的第 5 行' test.txt

    六、Shell 脚本实操注意事项(避坑指南)

  • 语法严格性 :Shell 对空格、引号、符号的格式要求极高,= 左右无空格、[ ] 左右有空格、字符串包含空格需用双引号包裹;

  • 变量边界 :取值时尽量用 ${变量名} 代替 $变量名,避免与后续字符混淆(如 ${i}txt 而非 $itxt);

  • 权限问题 :执行脚本前确认是否有对应文件的读取 / 执行权限,修改系统文件时需用 sudo 提升权限;

  • 备份优先 :使用 sed -i 直接修改原文件前,务必先备份(-i.bak),避免误操作导致数据丢失;

  • 调试脚本 :通过 bash -x a.sh 执行脚本,可打印脚本的执行过程(逐行解析命令),便于排查语法错误和逻辑问题。

相关推荐
浅陌sss2 小时前
使用Unity从IIS搭建的文件服务器下载资源时出现HTTP/1.1 404 Not Found
运维·服务器·http
小馬佩德罗3 小时前
如何将x264 x265的动态库编译入Linux系统中的FFmpeg源码 - x264库编译
linux·ffmpeg·x264
赵民勇3 小时前
awk用法与技巧详解
linux·shell
电商API&Tina3 小时前
【电商API接口】多电商平台数据API接入方案(附带实例)
运维·开发语言·数据库·chrome·爬虫·python·jenkins
V胡桃夹子3 小时前
Docker快速部署apollo
运维·docker·容器
有谁看见我的剑了?3 小时前
ssh服务限制用户登录
运维·服务器·ssh
航Hang*3 小时前
第三章:网络系统建设与运维(中级)——交换技术
运维·笔记·计算机网络·华为·ensp·交换机
终端行者3 小时前
Nginx端到端反向代理https配置
运维·nginx·https
代码炼金术士3 小时前
认识JVM
运维·服务器·jvm