0%

Linux脚本快速上手

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


语法

变量

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

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

# 用语句给变量 filelist 赋值
for filelist in $(ls /opt/bigdata/); do echo $filelist; done
# 输出结果
hadoop
kafka
mysql
spark

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

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

使用变量:

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 进程中都有效。每个 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
#!/bin/bash
# 定义函数
function func() {
a='sannaha'
}
# 调用函数
func
# 输出函数内部的变量
echo $a

输出结果:

1
2
$ ./varTest.sh
sannaha

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

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

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

环境变量

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建全局变量 g
$ g=global
# 创建环境变量 e
$ export e=environment
# 创建子进程
$ bash
# 子进程中无法使用父进程的全局变量
$ echo $g

# 子进程中可以使用父进程的环境变量
$ echo $e
environment
$ export se=subshellEnvironment
# 退出子进程,回到父进程
$ exit
exit
# 父进程中无法查看子进程中创建的环境变量
$ echo $se

变量内容替换

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

示例:

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
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) 的返回值。

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

<:小于

>:大于

<=:小于或等于

>=:大于或等于

==:等于

!=:不等于

[[email protected] bigdata]# (( a < b ))
[[email protected] bigdata]# echo $?
0
[[email protected] bigdata]# echo $((a<b))
1
[[email protected] 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

函数

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号
[email protected] 与$*相同,但是使用时加引号,并在引号中返回每个参数。
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

条件判断

if语句

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

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

Shell 会按顺序执行 if 语句:首先判断 EXPRESSION 的执行结果,如果执行后返回状态 0(即为真),则会执行该条件对应的命令,否则会跳过这些命令;如果有 elif 会再次进行判断,只有对应第一个返回状态 0 的条件的命令会被执行;如果所有的 COMMAND 返回状态都不为 0,则执行 else 中命令。

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
字符串不以英文字母开头

整数测试

用于比较整数之间的数值大小:

  • [ 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 以及是否为空,比较两个字符串之间的字典顺序:

  • [ -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
$ if [ -z "" ]; 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 ]:是否存在套接字文件。

文件权限测试

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

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

文件非空测试

  • [ -s 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
15
16
17
18
19
# 格式1
for 变量 in 值1 值2...; do
command
done

# 格式2
for 变量 in 列表; do
command
done

# 格式3
for 变量 in 文件; do
command
done

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

示例:

  1. 编写脚本 forinTest.sh
forinTest.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
#!/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 path in $; do
echo $path
done

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
$ ./forinTest.sh 
1
2
3
bin
etc
games
include
lib
lib64
libexec
local
sbin
share
src
tmp

for 表达式

格式:

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

while循环

until循环

工具命令

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
# 显示系统当前日期和时间
$ date
2019年 05月 06日 星期一 20:05:48 CST

# 设置系统日期和时间
$ date -s "2019-05-06 20:08:09"

# 显示昨天的日期和时间,支持day/week/month/year
$ date -d '-1 day'
2020年 10月 12日 星期一 00:46:49 CST

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

# 输出1天后的日期
$ date -d '+1 day' +%F
2020-10-14

# 输出300秒后的日期和时间
$ date -d "300 second" +"%Y-%m-%d %H:%M:%S"
2020-10-13 00:53:17

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

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

$ cat result.txt
aa,bb,cc

wc

wc 命令用于计算字数。利用wc指令我们可以计算文件的Byte数、字数、或是列数,若不指定文件名称、或是所给予的文件名为”-“,则wc指令会从标准输入设备读取数据。

语法:

1
2
3
4
5
6
7
wc [-clw][--help][--version][文件...]

-c或--bytes或--chars 只显示Bytes数。
-l或--lines 显示行数。
-w或--words 只显示字数。
--help 在线帮助。
--version 显示版本信息。

示例:

1
2
3
4
5
6
7
$ cat wc.txt
Hi sannaha!
This is my blog.

# 使用 wc 统计,wc.txt 文件的行数为 2,单词数 6,字节数 29
$ wc wc.txt
2 6 29 wc.txt

ftp

示例

获取目录文件列表

方法一:

1
2
3
4
5
6
ftp -in 192.168.153.121 << EOT
user ftpadmin ftpadmin
cd mydir
nlist . mydirlist.txt
bye
EOT

方法二:

1
2
3
4
5
6
ftp -in 192.168.153.121 << EOT > mydirlist.txt
user ftpadmin ftpadmin
cd mydir
ls r*
bye
EOT

lftp

lftp 是一个功能强大的文件传输工具,支持 ftp 协议。这里只介绍脚本中的用法,完整的安装与使用见 *Linux常用工具的安装与配置 *

下载文件

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 [email protected]:/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

lftp脚本示例

1
2
# -p指定端口,-u指定用户和密码 -e执行后面的命令
lftp -p 21 -u username,password ftp://ipaddress -e "cd data; put top.sql -o top.sql.tmp; mv top.sql.tmp top.sql; exit;" >> /dev/null

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 [email protected]
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
}

实例

通过互联网收集到的部分 Shell 脚本。

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 [email protected] --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

参考资料

* 理解 inode*