文章目录
参数
预设参数
$$
Shell本身的PID(ProcessID)$!
Shell最后运行的后台Process的PID$?
最后运行的命令的结束代码(返回值)$-
使用Set命令设定的Flag一览$*
所有参数列表。如"$*"
用「"」括起来的情况、以"$1 $2 … $n"
的形式输出所有参数。$@
所有参数列表。如"$@"
用「"」括起来的情况、以"$1" "$2" … "$n"
的形式输出所有参数。$#
添加到Shell的参数个数$0
Shell本身的文件名$1~$n
添加到Shell的各参数值。$1是第1参数、$2是第2参数…。
脚本举例
我们先写一个简单的脚本,执行以后再解释各个变量的意义
$ touch variable
$ vi variable
#脚本内容如下:
#!/bin/sh
echo "number:$#"
echo "scname:$0"
echo "first :$1"
echo "second:$2"
echo "argume:$@"
#保存退出
#赋予脚本执行权限
chmod +x variable
#执行脚本
$ ./variable aa bb
# 输出结果
number:2
scname:./variable
first: aa
second:bb
argume:aa bb
结果分析:
通过显示结果可以看到:
- $# 是传给脚本的参数个数
- $0 是脚本本身的名字
- $1是传递给该shell脚本的第一个参数
- $2是传递给该shell脚本的第二个参数
- $@ 是传给脚本的所有参数的列表
参数截断
--
是一个shell特性,它代表了一个命令的选项(options)已经结束,后面的都是这个命令的参数了,例如:
grep -- -v file
如果你想搜索file中的字符串'-v'
,直接grep '-v' file
或是其他方法都是导致-v
被识别为grep的选项,当加入--
代表选项已经没有了,-v
被理解为第一个参数,file被理解为第二个参数,于是就可以在file搜索'-v'
了
参数扩展
间接参数扩展${!parameter}
其中引用的参数并不是parameter
而是parameter
值为名字的变量值
parameter="var"
var="hello"
echo ${!parameter}
hello
空参数处理
${parameter:-word}
当parameter未设置或者为空则替换成word
set a b
echo ${3:-word} # word
echo ${1:-word} # a
echo ${par:-word} # word
par=c
echo ${par:-word} # c
${parameter:=word}
同上。也就是给parameter一个默认参数,所以位置参数和特殊参数不能以这种方式分配。即不能${3:=world}
set a b
echo ${3:=word} # -bash: $3: cannot assign in this way
echo ${1:=word} # a
echo ${par:=word} # word
par=c
echo ${par:=word} # c
${parameter:?word}
当变量 parameter 未设置或为空,shell 也是可交互时,进行报错并且退出。如果 shell 不可交互,则发生变量替换。
set a b
echo ${3:?word} # -bash: 3: word
echo $? # 1 说明错误
echo ${1:?word} # a
echo ${par:?word} # -bash: par: word
par=c
echo ${par:?word} # c
${parameter:+word}
如果parameter
为空或未设置,那么就什么都不做。不然使用word
进行替换。
set a b
echo ${3:+word} # 空
echo ${1:+word} # word
echo ${par:+word} # 空
par=c
echo ${par:+word} # word
计算字符串的长度
${#parameter}
- 如果parameter是字符串,表达式扩展为字符串的长度
- 如果parameter是*或者@,表达式扩展为参数的个数
- 如果parameter是一个数组名,并且下标为*或者@,表达式扩展为数组的元素个数
变量切片
变量切片
${parameter:offset}
${parameter:offset:length}
和大部分编程语言字符串切片一样,offset代表偏移值,length代表字符长度,需要注意的有以下几点
- 起始位置为0,截取的字符串包含第一个
- 当不指定length时,会截取到字符串结尾
string=01234567890abcdefgh
echo ${string: 1} # 1234567890abcdefgh
echo ${string: 1: 2} # 12
- 当offset小于0时,代表从尾部开始计算偏移值,开始值为-1,同时冒号和offset之间至少有一个空格避免歧义
string=01234567890abcdefgh
echo ${string: -2} # gh
echo ${string:-2} # 01234567890abcdefgh 没有空格被解释成了:-
echo ${string: -3:2} # fg
- 当length小于0时,代表的从尾部开始计算的偏移值而不是字符长度,所以表达式拓展为offset到length直接的字符,注意此时是不包括length这个字符的。
string=01234567890abcdefgh
echo ${string: -7:-2} # bcdef
- 如果参数为
@
,结果是位置参数的偏移值加长度,注意此时起始计数为1(注意和上面区分),如果offset为负值,则代表从尾部计数,length不能为负数
string=1234567890abcdefgh
set -- 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
echo ${string:7} # 890abcdefgh
echo ${@:7} # 7 8 9 0 a b c d e f g h 注意和上面的区别
echo ${@: 0} # 1 2 3 4 5 6 7 8 9 0 a b c d e f g h
echo ${@: 1} # 1 2 3 4 5 6 7 8 9 0 a b c d e f g h 和上面结果一样
echo ${string: -7} # bcdefgh
echo ${@: -7} # b c d e f g h
echo ${@:7:-2} # bash: -2: substring expression < 0
- 如果参数为下标是
@
或者*
的数组,表达式被扩展为从${parameter[offset]}
往后length
的长度的数组。负数的offset代表从尾部开始计数。length不能为负数
array=(0 1 2 3 4 5 6 7 8 9 0 a b c d e f g h)
echo ${array[@]:7} # 7 8 9 0 a b c d e f g h
echo ${array[@]:7:2} # 7 8
echo ${array[@]: -7:2} # b c
echo ${array[@]: -7:-2} # bash: -2: substring expression < 0
参数删除
${parameter#word}
${parameter##word}
- 若变量内容从头开始的数据符合“关键字”,则将符合的最短(使用#)或者最长(使用##)数据删除
其中word是可以使用sh中的模式匹配,如*
、?
等
set -- ab-cd-ef ef-gh-ij
echo ${1#-} # ab-cd-ef 只会从头部开始匹配,开头没有-所以就不会匹配上
echo ${1#*-} # cd-ef
echo ${1##*-} # ef ##会匹配最长的数据
- 如果parameter是@或者*,则会对位置参数的每一个进行配合和删除操作,最后的结果是改变之后的参数列表
set -- ab-cd-ef ef-gh-ij
echo ${@#*-} # cd-ef gh-ij
echo ${@##*-} # ef ij
- 如果parameter是一个数组名,并且下标为*或者@,则匹配和删除的操作会应用到每一个数组元素中,最后的结果是改变之后的数组列表
arr=(--a --b --c)
echo ${arr[@]#-} # -a -b -c
${parameter%word}
${parameter%%word}
整体逻辑和上面的${parameter#word}
、${parameter##word}
差别不大,只是这两者是从尾开始匹配关键字
set -- abcd-- efgh--
echo ${1%-} # abcd-
echo ${1%%-} # abcd-
echo ${1%-?} # abcd
echo ${@%-} # abcd- efgh-
arr=(a-- b-- c--)
echo ${arr[@]%-} # a- b- c-
字符移除可以实现一些很常见的操作:
- 获取文件名,文件后缀
FILENAME=linux_bash.sh
echo ${FILENAME%.*} # linux_bash
echo ${FILENAME##*.} # sh
- 获取文件路径,或者获取文件名
FILENAME=/home/somebody/linux_bash.sh
echo ${FILENAME##*/} # linux_bash.sh
echo ${FILENAME%/*} # /home/somebody
如上面介绍的docker-php-entrypoint.sh,判断某字符串是否以某字符开头
$ OPT='-option'
$ if [ ${OPT#-} != ${OPT} ];
> then
> echo "start with -"
> else
> echo "not start with -"
> fi
start with -
参数替换
${parameter/pattern/string}
将parameter中出现的第一个pattern替换为string。
其中:
- pattern可以使用
*
、?
、[]
等通配符
string=abceddabceddabcedd
echo ${string/d?a/f} # abcefbceddabcedd 只替换了第一个dda
echo ${string/d*a/f} # abcefbcedd 替换了ddabcedda
- 如果pattern使用
/
开头, 将会用string替换所有符合的匹配项
string=abceddabceddabcedd
echo ${string//d?a/f} # abcefbcefbcedd 第二个dda也被替换成了f
- 如果pattern使用
#
开头, 将会用string替换开头的匹配项,这个就是默认的表现 - 如果pattern使用
%
开头, 将会用sting替换结尾处的一个匹配项
string=abceddabceddabcedd
echo ${string/%d?a/f} # abceddabceddabcedd 注意此时并未匹配到最后一个dda,原因未知
echo ${string/%dd/f} # abceddabceddabcef 替换了尾部的dd
- 如果参数是
@
或者*
,将会对每一个位置参数进行匹配替换操作
set -- abc abd abe
echo ${@/a/f} # fbc fbd fbe
- 如果参数是数组且以
@
或者*
作为参数,则对数组每一个元素进行匹配替换操作
arr=(abc abd abe)
echo ${arr[@]/a/f} # fbc fbd fbe
大小写修改
大小写修改
${parameter^pattern}
${parameter^^pattern}
${parameter,pattern}
${parameter,,pattern}
注:此操作仅适用于bash4.0往上版本
这些拓展用于修改字符串中的大小写。pattern表示的是匹配的模式,对于匹配项会进行大小写转换(此处表示不理解)
小写转大写: ^
会把开头的小写字母转换成大写,^^
会转换所有小写成大写
par='abc'
echo ${par^} # Abc
echo ${par^^} # ABC
大写转小写: ,
会把开头的大写转换成小写,,,
会把所以大写转换成小写
par='ABC'
echo ${par,} # aBC
echo ${par,,} # abc
参数是@或者*,会对每一个位置参数进行转换
set -- ABC DEF HIJ
echo ${@,} # aBC dEF hIJ
echo ${@,,} # abc def hij
对于下标是@或者*的数组,大小写转换会应用于每一个数组元素
arr=(ABC DEF HIJ)
echo ${arr[@],} # aBC dEF hIJ
echo ${arr[@],,} # abc def hij
变量操作
${parameter@operator}
注:此操作仅适用于bash4.0往上版本
此拓展根据操作符(operator)执行参数转换或者,操作符如下
- Q
将字符串使用引号包裹
par='abc def'
echo ${par@Q} # 'abc def'
- E
对于使用反斜线\后的字符一律按转义处理
# 对于双引号包裹的字符串
par="abc\"u"
echo ${par} # abc"u 按照转义解释
echo ${par@E} # abc"u 按照转义解释
# 对于双引号包裹的字符串
par="abc\'u"
echo ${par} # abc\'u 此时单引号不需要转义,所以展示了\
echo ${par@E} # abc'u E操作,继续按照转义来解释了\
- P
如果parameter含有prompt string时,按照prompt解释(默认按照字符串解释)
par="\@-abcd-\u"
echo ${par} # \@-abcd-\u
echo ${par@P} # 05:09 AM-abcd-I have no name!
- A
拓展成参数赋值的语句
a=2
par=$a+1
echo ${par} # 2+1
echo ${par@A} # par='2+1' 此时$a已经被解释成实际值了
- a
由参数属性值组成的字符串- 对于@和*,此操作会对每一个位置参数进行处理
- 对于下标为@或*的数组,此操作会对每一个数组元素进行处理
脚本自动输入密码
-
重定向:用重定向方法实现交互的前提是指令需要有参数来指定密码输入方式,如
ftp
就有-i
参数来指定使用标准输入来输入密码,还有sudo的-S参数、passwd的-stdin参数。
shell用重定向作为标准输入的用法是:cmd << delimiter scripts delimiter
shell 会将分界符
delimiter
之后直到下一个同样的分界符之前的内容作为输入。
例如,你可以定义一个多行的SQL查询query=$(cat << EOF SELECT * FROM table WHERE column = "value" EOF )
EOF是一个特殊字符,表示文件的结束,在shell脚本中,EOF通常用于定义一个多行的字符串。
实现ftp自动登录并运行ls指令的用法如下:其中name
为用户名,passward
为密码ftp -i -n 192.168.21.46 << EOF user name "passward" ls EOF
sudo权限
sudo -S ls << EOF passward EOF
-
管道:跟重定向一样,指令同样要有参数来指定密码输入方式
所以实现sudo自动输入密码的脚本如下:echo "passward" | sudo -S cp file1 file2
实现自动修改密码的脚本写法如下:
echo "password" | passwd -stdin username
-
expect:上面介绍的两种方法前提条件是指令有参数来设定密码输入方式,像ssh指令就没有这样的参数,第三种交互方式就派上用场了
expect就是用来做交互用的,基本任何交互登录的场合都能使用,但是需要安装expect包
语法如下:#!/bin/expect set timeout 30 spawn ssh -l 用户名 IP 地址 expect "password:" send "要输入的密码" interact
注意:expect跟bash类似,使用时要先登录到expect,所以首行要指定使用expect
在运行脚本时候要expect file,不能sh file了。语句的含义是:- 设置超时时间为30s
- spawn 是 expect 的起始语句,可以理解为从此处开始
- spawn 后面的语句是执行 ssh 连接
- expect:当发现password:这个字符串后,在后方输入send后面的内容
- send:要输入的密码
- interact:执行完留在远程控制台,不加这句执行完后返回本地控制台
脚本创建新用户
sudo useradd [username] --shell /bin/bash --groups sudo --create-home
echo "[username]:[password]" | sudo chpasswd
脚本中切换用户
# 切换用户并执行命令
su 用户名 -c "命令"
# 切换用户并执行脚本
su 用户名 -s /bin/bash 脚本路径
# 切换用户并执行命令集
su 用户名 << EOF
命令集
EOF
super-user previlege
# 编辑/etc/sudoers
$ visudo
# 下面是规则配置:什么用户(组)在哪台服务器上可以以哪些身份执行哪些命令(sudoers文件可以在多个系统上共享)
USER MACHINES=(RUN_AS_USERS:RUN_AS_GROUPS) [NOPASSWD/PASSWD:] COMMANDS
- 第一个量
USER
表示这条规则适用的用户 MACHINES
代表这条规则可以适用于哪些主机RUN_AS_USERS
表示可以以哪些用户的身份运行命令RUN_AS_GROUPS
表示可以以哪些组的身份运行命令COMMANDS
表示规则适用与哪些命令NOPASSWD
是可选的标志位,表示无需输入密码;PASSWD
是默认选项,表示需要输入密码- 选项之间用
,
连接
在开始加上%则可指定组权限
%GROUP MACHINES=(RUN_AS_USERS:RUN_AS_GROUPS) [NOPASSWD/PASSWD:] COMMANDS
如果在配置文件中指定了RUN_AS_USERS
或RUN_AS_GROUPS
,则可以分别使用-u和-g标志来作为什么样的身份执行命令:
sudo -u run_as_user command
sudo -g run_as_group command
示例:
# 允许root用户执行任意路径下的任意命令
root ALL=(ALL) ALL
# 允许users用户组中的用户像root用户一样使用shutdown命令
%users localhost=/sbin/shutdown -h now
脚本自动开终端
写bash脚本自动开程序:
# 新开一个终端
gnome-terminal -- bash -c "命令;命令;"
重定向
一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:
- 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
- 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
- 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。
默认情况下,command > file
将 stdout 重定向到 file,与1>
相同,command < file 将stdin 重定向到 file。一个>
表示覆盖,两个>
表示追加。command &> file
表示将stdout和stderr都重定向到file。
/dev/null
是一个特殊的文件,所有传给它的都会被丢弃。
这里的&
没有固定的意思,放在>
后面的&
,表示重定向的目标不是一个文件,而是一个文件描述符。换言之 2>1
代表将stderr重定向到当前路径下文件名为1的regular file中,而2>&1
代表将stderr重定向到文件描述符为1的文件(即/dev/stdout
)中,这个文件就是stdout在file system中的映射。
<<xxx
这种形式被称为Here document,Here document可以以交互形式实现快捷的多行输入,xxx
为任意字符串,作为标签,为Here documen的起始,输入时直接在终端里输入多行内容,完成后再次输入xxx
,标记输入完成。
command <<标签
>内容
>内容
>...
>标签
if语句
语法
基本语法:
if [ command ]; then
符合该条件执行的语句
fi
扩展语法:
if [ command ];then
符合该条件执行的语句
elif [ command ];then
符合该条件执行的语句
else
符合该条件执行的语句
fi
- bash shell会按顺序执行if语句,如果command执行后且它的返回状态是0,则会执行符合该条件执行的语句,否则后面的命令不执行,跳到下一条命令。
- 当有多个嵌套时,只有第一个返回0退出状态的命令会导致符合该条件执行的语句部分被执行,如果所有的语句的执行状态都不为0,则执行else中语句。
- 返回状态:最后一个命令的退出状态,或者当没有条件是真的话为0。
[ ]
表示条件测试。注意两边的空格很重要。要注意在[
后面和]
前面都必须要有空格- 在shell中,then和fi是分开的语句。如果要在同一行里面输入,则需要用分号将他们隔开。
- 注意if判断中对于变量的处理,需要加引号,以免一些不必要的错误。没有加双引号会在一些含空格等的字符串变量判断的时候产生错误。比如
[ -n "$var" ]
如果var
为空会出错 - 判断是不支持浮点值的
- 如果在非
(())
中单独使用>
或者<
号,系统会认为是输出或者输入重定向,虽然结果显示正确,但是其实是错误的,因此要对这些符号进行转意 - 在默认中,运行if语句中的命令所产生的错误信息仍然出现在脚本的输出结果中
- 使用
-z
或者-n
来检查长度的时候,没有定义的变量也为0 - 空变量和没有初始化的变量可能会对shell脚本测试产生灾难性的影响,因此在不确定变量的内容的时候,在测试号前使用
-n
或者-z
测试一下 $?
变量包含了之前执行命令的退出状态(最近完成的前台进程)(可以用于检测退出状态)
常用判断命令
文件/目录判断:
[ -a FILE ] 如果 FILE 存在则为真。
[ -b FILE ] 如果 FILE 存在且是一个块文件则返回为真。
[ -c FILE ] 如果 FILE 存在且是一个字符文件则返回为真。
[ -d FILE ] 如果 FILE 存在且是一个目录则返回为真。
[ -e FILE ] 如果 指定的文件或目录存在时返回为真。
[ -f FILE ] 如果 FILE 存在且是一个普通文件则返回为真。
[ -g FILE ] 如果 FILE 存在且设置了SGID则返回为真。
[ -h FILE ] 如果 FILE 存在且是一个符号符号链接文件则返回为真。(该选项在一些老系统上无效)
[ -k FILE ] 如果 FILE 存在且已经设置了冒险位则返回为真。
[ -p FILE ] 如果 FILE 存并且是命令管道时返回为真。
[ -r FILE ] 如果 FILE 存在且是可读的则返回为真。
[ -s FILE ] 如果 FILE 存在且大小非0时为真则返回为真。
[ -u FILE ] 如果 FILE 存在且设置了SUID位时返回为真。
[ -w FILE ] 如果 FILE 存在且是可写的则返回为真。(一个目录为了它的内容被访问必然是可执行的)
[ -x FILE ] 如果 FILE 存在且是可执行的则返回为真。
[ -O FILE ] 如果 FILE 存在且属有效用户ID则返回为真。
[ -G FILE ] 如果 FILE 存在且默认组为当前组则返回为真。(只检查系统默认组)
[ -L FILE ] 如果 FILE 存在且是一个符号连接则返回为真。
[ -N FILE ] 如果 FILE 存在 and has been mod如果ied since it was last read则返回为真。
[ -S FILE ] 如果 FILE 存在且是一个套接字则返回为真。
[ FILE1 -nt FILE2 ] 如果 FILE1 比 FILE2 新, 或者 FILE1 存在但是 FILE2 不存在则返回为真。
[ FILE1 -ot FILE2 ] 如果 FILE1 比 FILE2 老, 或者 FILE2 存在但是 FILE1 不存在则返回为真。
[ FILE1 -ef FILE2 ] 如果 FILE1 和 FILE2 指向相同的设备和节点号则返回为真。
字符串判断
[ -z STRING ] 如果STRING的长度为零则返回为真,即空是真
[ -n STRING ] 如果STRING的长度非零则返回为真,即非空是真
[ STRING1 ] 如果字符串不为空则返回为真,与-n类似
[ STRING1 == STRING2 ] 如果两个字符串相同则返回为真
[ STRING1 != STRING2 ] 如果字符串不相同则返回为真
[ STRING1 < STRING2 ] 如果 “STRING1”字典排序在“STRING2”前面则返回为真。
[ STRING1 > STRING2 ] 如果 “STRING1”字典排序在“STRING2”后面则返回为真。
数值判断
[ INT1 -eq INT2 ] INT1和INT2两数相等返回为真 ,=
[ INT1 -ne INT2 ] INT1和INT2两数不等返回为真 ,<>
[ INT1 -gt INT2 ] INT1大于INT2返回为真 ,>
[ INT1 -ge INT2 ] INT1大于等于INT2返回为真,>=
[ INT1 -lt INT2 ] INT1小于INT2返回为真 ,<
[ INT1 -le INT2 ] INT1小于等于INT2返回为真,<=
逻辑判断
[ ! EXPR ] 逻辑非,如果 EXPR 是false则返回为真。
[ EXPR1 -a EXPR2 ] 逻辑与,如果 EXPR1 and EXPR2 全真则返回为真。
[ EXPR1 -o EXPR2 ] 逻辑或,如果 EXPR1 或者 EXPR2 为真则返回为真。
[ ] || [ ] 用OR来合并两个条件
[ ] && [ ] 用AND来合并两个条件
if高级特性:
双圆括号(( ))
:表示数学表达式
在判断命令中只允许在比较中进行简单的算术操作,而双圆括号提供更多的数学符号,而且在双圆括号里面的>
,<
号不需要转意。
双方括号[[ ]]
:表示高级字符串处理函数
双方括号中判断命令使用标准的字符串比较,还可以使用匹配模式,从而定义与字符串相匹配的正则表达式。
在shell中,[ $a != 1 || $b = 2 ]
是不允许的,要用[ $a != 1 ] || [ $b = 2 ]
,而双括号就可以解决这个问题的,[[ $a != 1 || $b = 2 ]]
。又比如这个[ "$a" -lt "$b" ]
,也可以改成双括号的形式(("$a" < "$b"))