简介
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 遍历日志,并将这些知识整合到一个可重复使用的日志汇总脚本中。
掌握这些基础知识后,后续的自动化脚本会更易读、更安全,也更便于重复运行,并且其他命令或定时任务也能更容易地判断其执行结果。



