Shell 脚本语法总结
1. 条件测试 [ ] (类似 Python 的 if)
Shell 中使用 [ ] 包裹条件表达式,注意方括号内部必须有空格:
bash
# Python: if x > 0:
# Shell:
if [ "$1" == "stop" ]; then
常见测试运算符
| 语法 | 含义 | Python 等价 |
|---|---|---|
-f file |
文件存在且是普通文件 | Path(file).is_file() |
-d file |
文件存在且是目录 | Path(file).is_dir() |
-z string |
字符串为空 | len(string) == 0 |
-n string |
字符串非空 | string (truthy) |
string1 == string2 |
字符串相等 | string1 == string2 |
-eq, -ne, -lt, -le, -gt, -ge |
数字比较 | ==, !=, <, <=, >, >= |
-a |
逻辑与 (and) | and |
-o |
逻辑或 (or) | or |
! |
逻辑非 (not) | not |
示例(来自脚本):
bash
# 如果目录不存在
if [ ! -d "$TASK_DIR" ]; then
echo "Error: Task directory not found"
exit 1
fi
# 如果文件存在
if [ -f "$EVAL_FILE" ]; then
COMMAND_ARRAY+=("--eval-file" "$EVAL_FILE")
fi
2. 变量操作
定义和访问
bash
# Python: name = "Alice"
# Shell:
NAME="Alice"
echo "$NAME" # 输出变量值(加 $ 符号)
echo "${NAME}" # 同上,大括号可选但更明确
特殊变量
bash
$0 # 脚本名称
$1 # 第一个参数
$2 # 第二个参数
$@ # 所有参数(列表)
${@:2} # 从第2个开始的所有参数(类似 Python slicing)
$? # 上一个命令的退出码
$$ # 当前进程 PID
3. 数组(类似 Python list)
bash
# 定义数组
ARRAY=("apple" "banana" "cherry")
# 访问元素
echo "${ARRAY[0]}" # "apple"
# 获取长度
echo "${#ARRAY[@]}" # 3
# 追加元素
ARRAY+=("date")
# 遍历
for item in "${ARRAY[@]}"; do
echo "$item"
done
示例(来自脚本):
bash
# 将元素添加到数组
PYTHON_ARGS=()
PYTHON_ARGS+=("$arg")
4. 字符串操作
bash
# 字符串拼接
STR="hello"
STR="$STR world"
# 子串提取
PATH="/home/yjp/projects/LoongFlow"
echo "${PATH:0:5}" # "/home" (从位置0取5个字符)
echo "${PATH##*/}" # "LoongFlow" (去掉最长匹配的前缀)
echo "${PATH%/*}" # "/home/yjp/projects" (去掉最短匹配的后缀)
5. 函数定义
bash
# 定义函数
my_function() {
local arg1="$1" # local 声明局部变量
local result="something"
echo "$result" # 返回值(通过 echo)
}
# 调用函数
result=$(my_function "input") # 获取返回值
示例(来自脚本):
bash
get_descendants() {
local parent="$1"
# ...
echo "$c"
get_descendants "$c" # 递归调用
}
6. 算术运算
bash
# 方法1: 使用 (( ))
a=5
b=3
((c = a + b)) # c = 8
# 方法2: 使用 let
let "c = a * b" # c = 15
# 方法3: 使用 $(( ))
c=$((a - b)) # c = 2
7. 命令替换
bash
# 获取命令输出
TODAY=$(date +%Y-%m-%d) # 推荐写法
TODAY=`date +%Y-%m-%d` # 旧式写法,不推荐
8. 流程控制
if-elif-else
bash
if [ condition1 ]; then
# do something
elif [ condition2 ]; then
# do something
else
# do something
fi # 注意:if 要以 fi 结尾!
for 循环
bash
for item in list; do
echo "$item"
done
while 循环
bash
while [ condition ]; do
# do something
done
9. 常见陷阱
| 常见错误 | 说明 |
|---|---|
[ $var == "test" ] |
变量未加引号,空格会报错 |
[ $var ] |
变量为空时可能出问题,用 [ -n "$var" ] |
if ($var) |
错误!Shell 不是这样写 |
a = 1 |
错误!等号两边不能有空格 |
10. 脚本中的实用模式
检查参数数量
bash
if [ -z "$1" ]; then
echo "Usage: $0 <arg>"
exit 1
fi
检查文件/目录
bash
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: Config file not found"
exit 1
fi
后台运行
bash
nohup command > output.log 2>&1 &
# 2>&1 表示将错误重定向到标准输出
这就是这个脚本中涉及的主要 Shell 语法!核心就是:变量用 $、条件用 [ ]、数组用 @、结尾用 fi。掌握这些就能读懂大部分 Bash 脚本了。
详细解释 nohup command > output.log 2>&1 &
这个命令是 Linux 中典型的后台运行+日志重定向组合,逐个拆解:
整体结构
nohup command > output.log 2>&1 &
└─────┘ └──────┘ └───────┘ └─┘└─┘
│ │ │ │ │ │
│ │ │ │ │ └── 后台运行符
│ │ │ │ └──── 标准错误重定向到标准输出
│ │ │ └────── 输出重定向(覆盖模式)
│ │ └────────────── 忽略 SIGHUP 信号
│ └──────────────────────── 要运行的命令
└─────────────────────────────── 防止终端关闭时进程被终止
逐个解析
1. nohup --- 防止终端关闭时进程被杀死
bash
# 不使用 nohup:关闭终端时进程会被终止
python script.py
# 使用 nohup:终端关闭后进程继续运行
nohup python script.py
原理 :nohup 会忽略 SIGHUP 信号(当终端退出时发送给前台进程组的信号)
2. command --- 你要运行的命令
bash
# 可以是任意命令
python script.py
./run.sh
npm start
3. > output.log --- 标准输出重定向
| 符号 | 含义 |
|---|---|
> |
覆盖写入(先清空文件再写入) |
>> |
追加写入(追加到文件末尾) |
bash
# command 的 stdout(标准输出)写入到 output.log
command > output.log
等价于 Python:
python
with open('output.log', 'w') as f:
subprocess.run(command, stdout=f)
4. 2>&1 --- 标准错误重定向到标准输出
这是最让人困惑的部分!需要理解文件描述符:
| 文件描述符 | 名称 | 默认目标 |
|---|---|---|
0 |
标准输入 (stdin) | 键盘 |
1 |
标准输出 (stdout) | 终端 |
2 |
标准错误 (stderr) | 终端 |
bash
# 2 > &1 的意思是:将 fd=2(stderr)重定向到 fd=1(stdout)指向的位置
2>&1
为什么顺序很重要?
bash
# ✅ 正确:先写 > output.log,再写 2>&1
command > output.log 2>&1
# ❌ 错误:顺序颠倒
command 2>&1 > output.log # stderr 仍会输出到终端!
原因 :2>&1 执行时,&1 指向的是当时的 stdout 状态。如果先执行 2>&1,那时 stdout 还是终端,所以 stderr 还是会输出到终端。
5. & --- 后台运行
bash
# 不带 &:前台运行,命令结束前无法操作终端
command
# 带 &:后台运行,可以继续输入其他命令
command &
完整示例对比
bash
# 1. 最简形式(输出到终端,可能被终端关闭杀死)
python script.py
# 2. 后台运行,但输出还在终端
python script.py &
# 3. 后台运行 + 日志文件(进程会被终端杀死)
python script.py > output.log &
# 4. 完整形式:后台运行 + 日志 + 忽略终端关闭
nohup python script.py > output.log 2>&1 &
在脚本中的实际应用
脚本中的这行代码:
bash
nohup "${COMMAND_ARRAY[@]}" > "${TASK_DIR}/run.log" 2>&1 &
PID=$!
echo "$PID" > "$PID_FILE"
等价于 Python 的:
bash
# Python 等价(简化版)
import subprocess
import os
process = subprocess.Popen(
command_array,
stdout=open(f"{task_dir}/run.log", "w"),
stderr=subprocess.STDOUT, # stderr 重定向到 stdout
start_new_session=True # 相当于 nohup + &
)
with open(pid_file, "w") as f:
f.write(str(process.pid))
常见变体
bash
# 丢弃所有输出(类似 /dev/null)
command > /dev/null 2>&1 &
# 同时记录日志和实时输出(使用 tee)
command 2>&1 | tee output.log &
# 只记录错误日志
command 2> error.log &