Shell内容讲解
一、Shell 脚本基础概念
-
什么是 Shell 脚本?
Shell 脚本是一个包含一系列 Shell 命令的文本文件,用于自动化执行任务(如文件操作、程序调用、系统管理等)。 -
Shell 类型
bash
(Bourne-Again Shell):Linux 系统默认 Shell。sh
(Bourne Shell):更早期的标准 Shell。zsh
、ksh
等:其他变体,语法略有差异。
推荐使用bash
,本教程以bash
为例。
二、编写第一个 Shell 脚本
1. 创建脚本文件
# 创建文件并编辑
vim hello.sh
2. 编写脚本内容
#!/bin/bash # Shebang 行:指定脚本解释器为 bash
echo "Hello World!" # 输出文本
3. 赋予执行权限
chmod +x hello.sh # 添加可执行权限
4. 运行脚本
./hello.sh # 直接运行(需在脚本所在目录)
# 或
bash hello.sh # 显式指定解释器
输出:
Hello World!
三、Shell 脚本核心语法
以下是 Shell 脚本语法和使用的超详细指南,结合实用示例,涵盖从基础到进阶的核心内容:
一、Shell 脚本基础结构
1. Shebang 行
Shebang 行(又称 hashbang)是脚本文件的第一行,用于指定执行该脚本的解释器。当你在终端中直接运行脚本时,系统会根据 Shebang 行选择正确的解释器
- 作用:指定脚本使用的解释器。
- 语法:
#!/bin/bash # 使用 bash 解释器 #!/bin/sh # 使用 sh 解释器
2. 注释
- 单行注释:以
#
开头。# 这是一个注释
- 多行注释(通过字符串技巧):
: ' 这是 多行注释 '
二、变量与数据类型
1. 变量定义与使用
- 定义变量(无数据类型,默认为字符串):
name="Alice" # 字符串 count=10 # 整数 files=$(ls) # 命令执行结果赋值 today=$(date +%F) # 日期格式化为字符串
- 使用变量:
echo $name # 直接引用 echo "${name}" # 推荐用 {} 包裹变量名
2. 变量作用域
- 局部变量:默认仅在当前脚本或函数内有效。
- 环境变量:通过
export
导出,子进程可继承。export PATH="/usr/local/bin:$PATH"
3. 特殊变量
变量 | 含义 |
---|---|
$HOME | 当前用户主目录的路径 |
$PATH | 可执行文件路径的列表 |
$0 | 脚本名称 |
$1 -$9 | 第 1 到第 9 个参数 |
$# | 参数个数 |
$@ | 所有参数(列表形式) |
$* | 所有参数(字符串形式) |
$? | 上一条命令的退出状态码,0通常表示没有错误,非0值表示有错误 |
$$ | 当前脚本的进程 ID |
$! | 最后一个后台命令的进程 ID |
echo $PATH
执行结果
三、条件判断
1. 基本语法
if [ 条件 ]; then# 命令
elif [ 条件 ]; then# 命令
else# 命令
fi
2. 条件测试类型
- 数值比较:
lt(less than):小于
le(less than or equal to):小于等于
gt(greater than):大于
ge(greater than or equal to):大于等于
eq(equal to):等于
ne(not equal to):不等于
[ $a -eq $b ] # a == b
[ $a -ne $b ] # a != b
[ $a -gt $b ] # a > b
[ $a -lt $b ] # a < b
-
字符串比较:
[ "$str1" == "$str2" ] # 字符串相等 [ "$str1" != "$str2" ] # 字符串不等 [ -z "$str" ] # 字符串为空 [ -n "$str" ] # 字符串非空
-
文件/目录测试:
[ -f "file.txt" ] # 文件存在且为普通文件 [ -d "dir" ] # 目录存在 [ -e "path" ] # 文件/目录存在 [ -r "file" ] # 文件可读 [ -w "file" ] # 文件可写 [ -x "file" ] # 文件可执行
3. 逻辑运算符
[ 条件1 ] && [ 条件2 ] # AND
[ 条件1 ] || [ 条件2 ] # OR
! [ 条件 ] # NOT
4. 示例:检查文件是否存在
#!/bin/bash
file="data.txt"
if [ -f "$file" ]; thenecho "$file 存在"
elseecho "$file 不存在"
fi
四、循环结构
1. for
循环
- 遍历列表:
for i in 1 2 3; doecho "数字: $i" done
- 遍历命令输出:
for file in $(ls *.txt); doecho "处理文件: $file" done
2. while
循环
count=1
while [ $count -le 5 ]; doecho "计数: $count"((count++))
done
3. until
循环
count=1
until [ $count -gt 5 ]; doecho "计数: $count"((count++))
done
4. 循环控制
break
:退出循环。continue
:跳过当前迭代。
五、函数
1. 定义与调用
# 定义函数
greet() {echo "Hello, $1!"
}# 调用函数
greet "Alice" # 输出:Hello, Alice!
2. 返回值
-
通过
return
返回状态码(0-255):is_even() {if [ $(($1 % 2)) -eq 0 ]; thenreturn 0 # 偶数,成功elsereturn 1 # 奇数,失败fi }is_even 4 echo $? # 输出 0
-
通过
echo
返回数据:add() {echo $(($1 + $2)) }result=$(add 3 5) echo $result # 输出 8
-
外层 $(( )):表示这是一个算术运算表达式,Shell 会计算括号内的内容并返回结果。
-
内部的 1 :表示函数的第一个参数(位置参数), 1:表示函数的第一个参数(位置参数), 1:表示函数的第一个参数(位置参数), 符号用于引用参数的值。
-
六、参数处理
1. 位置参数
#!/bin/bash
echo "脚本名: $0"
echo "第一个参数: $1"
echo "所有参数: $@"
2. 参数解析(getopts
)
#!/bin/bash
while getopts ":u:p:" opt; do # 静默模式(以 : 开头)case $opt inu) user="$OPTARG" ;;p) pass="$OPTARG" ;;:) echo "错误:选项 -$OPTARG 需要参数" >&2; exit 1 ;; # 缺少参数\?) echo "错误:无效选项 -$OPTARG" >&2; exit 1 ;; # 未知选项esac
done
shift $((OPTIND - 1)) # 移除已解析的选项,保留剩余参数
运行示例:
./script.sh -u alice -p 1234
代码功能
- 通过
getopts
解析命令行参数-u
和-p
,分别获取用户名和密码。 - 将参数值保存到变量
user
和pass
中。 - 输出用户和密码信息。
getopts
用法详解
getopts
是 Bash 中解析命令行选项的标准工具,适合处理短选项(如 -u
、-p
)。
1. 基本语法
while getopts "选项字符串" opt; docase $opt in# 处理逻辑esac
done
- 选项字符串:定义支持的选项和是否带参数。
- 单个字母表示选项(如
u
对应-u
)。 - 字母后加
:
表示该选项需要参数(如u:
表示-u value
)。 - 若选项字符串以
:
开头(如":u:p:"
),则静默处理错误(需自行捕获)。
- 单个字母表示选项(如
2. 内置变量
$OPTARG
:当前选项的参数值(仅当选项需要参数时有效)。$OPTIND
:下一个待处理参数的索引,通常用于shift
跳过已解析的参数。
3. 错误处理
- 无效选项:
opt
会被赋值为?
。 - 缺少参数:若选项字符串以
:
开头,opt
会被赋值为:
,否则为?
。
对上面示例代码的改进
- 对必选参数进行校验:在示例中,如果用户未提供
-u
或-p
,变量user
或pass
可能为空,但脚本不会报错。 - 清理已解析参数:使用
shift $((OPTIND - 1))
,避免后续处理位置参数时包含已解析的选项。
改进后的代码
#!/bin/bash
# 添加错误处理和参数校验
while getopts ":u:p:" opt; docase $opt inu) user="$OPTARG" ;;p) pass="$OPTARG" ;;:) echo "错误:选项 -$OPTARG 需要参数" >&2; exit 1 ;;\?) echo "错误:无效选项 -$OPTARG" >&2; exit 1 ;;esac
done# 校验必须参数
if [[ -z "$user" || -z "$pass" ]]; thenecho "错误:必须提供 -u 和 -p 参数" >&2echo "用法: $0 -u <用户> -p <密码>" >&2exit 1
fishift $((OPTIND - 1)) # 清理已解析的选项
echo "用户: $user, 密码: $pass"
- 使用 [[ ]] 更安全(避免变量未定义的错误)
- [[ -z “$var” ]] # 推荐
- [ -z “$var” ] # 也可用,但需注意变量未定义的情况
正确执行
$ ./script.sh -u alice -p 1234
用户: alice, 密码: 1234
错误情况
-
无效选项:
$ ./script.sh -a 错误:无效选项 -a
-
缺少参数:
$ ./script.sh -u 错误:选项 -u 需要参数
-
未提供必选参数:
$ ./script.sh -u alice 错误:必须提供 -u 和 -p 参数 用法: ./script.sh -u <用户> -p <密码>
getopts
是解析命令行选项的标准工具,需结合case
和内置变量使用。- 通过选项字符串定义选项是否需要参数(如
u:
)。 - 错误处理需区分“无效选项”和“缺少参数”,并校验必要参数。
- 使用
shift $((OPTIND - 1))
清理已解析的参数。
七、错误处理
1. 错误退出
if [ ! -f "file.txt" ]; thenecho "错误:文件不存在" >&2 # 输出到标准错误exit 1
fi
2. 捕获信号
trap "echo '脚本被中断!'; exit" SIGINT
3. 调试模式
set -x # 打印执行的命令
set -e # 遇到错误立即退出
set -o pipefail # 管道命令失败时退出
八、高级技巧
1. 数组操作
# 定义数组
fruits=("apple" "banana" "cherry")# 访问元素
echo ${fruits[0]} # apple# 遍历数组
for fruit in "${fruits[@]}"; doecho "$fruit"
done# 数组长度
echo ${#fruits[@]} # 3
在 Bash 脚本中,${fruits[@]}
中的 @
符号用于 展开数组的所有元素,并确保每个元素被视为独立的字符串(即使元素包含空格或特殊字符)。以下是详细解释:
1.${num[@]}
:
安全展开数组所有元素,保留每个元素的独立性,是遍历数组的推荐方式。
2.@
符号:
代表数组的全部元素,配合双引号使用时,确保数据完整性和可靠性。
1. 数组定义与 @
的作用
假设数组 fruits
定义如下:
fruits=("apple" "banana" "orange with spaces" "grape")
-
${fruits[@]}
:
展开数组的所有元素,每个元素保持独立。
结果:"apple" "banana" "orange with spaces" "grape"
。 -
对比
${fruits[*]}
:
展开数组的所有元素,合并成一个字符串(默认用空格分隔)。
结果:"apple banana orange with spaces grape"
。
2. 关键区别
语法 | 行为 | 适用场景 |
---|---|---|
"${fruits[@]}" | 每个元素保持独立,即使包含空格也会正确分割 | 遍历数组元素,保留原始数据 |
"${fruits[*]}" | 所有元素合并成一个字符串,用 IFS 的第一个字符(默认空格)分隔 | 需要整体输出数组内容时 |
${fruits[@]} (无引号) | 元素可能被二次分词(若元素含空格或通配符,会被拆分成多个部分) | 不推荐,可能导致意外行为 |
${fruits[*]} (无引号) | 同上,合并后的字符串可能被二次分词 | 不推荐 |
3. 示例演示
场景 1:遍历数组元素(正确方式)
for fruit in "${fruits[@]}"; doecho "Fruit: $fruit"
done
输出:
Fruit: apple
Fruit: banana
Fruit: orange with spaces
Fruit: grape
- 即使元素包含空格(如
"orange with spaces"
),也会被当作一个整体处理。
场景 2:错误用法(无引号)
for fruit in ${fruits[@]}; doecho "Fruit: $fruit"
done
输出:
Fruit: apple
Fruit: banana
Fruit: orange
Fruit: with
Fruit: spaces
Fruit: grape
"orange with spaces"
被拆分成 3 个“虚假”元素,导致逻辑错误!
4. 技术细节
-
引号的重要性:
使用"${fruits[@]}"
时,双引号包裹是必须的,确保元素中的空格和特殊字符被保留。 -
下标访问:
fruits[0]
表示第一个元素(Bash 数组默认从 0 开始)。fruits[-1]
表示最后一个元素。
-
数组长度:
${#fruits[@]}
返回数组元素个数。
5. 其他相关用法
-
遍历索引:
for i in "${!fruits[@]}"; doecho "索引 $i: ${fruits[i]}" done
-
数组拼接:
new_fruits=("${fruits[@]}" "kiwi" "mango")
-
函数参数传递:
print_args() {for arg in "$@"; do # "$@" 和 "${array[@]}" 行为一致echo "$arg"done } print_args "${fruits[@]}"
2. 关联数组
declare -A user
user["name"]="Alice"
user["age"]=30
echo "${user["name"]}" # Alice
3. 子 Shell 和命令替换
# 子 Shell 中执行命令
(cd /tmp && ls) # 不影响当前目录# 命令替换
files=$(ls)
九、实战示例
1. 备份日志文件
#!/bin/bash
backup_dir="/backup/logs"
log_dir="/var/log"
timestamp=$(date +%Y%m%d)mkdir -p "$backup_dir"
tar -czf "$backup_dir/logs_$timestamp.tar.gz" "$log_dir"
echo "备份完成: $backup_dir/logs_$timestamp.tar.gz"
2. 监控 CPU 使用率
#!/bin/bash
threshold=80
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}')if (( $(echo "$cpu_usage > $threshold" | bc -l) )); thenecho "警告:CPU 使用率 ${cpu_usage}% 超过阈值 ${threshold}%!" | mail -s "CPU 警报" admin@example.com
fi
3. 批量重命名文件
#!/bin/bash
prefix="photo"
counter=1for file in *.jpg; donew_name="${prefix}_$(printf "%03d" $counter).jpg"mv "$file" "$new_name"((counter++))
done
4.自启动脚本
思路分析:
首先,配置文件列出需要启动的程序及其路径和参数。这样只需编辑配置文件,而不必修改脚本本身,提高灵活性和可维护性。
然后,脚本需要读取配置文件中的每个条目,并依次启动这些程序。需要考虑每个程序是否已经在运行,避免重复启动。这可以通过检查进程ID(PID)文件或者使用pgrep命令来实现。
另外,需要处理程序的启动顺序和依赖关系。如果某些程序需要先于其他程序启动,或者需要等待某个条件满足,脚本需要能够处理这些情况。
还需要考虑日志记录,记录每个程序的启动状态,方便后续排查问题。可以输出到系统日志或者自定义的日志文件中。
安全性也是一个方面。需要确保脚本和配置文件有适当的权限,防止未经授权的修改。特别是当脚本以root权限运行时,需要小心处理。
实现步骤:
-
创建一个配置文件,例如programs.conf,每行定义一个程序,包含名称、路径、参数等。
-
脚本读取该配置文件,逐行处理。
-
对于每个程序,检查是否已经在运行,如果未运行,则启动它。
-
记录启动结果到日志文件。
-
提供命令行参数,例如start、stop、restart等,以控制程序的行为。
#!/bin/bash# 配置文件路径
CONFIG_FILE="$(dirname "$0")/programs.conf"
# 日志文件路径
LOG_FILE="/var/log/auto_start.log"
# PID目录存放进程ID文件
PID_DIR="/var/run/auto_start"# 创建PID目录
mkdir -p "$PID_DIR"# 日志记录函数
log() {echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}# 读取配置文件并启动程序
start_programs() {while read -r line; do# 忽略注释和空行[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue# 解析配置行: 名称, 命令, 参数name=$(echo "$line" | cut -d'|' -f1 | xargs)command=$(echo "$line" | cut -d'|' -f2 | xargs)args=$(echo "$line" | cut -d'|' -f3 | xargs)pid_file="${PID_DIR}/${name}.pid"# 检查进程是否已在运行if [ -f "$pid_file" ]; thenpid=$(cat "$pid_file")if ps -p "$pid" > /dev/null 2>&1; thenlog "程序 $name 已在运行 (PID: $pid)"continuefifi# 启动程序并记录PIDeval "$command $args" >> "${PID_DIR}/${name}.log" 2>&1 &pid=$!echo $pid > "$pid_file"log "启动 $name 成功 (PID: $pid)"done < "$CONFIG_FILE"
}# 停止所有程序
stop_programs() {for pid_file in "$PID_DIR"/*.pid; do[ -f "$pid_file" ] || continuename=$(basename "$pid_file" .pid)pid=$(cat "$pid_file")if kill -0 "$pid" > /dev/null 2>&1; thenkill "$pid"log "已停止 $name (PID: $pid)"elselog "程序 $name 未运行"firm -f "$pid_file"done
}# 查看程序状态
status_programs() {for pid_file in "$PID_DIR"/*.pid; do[ -f "$pid_file" ] || continuename=$(basename "$pid_file" .pid)pid=$(cat "$pid_file")if ps -p "$pid" > /dev/null 2>&1; thenecho "$name 正在运行 (PID: $pid)"elseecho "$name 未运行"fidone
}# 主程序逻辑
case "$1" instart)start_programs;;stop)stop_programs;;restart)stop_programssleep 2start_programs;;status)status_programs;;*)echo "用法: $0 {start|stop|restart|status}"exit 1
esac
📁 配套配置文件示例 (programs.conf
)
# 格式: 名称 | 执行命令 | 参数
web_server | /usr/bin/python3 | -m http.server 8080
logger | /usr/bin/logger | --tag auto_start
monitor | /usr/bin/nmon | -f -s 5
📜 脚本功能说明
-
配置文件管理:
- 使用
programs.conf
文件定义需要自启动的程序 - 支持注释(以
#
开头的行) - 格式:
程序名称 | 执行命令 | 参数
- 使用
-
进程管理:
- 自动生成PID文件(存放于
/var/run/auto_start
) - 启动前检查进程是否已存在
- 支持停止所有程序
- 自动生成PID文件(存放于
-
日志记录:
- 操作日志记录到
/var/log/auto_start.log
- 每个程序单独记录输出到
/var/run/auto_start/<程序名>.log
- 操作日志记录到
-
操作命令:
# 启动所有程序 sudo ./auto_start.sh start# 停止所有程序 sudo ./auto_start.sh stop# 重启所有程序 sudo ./auto_start.sh restart# 查看运行状态 sudo ./auto_start.sh status
🔧 部署说明
- 将脚本保存为
/usr/local/bin/auto_start.sh
- 创建配置文件
/etc/auto_start.conf
- 设置可执行权限:
chmod +x /usr/local/bin/auto_start.sh
- 配置systemd服务(实现开机自启):
# /etc/systemd/system/auto-start.service [Unit] Description=Auto Start Programs After=network.target[Service] Type=oneshot ExecStart=/usr/local/bin/auto_start.sh start RemainAfterExit=yes[Install] WantedBy=multi-user.target
- 启用服务:
systemctl enable auto-start.service
⚠️ 注意事项
- 需要使用root权限运行(建议通过systemd管理)
- 程序参数包含特殊字符时需正确转义
- PID文件可能需要在系统重启后清理
- 建议对敏感命令进行权限控制
十、Shell 脚本最佳实践
-
代码规范
- 使用
shellcheck
检查语法。 - 变量名使用小写,常量用大写。
- 添加清晰的注释。
- 使用
-
安全性
- 避免
eval
和未过滤的用户输入。 - 使用
set -euo pipefail
增强错误处理。
- 避免
-
性能优化
- 减少子 Shell 使用。
- 避免在循环中调用外部命令。
/etc/shells文件解析
/etc/shells
是 Linux 和类 Unix 系统中一个重要的配置文件,它列出了系统认可的合法 Shell 路径。以下是关于该文件的详细讲解:
1. 文件作用
- 定义合法 Shell:
/etc/shells
记录了系统允许用户使用的 Shell 程序路径。用户登录或切换 Shell 时,系统会检查其 Shell 是否在此列表中。 - 安全限制:
某些服务(如 FTP、SSH)会验证用户的 Shell 是否在/etc/shells
中。如果不在,可能拒绝登录(例如 FTP 用户若使用未列出的 Shell,会报错This account is not available
)。
2. 文件格式
- 每行一个路径:
文件中的每一行都是一个 Shell 的绝对路径,例如:/bin/sh /bin/bash /usr/bin/zsh /usr/bin/fish
3. 查看文件内容
使用以下命令查看当前系统认可的 Shell:
cat /etc/shells
输出示例:
/bin/sh
/bin/bash
/usr/bin/bash
/bin/dash
/usr/bin/zsh
4. 与用户账户的关系
-
用户默认 Shell:
用户的默认 Shell 定义在/etc/passwd
文件的最后一个字段。例如:alice:x:1001:1001:Alice:/home/alice:/bin/bash
这里用户
alice
的 Shell 是/bin/bash
。 -
切换 Shell:
使用chsh
命令切换用户 Shell 时,系统会检查目标 Shell 是否在/etc/shells
中:chsh -s /usr/bin/zsh # 需确保 /usr/bin/zsh 已添加到 /etc/shells
修改完后要注销后才能生效
5. 如何添加新的 Shell
如果安装了新的 Shell(如 fish
或 zsh
),需手动将其路径添加到 /etc/shells
:
-
编辑文件(需 root 权限):
sudo nano /etc/shells
-
添加路径:
# 添加新安装的 Shell 路径 /usr/bin/fish
-
验证:
cat /etc/shells | grep fish
6. 常见问题与解决
-
问题 1:用户无法登录
原因:用户的 Shell 不在/etc/shells
中。
解决:- 通过恢复模式或单用户模式进入系统。
- 将缺失的 Shell 路径添加到
/etc/shells
。
-
问题 2:
chsh
报错invalid shell
原因:目标 Shell 未在/etc/shells
中注册。
解决:
按上述步骤添加 Shell 路径。
7. 实际应用场景
- 限制 FTP 用户:
若 FTP 服务(如vsftpd
)配置为仅允许使用/usr/sbin/nologin
,需确保该路径存在于/etc/shells
。 - 容器环境:
在 Docker 容器中,若用户 Shell 被设置为/bin/false
,需确认该路径已添加到/etc/shells
。
总结
/etc/shells
是系统合法 Shell 的白名单,直接影响用户登录和 Shell 切换。- 维护此文件时需谨慎,避免误删默认 Shell 路径导致登录问题。
- 添加新 Shell 后,用户需通过
chsh
切换才能生效。
此生谁料,心在天山,身老沧洲。 —陆游