Shell内容讲解
一、Shell 脚本基础概念
-
什么是 Shell 脚本?
Shell 脚本是一个包含一系列 Shell 命令的文本文件,用于自动化执行任务(如文件操作、程序调用、系统管理等)。
-
Shell 类型
bash
(Bourne-Again Shell):Linux 系统默认 Shell。sh
(Bourne Shell):更早期的标准 Shell。zsh
、ksh
等:其他变体,语法略有差异。
推荐使用bash
,本教程以bash
为例。
二、编写第一个 Shell 脚本
1. 创建脚本文件
bash
# 创建文件并编辑
vim hello.sh
2. 编写脚本内容
bash
#!/bin/bash # Shebang 行:指定脚本解释器为 bash
echo "Hello World!" # 输出文本
3. 赋予执行权限
bash
chmod +x hello.sh # 添加可执行权限
4. 运行脚本
bash
./hello.sh # 直接运行(需在脚本所在目录)
# 或
bash hello.sh # 显式指定解释器
输出:
Hello World!
三、Shell 脚本核心语法
以下是 Shell 脚本语法和使用的超详细指南,结合实用示例,涵盖从基础到进阶的核心内容:
一、Shell 脚本基础结构
1. Shebang 行
Shebang 行(又称 hashbang)是脚本文件的第一行,用于指定执行该脚本的解释器。当你在终端中直接运行脚本时,系统会根据 Shebang 行选择正确的解释器
-
作用:指定脚本使用的解释器。
-
语法 :
bash#!/bin/bash # 使用 bash 解释器 #!/bin/sh # 使用 sh 解释器
2. 注释
-
单行注释:以
#
开头。bash# 这是一个注释
-
多行注释(通过字符串技巧):
bash: ' 这是 多行注释 '
二、变量与数据类型
1. 变量定义与使用
-
定义变量 (无数据类型,默认为字符串):
bashname="Alice" # 字符串 count=10 # 整数 files=$(ls) # 命令执行结果赋值 today=$(date +%F) # 日期格式化为字符串
-
使用变量 :
bashecho $name # 直接引用 echo "${name}" # 推荐用 {} 包裹变量名
2. 变量作用域
-
局部变量:默认仅在当前脚本或函数内有效。
-
环境变量 :通过
export
导出,子进程可继承。bashexport PATH="/usr/local/bin:$PATH"
3. 特殊变量
变量 | 含义 |
---|---|
$HOME |
当前用户主目录的路径 |
$PATH |
可执行文件路径的列表 |
$0 |
脚本名称 |
$1 -$9 |
第 1 到第 9 个参数 |
$# |
参数个数 |
$@ |
所有参数(列表形式) |
$* |
所有参数(字符串形式) |
$? |
上一条命令的退出状态码,0通常表示没有错误,非0值表示有错误 |
$$ |
当前脚本的进程 ID |
$! |
最后一个后台命令的进程 ID |
bash
echo $PATH
执行结果
三、条件判断
1. 基本语法
bash
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):不等于
bash
[ $a -eq $b ] # a == b
[ $a -ne $b ] # a != b
[ $a -gt $b ] # a > b
[ $a -lt $b ] # a < b
-
字符串比较:
bash[ "$str1" == "$str2" ] # 字符串相等 [ "$str1" != "$str2" ] # 字符串不等 [ -z "$str" ] # 字符串为空 [ -n "$str" ] # 字符串非空
-
文件/目录测试:
bash[ -f "file.txt" ] # 文件存在且为普通文件 [ -d "dir" ] # 目录存在 [ -e "path" ] # 文件/目录存在 [ -r "file" ] # 文件可读 [ -w "file" ] # 文件可写 [ -x "file" ] # 文件可执行
3. 逻辑运算符
bash
[ 条件1 ] && [ 条件2 ] # AND
[ 条件1 ] || [ 条件2 ] # OR
! [ 条件 ] # NOT
4. 示例:检查文件是否存在
bash
#!/bin/bash
file="data.txt"
if [ -f "$file" ]; then
echo "$file 存在"
else
echo "$file 不存在"
fi
四、循环结构
1. for
循环
-
遍历列表 :
bashfor i in 1 2 3; do echo "数字: $i" done
-
遍历命令输出 :
bashfor file in $(ls *.txt); do echo "处理文件: $file" done
2. while
循环
bash
count=1
while [ $count -le 5 ]; do
echo "计数: $count"
((count++))
done
3. until
循环
bash
count=1
until [ $count -gt 5 ]; do
echo "计数: $count"
((count++))
done
4. 循环控制
break
:退出循环。continue
:跳过当前迭代。
五、函数
1. 定义与调用
bash
# 定义函数
greet() {
echo "Hello, $1!"
}
# 调用函数
greet "Alice" # 输出:Hello, Alice!
2. 返回值
-
通过
return
返回状态码(0-255):bashis_even() { if [ $(($1 % 2)) -eq 0 ]; then return 0 # 偶数,成功 else return 1 # 奇数,失败 fi } is_even 4 echo $? # 输出 0
-
通过
echo
返回数据:bashadd() { echo $(($1 + $2)) } result=$(add 3 5) echo $result # 输出 8
-
外层 $(( )):表示这是一个算术运算表达式,Shell 会计算括号内的内容并返回结果。
-
内部的 1 :表示函数的第一个参数(位置参数), 1:表示函数的第一个参数(位置参数), 1:表示函数的第一个参数(位置参数), 符号用于引用参数的值。
-
六、参数处理
1. 位置参数
bash
#!/bin/bash
echo "脚本名: $0"
echo "第一个参数: $1"
echo "所有参数: $@"
2. 参数解析(getopts
)
bash
#!/bin/bash
while getopts ":u:p:" opt; do # 静默模式(以 : 开头)
case $opt in
u) user="$OPTARG" ;;
p) pass="$OPTARG" ;;
:) echo "错误:选项 -$OPTARG 需要参数" >&2; exit 1 ;; # 缺少参数
\?) echo "错误:无效选项 -$OPTARG" >&2; exit 1 ;; # 未知选项
esac
done
shift $((OPTIND - 1)) # 移除已解析的选项,保留剩余参数
运行示例:
bash
./script.sh -u alice -p 1234
代码功能
- 通过
getopts
解析命令行参数-u
和-p
,分别获取用户名和密码。 - 将参数值保存到变量
user
和pass
中。 - 输出用户和密码信息。
getopts
用法详解
getopts
是 Bash 中解析命令行选项的标准工具,适合处理短选项(如 -u
、-p
)。
1. 基本语法
bash
while getopts "选项字符串" opt; do
case $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))
,避免后续处理位置参数时包含已解析的选项。
改进后的代码
bash
#!/bin/bash
# 添加错误处理和参数校验
while getopts ":u:p:" opt; do
case $opt in
u) user="$OPTARG" ;;
p) pass="$OPTARG" ;;
:) echo "错误:选项 -$OPTARG 需要参数" >&2; exit 1 ;;
\?) echo "错误:无效选项 -$OPTARG" >&2; exit 1 ;;
esac
done
# 校验必须参数
if [[ -z "$user" || -z "$pass" ]]; then
echo "错误:必须提供 -u 和 -p 参数" >&2
echo "用法: $0 -u <用户> -p <密码>" >&2
exit 1
fi
shift $((OPTIND - 1)) # 清理已解析的选项
echo "用户: $user, 密码: $pass"
- 使用 [[ ]] 更安全(避免变量未定义的错误)
- [[ -z "$var" ]] # 推荐
- [ -z "$var" ] # 也可用,但需注意变量未定义的情况
正确执行
bash
$ ./script.sh -u alice -p 1234
用户: alice, 密码: 1234
错误情况
-
无效选项:
bash$ ./script.sh -a 错误:无效选项 -a
-
缺少参数:
bash$ ./script.sh -u 错误:选项 -u 需要参数
-
未提供必选参数:
bash$ ./script.sh -u alice 错误:必须提供 -u 和 -p 参数 用法: ./script.sh -u <用户> -p <密码>
getopts
是解析命令行选项的标准工具,需结合case
和内置变量使用。- 通过选项字符串定义选项是否需要参数(如
u:
)。 - 错误处理需区分"无效选项"和"缺少参数",并校验必要参数。
- 使用
shift $((OPTIND - 1))
清理已解析的参数。
七、错误处理
1. 错误退出
bash
if [ ! -f "file.txt" ]; then
echo "错误:文件不存在" >&2 # 输出到标准错误
exit 1
fi
2. 捕获信号
bash
trap "echo '脚本被中断!'; exit" SIGINT
3. 调试模式
bash
set -x # 打印执行的命令
set -e # 遇到错误立即退出
set -o pipefail # 管道命令失败时退出
八、高级技巧
1. 数组操作
bash
# 定义数组
fruits=("apple" "banana" "cherry")
# 访问元素
echo ${fruits[0]} # apple
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# 数组长度
echo ${#fruits[@]} # 3
在 Bash 脚本中,${fruits[@]}
中的 @
符号用于 展开数组的所有元素,并确保每个元素被视为独立的字符串(即使元素包含空格或特殊字符)。以下是详细解释:
1.${num[@]}
:
安全展开数组所有元素,保留每个元素的独立性,是遍历数组的推荐方式。
2.@
符号 :
代表数组的全部元素,配合双引号使用时,确保数据完整性和可靠性。
1. 数组定义与 @
的作用
假设数组 fruits
定义如下:
bash
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:遍历数组元素(正确方式)
bash
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
输出:
Fruit: apple
Fruit: banana
Fruit: orange with spaces
Fruit: grape
- 即使元素包含空格(如
"orange with spaces"
),也会被当作一个整体处理。
场景 2:错误用法(无引号)
bash
for fruit in ${fruits[@]}; do
echo "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. 其他相关用法
-
遍历索引:
bashfor i in "${!fruits[@]}"; do echo "索引 $i: ${fruits[i]}" done
-
数组拼接:
bashnew_fruits=("${fruits[@]}" "kiwi" "mango")
-
函数参数传递:
bashprint_args() { for arg in "$@"; do # "$@" 和 "${array[@]}" 行为一致 echo "$arg" done } print_args "${fruits[@]}"
2. 关联数组
bash
declare -A user
user["name"]="Alice"
user["age"]=30
echo "${user["name"]}" # Alice
3. 子 Shell 和命令替换
bash
# 子 Shell 中执行命令
(cd /tmp && ls) # 不影响当前目录
# 命令替换
files=$(ls)
九、实战示例
1. 备份日志文件
bash
#!/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 使用率
bash
#!/bin/bash
threshold=80
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}')
if (( $(echo "$cpu_usage > $threshold" | bc -l) )); then
echo "警告:CPU 使用率 ${cpu_usage}% 超过阈值 ${threshold}%!" | mail -s "CPU 警报" admin@example.com
fi
3. 批量重命名文件
bash
#!/bin/bash
prefix="photo"
counter=1
for file in *.jpg; do
new_name="${prefix}_$(printf "%03d" $counter).jpg"
mv "$file" "$new_name"
((counter++))
done
4.自启动脚本
思路分析:
首先,配置文件列出需要启动的程序及其路径和参数。这样只需编辑配置文件,而不必修改脚本本身,提高灵活性和可维护性。
然后,脚本需要读取配置文件中的每个条目,并依次启动这些程序。需要考虑每个程序是否已经在运行,避免重复启动。这可以通过检查进程ID(PID)文件或者使用pgrep命令来实现。
另外,需要处理程序的启动顺序和依赖关系。如果某些程序需要先于其他程序启动,或者需要等待某个条件满足,脚本需要能够处理这些情况。
还需要考虑日志记录,记录每个程序的启动状态,方便后续排查问题。可以输出到系统日志或者自定义的日志文件中。
安全性也是一个方面。需要确保脚本和配置文件有适当的权限,防止未经授权的修改。特别是当脚本以root权限运行时,需要小心处理。
实现步骤:
-
创建一个配置文件,例如programs.conf,每行定义一个程序,包含名称、路径、参数等。
-
脚本读取该配置文件,逐行处理。
-
对于每个程序,检查是否已经在运行,如果未运行,则启动它。
-
记录启动结果到日志文件。
-
提供命令行参数,例如start、stop、restart等,以控制程序的行为。
bash
#!/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" ]; then
pid=$(cat "$pid_file")
if ps -p "$pid" > /dev/null 2>&1; then
log "程序 $name 已在运行 (PID: $pid)"
continue
fi
fi
# 启动程序并记录PID
eval "$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" ] || continue
name=$(basename "$pid_file" .pid)
pid=$(cat "$pid_file")
if kill -0 "$pid" > /dev/null 2>&1; then
kill "$pid"
log "已停止 $name (PID: $pid)"
else
log "程序 $name 未运行"
fi
rm -f "$pid_file"
done
}
# 查看程序状态
status_programs() {
for pid_file in "$PID_DIR"/*.pid; do
[ -f "$pid_file" ] || continue
name=$(basename "$pid_file" .pid)
pid=$(cat "$pid_file")
if ps -p "$pid" > /dev/null 2>&1; then
echo "$name 正在运行 (PID: $pid)"
else
echo "$name 未运行"
fi
done
}
# 主程序逻辑
case "$1" in
start)
start_programs
;;
stop)
stop_programs
;;
restart)
stop_programs
sleep 2
start_programs
;;
status)
status_programs
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
esac
📁 配套配置文件示例 (programs.conf
)
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
- 操作日志记录到
-
操作命令:
bash# 启动所有程序 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
-
设置可执行权限:
bashchmod +x /usr/local/bin/auto_start.sh
-
配置systemd服务(实现开机自启):
bash# /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
-
启用服务:
bashsystemctl 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 的绝对路径,例如:plaintext/bin/sh /bin/bash /usr/bin/zsh /usr/bin/fish
3. 查看文件内容
使用以下命令查看当前系统认可的 Shell:
bash
cat /etc/shells
输出示例:
/bin/sh
/bin/bash
/usr/bin/bash
/bin/dash
/usr/bin/zsh
4. 与用户账户的关系
-
用户默认 Shell :
用户的默认 Shell 定义在
/etc/passwd
文件的最后一个字段。例如:plaintextalice:x:1001:1001:Alice:/home/alice:/bin/bash
这里用户
alice
的 Shell 是/bin/bash
。 -
切换 Shell :
使用
chsh
命令切换用户 Shell 时,系统会检查目标 Shell 是否在/etc/shells
中:bashchsh -s /usr/bin/zsh # 需确保 /usr/bin/zsh 已添加到 /etc/shells
修改完后要注销后才能生效
5. 如何添加新的 Shell
如果安装了新的 Shell(如 fish
或 zsh
),需手动将其路径添加到 /etc/shells
:
-
编辑文件(需 root 权限):
bashsudo nano /etc/shells
-
添加路径:
plaintext# 添加新安装的 Shell 路径 /usr/bin/fish
-
验证:
bashcat /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
切换才能生效。
此生谁料,心在天山,身老沧洲。 ---陆游