因为之前只是需要读懂, 会简单修改Shell, 现在需要自己重构/抽取优化shell ,并使用一些中高级技巧. 所以从Linux开发常见命令和技巧 中抽取出来了~ 因为shell写法和用法也很多,不足之处希望指出.
19年5月更新: 偶然看到一个挺不错的bash规范指南-精简版, 可以优先参考文中所说. 如果你需要, 这里是完整版PDF. 整个bash/shell的问题里面总结归纳的很全面(进阶使用1)
20年4月更新: 阮一峰新写的bash-guide是目前最推荐入门上手的, 非常感谢, 简洁明了. (入门使用)
20年5月更新: 大牛写的shell小功能优雅实现, 良心推荐, 可优先参考而不是看我写的:) (进阶使用2)
0x00.基础知识
Shell本质是C语言编写的程序 ,简单理解就是API~ 但是同时它也,满足PL的特性,所以比较特殊~
你写了一个shell, 执行的时候等同于你一行一行把命令敲到命令行下. (解释性 –需要解释器)
常见的shell解释器:
bash , sh , csh ,zsh(Linux默认是bash, Unix默认是sh)PS :
Linus本人一般就只用C和Shell编程 (www
更新: 为了统一缩进和格式化, 如果使用vscode , 请按照shell-format 插件, 写完一键格式化. 没有插件的时候养成类似的习惯.
同高级语言的常见区别:
变量默认全为字符串类型,不论你加不加单引号/双引号/数字. (所以操纵字符串基本是shell核心)
赋值
=周围不能有空格..a=1.不能是a = 1.使用赋值的变量加$.比如输出a : echo $a.可以加{},写成${a}的形式. 以便确定变量名结束.. 后面不要随便拼接字符 .单/双引号是有区别的. 单引号会原封不动输出所有引号内字符串.只适合显示纯字符串的地方 ; 双引号可以显示转义字符跟变量,所以建议若非必要,都使用双引号. 调用命令用反引号` 括起来 . 如果觉得容易与单引号混淆,建议使用$(command)的形式.
只支持一维数组,无需显示声明,可以直接赋值.. 比如
arr[0]=1,标准声明一堆的写法arr=(v1 v2 v3..)空格隔开.arr[*]或arr[@]获取所有元素值字符串处理比较常用.
1
2string="abcd"
echo ${ #string } #输出str长度4if-else语句写法是
if[xx] then xx else xx fi注意不管有无else, 必须以fi结尾(Unix哲学?)多一个
until循环. 与while相反,语法是until [cmd] do xx down,一般优先使用while/for1
2
3
4
5
6
7
8
9
10
11
12#for-in循环
for i in 1 3 5 7 9 #这里list 可以是字符串,可以是变量
do
echo "value = $i"
...
done
#传统for循环也支持,双括号..无需空格. 这其实是第二代shell,源自C的用法了
for((i=1;i<=10;++i))
do
echo "key = $i"
done
内置变量:
- $0 :表示当前shell的文件名. (注意间接调用/引用的时候慎用)
- $n : 表示运行脚本的时候,传递给函数的第n个参数
- $# : 表示参数的个数 (整个脚本内么?)
- $$ : 表示当前shell进程pid
- $? : 上个命令的退出状态,或函数返回值
- $* : 传递给脚本的所有参数(怎么用的?)
1 | x = $1 |
0x01.重构技巧
1.函数提取
难以置信的是…很多写shell的同学甚至都不适用函数抽取..都是从头写到尾的,造成了极大的修改障碍.传入参数一般也非常随便,<10 的参数个数直接$n 使用, 到时候调用的时候按顺序传入即可..
1 | #1.函数必须先定义才能调用 |
2.shell包含
类似C/PHP/JS的include xx.x . 包含公用的函数和变量可以极大减少重复代码的撰写, 以及修改时候的大幅省时, 在高级语言中这是很正常的事, 但是发现很多写shell的人都习惯重复写脚本…
1 | #这是a.sh的内容 |
补充:
- 被包含的脚本无需执行权限. (原因?)
- 包含脚本前注意检查是否有执行函数
- 包含之后一定要慎用相对路径, 因为此时很可能相对路径并不是你引用脚本的
a.sh的位置,而是选的当前运行的位置.
3.命令执行
因为shell中很多都是在执行命令, 那么重复执行的命令多了也需要加以复用. 常见有两个方式
1 | #两种调用命令赋值给变量的方法 |
0x02. 输入输出
0. 输入参数
shell执行的时候, 很多时候都会带上参数和可选的附加选项(比如文件), 这里很多文章讲的实在太差了, 以至于我觉得本来无比常见的内容, 还是专门写一下为好… 这篇虽然写的很早也有点冗长, 但是质量还行 , 基于这个基础, 我来易懂的讲一下常见的输入参数方式和由来.
在Unix下我们经常使用命令/shell的时候, 通常有这几种方式:
- 单纯的多个参数指定:
command -a -b -c - 组合在一起的参数: 例如
tar xzvf等价于tar -xzvf等价于tar -x -z -v -f(注意, 参数是否带-很多时候并非是可选的, 而是必须的 ) - 组合在一起, 并携带附加参数的:
du -csh ./dirName(这里指定的目标就是附加参数) - 其他参数: 比如
sh test.sh -f filename option1 option2(这里filename是-f的附加参数, 但是后面的选项12就是脚本的额外输入参数)
1 | # 举个例子, 这就是一个组合模式的例子 |
上面的需求, 我们可以通过比较繁琐的去手工解析输入参数$1, $2 …. $n的方式来确定, 但是非常不优雅, 且绑死了顺序, 一改就很容易出错, 常见是这样写的:
1 | # 这是一个最常见的例子, while循环是判断脚本输入参数是否>0, 再进入switch-case |
这样写的好处是非常简单直观, 适合快速写的脚本, 或者参数组合不多的的情况, 但是这样有个很大的弊端是, 命令都需要单独写( 比如 -a, -b, -c) 而不能直接写-abc (要实现得单独写逻辑) , 这导致使用体验比较差. 所以通常需要通过结合getopts 命令来简单实现:
1 | # 先给下常见的shell内调用方式 (命令下自己看) |
但是getopts这里有一些小技巧, 以至于有些人一知半解的情况下, 会误用它… 首先就是, 它跟着的 strs 这里, 是有特殊的地方的, 不是说乱输, 或者用: 做分隔符的..
- 如果
strs是单纯n个字符, 代表接受n个字符的参数识别 (你可以合在一起 , 比如-abfn这样的合写, 但是你不能直接写a裸字符) - 如果
strs字符后面跟了:号. 代表这个字符要求附加参数, 就比如需要指定一个文件, 或者一个紧跟着的字符串 (例如 :-f ./a.txt - 如果
strs有附加参数, 如何读取呢? 用特定的变量XXX来接收.
所以首先, 如果你不会用getopts, 或者需求简单, 建议直接使用手工方式获取, 用的话, 也要注意一下使用方式, 以免有隐患.
1.echo的各种用法
1 | #shell中经常需要发送邮件起到报警作用,使用\n可以实现. 注意前面需要-e开区 |
2.mail相关
最简单的使用就是读取Linux自带的邮件服务器, 使用你默认的user ,发报警邮件. 但是因为mail对附件, 或者在中文字符支持的问题, 加上如果我需要一个表格 ,那么该如何用mail去设计格式呢? 这时候两个考虑:
用
python单独写,包括所有的邮件服务器配置用mail的加强版替代 (目前采用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#安装mutt,默认是没有的
sudo yum install -y mutt
#无需跟网上所说那样在~/.muttrc新建,然后复制配置,默认也是先读取/etc/Muttrc文件.
#它的最一行source了 Muttrc.local就是给用户自定义的.
#编辑用户默认配置. centos在/etc/Muttrc.local 文件,加入可选的如下选项
set charset="utf-8"
set rfc2047_parameters=yes
set envelope_from=yes
set use_from=yes
set from=mailName@xx.com #改为自己需要的发件信箱
set realname="The name U like"
#1. 发送一份普通邮件, -a代表附件(可选)
echo -e "正文\n换行" | mutt -s "Title" xx@xx.com -a xx/a.log
#2. 我想拥有HTML的排版, 或者说表格的样式. 使用-e
echo "正文" | mutt -s "Title" xx@xx.com -e 'set conten_type="text/html"' <
model.html
#3.上述的 model.html内容就可以是一个table 或者其他带css的结构. 之后补充一个模板大家参考注意点
1 | #邮件这里有个神坑,就是中文编码的问题. |
3.输出shell相关的信息
因为shell到了crontab之后可能会有一些不可描述的隐患问题, 所以这时候为了加速定位,我们应该在邮件中输出当前运行的脚本的基本信息 :pid ,path & name . 这里要说的是pathName的问题
1 | #1.pwd可以打印当前路径. |
4. 重定向和2>&1 ? (待续)
这个问题简单解释一下貌似很好理解, 但是实际没有系统的去了解一下整个shell的重定向是很容易写错的, 到处从别人的shell 复制粘贴, 然后给个模棱两可的结论并不靠谱.. 这里来认真看看吧.
首先先得了解重定向本质:
- Linux每个进程都是
init的子进程,bash和每个命令也不例外, 而init会初始化三个特殊FD - Linux中子进程继承父进程的FD, 所以他们也拥有了init的那三个FD
shell开始执行的时候, 默认会打开3个file/fd(文件/文件描述符), 如下:
| Type | FD | Action | File-Handle Path |
|---|---|---|---|
| stdin(标准输入) | 0 | 键盘输入 | /proc/self/fd/0 |
| stdout(标准/正常输出) | 1 | 屏幕输出 | /proc/self/fd/1 |
| err (错误输出) | 2 | 屏幕输出 | /proc/self/fd/2 |
再借鉴一张图对照一下:
这里你再明白到底重定向里的0,1,2 的本质含义了, 写的时候你就知道, 本来是直接输出到屏幕的1/2 , 如果我希望改变它的默认操作 ,比如让它输出到一个文本. 这时就需要去手动> 或 >>改变指向. 来点实例:
1 | #1.假设当前文件夹有一个文件log,执行以下命令.对比一下区别 |
经过上面的例子, 可以知道 > 和 1> 是等同的, 也确切的知道了0,1,2 到底是什么意思, 但是还是不理解它们的组合使用, 稍安勿躁, 先继续看看没有说起的0 (标准输入)..
输入的重定向符号和输出相反 > 和 >> , 大家可能很少看到它, 因为估计很多人都不知道怎么连续输入, 这里说一种方法, 直接敲cat 命令, 会发现它把你的输入自动输出到了屏幕. 那么这样来做:
1 | #进入输入状态,但此时0默认是从键盘获取输入,我们希望改成从文件读取. |
好了, 这样就清楚的了解0,1,2三种重定向的实际作用, 再来看看它们的组合拳, 比如最常见的 2>&1 .为什么小明同学开始想的和输出并不相符? 是因为& 这里代表的是绑定, 而不是and , a>&b 的意思是a采用和b一样的操作. (本质是使a使用b的fd), 来看例子:
1 | #1.同上面的例子.1&2输出到一个文件 |
那小明同学就很奇怪了, & 理解了, 但是不理解为什么不直接采用上面的方式 ls xx >out 2>out的方式 呢? 还直观许多. 这是因为此时两个fd同时都要使用out 文件的管道, (通俗说~~一个坑有两个人想进~~~) :
- 容易导致内容被覆盖/丢失/乱序 (如果系统是多语言, 比如中文, 还容易乱码)
- 这样会两次打开out文件, fd之间抢占导致IO效率比
2>&1低不少.
所以, 你才会在各种地方都看到 2>&1 这种写法, 其实你当然也可以用 1>&2 ,只不过前面需要指定2罢了, 这下应该真正搞清楚整个关系了吧~
至于/dev/null ,它是Linux设计中的黑洞, 如果我不需要任何输出信息(1,2都不要), 那我就可以把上面的指定文件换成/dev/null就行了 ,等于说是丢弃输出. (类似的, 网络里大家经常听到丢包的概念, 那有没有想过**丢掉的包, 到底是去了哪**呢? ~~~下回分解~~)
然后大家还经常能看到nohup 和末尾的 & (后台)执行组合, 关于nohup在之前的Linux命令文章里有提过, 引用一下:
使用
nohup command和直接使用command &的区别是什么? 参考阮一峰-Linux守护进程 ,简单说nohup是为了避免程序停止(no hang up–>不中断 ,本身并没有后台运行功能)
以上就把整个重定向的常见操作和命令串通了一下…… 最后也来一个小问题:
1 | #这行命令输出什么? 假设log存在, log1不存在 |
12.17补充 : 目前发现有个需要特别注意的, 在shell中定义过的重定向, 在外部不会受到改变.
关于重定向的优先级问题, 还需要再深入分析一下源码里shell和bash调用的过程. 才能解释为什么shell里定义过的重定向, 在bash调用都不能被覆盖的情况.
从上面我们可以知道 >&2 等价于 1>&2 , 这行语句本身是一个单纯的echo, 那这句话到底是什么意思呢?
如上所示是使用strace 命令详细跟踪一下整个调用栈, 真是神器… 不过注意屏蔽一下代码, 否则整个调用会很长.
5. 管道
与上面的重定向有点类似使用更多的. 还有个管道 | . 之后探索一下管道的原理和实现, 以及管道和重定向的区别 ,优先级上的差异.
先引用一下网友的总结:
- 管道左右会触发两个子进程, 执行
|两侧命令, 重定向是一个进程内执行- 重定向优先级大于管道
0x03.时间处理
这里主要说的是日期 , 定时 , 和时间戳的处理. 虽然这些是Linux命令, 但是一般都是shell中使用组合.
1.日期处理date: (参考文档 )
1 | #date的使用尽量不要乱加单双引号和特殊符号,使用优雅简短の写法. |
0x04. 文件操作
Linux里操作文件是频率最高的操作, 而且因为需求各不一样, 得多积累, 才能应对各种场景. 我写的少, 很多高级技巧也不太熟, 可能也写的不够优雅, 希望多多指教.
1. 递归的合并多个文件为一个
注意如果是一个文件夹下递归的去合并所有文件为一个, 那一行命令就能搞定了, 这里说的是遍历到每个文件夹, 并单独合并为同名的文件. 用树形表示一下需求:
1 | #某个文件夹内可能有N层子文件夹,最后一层是n个数据文件,需要分别合并为dirName*.txt |
0x0n.补充
1. shell中一些容易踩得坑
1 | #1.注释不能直接写代码后面.shell很多地方都是用空格判断分隔,特别需要注意. |
2.shell变量的生命周期
想起以前Mr.T 说过, 在PL中搞清楚变量/方法/结构体各自的生命周期是一个很重要的问题, 在shell中也是如此, 特别因为它书写随意, 稍有不慎生命周期可能就会出现问题, 这里首先最好遵循从上至下的定义变量.
参考资料:
- IO重定向的原理和实现
- 重定向中dup()的源码实现
- Unix/Linux编程实践之管道和重定向-读书笔记
- Linux/Shell重定向相关
- Unix中的参数获取命令
- ruanyifeng-bash-guide(Recommend)
- 一些blogs…(但是复制粘贴/翻译太严重了,还是致敬给
Linus把, shell这里堪比战国时期…)


