0%

Linux脚本编写手册

生产环境中的 Shell 脚本,通常被设置为定时任务来完成自动调度。这里介绍 bash(Bourne Again Shell) 语法,以及常用命令和软件。


脚本如何执行

指定解释器

一个规范的 Shell 脚本会在第一行使用 #! 指定由哪个程序(解释器)来执行脚本中的内容,当脚本执行时,就会加载相关环境配置(通常是 non-login shell 的 ~/.bashrc)。

指定 bash 来执行脚本:

1
#!/bin/bash

执行顺序

Shell 脚本执行时会先加载环境变量,然后从上到下,从左至右分析与运行脚本中的命令。当读取到一个换行符 CR,就尝试开始运行该行的命令,如果编写的一行命令内容过长,可以在行末用 \ 表示换行后的内容仍与当前行为同一句命令。当脚本执行中遇到子脚本时,会执行子脚本的内容,完成后再返回父脚本继续执行后续的命令。

变量

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
25
26
27
28
29
30
31
32
33
# 定义变量,并用等号赋值
# 定义时变量名不加 $,变量名和等号之间不能有空格
# 变量名只能使用字母、数字和下划线,不能以数字开头,区分大小写
# 变量名中间不能有空格、不能使用标点符号、不能使用 bash 里的关键字(可用 help 命令查看保留关键字)
CURRENT_YEAR=2021
TEXT=Hello
MY_NAME="sannaha"

# 定义变量,但不赋值(或者说赋空值)
var=

# 定义只读变量 MY_BLOG
readonly MY_BLOG='sannaha.moe'
# 尝试更改只读变量
MY_BLOG='sannaha.com'
# 报错提示
-bash: MY_BLOG: readonly variable

# 删除变量,无法删除只读变量
unset MY_NAME
# 尝试删除只读变量
unset MY_BLOG
# 报错提示
-bash: unset: MY_BLOG: cannot unset: readonly variable

# 用循环语句给变量赋值,变量在循环体外仍能使用
for filelist in $(ls ~); do echo $filelist; done; echo "filelist=$filelist";
# 输出结果
hadoop
kafka
mysql
spark
filelist=spark

使用变量

1
2
3
4
5
# 通过 $var 使用变量,用 ${var} 能比较精确的界定变量名称的范围
echo $MY_NAME
echo "my blog is ${MY_NAME}.moe"
# 输出结果
my blog is sannaha.moe

变量作用域

Shell 变量的作用域可以分为三种:

  • 环境变量:可以在当前 Shell 进程以及子进程中使用;
  • 用户变量:可以在当前 Shell 进程中使用;
  • 局部变量:只能在函数内定义和使用。

环境变量

Shell 维护着一组环境变量,用来记录特定的系统信息。比如系统的名称、登录到系统上的用户名、用户的系统ID(也称为UID)、用户的默认主目录以及 Shell 查找程序的搜索路径。可以用 set 命令来显示一份完整的当前环境变量列表:

1
2
3
4
5
6
7
8
$ set
BASH=/bin/bash
HOME=/home/sannaha
HOSTNAME=cdh1
ID=1006
LINES=24
JAVA_HOME=/usr/java/jdk1.8.0_141-cloudera
...

使用 export 命令将变量“导出”,那么这个变量就成为了环境变量,在当前 Shell 和所有子 Shell 中都有效。

两个没有父子关系的 Shell 进程是不能传递环境变量的,并且环境变量只能向下传递而不能向上传递。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建全局变量 g
$ g=global
# 创建环境变量 e
$ export e=environment

# 创建子进程
$ bash
# 子进程中无法使用父进程的全局变量
$ echo $g

# 子进程中可以使用父进程的环境变量
$ echo $e
environment
$ export se=subshellEnvironment

# 退出子进程,回到父进程
$ exit
exit
# 父进程中无法查看子进程中创建的环境变量
$ echo $se

用户变量

全局变量是指变量在当前整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响。在 Shell 中定义的变量,默认就是全局变量。

全局变量的作用范围是当前的 Shell 进程,而不是当前的 Shell 脚本文件,它们是不同的概念。打开多个 Shell 终端窗口就创建了多个 Shell 进程,每个 Shell 进程都是独立的,拥有不同的进程 ID。在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效。示例:

  1. 编写 a.sh
a.sh
1
2
3
#!/bin/bash
echo $a
b='blog'
  1. 编写 b.sh
b.sh
1
2
#!/bin/bash
echo $b
  1. 执行脚本文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 定义全局变量 a
$ a='sannaha'
# 子 Shell 中无法使用使用父 Shell 中的全局变量
$ ./a.sh

# 一个子 Shell 无法使用另一个子 Shell 中的全局变量
$ ./b.sh

# source 命令用于在当前 Shell 中执行一个文件中的命令
# 脚本中的命令可以读取到全局变量
$ source a.sh
sannaha
# 一个脚本可以读取另一个脚本中创建的全局变量
$ source b.sh
blog
# . 命令的作用和 source 一样
$ . ./a.sh
sannaha
$ . ./b.sh
blog

sh / source 等方式执行脚本的区别?

  • sh demo.sh:建立一个子 Shell 并在子 Shell 中执行脚本中的命令,子 Shell 继承父 Shell 的环境变量,但不继承父 Shell 的全局变量,同时子 Shell 中创建的变量和对变量做的改动不会带回父 Shell。
  • source demo.sh:,是读取脚本中的命令并在当前 Shell 中执行,没有建立子 Shell。脚本中所有创建、改动变量的操作都作用在当前 Shell 中。
  • ./demo.sh:只有用 chmod +x 为脚本 demo.sh 添加执行权限后才能以这种方式执行脚本,当脚本具有可执行权限时,sh demo.sh./demo.sh 两种方式执行脚本是没有区别的。
  • . demo.sh:与 source demo.sh 一致。

局部变量

与 Java 等编程语言不同,在 Shell 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果。示例:

varTest.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# 定义用户变量
username='sannaha'
# 定义函数
function func() {
# 定义局部变量
local text='hi'
local username='admin'
echo $text
echo $username
}
# 调用函数
func
# 输出函数内部的变量
echo $text
echo $username

输出结果:

1
2
3
4
5
$ ./varTest.sh
hi
admin

sannaha

如果想要变量的作用域仅限于函数内部,可以在定义变量时加上 local 命令,此时该变量就成了局部变量。示例:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# 定义函数
function func() {
local a=99
}
# 调用函数
func
# 输出函数内部的变量
echo $a

输出结果为空,表明变量 a 在函数外部无效,是一个局部变量。

变量内容替换

  • ${变量#匹配规则}:从头匹配,最短删除;
  • ${变量##匹配规则}:从头匹配,最长删除;
  • ${变量%匹配规则},从尾匹配,最短删除;
  • ${变量%%匹配规则},从尾匹配,最长删除;
  • ${变量/旧字符串/新字符串}:用新字符串替换变量中第一个旧字符串;
  • ${变量//旧字符串/新字符串},用新字符串替换变量中所有旧字符串。

示例:

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
# 定义变量 FILE_PATH
FILE_PATH=/dir1/dir2/dir3/my.file.txt

# ${变量#匹配规则},从头匹配字符 /,最短删除
$ echo ${FILE_PATH#*/}
dir1/dir2/dir3/my.file.txt

# ${变量##匹配规则},从头匹配字符 /,最长删除
$ echo ${FILE_PATH##*/}
my.file.txt

# ${变量%匹配规则},从尾匹配字符 /,最短删除
$ echo ${FILE_PATH%/*}
/dir1/dir2/dir3

# ${变量%%匹配规则},从尾匹配字符 /,最长删除
$ echo ${FILE_PATH%%/*}
结果为空

# ${变量/旧字符串/新字符串},用新字符串替换变量中第一个旧字符串
$ echo ${FILE_PATH/dir/mydir}
/mydir1/dir2/dir3/my.file.txt

# ${变量//旧字符串/新字符串},用新字符串替换变量中所有旧字符串
$ echo ${FILE_PATH//dir/mydir}
/mydir1/mydir2/mydir3/my.file.txt

变量状态测试

由变量的状态决定变量的值,变量状态包括未设定 unset、空值 null、非空 non-null。变量测试其实就是对判断和赋值语句的简化,可以简化脚本的编写,但需要记忆。变量测试表:

变量置换方式 变量y没有设置 变量y为空值 变量y设置有值
x=${y-新值} x=新值 x=$y x=$y
x=${y:-新值} x=新值 x=新值 x=$y
x=${y+新值} x为空 x=新值 x=新值
x=${y:+新值} x为空 x为空 x=新值
x=${y=新值} x=新值,y=新值 x=$y,y值不变 x=$y,y值不变
x=${y:=新值} x=新值,y=新值 x=新值,y=新值 x=$y,y值不变
x=${y?新值} 新值输出到标准错误输出 x=$y x=$y
x=${y:?新值} 新值输出到标准错误输出 新值输出到标准错误输出 x=$y

示例:

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
# 变量 var 没有设置,返回新值
$ unset var;echo ${var-sannaha}
sannaha
# 变量 var 为空值或设置有值,返回 var 的值
$ var=;echo ${var-sannaha}

$ var=HelloWorld;echo ${var-sannaha}
HelloWorld

# 变量 var 没有设置或为空值,返回新值
$ unset var;echo ${var:-sannaha}
sannaha
$ var=;echo ${var:-sannaha}

# 变量 var 设置有值,返回 var
$ var=HelloWorld;echo ${var:-sannaha}
HelloWorld

# 变量 var 没有设置,新值输出到标准错误输出
$ unset var;echo ${var?sannaha}
-bash: var: sannaha
# 变量 var 为空值或设置有值,返回 var
$ var=;echo ${var?sannaha}

$ var=HelloWorld;echo ${var?sannaha}
HelloWorld

# 变量 var 没有设置或为空值,新值输出到标准错误输出
$ unset var;echo ${var?sannaha}
-bash: var: sannaha
$ var=;echo ${var:?sannaha}
-bash: var: sannaha
# 变量 var 设置有值,返回 var
$ var=HelloWorld;echo ${var:?sannaha}
HelloWorld

变量长度

1
2
3
# 通过 ${#变量} 获得变量长度
$ myname=sannaha; echo ${#myname}
7

变量运算

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
# 定义变量并赋值
$ a=5;b=7;c=2;
# 使用运算符连接变量并不会进行计算
$ echo $a+$b*$c
5+7*2

# 通过 $((变量运算)) 进行变量运算
# 支持的运算符包括 + - * / %,以及& | ^ !(分别为AND OR XOR NOT)
$ echo $(($a+$b*$c))
19
# 在 $(( )) 中,变量前的 $ 可省去
$ echo $((a+b+c))
14
$ echo $(((a+b)/c))
6

# a, b 的值用 2 进制表示为 0101, 0111
# 与
$ echo $((a&b))
5
# 或
$ echo $((a|b))
7
# 异或
$ echo $((a^b))
2
# 非
$ echo $((a==b))
0
$ echo $((!((a==b))))
1

其他

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
# 计算变量值的长度
${#var}

# $(()) 用于进行整数运算
# 支持的运算符包括 + - * / %,以及& | ^ !(分别为AND OR XOR NOT)
$ a=5;b=7;c=2;
$ echo $a+$b+$c
5+7+2
$ echo $((a+b+c))
14
$ echo $(($a+$b+$c))
14
$ echo $(((a+b)/c))
6

在 $(( )) 中的变量名称,可于其前面加 $ 符号来替换,也可以不用,如:

$(( $a + $b * $c)) 也可得到 19 的结果

此外,$(( )) 还可作不同进位(如二进制、八进位、十六进制)作运算呢,只是,输出结果皆为十进制而已:

echo $((16#2a)) 结果为 42 (16进位转十进制)




(())的用途:

事实上,单纯用 (( )) 也可重定义变量值,或作 testing:

a=5; ((a++)) 可将 $a 重定义为 6

a=5; ((a--)) 则为 a=4

a=5; b=7; ((a < b)) 会得到 0 (true) 的返回值。

常见的用于 (( )) 的测试符号有如下这些:

<:小于

>:大于

<=:小于或等于

>=:大于或等于

==:等于

!=:不等于

[root@05d764353843 bigdata]# (( a < b ))
[root@05d764353843 bigdata]# echo $?
0
[root@05d764353843 bigdata]# echo $((a<b))
1
[root@05d764353843 bigdata]# echo $((a>b))
0

字符串

Shell 脚本中常用的数据类型除了数字就是字符串了。字符串可以用单引号或双引号标识,也可以不用引号。

单引号

1
2
3
$ author='SANNAHA'
$ echo $author
SANNAHA

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
  • 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。

双引号

1
2
3
4
$ description="Hi I'm $author! My blog is \"$author.moe\"\n"
$ echo -e $description
Hi I'm SANNAHA! My blog is "SANNAHA.moe"

双引号字符串的特点:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符

不用引号

1
2
3
$ simpleStr=HelloWorld
$ echo $simpleStr
HelloWorld

字符串拼接

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
# 双引号和单引号都可以进行字符串拼接
$ title="Hi "$author'!'
$ echo $title
Hi SANNAHA!

# 双引号中的感叹号会被当做事件提示符,用来引用历史命令
$ title1="Hi "$author"!"
-bash: !: event not found

# 为避免报错,可以使用单引号来将感叹号原样输出
# 或是不使用引号
$ title2="Hi "$author!
$ echo $title2
Hi SANNAHA!

# 使用转义可以避免报错,但无法得到想要的效果
$ title2="Hi "$author"\!"
$ echo $title2
Hi SANNAHA\!

# 但这种报错在脚本中不会出现
$ vi stringTest.sh
#!/bin/bash
author=SANNAHA
title1="Hi "$author"!"
echo $title1
$ ./stringTest.sh
Hi SANNAHA!

提取子字符串

1
2
3
4
$ longStr="my blog is sannaha.moe"
# 从第 12 个字符开始,截取 7 个字符
$ echo ${longStr:11:7}
sannaha

查找子字符串

1
2
3
4
$ longStr="my blog is sannaha.moe"
# 查找字符 b 或 g 首次出现的位置
$ echo `expr index "$longStr" bg`
4

命令替换

Shell 脚本可以从命令输出中提取信息,并将其赋给变量。有两种方法可以将命令输出赋给变量:

  • 使用反引号 ```
  • 使用 $()

示例:

  1. 编写脚本 printDate.sh
printDate.sh
1
2
3
4
5
6
7
8
#!/bin/bash
year=年
month=月
day=日
today=`date -d "-0 day" +%Y$year%m$month%d$day`
yesterday=$(date -d "-1 days" +%Y$year%m$month%d$day)
echo "today is $today"
echo "yesterday is $yesterday"
  1. 运行脚本:
1
2
3
$ ./printDate.sh
today is 2021年08月11日
yesterday is 2021年08月10日

函数

1
2
3
4
5
6
# 语法
[function] funname()
{
action;
[return int;]
}
  1. 所有函数必须先定义再使用,使用函数名即可调用函数。
  2. 定义函数时可以不带 function,只有 fun(){} 即可。
  3. 函数可以通过 return 返回运行结果,数值为 0 ~ 255,如果不加 return 将以最后一条命令运行结果作为返回值。
  4. 函数返回值可以在调用该函数后通过 $? 来获得。$? 仅对其上一条指令负责,一旦函数返回后执行了其他命令,那么函数的返回值将不再能通过 $? 获得。
  5. 调用函数时可以向其传递参数。在函数体内部可以通过 $n 的形式来获取参数的值,$1 表示第一个参数,$2 表示第二个参数……当 n>=10 时,需要使用 ${n} 获取参数。此外,还有几个特殊字符用来处理参数:
字符 说明
$# 传递到脚本或函数的参数个数
$* 以一个单字符串显示所有向脚本传递的参数
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数。
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

条件判断

if语句

Shell 支持使用 if 进行条件判断:

1
2
3
4
5
6
7
8
# 格式
if COMMAND; then
COMMANDs
elif COMMAND; then
COMMANDs
else
COMMANDs
fi

Shell 会按顺序执行 if 语句:

  1. 首先运行 if 后面的命令,比如条件测试;
  2. 然后判断命令的退出状态码,如果是 0(即命令运行成功),则会执行该条件 then 后面对应的命令,否则会跳过这些命令;
  3. 如果有 elif 会再次进行判断。只有第一个退出状态码为 0 的条件对应的命令会被执行;
  4. 如果所有判断条件的退出状态码都不为 0,则执行 else 后面的命令。

示例:

  1. 编写脚本 ifTest.sh
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
user1=$1
user2=$2

if grep ^${user1} /etc/passwd; then
echo "${user1}是Linux用户"
elif grep ^${user2} /etc/passwd; then
echo "${user1}不是Linux用户,${user2}是Linux用户"
else
echo "${user1}${user2}都不是Linux用户"
fi
  1. 运行脚本:
1
2
3
./ifTest.sh SANNAHA root
root:x:0:0:root:/root:/bin/zsh
SANNAHA不是Linux用户,root是Linux用户

case语句

case 语句为多分支选择语句,匹配一个值或一个模式,如果匹配成功,执行相匹配的命令:

1
2
3
4
5
6
7
8
9
10
11
12
# 格式
casein
模式1)
command
;;
模式2|模式3)
command
;;
*)
command
;;
esac

说明:

  • 用值去匹配的每一个分支中的模式,值可以为变量或常数,分支以 ) 结束;
  • 一个分支可以由多个用 | 分隔的模式组成;
  • 一旦匹配到某一模式后,开始执行对应的所有命令直至 ;;;; 相当于其他语言中的 break,执行完命令后不再匹配其他模式。
  • 如果上面的模式都不匹配,最后可以使用 * 进行捕获,执行对应的命令。

示例:

  1. 编写脚本 caseTest.sh
caseTest.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
option="${1}"
case ${option} in
--start)
echo "start"
;;
--stop)
echo "stop"
;;
-r | --reload)
DIR="${2}"
echo "reload"
;;
*)
echo "Usage:${0} [-start] | [-stop] | [ -r|--reload]"
exit 1
;;
esac
  1. 执行脚本:
1
2
3
4
5
6
7
8
$ ./caseTest.sh 
Usage:./caseTest.sh [--start] | [--stop] | [ -r|--reload]
$ ./caseTest.sh --start
start
$ ./caseTest.sh --stop
stop
$ ./caseTest.sh -r
reload

条件测试

测试命令

可以使用以下几种测试命令来判断条件是否满足:

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
# 评估条件表达式
# 建议用双引号将变量引起,否则变量为空时会报错
# 需要对 > 和 < 等操作符转义
test EXPRESSION
# 示例
$ if test 1 -gt 2; then echo '1大于2'; else echo '1小于2'; fi
1小于2

# 等价于 test 命令
# 将表达式放进一对中括号内,表达式两边需留空格
[ EXPRESSION ]
# 示例
$ if [ 1 -gt 2 ]; then echo '1大于2'; else echo '1小于2'; fi
1小于2

# 执行条件命令,根据条件表达式 EXPRESSION 的估值返回状态 0 或 1
# 将表达式放进两对中括号内,表达式两边需留空格
# 表达式与 test 基本一致
# 不同的是变量即使是空值也不会报错;不要转义;剔除 -o 和 -a,可用 || 和 &&
# 同时支持逻辑运算符和正则表达式:
# ( EXPRESSION ) 返回 EXPRESSION 表达式的值
# ! EXPRESSION 如果 EXPRESSION 表达式为假则为真,否则为假
# EXPR1 && EXPR2 如果 EXPR1 和 EXPR2 表达式均为真则为真,否则为假
# EXPR1 || EXPR2 如果 EXPR1 和 EXPR2 表达式中有一个为真则为真,否则为假
# STR =~ PARTTERN 对 STR 用 PARTTERN 进行正则匹配
[[ EXPRESSION ]]
# 示例
$ if [[ "123abc" =~ ^[a-z]+ ]]; then echo '字符串以英文字母开头'; else echo '字符串不以英文字母开头'; fi
字符串不以英文字母开头

高级特性

Bash shell 提供了两项在 if 语句中使用的高级命令。

双括号

相较于 test 命令只能在比较中使用简单的算术操作,双括号命令允许在比较过程中使用高级数学表达式:

1
2
# 格式
(( expression ))

表达式可以是数学赋值或比较表达式,除了 test 命令使用的标准数学运算符外,还支持一下运算符:

符号 描述
val++ 后增
val-- 后减
++val 先增
--val 先减
! 逻辑求反
~ 按位求反
** 幂运算
<< 左移
>> 右移
& 按位和
| 按位或
&& 逻辑和
|| 逻辑或

双方括号

整数测试

用于比较整数之间的数值大小,不支持比较浮点数:

  • [ INT1 -gt INT2 ]INT1 是否大于 INT2
  • [ INT1 -ge INT2 ]INT1 是否大于等于 INT2
  • [ INT1 -eq INT2 ]INT1 是否等于 INT2
  • [ INT1 -ne INT2 ]INT1 是否不等于 INT2
  • [ INT1 -lt INT2 ]INT1 是否小于 INT2
  • [ INT1 -le INT2 ]INT1 是否小于等于 INT2

示例:

1
2
3
4
5
6
7
8
$ if [ 1 -gt 2 ]; then echo '1大于2'; else echo '1小于2'; fi
1小于2
$ if [ 1 -lt 2 ]; then echo '1小于2'; else echo '1大于2'; fi
1小于2
$ if [ 1 -eq 2 ]; then echo '1等于2'; else echo '1不等于2'; fi
1不等于2
$ if [ 1 -ne 2 ]; then echo '1不等于2'; else echo '1等于2'; fi
1不等于2

字符串测试

用于测试单个字符串的长度是否为 0 以及是否为空,比较两个字符串之间的 ASCII 顺序,注意字符串与操作符之间要有空格

  • [ -z STR ]STR 长度是否为 0
  • [ -n STR ]STR 长度是否不为 0
  • [ STR ]STR 是否不为空,与 -n 类似
  • [ STR1 = STR2 ]:两个字符串是否相同
  • [ STR1 == STR2 ]:两个字符串是否相同
  • [ STR1 != STR2 ]:两个字符串是否不相同
  • [[ STR1 < STR2 ]]STR1 是否排在 STR2 前面
  • [[ STR1 > STR2 ]]STR1 是否排在 STR2 后面
  • [[ STR =~ PARTTERN ]]STR 是否能够被 PARTTERN 模式匹配,支持扩展正则

示例:

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
$ if [ -z "" ]; then echo '字符串长度为0'; else echo '字符串长度不为0'; fi
字符串长度为0

# notDefine 是个没有被定义的变量,但它的字符串长度被认为是 0
$ if [ -z $notDefine ]; then echo '字符串长度为0'; else echo '字符串长度不为0'; fi
字符串长度为0

$ if [ -n "" ]; then echo '字符串长度不为0'; else echo '字符串长度为0'; fi
字符串长度为0

$ if [ "" ]; then echo '字符串不为空'; else echo '字符串为空'; fi
字符串为空

$ if [ "a" == "b" ]; then echo '两个字符串相同'; else echo '两个字符串不相同'; fi
两个字符串不相同

$ if [ "a" != "b" ]; then echo '两个字符串不相同'; else echo '两个字符串相同'; fi
两个字符串不相同

# 使用 [ ] 测试时需要对 > 和 < 进行转义,否则会被当作重定向
$ if [ "abc" \> "aab" ]; then echo 'abc>aab'; else echo 'abc<aab'; fi
abc>aab

# 使用 [[ ]] 时无需转义
$ if [[ "abc" < "aab" ]]; then echo 'abc<aab'; else echo 'abc>aab'; fi
abc>aab

# ^ 表示开头,+ 表示匹配一次及以上
$ if [[ "123abc" =~ ^[a-z]+ ]]; then echo '字符串以英文字母开头'; else echo '字符串不以英文字母开头'; fi
字符串不以英文字母开头

逻辑测试

用于进行逻辑判断:

  • [ ! EXPR ]:逻辑非;
  • [ EXPR1 -a EXPR2 ]:逻辑与;
  • [ EXPR1 -o EXPR2 ]:逻辑或;
  • [ EXPER1 ] && [ EXPER2 ]:用逻辑与连接两个条件;
  • [ EXPER1 ] || [ EXPER2 ]:用逻辑或连接两个条件;
  • [[ EXPER1 && EXPER2 ]]:用逻辑与连接两个条件;
  • [[ EXPER1 || EXPER2 ]]:用逻辑或连接两个条件。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ if [ ! 1 -eq 1 ]; then echo 'true'; else echo 'false'; fi
false
$ if [ 1 -eq 2 -a 2 -eq 2 ]; then echo 'true'; else echo 'false'; fi
false
$ if [ 1 -eq 2 -o 2 -eq 2 ]; then echo 'true'; else echo 'false'; fi
true
$ if [ 1 -eq 2 ] && [ 2 -eq 2 ]; then echo 'true'; else echo 'false'; fi
false
$ if [ 1 -eq 2 ] || [ 2 -eq 2 ]; then echo 'true'; else echo 'false'; fi
true
$ if [[ 1 -eq 2 && 2 -eq 2 ]]; then echo 'true'; else echo 'false'; fi
false
$ if [[ 1 -eq 2 || 2 -eq 2 ]]; then echo 'true'; else echo 'false'; fi
true

文件测试

文件存在测试

  • [ -a FILE ]:是否存在文件或目录;
  • [ -e FILE ]:与 -a 相同。

文件类别测试

  • [ -b FILE ]:是否存在块设备文件;
  • [ -c FILE ]:是否存在字符设备文件;
  • [ -d FILE ]:是否存在目录文件;
  • [ -f FILE ]:是否存在普通文件;
  • [ -h FILE ]:是否存在符号链接文件(在一些老系统上无效);
  • [ -p FILE ]:是否存在命名管道文件;
  • [ -L FILE ]:是否存在符号链接文件;
  • [ -S FILE ]:是否存在套接字文件。

文件非空测试

  • [ -s FILE ]:是否存在非空文件;

文件权限测试

判断文件的 r/w/x 权限时依据的是当前登录用户,文件权限测试的前提拥有该文件所在目录的执行权限:

  • [ -r FILE ]:是否存在且可读;
  • [ -w FILE ]:是否存在且可写;
  • [ -x FILE ]:是否存在且可执行;
  • [ -g FILE ]:是否存在设置了 SGID 位的文件;
  • [ -k FILE ]:是否存在设置了 sticky 位(冒险位)的文件;
  • [ -u FILE ]:是否存在设置了 SUID 位的文件;
  • [ -O FILE ]:当前有效用户是否为文件属主;
  • [ -G FILE ]:当前有效用户是否为文件属组。

文件打开测试

  • [ -t FD ]:文件描述符 FD(默认为 1)是否打开并指向一个终端;

文件改动测试

  • [ -N FILE ]:文件自从上一次被读取之后是否被修改过;

文件双目测试

  • [ FILE1 -ef FILE2 ]:两个文件是否指向相同的设备和 *Inode号码 *
  • [ FILE1 -nt FILE2 ]FILE1 是否新于 FILE2,或者是否 FILE1 存在但 FILE2 不存在;
  • [ FILE1 -ot FILE2 ]FILE1 是否旧于 FILE2,或者是否 FILE1 不存在但 FILE2 存在。

循环

for循环

Shell 脚本支持 for 循环,有以下几种用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 循环多个值
for 变量 in 值1 值2...; do
command
done

# 循环列表
for 变量 in 列表; do
command
done

# 循环控制条件
for ((初始值; 循环控制条件; 变量变化)); do
command
done

示例:

  1. 编写脚本 forTest.sh
forTest.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
#!/bin/bash

# 循环多个值
echo '开始循环多个值'
for i in 1 2 3; do
echo $i
done

# 循环序列
echo '开始循环序列'
for j in {4..6}; do
echo $j
done

echo '开始循环seq序列'
for k in $(seq 7 9); do
echo $k
done

# 循环列表
echo '开始循环ls列表'
for lsList in $(ls /usr/ | grep s); do
echo $lsList
done

echo '开始循环路径列表'
for path in /usr/s*; do
echo $path
done

echo '开始循环文件内容'
for file in $(cat file); do
echo $file
done

# 循环控制条件
echo '开始循环控制条件'
for ((l=10; l<=12; l++)); do
echo $l
done
  1. 运行脚本:
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
$ ./forTest.sh 
开始循环多个值
1
2
3
开始循环序列
4
5
6
开始循环seq序列
7
8
9
开始循环ls列表
games
sbin
share
src
开始循环路径列表
/usr/sbin
/usr/share
/usr/src
开始循环文件内容
line1
line2
line3
开始循环控制条件
10
11
12

while循环

只要条件成立,循环就会一直继续,直到条件不成立才会停止循环:

1
2
3
while [ EXPRESSION ]; do
command
done

示例:

  1. 编写脚本 whileTest.sh
whileTest.sh
1
2
3
4
5
6
7
8
#!/bin/bash
i=1
sum=0
until [ $i -gt 100 ]; do
sum=$(($sum+$i))
i=$(($i+1))
done
echo $sum
  1. 运行脚本:
1
2
$ ./whileTest.sh 
5050

until循环

只要条件不成立,循环就一直继续,一旦条件成立就终止循环:

1
2
3
until [ EXPRESSION ]; do
command
done

示例:

  1. 编写脚本 untilTest.sh
untilTest.sh
1
2
3
4
5
6
7
8
#!/bin/bash
i=1
sum=0
while [ $i -gt 100 ]; do
sum=$(($sum+$i))
i=$(($i+1))
done
echo $sum
  1. 运行脚本:
1
2
$ ./untilTest.sh 
5050

工具命令

echo

格式:

1
2
3
4
5
6
7
$ echo [SHORT-OPTION]... [STRING]...
-n:不输出尾随换行符
-e:启用反斜杠转义的解释
-E:禁用反斜杠转义的解释(默认)

# 查看echo的帮助文档
$ /bin/echo --help

date

date:显示系统当前日期和时间

  • -s <STRING>:根据字符串设置系统日期和时间;
  • -d <STRING>:显示由字符串描述的日期和时间;
  • +<FORMAT>:指定显示格式。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 显示系统当前日期和时间
$ date
2019年 05月 06日 星期一 20:00:00 CST

# 指定格式
$ date +"%Y-%m-%d %H:%M:%S"
2019-05-06 20:00:00

# 显示完整日期,两种方式等价
$ date +%F
$ date +"%Y-%m-%d"
2019-05-06

# 显示昨天的日期,支持days/weeks/months/years
$ date -d '-1 days' +%F
2019-05-05

# 输出上个月的年月
$ date -d '-1 months' +%Y%m
201904

# 输出1970年1月1日之后18848天的日期(可以用来计算shadow文件中口令的变更日期)
$ date -d "+18848 day 19700101" +%F
2021-08-09

lsof

lsof(list open files)是一个列出当前系统打开文件的工具。在 Linux 中任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件。由于应用程序打开文件的描述符列表提供了大量关于这个应用程序本身的信息,因此使用 lsof 工具查看这个列表对系统监测以及排错将是很有帮助的。格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 格式
lsof [选项] [文件]

选项:
-a:列出打开文件的进程
-c <进程名>:列出指定进程打开的文件
-g <GID>:列出 GID 进程详情
-d <F>:列出占用该文件的进程
+d <dir>:列出目录下打开的文件
+D <dir>:递归列出目录下打开的文件
-i <[4|6][proto][@host|addr][:svc_list|port_list]>:列出符合条件的进程,比如 IP 类型(IPv4/6)、协议、IP 地址、端口号
-p <PID>:列出指定进程号所打开的文件
-u <UID>

示例

查看文件是否被占用:

1
2
3
4
5
6
7
8
#!/bin/bash
flag=$( /usr/sbin/lsof /etc/file.txt | wc -l )
if [ $flag == "0" ];then
echo "文件没被占用"
else
echo "文件被占用"
exit
fi

其他常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 列出打开文件的进程
$ lsof /bin/bash
$ lsof -a /bin/bash
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 35918 hadoop txt REG 253,2 1089480 291512 /usr/bin/bash
bash 67647 hadoop txt REG 253,2 1089480 291512 /usr/bin/bash
bash 67731 hadoop txt REG 253,2 1089480 291512 /usr/bin/bash
startHive 68761 hadoop txt REG 253,2 1089480 291512 /usr/bin/bash

# 列出指定进程打开的文件
$ lsof -c startHive
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
startHive 68761 hadoop cwd DIR 253,3 278 16777286 /home/hadoop
startHive 68761 hadoop rtd DIR 253,0 273 64 /
startHive 68761 hadoop txt REG 253,2 1089480 291512 /usr/bin/bash
startHive 68761 hadoop mem REG 253,2 106075056 50349361 /usr/lib/locale/locale-archive
startHive 68761 hadoop mem REG 253,2 2151672 16794937 /usr/lib64/libc-2.17.so
startHive 68761 hadoop mem REG 253,2 19288 16794943 /usr/lib64/libdl-2.17.so
startHive 68761 hadoop mem REG 253,2 163400 16794930 /usr/lib64/ld-2.17.so
startHive 68761 hadoop mem REG 253,2 26254 16795266 /usr/lib64/gconv/gconv-modules.cache
startHive 68761 hadoop 0u CHR 136,0 0t0 3 /dev/pts/0
startHive 68761 hadoop 1u CHR 136,0 0t0 3 /dev/pts/0
startHive 68761 hadoop 2u CHR 136,0 0t0 3 /dev/pts/0
startHive 68761 hadoop 255r REG 8,65 162 2147559258 /app/data4/shellScript/startHive.sh

sleep

sleep NUMBER[SUFFIX]...:暂停数秒时间,SUFFIX 可以是以下几种:

  • s:秒,默认
  • m:分钟
  • h:小时
  • d:天

示例:

  1. 编写脚本 loop.sh,输出数字 1~5,每秒一次:
loop.sh
1
2
3
4
5
#!/bin/bash
for ((i = 1; i <= 5; i++)); do
echo $i
sleep 1
done
  1. 编写脚本 sleep.sh,调用并在后台执行 loop.sh 脚本,暂停 3 秒后执行 echo
sleep.sh
1
2
3
4
#!/bin/bash
/root/shellScript/loop.sh &
sleep 3
echo 'loop.sh Finished'
  1. 执行 sleep.sh,可以看到在 loop.sh 执行开始 3 秒后,执行了 echo
1
2
3
4
5
6
7
$ ./sleep.sh
1
2
3
loop.sh Finished
4
5

如果 loop.sh 不是在后台执行,可以看到会在 loop.sh 执行完后会等待 3 秒,再输出 echo 信息。

wait

wait [-n] [ID...]wait 命令用于等待任务完成并返回任务的退出状态

  • -n:等待下一个作业的完成并返回其退出状态;

  • ID:可以是进程 ID(PID)或任务编号(又叫任务声明,job_specification),如果没有给出 ID,wait 默认等待所有当前活跃的子进程,并且返回状态为 0;如果 ID 是任务编号,会等待任务管道中的的所有进程。

示例:

  1. 编写脚本 wait.sh,调用并在后台执行 loop.sh 脚本:
wait.sh
1
2
3
4
5
6
7
8
#!/bin/bash
/root/shellScript/loop.sh &
wait
echo 'loop.sh Finished'
/root/shellScript/loop.sh &
pid=$!
wait $pid
echo "loop.sh $pid Finished"
  1. 执行 wait.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./wait.sh
1
2
3
4
5
loop.sh Finished
1
2
3
4
5
loop.sh 11200 Finished

basename

basename 命令用于从路径字符串中提取文件名,可以选择是否保留文件后缀:

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
# 格式
basename NAME [SUFFIX]

选项:
-a, --multiple 支持处理多个文件名
-s, --suffix=<SUFFIX> 指定文件后缀为 SUFFIX 并删除后缀
-z, --zero 输出多个文件名时不使用换行分割

# 示例
# 取文件名
$ basename /usr/bin/sannaha.log
sannaha.log

# 删除后缀
$ basename bin/startHive.sh .sh
startHive

# 取多个文件名
$ basename -a bin/startHive.sh bin/startHBase.sh
startHive.sh
startHBase.sh

# 取多个文件名,删除后缀
$ basename -a -s .sh bin/startHive.sh bin/startHBase.sh
startHive
startHBase

# 取多个文件名,删除后缀,不使用换行分割
$ basename -a -z -s .sh bin/startHive.sh bin/startHBase.sh
startHivestartHBase

# 获取脚本的文件名
#!/bin/bash
scriptName=$(basename $0)

dirname

dirname 命令用于从路径字符串中提取目录,会删除字符串中最后一个 / 及后面的内容,如果名称不包含 /,则输出 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 格式
dirname [OPTION] NAME...

选项:
-z, --zero 输出多个目录时不使用换行分割

# 示例
# 取目录
$ dirname /usr/bin/sannaha.log
/usr/bin

# 取多个目录
$ dirname bin/startHive.sh startHBase.sh
bin
.

# 取多个目录,不使用换行分割
$ dirname -z bin/startHive.sh startHBase.sh
bin.

# 获取脚本所在目录
#!/bin/bash
scriptDir=$(dirname $0)

awk

awk 是一个处理文本文件的应用程序,几乎所有 Linux 系统都自带这个程序。它依次处理文件的每一行,并读取里面的每一个字段。对于日志、CSV 那样的每行格式相同的文本文件,awk 可能是最方便的工具。

基本格式

awk 的基本格式:

1
2
3
4
5
# 格式
$ awk 动作 文件名

# 示例
$ awk '{print $0}' demo.txt

上面示例中,demo.txtawk 所要处理的文本文件。前面单引号内部有一个大括号,里面就是每一行的处理动作 print $0。其中 print 是打印命令,$0 代表当前行,因此上面命令的执行结果,就是把每一行原样打印出来。

用标准输入(stdin)演示这个例子:

1
2
3
# 使用 print $0 把标准输入 this is a test 打印出来
$ echo 'this is a test' | awk '{print $0}'
this is a test

awk 会根据空格和制表符,将每一行分成若干字段,依次用 $1$2$3… 代表第一个字段、第二个字段、第三个字段等:

1
2
3
# $3 代表 this is a test 的第三个字段 a
$ echo 'this is a test' | awk '{print $3}'
a

为了便于接下来的演示,把 /etc/passwd 文件保存成 passwd.txt

1
2
3
4
5
6
$ cat /etc/passwd > passwd.txt

root:x:0:0:root:/root:/bin/bash
daemon:x:2:2:daemon:/sbin:/sbin/nologin
bin:x:1:1:bin:/bin:/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync

这个文件的字段分隔符是冒号(:),所以要用-F参数指定分隔符为冒号。然后,才能提取到它的第一个字段。

1
2
3
4
5
6
$ awk -F ':' '{ print $1 }' demo.txt
root
daemon
bin
sys
sync

变量

除了 $数字 表示某个字段,awk还提供其他一些其他内置变量:

  • NF: 表示当前行有多少个字段,因此 $NF 代表最后一个字段,$(NF-1) 就代表倒数第二个字段
  • NR:表示当前处理的是第几行
  • FILENAME:当前文件名
  • FS:字段分隔符,默认是空格和制表符。
  • RS:行分隔符,用于分割每一行,默认是换行符。
  • OFS:输出字段的分隔符,用于打印时分隔字段,默认为空格。
  • ORS:输出记录的分隔符,用于打印时分隔记录,默认为换行符。
  • OFMT:数字输出的格式,默认为%.6g
1
2
3
4
5
$ echo 'this is a test' | awk '{print $NF}'
test

$ echo 'this is a test' | awk '{print $(NF-1), $NF}'
a test

上面代码中,print命令里面的逗号,表示输出的时候,两个部分之间使用空格分隔。

变量 NR 表示当前处理的是第几行。

1
2
3
4
5
6
$ awk -F ':' '{print NR ") " $1}' demo.txt
1) root
2) daemon
3) bin
4) sys
5) sync

上面代码中,print 命令里面,如果原样输出字符,要放在双引号里面。

awk 的其他内置变量如下:

  • FILENAME:当前文件名
  • FS:字段分隔符,默认是空格和制表符。
  • RS:行分隔符,用于分割每一行,默认是换行符。
  • OFS:输出字段的分隔符,用于打印时分隔字段,默认为空格。
  • ORS:输出记录的分隔符,用于打印时分隔记录,默认为换行符。
  • OFMT:数字输出的格式,默认为%.6g

函数

awk还提供了一些内置函数,方便对原始数据的处理。

函数toupper()用于将字符转为大写。

1
2
3
4
5
6
$ awk -F ':' '{ print toupper($1) }' demo.txt
ROOT
DAEMON
BIN
SYS
SYNC

上面代码中,第一个字段输出时都变成了大写。

其他常用函数如下。

  • tolower():字符转为小写。
  • length():返回字符串长度。
  • substr():返回子字符串。
  • sin():正弦。
  • cos():余弦。
  • sqrt():平方根。
  • rand():随机数。

awk内置函数的完整列表,可以查看 手册

条件

awk允许指定输出条件,只输出符合条件的行。

输出条件要写在动作的前面。

1
$ awk '条件 动作' 文件名

请看下面的例子。

1
2
3
4
5
$ awk -F ':' '/usr/ {print $1}' demo.txt
root
daemon
bin
sys

上面代码中,print命令前面是一个正则表达式,只输出包含usr的行。

下面的例子只输出奇数行,以及输出第三行以后的行。

1
2
3
4
5
6
7
8
9
10
# 输出奇数行
$ awk -F ':' 'NR % 2 == 1 {print $1}' demo.txt
root
bin
sync

# 输出第三行以后的行
$ awk -F ':' 'NR >3 {print $1}' demo.txt
sys
sync

下面的例子输出第一个字段等于指定值的行。

1
2
3
4
5
6
$ awk -F ':' '$1 == "root" {print $1}' demo.txt
root

$ awk -F ':' '$1 == "root" || $1 == "bin" {print $1}' demo.txt
root
bin

if语句

awk提供了if结构,用于编写复杂的条件。

1
2
3
4
$ awk -F ':' '{if ($1 > "m") print $1}' demo.txt
root
sys
sync

上面代码输出第一个字段的第一个字符大于m的行。

if结构还可以指定else部分。

1
2
3
4
5
6
$ awk -F ':' '{if ($1 > "m") print $1; else print "---"}' demo.txt
root
---
---
sys
sync

示例

1
2
3
4
5
6
7
8
9
# 读取文件每行数据的前两个字段,写入为新文件
# -v给变量OFS赋值,指定输出时字段分隔符为逗号
# -F指定字段分隔符为逗号
awk -v OFS=',' -F ',' '{print $1,$2}' file.csv > file_awk.csv

# 读取${TMP_PATH}指定文件,判断如果第1个字段的值为sannaha,输出第1、第2以及最后一个字段到${PUSH_PATH}
# -v给变量OFS赋值,指定输出时字段分隔符为逗号
# -F指定字段分隔符为逗号
awk -v OFS="," -F ',' '{if ($1 == "sannaha") print $1,$2,$NF}' "${TMP_PATH}" >>"${PUSH_PATH}"

sed

sed 命令主要用来处理和编辑文件,简化对文件的反复操作,能够完美的配合正则表达式使用。处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(pattern space),接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。文件内容并没有改变,除非你使用重定向存储输出。

语法

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
64
65
66
67
68
69
70
sed [-hnV][-e<script>][-f<script文件>][文本文件]

参数说明:
-e<script>或--expression=<script> 以选项中指定的script来处理输入的文本文件。
-f<script文件>或--file=<script文件> 以选项中指定的script文件来处理输入的文本文件。
-h或--help 显示帮助。
-n或--quiet或--silent 仅显示script处理后的结果。
-V或--version 显示版本信息。
-i直接编辑文件

动作说明:
a :新增, a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行)~
c :取代, c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行!
d :删除,因为是删除啊,所以 d 后面通常不接任何咚咚;
i :插入, i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行);
p :打印,亦即将某个选择的数据印出。通常 p 会与参数 sed -n 一起运行~
s :取代,可以直接进行取代的工作哩!通常这个 s 的动作可以搭配正规表示法!例如 1,20s/old/new/g 就是啦!

------

sed命令:
a\ 在当前行下面插入文本。
i\ 在当前行上面插入文本。
c\ 把选定的行改为新的文本。
d 删除,删除选择的行。
D 删除模板块的第一行。
s 替换指定字符
h 拷贝模板块的内容到内存中的缓冲区。
H 追加模板块的内容到内存中的缓冲区。
g 获得内存缓冲区的内容,并替代当前模板块中的文本。
G 获得内存缓冲区的内容,并追加到当前模板块文本的后面。
l 列表不能打印字符的清单。
n 读取下一个输入行,用下一个命令处理新的行而不是用第一个命令。
N 追加下一个输入行到模板块后面并在二者间嵌入一个新行,改变当前行号码。
p 打印模板块的行。
P(大写) 打印模板块的第一行。
q 退出Sed。
b lable 分支到脚本中带有标记的地方,如果分支不存在则分支到脚本的末尾。
r file 从file中读行。
t label if分支,从最后一行开始,条件一旦满足或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。
T label 错误分支,从最后一行开始,一旦发生错误或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。
w file 写并追加模板块到file末尾。
W file 写并追加模板块的第一行到file末尾。
! 表示后面的命令对所有没有被选定的行发生作用。
= 打印当前行号码。
# 把注释扩展到下一个换行符以前。

替换标记:
g 表示行内全面替换。
p 表示打印行。
w 表示把行写入一个文件。
x 表示互换模板块中的文本和缓冲区中的文本。
y 表示把一个字符翻译为另外的字符(但是不用于正则表达式)
\1 子串匹配标记
& 已匹配字符串标记

元字符集
^ 匹配行开始,如:/^sed/匹配所有以sed开头的行。
$ 匹配行结束,如:/sed$/匹配所有以sed结尾的行。
. 匹配一个非换行符的任意字符,如:/s.d/匹配s后接一个任意字符,最后是d。
* 匹配0个或多个字符,如:/*sed/匹配所有模板是一个或多个空格后紧跟sed的行。
[] 匹配一个指定范围内的字符,如/[ss]ed/匹配sed和Sed。
[^] 匹配一个不在指定范围内的字符,如:/[^A-RT-Z]ed/匹配不包含A-R和T-Z的一个字母开头,紧跟ed的行。
\(..\) 匹配子串,保存匹配的字符,如s/\(love\)able/\1rs,loveable被替换成lovers。
& 保存搜索字符用来替换其他字符,如s/love/**&**/,love这成**love**。
\< 匹配单词的开始,如:/\<love/匹配包含以love开头的单词的行。
\> 匹配单词的结束,如/love\>/匹配包含以love结尾的单词的行。
x\{m\} 重复字符x,m次,如:/0\{5\}/匹配包含5个0的行。
x\{m,\} 重复字符x,至少m次,如:/0\{5,\}/匹配至少有5个0的行。
x\{m,n\} 重复字符x,至少m次,不多于n次,如:/0\{5,10\}/匹配5~10个0的行。

示例

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
# 替换末尾的逗号
# <前缀>/<查找的内容>/<用于替换的内容>/<后缀> s表示替换指定字符,即后面的,$ ,$表示行末的逗号
$ echo aa,bb,cc, | sed 's/,$//' >> result.txt

$ cat result.txt
aa,bb,cc

# 批量替换脚本中使用的队列名称
sed -i 's/defaultQueue/sannahaQueue/g' ./*.sh
# 批量替换脚本中使用的队列名称
sed -i 's/\/app\/data4/\/app\/data1/g' table_backup.sh

# Hive 数据导出为 csv 文件
# 1. 导出数据,指定分割符为逗号
insert overwrite local directory '/local/dir'
row format delimited
fields terminated by ','
select col_name1,col_name2 from table_name
;
# 从 Hive 中导出数据分布在多个小文件中
$ ls /local/dir/
000000_0
000001_0
...

# 2.合并为单个文件
$ cat *_0 > single.csv

# 将字段名插入文件第一行之前
$ sed -i '1i col_name1,col_name2' single.csv
$ head single.csv
col_name1,col_name2
data1,data2

wc

wc 命令用于计算文件的行数、字数和字节数,如果文件个数不止一个,还会将多个文件汇总统计。统计行数实际上是统计换行符 \n 出现的次数,统计字数则是统计用空格分隔的非空字符序列的个数。

语法

1
2
3
4
5
6
7
wc [option] <file...>
参数:
-l, --lines 显示行数
-w, --words 显示字数
-m, --chars 显示字符数
-c, --bytes 显示字节数
-L, --max-line-length 显示最长行的长度

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# wc 默认会统计行数、字数、字节数
$ wc tmp.txt
3 5 49 tmp.txt
# 使用参数指定统计类型
# 显示内容的先后顺序总是为行数、字数、字符数、字节数、最长行的长度
$ wc -lwmcL tmp.txt
3 5 49 49 32 tmp.txt

# 统计多个文件,会显示多个文件汇总统计结果
$ wc file1.txt file2.csv
3105520 3105520 37266240 file1.txt
3195780 3207266 162007753 file2.csv
6301300 6312786 199273993 total

ftp

语法

1
2
3
4
5
6
7
ftp [option] [host]
参数:
-v:强制FTP显示来自远程服务器的所有响应,以及数据传输统计信息的报告。
-d:启用调试,显示FTP客户端和FTP服务器之间传递的所有命令。
-i:在多个文件传输期间禁用交互式提示。
-n:在初始连接时禁止自动登录。
-g:禁止使用通配符。Glob 允许使用星号 (*) 和问号 (?) 作为本地文件名和路径名中的通配符。

连接服务器

1
2
$ ftp -in 192.168.153.121
ftp> user ftpadmin ftpadmin

上传文件

1
2
3
4
5
6
7
# 上传单个文件
# put [local/path/]lfile [remote/path/rfile]
ftp> put result.txt
ftp> put result.txt dir/res.txt

# 批量上传文件,支持通配符,不支持绝对路径
ftp> mput filename*

下载文件

1
2
3
4
5
6
7
8
9
10
11
12
# get remote-file [local-file]
# 从远程服务器下载文件到本地,如果未指定本地文件名,则与远程服务器上的名称相同
ftp> get rfile
ftp> get rfile lfile

# mget remote-files
# 从服务器下载文件到本地,支持通配符
ftp> mget *.txt
mget result.txt? y
6 bytes received in 0.000301 secs (19.93 Kbytes/sec)
mget result2.txt? y
4 bytes received in 0.000582 secs (6.87 Kbytes/sec)

更改传输模式

1
2
3
4
5
6
7
8
9
10
11
# 查看当前传输模式
ftp> type
Using binary mode to transfer files.

# 切换为字符模式
ftp> asc
200 Switching to ASCII mode.

# 切换为二进制模式
ftp> bin
200 Switching to Binary mode.

示例

获取文件列表

  1. 使用 nlist 命令获取文件列表:
1
2
3
4
5
6
7
8
9
#!/bin/bash
ftp_dir='/ftp_dir'
ls_file="/app/data/sannaha/ls.txt"

ftp -in 192.168.153.121 << EOT
user username password
nlist ${ftp_dir} ${ls_file}
bye
EOT

查看文件内容:

1
2
3
4
$ cat /app/data/sannaha/ls.txt
/ftp_dir/model_test1.csv
/ftp_dir/model_test2.csv
/ftp_dir/test.csv
  1. 使用 ls 命令获取文件列表,支持通配符,显示文件大小等信息:
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
ftp_dir="/ftp_dir"
ls_file="/app/data/sannaha/ls.txt"

ftp -in 192.168.153.121 << EOT > ${ls_file}
user username password
cd ${ftp_dir}
ls model*
bye
EOT

查看文件内容:

1
2
3
$ cat /app/data/sannaha/ls.txt
-rw-r--r-- 1 1019 1019 92985063 Aug 07 02:17 model_test1.csv
-rw-r--r-- 1 1019 1019 84522406 Sep 09 10:42 model_test2.csv

判断文件是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
ftp_dir='/ftp_dir'
ftp_filename="filename.txt"
local_dir="/local_dir"

file_num=$(echo "
user username password
cd ${ftp_dir}
ls
bye
" | ftp -v -n 192.168.153.121 | grep "${ftp_filename}" | wc -l)

if [ $file_num -ne 1 ]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件不存在, file_num=${file_num}
exit 1
fi

echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件存在

判断文件大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
ftp_dir='/ftp_dir'
ftp_filename="filename.txt"
local_dir="/local_dir"

file_size=$(echo "
user username password
cd ${ftp_dir}
ls
bye
" | ftp -v -n 192.168.153.121 | grep ${ftp_filename} | awk '{print $5}')

if [ $file_size -lt 100000000 ]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件大小异常, file_size=${file_size}
exit 1
fi

echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件大小正常

下载文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
ftp_dir='/ftp_dir'
ftp_filename="filename.txt"
local_dir="/local_dir"

echo "
user username password
prompt
lcd ${local_dir}
cd ${ftp_dir}
!pwd
pwd
binary
get ${ftp_filename}
bye
" | ftp -v -n 192.168.153.121

if [ $? -ne 0 ]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件下载失败
exit 1
fi

echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件下载完成

lftp

lftp 是一个功能强大的文件传输工具,支持 ftp 协议。

语法

1
2
3
lftp [-d] [-e cmd] [-p port] [-u user[,pass]] [site]
lftp -f script_file # 执行脚本后退出
lftp -c commands # 在选择后执行命令

连接服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
# 连接 ftp 服务器,连接后使用 user <username> <password> 命令登录
$ lftp ftp://192.168.153.121
lftp 192.168.153.121:~> user ftpadmin ftpadmin
# 连接 ftp 服务器,在 URL 中指定身份验证信息
$ lftp ftp://ftpadmin:ftpadmin@192.168.153.121
# 连接 ftp 服务器,使用 -p 指定端口,-u 指定用户和密码
$ lftp -p 21 -u ftpadmin,ftpadmin ftp://192.168.153.121
# 使用 -e 执行后面的命令
$ lftp -p 21 -u ftpadmin,ftpadmin ftp://192.168.153.121 -e "cd data; put top.sql -o top.sql.tmp;mv top.sql.tmp top.sql;exit;" >> /dev/null

# 退出
# exit [bg] [top] [kill] [code]
lftp ftpadmin@192.168.153.121:~> exit

操作服务器

lftp 中 pwd / ls / cd / mv / rm / mkdir / cat 等命令的功能与 Shell 基本一致,用来查看与操作 FTP 服务器上的文件和目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# pwd [-p]
# 显示当前的远程 URL 地址
# 使用 -p 会在 URL 中显示密码(使用带密码的 lftp 命令登录后默认会在 URL 中显示密码信息)
lftp ftpadmin@192.168.153.121:~> pwd
ftp://ftpadmin@192.168.153.121:21
lftp ftpadmin@192.168.153.121:~> pwd -p
ftp://ftpadmin:ftpadmin@192.168.153.121:21

# ls params
lftp ftpadmin@192.168.153.121:~> ls
drwx------ 2 ftp ftp 71 Jul 21 08:59 data
-rw------- 1 ftp ftp 0 Jul 21 08:58 result.txt

# cls [OPTS] files...
# cls 是在检索指定文件或目录的信息后自行格式化,可以设置格式,ls 是请求服务器格式化文件列表
lftp ftpadmin@192.168.153.121:/> cls
data/

# cd rdir
lftp ftpadmin@192.168.153.121:/> cd data/

操作本地

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
# lpwd 
# 显示本地机器当前工作目录
lftp ftpadmin@192.168.153.121:~> lpwd
/opt/bigdata/hivedata

# lcd ldir
# 改变本地目录
lftp ftpadmin@192.168.153.121:~> lcd /opt/bigdata/
lcd 成功, 本地目录=/opt/bigdata

# ! shell command
# 在本地执行 shell 命令
# 使用 !ls 显示本地目录下的内容
lftp ftpadmin@192.168.153.121:~> !ls
course.csv score.csv student.csv teacher.csv
# ! 也可以与 cd 等命令组合,但不会改变 lftp 的本地工作目录
lftp ftpadmin@192.168.153.121:/data> !pwd
/root
lftp ftpadmin@192.168.153.121:/data> lpwd
/root
lftp ftpadmin@192.168.153.121:/data> ! cd /opt/
lftp ftpadmin@192.168.153.121:/data> lpwd
/root
lftp ftpadmin@192.168.153.121:/data> lcd /opt
lcd 成功, 本地目录=/opt
lftp ftpadmin@192.168.153.121:/data> lpwd
/opt

下载文件

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
# 从服务器下载文件到本地,使用 Ctrl+C 可以中断下载
# get [-E] [-a] [-c] [-e] [-O base] rfile [-o lfile] ...
# -c:断点续传
# -E:传输成功后删除源文件
# -e:传输前删除目标文件
# -a:使用ascii模式,默认为二进制
# -O <base>:指定放置文件的目录或URL
# -o <lfile>:保存文件名
> get hadoopserver.tar -o hadoopserver.tar.tmp
中断

# 从服务器下载文件到本地,支持通配符
# mget [-c] [-d] [-a] [-E] [-O base] files
# -c:断点续传
# -d:创建与文件名相同的目录并将文件放入其中
# -E:传输成功后删除源文件
# -a:使用ascii模式,默认为二进制
# -O <base>:指定放置文件的目录或URL
> mget *.csv
522 bytes transferred
Total 2 files transferred

# 建立多个连接获取指定的文件
# 可以加快传输速度,但会给服务器和网络带来压力,影响其他用户使用
# pget [OPTS] rfile [-o lfile]
# -c:断点续传
# -n maxconn:设置最大连接数
> pget -c -n 10 hadoopserver.tar -o hadoopserver.tar.tmp
814088563 bytes transferred in 18 seconds (42.84M/s)

上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 文件上传到服务器
# put [-E] [-a] [-c] [-O base] lfile [-o rfile]
# -c:断点续传
# -E:传输成功后删除源文件
# -e:传输前删除目标文件
# -a:使用ascii模式,默认为二进制
# -O <base>:指定放置文件的目录或URL
# -o <lfile>:保存文件名
> put result.txt -o result.txt.tmp

# 文件上传到服务器,支持通配符
# mput [-c] [-d] [-a] [-E] [-O base] files
# -c:断点续传
# -E:传输成功后删除源文件
# -e:传输前删除目标文件
# -a:使用ascii模式,默认为二进制
# -O <base>:指定放置文件的目录或URL
# -o <lfile>:保存文件名
> mput *.csv
654 bytes transferred
Total 4 files transferred

移动和删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 重命名/移动文件
# mv file1 file2
lftp ftpadmin@192.168.153.121:/data> mv result.txt result.txt.tmp
rename successful

# 移动多个文件到指定目录,该命令为 4.8.0 版本新特性
# mmv [-O directory] file(s) directory

# 删除文件
# rm [-r] [-f] files
# -r:递归删除
# -f:不显示错误信息
> rm dir
rm: Access failed: 550 Delete operation failed. (dir)
> rm -f dir
> rm -r dir
rm 成功, 删除 `dir'

# 删除文件,支持通配符
# mrm file(s)
> mrm file*.txt
rm ok, 3 files removed

镜像目录

1
2
3
4
5
6
7
8
# 使用 mirror 将指定的源目录镜像到目标目录
# 默认将远程目录下载到本地目录
# 使用 -R 将本地的上传到远程目录
# mirror [OPTS] [source [target]]
> mirror -R hivedata
Total: 1 directory, 6 files, 0 symlinks
New: 6 files, 0 symlinks
654 bytes transferred

示例

获取文件列表

1
2
3
4
5
#!/bin/bash
ftp_dir="/ftp_dir"
ls_file="/app/data/sannaha/ls.txt"

lftp ftp://username:password@192.168.153.121 -e "cd ${ftp_dir}; ls; exit;" > ${ls_file}

判断文件是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
ftp_dir="/ftp_dir"
ftp_filename="filename.txt"
ftp_filename2="filename2.txt"
ls_log="/app/data/sannaha/ls.log"

lftp ftp://username:password@192.168.153.121 -e "cd ${ftp_dir}; ls; exit;" > ${ls_log}
file_num=$(grep ${ftp_filename} ${ls_log} | wc -l)
file_num2=$(grep ${ftp_filename2} ${ls_log} | wc -l)

if [ $file_num -ne 1 ]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : ${ftp_filename}不存在, file_num=${file_num}
exit 1
fi

if [ $file_num2 -ne 1 ]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : ${ftp_filename2}不存在, file_num=${file_num}
exit 1
fi

echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件存在

判断文件大小

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
ftp_dir='/ftp_dir'
ftp_filename="filename.txt"
local_dir="/local_dir"

file_size=$(lftp -p 21 -u username,password ftp://192.168.153.121 -e "ls ${ftp_dir}; exit;" | grep "${ftp_filename}" | awk '{print $5}')

if [ $file_size -lt 100000000 ]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件大小异常, file_size=${file_size}
exit 1
fi

echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : 文件大小正常

下载文件

1
2
3
4
5
6
#!/bin/bash
ftp_dir='/ftp_dir'
ftp_filename="filename.txt"
local_dir="/local_dir"

lftp ftp://username:password@192.168.153.121 -e "lcd ${local_dir}; cd ${ftp_dir}; get ${ftp_filename}; exit;"

except

expect 是一个用来处理交互的命令,可以使用 expect 完成登录时密码的自动输入。

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spawn [args] program [args]

send [-flags] string

expect [[-opts] pat1 body1] ... [-opts] patn [bodyn]
# expect command example
expect {
busy {puts busy\n ; exp_continue}
failed abort
"invalid password" abort
timeout abort
connected
}

interact [string1 body1] ... [stringn [bodyn]]

示例

编写 expect 脚本:

expect_lftp_cmd.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
#!/usr/bin/expect
# 判断参数数量是否符合要求
if {$argc != 4} {
send_user "Usage:$argv0 <ip> <username> <password> <lftp COMMEND>\n"
exit 1
}

# 设置超时时间
set timeout 30
# 接收参数
set ip [lindex $argv 0]
set url "ftp://$ip"
set username [lindex $argv 1]
set password [lindex $argv 2]
set COMMEND [lindex $argv 3]

# 启动一个 lftp 进程,连接 FTP 服务器
spawn lftp -u ${username} ${url}
# 等待模式匹配,匹配成功发送密码
expect "*口令:*" {send "$password\r"}
# 发送操作 lftp 的命令
send "$COMMEND\r"
# 如果用户想要与 lftp 交互,可以将当前进程的控制权交给用户
# interact
# 退出 lftp
send "exit\r"
# 等待 lftp 的退出提示 EOF
expect eof

执行命令:

1
$ ./expect_lftp_demo.sh 192.168.153.121 ftpadmin ftpadmin "mput /opt/bigdata/script/*.sh"
  • $argc 表示参数个数;
  • $argv0 表示可执行文件的名字,此处即为脚本名 ./expect_lftp_cmd.sh
  • [lindex $argv 0] 表示第一个参数,[lindex $argv 1] 表示第二个参数…;
  • set 用来设置变量;
  • set timeout num 用来设置超时时间(秒),默认为 10,无限超时可以设置为 -1
  • spawn 用于启动一个新进程;
  • expect 用来等待匹配以下几种模式:从衍生进程接收到特定字符串;超过指定的时间;看到文件结尾。根据进程的反馈,再发送对应的交互命令。如果模式的关键字是 timeout,则在超时后执行相应的命令,如果关键字是 default,则在超时或文件结束时执行相应的命令。
  • send 向当前进程发送一个字符串,换行符用 \r 表示;
  • interact 将当前进程的控制权交给用户,即允许用户与进程交互。

范例参考

参考资料:*expect - 自动交互脚本 **expect教程中文版 **expect说明 *

自动 telnet 会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/expect -f
set ip [lindex $argv 0 ] # 接收第1个参数,作为IP
set userid [lindex $argv 1 ] # 接收第2个参数,作为userid
set mypassword [lindex $argv 2 ] # 接收第3个参数,作为密码
set mycommand [lindex $argv 3 ] # 接收第4个参数,作为命令
set timeout 10 # 设置超时时间

# 向远程服务器请求打开一个telnet会话,并等待服务器询问用户名
spawn telnet $ip
expect "username:"
# 输入用户名,并等待服务器询问密码
send "$userid\r"
expect "password:"
# 输入密码,并等待键入需要运行的命令
send "$mypassword\r"
expect "%"
# 输入预先定好的密码,等待运行结果
send "$mycommand\r"
expect "%"
# 将运行结果存入到变量中,显示出来或者写到磁盘中
set results $expect_out(buffer)
# 退出telnet会话,等待服务器的退出提示EOF
send "exit\r"
expect eof

自动建立 FTP 会话:

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
#!/usr/bin/expect -f
set ip [lindex $argv 0 ] # 接收第1个参数,作为IP
set userid [lindex $argv 1 ] # 接收第2个参数,作为Userid
set mypassword [lindex $argv 2 ] # 接收第3个参数,作为密码
set timeout 10 # 设置超时时间

# 向远程服务器请求打开一个FTP会话,并等待服务器询问用户名
spawn ftp $ip
expect "username:"
# 输入用户名,并等待服务器询问密码
send "$userid\r"
expect "password:"
# 输入密码,并等待FTP提示符的出现
send "$mypassword\r"
expect "ftp>"
# 切换到二进制模式,并等待FTP提示符的出现
send "bin\r"
expect "ftp>"
# 关闭ftp的提示符
send "prompt\r"
expect "ftp>"
# 下载所有文件
send "mget *\r"
expect "ftp>"
# 退出此次ftp会话,并等待服务器的退出提示EOF
send "bye\r"
expect eof

自动登录 ssh 执行命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/expect
set IP [lindex $argv 0]
set USER [lindex $argv 1]
set PASSWD [lindex $argv 2]
set CMD [lindex $argv 3]

spawn ssh $USER@$IP $CMD
expect {
"(yes/no)?" {
send "yes\r"
expect "password:"
send "$PASSWD\r"
}
"password:" {send "$PASSWD\r"}
"* to host" {exit 1}
}
expect eof

自动登录 ssh:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/expect -f  
set ip [lindex $argv 0 ] # 接收第1个参数,作为IP
set username [lindex $argv 1 ] # 接收第2个参数,作为username
set mypassword [lindex $argv 2 ] # 接收第3个参数,作为密码
set timeout 10 # 设置超时时间

spawn ssh $username@$ip # 发送ssh请求
expect { # 返回信息匹配
"*yes/no" { send "yes\r"; exp_continue} # 第一次ssh连接会提示yes/no,继续
"*password:" { send "$mypassword\r" } # 出现密码提示,发送密码
}
interact # 交互模式,用户会停留在远程服务器上面

批量登录 ssh 服务器执行操作范例,设定增量的 for 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/expect
for {set i 10} {$i <= 12} {incr i} {
set timeout 30
set ssh_user [lindex $argv 0]
spawn ssh -i .ssh/$ssh_user abc$i.com

expect_before "no)?" {
send "yes\r" }
sleep 1
expect "password*"
send "hello\r"
expect "*#"
send "echo hello expect! > /tmp/expect.txt\r"
expect "*#"
send "echo\r"
}
exit

批量登录 ssh 并执行命令,foreach 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/expect
if {$argc!=2} {
send_user "usage: ./expect ssh_user password\n"
exit
}
foreach i {11 12} {
set timeout 30
set ssh_user [lindex $argv 0]
set password [lindex $argv 1]
spawn ssh -i .ssh/$ssh_user root@xxx.yy.com
expect_before "no)?" {
send "yes\r" }
sleep 1

expect "Enter passphrase for key*"
send "password\r"
expect "*#"
send "echo hello expect! > /tmp/expect.txt\r"
expect "*#"
send "echo\r"
}
exit

另一自动 ssh 范例,从命令行获取服务器 IP,foreach 语法,expect 嵌套:

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
#!/usr/bin/expect
# 使用方法: script_name ip1 ip2 ip3 ...

set timeout 20
if {$argc < 1} {
puts "Usage: script IPs"
exit 1
}
# 替换你自己的用户名
set user "username"
#替换你自己的登录密码
set password "yourpassword"

foreach IP $argv {
spawn ssh $user@$IP

expect \
"(yes/no)?" {
send "yes\r"
expect "password:?" {
send "$password\r"
}
} "password:?" {
send "$password\r"
}

expect "\$?"
# 替换你要执行的命令
send "last\r"
expect "\$?"
sleep 10
send "exit\r"
expect eof
}

ssh 自动登录 expect 脚本:

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
#!/usr/bin/expect -f
# Auther:YuanXing
# Update:2014-02-08
if {$argc < 4} {
send_user "Usage:\n $argv0 IPaddr User Passwd Port Passphrase\n"
puts stderr "argv error!\n"
sleep 1
exit 1
}

set ip [lindex $argv 0 ]
set user [lindex $argv 1 ]
set passwd [lindex $argv 2 ]
set port [lindex $argv 3 ]
set passphrase [lindex $argv 4 ]
set timeout 6
if {$port == ""} {
set port 22
}
#send_user "IP:$ip,User:$user,Passwd:$passwd,Port:$port,Passphrase:$passphrase"
spawn ssh -p $port $user@$ip

expect_before "(yes/no)\\?" {
send "yes\r"}

expect \
"Enter passphrase for key*" {
send "$passphrase\r"
exp_continue
} " password:?" {
send "$passwd\r"
exp_continue
} "*\[#\\\$]" {
interact
} "* to host" {
send_user "Connect faild!"
exit 2
} timeout {
send_user "Connect timeout!"
exit 2
} eof {
send_user "Lost connect!"
exit
}

实例

产生随机数

通过系统环境变量 $RANDOM 产生随机数:

1
2
3
4
5
6
7
8
9
10
11
# $RANDOM在每次调用时返回一个0-32767的伪随机整数
$ echo $RANDOM
114
$ echo $RANDOM
514
# 获取8位随机字符
echo $RANDOM |md5sum |cut -c 1-8
6ccfflty
# 获取8位随机数字
echo $RANDOM |cksum |cut -c 1-8
19198100

判断Hive表是否存在

1
2
3
4
5
6
7
8
9
#!/bin/bash
month=$(date +%Y%m)
table_exist=$(hive -e "desc sannaha.tbname_${month};" 2>&1 | grep "Table not found")
if [ -z "$table_exist" ]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : "db.tbname_${month}已具备"
else
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : "db.tbname_${month}不具备"
exit 1
fi

判断Hive表是否有数据

1
2
3
4
5
6
7
8
9
#!/bin/bash
month=$(date +%Y%m)
data_exist=$(hdfs dfs -ls hdfs://Cluster1/user/hive/warehouse/sannaha.db/tbname/dt=${month})
if [[ $(echo ${data_exist} | grep "000000_0") != "" ]]; then
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : "sannaha.tbname${month}分区数据已具备"
else
echo -e $(date "+%Y-%m-%d %H:%M:%S") [SHELL_LOG] : "sannaha.tbname${month}分区数据不具备"
exit 1
fi

SparkSQL跑批

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
###############################################################
# Script name: dw_train_ds.sh
# Execute eg: sh dw_train_ds.sh 20210101
###############################################################
#!/bin/bash
. ~/.bashrc

# 传入日期(yyyyMMdd)
d_date=${1}

# 参数为空,使用前1天的日期
if [ "X${d_date}" == "X" ]; then
d_date=`date -d "-1 day" +"%Y%m%d"`
fi

#传入日期所在月份(格式:yyyyMM)
m_date=`date -d "${d_date}" +"%Y%m"`
#当月月初日期(格式:yyyyMM01)
df_date=`date -d "${d_date}" +"%Y%m01"`
#前1天的日期(格式:yyyyMMdd)
d_p01d_date=`date -d "${d_date} -1 days" +%"Y%m%d"`
#前1月的月份(格式:yyyyMM)
m_p01m_date=`date -d "${df_date} -1 month" +%"Y%m"`
#后1天的日期(格式:yyyyMMdd)
d_n01d_date=`date -d "${d_date} 1 days" +%"Y%m%d"`

echo "日期:"$d_date
echo "月份:"$m_date
echo "当月月初日期:"$df_date
echo "前1天日期:"$d_p01d_date
echo "前1月月份:"$m_p01m_date
echo "后1天日期:"$d_n01d_date

kerberos_conf=" --principal user@SANNAHA --keytab /home/sannaha/user.keytab "

conf="--conf spark.files.ignoreMissingFiles=true
--conf spark.sql.files.ignoreMissingFiles=true
--conf spark.files.ignoreCorruptFiles=true
--conf spark.sql.files.ignoreCorruptFiles=true
--conf spark.sql.shuffle.partitions=200
--conf spark.default.parallelism=600
--conf spark.driver.maxResultSize=10g
--conf spark.yarn.executor.memoryOverhead=1024
--conf spark.executor.extraJavaOptions=-Xss4m
--conf spark.driver.extraJavaOptions=-Xss4m"

task_name="dw_train_ds_${d_date}"
driver_memory=8g
num_executors=200
executor_memory=3g
executor_cores=1
master=yarn

shell="/usr/bch/1.5.0/spark/bin/spark-shell \
--conf spark.ui.filters= \
--deploy-mode client \
--conf spark.yarn.stagingDir=hdfs://ns/user \
--conf spark.hadoop.dfs.replication=2 \
--queue root.bdoc.qwrk \
--name ${task_name} \
--master yarn \
--executor-memory ${executor_memory} \
--executor-cores ${executor_cores} \
--num-executors ${num_executors} \
--driver-memory ${driver_memory} \
--jars /home/sannaha/libs/postgresql-42.2.14.jar \
${conf} \
${kerberos_conf} "

echo $shell
echo "*************************** Complete time : `date` ***************************"

eval $shell <<!EOF
import org.apache.spark.sql._
import org.apache.spark.sql.types._
import org.apache.spark.sql.SaveMode
spark.sqlContext.setConf("spark.sql.shuffle.partitions","1200")

val d_date="${d_date}"
val m_date="${m_date}"
//基站级网络用户
spark.read.parquet("hdfs://ns/user/people/ods/ods_everybase/day="+d_date).registerTempTable("temp_ods_everybase")
//特殊区域
spark.read.parquet("hdfs://ns/user/people/dim/dim_channel_cell_level_6").filter("channel_subtype = '火车站'").registerTempTable("dim_channel_cell_level_6")

//当天在火车站出现过的人的记录
sql("""select a.imsi,b.channel_id,a.enter_time,a.leave_time,a.sec
from temp_ods_everybase a
inner join dim_channel_cell_level_6 b
on a.lac_id = b.lac_id and a.cell_id = b.cell_id
""").registerTempTable("temp_ods_everybase_train")


//得出时间排序下的 每个用户的 在所有车站记录序号和在每个车站的记录序号 再相减
sql(""" select imsi,channel_id,enter_time,leave_time,sec
,row_number() over(partition by imsi order by enter_time)-row_number() over(partition by imsi,channel_id order by enter_time) group_flag
from temp_ods_everybase_train
""").registerTempTable("temp_train_group")

//聚合-链表a-b-c-a会有4条记录
sql("""
select imsi,channel_id,min(enter_time) enter_time,max(leave_time) leave_time,sum(sec) sec_sum
from temp_train_group
group by imsi,channel_id,group_flag
""").write.mode(SaveMode.Overwrite).parquet("hdfs://ns/user/people/dw/dw_nationwide_train_ds/month="+sm_date+"/day="+sd_date)

spark.stop()
!EOF

echo "*************************** Complete time : `date` ***************************"
exit 0

存储过程跑批

data_load.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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
###########################################################
# 脚本功能:1.检查数据BAD、LOG目录
# 2.调用加载数据存储过程
# 3.检查是否存在REJECT文件并写入日志
#
# 调用方式:sh data_load.sh 目标表名称 跑批日期 加载数据文件名称 加载OK文件名称
# sh data_load.sh TEST 20171231 ODS_RPM_TEST_INT_20171231.DAT ODS_RPM_TEST_INT_20171231.OK
#
###########################################################
#!/bin/bash

#判断参数输入
if [ $# != 3 ]; then
echo "参数输入有误,请重新输入!"
echo "参数1:当前加载表的物理表名(ODS库中的物理表名)"
echo "参数2:当期跑批日期,格式:YYYYMMDD"
echo "参数3:完整的OK文件名称:***.OK"
exit -1
fi

#获取跑批参数
tabnm=$1
dataDate=$2
fileok=$3
dbuser=ods
dbpwd=ods
dbip=172.xx.xx.xx
dbport=1521
dbsid=rpmdb
logPath="/u01/db/oradata/RPM_LOAD/RPM_SHELL/log/${dataDate}"
dataBad="/u01/db/oradata/RPM_LOAD/RPM_DATA/BAD_DIR/${dataDate}"
dataLog="/u01/db/oradata/RPM_LOAD/RPM_DATA/LOG_DIR/${dataDate}"
dataDat="/u01/db/oradata/RPM_LOAD/RPM_DATA/DAT_DIR/${dataDate}"

echo "${tabnm}表数据开始加载:`date`"

# 获取OK文件中的数据条数
#ROWNUM_OK=$(grep -R ${tabnm} ${dataDat}/${fileok} |awk -F ":" '{print $2}')

# 调用数据库存储过程
sqlplus ${dbuser}/${dbpwd}@${dbip}:${dbport}/${dbsid} >> ${logPath}/exec_proc.log <<EOF
SET SERVEROUTPUT ON;
SET VERIFY OFF;
WHENEVER SQLERROR EXIT FAILURE ROLLBACK;
WHENEVER OSERROR EXIT FAILURE ROLLBACK;

exec ODS.PROC_${tabnm}(to_date(${dataDate},'YYYYMMDD'));

exit;

EOF

echo "存储过程调用完成!"

# 检查BAD文件
if [ -f ${dataBad}/${tabnm}.BAD ]; then
sqlplus ${dbuser}/${dbpwd}@${dbip}:${dbport}/${dbsid} > /dev/null << EOF
delete from run_dataload_log where as_of_date=to_date('${dataDate}','YYYYMMDD') and tab_name='${tabnm}' and run_name='$0';
commit;
insert into run_dataload_log values (to_date('${dataDate}','YYYYMMDD'),'${tabnm}','$0','failed','PROC_${tabnm}程序执行结束,存在REJECT文件,请检查BAD日志文件${dataBad}/${tabnm}.BAD',sysdate);
commit;
exit ;
EOF
else
sqlplus ${dbuser}/${dbpwd}@${dbip}:${dbport}/${dbsid} > /dev/null << EOF
delete from run_dataload_log where as_of_date=to_date('${dataDate}','YYYYMMDD') and tab_name='${tabnm}' and run_name='$0';
commit;
insert into run_dataload_log values (to_date('${dataDate}','YYYYMMDD'),'${tabnm}','$0','success','PROC_${tabnm}程序执行结束,无BAD文件',sysdate);
commit;
exit ;
EOF
fi

echo "data.sh执行完成!"

ods_load.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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
###########################################################
# 脚本功能:1.检查跑批日期
# 2.检查(.dat、.ok)文件是否就绪
# 3.修改外表表结构
# 4.调用加载数据脚本(data_load.sh)
# 5.判断数据是否加载成功并写入日志
# 6.备份数据文件
#
# 调用方式:sh ods_load.sh 跑批日期
# sh ods_load.sh 20171231
###########################################################
#!/bin/ksh

# 检查日期参数
# $#:添加到Shell的参数个数
# -ne:不等于
if [ $# -ne 1 ]
then
echo "没有输入日期参数或输入参数不正确!"
echo "参数1:当期跑批日期,格式:YYYYMMDD"
exit 1
fi

# 设置参数变量
dbuser=ods
dbpwd=ods
dbip=172.xx.xx.xx
dbport=1521
dbsid=rpmdb
dataDate=$1
dataPath="/u01/db/oradata/RPM_LOAD/RPM_DATA/DAT_DIR/${dataDate}"
dataLog="/u01/db/oradata/RPM_LOAD/RPM_DATA/LOG_DIR/${dataDate}"
dataBad="/u01/db/oradata/RPM_LOAD/RPM_DATA/BAD_DIR/${dataDate}"
shellPath="/u01/db/oradata/RPM_LOAD/RPM_SHELL"
logPath="/u01/db/oradata/RPM_LOAD/RPM_SHELL/log/${dataDate}"
hisdat="/u01/db/oradata/RPM_LOAD/RPM_DATA/HISDATA"
thread=5 #number of export tables one job
echo "dataPath:"${dataPath}

#检查数据LOG文件目录是否存在
# -d:为目录,返回真
if [ ! -d ${dataLog} ];
then
mkdir ${dataLog}
else
rm -rf ${dataLog}/*.LOG
fi

#检查数据BAD文件目录是否存在
if [ ! -d ${dataBad} ];
then
mkdir ${dataBad}
else
rm -rf ${dataBad}/*.BAD
fi

#检查程序日志目录是否存在
if [ ! -d ${logPath} ];
then
mkdir ${logPath}
else
rm -rf ${logPath}/*.log
fi

# 显示当前脚本运行的ID号,创建文件通讯通道
# $$:Shell本身的PID
# mkfifo:创建命名管道,用于不同不相关进程间的通信
# exec 6:指定 6 为文件描述符;默认的文件描述符有标准输入 0,标准输出 1,标准错误输出 2
tmp_fifofile="/tmp/$$.fifo"
echo "$tmp_fifofile"
mkfifo $tmp_fifofile
exec 6<>$tmp_fifofile
rm $tmp_fifofile

i=0
while(($i<=$thread))do
echo
let i=i+1
done >&6

# 判断OK文件是否完成
# wc -l:统计行数,结果显示为:行数 文件名。需要用awk将空格及后面的文件名去除掉,只保留行数
# awk:文本分析工具,读入有'\n'换行符分割的一条记录,将记录按指定的域分隔符划分域,$0则表示所有域,$1表示第一个域,$n表示第n个域
# awk -F ':':设置域的分隔符为冒号,默认为空格
while [ 1 = 1 ]
do
cd ${shellPath}
echo "正在读取加载文件名称..."
#
RES_LINE=`wc -l tablename | awk '{print $1}'`
echo "${RES_LINE} 个文件需要加载, 请等待..."
while read line1
do

filenm=`echo ${line1} | awk -F ':' '{print $2}' | sed "s/YYYYMMDD/${dataDate}/g"`
fileok=`echo ${line1} | awk -F ':' '{print $3}' | sed "s/YYYYMMDD/${dataDate}/g"`

echo ${dataPath}/${fileok}
if [ ! -f ${dataPath}/${fileok} ];
then
echo "文件缺失在或没有找到:${fileok}"
else
let RES_LINE=RES_LINE-1
fi
sleep 1
done < ${shellPath}/tablename
if [ ${RES_LINE} -ne 0 ];
then
echo "没有获取到OK文件!"
sleep 1
echo "重新检索OK文件!"
RES_LINE=`wc -l tablename | awk '{print $1}'`
else
echo "所有的OK文件已经就绪,准备加载数据..."
break
fi
done

echo "检查文件完成,进行下一项"

# 生成当期数据修改数据库外表的SQL脚本
# sed:在线编辑器,一次处理一行内容,用来自动编辑一个或多个文件
# sed 's/要被替换的字串/新的字串/g'

sed "s/YYYYMMDD/${dataDate}/g" alter_db.sql > alter_db_${dataDate}_temp.sql

echo "替换日期完成,进行下一项"

# 修改外部表的表结构以及DIRECTORY
sqlplus ${dbuser}/${dbpwd}@${dbip}:${dbport}/${dbsid} < ${shellPath}/alter_db_${dataDate}_temp.sql >> ${logPath}/alter_db.log 2>&1

echo "修改外部表结构完成,进行下一项"

rm ${shellPath}/alter_db_${dataDate}_temp.sql

# 调用data_load.sh
while read line2
do
tabnm=`echo ${line2} | awk -F':' '{print $1}'`

# read -u6
# $?:最后运行的命令的结束代码(返回值)

sh ${shellPath}/data_load.sh ${tabnm} ${dataDate} ${fileok} >> ${logPath}/dateload_sh.log

run_rs=$?

# 判断执行结果:正常,非0失败
if [ ${run_rs} = 0 ];
then
echo "${tabnm}${dataDate}期数据加载完成!"
sqlplus ${dbuser}/${dbpwd}@${dbip}:${dbport}/${dbsid} > /dev/null << EOF
delete from run_dataload_log where as_of_date=to_date('${dataDate}','YYYYMMDD') and tab_name='${tabnm}' and run_name='$0';
commit;
insert into run_dataload_log values (to_date('${dataDate}','YYYYMMDD'),'${tabnm}','$0','success','data_load.sh脚本执行成功,数据加载完成',sysdate);
commit;
exit ;
EOF
else
echo "${tabnm}${dataDate}期数据加载失败,data.sh程序报错,请检查!"
sqlplus ${dbuser}/${dbpwd}@${dbip}:${dbport}/${dbsid} > /dev/null << EOF
delete from run_dataload_log where as_of_date=to_date('${dataDate}','YYYYMMDD') and tab_name='${tabnm}' and run_name='$0';
commit;
insert into run_dataload_log values (to_date('${dataDate}','YYYYMMDD'),'${tabnm}','$0','failed','data_load.sh脚本执行失败,请检查日志:${logPath}/date_sh.log',sysdate);
commit;
exit ;
EOF
fi

echo "调用data_load.sh程序完成!"

done < ${shellPath}/tablename

# 备份数据文件
# 创建新的包
echo "所有数据加载完成,开始数据文件备份..."

tar -cf - ${dataPath} | gzip > ${hisdat}/${dataDate}.tar.gz
#rm -rf ${dataPath}/*.DAT
#rm -rf ${dataPath}/*.OK

echo "程序结束!"
wait
exec 6>&-
exit 0

参考资料

Linux 变量的使用
理解 inode