Bash-Script的复习与梳理

Linux_bash.png

最近把Linux Bash编程的知识复习了一遍,大概梳理了一下,做个记录。

第一个Bash-Script

LINUX shell的种类非常之多,但是目前用得最为广泛的还是Bash,本文也是基于Bash的Shell环境。
下面是一个简单的示例:

1
2
3
#! /bin/sh
echo 'hello world!'

这就是一个最简单的shell脚本了。
第一行的#!用来告诉系统,这个脚本用什么解释器来执行(说明:sh和bash本身是不同的Shell,但是在我目前用得CentOS7版本sh和bash是等价的,sh是一个指向bash的符号链接)。
echo命令输出文本到屏幕

如何运行脚本

一种方式就是将脚本作为解释器的参数,如:

1
sh test.sh

第二种方式就是授予文件可执行权限

1
2
3
4
5
6
chmod +x test.sh
或者
chmod 755 test.sh
执行脚本
./test.sh

变量与参数

变量

Bash是一种弱类型的语言,你只需要直接定义变量名=value即可。当需要引用这个变量的时候使用$var_name或者${var_name}即可。
$var_name${var_name}的一种简写形式,但是在某些情况下只能使用${var_name},例如:

1
2
3
4
5
# 当需要连接两个变量
your_id=${USER}-on-${HOSTNAME}
# 或者会与其他字符串写在一起的时候
echo '${num}instance running!'

所以建议在写shell的时候统一使用${var_name}的风格。
bash中变量是弱类型的,那么不管你传给他的是字符串、数字或者数组他都统统会接收,有时候这就会导致一些问题,比如你想做个计算器,这时候传给了变量一个’ABCDEFG’,那么这时候我们就想这个变量只能接收数字。又或者你想定义一个常量,不想用户修改。那么以上这些我们可以使用declare来声明变量的一些属性。
declare常用参数如下:

-r 定义一个只读变量,也可以使用”readonly var_name”将变量定义为只读变量
-i 定义变量类型为整型
-a 定义变量类型为数组
-f 定义一个函数
-x 定义一个全局变量

当想删除一个变量的时候直接使用unset var_name即可,注意这个命令不能删除只读变量。

参数

那么如何向一个脚本传递参数呢?
当我们想向一个脚本传递参数的时候,直接在执行脚本的命令后面跟参数即可:

1
sh ./test.sh parm1 parm2 parm3

在脚本中使用$n(n是一个数字,如:$1获取第一个参数)来接收传入的参数即可。下面是可能经常用到的参数变量:

$0 当前脚本的文件名。
$n 传递给脚本或函数的参数。n是一个数字。例如,第一个参数是 $1 。
$# 传递给脚本或函数的参数个数。
$ 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。被双引号 (“ “)包含时与$
不同。
$? 上个命令的退出状态,或函数的返回值。
$_ 上一个命令的最后一个参数

我们经常用到的Linux命令中有一种叫option的东西,基本模式-optionname或者--longoptionname。如果在我们编写脚本的时候需要option的话,简单的脚本直接把他当普通参数手工处理即可,面对比较复杂的可以使用getopt或者getopt
这里感觉有必要提一下set这个命令,我觉得很牛逼、很厉害、很有用但是我从来没在脚本中使用过的命令。
set命令用来改变内部脚本的变量或者选项。一种应用就是来改变options来影响脚本的行为。另一种应用就是利用set `commond` 的输出来重置位置参数。

Example
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
#!/bin/bash
echo "Command-line argument #1 = $1"
echo "Command-line argument #2 = $2"
echo "Command-line argument #3 = $3"
echo "--------------------------------------"
echo "利用`uname -a`的输出来重置位置参数"
set `uname -a`
echo "Field #1 of 'uname -a' = $1"
echo "Field #2 of 'uname -a' = $2"
echo "Field #3 of 'uname -a' = $3"
exit 0
output:
[root@localhost study]# sh test_set.sh one two three
Command-line argument #1 = one
Command-line argument #2 = two
Command-line argument #3 = three
--------------------------------------
Sets the positional parameters to the output of the commond `uname -a`
Field #1 of 'uname -a' = Linux
Field #2 of 'uname -a' = localhost.localdomain
Field #3 of 'uname -a' = 3.10.0-514.el7.x86_64

数组

目前只支持一维数组!!!!!!!!!!
可以通过以下两种方式来为数组赋值:

1
2
3
4
5
6
#!/bin/bash
my_array=(A B C "D")
my_array[4]=F
my_array[5]="G"

通过${my_array[idx]}来读取数组中的值,也可以通过${my_array[*]}${my_array[@]}来一次性读取数组中的所有元素。通过${#my_array[*]}${#my_array[@]}来获取数组长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
my_array=(A B C "D")
my_array[4]=F
my_array[5]="G123"
echo "第一个元素:${my_array[0]}"
echo "通过*来获取数组所有元素:${my_array[*]}"
echo "通过@来获取数组所有元素:${my_array[@]}"
echo "通过*来获取数组长度:${#my_array[*]}"
echo "通过@来获取数组长度:${#my_array[@]}"
output:
[root@localhost study]# sh test_arr.sh
第一个元素:A
通过*来获取数组所有元素:A B C D F G123
通过@来获取数组所有元素:A B C D F G123
通过*来获取数组长度:6
通过@来获取数组长度:6

关于引号

反引号

反引号括起来的字符串被bash解释为命令行执行,并以它的输出结果取代整个反引号部分。

1
2
3
4
5
6
7
8
9
10
#! /bin/sh
str=`echo $PATH`
echo ${str}
output:
[root@localhost study]# sh qut.sh
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

单引号

单引号扩起来的字符都视作普通字符,所有特殊字符都会失去原油的意义。

1
2
3
4
5
6
7
8
9
10
#! /bin/sh
str='${PATH}'
echo ${str}
output:
[root@localhost study]# sh qut.sh
${PATH}

双引号

由双引号括起来的字符,除$、\、’、和”这几个字符仍是特殊字符并保留其特殊功能外,其余字符作为普通字符对待。

while与for

for循环

for循环的一般格式为

1
2
3
4
for var in [list]
do
commonds
done

下面来看个例子:

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
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash
# 如果后面的list不加引号,则默认以空格分割
for var in A B C D
do
echo "${var}"
done
echo;echo
# 会将引号内的内容当做一个元素
for var in "A A" "B B B" "C C"
do
echo "${var}"
done
echo;echo
# 上面两种写法有点类似java的for-each
# 如果你用过C,那么你对下面这种写法更熟悉
for ((a=1, b=1; a <= 5 ; a++, b++))
do
echo -n "$a-$b "
done
output:
[root@localhost study]# sh for-loop.sh
A
B
C
D
A A
B B B
C C
1-1 2-2 3-3 4-4 5-5

while循环

while循环的一般格式为

1
2
3
4
while [ condition ]
do
commands
done

Example:

1
2
3
4
5
6
7
#!/bin/bash
while (( a <= 5 ))
do
echo -n "$a "
let "a+=1"
done

break与continue

break用来跳出整个循环,continue跳出本次循环。

算术运算

首先来看个例子:

1
2
3
4
5
6
7
8
9
#! /bin/bash
b=1+2
echo "${b}"
output:
[root@localhost study]# sh test.sh
1+2

默认情况下,bash中会把所有的赋值当做字符或字符串,所以看到的输出是1+2而不是3
可以用以下三种方式来做算术运算。

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
32
33
34
#! /bin/bash
# 1、使用`declare`来声明变量为整型。
declare -i b
b=1+2
echo "declare:${b}"
echo;echo
# 2、let命令
let "c=1+1"
echo "let:${c}"
echo;echo
# 3、利用双括号将表达式括起来
((d=2+3))
echo "((表达式)):${d}"
echo;echo
# 4、利用单个方括号
echo "[表达式]:$[1+2]"
output:
[root@localhost study]# sh test.sh
declare:3
let:2
((表达式)):5
[表达式]:3

test命令

首先需要注意的是在Linux中“0”表示成功。test命令用来检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。

  • 直接使用test这个命令
  • […]:这是test的一个同义词,用来简化test的书写方式
  • [[…]]:继承自test命令,主要用来方便其他语言的程序员。
  • ((…)): 返回其中表达式的计算结果

Example:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# test 1 -lt 2
[root@localhost ~]# echo $?
0
[root@localhost ~]# [ 1 -lt 2 ]
[root@localhost ~]# echo $?
0
[root@localhost ~]# [[ 2 -eq 2 ]]
[root@localhost ~]# echo $?
0
[root@localhost ~]# (( 2 == 2 ))
[root@localhost ~]# echo $?
0

数字测试

数值测试操作符:

  • -eq 或 == : 是否相等
  • -ne 或 != : 不等于
  • -gt 或 > : 大于
  • -lt 或 < : 小于
  • -ge 或 >= : 大于等于
  • -le 或 <= : 小于等于
    注意:或选项的用法其实就是讲数字当做字符串来比较

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#! /bin/sh
a=4
b=5
if [ ${a} -ne ${b} ]
then
echo "${a} is not equal to ${b}
fi
echo
if [ ${a} != ${b} ]
then
echo "${a} is not equal to ${b}."
fi

字符串测试
  • = : 判断两个字符串是否相等。if [ “$a” = “$b” ]
  • == : 判断两个字符串是否相等,=的一个同义词
  • != : 判断字符串是否不等
  • < : 小于,根据ASCII的排序,注意在[ a \< b ]需要转义,在[[ a < b ]]可以直接使用,>同理
  • : 大于,根据ASCII的排序

  • -z : 字符串是否为空,或长度为0
  • -n : 字符串是否非空

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#! /bin/sh
str1=""
if [ -z ${str1} ]
then
echo "str1 is null"
fi
echo
str1="abc"
if [ -n ${str1} ]
then
echo "str1 is not null"
fi
echo
str2="cdef"
if [ "${str1}" != "${str2}" ]
then
echo "${str1} is not equal to ${str2}"
fi

文件测试

下面列举一些常用的文件操作:

  • -e : 文件或目录是否存在
  • -f : 是否是一个文件(而非目录)
  • -d : 是否是目录
  • -s : 文件大小是否为0
  • -r : 是否对文件有读权限
  • -w : 是否对文件有写权限
  • -x : 是否对文件有执行权限
  • -O : 是否是文件所有者
  • -G :是否与文件组用户相同

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /bin/sh
filename="/study/bash/test_file.txt"
if [ -e "${filename}" ]
then
echo "${filename} file exists."
fi
if [ -f "${filename}" ]
then
echo "${filename} is a regular file."
fi
if [ -s "${filename}" ]
then
echo "${filename} not a zero file."
fi

if & case

if语句的一般格式:

1
2
3
4
if condition
then
commonds
fi

if-else语句的格式

1
2
3
4
5
6
7
8
9
if condition1
then
commonds
elif condition2
then
commonds
else
commonds
fi

case语句的一般格式:

1
2
3
4
5
6
7
8
casein
模式1)
commonds
;;
模式2)
commonds
;;
esac

在case语句中可以用*号来匹配任意值。

函数

平时在一些简单的脚本里面基本用不到函数,但是在复杂的脚本中用来组织代码以及封装常用逻辑,在bash中还是很好用的东西。
函数的定义格式如下:

1
2
3
4
[ function ] funname [()]{
action;
[return ${result};]
}

1、我们可以function funname方式来定义,也可以直接funname()
2、可以显示的加return来返回结果或者直接返回最后一条命令的执行结果。

Example:

1
2
3
4
5
6
7
#! /bin/sh
fun(){
echo "it's a function"
}
fun

向函数传递参数的形式类似执行脚本的传参:fun_name arg1 arg2
Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#! /bin/sh
fun(){
if [ ${1} -gt ${2} ]
then
echo "${1} bigger than ${2}."
elif [ ${2} -gt ${1} ]
then
echo "${2} bigger than ${1}."
else
echo "${1} equal to ${2}."
fi
}
fun 10 20

读取键盘的输入

可以通过read命令来读取标准输入的值。下面用一个例子来看下在bash中read的常用方式:

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
32
33
34
35
#! /bin/sh
# 将读取到的值赋值给var1这个变量,当我们在行尾加\时,按回车会换到下一行继续输入
read var1
echo "var1 is ${var1}"
echo "==============================="
# 直接read,可以使用$REPLY来获取读取到的值
read
echo "read $REPLY"
echo "==============================="
# -r参数会按字符串解释\,而不会换行
read -r mul_line
echo "mul_line:${mul_line}"
echo "==============================="
# 上面的方式都会在屏幕上打印输入值,在交互式的bash中是不友好的,我们可以加-s选项来屏蔽输入的打印
read -s var2
echo "dont echo input:${var2}"
echo "==============================="
# 有时候可能不希望一直等待用户输入,我们可以用-t选项来这只超时时间
TIMELIMIT=5
read -t $TIMELIMIT var3
if [ -z ${var3}]
then
var3="time out"
fi
echo "timed read:${var3}"

下面是测试的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost study]# sh read_test.sh
read value to var2\
new line
var1 is read value to var2new line
===============================
read had no var
read read had no var
===============================
read with -r option\
mul_line:read with -r option\
===============================
dont echo input:do not echo input
===============================
timed read:time out

字符串

在大多数编程语言中,字符串是非常重要、常用的数据类型。
1、字符串长度

${#string}
expr length $string
expr "$string" : '.*'

Example:

1
2
3
4
5
6
7
#! /bin/sh
str1="fdhjksdf"
echo "${#str1}"
echo "`expr length ${str1}`"
echo "`expr "${str1}" : '.*'`"

2、匹配子串长度

expr match "$string" '$substr_regxp'
expr "$string" : '$substr_regxp'

Example:

1
2
3
4
5
6
7
8
9
#!/bin/sh
str1="str;str-str;strend"
sub_regxp="[a-z]*;"
echo "`expr match "${str1}" "${sub_regxp}"`"
echo "`expr "${str1}" : "${sub_regxp}"`"

3、子串开始索引

expr index $string $substring

Example

1
2
3
4
5
6
7
#! /bin/sh
str="abc;efg;higj"
rgx_str=";.*;"
echo "`expr index ${str} ${rgx_str}`"

4、字符串截取

${string:position}截取从postition位置开始的字符串
${string:position:length}从position位置开始截取长度为length长的子串,postition的开始索引为0
expr substr $string $position $length从position位置开始截取长度为length长的子串,postition的开始索引为1
expr match "$string" '$sub_regxp'截取匹配正则的子串
expr "$string" : '$sub_regxp'截取匹配正则的子串

Example

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#! /bin/sh
str="abc;defg;hijk;lmnpq"
echo "\${string:position}"
echo "${str:4}"
echo "==================================="
echo "\${string:position:length}"
echo "${str:4:4}"
echo "==================================="
echo "expr substr \$string \$position \$length"
echo "`expr substr ${str} 5 4`"
echo "==================================="
sub_regxp=".*;\(.*\);"
echo "expr match "\$string" '${sub_regxp}'"
echo "`expr match "${str}" ${sub_regxp}`"
echo "==================================="
echo "expr "\$string" : '\$sub_regxp'"
echo "`expr "${str}" : ${sub_regxp}`"
output:
[root@localhost study]# sh sub_str.sh
${string:position}
defg;hijk;lmnpq
===================================
${string:position:length}
defg
===================================
expr substr $string $position $length
defg
===================================
expr match $string '.*;\(.*\);'
hijk
===================================
expr $string : '$sub_regxp'
hijk

5、删除子串

${string#substring},从前面最小匹配删除
${string##substring},从前面最长匹配删除
${string%substring},从后面最小匹配删除
${string%%substring},从后面最小匹配删除

Example

1
2
3
4
5
6
7
8
9
10
11
12
#! /bin/sh
str=abcABC123ABCabc
reg_str='a*C'
echo ${str#$reg_str} # 123ABCabc
echo ${str##$reg_str} # abc
reg_str='b*c'
echo ${str%$reg_str} # abcABC123ABCa
echo ${str%%$reg_str} # a

6、子串替换

${string/substring/replacement}从前面最小匹配替换
${string//substring/replacement}从前面最长匹配替换
${string/#substring/replacement}从后面最小匹配替换
${string/%substring/replacement}从后面最长匹配替换

Debug your Script

在bash中提供了相关的参数供我们进行脚本的调试追踪。

sh [-nvx] script.sh
参数:
-n:不执行基本,只检查语法问题
-v:执行脚本前,先将脚本内容输出到屏幕上
-x:将使用到的内容显示到屏幕上