news 2026/5/2 19:02:44

46、新手常见的Shell脚本错误及解决方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
46、新手常见的Shell脚本错误及解决方法

新手常见的Shell脚本错误及解决方法

在编写Shell脚本时,新手常常会遇到各种问题。本文将介绍一些常见的错误,并提供相应的解决方案。

1. 脚本间变量传递问题

在脚本编写中,有时会涉及到脚本之间的变量传递。但需要注意的是,导出的环境变量并非脚本间共享的全局变量,它们是单向通信的。所有导出的环境变量会在Linux或Unix(子)进程调用时被打包传递(可查看fork(2)手册页),但没有机制能将这些环境变量传回父进程。

解决方案

可以通过显式回显第二个脚本的结果,让第一个脚本使用$( )操作符(老的Shell用户也可用`)来调用它。例如,在第一个脚本中,./second.sh这一行可改为VAL=$(./second.sh)`,而第二个脚本需要将最终值(且仅为最终值)回显到标准输出(可将其他消息重定向到标准错误输出):

$ cat second.sh printf "%b" "in second\n" >&2 printf "initially VAL=%d\n" $VAL >&2 VAL=12 printf "changed so VAL=%d\n" $VAL >&2 echo $VAL $

2. 赋值时忘记加引号导致“命令未找到”错误

问题描述

脚本在给变量赋值时,运行脚本可能会出现“命令未找到”的错误。例如:

$ cat goof1.sh #!/bin/bash - # common goof: # X=$Y $Z # isn't the same as # X="$Y $Z" # OPT1=-l OPT2=-h ALLOPT=$OPT1 $OPT2 ls $ALLOPT . $ $ ./goof1.sh goof1.sh: line 10: -h: command not found aaa.awk cdscript.prev ifexpr.sh oldsrc xspin2.sh $

解决方案

需要在赋值给$ALLOPT的右侧加上引号。原本的ALLOPT=$OPT1 $OPT2应改为ALLOPT="$OPT1 $OPT2"

原因分析

这不仅仅是会丢失参数间的嵌入空格,正是由于存在空格才会引发此问题。如果参数通过斜杠连接或根本没有空格,就不会出现这个问题,因为它们会被视为一个单词,从而是一个单一的赋值。但中间的空格会让bash将其解析为两个单词,第一个单词是变量赋值,这种在命令开头的赋值只会在命令执行期间设置变量的值,命令执行完后,变量会恢复到之前的值(如果有)或未设置状态。第二个单词会被视为命令,这就是报告“未找到”的命令。

3. 忘记模式匹配会按字母排序

bash在模式匹配时会对数据进行字母排序。例如:

$ echo x.[ba] x.a x.b $

即使在方括号中指定了b然后是a,但在模式匹配完成并找到结果后,它们会在传递给命令执行之前进行字母排序。所以,如果你这样做:

$ mv x.[ba]

你可能以为它会扩展为$ mv x.b x.a,但实际上它会扩展为$ mv x.a x.b,这与你的预期正好相反。

4. 忘记管道会创建子Shell

问题描述

有一个脚本在while循环中读取输入时运行正常:

COUNT=0 while read PREFIX GUTS do # ... if [[ $PREFIX == "abc" ]] then let COUNT++ fi # ... done echo $COUNT

但当你将其改为从文件读取时:

cat $1 | while read PREFIX GUTS do # ...

脚本就不再正常工作了,$COUNT始终为零。

解决方案

管道会创建子Shell,while循环中的更改不会影响脚本外部的变量,因为while循环是在子Shell中运行的。一种解决方案是避免使用管道,如果可以的话。在这个例子中,可以使用I/O重定向让输入来自重定向的输入,而不是设置管道:

COUNT=0 while read PREFIX GUTS do # ... done < $1 echo $COUNT

如果这种重排不适合你的问题,你需要寻找其他技术。

其他技术

如果需要将更多工作放在while循环之后,可以将剩余的工作放在一个函数调用中,或者将它们放在包含while循环的子Shell中。例如:

COUNT=0 cat $1 | ( while read PREFIX GUTS do # ... if [[ $PREFIX == "abc" ]] then let COUNT++ fi # ... done echo $COUNT ) | read COUNT # continue on...

5. 让终端恢复正常

问题描述

当你终止一个SSH会话后,可能无法看到自己输入的内容;或者意外显示了一个二进制文件,导致终端窗口出现乱码。

解决方案

即使看不到输入内容,也可以输入stty sane然后按回车键,以恢复正常的终端设置。在开始输入stty命令之前,你可能需要先按几次回车键,以确保输入行上没有其他内容。如果你经常这样做,可以考虑创建一个更容易盲打的别名。

其他方法

你的终端应用程序可能也有某种重置功能,可以探索菜单选项和文档。你也可以尝试resettset命令,但在测试中,stty sane能按预期工作,而resettset的修复效果更激进。

6. 使用空变量删除文件

问题描述

你可能有一个变量,认为它包含了要删除的文件列表,可能是为了在脚本执行后清理。但实际上,该变量为空,这可能会导致严重的问题。

解决方案

永远不要这样做:

rm -rf $files_to_delete

更不要这样做:

rm -rf /$files_to_delete

应该使用:

[ "$files_to_delete" ] && rm -rf $files_to_delete

原因分析

第一个例子只是会抛出一个错误,而第二个例子会尝试删除根目录。如果你以普通用户身份运行(你应该这样做),可能问题不大,但如果你以根用户身份运行,那可能会严重破坏你的系统。

7.printf出现奇怪行为

问题描述

脚本给出的值可能与预期不符。例如下面的简单脚本及其输出:

$ bash oddscript good nodes: 0 bad nodes: 6 miss nodes: 0 GOOD=6 BAD=0 MISS=0 $ $ cat oddscript #!/bin/bash - badnode=6 printf "good nodes: %d\n" $goodnode printf "bad nodes: %d\n" $badnode printf "miss nodes: %d\n" $missnode printf "GOOD=%d BAD=%d MISS=%d\n" $goodnode $badnode $missnode

为什么6显示为good计数的值,而它本应该是bad计数的值呢?

解决方案

要么给变量赋初始值(例如0),要么在printf行上引用它们时加上引号。

原因分析

bash会对最后一行进行变量替换,当计算$goodnode$missnode时,它们都为空。所以传递给printf执行的行看起来像这样:

printf "GOOD=%d BAD=%d MISS=%d\n" 6

printf尝试打印三个十进制值(三个%d格式)时,它只有第一个值(即6),后面两个没有值,所以它们输出为零,得到GOOD=6 BAD=0 MISS=0。即使将它们声明为整数值也不够,需要实际给它们赋值。另一种避免此问题的方法是在printf语句中使用参数时加上引号,例如:

printf "GOOD=%d BAD=%d MISS=%d\n" "$goodnode" "$badnode" "$missnode"

8. 测试bash脚本语法

问题描述

在编辑bash脚本时,你希望确保语法正确。

解决方案

使用bash-n参数经常测试语法,理想情况下每次保存后都进行测试,在将任何更改提交到版本控制系统之前一定要进行测试:

$ bash -n my_script $ $ echo 'echo "Broken line' >> my_script $ bash -n my_script my_script: line 4: unexpected EOF while looking for matching `"' my_script: line 5: syntax error: unexpected end of file

注意事项

-n选项在bash手册页或其他参考资料中不太容易找到,它位于set内置命令下。在bash --help中提到-D时会顺带提及,但没有详细解释。此标志告诉bash“读取命令但不执行它们”,这样可以发现bash语法错误。但和所有语法检查器一样,它无法捕获逻辑错误或脚本调用的其他命令中的语法错误。

9. 调试脚本

问题描述

你无法弄清楚脚本中发生了什么,以及为什么它没有按预期工作。

解决方案

在运行脚本时,在脚本顶部添加set -x。或者在有问题的地方之前使用set -x开启跟踪,之后使用set +x关闭跟踪。你也可以尝试使用$PS4提示符。例如,有一个疑似有问题的脚本:

#!/usr/bin/env bash # cookbook filename: buggy # set -x result=$1 [ $result = 1 ] \ && { echo "Result is 1; excellent." ; exit 0; } \ || { echo "Uh-oh, ummm, RUN AWAY! " ; exit 120; }

现在调用这个脚本,首先设置并导出PS4提示符的值:

$ export PS4='+xtrace $LINENO:' $ echo $PS4 +xtrace $LINENO: $ ./buggy +xtrace 4: result= +xtrace 6: '[' = 1 ']' ./buggy: line 6: [: =: unary operator expected +xtrace 8: echo 'Uh-oh, ummm, RUN AWAY! ' Uh-oh, ummm, RUN AWAY! $ ./buggy 1 +xtrace 4: result=1 +xtrace 6: '[' 1 = 1 ']' +xtrace 7: echo 'Result is 1; excellent.' Result is 1; excellent. $ ./buggy 2 +xtrace 4: result=2 +xtrace 6: '[' 2 = 1 ']' +xtrace 8: echo 'Uh-oh, ummm, RUN AWAY! ' Uh-oh, ummm, RUN AWAY! $ /tmp/jp-test.sh 3 +xtrace 4: result=3 +xtrace 6: '[' 3 = 1 ']' +xtrace 8: echo 'Uh-oh, ummm, RUN AWAY! ' Uh-oh, ummm, RUN AWAY!

注意事项

使用-开启某些功能,使用+关闭它们,这可能看起来很奇怪,但这就是它的工作方式。许多Unix工具使用-n作为选项或标志,由于需要一种方法来关闭-x,所以+x看起来很自然。从bash 3.0开始,有许多新变量可以更好地支持调试,如$BASH_ARGC$BASH_ARGV$BASH_SOURCE等。使用跟踪是一种非常方便的调试技术,但它不同于真正的调试器。可以参考The Bash Debugger Project(http://bashdb.sourceforge.net/),它包含了对bash的补丁源,能提供更好的调试支持和改进的错误报告。

综上所述,在编写Shell脚本时,要注意这些常见的错误,并采取相应的预防和解决措施,以提高脚本的可靠性和可维护性。

下面是一个总结表格,列出了上述常见错误及解决方案:
| 错误类型 | 问题描述 | 解决方案 |
| — | — | — |
| 脚本间变量传递 | 导出的环境变量不能在脚本间双向传递 | 显式回显结果,使用$( )操作符调用脚本 |
| 赋值忘记加引号 | 赋值时出现“命令未找到”错误 | 在赋值右侧加引号 |
| 模式匹配排序 | 模式匹配结果按字母排序与预期不符 | 注意模式匹配的排序规则 |
| 管道创建子Shell | 管道中的while循环更改不影响外部变量 | 避免使用管道,使用I/O重定向或其他技术 |
| 终端异常 | 终止SSH会话或显示二进制文件后终端异常 | 输入stty sane恢复设置 |
| 使用空变量删除文件 | 可能删除重要文件 | 检查变量是否为空,避免使用rm -rf /$files_to_delete|
|printf奇怪行为 | 输出值与预期不符 | 给变量赋初始值或加引号 |
| 脚本语法错误 | 不确定脚本语法是否正确 | 使用bash -n测试语法 |
| 脚本调试 | 脚本运行不正常 | 使用set -x开启跟踪调试 |

以下是一个简单的mermaid流程图,展示了调试脚本的基本流程:

graph TD; A[发现脚本问题] --> B[添加set -x开启跟踪]; B --> C[运行脚本查看输出]; C --> D{是否找到问题}; D -- 是 --> E[修复问题]; D -- 否 --> F[进一步分析或使用其他调试工具]; F --> B; E --> G[测试脚本是否正常]; G -- 是 --> H[完成调试]; G -- 否 --> B;

通过以上的介绍和总结,希望能帮助新手更好地编写和调试Shell脚本,避免常见的错误。

10. 总结与建议

在编写和调试Shell脚本的过程中,新手会遇到各种各样的问题。通过对上述常见错误的分析和解决方案的介绍,我们可以总结出一些编写Shell脚本的通用建议:
-变量使用
- 初始化变量,尤其是在使用printf等需要明确值的语句时。
- 给变量赋值时使用引号,避免因空格等问题导致的错误。
- 注意变量在不同作用域(如子Shell)中的传递和使用。
-命令执行
- 谨慎使用rm -rf命令,确保变量不为空,避免误删重要文件。
- 在脚本中使用命令时,要注意命令的参数和语法,避免出现“命令未找到”等错误。
-调试与测试
- 经常使用bash -n测试脚本的语法,确保语法正确。
- 使用set -x开启跟踪调试,帮助定位脚本中的问题。
- 对于复杂的脚本,可以结合使用其他调试工具,如The Bash Debugger Project。

11. 常见问题解答

为了帮助新手更好地理解和解决这些常见问题,下面列出一些常见问题的解答:
1.:为什么导出的环境变量不能在脚本间双向传递?
:导出的环境变量是在Linux或Unix(子)进程调用时单向传递的,没有机制将其传回父进程。这是为了避免多个子进程返回值给父进程时产生冲突,因为一个父进程可以创建多个子进程。
2.:在赋值时加引号和不加引号有什么区别?
:不加引号时,如果值中包含空格,bash会将其解析为多个单词,导致赋值和命令执行出现问题。加引号可以确保整个值作为一个整体进行赋值。
3.:管道创建子Shell会对脚本产生什么影响?
:管道创建的子Shell会使其中的变量更改不影响外部脚本的变量。如果需要在循环中更改外部变量的值,应避免使用管道,或者采用其他技术,如I/O重定向或在子Shell中完成所有相关操作。
4.printf出现奇怪行为的根本原因是什么?
:根本原因是bash在将变量传递给printf之前进行了参数替换,如果变量为空,会导致printf接收到的参数数量与格式说明符不匹配,从而产生意外的输出。

12. 实际案例分析

为了更直观地展示这些常见错误和解决方案的应用,下面给出一个实际案例。

案例背景

假设我们有一个脚本,需要统计文件中以“abc”开头的行数,并输出统计结果。

初始脚本

#!/bin/bash COUNT=0 cat $1 | while read PREFIX GUTS do if [[ $PREFIX == "abc" ]] then let COUNT++ fi done echo $COUNT

问题分析

在这个脚本中,使用了管道cat $1 | while ...,这会创建子Shell。在子Shell中对COUNT变量的修改不会影响外部脚本的COUNT变量,所以最终输出的COUNT值始终为0。

解决方案

采用I/O重定向的方式,避免使用管道:

#!/bin/bash COUNT=0 while read PREFIX GUTS do if [[ $PREFIX == "abc" ]] then let COUNT++ fi done < $1 echo $COUNT

改进后的脚本运行结果

假设我们有一个测试文件test.txt,内容如下:

abc line1 def line2 abc line3

运行改进后的脚本:

$ ./script.sh test.txt 2

可以看到,通过避免使用管道,成功统计出了以“abc”开头的行数。

13. 最佳实践总结

为了帮助新手更好地编写高质量的Shell脚本,以下是一些最佳实践总结:
1.代码风格
- 保持代码的一致性,使用统一的缩进和注释风格。
- 给变量和函数取有意义的名称,提高代码的可读性。
2.错误处理
- 在脚本中添加适当的错误处理机制,例如检查命令的返回值,避免脚本因一个小错误而崩溃。
- 对可能出现的异常情况进行预判,并给出相应的提示信息。
3.模块化设计
- 将脚本中的功能拆分成多个函数,提高代码的复用性和可维护性。
- 每个函数只负责一个明确的任务,降低函数之间的耦合度。

最佳实践表格

最佳实践类型描述示例
代码风格统一缩进和注释,有意义的命名# 统计以abc开头的行数
count_lines_abc() { ... }
错误处理检查命令返回值,给出提示信息if [ $? -ne 0 ]; then echo "命令执行失败"; fi
模块化设计拆分功能为函数,降低耦合度function read_file() { ... }
function process_lines() { ... }

14. 未来学习方向

对于新手来说,掌握上述常见错误和解决方案只是一个开始。为了进一步提高Shell脚本编程的能力,可以考虑以下学习方向:
-深入学习Shell语法:了解更多的Shell特性,如数组、关联数组、正则表达式等。
-学习脚本优化技巧:优化脚本的性能,减少资源消耗,提高脚本的执行效率。
-结合其他工具和技术:将Shell脚本与Python、Perl等其他编程语言结合使用,发挥不同工具的优势。

以下是一个mermaid流程图,展示了新手学习Shell脚本的进阶路径:

graph LR; A[掌握基础语法和常见错误] --> B[深入学习Shell特性]; B --> C[学习脚本优化技巧]; C --> D[结合其他工具和技术]; D --> E[编写复杂、高效的脚本];

总之,编写Shell脚本需要不断学习和实践,通过积累经验,逐步提高脚本的质量和可靠性。希望本文能为新手在Shell脚本编程的道路上提供一些帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!