Shell脚本学习指南-第七章-输入/输出、文件与命令执行
第7章 输入/输出、文件与命令执行
7.1 标准IO基础
进程打开一个文件、管道、终端之后,内核会给它分配一个小整数编号,这个数字就叫文件描述符。进程通过这个数字,来读写对应的打开的资源(终端、普通文件、管道)。每一个运行的 shell 进程,手里都保存一套自己的文件描述符表。3 个固定标准文件描述符(0、1、2),每个程序一启动就自带。
| fd编号 | 名称 | 说明 |
|---|---|---|
| 0 | 标准输入 stdin | 读数据,默认键盘 |
| 1 | 标准输出 stdout | 正常输出,默认屏幕 |
| 2 | 标准错误 stderr | 报错信息,默认屏幕 |
过滤器程序:从标准输入读,处理后输出到标准输出,适合管道串联。
7.2 read 读取输入行
语法
|
|
-r:原始模式,不把行末尾\当成续行符,写脚本尽量带上‑r- 依据
$IFS(字段分隔符),把输入行切分成单词。 - 规则:
- 第1个单词赋值给
var1,第2个给var2; - 单词数量 > 变量数量,剩下全部内容一股脑给最后一个变量;
- 读到文件结束EOF,read返回非0退出码;常用于
while read循环终止。
- 第1个单词赋值给
示例:交互式输入
经典:while read 逐行读文件
读取/etc/passwd,冒号作为分隔符。
while后面重定向< /etc/passwd写在done后面,不是写在read后面!
IFS=:写在read命令前面(while 关键字之后),是临时环境变量,只作用于本次 read,不改变脚本全局 IFS。
错误写法(每次循环重新打开文件,永远读第一行,死循环)
⚠坑:管道后面的while read会运行在子shell,循环内部修改的变量,循环结束父shell拿不到值。
7.3 重定向大全
基础重定向复习
| 符号 | 作用 |
|---|---|
< file |
标准输入来自文件 |
> file |
标准输出覆盖写入文件 |
>> file |
标准输出追加写入文件 |
set -C noclobber 防止意外覆盖
here‑doc <<EOF 嵌入文档,脚本内写多行输入
- 定界符
EOF不加引号:shell执行变量替换、命令替换。 - 定界符加引号
<<'EOF':原样文本,不做任何替换。
<<‑EOF:会删除内容行开头的tab制表符,方便脚本缩进美化,只删tab,空格不动。
文件描述符高级重定向
2>&1不要加空格;含义:把fd2指向fd1当前指向的地方。
exec 修改shell本身的文件描述符
exec不加命令,只写重定向,就修改当前shell的文件描述符。
保存、恢复标准错误示例:
exec后面带上程序:直接替换当前shell进程,执行完不会返回脚本。
7.4 printf完整详解(优先于echo,可移植)
语法:
|
|
- 不会自动换行,换行必须手动写
\n - 参数多于格式占位符:循环复用格式字符串(shell printf特有,C语言printf不一样)
常用格式指示符
| 符号 | 含义 |
|---|---|
%s |
字符串 |
%d |
十进制整数 |
%c |
单个字符 |
%b |
解析参数字符串里面的转义字符\n \t |
%% |
输出字面的百分号 |
修饰符:宽度、对齐、精度
%10s:占10字符宽度,右对齐%-10s:占10字符宽度,左对齐,减号代表左对齐%.5d:数字,不足5位前面补0%.10s:字符串最多打印前10个字符
示例
注意:浮点
%f %g属于bash/ksh扩展,POSIX shell标准不强制支持,脚本浮点计算改用awk。
7.5 波浪号展开 & 通配符globbing
波浪号 ~
⚠可移植脚本不建议用~,部分老旧sh不支持,优先写
$HOME。
shell通配符(只匹配文件名)
| 符号 | 含义 |
|---|---|
* |
匹配0个或任意多个字符,不匹配文件名开头的.隐藏文件 |
? |
匹配任意单个字符 |
[abc] |
匹配括号中任意一个字符 |
[!abc] |
匹配不在括号的任意字符 |
注意:
[a‑z]受locale环境影响;可移植脚本尽量用POSIX字符集,不要依赖字母范围。 文件名开头.代表隐藏文件;*不会匹配隐藏文件;要匹配隐藏必须显式写.*。
7.6 命令替换:把命令输出当作参数
两种写法,优先使用$(...),不要用反引号```
- 老式反引号
\cmd``:嵌套转义非常痛苦 - 新式
$(cmd):支持嵌套,可读性好,POSIX标准。
注意:命令替换结果会做单词分割、通配符展开;如果内容有换行空格,建议用双引号包裹
"$(...)"。
expr(老旧算术工具,新脚本尽量不要用)
旧时代shell没有$(( ))的时候做整数运算。
现代脚本直接用shell内置算术替换 $((i+1))。
7.7 引用/引号机制(重中之重,解决各种变量坑)
1)反斜杠 \
转义单个元字符,把特殊符号变成普通字面。
|
|
2)单引号 ' 内容 '
强引用:引号内部所有字符原样字面,不做任何展开替换;内部不能再放单引号。
|
|
3)双引号 "内容"
弱引用:禁止通配符、单词分割;但是允许$变量替换、$(...)命令替换、\转义。
| 引号类型 | 变量$ | 命令替换$() | 通配符*? | 单词分割 |
|---|---|---|---|---|
单引号' ' |
不展开 | 不展开 | 原样 | 关闭 |
双引号" " |
展开 | 展开 | 原样 | 关闭 |
| 无引号 | 展开 | 展开 | 展开 | 分割 |
通用最佳实践:变量尽量用双引号包裹
"$var",防止空格换行带来的分词bug。
7.8 shell命令行完整处理流程(理解bug根源)
shell读取一行命令,严格按顺序执行这一套处理:
- 将命令分割成token:用元字符空格、
; | < > &切分成一个个单词;识别重定向。 - 别名展开(交互式)。
- 波浪号
~展开。 - 变量替换
$var。 - 命令替换
$(...)。 - 算术替换
$(( ))。 - IFS分词:对4‑6步骤得到的结果,用
$IFS再次切割单词(这一步是很多空格bug来源)。 - 通配符globbing
* ? []文件名展开。 - 执行重定向。
- 查找命令(内建→函数→PATH外部程序),运行。
eval:把参数再完整重新跑一遍整套处理流程,二次解析。慎用,容易安全漏洞。
7.9 子shell ( ) vs 代码块 { }
| 项目 | 子shell ( cmd1;cmd2 ) |
代码块 { cmd1;cmd2; } |
|---|---|---|
| 是否新建进程 | 新子进程 | 当前shell,不新建进程 |
| cd改变目录 | 只影响子进程,父shell不受影响 | 会改变当前shell工作目录 |
| 变量修改 | 子shell内修改,父shell看不见 | 直接修改当前shell变量 |
| exit行为 | 只会退出子shell | 直接终止整个脚本 |
| 语法 | 括号内任意位置;结尾分号可选 | {后面必须空格;最后一条命令必须带; |
示例子shell
示例代码块
7.10 内建命令 & set选项
shell内建命令运行在shell进程内部,不会fork新进程。
command命令:绕过shell函数,调用真正原始内建命令。
示例:
我们自定义一个名字叫cd的 shell 函数,想在 cd 切换目录之后,顺便修改提示符:
用command解决,绕过函数,调用真正原始的 cd 内建:
- POSIX 特殊内建命令
| 命令 | 简要功能说明 |
|---|---|
: |
空命令,什么都不做;仅做参数展开;常用于占位 |
. |
点命令,读取并在当前shell执行脚本(bash中等价source) |
break |
跳出 for/while/until 循环 |
continue |
直接进入循环的下一轮迭代 |
eval |
对参数再次完整执行一遍shell命令行解析流程 |
exec |
①带程序:替换当前shell进程;②仅加重定向:修改当前shell文件描述符 |
exit |
退出当前shell/脚本,可指定退出码 |
export |
把变量导出为环境变量,可供子进程继承 |
readonly |
设置变量为只读,不可修改、unset删除 |
return |
从shell函数返回,带返回状态码,仅用于函数内 |
set |
设置shell选项;修改位置参数$1 $2…;打印全部shell变量 |
shift |
丢弃前面的位置参数,参数整体左移 |
times |
打印shell及子进程累计CPU使用时间 |
trap |
设置信号捕获,脚本收到信号执行指定命令 |
unset |
删除变量、函数,清除其定义 |
- POSIX普通内建命令
| 命令 | 简要功能说明 |
|---|---|
alias |
设置命令别名,多用于交互式shell |
bg |
将作业放到后台运行(作业控制,交互) |
cd |
切换shell当前工作目录,必须为内建 |
command |
绕过shell函数,执行原始内建/外部命令;command -p使用默认PATH |
false |
返回非0失败退出码,不做实际工作 |
fc |
命令历史编辑修复(交互模式) |
fg |
把后台作业调至前台运行(交互) |
getopts |
解析脚本的命令行选项参数 |
jobs |
列出后台作业列表(交互) |
kill |
向进程/作业发送信号 |
newgrp |
切换用户组ID |
pwd |
打印当前工作目录 |
read |
读取标准输入,拆分赋值给shell变量,‑r原始读取 |
true |
返回0成功退出码,不做实际工作 |
umask |
设置新建文件的权限掩码 |
wait |
等待后台进程/作业结束,获取退出状态 |
补充:
echo、printf、test、[ ]POSIX建议实现为内建,但不属于上面两类。
- bash专属扩展内建(非POSIX,可移植脚本不要用)
| 命令 | 简要功能说明 |
|---|---|
shopt |
设置bash专属行为选项 |
declare |
声明变量类型(整数、数组等) |
local |
函数内部局部变量,仅函数内生效 |
history |
操作命令历史记录 |
help |
查看bash内建帮助 |
bind |
修改readline键盘绑定 |
set常用选项(短选项,-开启,+关闭)
| 短选项 | 长选项 | 作用 |
|---|---|---|
‑C |
noclobber | 禁止>覆盖已存在文件,> |
‑e |
errexit | 命令非0退出码,脚本直接退出 |
‑u |
nounset | 使用未定义变量,脚本报错退出 |
‑x |
xtrace | 打开调试跟踪,打印执行的命令 |
‑f |
noglob | 关闭通配符* ? []文件名展开 |
.(source)内建命令:在当前shell进程读取执行脚本文件,不会新建进程,变量、函数全部继承到当前shell。
|
|
小结
while read读文件,重定向写在done <file;不要写在read后面。管道后的while read,变量修改无法带回父shell。2>&1顺序不能颠倒;要合并stdout/stderr,必须>file 2>&1。printf参数多于占位符,循环复用格式串;浮点不要依赖%f。- 引号区分:单引号完全字面;双引号允许变量替换;变量建议
"$var"双引号包裹。 $(...)优先,淘汰反引号\cmd``。( )子shell,内部改动不影响父脚本;{ }代码块直接修改当前shell状态。- shell处理有固定顺序:变量/命令替换之后才会做IFS分词、通配符展开,很多怪异bug都来自这个流程。
set -eu写脚本强烈推荐,捕获未定义变量、命令失败问题。
速查小卡片
| 命令/语法 | 作用 |
|---|---|
read -r a b |
读取一行到变量,‑r关闭反斜杠续行 |
while IFS=: read ...; do ... done < file |
逐行读取文件 |
cmd >a 2>&1 |
标准输出+错误输出写入同一个文件 |
<<'EOF' |
here‑doc,完全原样文本,不替换变量 |
printf "%‑10s\n" "hi" |
格式化输出,左对齐 |
$(command) |
命令替换,获取命令输出 |
'字符串' |
强引用,完全字面 |
"$var" |
弱引用,保留变量,防止分词 |
(cmd1;cmd2) |
子shell,不污染父shell |
{ cmd1;cmd2; } |
代码块,当前shell执行 |
set -eux |
脚本开启错误检查、调试跟踪 |
./script.sh |
source,当前shell执行脚本 |
文章作者 会写代码的小郎中
上次更新 2011-07-20
许可协议 CC BY-NC-ND 4.0