blueyi's notes

Follow Excellence,Success will chase you!

0%

Shell脚本简单入门

Shell脚本简单入门,如果你分不清shell是什么,此文一定不适合你。毕竟只花了十多分钟看的教程,不会涉及细节,细节将在以后的使用中逐渐以示例形式给出。

PS:借来的东西,果然迟早是要还的。刚在linux下学习时,为了批量处理,当时会C语言,就用C语言编个程序实现,心想虽然不会shell,不是也很方便嘛。然后会了C++,发现比C用起来方便多了。再然后越来越的批处理工作需要程序,C++有点太大杀气了,Python这么火,而且也很强大,学了肯定不白学,遂各种自动化的工作就交给了Python。然后最近发现Python这货也有点太大杀所了,我只是想写个简单的自动化指令而已。所以现在来还当年欠下的shell。假如你也如此,还是花上几十分钟,掌握一把小刀,何乐而不为呢。

关于

记得脚本最前面添加shell解释器,通常为sh、bash,其他的不常见,依然先给个HelloWorld

1
2
#!/bin/sh
echo "HelloWorld" #我是注释

然后给了执行权限即可直接执行,或者手动通过sh script.sh来执行,shell脚本后缀通常为.sh。当然Linux下面文件只要有了可执行权限(chmod +x file),什么后缀都没有影响。
我们平时使用的terminal其实就是个shell解释器的交互窗口,所以学习shell编程直接开个terminal就可以开编了。shell中可以直接执行任何linux/unix命令(其实这些命令就是shell内部的)正是它方便之处

变量

shell变量不分类型,默认都是字符串。单引号的字符串类似raw字符串,不能解析转义字符及变量。不被引号引住的字符串中不能用空格。
变量定义形如:变量名=值,等号中间不能有空格。变量名要求即编程语言的常规要求
变量引用则直接在变量名前加$符号,为了限定shell解释器解释变量名的边界可以用大括号包围变量名:${变量名}
ex:

1
2
var="maxwi"
echo "Hello ${var}.com"

字符串

在shell的语句中间执行命令只需要将命令放在”`”(ESC下面那个键)之间即可,当然任何命令都可以在命令前面没有内容时直接执行:

1
2
3
4
5
6
cd $HOME;ls #进入个人目录并执行ls
var=`ls` #ls结果以空格分隔的字符串返回给var
for file in $var # for...in循环输出以空格分隔的字符串内容
do
echo $file
done

多个命令通过分号(;)并入一行执行

只读变量

1
2
3
var=8
readonly var
var=9 #出错

删除变量

1
unset var

拼接字符直接将其放在一起即可

1
2
3
4
5
a=I
b='love'
c="China"
d="$a $b $c"
echo $d

输出:I love China

获取字符串长

1
echo ${#var}

字符串切片

1
echo ${var:2:5}

注意字符串中字符是从0计数,上述切片内容是从第3个字符到第6个字符,包含位置2和5位置上的字符

数组

数组下标从0开始,使用小括号(())定义数组,元素以空格或回车分隔:arr=(val1, val2, val3)

1
2
3
${arr[index]} # 使用下标获取相应元素
echo ${arr[@]} or echo ${arr[*]} # 获取数组中的所有元素
echo ${#arr[@]} # 获取数组长度,当然里面可以是 *

for...in循环可以用于遍历数组中的元素

1
2
3
4
5
6
var=(1 2 3 I 'love' "China")
for v in ${var[@]}
do
str="$str $v"
done
echo $str

输出:

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
2
3
4
5
6
7
8
a=1
b=2
if [ $a -eq $b ]
then
echo "$a -eq $b : a 等于 b"
else
echo "$a -eq $b: a 不等于 b"
fi

布尔运算符

  • ! 取非,如[!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
2
3
4
5
6
a=1
b=2

val=`expr $a + $b`

val=`expr 9 \* $b`

注意乘运算需要有转义符,表达示之间必须用空格隔开

输入、输出控制

输出

echo默认输出会换行,可以使用\c转义实现不换行
可以通过echo直接回显命令执行结果:

1
echo `date`

可以使用linux下类型C语言中printf的格式控制输出命令,详见man printf

输入

通过read读取输入:

1
2
echo 'Input number'
read a b c d

如果输入为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/nullls tes ss.py 1>/dev/null 2>&1ls tes ss.py >/dev/null 2>&1ls 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
2
3
command << delimiter
document
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
2
3
4
5
6
7
8
9
if condition1
then
command1
elif condition2
then
command2
else
commandN
fi

for..in

1
2
3
4
5
6
7
for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done

列表也可以是文件名

for()

1
for((i=1;i<=10;i++));do echo $(expr $i \* 4);done

while

1
2
3
4
while condition
do
command
done

三种死循环:

1
2
3
4
5
6
7
8
9
10
11
while :
do
command
done

while true
do
command
done

for (( ; ; ))

until

1
2
3
4
until condition
do
command
done

条件为真是停止,类似do..while,循环体至少执行一次

case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case 值 in
模式1)
command1
command2
...
commandN
;;
模式2)
command1
command2
...
commandN
;;
esac

每一个格式后面为右括号结束,模式匹配后一直执行,直到;;(与break功能一样)然后执行*)后面的命令,不再匹配 其他选项。

支持break、continue

函数

函数的定义方式为:

1
2
3
4
func() {
command
return val
}

调用直接函数名后跟参数列表即可:

1
func arg1 arg2

参数性质与上面说的命令行参数一样,也有那几个特殊参数
注意参数超过10个时必须使用{}引用,如${10}
函数返回值通过$?

1
2
3
add() {
return $(($1+$2))
}

调用:

1
2
add 3 5
echo $?

Shell多脚本内容共享

当有两个shell脚本文件时,可以通过. filenamesource filename来引用另一个文件中的代码,其实就是同一个shell session之中信息是共享的。

其他相关内容

eval

脚本中需要使用到的命令
用法为eval cmd args
该命令后面跟的所有内容都做为其参数,它会对后面的内容(即cmd args)进行两次扫描,第一次扫描将其中的变量替换为实际值,所以如果args中含有需要在bash中执行的以$开头的内容,应该加上转换符(\),以避免被替换,例如如果cmd为awk,而awk是通过$来进行参数过滤的。第二次扫描时将eval后面的所有内容当作同一个命令组合来执行,相当于你直接在命令行中执行该命令。例如脚本如下:

1
2
3
a=5
cmd="echo $a | cat > tcat.txt"
eval $cmd

如果对cmd的执行不通过eval调用,而是直接$cmd,输出结果为5 | cat > tcat.txt。这显然不是我们想要的,shell将echo后面的所有内容当成了echo参数来执行。这种情况就需要使用eval。

exec

与source功能有点相反,source命令会在当前shell进程的上下文件环境中执行各命令。而exec虽然也不会创建新的进程,但会清除当前shell进程的上下文件内容,并执行相应命令。当然它也可以用来对文件描述符进行操作,上文已经提及。

declare(typeset)

内建命令declare与typeset功能类似,用于为变量指定类型,因为默认所有变量都会被当成字符串,当然数组除外。
用法:

1
2
declare [-aAfFgilnrtux] [-p] [name[=value] ...]
typeset [-aAfFgilnrtux] [-p] [name[=value] ...]

例如:

1
2
3
4
5
6
7
8
9
10
$ a=1
$ b=2
$ echo $a+$b
1+2
$ declare -i a=1
$ declare -i b=2
$ declare -i c
$ c=$a+$b
$ echo $c
3

local

用于在shell函数内部声明该变量为局部变量,只对当前函数或其子进程有效。用法:

1
local [option] [name[=value] ...]

option为declare可以接受的选项。

一不小心这里就会有一个很大的坑,因为默认情况下shell中的变量作用域是全局的,所以你一个for用了i,然后又在另一个函数调用中用了i,那这个i就会被前面的i覆盖,所以尽量在函数中有类似情况的地方用local

用例

输出系统中的所有磁盘

1
2
3
4
5
6
7
disks=`fdisk -l | grep 'Disk /dev/' | grep -oP '/dev/.{3}'`
for disk in $disks
do
echo '**'
echo $disk
echo '**'
done

参考

1.https://www.shellscript.sh/
2.http://ryanstutorials.net/bash-scripting-tutorial/
3.http://www.runoob.com/linux/

Welcome to my other publishing channels