Shell编程基础及实战演练

Shell 编程基础与实战演练

本文档主要面向具备一定计算机素养的学习者,旨在梳理 Shell 编程的核心知识点。本文排除了生涩术语与比喻,覆盖了从原理机制到高阶实例的核心应用,带你平滑掌握 Shell 的实战技巧。

1. Shell 基础概念

Shell 是一种命令解释器,能够读取用户的输入内容并执行对应的程序。

  • 互动机制与脚本化: Shell 既可在命令行进行直接人机交互,同时也是一种强大的脚本编程语言。
  • 常见的 Shell: bash(应用最广泛)、dash(通常用于作为脚本解释器使用)、zshash 等。大多数 Shell 均实现了 POSIX 通用规范。

2. Shell 处理输入行的生命周期

Shell 读取一行命令输入后,会严格依照以下顺序进行解析与转换操作。理解该解析顺序是避免 Bug 和安全漏洞的核心机制:

  1. 波浪号扩展 (Tilde expansion):例如将 ~user 转换为 /home/user
  2. 变量扩展 (Variable expansion):例如 $HOME 转换为 /home/z1234567
  3. 算术扩展 (Arithmetic expansion):计算数学表达式,例如 $((6 * 7)) 转换为 42
  4. 命令替换 (Command substitution):例如 $(whoami) 转换为执行该命令产生的标准输出内容。
  5. 单词拆分 (Word splitting):基于空格符,将当前行被扩展后的文本拆分为多个独立的单词传参。
  6. 文件名扩展 (Globbing):例如 *.c 扩展为当前目录下所有的 .c 文件列表。
  7. I/O 重定向 (I/O redirection):例如 <i.txt 会将所运行程序的标准输入指向 i.txt
  8. 应用执行:将上述处理完毕后的第一个单词作为程序名称运行,后续其余单词做为其入参。

3. 基础输出:echo

echo 会将后随的所有参数打印到系统标准输出 (stdout)。

  • -n 选项:不输出行尾的换行符。
  • -e 选项:解析命令里的反斜杠转义字符(如 \n 代表换行)。此功能在 dash 中通常默认开启。
    示例:
1
2
echo -n Hello Andrew
echo -e '\n'

4. Shell 变量设置与解析

Shell 变量都是无类型字符串,不需要声明甚至连初始化也可以省略(未预先初始化的变量默认为空字符串)。

  • 赋值name=value(等号两边绝对不能有任何空格)。
  • 取值:使用 $name 进行获取。

命令替换机制

执行内部命令并将其标准输出结果捕获为文本字符串合并至当前参数:

  • 现代常规语法:$(command)
  • 历史旧版语法(反引号):`command` (不建议在现代应用里层叠使用,容易产生解析混淆)
    示例:
1
2
now="$(date)" # 放入双引号可防止生成后因为含有空格而发生了非预期的单词折分情况
echo "$now"

变量默认值语法

  • ${j-10}:提取变量 j ,若其未设置则回退使用值 10
  • ${j=33}:若变量 j 未被赋值,将其安全置为 33 并返回它。
  • ${x:?No Value}:若 x 未赋值,立刻中止脚本处理并向控制台抛出 “No Value” 错误。

5. 常见字符串引号策略及差异

正确使用不同风格的引号,掌握 Shell 会不会拆分和解析特殊字符:

  • 单引号 '':将其包覆内部的所有字符看做完整静态字符串。单引号内部不会识别任何转义符或变量(不允许内部嵌套单独的单引号)。
  • 双引号 "":将其内部的字符看做整体字符串防范因为空格分化成各词。但 允许继续解析 $, \, 以及反引号 ``` `` 等

示例:

1
2
3
answer=42
echo "The answer is $answer." # 输出 The answer is 42.
echo 'The answer is $answer.' # 输出 The answer is $answer.

多行输入:Here Documents

当需要免创建临时文件就向某个需要读输入的指令塞入大量换行字符时:

1
2
3
4
5
name=Andrew
tr a-z A-Z <<END-MARKER
Hello $name
How are you
END-MARKER
  • <<word 标志到 word 这个停止符中间的所有行,照样进行变量解析扩展。
  • <<'word' 若停止词包上单引号,内部所有内容不再解析变量,等价纯静态单引号块。
  • <<-word 特殊标记能够将内容块中每行开头的制表符 (Tab) 安全移除,方便美观的在 shell 里缩排写文本块。

6. Shell 中的算术运算

通过组合标识 $(( expression )) 执行类似于 C 语言规则的整数运算。所得结果可按普通串引用:

1
2
3
x=8
answer=$((x*x - 3*x + 2))
echo $answer # 输出 42

算式表达式里可以省略变量名上的 $ 直接运算。由于涉及在整型、字符串之间做无缝换算,原生 Shell 处理算数速度相较 C 较低。

7. 文件名自动补全(文件模式匹配:Globbing)

在发生单词拆分(步骤 5)完成后,含有类似符合会尝试以文件视角进行匹配补全(注意这并非更完善的 Regex ):

  • * 匹配 0 个或任意数目的字符。
  • ? 匹配一个单独任意字符。
  • [chars] 匹配对应括号里存在的某一个字符。
  • [!chars] 匹配处于此集合以外的一个字符。
    若匹配不出相应的实际物理文件,原带符号的内容不变。

8. I/O 标准输入流/输出流重定向与管道机制

Unix程序天生携带三个主流接口:标准输入(stdin)、标准输出(stdout)、及标准错误输出(stderr)。

  • < file:令程序的输入源更换为 file 文件内容。
  • > file:令标准输出覆盖写入此 file (高危警告:指令调用成功前就会先排空现阶段文件,如果出问题将毁坏数据)
  • >> file:将输出数据追加于文件末尾。
  • 2> file2>> file 同样负责标准报错方向内容的输出流和增设处理。
  • > file 2>&1 操作能把前置报错合并于同一管线内打入该物理文件。

管道符 |
将前面程序所吐露出的 stdout 流,顺向灌入其后所紧靠程序的 stdin 输入端里。比如 command1 | command2
所有在管道里的命令,其本身都会处于一个克隆的子环境 Sub-shell 执行,若其中做了目录调换/变量覆盖操作都会消失,无影响当前总控脚本。

9. 操作命令查找路径的 PATH 环境

对在命令最开始所触发执行的程序词进行检索扫描。如果不含具体的物理系统相对/绝对路径体系(/ 开头或以 ./ 作为基础):
系统会依次按照环境变量 PATH 所存储通过冒号 : 切分开的一批绝对目录挨个扫描对应有无该可执行包。
不推荐放入针对自己当下的本地目录 .:非常容易遭致其他环境存在重名不安全替换指令而失手运行受害,在提升管理员权限下更须留意禁止此类设置。

10. 构筑与运行 Shell 脚本化实装

为了使得该堆砌式操作可以多次留用,将操作放入常规文件中并在页首注入专门标语 (Shebang):

1
2
#!/bin/dash
echo Hello, execution time is $(date)

利用 chmod 755 [该文件] 授权执行指令功能后即可通过本地路径 ./[文件] 在当前系统完成运行。

用于接受调用上下文的内置变量

  • $0:代指所启动触发脚本本身的称呼名。
  • $1, $2, ...:代表所接收命令后面的首个,第二位,依次顺延调用入参等。
  • $#:提供当时附带有效参数在内的汇总计数整型。
  • "$@":此魔法扩展标志能严格无损保护各部分由于本身夹带了空格等而生成的入参原本单词切分状态。(最为重要的安全调用手段之一)
  • $?:反馈了前置立刻运行的任意指令程序,给予操作系统的完结状态数值 (Exit Status)。
  • $$:展现本套剧本目前分配到的操作系统进程 ID 序列号 (可运用于构筑特殊或无重叠临检物理文件名等)。

11. 退出结束码 (Exit Status) 与 test 命令

正常平滑操作收工默认皆为 0 数值,其他皆对应发生了相应的处理错层及报错,此退出码能作为条件依据。
指令 test 是被核心内嵌的支持多种格式的核验组件:

  • 同等词语代称: 习惯以 [ 为替代标语 (调用方必在尾端补足收缩位 ])。
  • 支持各类字符串同级与区别验证 (=, !=)。
  • 涉及整数的高层级化大小对比符验证 ( -eq, -lt(小于), -gt(大于), 等等)。
  • 对系统的物理文件情况摸排:-f 是否存在并为标规文件;-x 查看当下自己是否可运行;-r-d 查验可读性或是否具备文件夹特质。
  • 布尔结构运算符搭建(-a:And、-o:Or、 !:Not拦截反转等)。

示例:

1
2
test "$msg" = "Hello" # 判断相等
[ -r xyz -a -d xyz ] # 判断文件存在读取权限并且属于物理结构目录

12. 控制流体系结构:

判定分流: If 条件分支

1
2
3
4
5
6
7
if command1; then
# 若指令 command1 的执行判定值为 0 (正常/达成); 则进驻该部分
elif command2; then
# 若指令 command2 判定为 0
else
# 全都失败时收留进入的分流部分
fi

若主体无需执行内容,可引入系统专门保留做无害无执行特指的 : (冒号语句) 作为空白处理体,否则系统报错体结构缺漏。

条件与范围遍历循环 (While/For)

1
2
3
4
5
6
7
8
9
# While 当核查点退出值为0时永驻式调用。
while command; do
body-commands
done

# For 将依据给与的范围列或内容循环走遍
for var in word1 word2 word3; do
body-commands
done

多条件匹配捕获:case 结构体系

借用通配符 *.o, [Yy]* 模式(而非完全基于严格正则表达式)完成文本对比分发体系(通常多应用于处理用户多维响应的问询或系统分配策略):

1
2
3
4
5
6
read answer
case "$answer" in
[Yy]*) response=":)" ;;
[Nn]*) response=":(" ;;
*) response="??" ;; # 当全部无法吻合做特殊后备接应
esac

13. I/O 简易并行与逻辑关系重叠判断

不借助复杂长式的结构语句,仅仅凭借操作链也能达成顺位执行目的。

  • 与操作 &&:要求当前端运行完美收录状态值 0 后,再延续调用及开启紧随机后程序段。
  • 或操作 ||:允许前方产生错误,充当保底和抢修功能的程序执行连接纽带。

命令组隔离:

  • 花括号 {}:将代码聚合于当下,运行产生的变更(目录变更或变量影响)全员全局生效。
  • 小括号 ():分配子级沙盒独立调用,所有的影响局限其内部自行运作,不会扩散或破坏宿主变量态。

14. 脚本交互流指令 read

提取单行的数据传递给变量体系,此内置动作经常绑定控制循环,专门做行记录提取操作和处理交互,其对回车等进行自然清理剔除。

1
2
3
4
5
6
7
8
# 单体使用场景:
echo -n "Do you like Shell? "
read answer

# 复合防丢环境:配合不使用词频符并保持原生字序及去除逃逸的防报错应用 -r
while IFS= read -r line; do
echo "$line"
done < file.txt

15. Shell 闭包化函数体系 (Function)

提供对代码的封装隔离复用。在语法调用上不沿袭大多数常规编程括号带入变量法体系。而是接纳参数为同前文剧本级调用法则 $1

1
2
3
4
5
6
7
8
9
10
11
12
13
is_prime() {
# 局部范围圈定:阻断泄露改写掉主程序里的调用。
local n i
n=$1
i=2
while [ $i -lt $n ]; do
# 余数为零提前强制释放0以外的错误判定数值。
test $((n % i)) -eq 0 && return 1
i=$((i + 1))
done
# 通过考核验证下发 0。
return 0
}

16. 关键实战编程思维及示例应用:

示例 1: 利用综合指令流水线处理文字分析(词频排序重组)

展示了无需自己写 C 或者 Python 进行文字解析计算的系统工具拼接解法能力:

1
2
3
4
5
6
7
8
cat "$@" |           # 放行带入全文
tr 'A-Z' 'a-z' | # 置换转换以全部化为底层一统的小写
tr ' ' '\n' | # 按行距隔离一切文字
tr -cd "a-z'\n" | # 使用-cd参数暴力过滤阻绝除开规范合法外其它的各种污染符号
grep -E -v '^$' | # 清除留下的毫无信息含量的空白内容串层区 (-v: 反向保留)
sort | # 第一遍按照基础序列重排堆叠整合在一起 (必须做此步,uniq才能做对比并计算基数)
uniq -c | # 根据前后左右紧贴着重复项数打上计数排比数码
sort -rn # 使用 -n 告知作为十进制数字系统比较和根据逆向参数 -r ,令数字巨大的高频词跃升最高顶部展示

示例 2: 借由捕获网页面正则并定期轮询监测通知发送体系

1
2
3
4
5
6
7
8
9
repeat_seconds=300
while true; do
# 以静默操作无日志反馈做访问并且查询符合度将其丢垃圾箱,随后仅仅保留状态检查
if curl --silent "$url" | grep -E "$regexp" > /dev/null; then
echo "Match triggered" | mail -s "Success matching regex $regexp" "$email_address"
exit 0
fi
sleep $repeat_seconds
done

示例 3: 应对断崖式终结退出产生的垃圾文件残留清理问题 (trap防阻断结合mktemp)

**不该使用在同级暴露名并叠加随机 PID $$ 作为应对并发及系统层临时文件安全保护。**应该选用具备可靠安全能力的专门内核调度组件 mktemp

1
2
3
4
5
6
temporary_directory="$(mktemp -d)" # 具备权限屏蔽阻绝他人在同一级共享体系干涉破坏机制的专业目录体系创建。

# 无论受到诸如人为破坏操作的 Control-C (由信号源 INT/TERM 控制)还是运行正常宣告完毕,皆由内部自发调动最终收尾垃圾场清扫保护,不再有泄露与遗留残留项堆积:
trap 'exit 1' INT TERM
trap 'rm -rf "$temporary_directory" ; exit' EXIT
cd "$temporary_directory" || exit 1

示例 4: 使用 Cryptographic Hash (哈希运算) 做超大型海量对比查重(突破 O(n²) 原生逐行比对限制)

针对原本使用两重 for 循环不断执行 diff 需要执行量爆炸式的难题,我们可以使用哈希打标签方式,让 O(n²) 转至 O(n + 排序时间):

1
2
3
4
5
6
7
8
9
10
11
12
sha2hash(){
# 可以用 sed 对原始逻辑层里可以随意乱写但不生效诸如注释/变量取名/字面文本强行化为一种模式抹除比对项影响。
sed '
s/\/\/.*//
s/"[^"]"/s/g
s/[a-zA-Z_][a-zA-Z0-9_]*/v/g
' "$1" | sort | sha256sum
}

for file in "$@"; do
echo "$(sha2hash "$file") $file"
done | sort | uniq -w 32 -d --all-repeated=separate

示例 5: 构建高性能跨系统支持多线程并发程序 (Parallelism)

利用 & 将指令释放及丢入后台非阻塞运行。通过下达独立节点结束再调用统一系统屏障 wait 处理回归,利用所有的CPU核去执行互不关联的小片操作流。

1
2
3
4
5
for f in "$@"; do
clang -c "$f" & # 剥离单核阻塞等待进程进行自由发射操作。
done
wait # 拦截脚本在后台没有反馈时擅自流经此处向系统请求进一步收整命令的操作,直到完全同步确认处理成功释放后通过。
clang -o binary -- *.o

当系统处理项目超额时这反而导致服务器死机或调度溢出瘫痪错误,更成熟高级方案如下能够基于本机多处理性能最大化做批控 xargs --max-procs:

1
2
3
4
5
# 调动内核查询核体单元配置数目:
max_processes=$(getconf _NPROCESSORS_ONLN 2>/dev/null) || max_processes=8
# 使用防止因为夹带异常名称诸如带空格回车乱换行导致误分词事故的以 `\0` 来隔离切割项参数:
find "$@" -print0 | xargs --max-procs=$max_processes --max-args=1 --null clang -c
clang -o binary -- *.o

最佳补充实践指南
永远不要在正式系统部署前使用肉壳人力去直接排查并寻找自己的 shell 致命写法逻辑。强制先依靠专门自动工具 ShellCheck 代码静态化检查器 做审核,该过程能有效杜绝由于疏漏及没给双引号、手滑空格、及执行步骤等引起的灾难安全与毁灭操作级事故错误。