shell高级用法笔记

子shell及嵌套模式

重定向

0:是一个文件描述符,表示标准输入(stdin)
1:是一个文件描述符,表示标准输出(stdout)
2:是一个文件描述符,表示标准错误(stderr)

# 在标准情况下, 这些FD分别跟如下设备关联: 
	stdin(0): keyboard 键盘输入,并返回在前端 
	stdout(1): monitor 正确返回值 输出到前端 
	stderr(2): monitor 错误返回值 输出到前端
	
# & 是一个描述符,如果1或2前不加&,会被当成一个普通文件
# 1>&2 意思是把标准输出重定向到标准错误.
# 2>&1 意思是把标准错误输出重定向到标准输出。
# &>filename 意思是把标准输出和标准错误输出都重定向到文件filename中

实例一

# 当前目录只有a.txt
[root@redhat box]# ls 
a.txt 
[root@redhat box]# ls a.txt b.txt 
ls: b.txt: No such file or directory 由于没有b.txt这个文件, 于是返回错误值, 这就是所谓的2输出 
a.txt 而这个就是所谓的1输出
----

[root@redhat box]# ls a.txt b.txt 1>file.out 2>file.err 
执行后,没有任何返回值. 原因是, 返回值都重定向到相应的文件中了,而不再前端显示 
[root@redhat box]# cat file.out 
a.txt 
[root@redhat box]# cat file.err 
ls: b.txt: No such file or directory 
一般来说, "1>" 通常可以省略成 ">". 
即可以把如上命令写成: ls a.txt b.txt >file.out 2>file.err 
有了这些认识才能理解 "1>&2""2>&1". 
1>&2 正确返回值传递给2输出通道 &2表示2输出通道 
如果此处错写成 1>2, 就表示把1输出重定向到文件2中. 
2>&1 错误返回值传递给1输出通道, 同样&1表示1输出通道. 
举个例子
[root@redhat box]# ls a.txt b.txt 1>file.out 2>&1   将错误的输出输出到标准输出中
[root@redhat box]# cat file.out 
ls: b.txt: No such file or directory 
a.txt 
$ ls nodir 1> filename.txt 2>&1
$ cat filename.txt
$ ls: nodir: No such file or directory

# 上面例子把 标准输出 重定向到文件 filename.txt,然后把 标准错误重定向到标准输出,所以最后的错误信息也通过标准输出写入到了文件中

实例二

# Linux重定向中 >&2 怎么理解?echo "aaaaaaaaaaaaaaaa" >&2 怎么理解?

>&21>&2 也就是把结果输出到和标准错误一样;之前如果有定义标准错误重定向到某log文件,那么标准输出也重定向到这个log文件
如:ls 2>a1 >&2 (等同 ls >a1 2>&1)
把标准输出和标准错误都重定向到a1,终端上看不到任何信息

# 只有a.txt
	ls b.txt a.txt 2>file 1>&2 #将标准输出输出到错误输出中
	ls b.txt a.txt 1>file 2>&1 #将错误输出输出到标准输出中

实例三

# 下列输出什么
	echo  hello 1>&2 |grep aaa
	echo  hello 2>&1 |grep aaa

# 结果
	第一个输出hello,第二个无输出
	
# 原因
	1>&2  将正确输出重定向到标准错误2通道
	2>&1  将错误输出重定向到标准输出1通道
	echo  hello 1>&2 后面跟了一个 | grep aaa,| 是管道,意思就是将第一条指令的结果放到第二条指令中,管道命令右边只处理左边的标准正确输出,也就是通道1
	echo  hello    1>&2 将结果输出到错误2通道,也就是在终端屏幕上显示hello,`而标准输出1通道没有值,| grep aaa 搜素左边传来的标准输出通道1的结果,发现没有aaa,所以没有输出,最终屏幕只显示一个hello`
	echo  hello    2>&1   将错误返回值输出到标准1通道,`不过该语句正确,没有错误,通道2也就为空`,屏幕没有显示,`| grep aaa 搜素左边传来的标准输出通道1的结果,通道1有hello 但是没有 aaa  所以没有输出,所以屏幕没有显示`

实例四

# 下列输出什么
	echo  hello 1>&2 | grep  e
	echo  hello 2>&1 | grep  e 

# 结果
	echo  hello  1>&2   将正确结果hello输出到错误2通道,也就是在终端屏幕上显示hello,而标准输出1通道没有值,| grep e 搜素左边传来的标准输出通道1的结果,发现没有e,所以没有输出,最终屏幕只显示一个hello
	echo  hello  2>&1   将错误返回值输出到标准1通道,不过该语句正确,没有错误,通道2也就为空,屏幕没有显示,| grep e 搜素左边传来的标准输出通道1的结果,发现有e,所以输出hello #e被加深颜色了

后台运行

  • 方法一
# shell脚本中当我们需要把一个任务放在后台运行时,通常我们会使用&符号:(此时主进程会继续往下执行,而子进程会在后台启动运行)
	subcommand &
  • 方法二
# nohup起两个作用:
1):正如名字所声称的,`忽略所有发送给子命令的挂断(SIGHUP)信号`
	nohup subcommand &
# 这样所有发给subcommand的SIGHUP信号都被忽略,subcommand就不会收到SIGHUP信号

# 什么是SIGHUP信号呢?
	简单的理解可以是终端结束时,操作系统会发送SIGHUP信号到后台进程
	
2):重定向子命令的标准输出(stdout)和标准错误(stderr),我们可以在终端看到如下输出:
	nohup: appending output to "nohup.out"

# 表示subcommand的标准输出和标准错误被重定向到nohup.out文件;如果没有使用nohup方式,则subcommand的标准输出和标准错误是复用父进程的标准输出和标准错误

1.后台运行(&)

$ cat testshell0.sh 
#!/bin/bash
#trap "echo \"SIGHUP is received\"" 1

for i in {1..10000}; do
       echo "$i: in $0"
  1>&2 echo "$i: in $0"
  	sleep 1
done

$ cat testshell1.sh  
#!/bin/bash

./testshell0.sh &

for i in {1..10000}; do
  echo "$i: in $0"
  sleep 1
done
$ ./testshell1.sh 

# 输出结果
1: in ./testshell1.sh
1: in ./testshell0.sh
1: in ./testshell0.sh
2: in ./testshell1.sh
2: in ./testshell0.sh
2: in ./testshell0.sh
CTRL-C(^C)
$ 3: in ./testshell0.sh
3: in ./testshell0.sh
4: in ./testshell0.sh
5: in ./testshell0.sh

# 脚本testshell1.sh以后台方式(&)调用testshell0.sh;我们看到testshell1.sh和testshell0.sh的输出都在屏幕上,而当(CTRL-C)杀掉testshell1.sh的时候,testshell0.sh继续在运行,继续往屏幕打印输出

2. nohup方式后台运行(&)

$ cat testshell0.sh 
#!/bin/bash

#trap "echo \"SIGHUP is received\"" 1

for i in {1..10000}; do
       echo "$i: in $0"
  1>&2 echo "$i: in $0"

  sleep 1
done
$ cat testshell1.sh  
#!/bin/bash

nohup ./testshell0.sh &

for i in {1..10000}; do
  echo "$i: in $0"
  sleep 1
done
$ ./testshell1.sh 
nohup: appending output to "nohup.out"
1: in ./testshell1.sh
1: in ./testshell0.sh
1: in ./testshell0.sh
2: in ./testshell1.sh
2: in ./testshell0.sh
2: in ./testshell0.sh
CTRL-C(^C)
$


# 脚本testshell1.sh以nohup的方式在后台(&)调用testshell0.sh;我们看到testshell1.sh的输出在屏幕上,testshell0.sh的输出在文件nohup.out里:
$ tail -f nohup.out
1: in ./testshell0.sh
1: in ./testshell0.sh
2: in ./testshell0.sh
2: in ./testshell0.sh
3: in ./testshell0.sh
3: in ./testshell0.sh

# 而当(CTRL-C)杀掉testshell1.sh的时候,testshell0.sh继续在运行,继续往nohup.log里面打印输出。在这两个例子中,当testshell1.sh已经停止时,testshell0.sh并不会结束,而都能继续运行

3. 后台运行(&) + 关闭终端

# 代码和运行方法同例子1,只是在运行时关闭终端。结果testshell1.sh和testshell0.sh都结束了
$ ps -ef | grep testshell | grep -v grep
$ 

4. nohup方式后台运行(&) + 关闭终端

# 代码和运行方法同例子2,只是在运行时关闭终端。结果testshell1.sh结束了,而testshell0.sh还在继续运行。
$ ps -ef | grep testshell | grep -v grep
<uid>  <pid>     1  0 22:29 ?        00:00:00 /bin/bash ./testshell0.sh
$ 

# 需要注意的是,此时testshell0.sh的父进程变成了进程号1,而不是testshell1.sh,因为testshell1.sh已经死了,操作系统接管了testshell1.sh进程

# 对比例子3和例子4,差别是是否以nohup的方式运行testshell0.sh,在例子3不是以nohup的方式,这样当终端结束的时候,testshell0.sh会收到SIGHUP信号,缺省的处理方式是杀掉自己,所以testshell0.sh也死了;而例子4使用了nohup方式,他会忽略SIGHUP信号,所以testshell0.sh继续运行

5. 后台运行(&) + 关闭父进程+关闭终端

# 代码和运行方法同例子1。启动进程testshell1.sh
$ ./testshell1.sh 
1: in ./testshell1.sh
1: in ./testshell0.sh
1: in ./testshell0.sh
2: in ./testshell1.sh
2: in ./testshell0.sh
2: in ./testshell0.sh
3: in ./testshell1.sh
3: in ./testshell0.sh
3: in ./testshell0.sh

# 此时testshell1.sh和testshell0.sh同时在运行,往终端打印输出。
$ ps -ef | grep testshell | grep -v grep
<uid>  13789 13642  0 22:34 pts/10   00:00:00 /bin/bash ./testshell1.sh
<uid>  13790 13789  0 22:34 pts/10   00:00:00 /bin/bash ./testshell0.sh

# 杀掉进程testshell1.sh,屏幕继续打印testshell0.sh的输出
CTRL-C(^C)
$ 4: in ./testshell0.sh
4: in ./testshell0.sh
5: in ./testshell0.sh
5: in ./testshell0.sh

# 查看进程状态
$ ps -ef | grep testshell | grep -v grep
<uid>  13790     1  0 22:34 pts/10   00:00:00 /bin/bash ./testshell0.sh
$ 

# 子进程testshell0.sh继续在运行,此时我们退出终端,再查看进程状态
$ ps -ef | grep testshell | grep -v grep
<uid>  13790     1  0 22:34 pts/10   00:00:00 /bin/bash ./testshell0.sh
$ 

# 为什么子进程testshell0.sh还在呢?既然终端已经退出了,按理操作系统应该给testshell0.sh发送SIGHUP信号,然后导致testshell0.sh退出啊?
原因是testshell0.sh是以后台任务的方式由testshell1.sh提交,当testshell1.sh已经退出后,testshell0.sh变成了孤儿进程,操作系统自动收集这些孤儿进程,此时我们看到testshell0.sh的父进程已经变成进程号1了,这样testshell0.sh和当前终端已经没有了关系,他们失去了联系,从而当当前终端结束的时候,testshell0.sh不会也不需要得到什么消息,那么也就不会收到SIGHUP信号了。

子shell产生

# 子Shell从父Shell继承得来的属性如下:
	当前工作目录
	环境变量
	标准输入、标准输出和标准错误输出
	所有已打开的文件标识符
	
# 子Shell不能从父Shell继承的属性:
	除环境变量和.bashrc文件中定义变量之外的Shell变量
	
# source和.执行文件不会生成子shell,不创建子shell,在当前shell环境下读取并执行脚本中的命令,相当于顺序执行脚本里面的命令

&产生子shell

# 代码
parent_var="这是父进程变量"    #定义父shell变量
echo "启动父进程: $BASH_SUBSHELL" #输出父shell层级,BASH_SUBSHELL为系统环境变量
{
  echo "启动子进程: $BASH_SUBSHELL" #输出子shell层级,和父shell层级对比
  sub_var="这是子进程变量"   #定义子shell的变量
  echo "子进程变量=$sub_var"
  echo "父进程变量=$parent_var"  #查看子shell是否能继承父shell的变量
  sleep 3                        #休息2s,用于分辨此处的子shell执行是不是异步的
  echo "子进程结束"        #子shell结束
} &
echo "父进程结束: $BASH_SUBSHELL"
if [ -z "$sub_var" ];then
  echo "子进程变量没有继承到"
else
  echo "子进程变量继承到了"
fi

# 输出
[root@iZbp13ws3ovvqapul2xa6fZ ~]# bash test.sh 
启动父进程: 0
父进程结束: 0
子进程变量没有继承到
[root@iZbp13ws3ovvqapul2xa6fZ ~]# 启动子进程: 1
子进程变量=这是子进程变量
父进程变量=这是父进程变量
[root@iZbp13ws3ovvqapul2xa6fZ ~]# 子进程结束  说明是异步执行的

# 结论
	在shell中可以使用&产生子shell
	由&产生的子shell可以直接引用父shell定义的本地变量
	由&产生的子shell中定义的变量不能被父shell引用
	在shell中使用&可以实现其他程序的多线程并发功能

管道产生子shell

# 代码
#!/bin/bash

parent_var="这是父进程变量"    #定义父shell变量
echo "启动父进程: $BASH_SUBSHELL" #输出父shell层级,BASH_SUBSHELL为系统环境变量
echo "" |\  #管道换行
{
  echo "启动子进程: $BASH_SUBSHELL" #输出子shell层级,和父shell层级对比
  sub_var="这是子进程变量"   #定义子shell的变量
  echo "子进程变量=$sub_var"
  echo "父进程变量=$parent_var"  #查看子shell是否能继承父shell的变量
  sleep 3                        #休息2s,用于分辨此处的子shell执行是不是异步的
  echo "子进程结束"        #子shell结束
}

echo "父进程结束: $BASH_SUBSHELL"
if [ -z "$sub_var" ];then
  echo "子进程变量没有继承到"
else
  echo "子进程变量继承到了"
fi

# 结果
[root@iZbp13ws3ovvqapul2xa6fZ ~]# bash test.sh 
启动父进程: 0
启动子进程: 1
子进程变量=这是子进程变量
父进程变量=这是父进程变量
子进程结束
父进程结束: 0
子进程变量没有继承到

# 结论
	在shell中使用管道可以产生子shell
	由管道产生的子shell可以直接引用父shell定义的本地变量
	由管道产生的子shell中定义的变量不能被父shell引用
	由管道产生的子shell不能异步执行,只能在执行完毕后才能返回父shell环境

()产生子shell

# 代码
#!/bin/bash

parent_var="这是父进程变量"    #定义父shell变量
echo "启动父进程: $BASH_SUBSHELL" #输出父shell层级,BASH_SUBSHELL为系统环境变量
( #命令括起来
  echo "启动子进程: $BASH_SUBSHELL" #输出子shell层级,和父shell层级对比
  sub_var="这是子进程变量"   #定义子shell的变量
  echo "子进程变量=$sub_var"
  echo "父进程变量=$parent_var"  #查看子shell是否能继承父shell的变量
  sleep 3                        #休息2s,用于分辨此处的子shell执行是不是异步的
  echo "子进程结束"        #子shell结束
)

echo "父进程结束: $BASH_SUBSHELL"
if [ -z "$sub_var" ];then
  echo "子进程变量没有继承到"
else
  echo "子进程变量继承到了"
fi
# 结果
[root@iZbp13ws3ovvqapul2xa6fZ ~]# bash test.sh 
启动父进程: 0
启动子进程: 1
子进程变量=这是子进程变量
父进程变量=这是父进程变量
子进程结束
父进程结束: 0
子进程变量没有继承到

# 结论
	在shell中使用()可以产生子shell
	由()产生的子shell可以直接引用父shell定义的本地变量
	由()产生的子shell中定义的变量不能被父shell引用
	由()产生的子shell不能异步执行,只能在执行完毕后才能返回父shell环境

调用外部Shell脚本产生shell

不能异步执行

linux登陆linux,就获得一个bash,之后你的bash就是一个独立的进程,被称为pid的就是,之后你在bash下面执行的任何命令都是由这个bash所衍生的,那些被执行的命令被称为子进程。子进程只会继承父进程的环境变量,子进程不会继承父进程的自定义变量。那么你原本bash中的自定义变量在进入子进程后就会消失不见,一直到你离开子进程并回到原本的父进程后,这个变量才会出现除非把自定义变量设置为环境变量 export name

  • 实例一
# 代码
[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat 1.sh 
#!/bin/bash
echo "父进程开始"
first=1
`source 2.sh`  # . 2.sh 结果一样
echo $second
echo "父进程结束"

[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat 2.sh 
#!/bin/bash
echo "子进程开始"
echo $first
second=2
echo $second
sleep 3
echo "子进程结束"


# 结果
父进程开始
子进程开始
1
2
子进程结束
2
父进程结束

# 分析
	当前shell执行,不会生成子shell,所有shell共享环境变量,且`不能异步`
  • 实例二
# 这里只对外部适用

# 子Shell从父Shell继承得来的属性如下:
	当前工作目录
	环境变量
	标准输入、标准输出和标准错误输出
	所有已打开的文件标识符
	忽略的信号
# 子Shell不能从父Shell继承的属性,归纳如下:
	`除环境变量和.bashrc文件中定义变量之外的Shell变量` #除非使用export
	未被忽略的信号处理
	
# 代码
[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat 1.sh 
#!/bin/bash
echo "父进程开始"
first=1
bash 2.sh  # sh ./2.sh 结果一样
echo $second
echo "父进程结束"

[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat 2.sh 
#!/bin/bash
echo "子进程开始"
echo $first
second=2
echo $second
sleep 3
echo "子进程结束"


# 结果
[root@iZbp13ws3ovvqapul2xa6fZ ~]# bash 1.sh 
父进程开始
子进程开始

2
子进程结束

父进程结束

# 分析
	`不能异步``bash生成的子shell没有继承到父shell的本地变量` 除非使用export
  • 实例三
#代码
[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat 1.sh 
#!/bin/bash
echo "父进程开始"
first=1
export first     # 将其设置为环境变量
bash 2.sh
echo $second
echo "父进程结束"

[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat 2.sh 
#!/bin/bash
echo "子进程开始"
echo $first
second=2
echo $second
sleep 3
echo "子进程结束"


# 结果
父进程开始
子进程开始
1
2
子进程结束
			# 从这里可以知道父进程没继承到子shell的变量值
父进程结束

子shell的坑

读取文件方法

#方式1:在while循环结尾done,通过输入重定向指定读取的文件
while read line #每一行赋值给line
do	
	cmd
done<FILE

#方式二:使用cat读取文件内容,然后通过管道进入while循环处理,会有bug
cat FILE_PATH|while read line
do
	cmd
done

#方式三:采用exec读取文件后,然后进入while循环处理
exec <FILE
sum=0
while read line
do
	cmd
done
#!/bin/bash
function readPassword(){
	local retval=0
	local count=0
	cat /tmp/passwd|while read line #使用cat读取文件,管道用while循环,每次读取一行
	do
		array[$count]="$line"
		if [ -z "${array[$count]}" ];then
			retval=1 && break
		fi
	done
	return $retval
}
function main(){
	readPassword
	echo "retval=$?"
	echo "array[0]=${array[0]}"
}
main

# 结果如下
retval=0
array[0]=

# 原因
	管道产生的子shell,子shell的特性之一就是在`子shell中定义的变量无法被父shell引用`,子shell能继承到父shell的变量
# 解决办法
	使用第1种方法

调用脚本模式说明

fork

	使用这个方法时,系统会开启一个子shell执行调用的脚本,子shell执行的时候,父shell还在,子shell执行完毕后返回到父shell,最后的结论是:子shell可以从父shell继承环境变量,但是默认情况下子shell的环境变量不能带回父shell
	fork模式调用脚本主要应用于常规嵌套脚本执行的场合,嵌套的脚本只是执行相应 的命令操作,不会生成相应的进程信息,`父脚本不需要引用嵌套的脚本内的变量及函数 等信息,其次在嵌套脚本中定义的变量及函数等不会影响到父脚本中相同的信息定义`

exec

	exec不需要开一个子shell来执行被调用的脚本,被调用的脚本与父脚本在同一个shell内执行,但是使用exec调用一个新脚本以后,父脚本中exec执行之后的内容就不会在执行了,这就是exec与source的区别
	`exec模式调用脚本需要应用于嵌套脚本在主脚本的末尾执行的场合`,因此,此种模 式的应用并不多见,并且可以被source模式完全取代。

source

	source模式调用脚本是比较重要且最常用的一种嵌套方式,主要应用之一是执行嵌 套脚本启动某些服务程序。例如:在利用嵌套脚本启动tomcat程序并生成PID程序文件 时,如果选择fork模式,那么生成的PID文件信息就和执行“ps-ef”命令输出的PID 信息不一致,这将会导致执行kill cat tomcat_pid'命令时,不能正确关闭tomcat程序 而选择source模式可以解决此问题。
	source模式调用脚本的另外一个应用就是使得嵌套脚本中的变量及函数等信息被父 脚本使用,从而实现更多的业务处理

命名规范

全局变量定义

全局变量也称环境变量,它的定义应全部大写,如APACHE_ ERR或APACHEERR,名字对应的语义要尽量清晰,能够正确表达变量内容的含义,对于过长的英文单词可用前几个字符代替。多个单词间可用“_” 号连接全局变量的定义一般放在系统的全局路径中,并且最好采用export来定义,全局变量一般可以在任意子Shell中直接使用(特殊情况除外,例如:定时任务执行Shell时就最好在Shell里重新定义这些全局变量,否则可能会出现问题)

[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat test.sh 
#!/bin/bash
a=12
export a
bash test1.sh


[root@iZbp13ws3ovvqapul2xa6fZ ~]# cat test1.sh 
#!/bin/bash
echo $a

# export 将变量传递到子shell了
# 结论
	1、执行脚本时是在一个子shell环境运行的,脚本执行完后该子shell自动退出;
	2、一个shell中的系统环境变量会被复制到子shell中(用export定义的变量);
	3、一个shell中的系统环境变量只对该shell或者它的子shell有效,该shell结束时变量消失
(并不能返回到父shell中)。
	3、不用export定义的变量只对该shell有效,对子shell也是无效的

局部变量定义

局部变量也称为普通变量,在常规脚本中,普通变量的命名也要尽可能统一,可以使用驼峰语法,即第二个单词的首字母大写,如oldboyTraining,或者每个单词首字母大写,如CheckUrl,当然也有网友喜欢采用全部大写或全部小写的方式,例如: CHECK、 check,选一种适合你的即可,或者跟着本书的规范走。

Shell函数中的变量可以使用local方式进行定义,使之只在本函数作用域内有效, 防止函数中的变量名称与外部程序中的变量相同,从而造成程序异常。下面是在函数中 定义变量的例子。

function TestFunc(){
	local i
	for ((i=0;i<n;i++))
	do
		do something
	done
}

变量的引用规范

在引用变量时,若变量前后都有字符,则需要使用${APACHE_ERR}(加大括号 的方式)引用变量,以防止产生歧义;当变量内容为字符串时,需要使用"${APACHE_ ERR}"(外面加双引号的方式)引用变量;当变量内容为整数时,则最好直接使用$APACHE ERR来引用变量。全局变量、局部变量、函数变量、数组变量等都是如此

函数命名

Shell函数的命名可采用单词首字母大写的形式,如CreateFile(),并且语义要清晰, 比如,使用CreateFile()代替CFile(),也可以使用小写形式,如createfile(),可以加前后缀,如后缀为Max则为最大值,为Min则表示最小值,前缀Is为判断 型函数,Get为取值函数,Do则为处理函数,这也有益于对函数功能的理解,使函数名 更直观、更清晰。

字符串相关操作

#!/bin/sh

#测试各种字符串比较操作。
#shell中对变量的值添加单引号,爽引号和不添加的区别:对类型来说是无关的,即不是添加了引号就变成了字符串类型,
#单引号不对相关量进行替换,如不对$符号解释成变量引用,从而用对应变量的值替代,双引号则会进行替代
#author:tenfyguo

A="$1"
B="$2"

echo "输入的原始值:A=$A,B=$B"

#判断字符串是否相等
if [ "$A" = "$B" ];then
echo "[ = ]"
fi

#判断字符串是否相等,与上面的=等价
if [ "$A" == "$B" ];then
echo "[ == ]"
fi

#注意:==的功能在[[]]和[]中的行为是不同的,如下

#如果$a以”a”开头(模式匹配)那么将为true 
if [[ "$A" == a* ]];then
echo "[[ ==a* ]]"
fi

#如果$a等于a*(字符匹配),那么结果为true
if [[ "$A" == "a*" ]];then
echo "==/"a*/""
fi


#File globbing(通配) 和word splitting将会发生, 此时的a*会自动匹配到对应的当前以a开头的文件
#如在当前的目录中有个文件:add_crontab.sh,则下面会输出ok
#if [ "add_crontab.sh" == a* ];then 
#echo "ok"
#fi
if [ "$A" == a* ];then
echo "[ ==a* ]"
fi

#如果$a等于a*(字符匹配),那么结果为true
if [ "$A" == "a*" ];then
echo "==/"a*/""
fi

#字符串不相等
if [ "$A" != "$B" ];then
echo "[ != ]"
fi

#字符串不相等
if [[ "$A" != "$B" ]];then
echo "[[ != ]]"
fi

#字符串不为空,长度不为0
if [ -n "$A" ];then
echo "[ -n ]"
fi

#字符串为空.就是长度为0.
if [ -z "$A" ];then
echo "[ -z ]"
fi

#需要转义<,否则认为是一个重定向符号
if [ $A /< $B ];then
echo "[ < ]"  
fi

if [[ $A < $B ]];then
echo "[[ < ]]"  
fi

#需要转义>,否则认为是一个重定向符号
if [ $A /> $B ];then
echo "[ > ]"  
fi

if [[ $A > $B ]];then
echo "[[ > ]]"  
fi


获取字符串长度

# 方法一:利用${#str}来获取字符串长度
	str="abc"
	echo ${#abc}

# 方法二:利用awk的length方法
	str='abc'
	echo ${str}|awk '{print length($0)}'
	备注:最好用{}来放置变量,可以用length($0)来统计文件中每行的长度
	
# 方法三:利用awk的NF项来获取字符串长度
	str='abc'
	echo $str|awk -F "" '{print NF}'
	备注:NF为域的个数,即单行字符串的长度

# 方法四:利用wc -L参数获取字符串长度
	echo ${str}|wc -L
	多行文件:打印最长行的长度
	单行字符串:打印单行字符串长度

# 方法五:利用wc -c,结合echo -n 参数
	echo -n "abc"|wc -c
	-c:统计字符个数
	-n:去除\n换行符,不去的话,默认带换行符,这时候就是4了
	
# 方法六:利用expr的length方法
	str='abc'
	expr length ${str}
	
# 方法七:利用expr的$str:".*"技巧
	str='abc';expr $str : ".*"
	.*:代表任意字符来匹配字符串,结果匹配到的是6个,即字符串的长度为6

echo颜色大全

echo 显示内容颜色,需要使用 -e 参数
	-e :打开反斜杠转义 (默认不打开) ,可以转义 “\n, \t” 等
	-n:在最后不自动换行

#字体颜色:30m-37m 黑、红、绿、黄、蓝、紫、青、白
str="kimbo zhang"
echo -e "\033[30m ${str}\033[0m"      ## 黑色字体
echo -e "\033[31m ${str}\033[0m"      ## 红色
echo -e "\033[32m ${str}\033[0m"      ## 绿色
echo -e "\033[33m ${str}\033[0m"      ## 黄色
echo -e "\033[34m ${str}\033[0m"      ## 蓝色
echo -e "\033[35m ${str}\033[0m"      ## 紫色
echo -e "\033[36m ${str}\033[0m"      ## 青色
echo -e "\033[37m ${str}\033[0m"      ## 白色
#背景颜色:40-47 黑、红、绿、黄、蓝、紫、青、白
str="kimbo zhang"
echo -e "\033[41;37m ${str} \033[0m"     ## 红色背景色,白色字体
echo -e "\033[41;33m ${str} \033[0m"     ## 红底黄字
echo -e "\033[1;41;33m ${str} \033[0m"   ## 红底黄字 高亮加粗显示
echo -e "\033[5;41;33m ${str} \033[0m"   ## 红底黄字 字体闪烁显示
echo -e "\033[47;30m ${str} \033[0m"     ## 白底黑字
echo -e "\033[40;37m ${str} \033[0m"     ## 黑底白字
\033[1;m 设置高亮加粗
\033[4;m 下划线
\033[5;m 闪烁
可以选择的编码如下所示(这些颜色是ANSI标准颜色):
      编码          颜色/动作
      0            重新设置属性到缺省设置
      1            设置粗体
      2            设置一半亮度(模拟彩色显示器的颜色)
      4            设置下划线(模拟彩色显示器的颜色)
      5            设置闪烁
      7            设置反向图象
      22           设置一般密度
      24           关闭下划线
      25           关闭闪烁
      27           关闭反向图象
      30           设置黑色前景
      31           设置红色前景
      32           设置绿色前景
      33           设置黄色前景
      34           设置蓝色前景
      35           设置紫色前景
      36           设置青色前景
      37           设置白色(灰色)前景
      38           在缺省的前景颜色上设置下划线
      39           在缺省的前景颜色上关闭下划线
      40           设置黑色背景
      41           设置红色背景
      42           设置绿色背景
      43           设置黄色背景
      44           设置蓝色背景
      45           设置紫色背景
      46           设置青色背景
      47           设置白色(灰色)背景
      49           设置缺省黑色背景
其他有趣的代码还有:
      \033[2J      清除屏幕
      \033[0q      关闭所有的键盘指示灯
      \033[1q      设置"滚动锁定"指示灯(Scroll Lock)
      \033[2q      设置"数值锁定"指示灯(Num Lock)
      \033[3q      设置"大写锁定"指示灯(Caps Lock)
      \033[15:40H   把关闭移动到第15行,40列
      \007        发蜂鸣生beep

转义问题

echo 转义

echo -e '#!/bin/bash\nif [ -z $(redis-cli -h $1 -p $2 -a $3 info|grep role:master) ]; then\n\texit 1\nelse\n\texit 0\nfi' >/tmp/test

# 如果使用双引号,里面的命令会被执行
[root@boy VSCode-linux-x64]# echo "export PATH=$(pwd)/bin:\$PATH" >> /etc/profile && source /etc/profile && echo $PATH
/root/VSCode-linux-x64/bin:/root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

EOF转义

# 在$符号前面加反斜杠,如:
    cat > test.sh <<EOF
    \$test
    EOF
# 给EOF加个双引号,如: #加了双引号,里面的变量是不会被解析了,会被当成字符串
    cat > test.sh <<"EOF"
    $test
    EOF
cat >/etc/my.cnf<<EOF
EOF
cat </etc/my.cnf #这种方法在shell脚本中不好使
EOF

函数返回值

函数子程序的标准输出赋值给主程序的变量

function test()
{
    echo 23
}

#调用test函数,并得到返回结果
ret=`test`

echo "ret:${ret}"

====
#!/bin/bash

function a(){
        echo 123
}

function b(){
        re=`a`
        echo $re
}
b

函数调用的return 值

function test()
{
    return 21
}

#调用test函数
test
#得到返回结果
ret=$?

echo "ret:${ret}"

echo中带命令

echo  "[ OK ] 即将备份的数据库有: $@ 正式备份时间为: $(date '+%Y-%m-%d %H:%m:%S')"
echo  "[ OK ] 即将备份的数据库有: $@ 正式备份时间为: `date '+%Y-%m-%d %H:%m:%S'`"

shell 用户输入的参数处理

  • 同时输入多个参数(可以是数值也可以是字符串)时,必须使用空 格分隔(要想在参数值中包含空格,就必须使用单引号或双引号)
  • 当参数多于9个后,必须在shell脚本内使用大括号将变量括起来,如${10}。从而可以使用任意个参数
  • shift命 令能够改变命令行参数的相对位置。默认将每个参数变量左移一个位置(变量$0不变,把$1丢弃,注意不可以恢复了!)在不清楚参数数目情况下,这是一个迭代参数的好办法
#!/bin/bash
db=$@
for i in $db #可以被遍历的
do
    echo $i
done
#!/bin/bash
function a(){
	echo $@
}
a $1 $2 $3
bash test.sh 1 2 3 4 #只有1,2,3会输出,因为只传了$1,$2,$3
  • shift移动参数
#!/bin/bash
function move(){
    echo $@
    echo $#
}
action=$1
case $1 in
    backup)
        shift
        move $@
    ;;
    *)
        useage
    ;;
esac

bash test.sh backup 1 2 3 4 # $@=1,2,3,4 $#=4
bash test.sh backup  # $@=空, $#=0

变量双引号问题

if [[ "$i" == "*.gz" ]]; then
	echo $i
fi
# $i 是文件路径例如:/backup/mysql/test-2021-05-09.sql.gz
# $i 必须要加双引号才能匹配得上,*.gz可以不加

# 为什么要加双引号??????????????
#!/bin/bash
if [[ "/backup/mysql/test1-2021-05-09.sql.gz" == "*.gz" ]]; then
    echo 1
else
    echo 2
fi
# 结果输出为2 原因是什么?????????????????????
"/backup/mysql/test1-2021-05-09.sql.gz" == *".gz" #这样可以,[[]]通配符时,不能加引号,正则怎么改
a=3
echo $a     # 3
echo '$a'   # $a
echo ''$a'' # 3 二个单引号
echo "$a"   # 3 双引号
#!/bin/bash
a=1
echo "'${a}'"

#结果
	'1'

生成随机数

re=$(awk 'BEGIN{srand();print rand()*10000}')
re=${re%%*.}

子shell和父shell执行结果分析

bash son.sh

cat /root/son.sh
#!/bin/bash
echo 1
[root@boy ~]# bash son.sh 
1
`夯住`

# 夯住时       
├─sshd(1076)───sshd(12592)─┬─bash(12596)───bash(12827)───sleep(12828)
                               └─bash(12673)───pstree(12829)
# 当执行完成后,子shell会回到父shell中
├─sshd(1076)───sshd(12592)─┬─bash(12596)
                               └─bash(12673)───pstree(12831)

bash son.sh(夯住)

cat /root/son.sh
#!/bin/bash
while :
do
	echo 1 >>/root/son.log
	sleep 10
done
[root@boy ~]# bash son.sh 
`卡住`

           ├─sshd(1076)───sshd(12592)─┬─bash(12596)───bash(12876)───sleep(12877)
                               └─bash(12673)───pstree(12879)


#分析:
	12596为登录时生成的那个bash,在我执行bash son.sh后,生成了一个子bash(12876),且在执行son.sh时12596会夯住(阻塞在那了,等待子bash执行完成,但是我son.sh会无限循环的,意味着子bash永远不会返回,也就是用户在12596那个shell上无法操作)

bash son.sh &

cat /root/son.sh
#!/bin/bash
while :
do
	echo 1 >>/root/son.log
	sleep 10
done
[root@boy ~]# bash son.sh &
[1] 13089
[root@boy ~]# jobs
[1]+  Running                 bash son.sh &
[root@boy ~]# echo $BASHPID
12596

           ├─sshd(1076)───sshd(12592)─┬─bash(12596)───bash(13089)───sleep(13099)
                               └─bash(12673)───pstree(13100)


# 分析:
	生成子bash方式与(2)没有多大区别,唯一的区别就是加了&后,13089放在后台去执行了,12596还可以进行其他操作

source son.sh

cat /root/son.sh
#!/bin/bash
while :
do
	echo 1 >>/root/son.log
	sleep 10
done
[root@boy ~]# source son.sh 
`卡住`

├─sshd(1076)───sshd(12592)─┬─bash(12596)───sleep(13133)
                               └─bash(12673)───pstree(13134)

# 分析
	使用source后,父bash不会生成子bash,但是存在12596这个bash夯在前台了

source son.sh &

cat /root/son.sh
#!/bin/bash
while :
do
	echo 1 >>/root/son.log
	sleep 10
done
[root@boy ~]# source son.sh &
[1] 13151
[root@boy ~]# echo $BASHPID
12596

├─sshd(1076)───sshd(12592)─┬─bash(12596)───bash(13151)───sleep(13152)
                               └─bash(12673)───pstree(13153)

# 分析:
	通过观察可以发现,不管是使用bash、source执行脚本,只要后面加了&放后台执行,都会生成子shell,12596此时能接受用户输入

nohub bash son.sh &

cat /root/son.sh
#!/bin/bash
while :
do
	echo 1 >>/root/son.log
	sleep 10
done
[root@boy ~]# nohup bash son.sh &
[1] 13284
[root@boy ~]# nohup: ignoring input and appending output to ‘nohup.out’
[root@boy ~]# echo $BASHPID
13248


├─sshd(1076)───sshd(12592)─┬─bash(12673)───pstree(13287)
                               └─bash(13248)───bash(13284)───sleep(13286)

# 13248 执行exit后
├─bash(13284)───sleep(13298)
├─sshd(1076)───sshd(12592)───bash(12673)───pstree(13299)

# 分析
	可以发现13284在用户退出登陆后,任然在后台执行

nohub source son.sh &

cat /root/son.sh
#!/bin/bash
while :
do
	echo 1 >>/root/son.log
	sleep 10
done
[root@boy ~]# nohup source son.sh &
[1] 13440
[root@boy ~]# nohup: ignoring input and appending output to ‘nohup.out’
nohup: failed to run command ‘source’: No such file or directory
# 如果用nohub 执行java等相关命令也显示没有时,脚本开头加上
	source /etc/profile 等,引入变量

结论:

  • 平常在rc.local或者cron中运行其他脚本的时候需要根据调用的脚本是否会夯住,灵活使用nohub和&,解决rc.local,cron中其他内容没有执行

BASH错误处理

脚本调用

# 无子shell方式
[root@boy ~]# cat 1.sh 
#!/bin/bash 
. /root/2.sh
echo 1

[root@boy ~]# cat 2.sh
#!/bin/env bash
echo 2
exit 1

[root@boy ~]# bash 1.sh  #可以看出echo 1并没有被执行
2
# 存在子shell方式
[root@boy ~]# cat 1.sh 
#!/bin/bash 
bash /root/2.sh
echo 1

[root@boy ~]# cat 2.sh
#!/bin/env bash
echo 2
exit 1

[root@boy ~]# bash 1.sh  # 可以发现当如果是子shell调用时,子shell不正常退出,并不会导致父shell的退出
2
1

命令方式

[root@jiayu ~]# cat test.sh 
#!/bin/bash
foo
echo bar
[root@jiayu ~]# bash test.sh 
test.sh: line 2: foo: command not found
bar

​ 可以看到,Bash 只是显示有错误,并没有终止执行,这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法

# 只要command有非零返回值,脚本就会停止执行
	command || exit 1
# 方法一
	command || { echo "command failed"; exit 1; }
=========================================================
[root@jiayu ~]# ls /et &>/dev/null || echo 1
1
# 方法二
	if ! command; then echo "command failed"; exit 1; fi
==========================================================	
[root@jiayu ~]# cat test.sh 
#!/bin/bash
if [ ! $(ls /tests &>/dev/null) ]; then
	echo "this dir not found"
fi

[root@jiayu ~]# bash test.sh 
this dir not found
# 方法三
	command
	if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi
	
# 除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法
	command1 && command2

获取脚本路径分析

{BASH_SOURCE}

​ BASH_SOURCE,是一个数组,不过它的第一个元素是当前脚本的名称。这在source的时候非常有用,因为在被source的脚本中,$0是父脚本的名称,而不是被source的脚本名称,BASH_SOURCE[0] 等价于 BASH_SOURCE, 取得当前执行的shell文件所在的路径及文件名

# BASH_SOURCE表示的是用户所在的目录到脚本的路径
[root@jiayu ~]# cat source.sh 
#!/bin/bash
echo ${BASH_SOURCE}
####################
[root@jiayu ~]# bash source.sh 
source.sh
[root@jiayu ~]# bash /root/source.sh 
/root/source.sh
[root@jiayu ~]# source source.sh 
source.sh
[root@jiayu ~]# source /root/source.sh 
/root/source.sh
[root@boy ~]# cat 1.sh 
#!/bin/bash 
. /root/2.sh
echo ${BASH_SOURCE[0]}

[root@boy ~]# cat 2.sh 
#!/bin/bash
echo ${BASH_SOURCE[0]}

[root@boy ~]# bash 1.sh 
/root/2.sh
1.sh
[root@boy ~]# ls
1.sh  2

[root@boy ~]# cat 1.sh 
#!/bin/bash 
. /root/2/2.sh
echo ${BASH_SOURCE[0]}

[root@boy ~]# cat 2/2.sh 
#!/bin/bash
echo ${BASH_SOURCE[0]}
re=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
echo $re

[root@boy ~]# bash 1.sh 
/root/2/2.sh
/root/2
1.sh

$0

# 可以发现当脚本被source执行的时候,$0并不是脚本名字了,而是-bash了
[root@jiayu ~]# cat test.sh 
#!/bin/bash
echo $0
[root@jiayu ~]# bash test.sh 
test.sh
[root@jiayu ~]# source test.sh 
-bash
[root@jiayu ~]# bash /root/test.sh 
/root/test.sh

区分source和bash加载

方法一

[root@jiayu ~]# cat example.sh 
#!/bin/bash
script_name=$( basename ${0#-} ) #- needed if sourced no path
this_script=$( basename ${BASH_SOURCE} )
if [[ ${script_name} = ${this_script} ]] ; then
    echo "running me directly"
else
    echo "sourced from ${script_name}"
fi 
[root@jiayu ~]# cat example2.sh 
#!/bin/bash
. ./example.sh
======================
[root@jiayu ~]# bash example.sh 
running me directly
[root@jiayu ~]# source example.sh 
sourced from bash

======================
[root@jiayu ~]# bash example2.sh 
sourced from example2.sh

方法二

[root@jiayu ~]# cat test.sh 
#!/bin/bash
function func1(){
        echo "func1................"
        echo ${FUNCNAME[@]}
        echo ${FUNCNAME[0]}
        echo ${FUNCNAME[1]}
}
function func2(){
        func1
        echo "func2................"
        echo ${FUNCNAME[@]}
        echo ${FUNCNAME[0]}
        echo ${FUNCNAME[1]}
}
func2
================================
[root@jiayu ~]# bash test.sh 
func1................
func1 func2 main
func1
func2
func2................
func2 main
func2
main
# 可以发现没有被其他函数调用的${FUNCNAME[@]} 只有二个值,一个是本身函数值和调用改函数的main

command -v

对函数判断:command命令在shell脚本里面,如果发现有个函数和我们需要执行的命令同名,我们可以用command用来强制执行命令,而不是同名函数,然后我们也可以在shell脚本里面判断某个命令是否存在,我们平时一般用which命令也行

#!/bin/bash
 
function pwd()
{
    echo "I am pwd function"
}
 
echo "shell run pwd"
pwd
 
echo "shell command pwd"
command pwd
 
if  command -v pwd > /dev/null; then
    echo "pwd command has found"
else
    echo "pwd command has not found"
fi
 
if  command -v pwd1 > /dev/null; then
    echo "pwd1 command has found"
else
    echo "pwd1 command has not found"
fi
=================
./command 
shell run pwd
I am pwd function
shell command pwd
/home/chenyu/Desktop/linux/dabian/python
pwd command has found
pwd1 command has not found

对命令判断:在Linux中,command -v 可以判断一个命令是否支持,如果一个脚本需要,或者还要家if判断,则示例如下:

if command -v python >/dev/null 2>&1;then 
   echo "yes"
else 
   echo "no"
fi

# 脚本运行后,如果系统支撑python,则运行结果为yes
for i in curl wget tar; do
  command -v $i >/dev/null || {
      echo_red "$(gettext 'Please install it first') $i"
      exit 1
  }
done

BASH_REMATCH

双目运算符=~它和==以及!=具有同样的优先级。如果使用了它,则其右边的字符串就被认为是一个扩展的正则表达式来匹配如果字符串和模式匹配,则返回值是0,否则返回1。如果这个正则表达式有语法错误,则整个条件表达式的返回值是2。如果打开了shell的nocasematch 选项则匹配时不考虑字母的大小写。模式的任何部分都可以被引用以强制把其当作字符串来匹配。由正则表达式中括号里面的子模式匹配的字符串被保存在数组变量BASH_REMATCH 中。BASH_REMATCH 中下标为0的元素是字符串中与整个正则表达式匹配的部分。BASH_REMATCH 中下标为n的元素是字符串中与第n 个括号里面的子模式匹配的部分

#!/bin/bash
info="cxsxafasdf,id=55,name=lkw,sex=man,oc=game"
if [[ $info =~ as.*id=([0-9]+),name=([a-z]+),sex=(.*) ]]; then
	echo "this is 0: ${BASH_REMATCH[0]}" 
	echo "this is 1: ${BASH_REMATCH[1]}" 
	echo "this is 2: ${BASH_REMATCH[2]}"
	echo "this is 3: ${BASH_REMATCH[3]}"
fi
=================================
[root@init-mysql ~]# bash source.sh 
this is 0: asdf,id=55,name=lkw,sex=man,oc=game
this is 1: 55
this is 2: lkw
this is 3: man,oc=game

	通过上面可以发现,${BASH_REMATCH}有点和sed中的\1\2类似,${BASH_REMATCH[0]}:代表了if中整个匹配到的字符串,后边BASH_REMATCH 中下标为n的元素是字符串中与第n 个括号里面的子模式匹配的部分

$?

systemctl status keepalived|grep running  #如果grep到了,会返回0,如果没grep到,会返回非0
#!/bin/bash
if [ $(rpm -qa|grep gcc) ]; then #这种不加双引号的,如果grep到的结果有二个就会报错,如果没有就不会
    echo 1
else
    echo 2
fi

if [ "$(rpm -qa|grep gcc)" ]; then #加双引号,将其变为字符串,只要搜索到,就会为真
    echo 1
else
    echo 2
fi
#!/bin/bash
function a(){
    return 1
}

function b(){
    a
    echo $? # $?=1 return的返回值,会成为$?
}
b
rpm -ivh xx 重复安装时,第二次以后$?都是非0
yum -y localinstall --disablerepo=\* --skip-broken xx #可以幂等

var=$(
[root@init-mysql ~]# cat test
"CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=869 master_host='mysql-0.mysql', master_user='root', master_password='', master_connect_retry=10; start slave;"
[root@init-mysql ~]# cat test.sh 
#!/bin/bash
sql="$(<test)"
echo $sql
[root@init-mysql ~]# sh -x test.sh 
+ sql='"CHANGE MASTER TO MASTER_LOG_FILE='\''mysql-bin.000003'\'', MASTER_LOG_POS=869 master_host='\''mysql-0.mysql'\'', master_user='\''root'\'', master_password='\'''\'', master_connect_retry=10; start slave;"'
+ echo '"CHANGE' MASTER TO 'MASTER_LOG_FILE='\''mysql-bin.000003'\'',' MASTER_LOG_POS=869 'master_host='\''mysql-0.mysql'\'',' 'master_user='\''root'\'',' 'master_password='\'''\'',' 'master_connect_retry=10;' start 'slave;"'
"CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=869 master_host='mysql-0.mysql', master_user='root', master_password='', master_connect_retry=10; start slave;"

#!/usr/bin/env bash

	当你执行 env python 时,它其实会去 env | grep PATH 里(也就是 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin)这几个路径里去依次查找名为 python 的可执行文件
	#!/usr/bin/env bash 就是在 $PATH 中挨个目录依次去找 bash 的,先找到哪个,就用哪个

PROMPT_COMMAND

# 使用PROMPT_COMMAND记录每个用户命令记录

() &(())&[]&[[]]&{}

()

  • 命令组。括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用。括号中多个命令之间用分号隔开,最后一个命令可以没有分号,各命令和括号之间不必有空格
a=0
(a=1;b=2;echo $a) # 子进程内输出a为1
echo $a # 主进程,输出a为0
  • 命令替换。等同于cmd,shell扫描一遍命令行,发现了结构,便将(cmd)中的cmd执行一次,得到其标准输出,再将此输出放到原来命令。有些shell不支持,如tcsh。格式为 $(command),相当于 command。shell遇到此格式,会先执行 command 命令,得到标准输出,再将此输出放回到原来命令
SCRIPTPATH=$(dirname $0)  # sh脚本的相对目录
  • 用于初始化数组。如:array=(a b c d)
array=(a b c)
echo ${array[1]} # 输出b

(())

  • 整数扩展: 这种扩展计算是整数型的计算,不支持浮点型。((exp))结构扩展并计算一个算术表达式的值,如果表达式的结果为0,那么返回的退出状态码为1或者 是"假",而一个非零值的表达式所返回的退出状态码将为0,或者是"true"。若是逻辑判断,表达式exp为真则为1,假则为0
  • 只要括号中的运算符、表达式符合C语言运算规则,都可用在[[exp]]中,甚至是三母运算符,表达式符合C语言运算规则。作不同进位(如二进制、八进制、十六进制)运算时,输出结果全自动转化成了十进制。如:echo,((16#5f)) 结果为95 (16进位转十进制)
# 表达式 $((exp)),其中exp只要符合C语言规则的运算符即可,包括加减乘除、+=、<=、>=等。
# 进行不同进位(如二进制、八进制、十六进制)运算时,输出结果会全都自动转化成十进制

echo $((2*2+(1-2))) # 输出3
a=1
echo $((a++)) # 输出2,且从此之后a=2
echo $((2#10+4)) # 输出6,在10前面的2#表示这是个二进制
echo $((a<2)) # 输出1。如果为假,则输出0。
  • ((exp))中可以对变量进行定义或重新赋值,且之后脚本全部有效(不是像()那样只在子进程有效)
a=1
((a=2))
echo $a # 输出2
  • ((exp))中可以进行算术比较(不能进行字符串比较),双括号中的变量可以省略$符号前缀,当然也可以带着
a=1
((a==1)) && echo "true" # 输出 true
(($a==1)) && echo "true" # 输出 true

[]

  • bash 的内部命令,[和test是等同的。如果我们不用绝对路径指明,通常我们用的都是bash自带的命令。if/test结构中的左中括号是调用test的命令标识,右中括号是关闭条件判断的。这个命令把它的参数作为比较表达式或者作为文件测试,并且根据比较的结果来返回一个退出状态码。if/test结构中并不是必须右中括号,但是新版的Bash中要求必须这样
# [] 本质上是 test 语句,[是调用test的命令标识,]是关闭条件判断,如:
a="abc"
if [ -n "$a" ]; then echo "true"; fi # 输出true,注意[]内-n或者-z时字符串两边必须加上双引号
if test -n "$a"; then echo "true"; fi # 输出true,注意test内-n或者-z时字符串两边必须加上双引号
  • Test和[]中可用的比较运算符只有==和!=,两者都是用于字符串比较的,不可用于整数比较,整数比较只能使用-eq,-gt这种形式。无论是字符串比较还是整数比较都不支持大于号小于号。如果实在想用,对于字符串比较可以使用转义形式,如果比较"ab"和"bc":[ ab < bc ],结果为真,也就是返回状态为0。[ ]中的逻辑与和逻辑或使用-a 和-o 表示
  • 字符范围。用作正则表达式的一部分,描述一个匹配的字符范围。作为test用途的中括号内不能使用正则
# 用作正则表达式的一部分,描述一个匹配的字符范围,如删除一个字符串中的所有数字:
echo "ap1p23le" | tr -d "[0-9]" # 输出 apple
echo [a-b].txt # 输出:a.txt b.txt,前提是两个文件都存在。
# 但是如果当前路径只存在a.txt或b.txt,则只会输出存在的文件。
# 如果两个文件均不存在,则输出固定字符串:[a-b].txt
  • 在一个array 结构的上下文中,中括号用来引用数组中每个元素的编号
# 在一个array后,作为引用数组元素的下标,如
a=(1 2 3)
echo ${a[0]} # 输出:1
echo ${a[*]} # 输出:1 2 3

[[]]

  • [[是 bash 程序语言的关键字。并不是一个命令,[[ ]] 结构比[ ]结构更加通用。在[[和]]之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换
  • 支持字符串的模式匹配,使用=~操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如[[ hello == hell? ]],结果为真。[[ ]] 中匹配字符串或通配符,不需要引号
  • 使用[[ … ]]条件判断结构,而不是[ … ],能够防止脚本中的许多逻辑错误。比如,&&、||、<和> 操作符能够正常存在于[[ ]]条件判断结构中,但是如果出现在[ ]结构中的话,会报错。比如可以直接使用if [[ $a != 1 && $a != 2 ]], 如果不适用双括号, 则为if [ $a -ne 1] && [ $a != 2 ]或者if [ $a -ne 1 -a $a != 2 ]
# 使用[[]]条件判断结构,与[]有以下区别:
本质上,[]和test是命令,而[[]]是关键字,所以重定向等字符在[]中会被认为成重定向,而在[[]]中是比较符号的意思。
&&||<> 操作符能够正常存在于[[]]条件判断结构中,但是如果出现在[]结构中的话,会报错。比如可以直接使用if [[ $a != 1 && $a != 2 ]], 如果不使用双括号, 则为if [ $a != 1] && [ $a != 2 ]或者if [ $a != 1 -a $a != 2 ]
[[]]支持字符串的模式匹配,[]不支持。使用===进行字符串比较,等号右边的可以作为一个模式,比如[[ "hello" == hell? ]]为真。模式匹配不能加引号,否则会作为固定字符串,如[[ "hello" == "hell?" ]]为假。
[[]]支持算术扩展,而[]不支持。如if [[ 1+2 -eq 3 ]],而if [ 1+2 -eq 3 ]则会报错
  • bash把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码
if ($i<5) 
if [ $i -lt 5 ] 
if [ $a -ne 1 -a $a != 2 ] 
if [ $a -ne 1] && [ $a != 2 ] 
if [[ $a != 1 && $a != 2 ]] 
for i in $(seq 0 4);do echo $i;done
for i in `seq 0 4`;do echo $i;done
for ((i=0;i<5;i++));do echo $i;done
for i in {0..4};do echo $i;done

{}

  • 大括号拓展。(通配(globbing))将对大括号中的文件名做扩展。在大括号中,不允许有空白,除非这个空白被引用或转义。第一种:对大括号中的以逗号分割的文件列表进行拓展。如 touch {a,b}.txt 结果为a.txt b.txt。第二种:对大括号中以点点(…)分割的顺序文件列表起拓展作用,如:touch {a…d}.txt 结果为a.txt b.txt c.txt d.txt
# ls {ex1,ex2}.sh 
ex1.sh ex2.sh 
# ls {ex{1..3},ex4}.sh 
ex1.sh ex2.sh ex3.sh ex4.sh 
# ls {ex[1-3],ex4}.sh 
ex1.sh ex2.sh ex3.sh ex4.sh
  • 代码块,又被称为内部组,这个结构事实上创建了一个匿名函数 。与小括号中的命令不同,大括号内的命令不会新开一个子shell运行,即脚本余下部分仍可使用括号内变量。括号内的命令间用分号隔开,最后一个也必须有分号。{}的第一个命令和左括号之间必须要有一个空格
  • 特殊替换结构:${var:-string},${var:+string},${var:=string},${var:?string}
(1):${var:-string}表示若变量var不为空,则等于var的值,否则等于string的值
a=''
b="bbb"
echo ${a:-foo} # 输出:foo
echo ${a:-"foo"} # 输出:foo
echo ${a:-$b} # 输出:bbb


(2):${var:=string}的替换规则与${var:-string}相同,不过多了一步操作:若var为空时,还会把string的值赋给var。
a=''
b="bbb"
echo ${a:=$b} # 输出:bbb
echo $a # 输出:bbb

(3):${var:+string}的替换规则和上面的相反,表示若变量var不为空,则等于string的值,否则等于var的值(即空值)
a=""
b="bbb"
echo ${a:+"foo"} # 输出为空
echo ${b:+"foo"} # 输出:foo

(4):${var:?string}表示若变量var不为空,则等于var的值,否则把string输出到标准错误中,并从脚本中退出
	# 我们可利用此特性来检查是否设置了变量的值,比如一个脚本test.sh内容如下
#!/bin/bash
a=""
b="bbb"
echo ${a:?"warn:string is null"}
echo ${b:?"warn:string is null"}
echo "done"
	#然后在命令行中执行命令:
$ sh test.sh > out.log 2> err.log # 标准输出至out.log,标准错误输出至err.log
$ cat out.log
aaa
$ cat err.log
test.sh: line 5: b: warn:string is null
	#可以看到,out.log中没有done,因此脚本执行到Line 5的错误就提前退出了。
  • 模式匹配截断:${var%pattern},${var%%pattern},${var#pattern},${var##pattern}
(1):${var%pattern}表示看var是否以模式pattern结尾,如果是,就把var中的内容去掉右边最短的匹配模式
(2):${var%%pattern}表示看var是否以模式pattern结尾,如果是,就把var中的内容去掉右边最长的匹配模式
(3):${var#pattern}表示看var是否以模式pattern开始,如果是,就把var中的内容去掉左边最短的匹配模式
(4):${var##pattern}表示看var是否以模式pattern开始,如果是,就把var中的内容去掉左边最长的匹配模式
  • 长度截断
# 有2种模式:${var:num},${var:num1:num2}
${var:num}表示提取var中第num个字符到末尾的所有字符。若num为正数,从左边0处开始;若num为负数,从右边开始提取字串,但必须使用在冒号后面加空格或一个数字或整个num加上括号,如${var: -2}${var:1-3}${var:(-2)}

${var:num1:num2}表示从var中第num1个位置开始提取长度为num2的子串。num1从0开始

a="apple"
echo ${a:2} # 去掉前2个,输出:ple
echo ${a: -2} # 保留后2个,输出:le
echo ${a:1:3} # 保留从1开始的3个字符,输出:ppl

多条命令执行

  • 单小括号,(cmd1;cmd2;cmd3) 新开一个子shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后可以没有分号
  • 单大括号,{ cmd1;cmd2;cmd3;} 在当前shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后必须有分号, 第一条命令和左括号之间必须用空格隔开
  • 对{}和()而言, 括号中的重定向符只影响该条命令, 而括号外的重定向符影响到括号中的所有命令
[root@boy ~]# bash 2.sh 
1
[root@boy ~]# cat 2.sh 
#!/bin/env bash
{ echo 1;}
command -v docker >/dev/null || {
      log_error "$(gettext 'The current Linux system does not support systemd management. Please deploy docker by yourself before running this script again')"
      exit 1
    }

你可能感兴趣的:(Linux,bash,linux,运维,centos)