什么是 Shell 脚本?
Shell 脚本是一个包含一系列 Shell 命令的文本文件,用于自动化执行任务(如文件操作、程序调用、系统管理等)。
Shell 类型
bash
(Bourne-Again Shell):Linux 系统默认 Shell。sh
(Bourne Shell):更早期的标准 Shell。zsh
、ksh
等:其他变体,语法略有差异。bash
,本教程以 bash
为例。# 创建文件并编辑
vim hello.sh
#!/bin/bash # Shebang 行:指定脚本解释器为 bash
echo "Hello World!" # 输出文本
chmod +x hello.sh # 添加可执行权限
./hello.sh # 直接运行(需在脚本所在目录)
# 或
bash hello.sh # 显式指定解释器
输出:
Hello World!
以下是 Shell 脚本语法和使用的超详细指南,结合实用示例,涵盖从基础到进阶的核心内容:
Shebang 行(又称 hashbang)是脚本文件的第一行,用于指定执行该脚本的解释器。当你在终端中直接运行脚本时,系统会根据 Shebang 行选择正确的解释器
#!/bin/bash # 使用 bash 解释器
#!/bin/sh # 使用 sh 解释器
#
开头。# 这是一个注释
: '
这是
多行注释
'
name="Alice" # 字符串
count=10 # 整数
files=$(ls) # 命令执行结果赋值
today=$(date +%F) # 日期格式化为字符串
echo $name # 直接引用
echo "${name}" # 推荐用 {} 包裹变量名
export
导出,子进程可继承。export PATH="/usr/local/bin:$PATH"
变量 | 含义 |
---|---|
$HOME |
当前用户主目录的路径 |
$PATH |
可执行文件路径的列表 |
$0 |
脚本名称 |
$1 -$9 |
第 1 到第 9 个参数 |
$# |
参数个数 |
$@ |
所有参数(列表形式) |
$* |
所有参数(字符串形式) |
$? |
上一条命令的退出状态码,0通常表示没有错误,非0值表示有错误 |
$$ |
当前脚本的进程 ID |
$! |
最后一个后台命令的进程 ID |
echo $PATH
if [ 条件 ]; then
# 命令
elif [ 条件 ]; then
# 命令
else
# 命令
fi
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" ] # 文件可执行
[ 条件1 ] && [ 条件2 ] # AND
[ 条件1 ] || [ 条件2 ] # OR
! [ 条件 ] # NOT
#!/bin/bash
file="data.txt"
if [ -f "$file" ]; then
echo "$file 存在"
else
echo "$file 不存在"
fi
for
循环for i in 1 2 3; do
echo "数字: $i"
done
for file in $(ls *.txt); do
echo "处理文件: $file"
done
while
循环count=1
while [ $count -le 5 ]; do
echo "计数: $count"
((count++))
done
until
循环count=1
until [ $count -gt 5 ]; do
echo "计数: $count"
((count++))
done
break
:退出循环。continue
:跳过当前迭代。# 定义函数
greet() {
echo "Hello, $1!"
}
# 调用函数
greet "Alice" # 输出:Hello, Alice!
通过 return
返回状态码(0-255):
is_even() {
if [ $(($1 % 2)) -eq 0 ]; then
return 0 # 偶数,成功
else
return 1 # 奇数,失败
fi
}
is_even 4
echo $? # 输出 0
通过 echo
返回数据:
add() {
echo $(($1 + $2))
}
result=$(add 3 5)
echo $result # 输出 8
外层 $(( )):表示这是一个算术运算表达式,Shell 会计算括号内的内容并返回结果。
内部的 1 :表示函数的第一个参数(位置参数), 1:表示函数的第一个参数(位置参数), 1:表示函数的第一个参数(位置参数), 符号用于引用参数的值。
#!/bin/bash
echo "脚本名: $0"
echo "第一个参数: $1"
echo "所有参数: $@"
getopts
)#!/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)) # 移除已解析的选项,保留剩余参数
运行示例:
./script.sh -u alice -p 1234
getopts
解析命令行参数 -u
和 -p
,分别获取用户名和密码。user
和 pass
中。getopts
用法详解getopts
是 Bash 中解析命令行选项的标准工具,适合处理短选项(如 -u
、-p
)。
while getopts "选项字符串" opt; do
case $opt in
# 处理逻辑
esac
done
u
对应 -u
)。:
表示该选项需要参数(如 u:
表示 -u value
)。:
开头(如 ":u:p:"
),则静默处理错误(需自行捕获)。$OPTARG
:当前选项的参数值(仅当选项需要参数时有效)。$OPTIND
:下一个待处理参数的索引,通常用于 shift
跳过已解析的参数。opt
会被赋值为 ?
。:
开头,opt
会被赋值为 :
,否则为 ?
。-u
或 -p
,变量 user
或 pass
可能为空,但脚本不会报错。shift $((OPTIND - 1))
,避免后续处理位置参数时包含已解析的选项。#!/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"
$ ./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))
清理已解析的参数。if [ ! -f "file.txt" ]; then
echo "错误:文件不存在" >&2 # 输出到标准错误
exit 1
fi
trap "echo '脚本被中断!'; exit" SIGINT
set -x # 打印执行的命令
set -e # 遇到错误立即退出
set -o pipefail # 管道命令失败时退出
# 定义数组
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
定义如下:
fruits=("apple" "banana" "orange with spaces" "grape")
${fruits[@]}
:
展开数组的所有元素,每个元素保持独立。
结果:"apple" "banana" "orange with spaces" "grape"
。
对比 ${fruits[*]}
:
展开数组的所有元素,合并成一个字符串(默认用空格分隔)。
结果:"apple banana orange with spaces grape"
。
语法 | 行为 | 适用场景 |
---|---|---|
"${fruits[@]}" |
每个元素保持独立,即使包含空格也会正确分割 | 遍历数组元素,保留原始数据 |
"${fruits[*]}" |
所有元素合并成一个字符串,用 IFS 的第一个字符(默认空格)分隔 |
需要整体输出数组内容时 |
${fruits[@]} (无引号) |
元素可能被二次分词(若元素含空格或通配符,会被拆分成多个部分) | 不推荐,可能导致意外行为 |
${fruits[*]} (无引号) |
同上,合并后的字符串可能被二次分词 | 不推荐 |
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
输出:
Fruit: apple
Fruit: banana
Fruit: orange with spaces
Fruit: grape
"orange with spaces"
),也会被当作一个整体处理。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 个“虚假”元素,导致逻辑错误!引号的重要性:
使用 "${fruits[@]}"
时,双引号包裹是必须的,确保元素中的空格和特殊字符被保留。
下标访问:
fruits[0]
表示第一个元素(Bash 数组默认从 0 开始)。fruits[-1]
表示最后一个元素。数组长度:
${#fruits[@]}
返回数组元素个数。
遍历索引:
for i in "${!fruits[@]}"; do
echo "索引 $i: ${fruits[i]}"
done
数组拼接:
new_fruits=("${fruits[@]}" "kiwi" "mango")
函数参数传递:
print_args() {
for arg in "$@"; do # "$@" 和 "${array[@]}" 行为一致
echo "$arg"
done
}
print_args "${fruits[@]}"
declare -A user
user["name"]="Alice"
user["age"]=30
echo "${user["name"]}" # Alice
# 子 Shell 中执行命令
(cd /tmp && ls) # 不影响当前目录
# 命令替换
files=$(ls)
#!/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"
#!/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 警报" [email protected]
fi
#!/bin/bash
prefix="photo"
counter=1
for file in *.jpg; do
new_name="${prefix}_$(printf "%03d" $counter).jpg"
mv "$file" "$new_name"
((counter++))
done
思路分析:
首先,配置文件列出需要启动的程序及其路径和参数。这样只需编辑配置文件,而不必修改脚本本身,提高灵活性和可维护性。
然后,脚本需要读取配置文件中的每个条目,并依次启动这些程序。需要考虑每个程序是否已经在运行,避免重复启动。这可以通过检查进程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" ]; 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
)# 格式: 名称 | 执行命令 | 参数
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
文件定义需要自启动的程序#
开头的行)程序名称 | 执行命令 | 参数
进程管理:
/var/run/auto_start
)日志记录:
/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
# /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
代码规范
shellcheck
检查语法。安全性
eval
和未过滤的用户输入。set -euo pipefail
增强错误处理。性能优化
/etc/shells
是 Linux 和类 Unix 系统中一个重要的配置文件,它列出了系统认可的合法 Shell 路径。以下是关于该文件的详细讲解:
/etc/shells
记录了系统允许用户使用的 Shell 程序路径。用户登录或切换 Shell 时,系统会检查其 Shell 是否在此列表中。/etc/shells
中。如果不在,可能拒绝登录(例如 FTP 用户若使用未列出的 Shell,会报错 This account is not available
)。/bin/sh
/bin/bash
/usr/bin/zsh
/usr/bin/fish
使用以下命令查看当前系统认可的 Shell:
cat /etc/shells
输出示例:
/bin/sh
/bin/bash
/usr/bin/bash
/bin/dash
/usr/bin/zsh
用户默认 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
修改完后要注销后才能生效
如果安装了新的 Shell(如 fish
或 zsh
),需手动将其路径添加到 /etc/shells
:
编辑文件(需 root 权限):
sudo nano /etc/shells
添加路径:
# 添加新安装的 Shell 路径
/usr/bin/fish
验证:
cat /etc/shells | grep fish
问题 1:用户无法登录
原因:用户的 Shell 不在 /etc/shells
中。
解决:
/etc/shells
。问题 2:chsh
报错 invalid shell
原因:目标 Shell 未在 /etc/shells
中注册。
解决:
按上述步骤添加 Shell 路径。
vsftpd
)配置为仅允许使用 /usr/sbin/nologin
,需确保该路径存在于 /etc/shells
。/bin/false
,需确认该路径已添加到 /etc/shells
。/etc/shells
是系统合法 Shell 的白名单,直接影响用户登录和 Shell 切换。chsh
切换才能生效。此生谁料,心在天山,身老沧洲。 —陆游