这个文档用于记录自己在学习shell编程时的一些心得,还有一个示例以及对其的解读,以便日后的查阅。
文件基本结构
一个完整的shell文件中可能有的部分为:
- #!/bin/sh,第一行的这个命令告诉系统应该用哪个程序来运行这个shell文件。
- 代码,shell程序将要执行的代码部分
- 注释,注释以#开头,#的后面皆为注释内容
shell中的变量
在shell中定义变量的格式为:
var1="Hello"
var2=13
代码第一行定义了一个var1
变量,并将其赋值为Hello
,第二行则定义了一个var2
变量,定义为13
。
注意:在代码中=
两边不能加空格,(像这样var1 = Hello
),这样会使shell认为var1
是一个命令,而不是你定义的变量的名字
如果要使用变量,使用下面的语法:
echo $var1
echo
是一个命令,他会将后面的内容输出到终端上。$
表示取值,这行命令的含义是将变量var1中的值输出到终端上,如果不加$
(echo var1
),则表示将字符串var1
输出到终端上。
在表示一个字符串的时候,我们通常用""
或''
将字符串包围起来,而单引号跟双引号是有区别的。下面的程序展示了这种区别:
var="Hello"
echo "$var"
echo '$var'
# 输出为
Hello
$var
使用双引号时,碰到$
时,会将其作为一个取值符号,并对其进行处理,将变量中的值取出来。而使用单引号时,只是将$
作为一个字符,并不代表取值操作。
下面介绍另一个命令,read
表示从终端中读取一个值并赋值给一个变量,这个被赋值的变量可以是之前没有被声明过的变量。例如:
read x
echo $x
控制流语句
shell中也有一些判断和循环语句用来控制程序的执行。
if语句
下面是一个简单的if语句示例:
if test -f first
then #part of the if control segment
echo file \"first\" exist.
else
echo file \"first\" doesn\'t exist.
fi #suggest end of if statement
这个程序的作用是判断当前文件夹中是否存在名为first
的文件。其中if
控制语句的框架为:
if test ARGUMENT
then
#code segment
else
#code segment
fi
其中test
是一个命令,他根据后面的参数ARGUMENT
返回true
或false
,if
语句根据test
命令的返回值来决定执行哪一个代码块。可以在shell中使用man test
命令来查看更多test的用法。这里test
后面的-f first
表示判读当前目录是否存在名为first
的文件。
if
后面要跟上一个then
表示分支语句块的开始,对应的在语句块结束的时候要加上fi
。在shell中,一行表示一个语句,所以语句之间不需要分隔符。但是也可以将多个语句写在同一行中,但是这样的话就需要用分号;
隔开。
elif语句
elif
语句的框架如下:
read num
if test EXPRESSION1
then
CODE
elif test EXPRESSION2
then
CODE
elif test EXPRESSION3
then
CODE
else
fi
注意:如果判断语句中出现了if
,则在判断语句的后面必须要加上then
与其配套,而对于else
则不需要。
下面是一个简单的elif
语句示例:
if test $num = 1 #use $ to get content from the variable num
then
echo You get one apple.
elif test $num = 2
then
echo You get two apples.
elif test $num = 3
then
echo You get three apples.
else
echo You input is $num
echo Please enter a number between 1 and 3.
fi
for循环
下面是一个简单的for循环例子:
for foo in bar fud 43 #take three strings (bar, fud, 43) as input
do
echo $foo
done
这段代码中,for循环的框架为:
for VAR in LIST
do
LOOP
done
跟其他的代码快一样,for
循环的循环体也有一个do
和done
表示代码快的开始和结尾。VAR
表示LIST
中的其中一个元素,LIST
中可以是字符串,变量(使用变量的时候记得加$
),或者数字。
while循环
下面是一个简单的while循环的例子:
num=1 #don't use num = 1, space makes shell think num is a command
while test $num != 0
do
read num
echo You entered $num, enter 0 to exit.
done
该程序从终端中读取数字,如果数字是0,则跳出循环。其中while
循环的框架为:
while test EXPRESSION
do
CODE
done
其中test
命令的用法跟if
判断语句中的用法一致,并且while
循环也有表示循环体开始和结束的关键字do
和done
。
&& 和 ||
跟C语言一样,shell中也有表示条件与和或的运算符,并且其写法和用法都跟C语言中的一致,下面是一个简单示例:
echo Please enter num1:
read num1
echo Please enter num2:
read num2
echo num1 = $num1, num2 = $num2
if test $num1 = 0 && $num2 = 0 #each sub segment requires a test command
then
echo Double zeros
elif test $num1 = 0 || test $num2 = 0
then
echo One zero
else
echo No zero
fi
函数
在shell中也可以定义函数,但是调用的时候跟其他语言有很多不一样的地方,特别是传递参数的方式。下面看一个例子:
set "Hello"
#定义函数
fun(){
echo $0
echo $1
echo $2
}
#调用函数
fun Hello var2
###################输出########################
/bin/bash
Hello
var2
###############################################
跟很多编程语言不一样,shell中的函数如果要接受参数,不需要在函数签名中声明参数列表。只需要在函数中使用$1
,$2
…就行,注意这里是从1开始的,而$0
是shell中的$0
的值。
同样如果函数需要返回一个值,也不需要做任何声明,而只需要在函数体中直接用return
返回即可。
常用命令
set
在shell运行的时候,在其运行环境中存在一系列变量,他们用$NUM
表示,例如$0
,$1
,$2
,…。他们可以在运行一个shell文件的时候给定,也可以在shell中用set
命令进行设置。在默认的情况下,$0
的值是/bin/bash
,也就是shell终端的路径。
在使用set
设置参数的时候,是从$1
开始设置的,如果想设置$2
,则需要提供两个参数,以此类推,想要设置$3
则需要提供3个参数。例如:
echo $0 $1 $2
###################输出########################
/bin/bash
###############################################
set "Again" "Variable2"
echo $0 $1 $2
###################输出########################
/bin/bash
Again
Variable2
###############################################
set "Hello"
echo $0 $1 $2
###################输出########################
/bin/bash
Hello
###############################################
在默认状态下,只有$0
参数有值为/bin/bash
,后面的参数都是空的。可以通过set
命令为shell设置参数,设置了参数之后
shift
该命令使当前shell中的参数向左移动一位,而最左端的参数将被丢弃,但是$0
不受影响。下面看一个例子:
echo '$0' = $0, '$1' = $1, '$2' = $2, '$3', = $3
set var1 var2 var3
echo '$0' = $0, '$1' = $1, '$2' = $2, '$3', = $3
shift
echo '$0' = $0, '$1' = $1, '$2' = $2, '$3', = $3
#############################Output###################################
$0 = /bin/bash, $1 = , $2 = , $3 =
$0 = /bin/bash, $1 = var1, $2 = var2, $3 = var3
$0 = /bin/bash, $1 = var2, $2 = var3, $3 =
break
break
命令跟C语言中的命令含义跟用法一样,这里不再赘述。
:
在shell中,:
运算符表示一个空命令,跟C语言中的continue
类似。但这只是:
的其中一个用法,还有一个常见的用法就是用来构建无限循环。例如:
echo Please enter a num \(0 to break\)
while :
do
read x
if test $x = 0
then
echo You entered a 0, break out of the loop
break;
fi
done
.
.
命令会在当前shell程序下执行另一个shell文件,其运行原理跟C语言中的#include
类似,它会将另一个shell文件中的文字内容复制到当前shell程序中,并执行其内容。也就是说,执行这个命令相当于你手动从另一个shell文件中复制里面的内容,并粘贴到当前命令行进行运行,这样的运行方式可以使另一个shell文件中的操作改变当前shell程序的环境。下面是一个例子:
##############################Main.sh#################################
var="main"
echo $var
. ./Second.sh
echo
##############################Second.sh###############################
var="second"
##############################Output##################################
main
second
在上面的程序中,文件Main.sh
中定义了一个变量var
,并且在使用.
调用的Second.sh
文件中可以访问并改变Main.sh
中var
的值。
eval
eval
命令将一个字符串作为一条命令并执行,这条命令在需要动态地根据程序运行情况来决定执行什么命令的时候特别管用,下面是一个例子:
foo=10
x=foo
y='$'$x
echo $y
eval res=$y
echo $res
这段程序首先定义了一个foo
变量并将其初始化为10
,接着定义了一个x
变量,并将字符串foo
赋值给他。注意这里并不是将变量foo
赋值给x
,将变量foo
赋值给x
的语法为x=$foo
。进一步,程序在x
的值(也就是字符串foo
)前面加上了一个字符$
,将其变成一个字符串$foo
。最后使用eval
命令将变量y
中的值(字符串$foo
)作为一条命令执行,执行的结果就是变量foo
中的之被取出,进一步被赋值给变量res
。
exit
exit
命令将使当前shell程序退出,并返回一个状态码,其中0
表示执行成功,1
表示失败,这里跟平时判断布尔值的方式不一样需要注意。下面是语法:
exit 0 #程序执行成功,返回成功状态码0
exit 1 #程序执行失败,返回失败状态码1
exec
这个指令会结束当前运行的shell程序,并在推出之后运行一个命令。例如:
##############################exec.sh#################################
exec ls
##############################Terminal################################
./exec.sh
上面的代码在一个终端中调用exec.sh
文件,该文件中有一个exec ls
指令,该指令导致exec.sh
立即推出,并且在退出之后立即执行命令ls
。
export
该命令会让当前shell环境中的一个变量对于其子shell可见,子shell就是从当前shell文件中打开的shell程序。例如:
##############################main.sh#################################
var="Hello"
echo var in main bash is $var
export var
./sub.sh
##############################sub.sh##################################
echo var in sub bash is $var
##############################Output##################################
var in main bash is Hello
var in sub bash is Hello
可以看到,在子shellsub.sh
中即使没有定义变量var
也可以访问到var
中的值,这就是以为在父shell中用export
命令将已经定义好的var
变量传递到了子shell中。
expr
该命令接受一个参数作为表达式,它会将该表达式的值打印到终端上。这里的表达式跟eval
可以接受的不一样,eval
接受的是命令,而expr
可以接受的表达式是有要求的,只能接受固定的一些表达式。至于expr
命令具体可以接受哪些表达式,可以使用man expr
命令查看,下面是一个例子:
x=1
expr x + 1
#############################Output###################################
2
unset
该命令用于从环境中删除变量或函数,但是不能删除shell本身的定义的只读变量。例如:
foo=Hello
echo $foo
unset foo
echo $foo
#############################Output###################################
Hello
上面程序使用unset
将自己定义的foo
变量删除,所以在第二次echo
的时候,没有输出任何值。
trap
该命令使得当程序接到系统发送的给定命令时,执行相应的操作。通常用于处理系统中断,异常等。其中系统会发送的信号有:
信号 | 说明 |
---|---|
HUP(1) | 挂起,通常因终端掉线或用户退出引发 |
INT(2) | 中断,通常因按下Ctrl+C组合键而引发 |
QUIT(3) | 退出,通常因按下Ctrl+\组合键而引发 |
ABRT(6) | 中止,通常因某些严重的执行错误而引发 |
ALRM(14) | 报警,通常用来处理超时 |
TERM(15) | 终止,通常在系统关机时发送 |
trap
命令使用时的通常格式为trap 'command' signal
。
下面是一个例子:
trap "rm -f *.tmp" INT
命令的执行
$()
这个命令会将括号里的命令执行的结果作为返回值返回,可以使用这个命令将另一个命令的返回值存储到一个变量里。例如
ls
############################Output####################################
first.sh second.sh third.sh
上面的ls
命令将当前目录下所有文件名字打印在终端上,我们可以使用$()
将ls
命令的输出存储在一个变量里,而不是直接打印在终端上。
x=$(ls)
echo $x
############################Output####################################
first.sh second.sh third.sh
$(())
算术扩展$(())
跟之前的eval
命令效果一样,但是eval
会额外打开一个shell程序来执行给定的算术内容,而$(())
的执行会更加高效。
为什么需要这样一个算术扩展呢?,在shell中,所有的变量都是以字符串的形式存储的,例如你定义一个变量x=1
,实质上你是定义了一个字符串1
而不是一个整数1
。所以如果直接使用算术表达式进行运算,例如x=$x+1
,x
不会像我们预想的那样等于2
,而是等于1+1
。因为在上面的表达式中,shell进行的只是字符串的拼接操作,注意x
是一个字符串1
,而不是整数1
。所以需要使用算术扩展使字符串1
变成整数1
。像这样,x=$(($x + 1))
。
${}
参数扩展,参数扩展用于对给定参数进行说明。通过{}
中的不同值,可以对变量的值做各种处理。其中不同的格式对应不同的处理如下:
注意:花括号里面的变量名不需要再用$
进行取值操作。对于shell环境变量(使用$1
,$2
等访问的),我们只需要通过${1}
这样的格式访问即可,不需要加上$
格式 | 处理 |
---|---|
${param:-default} | 如果param为空,就把他设置为字符串default |
${#param} | 返回param的长度 |
${param%word} | 从param尾部开始删除与word匹配的最小部分,然后返回剩余部分 |
${param%%word} | 从param尾部开始删除与word匹配的最大部分,然后返回剩余部分 |
${param#word} | 从param头部开始删除与word匹配的最小部分,然后返回剩余部分 |
${param##word} | 从param头部开始删除与word匹配的最大部分,然后返回剩余部分 |
参数扩展还有另外一种使用方式,假设要对一系列已经编号的文件(1_file, 2_file, 3_file)进行处理。下面的程序
for i in 1 2 3
do
process ${i}_file
done
如果这里不使用参数扩展${}
而使用$i_file
,shell会认为整个后面的字符串i_file
表示一个变量,便会寻找一个名为i_file
的变量,但是这样的变量并不存在,这样就会引发程序错误。
Here 文档
该指令的作用是截取shell中的一部分连续的字符串作为一个临时文档,并将该文档输入到命令当中,这样刻意使得命令像是从一个文档中读取内容。例如对于cat
命令,该命令接收一个文件名作为参数,并将文件中的内容打印到控制台上。但是如果我们要在shell中动态输入内容给cat
,让其将内容打印到控制台上,就可以使用Here文档
:
cat << MARK
first line of the content
second line of the content
.....
last line of the content
MARK
############################Output####################################
first line of the content
second line of the content
.....
last line of the content
上面的<<
表示下面的内容是一个Here文档
,MARK
表示一个标志,表示HERE文档
的开始,并且该文档也已该标志结束。所以实际中使用Here文档
的时候要选取一个与正文内容不会冲突的字符串作为开始和结束的标志。使用这个命令,就可以用手动输入字符串的方式输入内容给那些接受文件作为输入的命令。
Here文档
还有另一个用法,就是将一系列操作写在一个Here文档
中,并将该文档输入到程序中,这样做的效果就像是程序在执行的过程中从命令行连续接受了一系列输入作为指令。例如,下面一个程序使用vim
编辑器删除给定文件中的第一行,并保存退出:
vim FILE << MARK
ggdd:wq
MARK
上面的例子可以看到,通过Here文档
输入的指令跟用键盘输入的指令一样。