Shell 脚本基础

ShellBeginner
立即练习

简介

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

使用文件测试进行分支判断

在本步骤中,你将结合 ifelifelse,通过文件测试判断路径的类型。

创建 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 遍历日志,并将这些知识整合到一个可重复使用的日志汇总脚本中。

掌握这些基础知识后,后续的自动化脚本会更易读、更安全,也更便于重复运行,并且其他命令或定时任务也能更容易地判断其执行结果。