
linuxshell编程怎么用:从准备到完成的实操步骤
Shell 脚本是把一组命令写进文件,再交给 Shell 解释执行。它适合串联 Linux 命令、批量处理文件、定时执行维护任务,以及自动化部署中的简单流程。它不是通用编程语言的替代品:复杂数据结构、并发任务、跨平台应用或严格的事务处理,通常更适合 Python、Go 等工具。
本文以 Bash 为主要示例。Bash 在许多 Linux 发行版中常见,但并非所有系统都默认安装或将其设为交互式 Shell。脚本开头的解释器声明、执行权限、参数边界和错误处理方式都会影响运行结果。下面从准备环境开始,完成一段可以实际运行的脚本。
一、先确认要用哪个 Shell
在终端中输入以下命令,查看当前交互式 Shell,以及 Bash 是否可用:
printf '当前 Shell:%s\n' "$SHELL"
command -v bash
bash --version | head -n 1
$SHELL 通常表示用户的登录 Shell,不一定就是当前正在执行命令的进程。要查看当前 Shell 进程,可以用:
ps -p "$$" -o comm=
脚本选择哪种语法,应由脚本的解释器决定,而不是由当前终端决定。本文使用 Bash,并在文件首行写:
#!/usr/bin/env bash
#!/usr/bin/env bash 会通过 env 在当前 PATH 中查找 Bash,适合 Bash 安装路径因系统而异的环境。如果运行环境要求固定解释器路径,也可以写成 #!/bin/bash,但应先确认该路径确实存在。
如果需要兼容 POSIX Shell,应改用 #!/bin/sh,并避免 Bash 专有语法,例如数组、[[ ... ]]、进程替换和 <<<。/bin/sh 在不同系统上可能链接到不同实现,因此不要把 Bash 脚本仅仅改名为 .sh 就当作可移植脚本。
二、创建并运行第一个脚本
新建文件 hello.sh,写入下面的内容:
#!/usr/bin/env bash
name=${1:-访客}
printf '你好,%s!\n' "$name"
有两种常见运行方式。第一种显式调用 Bash,不要求文件有执行权限:
bash hello.sh 小林
第二种先添加执行权限,再直接运行:
chmod u+x hello.sh
./hello.sh 小林
./ 表示从当前目录运行文件。当前目录通常不在 PATH 中,因此只输入 hello.sh 可能会提示找不到命令。文件有执行权限,也不代表它可以绕过权限、挂载策略或系统安全限制;例如某些目录可能以禁止执行的方式挂载。
示例中的 ${1:-访客} 表示:如果第一个位置参数未设置或为空,就使用“访客”。它与 ${1-访客} 略有不同:后者只在参数未设置时使用默认值,参数存在但为空时仍会得到空字符串。
三、变量、引号与命令替换
Shell 变量赋值时,等号两边不能加空格。读取变量要使用美元符号,通常应把展开结果放入双引号中:
#!/usr/bin/env bash
work_dir="/tmp/my work"
message="目录是:$work_dir"
printf '%s\n' "$message"
如果写成 work_dir = "/tmp/my work",Shell 会把它当成名为 work_dir 的命令并传入其他参数,而不是执行赋值。未加引号的变量展开还可能发生单词拆分和通配符展开。比如目录名中有空格时,rm $target 可能会被拆成多个参数。安全的基本习惯是:变量展开时使用双引号,即 "$target"。
双引号允许变量展开,但会抑制大多数分词和通配符展开。单引号则保留其中的字面内容:
user='Ada'
printf '双引号会展开:%s\n' "$user"
printf '单引号不会展开:%s\n' '$user'
用 $(...) 捕获命令输出,通常比旧式反引号更易读:
today=$(date +%F)
printf '日期:%s\n' "$today"
命令替换会删除结果末尾的换行符,不适合无损保存任意二进制数据,也不适合把多行文件内容当作一个精确字符串传递。处理文件内容时,应优先使用文件、管道或支持相应数据格式的工具。
四、位置参数与参数校验
运行 bash script.sh a b 时,脚本中的 $1、$2 分别对应 a、b;$# 表示参数个数,"$@" 表示所有参数。传递参数列表时,正确写法通常是 "$@",而不是未加引号的 $@ 或 $*。加引号后的 "$@" 能保留每个参数的边界,包括含空格的参数。
下面这个脚本要求传入一个目录,并检查它是否存在:
#!/usr/bin/env bash
if [[ $# -ne 1 ]]; then
printf '用法:%s DIRECTORY\n' "$0" >&2
exit 2
fi
directory=$1
if [[ ! -d "$directory" ]]; then
printf '错误:目录不存在:%s\n' "$directory" >&2
exit 1
fi
printf '目录存在:%s\n' "$directory"
[[ ... ]] 是 Bash 的条件判断语法,不是 POSIX sh 语法。>&2 是将信息写到标准错误,适合输出错误提示;脚本以非零状态退出,方便调用方判断失败。退出码具体含义应由脚本约定,0 通常表示成功,非零通常表示失败,但不同程序的非零码不一定代表同一种错误。
多个参数可以逐一遍历。用 "$@" 可以让每个参数作为独立的循环项:
#!/usr/bin/env bash
if [[ $# -eq 0 ]]; then
printf '请至少提供一个文件名。\n' >&2
exit 2
fi
for file in "$@"; do
if [[ -f "$file" ]]; then
printf '普通文件:%s\n' "$file"
else
printf '不是普通文件或不可访问:%s\n' "$file"
fi
done
参数校验不能只检查“有没有参数”。还要根据用途验证路径、数值范围、选项组合和输入格式。例如,准备删除文件时,不应仅仅相信调用者传入了一个非空字符串;还应限制可操作目录、检查目标类型,并尽量先预览操作结果。
五、条件判断与循环的常见写法
Bash 的整数比较可以用 -eq、-lt、-gt 等测试操作符:
count=3
if (( count > 0 )); then
printf '数量大于零\n'
fi
(( ... )) 是 Bash 的算术上下文。它适合整数运算,不适合直接处理小数或任意精度数字。字符串比较可在 [[ ... ]] 中使用 ==:
mode="test"
if [[ "$mode" == "test" ]]; then
printf '当前是测试模式\n'
fi
循环可以遍历明确的参数列表。例如,批量检查传入的路径:
for path in "$@"; do
if [[ -e "$path" ]]; then
printf '存在:%s\n' "$path"
else
printf '不存在:%s\n' "$path"
fi
done
不要轻易用 for file in $(find ...) 处理文件名。命令替换后的结果会按空白拆分,文件名包含空格、制表符或换行时可能被错误拆开。处理任意文件名时,可以使用 NUL 分隔的方式:
find . -type f -print0 |
while IFS= read -r -d '' file; do
printf '文件:%s\n' "$file"
done
这段写法使用了 Bash 的 read -d 行为。另一个需要留意的边界是:Bash 通常会在管道的子 Shell 中运行循环,因此循环里修改的普通变量不一定会在管道结束后保留。若要在当前 Shell 中处理 NUL 分隔输入,可使用进程替换:
count=0
while IFS= read -r -d '' file; do
((count += 1))
done < <(find . -type f -print0)
printf '文件数量:%s\n' "$count"
进程替换同样是 Bash 功能,不能不加修改地用于所有 sh 实现。
六、用函数整理脚本
重复使用的操作可以写成函数,让主流程更清晰。下面的脚本检查输入路径是否为普通文件:
#!/usr/bin/env bash
is_regular_file() {
[[ -f "$1" ]]
}
for path in "$@"; do
if is_regular_file "$path"; then
printf '普通文件:%s\n' "$path"
else
printf '不是普通文件:%s\n' "$path"
fi
done
Shell 函数通常通过退出状态返回成功或失败,数据可以通过标准输出输出,调用方再用命令替换获取。不要把函数的退出状态和它输出的内容混为一谈。例如,[[ -f "$1" ]] 的判断结果会成为函数的退出状态:条件成立通常为 0,不成立为非零。
函数和脚本共享大部分 Shell 变量环境。大型脚本中可以使用 local 声明函数内部变量,减少意外覆盖:
greet() {
local name=${1:-访客}
printf '你好,%s\n' "$name"
}
greet "$@"
七、错误处理:不要盲目依赖“严格模式”
常见 Bash 脚本会使用下面的设置:
set -Eeuo pipefail
它们能帮助发现一部分问题,但不是完整的错误处理方案:
-e会在许多未被检查的非零状态下退出,但在条件判断、部分列表、管道和函数等语境中有例外。不能简单理解为“任何命令一失败,脚本都会退出”。-u会让读取未设置变量时报错。对于可选变量,应使用有默认值的展开,例如${value:-default},并测试实际调用路径。pipefail让管道的状态在一定程度上反映其中失败的命令;没有它时,管道状态通常只反映最后一个命令。它是 Bash 和部分其他 Shell 提供的选项,不是所有 POSIX Shell 都支持。-E主要影响错误陷阱在函数、命令替换和子 Shell 中的继承行为,不会自动让每个失败都得到妥善恢复。
对关键操作,显式检查返回状态通常更清楚:
if cp -- "$source_file" "$destination"; then
printf '复制完成。\n'
else
status=$?
printf '复制失败,退出码:%s\n' "$status" >&2
exit "$status"
fi
-- 常被支持它的命令用作选项结束标记,可避免以连字符开头的路径被误认为选项。不过,是否支持以及参数应放在哪里取决于具体命令,不能认为所有程序都支持 --。即使支持,也应先核对命令的参数格式。
如果一个命令可能“没有结果”而这属于正常情况,就应有意识地处理其退出状态,而不是依赖 set -e 猜测。例如,grep 找不到匹配项通常返回非零,这不一定意味着脚本本身发生故障。
八、一个完整可运行示例:安全地生成清单
下面的 Bash 脚本接收一个目录和一个输出文件,把该目录下的普通文件路径写入清单。它要求两个参数,拒绝覆盖已存在的输出文件,并使用临时文件避免在生成过程中直接写入目标文件。请在测试目录先运行,再用于自己的文件。
#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
printf '用法:%s DIRECTORY OUTPUT_FILE\n' "$0" >&2
}
if [[ $# -ne 2 ]]; then
usage
exit 2
fi
source_dir=$1
output_file=$2
if [[ ! -d "$source_dir" ]]; then
printf '错误:输入目录不存在:%s\n' "$source_dir" >&2
exit 1
fi
if [[ -e "$output_file" ]]; then
printf '错误:输出文件已存在:%s\n' "$output_file" >&2
exit 1
fi
output_dir=$(dirname -- "$output_file")
if [[ ! -d "$output_dir" ]]; then
printf '错误:输出文件的父目录不存在:%s\n' "$output_dir" >&2
exit 1
fi
tmp_file=$(mktemp "${output_file}.tmp.XXXXXX")
cleanup() {
rm -f -- "$tmp_file"
}
trap cleanup EXIT
find "$source_dir" -type f -print0 |
while IFS= read -r -d '' file; do
printf '%s\n' "$file"
done > "$tmp_file"
if [[ -e "$output_file" ]]; then
printf '错误:输出文件在运行期间已出现,拒绝覆盖:%s\n' "$output_file" >&2
exit 1
fi
mv -- "$tmp_file" "$output_file"
printf '清单已写入:%s\n' "$output_file"
将内容保存为 make-list.sh,然后运行:
chmod u+x make-list.sh
mkdir -p demo
printf '示例内容\n' > demo/one.txt
./make-list.sh demo demo-list.txt
cat demo-list.txt
这个示例展示了参数检查、路径引用、标准错误提示、临时文件和退出时清理。它仍有明确边界:清单使用一行一个路径的文本格式,如果文件名本身含换行符,读取者就无法仅凭换行可靠还原文件名。若清单要供程序进一步处理,应继续采用 NUL 分隔等明确的格式约定。
此外,临时文件名由 mktemp 创建,通常比自己拼接固定临时文件名更安全;但输出文件创建、检查和重命名之间仍可能受到并发进程影响。上述检查减少了误覆盖风险,却不应被当作所有并发场景下的原子“仅当不存在时创建”保证。对重要数据,应根据系统和目标文件系统选用经过验证的原子创建方案,并明确并发策略。
九、路径与权限:最容易造成实际损失的边界
执行删除、覆盖、移动等操作前,应先确认目标路径和影响范围。不要把用户输入直接拼成命令文本,也不要对未验证的变量执行 eval。Shell 会解析变量展开、命令替换和重定向;把外部输入交给 eval 会让输入内容再次作为 Shell 代码解释,可能导致任意命令执行。
下面的写法会把路径作为参数传给命令,避免它被拆分成多个参数:
rm -- "$target"
但引号并不能使危险操作自动变安全。若 target 可能为空、指向错误目录或来自不可信输入,仍需先验证。不要通过删除 /、通配符或未经检查的拼接路径来“简化”清理逻辑。重要操作应先输出将要处理的目标,使用测试目录验证,并保留可恢复备份。
普通用户脚本尽量不要以 root 身份运行。只有确实需要系统权限时,才使用最小必要权限,并检查每一步输入和目标路径。sudo 只是授权手段,不会替脚本自动验证参数或防止命令注入。还要注意,脚本中的环境变量可能来自调用者;对关键可执行程序、配置路径和临时目录,应明确来源与权限要求。
权限位也需要区分:chmod u+x script.sh 只给文件所有者增加执行权限。直接执行脚本时,系统会同时检查目录访问权限、文件权限、挂载策略和安全策略。脚本以谁的身份运行,通常取决于启动它的用户或服务,不由脚本自身决定。
十、常见错误及排查方法
1. 报错“权限不够”
确认是否需要执行权限:chmod u+x script.sh。也可以先用 bash script.sh 运行以区分脚本语法与执行权限问题。如果目录禁止执行,增加文件执行位也无法解决挂载策略限制。
2. 报错“找不到命令”
检查命令是否安装、命令所在目录是否在 PATH 中,以及脚本使用的解释器是否存在。排查时可以运行 command -v 命令名。若脚本依赖特定工具,应在开头检测依赖并给出明确错误,而不是等到流程中途失败。
3. 文件名带空格后处理错乱
检查变量展开是否加了双引号,处理多个参数时是否使用 "$@"。遍历 find 结果时,避免依赖空格分隔,改用 NUL 分隔。需要传递参数列表的场景,不要把参数先拼成一个普通字符串再拆开。
4. 用 sh script.sh 运行 Bash 脚本后报语法错误
显式使用 bash script.sh,或确保文件的 shebang 指向 Bash 并通过 ./script.sh 执行。不要用 sh 运行依赖 Bash 语法的脚本。
5. 变量明明赋值了,后面却是空的
检查变量赋值中是否误加空格、变量名大小写是否一致,以及变量是否只在子 Shell 中修改。管道中的循环常会运行在子 Shell 环境里,变量修改不能当然地带回父 Shell。
6. 脚本中途退出,找不到具体原因
先用 bash -n script.sh 检查语法,再用 bash -x script.sh 跟踪执行。跟踪输出可能包含令牌、密码或其他敏感数据,不应直接公开或写入权限过宽的日志。需要逐步确认时,可在关键位置输出不含敏感信息的状态。
7. 换行格式造成解释器错误
从某些 Windows 编辑器复制脚本后,文件可能使用 CRLF 换行,导致 shebang 或命令出现异常。可使用 file script.sh 检查,并在需要时用编辑器转换为 Unix 换行格式。不要仅凭报错文字就认定是换行问题,也要同时检查解释器路径和文件内容。
十一、测试、日志与维护
写完脚本后,先做语法检查:
bash -n script.sh
再以测试数据实际运行。对于有副作用的脚本,设计一个明确的测试目录和可重复的输入;必要时提供只预览、不执行修改的模式。预览模式也要经过测试,避免它与真实执行路径出现不一致。
调试时可暂时使用:
bash -x script.sh 参数
跟踪信息会显示执行过程,有助于检查变量展开和命令参数,但敏感值也可能出现在输出中。正式日志应记录足够定位问题的信息,同时避免打印密码、访问令牌、私钥和个人敏感数据。使用 printf 通常比依赖不同实现间行为有差异的 echo 更可控。
长期运行的脚本还应说明用途、参数、依赖、退出状态和副作用。调用外部命令时,检查其失败状态;涉及重定向时,确认输出目录存在且有权限;涉及后台任务时,考虑如何收集其退出状态和日志。定时任务的环境通常比交互终端精简,可能缺少预期的 PATH、当前目录和环境变量,因此应使用明确路径或在脚本中设置必要环境。
十二、何时继续用 Shell,何时换工具
如果任务主要是调用系统命令、移动文件、串接管道、读取少量参数,Shell 往往简单直接。若脚本逐渐出现复杂的数据解析、嵌套状态、并发控制、跨平台需求,或必须精确处理各种异常输入,维护成本可能很快上升。此时可让 Shell 负责启动流程,把核心逻辑交给更适合的数据处理或应用语言。
选择工具时,应比较部署环境、输入规模、错误恢复要求和维护人员熟悉程度,而不是单看代码行数。Shell 的强项是利用系统已有命令完成自动化;它的边界同样明确:命令行为受工具实现和版本影响,路径与文本处理容易受空白和编码影响,Shell 本身也不是数据安全或权限隔离机制。
完成前检查清单
- 脚本解释器与实际使用的语法一致,并已确认目标环境安装了该解释器。
- 参数数量、类型、路径和可选值均经过检查,错误提示写到标准错误。
- 变量展开和参数列表使用了合适的引号;处理任意文件名时没有依赖空格分隔。
- 关键命令的退出状态经过处理,且没有把
set -e当作万能错误处理。 - 删除、覆盖、提权等操作经过测试,并明确说明权限、并发和恢复边界。
- 已通过语法检查,并在测试数据上运行;调试信息不会泄露敏感内容。
掌握这些步骤后,可以从一条可重复执行的命令开始,逐步加入参数校验、错误处理和日志。对 Shell 脚本而言,可靠性往往不取决于语法写得多复杂,而取决于是否明确解释器、输入格式、失败行为和操作范围。




