简介
- shell是解释性语言,解释器如
bash
、sh
- Shell教程
基本语法
基本概念
- 程序有两类返回值:执行结果、执行状态(即
$?
的值,0
表示正确,1-255
错误)
特殊符号
- 注释:
#
单行注释,<<COMMENT xxx COMMENT
多行注释 - linux 引号
- 反引号:`cmd` 命令替换,类似
$(cmd)
- 双引号: “” 变量替换
- 单引号:‘’ 字符串
- 反引号:`cmd` 命令替换,类似
- 命令替换:使用 `cmd`(反引号)包裹或 $(cmd)(美元括号)
wall
date所有人都收到当前时间
wall date
所有人都收到date这个字符串
管道符
|
- 将一个命令的输出传送给另外一个命令,作为另外一个命令的输入。如:
命令1|命令2|...|命令n
- 使用管道符连接的左右两边的命令都是运行在子shell中,存在变量无法传递的问题($变量无法传递,但是$$可以传递);此时可通过export导出变量,通过export导出的变量在创建子进程的时候相当于快照一份给子进程,即父子进行修改export变量不会相互影响
copy on write
写时复制,fork()创建子进程时即通过此方式。当创建子进程时,不复制变量(此时是不同的指针指向相同物理内存,这样创建子进程速度会很快);当出现变量发生修改时,再复制出一个新的物理内存- 当运行脚本时也相当于创建了一个子进程(脚本文件头为
#!/bin/bash
)
示例(管道符和父子进程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53# 显示文件第2行
head -2 fd-file | tail -1 # 管道为把左边的输出给右边作为输入
ls -l * | grep "^_" | wc -l # 统计当前目录有多少个文件
# 父子进程
x=100
/bin/bash # 创建并进入子进程
echo $x # 此时无法获取父进程的变量,可以使用export x导出到环境变量(导出的环境变量,子进程第一次可以读取到,之后修改并不会修改父进程的此变量)
exit # 退出子进程
# 管道符的父子进程
x=100
{ x=101; echo $x; } # 打印101. 花括号为代码块,可执行多条指令
echo $x # 打印101,此时x被重新赋值
{ x=102; echo $x; } | cat # 打印102. 由于管道进行连接,因此左边是一个子进程,x是在子进程中进行操作的,并没有修改到父进程的x
echo $x # 打印101
# 在管道中获取进程id
echo $$ # 9287
echo $$ | cat # 9287. 由于$$的优先级高于|,因此在执行子进程之前就已经把父进程的pid替换了$$,然后在在子进程中执行
echo $BASHPID | cat # 10101. 此时获取的是子进程的pid
```
- 重定向:将命令的结果重定向到某个地方
- `>`、`>>` 输出重定向(`>`覆盖,`>>`追加)
- `ls > ls.txt` 将ls的输出保存到ls.txt中
- `>` 如果文件不存在,就创建文件;如果文件存在,就将其清空。`>>` 如果文件不存在,就创建文件;如果文件存在,则将新的内容追加到那个文件的末尾
- `2>` 错误重定向,如:`lsss 2> ls.txt`
- `&>` 全部重定向:`ls &> /dev/null` 将结果全部传给数据黑洞
- `<`、`<<` 输入重定向
- `wall < notice.txt` 将notice.txt中的数据发送给所有登录人
- 标准输入(stdin)代码为 `0`,实际映射关系:/dev/stdin -> /proc/self/fd/0
- 标准输出(stdout)代码为 `1`,实际映射关系:/dev/stdout -> /proc/self/fd/1
- `echo xxx` 将输出放到标准输出中
- 标准错误输出(stderr)代码为 2 ,实际映射关系: /dev/stderr ->/pro/self/fd/2
- 转义符`\`,或者使用引号
```bash
# 一般特殊符号要出现必须用转义字符:' " * ? \ ~ ` ! # $ & | { } ; < > ^
## 对于特殊字符可使用转义符`\`,或者使用引号
echo 9 * 9 = 81 # 报错
echo 9 '*' 9 = 81 # 9 * 9 = 81
echo '9 * 9 = 81' # 9 * 9 = 81
echo "9 * 9 = 81" # 9 * 9 = 81
echo 9 \* 9 = 81 # 9 * 9 = 81
## 在一对引号中不允许出现单引号,转义字符也不行
# 以下因为第一个引号和第二个引号自动配成一对,最后一个单引号在没得配的情况下,bash认为输入尚未完成,出现>等待命令继续输入
echo 'it is wolf's book' # 进入待输入 > ^C '
echo 'it is wolf\'s book' # 进入待输入 > ^C '
# 解决如下
echo "it is wolf's book"
echo it is wolf\'s book
echo 'it is wolf'\''s book'
- 将一个命令的输出传送给另外一个命令,作为另外一个命令的输入。如:
变量
变量类型
- 环境变量(作用于可跨bash):
export <var_name>=<var_value>
- 本地变量(作用于当前bash):
<var_name>=<var_value>
(注意:=前后不要有空格) - 局部变量(作用于当前代码段):
local <var_name>=<var_value>
- 位置变量(作用于脚本执行的参数):
$1
表示第一个参数,以次类推$2
、$3
特殊变量
$?
上一个命令的执行状态返回值(0
表示正确,其他为错误)$#
传递到脚本的参数个数$@
使用时加引号,并在引号中返回所有参数(用空格分割)$*
传递到脚本的参数,与位置变量不同,此选项参数可超过9个$$
脚本运行时当前进程的ID号,常用作临时变量的后缀,如 haison.$$$!
后台运行的(&)最后一个进程的ID号$-
上一个命令的最后一个参数$0
当前Shell程序的文件名(只在脚本文件里才有作用)1
2
3
4
5
6
7# 返回这个脚本文件放置的目录,这个命令写在脚本文件里才有作用。如`dirname /usr/local/bin` 结果为`/usr/local`
dirname $0
# 进入当前Shell程序的目录
cd `dirname $0`
# 定义当前脚本目录,并执行jar。cd -P表示基于物理路径
APP_HOME="$(cd -P "$(dirname "$0")" && pwd)"/..
(cd "$APP_HOME" && java -jar app.jar)
- 环境变量(作用于可跨bash):
set
查看shell中变量printenv
/env
查看shell中环境变量unset <var_name>
撤销变量- 引用变量
${var_name}
,一般可以省略{}
字符串
1 | ## 字符串替换 |
数组
- Bash Shell 只支持一维数组(不支持多维数组),初始化时不需要定义数组大小,数组元素的下标由 0 开始,元素用”空格”符号分割开
1 | # 初始化 |
- 案例
1 | my_array=(A B "C" D) |
运算
1 | # 使用 $(( )) |
函数
- 函数定义、调用、返回
1 | ## test.sh |
- 函数结合
xargs
(参考下文)
1 | ## test.sh |
控制语句
- 控制语句也可在命令行中需要使用
1 | # if |
条件判断
- 参考
- 条件表达式
[ expression ]
注意其中的空格[[
,是关键字;[
是一条命令,与test
等价,大多数shell都支持;推荐使用[[]]
[[ ]]
内是不能使用 -a 或者 -o 进行比较,[ ]
内可以- 使用
[]
和[[]]
的时候不要吝啬空格,每一项两边都要有空格,[[ 1 == 2 ]]
的结果为“假”,但[[ 1==2 ]]
的结果为“真”!
- 条件表达式的逻辑关系
- 在linux中命令执行状态:0 为真,其他为假
&&
(第一个表达式为true才会运行第二个)、||
、!
-a
逻辑与,如:[ $# -gt 1 –a $# -lt 3 –o $# -eq 2 ]
-o
或
- 整数比较
-eq
、==
相等,比如:[ $A -eq $B ]
-ne
、!=
不等-gt
、>
大于-lt
、<
小于-ge
大于等于-le
小于等于
- 文件测试(需要中括号)
-e <file>
测试文件是否存在-f <file>
测试文件是否为普通文件-d <file>
测试文件(linux是基于文件进行编程的)是否为目录-r
权限判断-w
-x
- 字符串测试
==
或=
等号两端需要空格- 如:
[[ $res == *"yes"* ]]
(通配符判断是否包含)
- 如:
=~
正则比较- 如:
[[ /bin/bash =~ sh$ ]]
、[[ "$var" =~ $reg ]]
(reg='^hello'
其中$reg不能加双引号)
- 如:
!=
>
、<
字符串大小比较,字符串有空格则不能使用-gt
- 如:
[[ "2005 03.01" > "2004 05.23.00" ]] && echo gt || echo lt
- 如:
-z
判断变量的值是否为空。为空,返回0,即true-n
判断变量的值是否不为空。非空,返回0,即true-s <string>
判非空[[ $str != h* ]]
判断字符串是否不是以h开头[[ "$str" =~ ^he.* ]]
判断字符串是否以he开头[ "$item" \< /home/smalle/demo/$lastMon ]
判断字符串小于(需要转义)
- 常用判断
[[ $JAVA_HOME ]]
判断是否存在此变量/环境变量[[ -z $JAVA_HOME ]]
判断此变量是否为空
- 算术运算(其中任意一种即可)
let C=$A+$B
(=、+附近不能有空格,下同。此时C不能有$,使用变量的使用才需要加$)C=$[$A+$B]
C=$(($A+$B))
- C=`expr $A + $B` (表达式中各操作数及运算符之间要有空格,而且要使用命令引用)
- 控制结构
1 | # 控制结构 |
循环
- 控制结构
1 | # for |
- 如何生成列表
{1..100}
seq [起始数] [跨度数] 结束数
如:seq 10
、seq 1 2 10
ls /etc 文件列表
case语句
- 控制结构
1 | case 变量 in |
脚本说明
脚本基本使用
- 注意文件格式必须是Unix格式(否则执行报错:
: No such file or directory
)- 解决办法:
vim my.sh
-:set ff=unix
-:x
- 解决办法:
#!/bin/bash
脚本第一行建议以此开头exit
退出脚本- 退出脚本可以指定脚本执行的状态:
exit 0
成功退出,exit 1
/exit 2
/… 失败退出 - 退出码
0
成功2
shell内建命令使用错误124
执行命令超时,如timeout 10 sleep 30
126
程序或命令的权限是不可执行的127
命令不存在command not found(估计是$PATH不对)128
exit的参数错误(exit只能以整数作为参数,范围是0-255)128+n
信号n的致命错误(kill -9 $PPID,$? 返回137=128 + 9)130
用Control-C来结束脚本255*
超出范围的退出状态(exit -1)
- 退出脚本可以指定脚本执行的状态:
- 脚本中使用
set -x
是开启代码执行显示,set +x
是关闭,set -o
是查看(xtrace)。执行set -x
后,对整个脚本有效 - 执行脚本方式
1 | ## 执行命令 |
- 脚本中使用nohup命令
1 | !#/bin/bash |
- 远程执行脚本 ^4
- 简单执行远程命令:
ssh user@remoteNode "cd /home ; ls"
双引号必须有,两个命令直接用分号分割 - 脚本内执行远程命令
- 简单执行远程命令:
1 |
|
简单示例
- 添加用户:
./test1.sh user1
1 | !/bin/bash |
- 获取某目录下最大的文件:
./test2.sh /home/smalle
1 | !/bin/bash |
接受参数
POSIX 和 GUN 规范
- POSIX(可移植操作系统接口)
- 以一个横杠开头的为选项,选项名是单字符的英文字母或者数字
- 如果不带参数的话,多个选项可以写在一个横杠后面,如
-abc
与-a -b -c
的含义相同 - 如果带参数的话,选项和它的参数既可以分开写也可以在一起,grep选项中的
-A 10
与-A10
都是合乎规范的- 如果选项接受的参数有多个值,那么程序应该将参数作为一个字符串接收进来,字符串中的这些值用逗号或空白符分隔开
- 选项参数写在非选项参数之前
- 特殊参数
--
指明所有参数都结束了。命令行中后面的任何参数都被认为是操作数,即使它们以-
开始 - 同一参数可以重复出现,一般程序应该这么却解析:当一个选项覆盖其他选项的设置时,那么最后一个选项起作用。如果带参数的选项出现重复,那么程序应该按顺序处理这些选项参数。例如
myprog -u arnold -u jane
和myprog -u "arnold,jane"
应该被解释为相同
- GNU(自由的操作系统)
- GNU鼓励使用
--help
、--verbose
等形式的长选项。这些选项不仅不与POSIX约定冲突,而且容易记忆 - 选项参数与长选项之间或通过空白字符或通过一个
=
来分隔
- GNU鼓励使用
常见参数如
1 | # [-?hvVtTq] 表示接受 -? -h -v等参数 |
顺序参数
如
./test.sh 1 2
执行下面脚本1
2
3
4
5
6
7
# test.sh
echo "脚本$0" # test.sh
echo "第一个参数$1" # 1
echo "第二个参数$2" # 2
# 超过10个的参数需要使用${10}, ${11}来接收示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20start() {
echo 'start...'
}
stop() {
echo 'stop...'
}
case "$1" in
'start')
start
;;
'stop')
stop
;;
*)
echo "[info] Usage: $0 {start|stop}"
exit 1
esac
exit $?
getopt 与 getopts
- getopts 接收命令行选项和参数。语法:
getopts OptionString Name [ Argument ...]
- OptionString 选项名称,Name选项值变量
- 一个字符是一个选项,如个某字符
:
表示选项后面有传值。当getopts命令发现冒号后,会从命令行该选项后读取该值。如该值存在,将保存在特殊的变量OPTARG中 - 每次调用 getopts 命令时,它将下一个选项的值放置在 Name 内,并将下一个要处理的参数的索引置于 shell 变量 OPTIND 中。每当调用 shell 时,都会将 OPTIND 初始化为 1
- 当OptionString用
:
开头,getopts会区分invalid option错误(Name值会被设成?
)和miss option argument错误(Name会被设成:
);否则出现错误,Name都会被设成?
- getopts示例(b.sh) ^3
1 |
|
- getopts示例结果
1 | <!-- bash b.sh -a 1 -b 2 -c 3 test -oo xx -test --> |
- getopt示例
1 |
|
- getopt示例结果
1 | # ./run.sh |
多行输入
1 | # EOF之间的数据覆盖/home/smalle/test文件;如果需要追加则为 `cat >> /home/smalle/test << EOF...EOF` |
functions模块
- 位于
/etc/rc.d/init.d/functions
文件中 - 在shell脚本中引用只需加入
. /etc/rc.d/init.d/functions
即可
方法介绍
killproc
杀死进程
Tips
零散
- 如果脚本中有
vi
等操作,当用户保存该文件后会继续执行脚本 直接执行github等网站脚本
1
2
3
4# 法1(需要是raw类型的连接)。tee 实时重定向日志(同时也会在控制台打印,并且可进行交互)
bash <(curl -L https://raw.githubusercontent.com/sprov065/v2-ui/master/install.sh) 2>&1 | tee my.log # 此处 bash 也可改成 source
# 法2(需要是raw类型的连接)
wget --no-check-certificate https://github.com/sprov065/blog/raw/master/bbr.sh && bash bbr.sh 2>&1 | tee my.log命令执行失败后,是否执行后续命令
1
2command || true # 此command执行失败后继续执行后续命令
command || exit 0 # 此command执行失败后不执行后续命令cp
命令强制覆盖不提示\cp test test.bak
为shell命令设置超时时间
1
2timeout 10 ./test.sh # 设置执行脚本超时时间为10s
echo $? # 如果超时则返回 124脚本中执行nohup命令不生效(主要是找不到环境变量)
1
2source /etc/profile
nohup echo "hello world"
json处理
- 使用内置的 awk/sed 来获取指定的 JSON 键值,缺点需要根据实际情况写对于的正则表达式 ^7
- 使用
jq
软件获取yum install jq
安装jqjq .subjects[0].genres[0] douban.json
curl -s https://douban.uieee.com/v2/movie/top250?count=1 | jq .subjects[0].genres[0]
调用其他脚本解释器(python/php/js),推荐
1
2
3
4
5
6
7
8## python2(服务器一般会安装)
export PYTHONIOENCODING=utf8 && curl -s 'https://douban.uieee.com/v2/movie/top250?count=1' | python -c "import sys, json; print json.load(sys.stdin)['subjects'][0]['genres'][0]"
echo '{"instance": "smalle'\''aezo"}' | python -c "import sys, json; print json.load(sys.stdin)['instance']"
## python3
curl -s 'https://douban.uieee.com/v2/movie/top250?count=1' | \
python3 -c "import sys, json; print(json.load(sys.stdin)['subjects'][0]['genres'][0])"
expect
- expect 工具是一个根据脚本与其他交互式程序进行交互
- 相关命令
- spawn: 启动进程,并跟踪后续交互信息
- expect: expect的一个内部命令,判断上次输出结果里是否包含指定的字符串,如果有则立即返回,否则就等待超时时间后返回(无法控制不执行后面的语句)。只能捕捉由spawn启动的进程的输出expect
- send: 向进程发送字符串,用于模拟用户的输入,该命令不能自动回车换行,一般要加
\r
(回车) - interact: 执行完成后保存交互状态,把控制权交给控制台
- 单位是:秒;timeout -1 为永不超时;默认情况下,timeout是10秒
- set timeout 30: 设置超时时间为30秒(默认的超时时间是 10 秒,通过 set 命令可以设置会话超时时间, 若不限制超时时间则应设置为-1)
- exp_continue: 允许expect继续向下执行指令meout:指定超时时间,过期则继续执行后续指令
- send_user: 回显命令,相当于echo
- $argv 参数数组: Expect脚本可以接受从bash传递的参数,可以使用 [lindex $argv n] 获得,n从0开始,分别表示第一个$1,第二个$2,第三个$3……参数 ($argvn没有空格则表示脚本名称;$argv n有空格则代表下标)
- 一般流程: spawn 启动追踪 —> expect 匹配捕捉关键字 ——> 捕捉到将触发send代替人为输入指令(一般是一个spawn登录命令行,之后全部为send模拟执行命令) —> interact / expect eof
- Expect脚本必须以interact或expect eof 结束,执行自动化任务通常expect eof就够了
expect eof
是在等待结束标志。由spawn启动的命令在结束时会产生一个eof标记,expect eof 即在等待这个标记
- Expect脚本必须以interact或expect eof 结束,执行自动化任务通常expect eof就够了
- Expect语法
1 | # 单一分支语法("password: "后面包含空格也可以匹配到) |
1 | #!/usr/bin/expect |
- 直接执行案例:登录(login.exp)
./login.exp 22 root 192.168.1.100 mypass
即可自动登录服务器
1 | #!/usr/bin/expect |
- 嵌入执行案例:登录(login.sh)
./login.sh 192.168.1.1 root
1 |
|
- FTP登录案例
./ftp.sh 192.168.1.1
本机要开启ftp,对方也要开启
1 |
|
- 其他案例参考
示例
jar包运行/停止示例
- OFBiz自动启动脚本参考ofbiz进阶.md#自定义启动脚本
- 自启动脚本可参考
/etc/init.d
目录下的文件如network
,假设下列脚本文件名为my_script
^1 - 将脚本加入到开机启动
chkconfig --add my_script
- 以非root用户启动java程序
useradd app
创建用户- JDK不能安装在/root目录(此目录其他用户无执行权限),可安装在
/opt
等目录 chown app:app -R /www/app/
设置目录所属权限su - app
切换到用户进行启动,或者通过root执行runuser -l app -c "nohup bash /www/app/start.sh > /dev/null 2>&1 &"
进行启动
1 | !/bin/bash |
备份Mysql
脚本具体参考:http://blog.aezo.cn/2016/10/12/db/mysql-dba/)
备份Oracle
- 配置定时任务参考linux.md#corn定时任务
- /home/oracle/script/backup-oracle.sh
1 |
|
实现交互
1 | read -p "you are sure you wang to xxxxxx?[y/n, default:y]" input |
定时判断
1 | # this is a function to find whether the docker is run. |
删除日志文件
- 定时删除
crontab -e
编辑定时任务配置,参考linux.md#corn定时任务00 02 * * * /home/smalle/script/clear-log.sh
添加配置,每天凌晨2点执行定时systemctl restart crond
重启定时任务
- clear-log.sh
1 | # clear-log.sh |
压缩历史日志
1 | # 备份2019开头的文件或文件夹到his-2019.tar.gz文件中,并删除原文件 |
生成随机数和字符串
1 | ## $RANDOM 的范围是 [0, 32767] |
- 或者创建shell文件
1 |
|
分割字符串
1 | # 参考:https://blog.csdn.net/u010003835/article/details/80750003 |
使用表格显示结果
1 | local line="+-------------------------------------------+\n" |
创建vsftpd虚拟账号
- vsftpd虚拟账号设置见 http://blog.aezo.cn/2019/03/19/arch/ftp/
- 脚本使用如:
sudo ./vsftp_user.sh -u s_test1,s_test2
,以sudo执行脚本,则脚本中的命令都为sudo权限执行
1 |
|
日期
1 | ## 带日期的日志输出 |
执行telnet命令
1 |
|
C 源码脚本
简单示例
1 | # 编写源码 |
- test.c (参考:Linux 输出流重定向缓冲设置 ^5)
1 |
|
参考文章