1 基本概念

  • shell是用户和内核之间的桥梁,shell自带的叫内置命令,其它应用程序的命令叫外部命令
  • shell是一种脚本语言,支持基本的编程元素,如数组,变量,字符串,注释,四则运算,逻辑运算,if-else,for,case-in,until等

Hello World:

创建一个叫hello.sh的文件,输入

1
2
#! /bin/bash
echo "Hello World"

保存退出。

运行有两种方法:

  • 一种是让文件开启可执行权限,chmod +x ./hello.sh,然后执行该文件即可:./test.sh,此时系统会根据文件中的第一行注释来选择相应的解释器,在执行文件时一定要指定当前目录`,否则系统只会到环境$PATH中去寻找该文件,导致报错。
  • 另一种是直接运行解释器,这时文件中指定解释器的第一行就无效,bash test.sh

2 变量

  • 定义变量时,等号两边不能有空格
  • 变量名不能使用shell中的关键字
  • 变量名可以使用数字,字母,下划线,但不能以数字开头

定义变量:myname="bash"

使用变量:${myname},花括号帮助解释器识别变量的边界,建议使用,可以避免变量和其它名称连在一起时无法被识别的情况。如echo $mynameis

只读变量:readonly myname

取消只读:只读变量一旦被创建就不能被修改,如果变量可能会被修改,就不该声明为只读,没有常规的命令可以修改只读变量,除非重启shell。但有一些特殊的方法可以删除,如:先下载gdb,再运行gdb --batch-silent --pid=$$ --eval-command='call unbind_variable("mySite")'在wsl中无效,类似方式不应该在生产环境中使用

删除变量:unset myname1 myname2 ...

2.1 字符串

  • 字符串可以用单引号,双引号,或者不用引号

  • 单引号和双引号的区别:

    • 单引号内部不能引用变量,也不能转义
    • 双引号内部可以引用变量,也能使用转义符,如"myname is ${myname}"
  • 字符串连接

    • 分串连接方式:如:"my name is"${myname}"!",这种分式用单引号和双引号都行
    • 引用变量方式:如:"my name is ${myname}"这种方式只能用双引号
  • 字符串长度

    1
    2
    3
    
    ${#myname}
    expr length ${myname}
    
  • 提取子串:

    • 下标从0开始,${myname:1:3}
    • 下标从1开始,expr substr ${myname} 1 3
  • 查找字符位置

    expr index ${myname} hsab返回1,查找hsab中最早出现的字符,下标从1开始

2.2 数组

只有一维数组,且没有大小限制,像一个集合,下标从0开始,可以用不连续的下标。

  • 定义数组

    • array_name=(name1 name2 …)元素之间用空格分开,也可以写成每行一个元素
    • 直接用下标定义,array_name[0]=name1
  • 读取数组

    • 用下标获取单个元素:${array_name[0]}
    • 获取所有元素:${array_name[@]}
  • 数组长度和元素长度

    1
    2
    
    - 数组元素个数:${#array_name[@]}
    - 对应元素的长度:${#array_name[0]}
    

3 基本运算

shell自身不支持浮点数运算,需要借助其它程序,如bc,awk等,下面的内容默认不涉及浮点数。

((...))表达式,读取结果用$((...))格式,表达式支持嵌套,也支持用括号表示优先级。

  • 算术运算:+、-、*、/、%、++、--、**:加、滅、乘、除、取余、自增、自减、指数。i++表示先计算再自增,++i反之。
  • 位运算:<<、>>、&、|、~、^:左移、右移、与、或、非、异或。

    示例:

    1
    2
    3
    4
    5
    6
    
    echo $((8>>2))  //2
    echo $((2<<6))  //128
    echo $((13&6))  //4  1101&0110=>0100
    echo $((13|6))  //15   1101|0110=>1111
    echo $((13^6))  //11   1101^0110=>1011
    echo $((~13))   //-14  这里是对所有位取反,显示补码下的结果,假设用8个二进制位表示,原00001101=>11110010
    
  • 逻辑运算:&&、||、!、<、>、<=、>=、==、!=还支持三元表达式a==1?b:c

  • 赋值运算:可以跟部分算术运算符和位运算符结合,如:=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=、

4 流程控制

4.1 if-else

有elif就必须有else,所有的条件分支都应该有内容,没有内容就去掉该分支。

语法:if list; then list; [ elif list; then list; ] ... [ else list; ] fi;可以用换行代替

list表示shell可以运行的那些指令,包括管道|,连接符&&等组成的多个指令的集合,其中也包含了((expr))[[ condition expr ]]test xx[ condition expr ]等表达式。

条件表达式[ condition expr ]test是等价的,而[[ condition expr ]]则是bash在test基础上添加了一些额外功能表达式(比如可以判断&&组成的两个条件,而[]只能在括号外使用&&),test是内置命令,而[[ ]]属于关键字,它们都用来判断文件相关的条件,如果判断数字,也只能是简单的几种比较[[ 3 -eq 4 ]],通常跟算术有关的计算和判断都应该用((expr))这种形式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (echo "if (list) run succ,show echo")
then echo "run succ"
fi

if ((3<4))
then echo "((3<4)) 3<4 is a true,so true equal to exp's result=0"
fi

if ((3>4))
then echo "((3>4)) 3>4 is false,so this msg will not show up" #不显示
fi

if ((0))
then echo "((0)) 0 means the exp=false,so the ((exp))!=0,so this msg will not show up" #不显示
fi

if ((345))
then echo "((345)) non-zero mean's the exp succ,so ((345))=0,this msg will show up"
fi

if { echo "if {list} run succ,show"; }
then echo "run succ"
fi

if { echo 1; }
then echo "{ echo 1} show 1 as result,but it run succ,so this msg will show up"
fi

if [[ "str"=="str" ]]
then echo "[[ 'str'=='str' ]] is true,so this msg show up"
fi

4.2 for循环

基本语法:for (( expr1 ; expr2 ; expr3 )) ; do list ; done。expr非0表示真,如果任何一个exp被省略,则用1代替。这个循环先计算expr1,然后只要expr2为真(!=0),则执行list和expr3,循环的执行结果为最后一次遍历时的结果,如果任何一个expr出错,为false

for循环还有一种遍历列表的简单形式:for name [ [ in [ word ... ] ] ; ] do list ; done,这里的word可以是一个列表变量,也可以直接传入一系列的值,如1 2 3 4。循环的执行结果为最后一次遍历时的结果,这种方式不会有中断,当word是空集合时,返回0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
numlist=(1 2 3 4 5 6 7)

for((i=0;i<${#numlist[@]};i=i+1))
do echo ${numlist[i]} #输出1-7,每行一个数字
done

for num in ${numlist[@]}
do echo $num #同上输出
done

for num in 1 2 3 4
do echo $num #输出1 2 3 4 每行一个数字
done

4.2 while循环和until循环

语法:while list-1; do list-2; doneuntil list-1; do list-2; done

while循环在list-1正确返回(返回0)时执行list-2,而until循环则反过来,不断执行list-2直到list-1返回正确的结果,until类似其它语言的do-while循环,只是语法更简单。两者的执行结果都是最后一次list-2执行结果,如果循环没有执行,则返回0。

下面两个循环都输出0-4

1
2
3
4
5
6
7
8
9
a=0
while ((a<=4))
do echo $a;((a=$a+1))
done

a=0
until (($a>4))
do echo $a;((a=$a+1))
done

4.3 case….esac语句

语法是:case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac,跟其它语言的switch case类似,注意这里的pattern选择中,左括号是可选的,每个pattern对应的执行list需要以;;结尾,esaccase倒过来写。

1
2
3
4
5
6
echo "请输入1-6之间的数字:"
read a
case $a in
        (1|2|3)echo "one of 1 2 3";;
        4|5|6)echo "one of 4 5 6";; # 这一行没有用左括号
esac

5 函数

语法:name () compound-command [redirection]或者function name [()] compound-command [redirection]function和函数名后的()至少存在一个,redirection表示重定向。

下面4种形式都称为compound-command

  • (list):返回list运行的结果,命令正常运行结束返回0,这个表达式里的list是在subshell中运行
  • ((exp)):返回算术表达式的计算结果,这里当exp结果是非0的时候表示exp为真,整个表达式返回0,其它情况表达式返回1,
  • { list; }:跟(list)类似,注意这里{}需要用空格分开,并且list必须以;或者换行符结尾,这个表达式里的list是在当前shell中运行
  • [[ conexpr ]]:返回条件表达式的结果,成立为0,注意expr两边要用空格分开,条件表达式主要用来判断文件属性等或者字符串是否符合正则表达式,如果是算术比较,则用((exp))就可以。

注意:

  • 函数在调用时就跟其它命令一样,还可以传递参数,直接写在函数名后面就可以,在函数体中用${n}方式访问第n个参数(下标从1开始),如果n<10{}也可以省略。
  • 函数体内可以有local变量限制作用域,否则函数体内定义的变量可以在外部被访问到(前提是函数至少被调用过一次,只声明过的函数体内的变量无效)。
  • [[ conexpr ]]相对应的test[]在这里不属于compound-command
1
2
3
function showParams() { echo ${1};}

showParams "firstParam" "second" # 输出:firtParam

注意:上面的语法中,}前必须有;或者换行结尾

6 重定向

重定向可以改变命令的输入和输出对象。在linux中,有三个标准的输入输出对象:

  1. stdin:标准输入,当我们的应用需要数据的时候,通常默认从stdin接收,文件描述符0,对应/dev/stdin
  2. stdout:标准输出,应用执行的结果,通常输出在stdout,编号1,对应/dev/stdout
  3. stderr:错误信息输出,当应用执行出错的时候,错误信息输出在stderr,编号2,对应/dev/stderr

stdin,stdout,stderr在交互的时候都是显示在shell中的信息,尤其是stdout,stderr,看不出区别,但逻辑上它们分成这3类,重定向操作时区别很大。

还有一些比较特殊的对象,如/dev/null,重定向到这里的内容都会"消失"。

重定向操作在命令执行之前发生。

6.1 stdin重定向

<符号表示输入重定向,比如一些接收用户输入的程序,在我们调用命令后,会等待用户输入,直到EOT,这种情况下可以用< file的方式把stdin重定向到文件file,把file的内容作为输入。()

PS:也有很多命令把重定向的功能整合到使用方式里面,当你在参数中指定文件的时候,就会把该文件用作输入对象。

我们以邮件发送为例,在debian中用mutt发送(centos中可以用mailx等)。

如果不使用重定向,在执行如下命令后,程序会等待我们输入正文的内容:

mutt -s target@example.com

而使用重定向的话,比如我们的正文存放在当前目录下的mailbody文件中,只要执行

mutt -s target@exmaple.com < mailbody

邮件就会把mailbody中的内容作为邮件正文,然后直接发送出去,这在执行脚本的时候通常很有用。

6.2 stdout和stderr重定向

>符号表示输出重定向,比如我们不想看到一些命令的输出结果,或者想把输出结果保存起来,都可以使用输出重定向。

最简单的文式:> file,把stdout重定向到file中,所有正常的输出(不包含错误信息)都会保存到file中。这种情况下,假如file不存在,则创建一个,如果存在,则清空file后再输入(相当于覆盖),而file可以用set -C file的方式保护,避免被覆盖,>| file的方式可以无视set -C的保护。

>> file可以把内容重定向并追加到file文件中,这种方式用来处理日志比较适用。

如果要同时把stdoutstderr重定向,可以用下面3种方式,效果是等价的:

  • &>file (推荐)
  • >&file
  • >file 2>&1

注意,第3种方式中2>&1必须在>file后面,反过来则不行:2>&1 >file,重定向这从左到右执行的,当我们执行>file的时候,前面的stderr并没有受影响,它还是指向原来的stdout,而stdout的结果却重定向到了file中,可以理解为:重定向操作只影响跟它们相关的内容的去向,而本身的stdout,stdin等标准的输入输出对象并不会消失。

如果要同时把stdout,stderr追加到file中,可以用&>>file的方式,其它一些不太常用的命令可以查看bash手册的redirection一节。

7 小结

shell本身的语法很简单,只要了解条件判断和循环控制等就可以看懂脚本,如果编写脚本的话则要根据不同的场景,结合shell自带的命令或者第三方应用指令来完成。