万字总结 | 完全掌握Shell编程
对于Linux平台下的开发者和维护人员来说,Shell编程是必须要掌握的一个知识点。通过Shell脚本能够将十分复杂的操作进行简化,从而大大的提高我们工作效率。
什么是Shell?
实际上,Shell是一个比较宽泛的概念,它可以有多种含义。比如,一个终端或命令行软件我们可以称为Shell,实际上它就是一个应用程序,是人与系统进行交互的一个操作界面;它也是一种程序语言或者命令语言,可以通过它编译一系列的脚本。
作为Shell终端软件来说,它实际上也是属于泛指。之所以这么说是因为Shell终端软件也有多种。不过,基本上所有的Linux和大多数的Mac OS X里默认用的都是Bourne Again Shell,也就是平时我们说的bash。它早在1987年由Brian Fox开发。
除bash之外,还有其他的Shell应用程序:
Shell应用程序
然而,今天我们要学习的实际上是在Shell上执行的脚本语言,所以我们说Shell脚本编程。由于它是一种解释性语言,Shell脚本编程不需要额外的安装编译器,它可以直接用编辑器直接编辑,然后直接在Shell上直接运行即可。通常,我们在编写脚本时,在第一行需要用#!来指定解释器来运行Shell脚本,比如,#!/bin/sh。
下面我们将为大家从如下几个方面全面系统的为大家梳理Shell编程的相关知识点。
Shell编程
输入输出
shell中有两种输出命令:echo和printf。学习程序,生硬的文字始终没有代码来的直接易懂。我们就直接通过例子来学习吧。
echo 'hello world'printf '%s %s' 'hello' 'world'printf '!!!\n'
output
从上面的例子很容易发现,echo命令默认带有换行符的,而printf则不是。与C语言中类似,printf是可以进行格式化输出的,其中,%s就是格式控制符,还有%c %d %f等控制符。另外,还可以通过在控制符中添加数字来制定字符的长度,比如,%3s表示三个字符长度;%4.3f表示4位整数,3位小数。
#!/bin/shprintf '%6s %3s %4s %5s\n' 公司名 评级 销售额 市场占比printf '%6s %3c %4d %5f\n' 公司A A 5001 0.5001printf '%6s %3c %4d %5f\n' 公司B C 1999 0.1999printf '%6s %3c %4d %5f\n' 公司B B 3000 0.3
printf
如果需要字符对齐,还可以使用-进行字符左对齐,省略不加默认右对齐。
在shell中是使用read命令作为输入,它可以接受标准键盘的输入;除此之外,也可以作为文件描述符的输入,对文件进行操作。
read命名的格式如下:
read [选项名] [变量名]
read命令在执行时,会将输入的数据放到变量中,通常,我们会指定一个自定义的变量,如果不指定变量的话,则就会把输入的数据保存到REPLY变量中。关于变量的一些具体信息可以在下面的变量章节了解。
read命令的选项名有下面几种参数,我们可以选择一种或几种进行设置。
- -p:设置提示信息,屏幕会输出提示信息;
- -t:设置等待时间(秒),等待用户输入的时间;
- -n:设置接收指定的字符数;
- -s:隐藏输入的数据,用于比较隐私机密信息的输入。
具体的使用方法大家可以参考下面的例子:
#!/bin/shread -p 'input a name:' nameread -p 'input a password:' -s passwdecho ''echo $nameecho $passwd
read
注释
每种语言都少不了注释,对于Shell也是一样。一个好的注释可以让代码更容易阅读和维护。shell脚本里也可以使用两种注释:单行注释和多行注释。单行注释可以直接在所在行使用#,多行注释就需要:<<!和!。
# 这是一行内容# 这是一行内容:<<!这是一行内容这是一行内容!
对于多行注释除了用!符号外,还可以用EOF '等符号。
除了这两种方法之外,还可以通过条件语句来实现。
变量
Shell可以定义变量,并通过=给变量赋值,与其他语言不同的是,在=和变量及被赋的值之间不能有空格。习惯了其他语言的同学可能会有些不适应,不过要注意这一点。
对于命名的规则,其实是与其他语言是类似的:
- 使用英文字母,数字和下划线,但不能以数字开头。
- shell的保留关键字不可以使用
v1=1234 #正确v2='hello' #正确v3_1='world' #正确v4_1 = 'world' #错误,'=’符前后不能有空格
除此之外,在访问变量时需要在变量前$符来访问,如果需要区分变量的边界,还需要在变量前后加上{}用来区分变量名的边界,建议在使用变量时加上{}。
a='hell world:'b='一个敲代码的厨子'echo ${a}${b}
Shell的数据类型比较简单,变量的默认数据类型是字符串类型。我们可以使用单引号或双引号,因此,也是可以不用引号的。
我们再来看一个例子:
a=1b=2echo ${a} ${b}
大家觉得最后应该输出多少呢?
答案是不是超出了大家的预料?这样大家应该理解了为什么说Shell定义变量时默认是字符串类型。
那么问题来了,该怎么表示数字呢?其实,这里我们稍微进行特殊处理一下就可以了,在数据运算的时候我们用$[运算表达式]形式就可以了。
a=1b=2echo $[${a} ${b}]
a b
除了这种方法,还有其他的方法可以进行数据运算,我们在后面的数据运算章节在详细展开,我们接着说变量。
我们可以将变量分成局部变量和环境变量:
- 局部变量:是在Shell命令或脚本中定义的变量,只能在当前Shell命令或脚本中有效。
- 环境变量:创建它们的shell及其派生出来的任意子进程中使用等。它可以保证一些程序的正常运行。
在一些特殊的场景,我们不希望我们定义的变量数值被改变,这时,我们可以使用readonly命令将变量设置成只读。
a='hello world'readonly aecho ${a}a='hahah'
还有一些场景需要清除一个变量,我们可以使用unset命令将变量删除,需要注意的是对于只读变量是不能删除的。
a='hello world'b='一个敲代码的厨子'readonly bunset aunset becho ${a}echo ${b}
字符串变量操作
了解了上面变量的内容之后,我们知道变量模式是字符串类型的。那字符串的操作有哪些呢?
一般我们可以会对字符串变量进行如下操作:
- 获取字符串的长度:可以变量前加上#符号,${#变量};
- 截取字符串:可以在变量后使用:截取,${变量:x:y};
- 替换字符串中子字符串:可以在变量后使用/符号,${变量/子字符串/替换的字符串};
- 删除字符串中的子字符串:实际上可以通过替换子字符串的方法实现;
- 字符串大小写替换:可以使用^^转换成大写,使用,,转换成小写;
下面,我们可以从下面的例子更直观的了解这些操作。
a='Hello World'echo '${#a}' #获取字符串长度echo '${a:6:3}' #从下标6开始截取3字符echo '${a/ll/hh}' #将字符串中的ll替换为hhecho '${a/or/}' #删除子字符串orecho '${a^^}' #全部转化成大写echo '${a,,}' #全部转化成小写
字符串操作
数字运算
在上面的变量章节,我们学习到了可以使用$[运算表达式]形式,使数字变量进行运算。这一章节,我们将会详细了解数字运算。实际上Shell可以使用命令和运算表达式的方式进行数字运算,它们可以支持 - * / %等算术运算。
命令方式主要有let declare expr等命令,下面我们一一通过例子来学习他们的使用。
通过let命令进行数字运算,let命名后直接跟上运算表达式即可。
#!/bin/sha='4'b='2'let c1=${a} ${b}let c2=${a}-${b}let c3=${a}*${b}let c4=${a}/${b}let c5=${a}%${b}echo 'a b =' ${c1}echo 'a - b =' ${c2}echo 'a * b =' ${c3}echo 'a / b =' ${c4}echo 'a % b =' ${c5}
let命令
通过expr命令进行数字运算,可以查看下面的示例代码,但是需要注意两点:
- 运算符和两边的变量需要使用空格隔开;
- 需要注意需要将*转义为\*。
#!/bin/sha=4b=2c1=$(expr ${a} ${b})echo 'a b =' ${c1}c2=$(expr ${a} - ${b})echo 'a - b =' ${c2}c3=$(expr ${a} \* ${b})echo 'a * b =' ${c3}c4=$(expr ${a} / ${b})echo 'a / b =' ${c4}c5=$(expr ${a} % ${b})echo 'a % b =' ${c5}
expr
declare命令也可以进行数字运算,它的参数选项中有一个-i选项,它可以将变量声明为整数型,因此,我们也可以通过declare实现数字的运算。
#!/bin/sha='4'b='2'declare -i c1=${a} ${b}declare -i c2=${a}-${b}declare -i c3=${a}*${b}declare -i c4=${a}/${b}declare -i c5=${a}%${b}echo 'a b =' ${c1}echo 'a - b =' ${c2}echo 'a * b =' ${c3}echo 'a / b =' ${c4}echo 'a % b =' ${c5}
declare命令 declare命令
通过运算表达式实现数字运算的方式,主要有$((运算表达式))和$[运算表达式],我们依次来看看他们的使用方法。
#!/bin/sha=4b=2c1=$((${a} ${b}))c2=$((${a}-${b}))c3=$((${a}*${b}))c4=$((${a}/${b}))c5=$((${a}%${b}))echo 'a b =' ${c1}echo 'a - b =' ${c2}echo 'a * b =' ${c3}echo 'a / b =' ${c4}echo 'a % b =' ${c5}
表达式方式1
#!/bin/sha=4b=2c1=$[${a} ${b}]c2=$[${a}-${b}]c3=$[${a}*${b}]c4=$[${a}/${b}]c5=$[${a}%${b}]echo 'a b =' ${c1}echo 'a - b =' ${c2}echo 'a * b =' ${c3}echo 'a / b =' ${c4}echo 'a % b =' ${c5}
表达式方式2
除了这些运算之外,shell也支持自增和自减运算,这里以let命令为例:
#!/bin/shc1=2c2=2c3=2let c1 let c2--let c3 =1echo 'c1 =' ${c1}echo 'c2 =' ${c2}echo 'c3 =' ${c3}
自增和自减
上面的这些运算方法都有各自的要求,在使用的时候我们一定要清楚它们的使用方法。另外,在默认情况下shell是不支持小数运算,大家可以发现上面的运算都是整数运算,怎么进行小数运算呢?
我们可以借助Linux平台下的bc计算器进行小数运算。
#!/bin/sha=1.411b=1.332c1=`echo '$a $b'|bc`c2=`echo '$a-$b'|bc`c3=`echo '$a*$b'|bc`c4=`echo 'scale=3;$a/$b'|bc` #scale用来指定小数的位数echo $c1echo $c2echo $c3echo $c4
小数运算
数组
Shell可以定义数组用来存放多个数据,格式如下,数组中的各个元素需要使用空格隔开,数组在定义时可以不用指定数组的大小。
array=(value1 value2 value3 ...)
在访问数组时,可以使用中括号和下标([下标])访问各个元素,它的下标也是从0开始的。
#!/bin/sha=(hello world code)echo ${a[0]}echo ${a[1]}echo ${a[2]}
数组访问
除了用下标访问单个元素之外,是否有其他方法获取所有元素呢?我们可以使用*和@符号获取数组的所有元素。这两个符号是不是很熟悉?我们在字符串变量操作章节用到过这两个符号。
#!/bin/sha=(hello world code)echo ${a[*]}echo ${a[@]}
获取数组所有元素
同样的,与获取字符变量长度类似,我们也可以使用#符号来获取数组的长度。
#!/bin/sha=(hello world code)echo ${#a[*]}
获取数组长度
关系运算
关系运算也就是比较运算,因为在shell里都是字符串类型,我们怎么比较数字的大小呢?shell中专门提供了一些专门用来关系运算的运算符。如下:
- -eq:可以判断两个数是否相等,相等则为ture,格式为[ $a -eq $b ];
- -ne:可以判断两个数是否不相等,不相等则为true,格式为[ $a -ne $b ];
- -gt:可以左边的数是否大于右边的,如果是则为true,格式为[ $a -gt $b ];
- -lt:可以判断左边的数是否小于右边的,如果是则为true,格式为[ $a -lt $b ];
- -ge:可以判断左边的数是否大于等于右边的,如果是则为true,格式为[ $a -ge $b ];
- -le:可以判断左边的数是否小于等于右边的,如果是则为true,格式为[ $a -le $b ]。
这里要注意变量和中括号两边是有空格隔开,运算符和变量之间也有空格隔开,具体我们可以通过一个例子来进一步了解它们的使用。
#!/bin/sha=1b=2if [ $a -eq $b ];then echo 'yes'else echo 'no'fiif [ $a -ne $b ];then echo 'yes'else echo 'no'fiif [ $a -gt $b ];then echo 'yes'else echo 'no'fiif [ $a -lt $b ];then echo 'yes'else echo 'no'fi
这里用到了判断语句if...else,详细内容可以在条件语句章节再深入了解。
关系运算
上面的是数值关系运算,当然,对于字符串同样也有类似的元素符。
- =:判断两个字符串是否相等,相等则为真,[ $a = $b ] ;
- != 判断两个字符串是否不相等,不相则为真,[ $a != $b ];
- -z 判断字符串长度是否为0,如果是则为真,[ -z $a ];
- -n 判断字符串长度是否不为0,如果是则为真,[ -n '$a' ];
- $ 判断字符串是否为空,如果不为空则为真,[ $a ];
#!/bin/sha='hello'b='hello'c='world'd=''echo 'a = b ?'if [ $a = $b ];then echo 'yes'else echo 'no'fiecho 'a != b ?'if [ $a != $b ];then echo 'yes'else echo 'no'fiecho 'a != c ?'if [ $a != $c ];then echo 'yes'else echo 'no'fiecho 'len(c) = 0 ?'if [ -z $c ];then echo 'yes'else echo 'no'fiecho 'len(d) != 0 ?'if [ -n '$d' ];then echo 'yes'else echo 'no'fiecho 'len(a) = 0 ?'if [ $a ];then echo 'yes'else echo 'no'fi
字符串
逻辑运算
除了我们上面介绍的算术运算和关系运算,Shell还有逻辑运算。逻辑运算主要有逻辑与和逻辑或运算。
- 逻辑与运算使用&&表示,运算符两边都为真则结果为真;
- 逻辑或运算使用||表示,运算符两边只要有一个为真则结果为真。
我们通过一个例子来对逻辑运算进一步了解。
#!/bin/sha=1b=2c=1echo 'a = c && a != b ?'if [[ $a -eq $c && $a -ne $b ]];then echo 'yes'else echo 'no'fiecho 'a = b || a = c ?'if [[ $a -eq $b || $a -eq $c ]];then echo 'yes'else echo 'no'fi
逻辑运算
条件语句
与其他语言一样,shell脚本编程也可以进行流程控制,比如,条件语句,循环语句等,这一章节我们学习条件语句。
条件语句中主要通过if else then elif fi等关键字组成,主要可以组成下面几种情况:
- 单分支
- 双分支
- 多分支:多个条件,多个执行分支
单分支这种情况,结构比较简单,一个条件一个执行分支。
a=1if [ $a == 1 ];then echo 'a = 1'fi
单分支
双分支的情况,比单分支多一个执行分治。
a=1if [ $a == 2 ];then echo 'a = 1'else echo 'a != 1'fi
双分支
多分支结构比较适合多种条件,多个执行分支的情况。
a=2if [ $a == 1 ];then echo 'a = 1'elif [ $a == 2 ];then echo 'a = 2'else echo $afi
多分支
对于if的分支语句大家要注意格式问题,在[]里的条件表达式一定要和两边中括号符号用空格隔开。
此外,还有一种多分支语句case语句,格式为:
case $变量 in 'value1') 执行语句1 ;; 'value2') 执行语句2 ;; *) 执行其他语句 ;;esac
我们看一个case语句的示例:
#!/bin/shread -p 'please in put a number:' numcase $num in'1') echo 'Start 1th flow' ;;'2') echo 'Start 2th flow' ;;*) echo 'Do nothing'esac
case语句
上面的这些只是一些简单的语句结构,大家只要掌握了这几种分支语句的用法,就可以组成更加复杂的分支语句,比如,多个判断条件,多个分支嵌套等。
循环语句
用于流程控制的另一种方式是循环语句,Shell中常见有for while until select这四种循环语句。下面我们依次来了解这四种循环方式。
for循环for(())和for...in这两种形式,我们可以根据自己的需要进行选择。
先来看看for(())这种循环格式:
for((ex1;exp2;exp3))do 循环体done
这里我们可以结合的数组的知识来举个循环的栗子。
a=('hello' 'world' 'hello' 'shell')for((i=0;i<4;i ))do echo ${a[i]}done
for循环
我们通过定义一个递增变量i,依次访问数组各个元素。有没有更简单的变量方法呢?我们再看下面一个例子。
a=('hello' 'world' 'hello' 'shell')for v in ${a[*]}do echo $vdone
for...in
上面的例子用的是第二种for...in的循环格式,其格式如下,它可以方便的遍历一个列表或数组,而不需再定义递增/递减变量。
for var in listdo 循环体done
接下来我们看while循环,while循环的格式如下,
while [ 条件表达式 ]do 循环体done
再看一个例子:
a=('hello' 'world' 'hello' 'shell')i=0while [ $i -lt 4 ]do echo ${a[i]} let i done
while
while循环需要注意条件表达的写法,相信看了上面的关于条件语句的同学已经很清楚了。
在shell中有一个与while循环恰好相反的循环until循环;在while循环中条件表达式成立就会进入循环体,而在until循环中条件表达式不成立才会进入循环,until循环的格式如下:
until [ 条件表达式 ]do 循环体done
我们将上面while循环例子的条件表达式稍加改动。
a=('hello' 'world' 'hello' 'shell')i=0until [ $i -ge 4 ]do echo ${a[i]} let i done
until
对于until循环语句来说,一般没有上面的几种循环语句较为常用。
最后,还有一种较为特殊的循环select,我们先看一下它的格式:
select var in listdo statementsdone
为什么说它是一种特殊的循环?我们看下面这个例子:
a=('hello' 'world' 'hello' 'shell')i=0select v in ${a[*]}do echo $vdone
select
从这里例子上,我们可以发现在每行打印前面都有一个序号,我们还可以选择其中一个序号,会输出对应需要的结果。它是shell中特有的一种结构,通常和case...in语句一起使用,通过根据选择的序号不同,可以选择执行case...in里不同的动作。
a=('公司A' '公司B' '公司C' '公司D')select v in ${a[*]}do case $v in '公司A') echo '恭喜您,你选择了公司A !' break ;; '公司B') echo '恭喜您,你选择了公司B !' break ;; '公司C') echo '恭喜您,你选择了公司C !' break ;; '公司D') echo '恭喜您,你选择了公司D !' break ;; *) echo '您没有选择任何一个公司,请重新选择!' esacdone
select in 和 case in
函数
在一个复杂的功能的脚本程序中,会有很多重复或相似的功能,为了避免大量的重复代码,这个时候函数的作用体现出来了。一般的我们会将一些相似的或重复的操作抽象成一个函数,并根据参数的不同返回相应的结果。这样程序将更具模块化,逻辑也更加清楚,便于开发和维护。
当然,在shell中也可以使用函数。其格式如下:
function name() { # 函数体 return value}
这里function是用来定义函数的关键字,name是需要我们自定义的函数名,return value是函数的返回值。现在我们来定义一个函数:
function test1() { echo 'this is a function' return 0}
该怎么调用函数呢?直接写函数名即可,我们看一个完整版程序:
function test1() { echo 'this is a function' return 0}test1 #调用函数
函数1
上面函数没有带参数的,那如果有参数怎么办呢?我们继续看例子:
function test2() { echo 'parameter1 : $1' echo 'parameter2 : $2' return 3}test2 1 2 #调用函数echo $? #获取函数返回值
函数2
实际上,函数中是不要定义形参的,在调用时在函数后面加上实参就可以了。而函数体中可以通过$加参数编号访问参数,比如,第一个参数$1,到第十个参数以上就需要加上{}符号,比如${10},而函数返回值需要在调用该函数后通过$?获得。
文件包含
有的时候某个功能可能被多个脚本使用,这时就需要在将这个功能放到一个公共的文件中,需要这个功能的脚本直接包含这个公共的文件即可,这就是文件的包含,同样的Shell也是支持文件包含的。
在Shell中可以使用.或source加上文件名来包含文件。直接来看例子:
先建一个文件test_one.sh:
#test_one.shvar1=1
再建一个文件test_two.sh:
#test_two.shvar2=2
下面我们在建一个文件包含test_one.sh和test_two.sh这两个文件。
. ./test_one.shsource ./test_two.shecho 'file one var1=${var1}'echo 'file two var2=${var2}'
文件包含
这里需要注意.和./test_one.sh文件之间是有一个空格。
最后
至此,我们已经学会了shell编程的一些基本知识。文中使用的都是一些简单的例子,在实际的shell脚本中往往都是比较复杂的逻辑。不过,再复杂的代码也是有这些简单的结构组成。因此,大家一定要有一个扎实基础和掌握一个完整的shell知识体系。