Shell脚本技巧

参考文章

http://www.ruanyifeng.com/blog/2017/11/bash-set.html

[TOC]

shell语法检查与执行

verbose模式

读取脚本时,显示读到的每一行

1
$ bash -v script.sh

语法检查调试模式

shell读取脚本时检查语法,一旦发现语法错误即在终端输出,无语法错误不输出

1
$ bash -n script.sh

分步执行

1
$ bash -x script.sh

shell 中 && || () {} 用法

&& 运算符:

格式

1
command1  && command2

&&左边的命令(命令1)返回真(即返回0,成功被执行)后,&&右边的命令(命令2)才能够被执行;换句话说,“如果这个命令执行成功&&那么执行这个命令”。
语法格式如下:

1
command1 && command2 && command3 ...
  1. 命令之间使用 && 连接,实现逻辑与的功能。
  2. 只有在 && 左边的命令返回真(命令返回值 $? == 0),&& 右边的命令才会被执行。
  3. 只要有一个命令返回假(命令返回值 $? == 1),后面的命令就不会被执行。

|| 运算符:

格式

1
command1 || command2

||则与&&相反。如果||左边的命令(command1)未执行成功,那么就执行||右边的命令(command2);或者换句话说,“如果这个命令执行失败了||那么就执行这个命令。

  1. 命令之间使用 || 连接,实现逻辑或的功能。
  2. 只有在 || 左边的命令返回假(命令返回值 $? == 1),|| 右边的命令才会被执行。这和 c 语言中的逻辑或语法功能相同,即实现短路逻辑或操作。
  3. 只要有一个命令返回真(命令返回值 $? == 0),后面的命令就不会被执行。

||与&&

1
command1 && command2 || command3

将||与&&组合使用则表达,如果command1执行成功,则执行command2,否则执行command3

下面是一个shell脚本中常用的||组合示例

1
echo $BASH |grep -q 'bash' || { exec bash "$0" "$@" || exit 1; }    系统调用exec是以新的进程去代替原来的进程,但进程的PID保持不变。因此,可以这样认为,exec系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。原进程的代码段,数据段,堆栈段被新的进程所代替。

() 运算符:

如果希望把几个命令合在一起执行,shell提供了两种方法。既可以在当前shell也可以在子shell中执行一组命令。
格式:

1
(command1;command2;command3....)               多个命令之间用;分隔
  1. 一条命令需要独占一个物理行,如果需要将多条命令放在同一行,命令之间使用命令分隔符(;)分隔。执行的效果等同于多个独立的命令单独执行的效果。
  2. () 表示在当前 shell 中将多个命令作为一个整体执行。需要注意的是,使用 () 括起来的命令在执行前面都不会切换当前工作目录,也就是说命令组合都是在当前工作目录下被执行的,尽管命令中有切换目录的命令。
  3. 命令组合常和命令执行控制结合起来使用。

{} 运算符:

如果使用{}来代替(),那么相应的命令将在子shell而不是当前shell中作为一个整体被执行,只有在{}中所有命令的输出作为一个整体被重定向时,其中的命令才被放到子shell中执行,否则在当前shell执行。
它的一般形式为:

1
{ command1;command2;command3… }      注意:在使用{}时,{}与命令之间必须使用一个空格

shell脚本获取当前目录和文件夹名

1
2
3
4
#!/bin/bash

project_path = $(cd `dirname $0`;pwd)
project_name = "${project_path##*/}"

可以用${ }分别替换得到不同的值:file=/dir1/dir2/dir3/my.file.txt
${file#/}:删掉第一个 / 及其左边的字符串:dir1/dir2/dir3/my.file.txt
${file##
/}:删掉最后一个 / 及其左边的字符串:my.file.txt
${file#.}:删掉第一个 . 及其左边的字符串:file.txt
${file##
.}:删掉最后一个 . 及其左边的字符串:txt
${file%/}:删掉最后一个 / 及其右边的字符串:/dir1/dir2/dir3
${file%%/
}:删掉第一个 / 及其右边的字符串:(空值)
${file%.}:删掉最后一个 . 及其右边的字符串:/dir1/dir2/dir3/my.file
${file%%.
}:删掉第一个 . 及其右边的字符串:/dir1/dir2/dir3/my

Shell脚本输出颜色

介绍

1
2
3
4
5
6
7
echo -e "\033[43;35m david use echo say Hello World \033[0m \n" 
printf "\033[44;36m david use printf say Hello World \033[0m \n"
echo -e "\033[47;30;5m david use echo say \033[0m Hello World \n"

echo -e "\033[字背景颜色;字体颜色m 字符串 \033[0m" 或者
printf "\033[字背景颜色;字体颜色m 字符串 \033[0m" 或者
echo -e "\033[字背景颜色;字体颜色m;ascii码m 字符串 \033[0m 字符串(can null) \n"
1
2
3
4
5
6
7
echo -e "\033[47;30;5m david use echo say \033[0m Hello World \n" 
作用:
42->背景色为白色,
30->字体为黑色,
5->字体闪烁,
0->关闭所有属性
输出字符 “david use echo say”,然后重新设置屏幕到缺省设置,输出字符 “Hello World”后颜色回复正常
1
echo -e "\033[20;1H\033[1;4;32m david use echo say \033[0m Hello World \n"

这行命令首先\033[20;1H将光标移动到终端第20行第1列,之后的\033[1;4;32m将文本属性设置为高亮、带下划线且颜色为绿色,然后输出Hello,world;最后\033[0m将终端属性设为缺省,这样就不会看到连命令完成后的命令提示符也变了样儿了。我们可以通过各种命令的组合可以实现对终端输出地复杂控制。

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
0 重新设置属性到缺省设置
1 设置粗体
2 设置一半亮度(模拟彩色显示器的颜色)
4 设置下划线(模拟彩色显示器的颜色)
5 设置闪烁
7 设置反向图象
8 消隐
22 设置一般密度
24 关闭下划线
25 关闭闪烁
27 关闭反向图象

// 字体颜范围(前景颜色):30~39
30:黑
31:红
32:绿
33:黄
34:蓝色
35:紫色
36:深绿
37:白色

38:在缺省的前景颜色上设置下划线
39:在缺省的前景颜色上关闭下划线

// 字背景颜色范围(背景颜色):40~49
40:黑
41:深红
42:绿
43:黄色
44:蓝色
45:紫色
46:深绿
47:白色

nA 光标上移n行
nB 光标下移n行
nC 光标右移n行
nD 光标左移n行
y;xH设置光标位置
2J 清屏
K 清除从光标到行尾的内容
s 保存光标位置
u 恢复光标位置
?25l 隐藏光标
?25h 显示光标

脚本颜色变量

1
2
3
4
5
6
7
8
9
RED_COLOR='\E[1;31m'  
YELOW_COLOR='\E[1;33m'
BLUE_COLOR='\E[1;34m'
RESET='\E[0m'

#需要使用echo -e
echo -e "${RED_COLOR}===david say red color===${RESET}"
echo -e "${YELOW_COLOR}===david say yelow color===${RESET}"
echo -e "${BLUE_COLOR}===david say green color===${RESET}"

set命令

  1. set -u

    遇到不存在的变量,Bash报错并停止执行。还有另外一中写法-u还有另一种写法-o nounset,两者是等价的。

  2. set -x

    默认情况下,脚本执行后,屏幕只显示运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。set -x用来在运行结果之前,先输出执行的那一行命令。-x还有另一种写法-o xtrace

  3. set -e

    它使得脚本只要发生错误,就终止执行。set -e根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e,该命令执行结束后,再重新打开set -e

    set +e表示关闭-e选项,set -e表示重新打开-e选项。

    -e还有另一种写法-o errexit

  4. set -o pipefail

    set -e有一个例外情况,就是不适用于管道命令。所谓管道命令,就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e就失效了。

总结

set命令的上面这四个参数,一般都放在一起使用

1
2
3
4
5
6
# 写法一
set -euxo pipefail

# 写法二
set -eux
set -o pipefail

这两种写法建议放在所有 Bash 脚本的头部。

另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数。

1
$ bash -euxo pipefail script.sh

Bash的错误处理

如果脚本里面有运行失败的命令(返回值非0),Bash 默认会继续执行后面的命令。

1
2
3
4
5
> #!/usr/bin/env bash
>
> foo
> echo bar
>

上面脚本中,foo是一个不存在的命令,执行时会报错。但是,Bash 会忽略这个错误,继续往下执行。

1
2
3
4
> $ bash script.sh
> script.sh:行3: foo: 未找到命令
> bar
>

可以看到,Bash 只是显示有错误,并没有终止执行。

这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法。

1
2
> command || exit 1
>

上面的写法表示只要command有非零返回值,脚本就会停止执行。

如果停止执行之前需要完成多个操作,就要采用下面三种写法。

1
2
3
4
5
6
7
8
9
10
> # 写法一
> command || { echo "command failed"; exit 1; }
>
> # 写法二
> if ! command; then echo "command failed"; exit 1; fi
>
> # 写法三
> command
> if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi
>

另外,除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法。

1
2
> command1 && command2
>

command || true,使得该命令即使执行失败,脚本也不会终止执行。

1
2
3
4
5
6
> #!/bin/bash
> set -e
>
> foo || true
> echo bar
>

上面代码中,true使得这一行语句总是会执行成功,后面的echo bar会执行。

常用表达式

文件表达式

  • -e filename 如果 filename存在,则为真
  • -d filename 如果 filename为目录,则为真
  • -f filename 如果 filename为常规文件,则为真
  • -L filename 如果 filename为符号链接,则为真
  • -r filename 如果 filename可读,则为真
  • -w filename 如果 filename可写,则为真
  • -x filename 如果 filename可执行,则为真
  • -s filename 如果文件长度不为0,则为真
  • -h filename 如果文件是软链接,则为真
  • filename1 -nt filename2 如果 filename1比 filename2新,则为真。
  • filename1 -ot filename2 如果 filename1比 filename2旧,则为真。

整数变量表达式

  • -eq 等于
  • -ne 不等于
  • -gt 大于
  • -ge 大于等于
  • -lt 小于
  • -le 小于等于

字符串变量表达式

  • If [ $a = $b ] 如果string1等于string2,则为真,字符串允许使用赋值号做等号
  • if [ $string1 != $string2 ] 如果string1不等于string2,则为真
  • if [ -n $string ] 如果string 非空(非0),返回0(true)
  • if [ -z $string ] 如果string 为空,则为真
  • if [ $sting ] 如果string 非空,返回0 (和-n类似)
  • if [ ! -d $num ]
  • if [ 表达式1 –a 表达式2 ]
  • if [ 表达式1 –o 表达式2 ]

Shell脚本中调用另外一个脚本的方法

在Linux平台上开发,经常会在console(控制台)上执行另外一个脚本文件,经常用的方法有:./my.shsource my.sh. my.sh;这三种方法有什么不同呢?我们先来了解一下在一个shell脚本中如何调用另外一个shell脚本,其方法有 fork exec source。

  1. fork ( /directory/script.sh) :

    如果shell中包含执行命令,那么子命令并不影响父级的命令,在子命令执行完后再执行父级命令。子级的环境变量不会影响到父级。

    fork是最普通的, 就是直接在脚本里面用/directory/script.sh来调用script.sh这个脚本. 运行的时候开一个sub-shell执行调用的脚本,sub-shell执行的时候,parent-shell还在。

    sub-shell执行完毕后返回parent-shell. sub-shell从parent-shell继承环境变量.但是sub-shell中的环境变量不会带回parent-shell

  2. exec (exec /directory/script.sh):

    执行子级的命令后,不再执行父级命令。

    exec与fork不同,不需要新开一个sub-shell来执行被调用的脚本. 被调用的脚本与父脚本在同一个shell内执行。但是使用exec调用一个新脚本以后, 父脚本中exec行之后的内容就不会再执行了。这是exec和source的区别

  3. source (source /directory/script.sh):

    执行子级命令后继续执行父级命令,同时子级设置的环境变量会影响到父级的环境变量。

    与fork的区别是不新开一个sub-shell来执行被调用的脚本,而是在同一个shell中执行. 所以被调用的脚本中声明的变量和环境变量, 都可以在主脚本中得到和使用.

以上三种就是调用shell脚本的不同方法,./my.sh即是fork的方法,source my.sh和. my.sh(点加空格加脚本文件)既是source的方法。

在linux系统上,搭建嵌入式开发平台,在交叉编译代码之前,都需要执行脚本设置环境变量,切记需要使用sourc 或 点的方式执行shell脚本,原因如上。

Shell脚本执行mysql命令

  1. mysql -hhostname -uuser -ppasswd -e “mysql_cmd”

  2. mysql -hhostname -uuser -ppasswd << EOF

    mysql_cmd

    EOF

Shell for循环多个变量

需求:需要输出以下2开头的端口号和其对应的文件

1
port and port_k8s_xxx.conf

image

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

#以value_name=(value1 value2 value3)的形式定义数组
a=(`ls |grep -v ^1|grep -v 22281_k8s_qkd_http.conf|grep -v for.sh| awk -F '_' '{print $1}'`)
b=(`ls |grep -v ^1|grep -v 22281_k8s_qkd_http.conf|grep -v for.sh`)

#以 ${a[number]} 的形式调用数组的第 number 个变量
for (( i=0; i<16; i++ ))
do
echo ${a[$i]} and ${b[$i]}
done

image

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
25
26
27
28
[root@localhost ~]# echo ' A B C ' | awk '{gsub(/^\s+|\s+$/, "");print}'
^\s+ 匹配行首一个或多个空格
\s+$ 匹配行末一个或多个空格
^\s+|\s+$ 同时匹配行首或者行末的空格

[root@local ~]# echo " A BC "
A BC
[root@local ~]# eval echo " A BC "
A BC

[root@linux ~]# echo ' A BC ' | python -c "s=raw_input();print(s.strip())"
A BC

[root@linux ~]# s=`echo " A BC "`
[root@linux ~]# echo $s
A BC

[root@linux ~]# echo ' A BC ' | sed -e 's/^[ ]*//g' | sed -e 's/[ ]*$//g'
A BC

[root@linux ~]# echo " A BC " | awk '$1=$1'
A BC

[root@linux ~]# echo " A BC " | sed -r 's/^[ \t]+(.*)[ \t]+$//g'
A BC

[root@linux ~]# echo ' A BC ' | awk '{sub(/^ */, "");sub(/ *$/, "")}1'
A BC

Shell脚本操作mysql数据库

mysql -hhostname -Pport -uusername -ppassword -e 相关mysql的sql语句,不用在mysql的提示符下运行mysql,即可以在shell中操作mysql的方法。

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

HOSTNAME="192.168.111.84" #数据库信息
PORT="3306"
USERNAME="root"
PASSWORD=""

DBNAME="test_db_test" #数据库名称
TABLENAME="test_table_test" #数据库中表的名称
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
#创建数据库
create_db_sql="create database IF NOT EXISTS ${DBNAME}"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} -e "${create_db_sql}"

#创建表
create_table_sql="create table IF NOT EXISTS ${TABLENAME} ( name varchar(20), id int(11) default 0 )"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} ${DBNAME} -e "${create_table_sql}"

#插入数据
insert_sql="insert into ${TABLENAME} values('billchen',2)"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} ${DBNAME} -e "${insert_sql}"

#查询
select_sql="select * from ${TABLENAME}"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} ${DBNAME} -e "${select_sql}"

#更新数据
update_sql="update ${TABLENAME} set id=3"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} ${DBNAME} -e "${update_sql}"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} ${DBNAME} -e "${select_sql}"

#删除数据
delete_sql="delete from ${TABLENAME}"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} ${DBNAME} -e "${delete_sql}"
mysql -h${HOSTNAME} -P${PORT} -u${USERNAME} -p${PASSWORD} ${DBNAME} -e "${select_sql}"


show processlist如何过滤的问题我终于知道如何解决了
mysql -uroot -e -p password 'show processlist\G';

mysql -uroot -e 'show processlist\G'|grep 'Info'|grep -v "NULL"|awk -F ":" '{print $2}'|sort|uniq -c|sort -rn;(查看正在执行的语句有哪些,并做好归并排序:)
1
2
3
mysql -uroot -h$host -p$password << EOF
create database if not exists ${database};
EOF
--------------------本文结束,感谢您的阅读--------------------

本文标题:Shell脚本技巧

文章作者:弓昭

发布时间:2019年04月28日 - 22:11

最后更新:2020年04月08日 - 22:20

原始链接:https://gongzhao1.gitee.io/Shell脚本技巧/

联系邮箱:gongzhao1@foxmail.com