我其实特烦那种把Shell脚本写成“一次性工具”的感觉。

就好像写完就跑,下次再用?
不好意思,环境变了,直接报错,一脸懵。
后边被坑骗的次数多起来了,方可领悟那些表面瞧着“高级”的写法情形,实际并非是在炫耀技巧,而是用作保住所身性命之用意目的的。
var=""
[ $var = "hello" ]
那个该死的方括号,和它背后的男人
**空变量在`[ ]`里会直接导致语法错误**,但在`[[ ]]`里完全没问题。这就是为什么现代脚本都推荐用双括号,安全多了。

你可知道,最初之际我曾以为,[乃语法之中的一部分,然后续之方才晓得,这东西实际上是一条命令,跟 ls 并无啥差异。
which [ 一下,居然真有这个二进制文件。
ping -c 1 google.com > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "网络畅通!"
else
echo "糟糕,连不上网..."
fi
这便说明,缘由在于老式写法当中的样子,变量两侧得加上引号才行,否则要是变量处于为空的状况时,[ $var ="hello" ] 就会演变成 [ ="hello" ],命令在对其进行解析之际,遇到 = 直接就会出现语法错误,根本不会给你任何机会。
那个错误提示,冷冰冰的,像极了分手时的已读不回。
if ping -c 1 google.com &> /dev/null; then
echo "一切正常"
fi
而 [[ 就不一样了,它是Bash的关键字,是亲儿子。
它知道你在想什么,空变量?
运行这个脚本,你会看到清晰的错误信息:“naem: 未绑定的变量”。没有`set -u`的话,脚本会默默输出“你好,”,留给你一脸懵逼。
**生产环境的脚本都应该加上`set -u`**,它能帮你提前发现很多低级错误。配合`set -e`(遇到错误立即退出)和`set -o pipefail`(管道中任意命令失败就失败),你的脚本健壮性会提升好几个档次。
不过要注意,检查变量是否为空要用正确姿势:
```bash
没事,它自己会处理。
for file in *.sh; do
echo "$file"
done
正则匹配?

也行。
这才是现代脚本该有的样子 。
find . -name "*.sh" -type f | while read file; do
echo "找到脚本:$file"
done
引号,全是引号
count=0
find . -name "*.sh" | while read file; do
count=$((count + 1))
done
echo "找到 $count 个文件"
实实在在讲,在Bash之中,占据百分之九十比例的那种诡异莫名的错误,通通是跟空格以及空字符串存在关联的。
解决方法是用进程替换:
```bash
while read file; do
你书写rm -rf $MYDIR/,哪一日 MYDIR未执行赋值行为,便演变成了 rm -rf /。
哥们,这玩笑开大了。
./some_script.sh 2>&1 | tee -a app.log
使用 set -u 的话,要是你在程序运行期间用到了那时尚未定义好的变量呢,程序就不会懵懵懂懂、毫无觉察且不管不顾地傻乎乎径直接着执行下去了,而是恰恰相反,会直接退出该程序过程。
./some_script.sh 2>&1 | tee >(grep ERROR > errors.log) >(grep WARN > warnings.log) > all.log
还有就是永远、永远、永远要记得加双引号。
"$1" 和 $1 是两个世界。
前一个是具备完整性的参数,即便其内部存有空格;后一个将会经由词法进行分割,从而转变为好几个各自独立的词。

这就像“爱吃榴莲的单身狗”和“爱吃榴莲的、单身狗”之间的区别,懂的都懂。
脚本 test.sh:
echo "用$@:"
for arg in "$@"; do
echo "[$arg]"
done
echo "用$*:"
for arg in "$*"; do
echo "[$arg]"
done
运行:./test.sh "a b" c
输出:
用$@:
[a b]
[c]
用$*:
[a b c]
管道里的那些鬼打墙
我最为厌烦的一种情形是,while read line; do let count++; done < somefile 这样的代码段,在执行完毕之后进行查看,可发现 count 的数值依旧是0。
if [[ -x "$file" ]]; then
echo "文件可执行"
fi
原因?
管道两边的命令是在子Shell里跑的。
你的循环在另一个世界累死累活,外面的变量岿然不动。
if [[ -x "$file" ]] && [[ -f "$file" ]] && head -n 1 "$file" | grep -q "^#!.*bash|sh"; then
echo "这是一个可执行的Shell脚本"
fi
那种无力感,就像你在游戏里救了半天公主,结果发现存档没存上。
if file "$file" | grep -q "shell script"; then
echo "file命令也认为是Shell脚本"
fi
解决办法?
用进程替换 while ... done < <(command),或者干脆把循环体写成这样,让它在当前Shell执行。
echo "a b c" | cut -d' ' -f1
处理文件名?
**awk默认用任意空白字符(空格、制表符)分隔**,而且会压缩连续的空白:
```bash
echo "a b c" | awk '{print $1}'
小心点吧朋友
你以为文件名就是 a.txt 和 b.txt?
awk更强大的地方在于能处理复杂情况:
```bash
太天真了。
**简单固定格式用cut,复杂处理用awk**。还有一个常用组合:`awk -F','`指定逗号分隔,处理CSV文件比cut方便多了。
哪一天,碰到一个名为 “-rf” 的文件,或者名字当中带有换行符的,你的脚本就要等着出丑了。
将这种写法,也就是“for i in $(ls .txt)”,在比较及时地尽早地丢掉忘记忘掉消除抹去把其抛到九霄云外摒弃忘却。
用 for i in .txt 都比你强。
sed -i 's/old/new/g' file.txt
然而最为稳妥的情形,还得是像 find . -type f -print0 | xargs -0 这样的组合方式,其采用空字符进行分隔,不管是何种稀奇古怪、难以捉摸的文件名,它都能够加以处理。
**第三个坑:贪婪匹配**:
```bash
echo "old old thing" | sed 's/old.*thing/NEW/g'
安全,永远是第一位的。别让脚本成了定时炸弹。
脚本头部的 shebang,别搞错了
**第四个坑:换行符**。sed默认按行处理,跨行匹配要特殊处理:
```bash
sed ':a;N;$!ba;s/oldnold/NEW/g' file.txt
“#!/bin/sh”跟“#!/bin/bash”并非一样,是特别在像Ubuntu这样的系统之上,“sh”所指向的是那个“dash”,是一个更为严格、具备更少功能的Shell。
sed -i.bak 's/old/new/g' file.txt
你用了个数组?
对于大文件,sed比一些图形编辑器快得多。**掌握sed的正则表达式**,你就能批量处理成千上万个文件,效率提升不是一点半点。
不好意思,报错。
用了个 ==?
对不起,不认识 。
明确指定为 #!/bin/bash。或者呢,为了更为通用情形下的环境,选择使用 #!/usr/bin/env bash。以此来告知系统说,哥们儿,我想借助 Bash所具备的特性,别再来给我处理那些兼具兼容性方面的麻烦事儿了。
用`#!/bin/sh`(实际是dash)运行会报错:“Syntax error: "(" unexpected”。因为**数组语法是Bash的扩展**,标准Shell不支持。
其他Bash特有的功能:
双括号`[[ ]]`
进程替换`<(command)`
大括号展开`{1..10}`
正则表达式匹配`=~`
**安全建议**:如果你用了任何Bash特有功能,就必须写`#!/bin/bash`。如果想让脚本更通用,限制自己只用POSIX标准语法,然后写`#!/bin/sh`。
检查脚本兼容性可以用:
```bash
bash -n script.sh
调试?
让脚本自己说话
换一种方式,不要先进行这样的操作,即输出显示“到这里了”,接着又进行那样的操作,也就是输出陈述“也到这里了”。
运用 set -x 呀,它能够将执行的任意一条命令以及展开之后的模样都予以打印呈现,清晰得很。

与set -e(一旦出错便停止运行)以及set -o pipefail(只要管道中的某一个命令出现错误,就认定整个管道操作失败)相结合,你的脚本才算是具备了基础性的自我保护意识。
信号捕获,做个有素质的脚本
写了个脚本,生成了临时文件。
Shell脚本就像一把瑞士军刀,小巧但功能丰富。每个细节背后都有设计逻辑,理解这些,你就能写出既健壮又优雅的脚本。别停留在“能用就行”,深入一点,你会发现另一个世界。
结果用户按了个 Ctrl+C,脚本跑了,临时文件留了一地。
这就是没素质。

利用 trap 这个命令,去捕获 EXIT 信号,或者捕获 INT 信号,每当退出的时候,就要去执行清理相关的工作,要做完以下这些事,也就是删除临时所涉文件,还要恢复环境变量,以此达成目的。
这不仅是技术,更是脚本的教养 。
处理参数,用 "$@" 别用 "$"
这俩的区别很微妙。
代码内的 “$” 会将所有用到的参数视同而为一个用以表达的字符串,而代码中的 “$@” 却会保持每一个参数自身单独的性质。
绝大多数时候,你想要的是后者。
**但这样写其实不够优雅**!更好的做法是直接判断命令:
```bash
if ping -c 1 baidu.com &> /dev/null; then
echo "网络畅通无阻!"
else
echo "哎呀,网络好像断了..."
fi
特别是参数里带空格时,用 "$*" 会死得很惨 。
最后,记得看门狗
grep "pattern" file.txt
case $? in
0) echo "找到了!" ;;
1) echo "没找到..." ;;
2) echo "文件打不开!" ;;
*) echo "发生了未知错误" ;;
esac
shellcheck 这个工具,简直就是脚本员的救星。
朝里面投放你的脚本,那里可以讲给你何处有可能会出现阻碍之处,何处存在着语法方面不相互兼容的状况,何处有着变量引用呈现不对劲的情况!

在你盲目自信的时候,给你泼盆冷水,让你清醒一下 。
实际历经这么多年去编撰脚本有着如此感触,那便是机器是不灵活变动的,而是逻辑具备灵活变动性,你觉得自己所撰写的好像是交由机器予以执行的指令,但实际上每一行乃是面向未来自身予以查看的遗书。
让书写表述清晰一些,做到严谨些,未来时候对应的你,会对当下此刻敲打出每一行代码的自身心怀感激之情。
别让那些暗坑,埋葬了你本该顺畅的自动化之路。
现在加上`set -u`:
```bash

Comments NOTHING