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 脚本的执行过程:
- 源代码
name="张三"
echo "Hello, $name"
-
词法分析:将脚本分解为 token
- 关键字:
if,for,while等 - 变量:
$name,${var}等 - 运算符:
=,+,-等 - 字符串:引号内的内容
- 关键字:
-
语法分析:构建命令结构
- 简单命令
- 管道命令
- 重定向
- 控制结构
-
展开和替换
- 变量展开:
$name→张三 - 命令替换:
$(date)→ 执行结果 - 通配符展开:
*.txt→ 匹配的文件列表 - 花括号展开:
{1..5}→1 2 3 4 5
- 变量展开:
-
执行
- 查找命令(内置命令、函数、外部程序)
- 创建子进程(外部命令)
- 执行命令
- 返回退出状态
变量
Shell 变量用于存储数据,变量名区分大小写。
变量的生命周期
- 定义和初始化:变量定义时立即初始化
name="张三" # 定义并初始化
age=25
readonly PI=3.14 # 只读变量
- 使用:通过
$符号访问变量
echo $name # 输出:张三
echo ${name} # 推荐方式,明确变量边界
echo "${name}先生" # 输出:张三先生
- 更新:重新赋值
name="李四"
age=$((age + 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
}