Shell 脚本编程简述

135 阅读4分钟

Shell 是 Unix/Linux 系统中的命令行解释器,也是一种脚本编程语言。Shell 脚本广泛应用于系统管理、自动化任务、DevOps 等领域。本文将介绍 Shell 编程的核心概念和特性。

语言生命周期

Shell 是一种解释型语言,由 Shell 解释器逐行执行脚本命令。

常见的 Shell 解释器:

  • Bash (Bourne Again Shell):最流行的 Shell,Linux 默认
  • Zsh (Z Shell):功能强大,macOS 默认
  • Fish (Friendly Interactive Shell):现代化的 Shell
  • Dash:轻量级,常用于系统脚本
  • sh (Bourne Shell):POSIX 标准 Shell

Shell 脚本执行方式:

# 方式1: 直接执行(需要执行权限)
chmod +x script.sh
./script.sh

# 方式2: 通过解释器执行
bash script.sh
sh script.sh

# 方式3: source 或 . 执行(在当前 Shell 进程中执行)
source script.sh
. script.sh

Shebang(释伴): 脚本第一行指定解释器:

#!/bin/bash      # 使用 Bash
#!/bin/sh        # 使用 sh
#!/usr/bin/env bash  # 推荐方式,自动查找 bash

解释过程

Shell 脚本的执行过程:

  1. 源代码
name="张三"
echo "Hello, $name"
  1. 词法分析:将脚本分解为 token

    • 关键字:if, for, while
    • 变量:$name, ${var}
    • 运算符:=, +, -
    • 字符串:引号内的内容
  2. 语法分析:构建命令结构

    • 简单命令
    • 管道命令
    • 重定向
    • 控制结构
  3. 展开和替换

    • 变量展开:$name张三
    • 命令替换:$(date) → 执行结果
    • 通配符展开:*.txt → 匹配的文件列表
    • 花括号展开:{1..5}1 2 3 4 5
  4. 执行

    • 查找命令(内置命令、函数、外部程序)
    • 创建子进程(外部命令)
    • 执行命令
    • 返回退出状态

变量

Shell 变量用于存储数据,变量名区分大小写。

变量的生命周期

  1. 定义和初始化:变量定义时立即初始化
name="张三"          # 定义并初始化
age=25
readonly PI=3.14    # 只读变量
  1. 使用:通过 $ 符号访问变量
echo $name          # 输出:张三
echo ${name}        # 推荐方式,明确变量边界
echo "${name}先生"  # 输出:张三先生
  1. 更新:重新赋值
name="李四"
age=$((age + 1))
  1. 销毁:使用 unset 删除变量
unset name

变量的作用域

Shell 具有动态作用域的特点,变量的可见性基于函数调用栈。

局部变量 vs 全局变量:

global_var="全局变量"

function outer() {
    local local_var="局部变量"  # 仅在函数内可见
    echo $global_var            # 可以访问全局变量
    inner
}

function inner() {
    echo $local_var             # 无法访问 outer 的局部变量
    echo $global_var            # 可以访问全局变量
}

outer

环境变量: 环境变量会传递给子进程。

# 定义环境变量
export PATH="/usr/local/bin:$PATH"
export MY_VAR="value"

# 查看环境变量
env
printenv MY_VAR
echo $MY_VAR

# 临时设置环境变量
MY_VAR="value" ./script.sh  # 仅对该命令有效

动态作用域示例:

x=10

function outer() {
    x=20
    inner
    echo "outer: $x"  # 输出:outer: 30
}

function inner() {
    x=30  # 修改的是 outer 中的 x
    echo "inner: $x"  # 输出:inner: 30
}

outer
echo "global: $x"  # 输出:global: 30(全局变量被修改)

变量类型

Shell 变量默认是字符串类型,但可以通过声明指定特殊类型。

字符串

# 单引号字符串:原样输出,不支持变量替换
str1='Hello, $name'
echo $str1  # 输出:Hello, $name

# 双引号字符串:支持变量替换和转义
str2="Hello, $name"
echo $str2  # 输出:Hello, 张三

# 字符串操作
str="Hello World"
echo ${#str}           # 长度:11
echo ${str:0:5}        # 子串:Hello
echo ${str/World/Bash} # 替换:Hello Bash
echo ${str^^}          # 转大写:HELLO WORLD
echo ${str,,}          # 转小写:hello world

数组

索引数组:

# 定义数组
arr=(apple banana cherry)
arr[3]="date"

# 访问元素
echo ${arr[0]}      # apple
echo ${arr[@]}      # 所有元素
echo ${arr[*]}      # 所有元素
echo ${#arr[@]}     # 数组长度:4

# 数组切片
echo ${arr[@]:1:2}  # banana cherry

# 遍历数组
for item in "${arr[@]}"; do
    echo $item
done

关联数组(Bash 4.0+):

# 声明关联数组
declare -A user

# 定义键值对
user[name]="张三"
user[age]=25
user[email]="zhang@example.com"

# 访问元素
echo ${user[name]}      # 张三

# 获取所有键
echo ${!user[@]}        # name age email

# 获取所有值
echo ${user[@]}         # 张三 25 zhang@example.com

# 遍历关联数组
for key in "${!user[@]}"; do
    echo "$key: ${user[$key]}"
done

整数

# 声明整数变量
declare -i num=10

# 算术运算
num=$((10 + 5))        # 15
num=$((num * 2))       # 30
num=$((num / 3))       # 10
num=$((num % 3))       # 1

# 算术运算符
echo $((5 + 3))        # 8
echo $((5 - 3))        # 2
echo $((5 * 3))        # 15
echo $((5 / 3))        # 1(整数除法)
echo $((5 % 3))        # 2(取模)
echo $((5 ** 2))       # 25(幂运算)

# 自增自减
((num++))
((num--))
((num += 5))
((num -= 3))

只读变量

readonly PI=3.14159
declare -r CONST="常量"

# 尝试修改会报错
PI=3.14  # 错误:PI: readonly variable

特殊变量

Shell 提供了许多特殊变量:

$0      # 脚本名称
$1-$9   # 位置参数(命令行参数)
$#      # 参数个数
$@      # 所有参数(作为独立字符串)
$*      # 所有参数(作为单个字符串)
$?      # 上一个命令的退出状态
$$      # 当前进程 PID
$!      # 最后一个后台进程 PID
$_      # 上一个命令的最后一个参数

示例:

#!/bin/bash

echo "脚本名称: $0"
echo "参数个数: $#"
echo "所有参数: $@"
echo "进程 PID: $$"

ls /nonexistent
echo "退出状态: $?"  # 非零表示错误

sleep 10 &
echo "后台进程 PID: $!"

变量展开

Shell 提供强大的变量展开功能:

# 默认值
echo ${var:-"default"}      # 如果 var 未设置,使用默认值
echo ${var:="default"}      # 如果 var 未设置,设置并返回默认值
echo ${var:?"error message"}  # 如果 var 未设置,输出错误并退出
echo ${var:+"alternative"}  # 如果 var 已设置,使用替代值

# 字符串操作
var="Hello World"
echo ${var#Hello }          # World(删除最短前缀匹配)
echo ${var##*/}             # 删除最长前缀匹配
echo ${var%World}           # Hello (删除最短后缀匹配)
echo ${var%%/*}             # 删除最长后缀匹配

# 模式替换
echo ${var/o/O}             # HellO World(替换第一个)
echo ${var//o/O}            # HellO WOrld(替换所有)

# 大小写转换
echo ${var^^}               # HELLO WORLD
echo ${var,,}               # hello world
echo ${var^}                # Hello World(首字母大写)

运算与表达式

算术运算

# 方式1: $(( ))
result=$((5 + 3))
result=$((10 * 2))

# 方式2: let: 一个内置命令,用于执行算术运算
let result=5+3
let result++

# 方式3: expr(不推荐,已过时)
result=$(expr 5 + 3)

# 浮点运算(需要 bc)
result=$(echo "scale=2; 10 / 3" | bc)  # 3.33

比较运算

数值比较:

# 使用 [[ ]] 或 [ ]
if [[ $a -eq $b ]]; then echo "相等"; fi
if [[ $a -ne $b ]]; then echo "不相等"; fi
if [[ $a -lt $b ]]; then echo "小于"; fi
if [[ $a -le $b ]]; then echo "小于等于"; fi
if [[ $a -gt $b ]]; then echo "大于"; fi
if [[ $a -ge $b ]]; then echo "大于等于"; fi

# 使用 (( ))(更直观)
if ((a == b)); then echo "相等"; fi
if ((a != b)); then echo "不相等"; fi
if ((a < b)); then echo "小于"; fi
if ((a > b)); then echo "大于"; fi

字符串比较:

if [[ $str1 = $str2 ]]; then echo "相等"; fi
if [[ $str1 != $str2 ]]; then echo "不相等"; fi
if [[ $str1 < $str2 ]]; then echo "字典序小于"; fi
if [[ $str1 > $str2 ]]; then echo "字典序大于"; fi
if [[ -z $str ]]; then echo "字符串为空"; fi
if [[ -n $str ]]; then echo "字符串不为空"; fi

文件测试:

if [[ -f $file ]]; then echo "文件存在"; fi
if [[ -d $dir ]]; then echo "目录存在"; fi
if [[ -e $path ]]; then echo "路径存在"; fi
if [[ -r $file ]]; then echo "可读"; fi
if [[ -w $file ]]; then echo "可写"; fi
if [[ -x $file ]]; then echo "可执行"; fi
if [[ -s $file ]]; then echo "文件非空"; fi
if [[ $file1 -nt $file2 ]]; then echo "file1 更新"; fi
if [[ $file1 -ot $file2 ]]; then echo "file1 更旧"; fi

逻辑运算

# 与运算
if [[ $a -gt 0 && $b -gt 0 ]]; then
    echo "都大于0"
fi

# 或运算
if [[ $a -gt 0 || $b -gt 0 ]]; then
    echo "至少一个大于0"
fi

# 非运算
if [[ ! -f $file ]]; then
    echo "文件不存在"
fi

# 组合条件
if [[ ($a -gt 0 && $b -gt 0) || $c -eq 0 ]]; then
    echo "复杂条件"
fi

流程控制

条件判断

if 语句:

if [[ condition ]]; then
    # 代码块
elif [[ condition ]]; then
    # 代码块
else
    # 代码块
fi

case 语句:

case $variable in
    pattern1)
        # 代码块
        ;;
    pattern2|pattern3)
        # 代码块
        ;;
    *)
        # 默认代码块
        ;;
esac

示例:

#!/bin/bash

read -p "输入选项 (a/b/c): " choice

case $choice in
    a|A)
        echo "选择了选项 A"
        ;;
    b|B)
        echo "选择了选项 B"
        ;;
    c|C)
        echo "选择了选项 C"
        ;;
    *)
        echo "无效选项"
        exit 1
        ;;
esac

循环

for 循环:

# C 风格
for ((i = 0; i < 10; i++)); do
    echo $i
done

# 遍历列表
for item in apple banana cherry; do
    echo $item
done

# 遍历数组
arr=(1 2 3 4 5)
for num in "${arr[@]}"; do
    echo $num
done

# 遍历文件
for file in *.txt; do
    echo "处理文件: $file"
done

# 遍历范围
for i in {1..10}; do
    echo $i
done

# 遍历命令输出
for line in $(cat file.txt); do
    echo $line
done

while 循环:

# 基本 while
count=0
while ((count < 5)); do
    echo $count
    ((count++))
done

# 读取文件(推荐方式)
while IFS= read -r line; do
    echo "$line"
done < file.txt

# 无限循环
while true; do
    echo "按 Ctrl+C 退出"
    sleep 1
done

until 循环:

count=0
until ((count >= 5)); do
    echo $count
    ((count++))
done

循环控制:

# break: 退出循环
for i in {1..10}; do
    if ((i == 5)); then
        break
    fi
    echo $i
done

# continue: 跳过当前迭代
for i in {1..10}; do
    if ((i % 2 == 0)); then
        continue
    fi
    echo $i  # 只输出奇数
done

函数

Shell 函数用于代码复用和模块化。

函数定义

# 方式1: function 关键字
function greet() {
    echo "Hello, $1!"
}

# 方式2: 不使用 function 关键字(更常用)
greet() {
    echo "Hello, $1!"
}

# 调用函数
greet "张三"  # 输出:Hello, 张三!

函数参数

function print_info() {
    echo "函数名: $FUNCNAME"
    echo "参数个数: $#"
    echo "第一个参数: $1"
    echo "第二个参数: $2"
    echo "所有参数: $@"
}

print_info "arg1" "arg2" "arg3"

函数返回值

# 只能返回数字/退出状态(0-255)
function is_positive() {
    if ((   $1 > 0 )); then
        return 0  # 成功
    else
        return 1  # 失败
    fi
}

if is_positive 10; then
    echo "是正数"
fi

# 返回字符串(通过 echo)
function get_user_name() {
    local name="张三"
    echo "$name"
}

username=$(get_user_name)
echo "用户名: $username"

递归函数

# 计算阶乘
function factorial() {
    local n=$1
    if ((n <= 1)); then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}

result=$(factorial 5)
echo "5! = $result"  # 输出:5! = 120

输入输出

标准输入输出

# 标准输出
echo "Hello World"
printf "格式化输出: %s %d\n" "数字" 42

# 标准输入
read -p "输入姓名: " name
echo "你好, $name"

# 读取多个值
read -p "输入三个数字: " a b c
echo "a=$a, b=$b, c=$c"

# 读取密码(不显示输入)
read -sp "输入密码: " password
echo

# 读取数组
read -a arr -p "输入多个值: "
echo "数组: ${arr[@]}"

重定向

# 输出重定向
echo "Hello" > file.txt       # 覆盖写入
echo "World" >> file.txt      # 追加写入

# 输入重定向
wc -l < file.txt              # 从文件读取输入

# 错误重定向
command 2> error.log          # 错误输出到文件
command 2>&1                  # 错误重定向到标准输出
command &> output.log         # 标准输出和错误都重定向

# Here Document
cat << EOF > file.txt
这是多行文本
第二行
第三行
EOF

# Here String
grep "pattern" <<< "这是一行文本"

管道

# 基本管道
ls | grep ".txt"
cat file.txt | grep "pattern" | wc -l

# tee:同时输出到文件和标准输出
ls | tee file_list.txt | wc -l

# xargs:将标准输入转换为命令参数
find . -name "*.txt" | xargs rm -f
echo "file1 file2 file3" | xargs -n 1 echo

错误处理

退出状态

# 检查命令是否成功
if cp source.txt dest.txt; then
    echo "复制成功"
else
    echo "复制失败"
    exit 1
fi

# 使用 $? 检查退出状态
mkdir /tmp/test
if [[ $? -eq 0 ]]; then
    echo "目录创建成功"
fi

set 选项

# 遇到错误立即退出
set -e

# 使用未定义变量时报错
set -u

# 管道中任何命令失败都会失败
set -o pipefail

# 组合使用(推荐)
set -euo pipefail

# 调试模式:打印每个命令
set -x

# 关闭选项
set +x

trap 捕获信号

# 捕获 EXIT 信号(脚本退出时执行)
trap "echo '清理资源'; rm -f /tmp/tempfile" EXIT

# 捕获 ERR 信号(命令失败时执行)
trap "echo '错误: 第 $LINENO 行'" ERR

# 捕获 Ctrl+C (SIGINT)
trap "echo '收到中断信号'; exit 1" INT

# 捕获多个信号
trap "cleanup" EXIT INT TERM

function cleanup() {
    echo "执行清理..."
    rm -f /tmp/*.tmp
}