Linux中的bash语法详解

Linux中的bash语法,主要介绍bash的io、变量、分支、循环等基本操作。

shell就是一个bash程序(进程),就是一个解释器,启动器

脚本本质:

  • #!/bin/bash
  • #!/usr/bin/python

上面的不是普通的注释,是你用./执行的时候,默认走的哪一个解释器

bash test.sh执行的时候,是不看第一行的#!/bin/bash#!/usr/bin/python ,直接用bash解释器。

同样的,用sh test.sh执行的时候也一样,其直接用sh解释器。

但是,用./ test.sh执行的时候,要看第一行,然后选择走哪一个解释器。

脚本读取(或解释执行)方式常用的有五种:

  • bash filename
  • sh filename
  • ./filename
  • source filename
  • . filename

说明: ./*.sh的执行方式等价于sh ./*.sh或者bash ./*.sh此三种执行脚本的方式都是重新启动一个子shell,在子shell中执行此脚本。

source ./*.sh. ./*.sh的执行方式是等价的,即两种执行方式都是在当前shell进程中执行此脚本,而不是重新启动一个子shell执行。

一个重要的点:父shell进程中没有被export导出的变量(即非环境变量)是不能被子shell进程继承的,但是在父进程中被export的变量可以被子shll进程使用。子shell进程中的所有变量(无论是否被export)都不能被父进程使用

1. bash基本使用

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
[root@layne ~]# bash  # 启动一个新的bash进程,该bash进程为之前bash的子进程
[root@layne ~]# echo $$ # 打印当前bash的PID
1852
[root@layne ~]# pstree -p # 打印进程树,并显示进程id
init(1)─┬─auditd(899)───{auditd}(900)
├─crond(1312)
├─mingetty(1338)
├─mingetty(1340)
├─mingetty(1342)
├─mingetty(1344)
├─mingetty(1346)
├─mingetty(1348)
├─ntpd(973)
├─rsyslogd(915)─┬─{rsyslogd}(917)
│ ├─{rsyslogd}(919)
│ └─{rsyslogd}(920)
├─sshd(965)───sshd(1030)───bash(1219)───bash(1852)───pstree(1862)
└─udevd(354)───udevd(640)
[root@layne ~]# exit # (第一次输入exit,退出当前bash,再输入exit会退出ssh连接的bash,即就断掉连接了)
exit
[root@layne ~]# echo $$
1219
[root@layne ~]# pstree -p
init(1)─┬─auditd(899)───{auditd}(900)
├─crond(1312)
├─mingetty(1338)
├─mingetty(1340)
├─mingetty(1342)
├─mingetty(1344)
├─mingetty(1346)
├─mingetty(1348)
├─ntpd(973)
├─rsyslogd(915)─┬─{rsyslogd}(917)
│ ├─{rsyslogd}(919)
│ └─{rsyslogd}(920)
├─sshd(965)───sshd(1030)───bash(1219)───pstree(1863)
└─udevd(354)───udevd(640)

可以看到,当我们用ssh连接虚拟机时,会启动sshd进程,进入bash命令行界面后,会启动bash进程,这个bash进程是sshd进程的子进程,当我们在命令行中输入bash会启动一个新的bash进程,这个新的bash进程是之前bash进程的子进程。

现在我们来验证:父shell进程中没有被export导出的变量(即非环境变量)是不能被子shell进程继承的,但是在父进程中被export的变量可以被子shll进程使用。

1
2
3
4
5
6
7
8
[root@layne ~]# name=layne
[root@layne ~]# age=24
[root@layne ~]# echo "$name,$age"
layne,24
[root@layne ~]# export name
[root@layne ~]# bash
[root@layne ~]# echo "$name,$age"
layne,

可以看到,在子进程的bash,只能输出被export导出的变量(即环境变量),不能输出普通变量。

还有一个重要点:子shell进程中的所有变量(无论是否被export)都不能被父进程使用。验证如下:

1
2
3
4
5
6
7
[root@layne bashdir]# GH=lazz
[root@layne bashdir]# export GH
[root@layne bashdir]# echo $GH
lazz
[root@layne bashdir]# exit
exit
[root@layne bashdir]# echo $GH #输出空

另外,在.sh文件的开头一般输入下列中的一个:

1
2
3
#!/bin/bash
#!/usr/bin/python
#!/bin/awk -f

用于指定该脚本由哪个程序负责解释执行。

现在我们创建两个文件mysh.sh和mysh1.sh,这两个文件内容分别为:

mysh.sh内容

1
2
3
#!/bin/bash
echo "hello mysh"
echo $$

mysh1.sh内容

1
2
3
4
#!/bin/bash
echo "hello mysh1"
echo $$
pstree -p

下面是执行过程:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
[root@layne bashdir]# ll
total 8
-rwxr-xr-x 1 root root 49 Feb 9 16:27 mysh1.sh
-rwxr-xr-x 1 root root 39 Feb 9 16:27 mysh.sh
[root@layne bashdir]# chmod -x mysh.sh # 清除执行权限
[root@layne bashdir]# chmod -x mysh1.sh
[root@layne bashdir]# ll
total 8
-rw-r--r-- 1 root root 49 Feb 9 16:27 mysh1.sh
-rw-r--r-- 1 root root 39 Feb 9 16:27 mysh.sh
[root@layne bashdir]# source mysh.sh
hello mysh
1219
[root@layne bashdir]# source mysh1.sh
hello mysh1
1219
init(1)─┬─auditd(899)───{auditd}(900)
├─crond(1312)
├─mingetty(1338)
├─mingetty(1340)
├─mingetty(1342)
├─mingetty(1344)
├─mingetty(1346)
├─mingetty(1348)
├─ntpd(973)
├─rsyslogd(915)─┬─{rsyslogd}(917)
│ ├─{rsyslogd}(919)
│ └─{rsyslogd}(920)
├─sshd(965)───sshd(1030)───bash(1219)───pstree(1890)
└─udevd(354)───udevd(640)
[root@layne bashdir]# pstree
init─┬─auditd───{auditd}
├─crond
├─6*[mingetty]
├─mysqld_safe───mysqld───27*[{mysqld}]
├─ntpd
├─rsyslogd───3*[{rsyslogd}]
├─sshd───sshd───bash───pstree
└─udevd───udevd
[root@layne bashdir]# ./mysh.sh
-bash: ./mysh.sh: Permission denied
[root@layne bashdir]# chmod +x mysh.sh
[root@layne bashdir]# ./mysh.sh
hello mysh
1894
[root@layne bashdir]# chmod +x mysh1.sh
[root@layne bashdir]# ./mysh1.sh
hello mysh1
1898
init(1)─┬─auditd(899)───{auditd}(900)
├─crond(1312)
├─mingetty(1338)
├─mingetty(1340)
├─mingetty(1342)
├─mingetty(1344)
├─mingetty(1346)
├─mingetty(1348)
├─ntpd(973)
├─rsyslogd(915)─┬─{rsyslogd}(917)
│ ├─{rsyslogd}(919)
│ └─{rsyslogd}(920)
├─sshd(965)───sshd(1030)───bash(1219)───mysh1.sh(1898)───pstree(1899)
└─udevd(354)───udevd(640)

可以发现:

  • 当前shell执行脚本:source mysh.sh不需要执行权限就可以执行
  • 子进程执行:bash mysh.sh或者./mysh.sh,需要该文件具有可执行权限才能执行

.sh文件中的函数格式为:

1
2
3
funName() {
各种命令
}

在下面的例子中会用到。

2. bash的io

程序自身都有I/O

  • 0:标准输入
  • 1:标准输出
  • 2:错误输出

例如:

1
ls / /hello 1> log.out 2> log.err

上面命令把正确的输出放在log.out里面,把错误的输出放在log.err里面。

下面,我们看几个常用场景

(1)输出重定向: 重定向从左到右绑定

1
2
3
4
5
6
7
8
# 命令1: 将错误输出(2)绑定到标准输出(1),此时标准输出先输出到控制台,然后才是标准输出重定向到文件。两个重定向的绑定没有关系,所以第二个标准输出1> mylog.log的数据仍然是正确的输出
ls / /hello 2>&1 1> mylog.log
## 命令2:先让标准输出重定向到文件,然后将错误输出绑定到标准输出,也就是左边绑定的文件。
ls / /hello 1> mylog1.log 2>&1
## 命令3:标准输出和错误输出都重定向到文件
ls / /hello >& mylog2.log
## 命令4:作用同命令3
ls / /hello &> mylog3.log

(2)现在,我们创建rd.sh文件,内容如下:

1
2
3
4
5
#!/bin/bash
read -p "请输入一个整数:" num
echo $num
read -p "再输入一个数:"
echo $REPLY

执行结果:

1
2
3
4
5
6
[root@layne bashdir]# chmod +x rd.sh
[root@layne bashdir]# ./rd.sh
请输入一个整数:123
123
再输入一个数:45
45

sh -x rd.sh 检查你写的脚本,执行到哪一步,把执行的过程打出来

1
2
3
4
5
6
7
8
9
[root@layne bashdir]# sh -x rd.sh
+ read -p $'\350\257\267\350\276\223\345\205\245\344\270\200\344\270\252\346\225\264\346\225\260\357\274\232' num
请输入一个整数:123
+ echo 123
123
+ read -p $'\345\206\215\350\276\223\345\205\245\344\270\200\344\270\252\346\225\260\357\274\232'
再输入一个数:45
+ echo 45
45

(3)将标准输入重定向到字符串,read读取后赋值给指定的变量:

1
2
3
[root@layne bashdir]# read  aaa  0<<<"hello"  # 赋给变量aaa
[root@layne bashdir]# echo $aaa
hello

上面 <<<将标准输入重定向到字符串

(4)cat 0<<CATEOF用在脚本中用于向控制台打印n行,比如:

1
2
3
4
5
6
7
8
[root@layne bashdir]# cat 0<<CATEOF
> aaaaa
> bbbbb
> ccccc
> CATEOF
aaaaa
bbbbb
ccccc

创建catof.sh文件,内容为:

1
2
3
4
5
#!/bin/bash
cat 0<<CATEOF
这里有一个bug
这个bug是index out of bounds exception
CATEOF

执行:

1
2
3
4
[root@layne bashdir]# chmod +x catof.sh 
[root@layne bashdir]# ./catof.sh
这里有一个bug
这个bug是index out of bounds exception

(5)exec:使用指定的命令替换当前shell命令。

以读写方式打开到www.baidu.com的80端口的tcp连接

1
2
3
4
5
6
7
8
9
[root@layne bashdir]# exec 8<> /dev/tcp/www.baidu.com/80 # 创建文件描述符8,并赋值
[root@layne bashdir]# echo -e "GET / HTTP/1.0\n" >&8 # 表示重定向到8文件描述符
[root@layne bashdir]# cat <&8 # 从文件描述符8读取信息
HTTP/1.0 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-Length: 14615
Content-Type: text/html
...

echo的-e选项含义为:激活转义字符。使用-e选项时,若字符串中出现以下字符,则特别加以处理,而不会将它当成一般文字输出:

  • \a 发出警告声;
  • \b 删除前一个字符;
  • \c 最后不加上换行符号;
  • \f 换行但光标仍旧停留在原来的位置;
  • \n 换行且光标移至行首;
  • \r 光标移至行首,但不换行;
  • \t 插入tab;
  • \v\f相同;
  • \\ 插入\字符;
  • \nnn 插入nnn(八进制)所代表的ASCII字符;

echo -e "GET / HTTP/1.0\n" >&8 表示重定向到8文件描述符

cat <&8 从文件描述符8读取信息

3. bash的变量

3.1 bash中变量的类型

  • 本地变量
  • 局部变量
  • 位置变量
  • 特殊变量
  • 环境变量

3.2 本地变量

  • 当前shell所有
  • 生命周期跟当前shell一样

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@layne bashdir]# a=99 # 定义一个变量
[root@layne bashdir]# echo $a # 输出变量a
99
[root@layne bashdir]# myfunc() {
> myvar=99
> echo $myvar
> }
[root@layne bashdir]# echo $myvar #访问不到

[root@layne bashdir]# myfunc #调用函数
99
[root@layne bashdir]# echo $myvar #可以访问到
99
[root@layne bashdir]# abc=sxt
[root@layne bashdir]# echo $abc
sxt
[root@layne bashdir]# echo "$abcisnothere" #访问不到

[root@layne bashdir]# echo "${abc}isnothere" #可以访问
sxtisnothere
[root@layne bashdir]# echo "$abc isnotthere"
sxt isnotthere
[root@layne bashdir]# echo "{$abc}isnotthere"
{sxt}isnotthere

3.3 局部变量

  • 只能用于函数
  • local var=100 定义一个局部变量

看例子:

1
2
3
4
5
6
7
8
9
10
[root@layne bashdir]# unset myvar # 取消变量
[root@layne bashdir]# echo $myvar

[root@layne bashdir]# myfunc(){
> local myvar=101
> echo $myvar
> }
[root@layne bashdir]# echo $myvar #访问不到局部变量

[root@layne bashdir]#

继续往下看:

1
2
3
4
5
6
7
8
9
10
11
[root@layne bashdir]# funa(){
> a=1
> local b=2
> echo "a = $a"
> echo "b = $b"
> }
[root@layne bashdir]# funa
a = 1
b = 2
[root@layne bashdir]# echo "$a,$b" #b访问不到
1,

上面,a是本地变量,b是局部变量,本地变量生命周期跟当前shell一样,局部变量的声明周期在一个函数内,函数执行完,局部变量就消失了。

3.4 位置变量

直接看例子:

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
[root@layne bashdir]# myfun1(){
> echo $1
> }
[root@layne bashdir]# myfun1

[root@layne bashdir]# myfun1 hello # hello为函数myfun1的参数
hello
[root@layne bashdir]# myfun2(){
> echo $4
> }
[root@layne bashdir]# myfun2 a b c

[root@layne bashdir]# myfun2 a b c d
d
[root@layne bashdir]# myfunc3(){
> echo $13
> }
[root@layne bashdir]# myfunc3 1 2 3 4 5 6 7 8 9 a b c d e f
13
[root@layne bashdir]# myfunc3 a b c d 1 2 3 4 5 6 7 8 9
a3
[root@layne bashdir]# myfunc3(){
> echo ${13}
> }
[root@layne bashdir]# myfunc3 1 2 3 4 5 6 7 8 9 a b c d
d

可以看到,$n为函数的第n个位置变量(第n个参数),${13}是函数的第13个参数,不能写为$13

再来看一个例子,创建文件mysh1.h,内容如下:

1
2
3
4
#!/bin/bash
echo $1
echo $2
echo ${10}

执行结果:

1
2
3
4
[root@layne bashdir]# source mysh1.sh a b 1 2 3 4 5 6 7 8 9
a
b
8

3.5 特殊变量

  • $#:位置参数个数
  • $*:参数列表,所有的参数作为一个字符串,以空格隔开
  • $@:参数列表,双引号引用为单独的字符串,所有的参数作为单个的字符串,以空格隔开
  • $$:当前shell的PID
  • $?:上一个命令的退出状态
  • 0:成功
  • 其他:失败

例子:创建mysh2.sh,内容如下:

1
2
3
4
5
6
#!/bin/bash
echo "number of args:$#"
echo "string of args:$*"
echo "args:$@"
echo "current pid:$$"
pstree -p

执行结果:

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
[root@layne bashdir]# chmod +x mysh2.sh
[root@layne bashdir]# ./mysh2.sh a b 1 2 3 4 5 #创建子shell进程执行
number of args:7
string of args:a b 1 2 3 4 5
args:a b 1 2 3 4 5
current pid:2033
init(1)─┬─auditd(899)───{auditd}(900)
├─crond(1312)
├─mingetty(1338)
├─mingetty(1340)
├─mingetty(1342)
├─mingetty(1344)
├─mingetty(1346)
├─mingetty(1348)
├─ntpd(973)
├─rsyslogd(915)─┬─{rsyslogd}(917)
│ ├─{rsyslogd}(919)
│ └─{rsyslogd}(920)
├─sshd(965)───sshd(1945)───bash(1947)───mysh2.sh(2033)───pstree(2034)
└─udevd(354)───udevd(640)
[root@layne bashdir]# source mysh2.sh a b 1 2 3 4 5 #在当前shell执行
number of args:7
string of args:a b 1 2 3 4 5
args:a b 1 2 3 4 5
current pid:1947
init(1)─┬─auditd(899)───{auditd}(900)
├─crond(1312)
├─mingetty(1338)
├─mingetty(1340)
├─mingetty(1342)
├─mingetty(1344)
├─mingetty(1346)
├─mingetty(1348)
├─ntpd(973)
├─rsyslogd(915)─┬─{rsyslogd}(917)
│ ├─{rsyslogd}(919)
│ └─{rsyslogd}(920)
├─sshd(965)───sshd(1945)───bash(1947)───pstree(2036)
└─udevd(354)───udevd(640)

$?用于获取上一个命令的退出状态

1
2
3
4
5
6
7
8
9
10
11
[root@layne bashdir]# mydir=/root
[root@layne bashdir]# myfile=mysh2.sh
[root@layne bashdir]# [ -d $mydir ] && echo "$mydir is dir" # [ -d $mydir ]用于测试该变量是否是一个目录
/root is dir
[root@layne bashdir]# [ -d $myfile ] && echo "$mydir is dir"
[root@layne bashdir]# [ -d $mydir ]
[root@layne bashdir]# echo $?
0 # 0相当于成立
[root@layne bashdir]# [ -d $myfile ]
[root@layne bashdir]# echo $?
1

3.6 数组

Bash提供了一维数组变量。任何变量都可以作为一个数组;内建命令 declare 可以显式地定义数组。数组的大小没有上限(初始创建多大的数组,可以超过这个大小继续往后赋值),也没有限制在连续对成员引用和赋值时有什么要求。数组以整数为下标,从0开始。

如果变量赋值时使用语法 name[subscript]=value,那么就会自动创建数组。 subscript 被当作一个算术表达式,结果必须是大于等于 0 的值。数组赋值可以使用复合赋值的方式,形式是 name=(value1 value2 ... valuen),这里每个 value 的形式都是[subscript]=string。string 必须出现。如果出现了可选的括号和下标,将为这个下标赋值,否则被 赋值的元素的下标是语句中上一次赋值的下标加一。下标从 0 开始。这个语法也被内建命令 declare 所接受。单独的数组元素可以用上面介绍的语法 name[subscript]=value 来赋值

数组的任何元素都可以用 ${name[subscript]} 来引用。花括号是必须的,以避免和路径扩展冲突。如果subscript 是 @ 或是 *,它扩展为 name 的所有成员。这两种下标只有在双引号中才不同。在双引号中,${name[*]} 扩展为一个词,由所有数组成员的值组成,用特殊变量 IFS 的 第 一 个 字 符 分 隔;${name[@]}将 name 的每个成员扩展为一个词。如果数组没有成员,${name[@]} 扩展为空串。这种不同类似于特殊参数 *@ 的扩展 (参见上面的 Special Parameters 段落)。${#name[subscript]} 扩展为 ${name[subscript]} 的长度。如果 subscript 是 * 或者是 @,扩展结果是数组中元素的个数。引用没有下标数组变量等价于引用元素 0。

内建命令 unset 用于销毁数组unset name[subscript] 将销毁下标是 subscript 的 元 素 。 unset name, 这里 name 是一个数组,或者 unset name[subscript], 这里 subscript 是 * 或者是 @,将销毁整个数组。 内建命令 declare, local, 和 readonly 都能接受-a 选项,从而指定一个数组。内建命令 read 可 以接受 -a 选项,从标准输入读入一列词来为数组赋值。内建命令 set 和 declare 使用一种可以重用为输入的格式来显示数组元素。

1
2
3
4
5
6
7
8
9
[root@layne bashdir]# sxt=(a  b  c)  #数组
[root@layne bashdir]# echo $sxt
a
[root@layne bashdir]# echo ${sxt[1]}
b
[root@layne bashdir]# echo ${sxt[*]}
a b c
[root@layne bashdir]# echo ${sxt[@]}
a b c

注意:echo $sxt[1] 是错误的写法

3.7 管道

直接看例子:

1
2
3
4
5
6
[root@layne bashdir]# a=9
[root@layne bashdir]# echo $a
9
[root@layne bashdir]# b=22 | echo ok #启动子进程给b赋值22
ok
[root@layne bashdir]# echo $b #访问不到子进程的数据,父进程访问不了子进程的结果

管道两边的命令在当前shell的两个子进程中执行。

3.8 $$和$PASHPID

$$$BASHPID的区别

  • $$是在哪个进程中执行命令,该值就是哪个shell进程的PID
  • echo "hello"| echo $BASHPID 这里的id是前面执行echo "hello"的shell的id,而echo $$ 是当前shell的id

另外,我们创建一个test.sh再来测试一下,test.sh内容如下:

1
2
3
4
#!/bin/bash
echo "\$\$ value:$$"
echo "BASHPID value:$BASHPID"
pstree -p

执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@layne bashdir]# chmod +x test.sh
[root@layne bashdir]# ./test.sh
$$ value:2208
BASHPID value:2208
init(1)─┬─auditd(899)───{auditd}(900)
├─crond(1312)
├─mingetty(1338)
├─mingetty(1340)
├─mingetty(1342)
├─mingetty(1344)
├─mingetty(1346)
├─mingetty(1348)
├─ntpd(973)
├─rsyslogd(915)─┬─{rsyslogd}(917)
│ ├─{rsyslogd}(919)
│ └─{rsyslogd}(920)
├─sshd(965)───sshd(1945)───bash(1947)───test.sh(2208)───pstree(2209)
└─udevd(354)───udevd(640)

可以看到,在子进程执行的时候,$$$BASHPID 输出的都是当前进程的ID

另外,再创建test1.sh,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

echo "\$\$ outside of subshell = $$" # 9602
echo "\$BASH_SUBSHELL outside of subshell = $BASH_SUBSHELL" # 0
echo "\$BASHPID outside of subshell = $BASHPID" # 9602

echo

( echo "\$\$ inside of subshell = $$" # 9602
echo "\$BASH_SUBSHELL inside of subshell = $BASH_SUBSHELL" # 1
echo "\$BASHPID inside of subshell = $BASHPID" ) # 9603
# Note that $$ returns PID of parent process.

执行:

1
2
3
4
5
6
7
8
9
[root@layne bashdir]# chmod +x test1.sh
[root@layne bashdir]# ./test1.sh
$$ outside of subshell = 2214
$BASH_SUBSHELL outside of subshell = 0
$BASHPID outside of subshell = 2214

$$ inside of subshell = 2214
$BASH_SUBSHELL inside of subshell = 1
$BASHPID inside of subshell = 2215

我的理解: $$表示脚本文件在其下运行的进程ID。对于任何给定的脚本,当它运行时,它将只有一个“主”进程标识。不管您调用了多少个子shell,$$将始终返回与脚本关联的第一个进程标识。 $BASHPID将显示当前bash实例的进程ID,因此在子shell中它将与可能调用它的**“顶级”**bash不同。$BASH_SUBSHELL 表示你所在的“subshell级别”。如果你不在任何子级别,则级别为零。如果你在主程序中启动子shell,则该子shell级别为1。如果在该子shell内启动子shell,则级别为2,依此类推。

4. bash的基本语法

4.1 单引号和双引号

单引号将其中的内容都作为了字符串来,忽略所有的命令和特殊字符,类似于一个字符串的用法

双引号与单引号的区别在于其可以包含特殊字符(单引号直接输出内部字符串,不解析特殊字符;双引号内则会解析特殊字符),包括', ", $, \,如果要忽略特殊字符,就可以利用\来转义,忽略特殊字符,作为普通字符输出:

1
2
3
4
5
6
7
[root@layne bashdir]# var=3
[root@layne bashdir]# echo '$var'
$var
[root@layne bashdir]# echo "$var"
3
[root@layne bashdir]# echo "Here \"this is a string\" is a string"
Here "this is a string" is a string

4.2 命令替换

命令替换允许我们将shell命令的输出赋值给变量。它是脚本编程中的一个主要部分。

命令替换会创建子shell进程来运行相应的命令。子shell是由运行该脚本的shell所创建出来的一个独立的子进程,由该子进程执行的命令无法使用(父)脚本中所创建的变量(除非是export的环境变量)。

反引号提升扩展优先级,先执行反引号的内容,再执行其他的

1
2
3
4
5
[root@layne bashdir]# myvar=echo "hello"
-bash: hello: command not found
[root@layne bashdir]# myvar=`echo "hello"` #使用反引号,先创建子shell执行echo "hello",再将执行的结果赋给myvar
[root@layne bashdir]# echo $myvar
hello

再来看一个例子:

1
2
3
4
5
6
7
[root@layne tdir]# scp /root/tdir/test.txt  layne2:/root/tdir #把当前主机的test.txt文件copy到layne2主机的同一个目录下,前提是layne2的/root/tdir目录存在
root@layne2's password:
test.txt 100% 20 0.0KB/s 00:00
[root@layne tdir]# scp /root/tdir/log.txt layne2:`pwd` # 作用同上,相当于先输入pwd,把pwd的返回结果赋值给`pwd`
root@layne2's password:
log.txt 100% 12 0.0KB/s 00:00
[root@layne tdir]#

4.3 逻辑判断

  • command1 && command2
    • n 如果command1退出状态是0(0代表正确),则执行command2
  • command1 || command2
    • n 如果command1的退出状态不是0,则执行command2
1
2
3
4
5
6
7
8
9
10
[root@layne2 ~]# test -d /hello || echo "文件夹/hello不存在"
文件夹/hello不存在
[root@layne2 ~]# test -d /bin && echo "文件夹/bin存在"
文件夹/bin存在
[root@layne2 ~]# test -f a.txt && rm -f a.txt && touch a.txt #-f 判断是不是文件
[root@layne2 ~]# ls / && echo ok
bin boot dev etc home lib lib64 lost+found media mnt opt proc root sbin selinux srv sys tmp usr var
ok
[root@layne2 ~]# ls / || echo ok
bin boot dev etc home lib lib64 lost+found media mnt opt proc root sbin selinux srv sys tmp usr var

4.4 表达式

(1)算术表达式

  • let 算数运算表达式
    • let C=$A+$B (很重要)
  • $[算术表达式]
    • C=$[$A+$B]
  • $((算术表达式))
    • C=$((A+B))
    • C=$((A+B+1)) #在原来结果的基础上再加1
  • expr算术表达式
    • 表达式中各操作数及运算符之间要有空格,同时要使用命令引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@layne2 ~]# a=1
[root@layne2 ~]# b=2
[root@layne2 ~]# let c=$a+$b
[root@layne2 ~]# echo $c
3
[root@layne2 ~]# d=$((a+b+1))
[root@layne2 ~]# echo $d
4
[root@layne2 ~]# ((a++))
[root@layne2 ~]# echo $a
2
[root@layne2 ~]# e=$((--d+a))
[root@layne2 ~]# echo $e
5

(2)条件表达式

  • [ 表达式 ]
  • test 表达式
  • [[ 表达式 ]]
1
2
3
4
5
6
7
test  3  -gt  2  &&  echo  ok
等价于
[ 3 -gt 2 ] && echo ok

test 3 -gt 8 && echo ok
等价于
[ 3 -gt 8 ] && echo ok

5. bash的分支

  • if
  • case

5.1 if分支

单分支结构

1
2
3
4
5
6
7
8
9
10
if [ 条件判断 ] 
then
//命令
fi

或者

if [ 条件判断 ]; then
条件成立执行,命令;
fi # 将if反过来写,就成为fi,结束if语句

双分支结构

1
2
3
4
5
if [ 条件1 ];then 
条件1成立执行,指令集1
else
条件1不成执行指令集2;
fi

多分支结构

1
2
3
4
5
6
7
if [ 条件1 ];then
条件1成立,执行指令集1
elif [ 条件2 ];then
条件2成立,执行指令集2
else
条件都不成立,执行指令集3
fi

使用[命令判断

1
2
3
4
5
6
7
8
if [ 3 -gt 2 ]; then
echo ok
fi

if [ 3 -gt 2 ]
then
echo ok
fi

示例:

创建sh01.sh,内容为:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
a=20
if [ $a -gt $1 ];
then
echo "你输入的数字太小"
elif [ $a -eq $1 ];
then
echo "恭喜哈,数字相等"
else
echo "你输入的数字太大"
fi

执行:

1
2
3
4
5
6
7
8
9
10
11
12
[root@layne bashdir]# chmod +x sh01.sh
你输入的数字太大
[root@layne bashdir]# ./sh01.sh 20
恭喜哈,数字相等
[root@layne bashdir]# ./sh01.sh 15
你输入的数字太小
[root@layne bashdir]# ./sh01.sh 25
你输入的数字太大
[root@layne bashdir]# ./sh01.sh
./sh01.sh: line 3: [: 20: unary operator expected
./sh01.sh: line 6: [: 20: unary operator expected
你输入的数字太大

注意:if和中括号之间要有空格,中括号和条件表达式之间要有空格

;和then之前有空格没空格都可以

5.2 case分支

1
2
3
4
5
6
7
8
9
10
case $变量名称 in “值1") 
程序段1
;;
“值2")
程序段2
;;
*)
exit 1
;;
esac # esac是case的逆置,代表case的结尾

上面*类似java里面的default

exit 1echo $?获取到的返回值,exit 0代表正确的返回,exit 1代表错误的返回结果

案例:判断用户输入的是哪个数,1-7显示输入的数字,1显示 Mon,2 :Tue,3:Wed,4:Thu,5:Fir,6-7:weekend,其它值的时候,提示:please input [1,7],该如何实现?

创建sh02.sh,内容为:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/bin/bash
read -p "please input a number[1,7]:" num
case $num in
1)
echo "Mon"
;;
2)
echo "Tue"
;;
3)
echo "Wed"
;;
4)
echo "Thu"
;;
5)
echo "Fir"
;;
[6-7])
echo "weekend"
;;
*)
echo "please input [1,7]"
;;

esac

或者
#!/bin/bash
# Mon,2 :Tue,3:Wed,4:Thu,5:Fir,6-7:weekend
case $1 in 1)
echo "Mon"
exit 0
;;
2)
echo "Tue"
exit 0
;;
3)
echo "Wed"
exit 0
;;
4)
echo "Thu"
exit 0
;;
5)
echo "Fir"
exit 0
;;
[6-7])
echo "weekend"
exit 0
;;
*)
echo "please input [1,7]"
exit 1
;;

esac

执行:

1
2
3
4
5
6
7
[root@layne bashdir]# chmod +x sh02.sh
[root@layne bashdir]# ./sh02.sh
please input a number[1,7]:5
Fir
[root@layne bashdir]# ./sh02.sh
please input a number[1,7]:8
please input [1,7]

6. bash的循环

6.1 while循环

1
2
3
4
5
6
7
8
while [ condition ] ; do # 如果do换一行就不要;了
命令
done
或者
while [ condition ]
do
命令
done

注意:while也与中括号之间有空格

案例一:每隔两秒打印系统负载情况,如何实现?

创建while1.sh,内容如下:

1
2
3
4
5
6
#!/bin/bash
while true
do
uptime #显示系统负载情况
sleep 2 #休眠2秒
done

执行:

1
2
3
4
5
6
[root@layne bashdir]# chmod +x while1.sh
[root@layne bashdir]# ./while1.sh
23:05:07 up 12:56, 1 user, load average: 0.00, 0.00, 0.00
23:05:09 up 12:56, 1 user, load average: 0.00, 0.00, 0.00
23:05:11 up 12:56, 1 user, load average: 0.00, 0.00, 0.00
# 按ctrl +c 终止进程

案例二:使用while循环,编写shell脚本,计算1+2+3+…+100的和并输出,如何实现?

创建while2.sh,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
sum=0
i=1
while [ $i -le 100 ] #while和[之间要加一个空格 true则执行
do
sum=$((sum+i))
i=$((i+1)) #运算结果为变量赋值可以使用$(( … ))
done
echo "the result of '1+2+3+...+100' is $sum"

或者:

#!/bin/bash
sum=0
i=1
while [ $i -le 100 ] #while和[之间要加一个空格 true则执行
do
let sum=$sum+$i
let i=$i+1 #运算结果为变量赋值可以使用$(( … )) 或let i++ 或 let i+=1
done
echo "the result of '1+2+3+...+100' is $sum"

6.2 for循环

1
2
3
4
for 变量名 in 变量取值列表
do
命令
done

案例1:创建for1.sh,输入以下内容

1
2
3
4
5
#!/bin/sh
for num in 1 2 3 4
do
echo $num
done

执行:

1
2
3
4
5
6
[root@layne bashdir]# chmod +x for1.sh
[root@layne bashdir]# ./for1.sh
1
2
3
4

使用大括号的方法:

1
2
3
4
5
6
[root@layne bashdir]# echo {1..8}
1 2 3 4 5 6 7 8
[root@layne bashdir]# echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z
[root@layne bashdir]# echo 10.13.20.{1..3}
10.13.20.1 10.13.20.2 10.13.20.3

编辑for1.sh,输入以下内容:

1
2
3
4
5
#!/bin/sh
for num in {1..4}
do
echo $num
done

案例2:使用seq –s 分隔符 起始 步长 终点

1
2
3
4
[root@layne bashdir]# seq -s " " 2 2 100
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 92 94 96 98 100
[root@layne bashdir]# seq -s " " 1 1 5
1 2 3 4 5

创建for2.sh ,内容为:

1
2
3
4
5
#!/bin/sh
for num in `seq -s " " 1 1 5`
do
echo $num
done

注意:for后面``里面是执行的命令

执行for2.sh

1
2
3
4
5
6
7
[root@layne bashdir]# chmod +x for2.sh
[root@layne bashdir]# ./for2.sh
1
2
3
4
5

7. bash练习

7.1 案例1

  • 用户给定路径
  • 输出文件大小最大的文件
  • 递归子目录

$IFS默认空格、制表符和换行符 都可以识别

for循环的时候 空格、制表符和换行符都可以识别(在不改$IFS情况下),如果要改变$IFS,让它识别这三个中的一个,比如让它只识别换行符,把IFS=$'\n',这样一行一个循环,即一次一行来循环操作,不用管一行里面的空格或制表符。

du -a 列出所有的文件与目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@layne bashdir]# du #不用-a,不会列出所有的
32 ./rdir
120 .
[root@layne bashdir]# du -a
4 ./rdir/a.sh
4 ./rdir/test.txt
32 ./rdir
4 ./mylog2.log
4 ./log.err
4 ./mylog.log
4 ./mylog3.log
4 ./nohup.out
4 ./test.sh
4 ./my.log
4 ./sh01.sh
...

步骤一:

思路:使用du命令加-a遍历用户指定目录($1获取)的所有文件,使用管道将结果传递给sort,让sort使用数值序倒序排序,依次输出各个条目。

1
2
3
4
5
6
7
8
#!/bin/bash
oldIFS=$IFS
IFS=$'\n' # 将for循环获取不同元素的标记修改为换行符
# for循环获取元素的时候使用空格区分各个不同的元素
for item in `du -a $1 | sort -nr`; do
echo $item
done
IFS=$oldIFS #用完后重置IFS变量的值。
  • $1是第一个参数
  • sort -nr :因为du输出的两列,第一列是大小,第二列是目录或文件,中间用制表符(\t)隔开,sort可以默认识别这种写法,并默把两列分开后用第一列排序,这里是缩写,全称为sort -t '\t' -k 1 -nr sort.txt

因为要获取最大的文件,需要改进

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
oldIFS=$IFS
IFS=$'\n' # 将for循环获取不同元素的标记修改为换行符
# for循环获取元素的时候使用空格区分各个不同的元素
for item in `du -a $1 | sort -nr`; do
fileName=`echo $item | awk '{print $2}'`
if [ -f $fileName ]; then
echo $fileName
break
fi
done
IFS=$oldIFS #用完后重置IFS变量的值。
  • awk默认也是识别制表符,换行符,空格等
  • awk '{print $2}' 这是取第二列,不是文件的第二个参数

7.2 案例2

问题:

​ 循环遍历文件每一行:流程控制语句 IFS

  • 定义一个计数器
  • 打印num正好是文件行数

思路:

  • 管道
  • 重定向
  • 命令替换

方案一

思路:定义一个计数器,使用for循环从文件内容按行获取元素,每个元素是一行,在for循环中递增计数器用于计行数

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
num=0
oldIFS=$IFS
IFS=$'\n'
# hello world are you ok
for item in `cat $1`; do
echo $item
((num++)) # 或$((num++)) 或 let num++
done
echo "line number is :$num"
IFS=$oldIFS

解释:

1
for item in `cat $1`; do

这里for循环可以识别换行符,所以可以遍历(即一行循环一次)

方案二:

思路:先使用wc数出总行数,然后使用带下标的for循环分页遍历该文件,打印出每行,最后打印出总行数。

注意:

1、cat $1 | wc -l:返回行数

2、如果for循环想要java的写法,则可以在for后面用两个((包起来 ,如for ((i=1;i<=lines;i++))

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
num=0
lines=`cat $1 | wc -l`
hello=$1
for ((i=1;i<=lines;i++)); do
line=`head -$i $hello | tail -1`
echo $line
((num++))
done
echo "line number is :$num"

方案三:

思路:将while的read标准输入重定向到文件file.txt,按行读取,每读一行,就打读到的行记录,同时计数器+1,最后得出总行数。

1
2
3
4
5
6
7
#!/bin/bash
num=0
while read line ;do
echo $line
((num++))
done < $1
echo "line number is : $num"

解释:>>>都属于输出重定向,<属于输入重定向

while read line [linux] shell 学习可参考:https://blog.csdn.net/qq_22083251/article/details/80484176

写的不错。

方案四:

使用管道命令,但是通过管道无法向父进程传递数据管道两边的命令在当前shell的两个子进程中执行),需要将结果数据写到文件中,运行结束后,将数据读出来即可

1
2
3
4
5
6
7
#!/bin/bash
num=0 # 该num和管道后面的num不是一回事,因为num没有export
cat $1 | while read line; do
echo $line
((num++)) #即使当前环境没有num这个变量,((num++))一样可以使用
done
echo "line number is: $num" > $2 #将结果重定向到一个文件