简介
Shell 脚本会将命令保存到文件中,以便稳定地重复执行任务。可靠的初学者脚本会明确指定解释器、为变量展开添加引号、在执行操作前验证输入,并返回其他命令能够理解的退出状态。
在本实验中,你将逐步编写多个简短的 Bash 脚本。你将通过 Bash 运行脚本,也会将脚本作为可执行文件运行;同时学习使用变量和位置参数、读取交互式输入、使用 if 和文件测试进行分支、选择退出代码、使用 for 进行迭代,并将这些知识组合到一个简单的日志汇总脚本中。
创建并运行第一个脚本
在本步骤中,你将创建一个 Bash 脚本,通过解释器运行它,并让它可以直接执行。
进入实验工作目录:
cd /home/labex/project/shell-lab
使用 here-document 创建 hello.sh。第一行是 shebang,用于在直接执行文件时选择 Bash:
cat > hello.sh <<'EOF'
#!/bin/bash
echo "Hello from a Bash script"
pwd
EOF
运行前先查看文件内容:
cat hello.sh
显式地将文件传递给 Bash。即使此时还没有添加可执行权限,也可以这样运行:
bash hello.sh
添加可执行权限并查看文件模式:
chmod u+x hello.sh
ls -l hello.sh
从当前目录执行该文件。./ 提供文件路径;Shell 通常不会自动搜索当前目录:
./hello.sh
使用变量和安全引号
在本步骤中,你将为变量赋值、展开变量,并了解为什么双引号可以保护包含空格的值。
创建一个包含两个变量的脚本:
cat > variables.sh <<'EOF'
#!/bin/bash
course="Linux for Noobs"
workspace="/home/labex/project/shell-lab"
echo "Course: $course"
echo "Workspace: $workspace"
printf 'Quoted argument count: %s\n' "$#"
printf 'First argument: %s\n' "${1:-not provided}"
EOF
Shell 变量赋值中的 = 两侧不能有空格。双引号允许展开 $course,同时将 Linux for Noobs 保持为一个值。
运行脚本,并传入一个包含空格的参数:
bash variables.sh "beginner learner"
特殊变量 $# 表示位置参数的数量,$1 表示第一个参数。当第一个参数不存在时,${1:-not provided} 会提供备用值。
将输出保存下来以便查看:
bash variables.sh "beginner learner" > variable-output.txt
cat variable-output.txt
验证位置参数
在本步骤中,你将要求用户提供参数,输出用法提示,并针对失败和成功返回不同的退出状态。
创建 greet-argument.sh:
cat > greet-argument.sh <<'EOF'
#!/bin/bash
if [ "$#" -ne 1 ]; then
echo "Usage: $0 NAME" >&2
exit 2
fi
name=$1
echo "Hello, $name"
exit 0
EOF
测试命令 [ ... ] 会检查参数数量是否不等于 1。>&2 会将用法文本发送到标准错误,exit 2 表示用法无效。
不提供参数运行脚本,然后立即显示 $?,也就是上一条命令的退出状态:
bash greet-argument.sh
echo "missing argument status: $?"
预期状态为 2。接着按正确方式运行:
bash greet-argument.sh "Ada Lovelace"
echo "valid argument status: $?"
保存成功的问候文本:
bash greet-argument.sh "Ada Lovelace" > argument-greeting.txt
读取交互式输入
在本步骤中,你将提示用户输入内容,安全地读取一行文本,并处理空输入。
创建 ask-name.sh:
cat > ask-name.sh <<'EOF'
#!/bin/bash
read -r -p "Enter your name: " name
if [ -z "$name" ]; then
echo "A name is required" >&2
exit 1
fi
echo "Welcome, $name"
EOF
read -r 会读取一行文本,不会对反斜杠进行特殊处理。-p 会显示提示符,-z 会测试带引号的值是否为空。
运行脚本:
bash ask-name.sh
在提示符处输入:
Grace Hopper
你应该会看到 Welcome, Grace Hopper。如需获得可重复的保存结果,可以将一行输入通过管道传给同一个脚本:
printf 'Grace Hopper\n' | bash ask-name.sh > interactive-greeting.txt
cat interactive-greeting.txt
使用文件测试进行分支
在本步骤中,你将结合 if、elif 和 else 使用文件测试,对路径进行分类。
创建 classify-path.sh:
cat > classify-path.sh <<'EOF'
#!/bin/bash
path=${1:-}
if [ -z "$path" ]; then
echo "Usage: $0 PATH" >&2
exit 2
elif [ -d "$path" ]; then
echo "directory: $path"
elif [ -f "$path" ]; then
echo "file: $path"
else
echo "missing: $path" >&2
exit 1
fi
exit 0
EOF
-d 用于测试路径是否为目录,-f 用于测试路径是否为普通文件。判断顺序很重要:脚本会先处理无效用法,然后再判断路径类型。
尝试以下三种路径结果:
bash classify-path.sh logs
bash classify-path.sh notes.txt
bash classify-path.sh missing.txt
echo "missing path status: $?"
将成功结果保存到同一个文件中。只有第一条命令成功时,&& 才会运行第二条命令:
bash classify-path.sh logs > path-results.txt && bash classify-path.sh notes.txt >> path-results.txt
cat path-results.txt
使用 For 循环处理多个文件
在本步骤中,你将遍历日志文件,并统计每个文件中匹配记录的数量。
创建 count-errors.sh:
cat > count-errors.sh <<'EOF'
#!/bin/bash
log_dir=${1:-logs}
if [ ! -d "$log_dir" ]; then
echo "Log directory not found: $log_dir" >&2
exit 1
fi
for log_file in "$log_dir"/*.log; do
error_count=$(grep -c '^ERROR ' "$log_file" || true)
printf '%s errors=%s\n' "$(basename "$log_file")" "$error_count"
done
EOF
循环每次迭代时,都会将一个匹配到的路径赋给 log_file。为 "$log_file" 添加引号可以保护其中的空格,而 basename 会移除目录前缀。grep -c 会统计匹配行的数量;|| true 可以避免在更大的严格执行流程中因没有匹配项而停止。
运行循环并保存输出:
bash count-errors.sh logs | tee error-counts.txt
你应该会看到每个 .log 文件对应一条结果。
组合验证、循环和退出状态
在本步骤中,你将构建一个可复用的脚本,用于验证目录、汇总其中的日志,并清晰地报告成功或失败。
创建 summarize-logs.sh:
cat > summarize-logs.sh <<'EOF'
#!/bin/bash
input_dir=${1:-}
output_file=${2:-}
if [ -z "$input_dir" ] || [ -z "$output_file" ]; then
echo "Usage: $0 INPUT_DIR OUTPUT_FILE" >&2
exit 2
fi
if [ ! -d "$input_dir" ]; then
echo "Input directory not found: $input_dir" >&2
exit 1
fi
: > "$output_file"
for log_file in "$input_dir"/*.log; do
lines=$(wc -l < "$log_file")
errors=$(grep -c '^ERROR ' "$log_file" || true)
printf '%s lines=%s errors=%s\n' "$(basename "$log_file")" "$lines" "$errors" >> "$output_file"
done
echo "Wrote summary to $output_file"
exit 0
EOF
命令 : > "$output_file" 会在循环开始前安全地创建或清空目标文件。为脚本传入两个必需参数并运行:
chmod u+x summarize-logs.sh
./summarize-logs.sh logs log-summary.txt
echo "summary status: $?"
cat log-summary.txt
使用不存在的目录测试失败路径:
./summarize-logs.sh missing log-summary.txt
echo "missing directory status: $?"
成功状态应为 0,而目录不存在时的状态应为 1。
总结
你已经创建并运行了 Bash 脚本,使用了 shebang 和权限,展开了带引号的变量,读取了位置参数和交互式输入,并返回了有意义的退出状态。你还使用 if/elif/else 进行分支,测试文件和目录,使用 for 遍历日志,并将这些知识组合成一个可复用的汇总脚本。
掌握这些基础知识后,后续的自动化脚本会更易读、更安全,也更容易让其他命令或定时任务判断执行结果。



