shell脚本怎么执行命令并获取执行结果的方法
详细讲解在Shell脚本中执行命令并获取执行结果的常用方法,涵盖标准输出捕获、退出状态码判断、错误流重定向、多行结果处理及常见避坑要点。
在编写 Shell 脚本时,“获取命令执行结果”通常包含两层不同维度的含义:一层是命令输出到终端屏幕上的文本内容(包括标准输出 stdout 与标准错误 stderr),另一层是命令运行完成后的退出状态码(Exit Code,数值 0 代表成功,非 0 代表失败)。获取文本输出最通用、推荐的方式是命令替换语法 $(command),而获取状态码则依赖特殊变量 $? 或直接配合条件分支语句。本文将系统梳理这两类结果的获取方式、捕获标准错误的技巧、多行输出处理及脚本编写中的关键避坑细节。
一、理解 Shell 命令执行结果的两种形态
许多初学者在刚接触 Shell 脚本时,容易将“打印在屏幕上的内容”与“命令是否执行成功”混为一谈。实际上,在 Linux/Unix 进程模型中,任何一条命令或脚本执行时,内核都会为其分配标准的输入输出流,并在其进程终止时返回一个数值状态。
命令执行所产生的结果形态主要分为以下两部分:
- 文本输出流:分为标准输出(stdout,文件描述符为 1)和标准错误(stderr,文件描述符为 2)。命令正常打印的日志、查询的数据属于标准输出;而警告、报错或诊断信息默认发送到标准错误流。
- 退出状态码(Exit Status):每个命令执行结束时都会向父 Shell 返回一个 0 到 255 之间的整数。按照业界通用规范,
0代表命令执行成功无异常,任何1-255的非零值均代表某种形式的执行失败或中断。
二、获取命令的标准输出文本
当需要将命令生成的文本保存起来供后续判断、写入日志或作为其他命令的参数时,需要使用“命令替换”(Command Substitution)。
1. 现代推荐语法:$(command)
这是 POSIX 标准推荐的标准写法。Shell 会创建一个子进程(Subshell)执行括号内的命令,并将命令的标准输出直接捕获替换到表达式所在位置。
#!/bin/bash
# 获取当前日期并存入变量
current_date=$(date +%Y-%m-%d)
echo "当前日期是: ${current_date}"
# 获取当前工作目录下的文件数量
file_count=$(ls -1 | wc -l)
echo "当前目录包含文件数量: ${file_count}"
$(...) 语法最大的优势在于支持任意层级的清晰嵌套。例如,要在一条语句中获取某个脚本所在目录的绝对路径,可以直接嵌套编写:
script_dir=$(cd "$(dirname "$0")" && pwd)
echo "脚本所在目录: ${script_dir}"
2. 传统反引号语法:`command`
在较旧的 Bourne Shell 脚本中,常常见到使用一对反引号(Backticks)包围命令:
# 传统语法示例
current_user=`whoami`
echo "执行用户: ${current_user}"
尽管现代 Shell 依然兼容反引号语法,但在实际开发中强烈建议弃用。主要原因有两点:首先,反引号在嵌套时必须对内层的反引号使用反斜杠 \` 进行转义,层级加深后极难阅读与维护;其次,反引号在不同字体渲染下极易与单引号 ' 混淆,增加排错成本。
3. 命令替换的关键特性:尾随换行剥离
一个必须注意的底层机制是:POSIX 规范规定,命令替换操作会自动剔除输出结果末尾的所有换行符(Trailing Newlines)。
例如,命令 printf "hello\n\n\n" 原本输出了文本及连续三个换行,但在通过 res=$(printf "hello\n\n\n") 赋值后,$res 中仅保留 hello 文本,尾部的所有换行符均被剥离。如果业务逻辑严格依赖末尾换行,需在输出末尾追加一个非换行占位符(如 _),读取变量后再将其移除。
三、获取与判断命令的退出状态码
很多时候,脚本并不需要解析命令输出了什么字符串,而仅仅需要知道操作是成功了还是失败了。这时,退出状态码就是核心判断依据。
1. 特殊变量 $? 的瞬态性
Shell 内置特殊变量 $? 会自动存储上一条刚执行完毕命令的退出状态码。由于它每次都会被最新执行的命令覆盖,因此如果需要复用或在多个条件中使用状态码,必须在命令执行后立刻将其赋值给自定义变量保存。
# 错误示例:echo 命令会重置 $? 的值
mkdir /protected_directory
echo "尝试创建目录完毕"
if [ $? -ne 0 ]; then
# 这里的 $? 实际上是 echo 命令的返回值 (0),而不是 mkdir 的返回值!
echo "创建失败"
fi
# 正确示例:立即将状态码保存到变量中
mkdir /protected_directory
status=$?
echo "尝试创建目录完毕,状态码为: ${status}"
if [ $status -ne 0 ]; then
echo "创建失败,错误码: ${status}"
fi
2. if 语句直接评估命令状态
如果不需要具体的非零数字,只需要区分“成功”与“失败”,最简洁且不易出错的写法是直接将命令放在 if 关键字之后,无需借助 $?:
if grep -q "ERROR" /var/log/app.log; then
echo "日志中发现了错误记录"
else
echo "未发现错误"
fi
在上述代码中,grep -q 匹配到内容时返回 0(if 分支成立),未匹配到时返回 1(进入 else 分支)。
3. 管道命令中的状态码陷阱:PIPESTATUS
在由管道 | 连接的多条命令中,默认情况下 $? 只能获取到管道最后一条命令的退出状态码。如果前面的命令失败而最后一条命令成功,整个管道表达式依然会返回 0,这往往会掩盖严重错误。
在 Bash 环境中,可以通过数组 ${PIPESTATUS[@]} 获取管道中每个环节的独立返回值:
# 制造一个前置命令失败的管道场景
cat /non_existent_file | tr 'a-z' 'A-Z'
echo "最终状态码: $?" # 输出 0,因为 tr 执行成功了
echo "管道各个阶段状态码: ${PIPESTATUS[0]} ${PIPESTATUS[1]}" # 输出 1 0
# 在脚本开头启用 pipefail 选项
set -o pipefail
cat /non_existent_file | tr 'a-z' 'A-Z'
echo "启用 pipefail 后的状态码: $?" # 输出 1,只要任意环节失败即判定为失败
四、同时获取输出内容与状态码
在实际生产脚本中,往往需要“既拿到输出文本,又判断是否成功”。由于命令替换语法在执行命令的同时会将子进程的状态码同步更新到当前环境的 $? 中,因此可以采用组合写法。
output=$(curl -s -f "https://api.example.com/status")
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "请求成功,返回数据: ${output}"
else
echo "请求失败,cURL 退出状态码: ${exit_code}"
fi
捕获错误输出(stderr)
普通的 output=$(command) 只能捕获到标准输出(stdout)。如果命令报错,错误信息会直接越过变量赋值、直接显示在终端屏幕上。如果需要连同错误信息一并捕获到变量中,可以使用 2>&1 将标准错误重定向至标准输出:
# 将标准输出与标准错误全部捕获到 output 中
output=$(ls -l /tmp /not_exist_dir 2>&1)
exit_code=$?
echo "完整执行日志:"
echo "${output}"
五、多行文本结果的安全读取与处理
当命令输出多行结果时,如果不了解 Shell 的字符串分词规则,很容易写出引发隐蔽 Bug 的代码。
1. 为什么禁止使用 for 循环遍历命令输出
初学者常常写出类似以下的代码:
# 错误示例:不要这样遍历多行命令输出!
for item in $(ls -l); do
echo "条目: $item"
done
因为 Shell 默认使用 IFS(内部字段分隔符,包含空格、制表符和换行)对未加引号的变量展开进行拆分。一旦文件名或输出内容中包含空格,for 循环就会把同一个文件的名字拆成两个独立的项进行遍历,造成逻辑错误。
2. 标准解法:使用 while read 逐行处理
处理多行输出的最佳实践是结合 while IFS= read -r line 语法,并利用 Here-String(<<<)或进程替换将捕获到的输出喂给循环体:
# 方案 A:使用 Here-String 将多行变量传入循环
output=$(df -h)
while IFS= read -r line; do
echo "磁盘信息行: ${line}"
done <<< "$output"
参数说明:
IFS=:清空临时分隔符,防止read自动去除行首和行尾的空白字符。-r:禁止反斜杠转义,保证文本中的\原样保留。<<< "$output":必须加双引号,将多行字符串原样作为标准输入送入循环。
六、关键避坑要点
1. 引用变量时必须使用双引号 "$output"
在 Shell 中,未加双引号的 $output 在执行时会经历两个危险阶段:字段拆分(Word Splitting)和路径通配符展开(Globbing)。
如果命令输出包含 * 符号,未加引号的 echo $output 会将 * 展开为当前目录下的所有文件名,并且所有的连续空格和换行都会被折叠为一个空格。始终使用 echo "$output" 能够确保换行和格式完整保留。
2. 警惕管道连接引起的子 Shell 作用域隔离
当通过管道 | while read line 逐行处理输出时,管道右侧的 while 循环通常会在一个独立的子 Shell 中运行。循环内部修改的任何变量,在循环结束后都会丢失:
# 陷阱示例:count 的累加在循环结束后丢失
count=0
find . -name "*.txt" | while read -r file; do
count=$((count + 1))
done
echo "总数是: $count" # 输出仍然是 0!
# 解决方案:使用进程替换(Process Substitution)
count=0
while read -r file; do
count=$((count + 1))
done < <(find . -name "*.txt")
echo "正确总数是: $count" # 正确输出统计数值
七、总结速查表
为了便于在日常编写脚本时快速查阅,下表总结了各类场景下的标准语法与关键注意事项:
| 需求目标 | 推荐语法 | 核心机制与注意事项 |
|---|---|---|
| 获取标准输出文本 | var=$(command) |
POSIX 推荐标准,末尾多余换行符会自动被截断。 |
| 获取输出并包含报错 | var=$(command 2>&1) |
将标准错误合并到标准输出中,便于保存完整排错日志。 |
| 获取退出状态码 | status=$? |
必须紧随命令之后立即读取,防止被后续命令覆盖。 |
| 仅做成功与否的分支 | if command; then ... fi |
语法最简洁安全,代码可读性好,无额外内存开销。 |
| 多行输出逐行遍历 | while IFS= read -r line; do ... done <<< "$var" |
防止空格和制表符导致错误切词,变量外层必须加双引号。 |
| 检测管道中间命令失败 | set -o pipefail |
防止管道后续命令的成功状态掩盖前序命令的失败状态。 |
掌握标准输出流与退出状态码的分工逻辑,是写出健壮、可靠 Shell 脚本的基础。在实际编写过程中,优先使用 $(...) 替代传统反引号,对命令替换输出使用双引号包裹,并在需要状态码时第一时间完成暂存,即可有效规避绝大多数常见的解析异常与流程中断问题。
WPS Office是一款面向macOS用户的综合办公套件,将文字处理、电子表格、演示文稿、PDF工具、模板与云协作整合在同一应用中。它兼容常见Microsoft Office格式,支持Intel及Apple芯片Mac,基础功能可免费使用,
Shapr3D是一款面向工业设计、机械工程、建筑概念和三维打印工作流的CAD软件。Mac版采用Parasolid建模内核,支持草图约束、实体建模、工程图、可视化渲染及常见CAD格式交换,并可通过账户在多台设备之间同步项目。
REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。
Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许
Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator
Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。













