Shell 脚本就是把一堆命令写进文件,一次执行。不需要学任何新语言——那些 ls、cd、grep 就是脚本的原材料。今天这篇就聊一下脚本编写的核心内容:变量、条件、循环、函数,最后再附几个拿来就能用的实用脚本。
脚本就是一个普通文本文件,约定俗成用 .sh 结尾(不加也可以,但加上更清晰):
nano hello.sh
写入以下内容:
#!/bin/bash
# 这是注释,#号开头
echo "Hello, World!"
echo "当前用户:$USER"
echo "当前目录:$(pwd)"
#!/bin/bash 叫做 shebang,告诉系统用哪个解释器来执行这个文件。
#!/bin/bash | |
#!/usr/bin/env bash | |
#!/bin/sh |
💡 提示: 推荐写 #!/usr/bin/env bash,在不同 Linux 发行版上兼容性最好。
chmod +x hello.sh # 加可执行权限
./hello.sh # 执行(注意要加 ./)
# 如果你习惯了 Windows,可能会想双击运行,但在 Linux 中,脚本是通过终端执行的。
或者不加权限,直接用 bash 运行:
bash hello.sh
💡 提示: 为什么要加
./?因为 Linux 出于安全考虑,不会在当前目录找命令,必须明确指出路径。
name="冯哥"
age=18
echo $name# 输出:冯哥
echo ${name}# 加花括号,更规范(避免歧义)
echo "我叫${name},今年${age}岁"
⚠️ 注意: 等号两边不能有空格,
name = "冯哥"是错的。
"双引号" | |
'单引号' | |
$() |
name="冯哥"
echo "你好 $name"# 输出:你好 冯哥
echo '你好 $name'# 输出:你好 $name(原样)
echo "今天是 $(date)"# 输出:今天是 2026年6月21日...
today=$(date +%Y-%m-%d)
free_space=$(df -h / | awk 'NR==2{print $4}')
echo "今天:$today"
echo "根分区剩余:$free_space"
bash 默认把所有变量当字符串,数值运算需要特殊写法:
a=10
b=3
# 方法一:$(( ))——推荐
echo $((a + b)) # 13
echo $((a * b)) # 30
echo $((a / b)) # 3(整数除法,丢掉小数)
echo $((a % b)) # 1(取余)
# 方法二:let
let c=a+b
echo $c# 13
# 浮点数运算(bash 不支持,用 bc)
# 如果 bc 未安装先安装
sudo apt install bc
# 如果 bc 已经安装直接
echo "scale=2; 10/3" | bc # 3.33
$0 | |
$1$2 ... | |
$@ | |
$* | |
$# | |
$? | |
$$ |
#!/usr/bin/env bash
echo "脚本名:$0"
echo "第一个参数:$1"
echo "参数总数:$#"
echo "所有参数:$@"
运行 ./test.sh hello world,输出:
脚本名:./test.sh
第一个参数:hello
参数总数:2
所有参数:hello world
str="Hello, Linux World"
# 长度
echo ${#str} # 18
# 截取:${变量:起始位置:长度}
echo ${str:7:5} # Linux
# 替换:${变量/查找/替换}(只替换第一个)
echo ${str/Linux/Shell} # Hello, Shell World
# 替换所有:${变量//查找/替换}
echo ${str// /_} # Hello,_Linux_World
# 删除前缀(最短匹配)
file="backup_2026-06-21.tar.gz"
echo${file#backup_}# 2026-06-21.tar.gz
# 删除后缀(最短匹配)
echo ${file%.tar.gz} # backup_2026-06-21
# 转大写 / 小写(bash 4.0+)
echo ${str^^} # HELLO, LINUX WORLD
echo ${str,,} # hello, linux world
💡 提示: 字符串操作是 Shell 脚本里很常用的技巧,尤其是从文件名里提取日期、去掉扩展名等场景。
if [ 条件 ]; then
命令
elif [ 另一个条件 ]; then
命令
else
命令
fi
⚠️ 注意:
[和]里面两侧必须有空格,[ $a -eq 0 ]是对的,[$a -eq 0]是错的。
[ $a -eq $b ] | |
[ $a -ne $b ] | |
[ $a -gt $b ] | |
[ $a -lt $b ] | |
[ $a -ge $b ] | |
[ $a -le $b ] |
也可以用 (( )) 做数值比较(更像 C 语言,支持 ><==):
a=10
if (( a > 5 )); then
echo"a 大于 5"
fi
[ "$a" = "$b" ] | = 不是 ==) |
[ "$a" != "$b" ] | |
[ -z "$a" ] | |
[ -n "$a" ] |
⚠️ 注意: 比较字符串时,变量一定要加引号:
[ "$name" = "root" ],不加引号如果变量为空会报语法错误。
[ -f 文件 ] | |
[ -d 目录 ] | |
[ -e 路径 ] | |
[ -r 文件 ] | |
[ -w 文件 ] | |
[ -x 文件 ] | |
[ -s 文件 ] |
if [ -f "/etc/hosts" ]; then
echo"hosts 文件存在"
fi
if [ ! -d "$HOME/backup" ]; then
mkdir"$HOME/backup"
echo"备份目录已创建"
fi
# AND(两个条件都满足)
if [ -f file.txt ] && [ -r file.txt ]; then
echo"文件存在且可读"
fi
# OR(满足一个即可)
if [ "$user" = "root" ] || [ "$user" = "admin" ]; then
echo"有管理员权限"
fi
# NOT
if [ ! -f file.txt ]; then
echo"文件不存在"
fi
read -p "请输入操作(start/stop/restart):" action
case "$action" in
start)
echo "启动服务..."
;;
stop)
echo "停止服务..."
;;
restart)
echo "重启服务..."
;;
*)
echo "未知操作:$action"
;;
esac
💡 提示:
case比一堆if-elif更清晰,适合处理多个固定选项的分支,比如处理命令行参数时很常用。
# 遍历固定列表
for fruit in apple banana cherry; do
echo "水果:$fruit"
done
# 遍历数字范围(bash 内置)
for i in {1..5}; do
echo "第 $i 次"
done
# C 语言风格(数值循环)
for (( i=0; i<5; i++ )); do
echo "i = $i"
done
# 遍历文件(通配符)如果没有匹配的文件,file 会变成字面值 *.log,可以用 if [ -f "$file" ] 过滤。
for file in /tmp/*.log; do
echo "处理:$file"
rm "$file"
done
# 遍历命令输出(命令替换)
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "用户:$user"
done
# 基本 while
count=1
while [ $count -le 5 ]; do
echo "第 $count 次"
(( count++ ))
done
# 逐行读取文件(很常用)
while IFS= read -r line; do
echo "行内容:$line"
done < /etc/hosts
# 无限循环(用 break 退出)
while true; do
read -p "输入 quit 退出:" input
if [ "$input" = "quit" ]; then
break
fi
echo "你输入了:$input"
done
for i in {1..10}; do
if (( i == 3 )); then
continue # 跳过这次,继续下一轮
fi
if (( i == 7 )); then
break # 直接退出整个循环
fi
echo $i
done
# 输出:1 2 4 5 6
# 定义
say_hello() {
echo "你好,$1!"# $1 是传给函数的第一个参数
}
# 调用
say_hello "冯哥"# 输出:你好,冯哥!
say_hello "Linux"# 输出:你好,Linux!
函数内部的 $1 $2 $@ 是函数自己的参数,不是脚本的参数。
add() {
local result=$(( $1 + $2 )) # local 让变量只在函数内有效
echo $result
}
sum=$(add 10 20)
echo "结果:$sum" # 输出:结果:30
💡 提示: 函数内的变量尽量加
local,避免污染全局变量空间,尤其是在大脚本里。
Shell 函数的"返回值"有两种:
return N | return 0 | |
echo$() 接收 |
# 用 return 返回状态码
is_file_exist() {
if [ -f "$1" ]; then
return 0 # 成功
else
return 1 # 失败
fi
}
if is_file_exist "/etc/hosts"; then
echo "文件存在"
fi
# 用 echo 返回字符串
get_timestamp() {
echo $(date +%Y%m%d_%H%M%S)
}
ts=$(get_timestamp)
echo "时间戳:$ts"
cp source.txt dest.txt
if [ $? -ne 0 ]; then
echo "复制失败!"
exit 1 # 退出脚本,返回错误码
fi
更简洁的写法(用 ||):
cp source.txt dest.txt || { echo "复制失败!"; exit 1; }
#!/usr/bin/env bash
set -euo pipefail
set -e | |
set -u | |
set -o pipefail |
⚠️ 注意:
set -e有时会在你预期内的"失败"处退出(比如grep没找到东西返回1),这时候可以用cmd || true来告诉脚本"这里失败是正常的,继续走"。
# 信息输出到 stdout
echo "正在备份..."
# 错误信息输出到 stderr(不会混进管道)
echo "错误:文件不存在" >&2
# 带颜色的输出
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'# No Color
echo -e "${GREEN}成功${NC}:备份完成"
echo -e "${RED}失败${NC}:源文件不存在"
复制脚本代码保存为文件,chmod +x 后执行。
#!/usr/bin/env bash
set -euo pipefail
# ===== 配置区(修改这里)=====
SOURCE_DIR="$HOME/Documents"
BACKUP_DIR="$HOME/backup"
KEEP_DAYS=7 # 保留最近几天的备份
# ===== 配置结束 =====
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 生成带时间戳的文件名
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/docs_${TIMESTAMP}.tar.gz"
# 执行备份
echo "开始备份 $SOURCE_DIR ..."
tar -czf "$BACKUP_FILE" -C "$(dirname $SOURCE_DIR)""$(basename $SOURCE_DIR)"
echo "备份完成:$BACKUP_FILE"
echo "备份大小:$(du -sh $BACKUP_FILE | cut -f1)"
# 删除超过 KEEP_DAYS 天的旧备份
echo "清理 ${KEEP_DAYS} 天前的旧备份..."
find "$BACKUP_DIR" -name "docs_*.tar.gz" -mtime +$KEEP_DAYS -delete
echo "清理完成"
#!/usr/bin/env bash
# 用法:./rename.sh 目录 旧字符串 新字符串
# 例:./rename.sh /tmp/photos IMG img
set -euo pipefail
TARGET_DIR="${1:?请提供目标目录}"
OLD_STR="${2:?请提供要替换的字符串}"
NEW_STR="${3:?请提供替换成的字符串}"
if [ ! -d "$TARGET_DIR" ]; then
echo "错误:目录不存在:$TARGET_DIR" >&2
exit 1
fi
count=0
for file in"$TARGET_DIR"/*"$OLD_STR"*; do
[ -f "$file" ] || continue # 如果没有匹配文件,跳过
new_name="${file/$OLD_STR/$NEW_STR}"
mv "$file""$new_name"
echo "重命名:$(basename $file) → $(basename $new_name)"
(( count++ ))
done
echo "完成,共重命名 $count 个文件"
#!/usr/bin/env bash
# 检查磁盘使用率,超过阈值就发警告
# 可以加入 cron 定时执行
THRESHOLD=80 # 告警阈值(百分比)
df -h | grep -vE '^(Filesystem|tmpfs|udev|none)' | whileread -r line; do
usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
mount=$(echo "$line" | awk '{print $6}')
if (( usage >= THRESHOLD )); then
echo "⚠️ 警告:挂载点 $mount 使用率已达 ${usage}%,超过阈值 ${THRESHOLD}%"
# 如果配了邮件,可以在这里加发邮件命令
# echo "磁盘告警:$mount 使用率 ${usage}%" | mail -s "磁盘空间告警" admin@example.com
fi
done
把它加进 crontab,每天自动检查:
crontab -e
# 加入这行(每天早上8点执行)
0 8 * * * /home/用户名/bin/check_disk.sh >> /tmp/disk_check.log 2>&1
脚本出问题了,怎么找原因?
# 方法一:bash -x 追踪执行(每行命令都打印出来)
bash -x ./myscript.sh
# 方法二:在脚本里局部开启追踪
如果遇到脚本报错但看不出哪里错,用调试模式跑一遍。
set -x # 开始追踪
...代码...
set +x # 关闭追踪
# 方法三:在出问题的地方加 echo 打印变量
echo "DEBUG: file=$file, count=$count"
# 方法四:shellcheck 静态检查(需安装)
sudo apt install shellcheck
shellcheck myscript.sh
💡 提示:
shellcheck是 Shell 脚本的"语法检查+较佳实践检查"工具,推荐,能在运行前发现大量潜在问题。shellcheck myscript.sh 会列出所有警告,按提示修改即可。
name="value" | |
$name${name} | |
$(命令) | |
$(( a + b )) | |
if [ 条件 ]; then ... fi | |
-eq -ne -gt -lt -ge -le | |
=!=-z-n | |
-f-d-e-r-w-x | |
for x in 列表; do ... done | |
while [ 条件 ]; do ... done | |
func_name() { ... } | |
local var=value | |
exit 0exit 1(失败) | |
set -euo pipefail | |
bash -x 脚本 |
如果记不住那么多,记住 if、for、while 和 $() 就够了,其他可以随时查。 脚本编写是 Linux 使用效率的倍增器。今天这篇是入门,重点在把语法搞清楚。
下篇预告:函数库管理——随着脚本越写越多,怎么把常用函数抽出来统一管理,在任意脚本里 source 一下就能用,建立自己的工具箱。