Shell脚本简单入门,如果你分不清shell是什么,此文一定不适合你。毕竟只花了十多分钟看的教程,不会涉及细节,细节将在以后的使用中逐渐以示例形式给出。
PS:借来的东西,果然迟早是要还的。刚在linux下学习时,为了批量处理,当时会C语言,就用C语言编个程序实现,心想虽然不会shell,不是也很方便嘛。然后会了C++,发现比C用起来方便多了。再然后越来越的批处理工作需要程序,C++有点太大杀气了,Python这么火,而且也很强大,学了肯定不白学,遂各种自动化的工作就交给了Python。然后最近发现Python这货也有点太大杀所了,我只是想写个简单的自动化指令而已。所以现在来还当年欠下的shell。假如你也如此,还是花上几十分钟,掌握一把小刀,何乐而不为呢。
关于
记得脚本最前面添加shell解释器,通常为sh、bash,其他的不常见,依然先给个HelloWorld
1 | !/bin/sh |
然后给了执行权限即可直接执行,或者手动通过sh script.sh
来执行,shell脚本后缀通常为.sh
。当然Linux下面文件只要有了可执行权限(chmod +x file),什么后缀都没有影响。
我们平时使用的terminal其实就是个shell解释器的交互窗口,所以学习shell编程直接开个terminal就可以开编了。shell中可以直接执行任何linux/unix命令(其实这些命令就是shell内部的)正是它方便之处
变量
shell变量不分类型,默认都是字符串。单引号的字符串类似raw字符串,不能解析转义字符及变量。不被引号引住的字符串中不能用空格。
变量定义形如:变量名=值,等号中间不能有空格。变量名要求即编程语言的常规要求
变量引用则直接在变量名前加$
符号,为了限定shell解释器解释变量名的边界可以用大括号包围变量名:${变量名}
ex:
1 | var="maxwi" |
字符串
在shell的语句中间执行命令只需要将命令放在”`”(ESC下面那个键)之间即可,当然任何命令都可以在命令前面没有内容时直接执行:
1 | cd $HOME;ls #进入个人目录并执行ls |
多个命令通过分号(;)并入一行执行
只读变量
1 | var=8 |
删除变量
1 | unset var |
拼接字符直接将其放在一起即可
1 | a=I |
输出:I love China
获取字符串长
1 | echo ${#var} |
字符串切片
1 | echo ${var:2:5} |
注意字符串中字符是从0计数,上述切片内容是从第3个字符到第6个字符,包含位置2和5位置上的字符
数组
数组下标从0开始,使用小括号(()
)定义数组,元素以空格或回车分隔:arr=(val1, val2, val3)
1 | {arr[index]} # 使用下标获取相应元素 |
for...in
循环可以用于遍历数组中的元素
1 | var=(1 2 3 I 'love' "China") |
输出:
1 | 1 2 3 I love China |
命令行参数
通过$n
来访问传递给shell脚本的命令行参数,$0
为脚本本身,下面为几个特殊的参数:
参数 | 功能 |
---|---|
$# | 传递到脚本的参数个数 |
$* | 以一个单字符串显示所有向脚本传递的参数。如”$*”用「”」括起来的情况、以”$1 $2 … $n”的形式输出所有参数。即无法用for…in遍历每一个元素 |
$$ | 脚本运行的当前进程ID号 |
$! | 后台运行的最后一个进程的ID号 |
$@ | 与$*相同,但是使用时加引号,并在引号中返回每个参数。 如”$@”用「”」括起来的情况、以”$1” “$2” … “$n” 的形式输出所有参数。 |
$- | 显示Shell使用的当前选项,与set命令功能相同。 |
$? | 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。” |
运算符
数字关系运算符
该运算符只支持数字:
运算符 | 说明 | 举例 |
---|---|---|
-eq | 检测两个数是否相等,相等返回 true。 | [ $a -eq $b ] 返回 false。 |
-ne | 检测两个数是否相等,不相等返回 true。 | [ $a -ne $b ] 返回 true。 |
-gt | 检测左边的数是否大于右边的,如果是,则返回 true。 | [ $a -gt $b ] 返回 false。 |
-lt | 检测左边的数是否小于右边的,如果是,则返回 true。 | [ $a -lt $b ] 返回 true。 |
-ge | 检测左边的数是否大于等于右边的,如果是,则返回 true。 | [ $a -ge $b ] 返回 false。 |
-le | 检测左边的数是否小于等于右边的,如果是,则返回 true。 | [ $a -le $b ] 返回 true。 |
也可以使用==
和!=
注意使用时必须使用方括号括住
eg:
1 | a=1 |
布尔运算符
!
取非,如[!false]返回true-o
或运算-a
与运算
逻辑运行符
&&
逻辑与||
逻辑或
字符串比较运算符
运算符 | 说明 | 举例 |
---|---|---|
= | 检测两个字符串是否相等,相等返回 true。 | [ $a = $b ] 返回 false。 |
!= | 检测两个字符串是否相等,不相等返回 true。 | [ $a != $b ] 返回 true。 |
-z | 检测字符串长度是否为0,为0返回 true。 | [ -z $a ] 返回 false。 |
-n | 检测字符串长度是否为0,不为0返回 true。 | [ -n $a ] 返回 true。 |
str | 检测字符串是否为空,不为空返回 true。 | [ $a ] 返回 true。 |
文件测试运算符
常用的几个
[-f file]
检测文件是否为普通文件,既不能是目录也不能是设备[-d file]
检测是否为目录[-s file]
检测文件是否为空[-e file]
检测文件是否存在[-r file]
检测文件是否可读[-w file]
检测文件是否可写[-x file]
检测文件是否可执行
通过expr实现的算术运行符
expr命令可以执行算术表达式并返回结果
如:
1 | a=1 |
注意乘运算需要有转义符,表达示之间必须用空格隔开
输入、输出控制
输出
echo
默认输出会换行,可以使用\c
转义实现不换行
可以通过echo
直接回显命令执行结果:
1 | echo `date` |
可以使用linux下类型C语言中printf的格式控制输出命令,详见man printf
输入
通过read
读取输入:
1 | echo 'Input number' |
如果输入为1 2 3 4 5 6
,则a,b,c分别为1,2,3,但d是4 5 6
文件重定向
shell的重定向功能非常强大,重定向命令列表如下:
| 命令 | 说明 |
|—|—|—| |
| command > file
|将标准输出重定向到file。 |
| command < file
|将标准输入重定向到file。 |
| command >> file
| 将输出以追加的方式重定向到 file。 |
| n>file
| 将文件描述符为n的文件重定向到 file。注意不能有空格,下同 |
| n>>file
| 将文件描述符为 n 的文件以追加的方式重定向到 file。 |
| n>&m
| 将输出流n合并到m。可以理解为使用&n来引用名为n的文件描述符,即将文件描述符n重定向到文件描述符m |
| n<&m
| 将输入流m合并到n。 |
| << tag
| 将开始标记 tag 和结束标记 tag 之间的内容作为输入。
文件描述符0通常是标准输入(stdin),1是标准输出(stdout),2是标准错误输出(stderr),/dev/null为黑洞文件,所有写入到它的内容都会被丢弃
,如果不指定数字,则默认为输出为1,输入为0
查看系统文件描述符路径:ls /proc/self/fd/ -l
几个特殊的用法:n>&-
n>&-
表示关闭输出文件描述符n,如1>&-
或>&-
表示关闭标准输出;类似的n<&-
表示关闭输入文件描述符n,如0<&-
或<&-
表示关闭标准输出。
|&
|&
等价于2>&1
,即将标准错误合并到标准输出并作为管道的标准输入,输出结果中标准错误将在标准输出的前面,用法为cmd1 |& cmd2,cmd2将以cmd1的输出和错误输出作为输入。
&>/dev/null
&>/dev/null
等价于>/dev/null 2>&1
,即将标准输出和错误输出重定向到null,什么也不会输出
>/dev/null
等价于1>/dev/null
,即将标准输出重定向到null,只会输出标准错误
举例:
1.cat ss.py > cat_test.txt
,获取文件ss.py中的内容,并将其重定向到文件cat_test.txt,由于没有指定文件描述符,所以默认为将cat输出到标准输出中的内容重定向到文件。其等价于cat ss.py 1>cat_test.txt
。
2.ls tes ss.py 1>out.txt 2>err.txt
表示将标准输出输出到文件out.txt,标准错误输出到文件err.txt
3.ls tes ss.py > oe.txt 2>&1
表示将标准错误输出重定向到标准输出,并将标准输出重定向到文件oe.txt。
4.ls tes ss.py 1>&- 2>&-
关闭标准输出和错误输出,效果等价于以下几个命令ls tes ss.py 1>/dev/null 2>/dev/null
,ls tes ss.py 1>/dev/null 2>&1
,ls tes ss.py >/dev/null 2>&1
, ls tes ss.py &>/dev/null
5.cat > t.txt
将标准输入重定向到t.txt,输入内容之后,通过ctrl+d发送文件结束符停止输入。
6.cat > t.txt < ss.py
从ss.py读取数据,并重定向到t.txt
Here Document是shell用于将delimiter之间的内容重定向到命令的特殊重定向:
1 | command << delimiter |
注意,第二个delimiter
必须顶格写,这样其之间的document内容将都会传递给command
修改当前shell session下的所有命令重定向exec 1>out.txt
,执行之后会将当前shell窗口下执行的所有命令的标准输出内容重定向到文件out.txt,而在命令行窗口中不会输出任何内容。可以通过关闭当前shell终端并重新开一个来解决。也可以通过提前将标准输出绑定到一个新的文件描述符,重定向到文件之后,再将其绑回来的方式解决。
如:exec 6>&1
,将6绑定到标准输出1,可以通过ls /proc/self/fd/ -l
查看。exec 1>out.txt
,进行一些操作,发现没有输出,实际上结果都在out.txt中exec 1>&6
,将1绑定到文件描述符6,其实就是最初是标准输出exec 6>&-
现关闭文件描述符6即可
控制流
if
一个完整的if控制流程:
1 | if condition1 |
for..in
1 | for var in item1 item2 ... itemN |
列表也可以是文件名
for()
1 | for((i=1;i<=10;i++));do echo $(expr $i \* 4);done |
while
1 | while condition |
三种死循环:
1 | while : |
until
1 | until condition |
条件为真是停止,类似do..while,循环体至少执行一次
case
1 | case 值 in |
每一个格式后面为右括号结束,模式匹配后一直执行,直到;;
(与break功能一样)然后执行*)
后面的命令,不再匹配 其他选项。
支持break、continue
函数
函数的定义方式为:
1 | func() { |
调用直接函数名后跟参数列表即可:
1 | func arg1 arg2 |
参数性质与上面说的命令行参数一样,也有那几个特殊参数
注意参数超过10个时必须使用{}
引用,如${10}
函数返回值通过$?
1 | add() { |
调用:
1 | add 3 5 |
Shell多脚本内容共享
当有两个shell脚本文件时,可以通过. filename
或source filename
来引用另一个文件中的代码,其实就是同一个shell session之中信息是共享的。
其他相关内容
eval
脚本中需要使用到的命令
用法为eval cmd args
该命令后面跟的所有内容都做为其参数,它会对后面的内容(即cmd args)进行两次扫描,第一次扫描将其中的变量替换为实际值,所以如果args中含有需要在bash中执行的以$开头的内容,应该加上转换符(\
),以避免被替换,例如如果cmd为awk,而awk是通过$来进行参数过滤的。第二次扫描时将eval后面的所有内容当作同一个命令组合来执行,相当于你直接在命令行中执行该命令。例如脚本如下:
1 | a=5 |
如果对cmd的执行不通过eval调用,而是直接$cmd
,输出结果为5 | cat > tcat.txt
。这显然不是我们想要的,shell将echo后面的所有内容当成了echo参数来执行。这种情况就需要使用eval。
exec
与source功能有点相反,source命令会在当前shell进程的上下文件环境中执行各命令。而exec虽然也不会创建新的进程,但会清除当前shell进程的上下文件内容,并执行相应命令。当然它也可以用来对文件描述符进行操作,上文已经提及。
declare(typeset)
内建命令declare与typeset功能类似,用于为变量指定类型,因为默认所有变量都会被当成字符串,当然数组除外。
用法:
1 | declare [-aAfFgilnrtux] [-p] [name[=value] ...] |
例如:
1 | a=1 |
local
用于在shell函数内部声明该变量为局部变量,只对当前函数或其子进程有效。用法:
1 | local [option] [name[=value] ...] |
option为declare可以接受的选项。
一不小心这里就会有一个很大的坑,因为默认情况下shell中的变量作用域是全局的,所以你一个for用了i,然后又在另一个函数调用中用了i,那这个i就会被前面的i覆盖,所以尽量在函数中有类似情况的地方用local
用例
输出系统中的所有磁盘
1 | disks=`fdisk -l | grep 'Disk /dev/' | grep -oP '/dev/.{3}'` |
参考
1.https://www.shellscript.sh/
2.http://ryanstutorials.net/bash-scripting-tutorial/
3.http://www.runoob.com/linux/