0%

Bash学习

命令入门

基本内容

bash命令基本都是下面格式

1
$ command [ arg1 ... [ argN ]]

command是具体的命令或者一个可执行文件,arg1...argN是传递给命令或文件的参数

有些参数是命令的配置项,这些配置项一般都以一个连词线开头,比如

1
$ ls -l

同一个配置项往往有长和短两种形式,比如-l是短形式,--list是长形式,它们的作用完全相同。短形式便于手动输入,长形式一般用在脚本之中,可读性更好,利于解释自身的含义

Bash单个命令一般都是一行,用户按下回车就开始执行,有些命令比较长,写成多行有利于阅读,这时就可以在每一行结尾加上\(反斜杠),Bash就会将下一行跟当前行放在一起解释

命令组合

;可以用来在同一行中分隔不同命令,此时不管分号前的命令执行成功或失败,分号后的命令都会执行

&&||可以用于控制命令间的继发关系,如:

1
2
3
4
5
#如果command1 命令运行成功,则运行command2命令
command1 && command2

#如果command1 命令运行失败,则运行command2命令
command1 || command2

echo命令

使用echo命令输出一行文本可以不用双引号包含,如:

1
2
$ echo Hello world!
Hello world!

如果想输出多行文本,则需要用单引号或双引号包含,如:

1
2
3
4
5
6
7
8
$ echo "<HTML>
<head>
<title> Page Title</title>
</head>
<body>
Page body.
</body>
</HTML>"

echo命令输出的文本末尾会自带一个回车符,可以用-n参数取消末尾的回车符

1
2
$ echo -n a; echo b
ab

使用-e参数可以将引号中的转义字符(如\n)转义

1
2
3
4
5
$ echo "Hello\nAjsoabk!"
Hello\nAjsoabk!
$ echo -e "Hello\nAjsoabk!"
Hello
Ajsoabk!

判断内置命令

type命令用于判断某个命令是内置命令还是外部程序,如

1
2
3
4
$ type echo 
echo is a shell builtin
$ type ls
ls is hashed (/bin/ls)

如果要查看一个命令的所有定义,可以使用type命令的-a参数

1
2
3
4
5
$ type -a echo 
echo is shell builtin
echo is /usr/bin/echo
echo is /bin/echo
#说明echo既是内置命令,也有对应的外部程序

type命令的-t参数,可以返回一个命令的类型(alias别名,keyword关键词,function函数,builtin内置命令,file文件)

1
2
3
4
$ type -t bash
file
$ type -t if
keyword

快捷键

  • ctrl+l,清屏
  • ctrl+c,中止当前命令的执行
  • shift+PageUp/PageDown,向上/下滚动
  • ctrl+u /k,从光标位置删除到行首/行尾
  • ctrl+d,关闭shell对话

除了上面这些快捷键,Bash还有自动补全功能。命令输入到一半的时候,可以按下Tab键自动补全,路径也支持自动补全。如果有多个可能的选择,只需要按两次Tab,Bash会显示所有选项让你选择

注释

注释的方式有很多

  • #开头的一行会被当成注释
  • <<BLOCKBLOCK之间的内容会被当成注释
  • :''之间的内容会被当成注释

变量

变量名规则与c相同,大小写敏感

变量定义规则:

  • 变量名与值之间的=两侧都不能有空格
  • 读取或打印变量时需要使用$+变量名
  • 不需要指定数据类型,bash会自动判断数据类型

模式拓展

Shell接收到用户输入的命令以后,会根据空格将用户的输入拆分成一个个词元。然后Shell会将词元里面的特殊字符进行拓展,拓展完成后才会调用相应的命令

Bash允许用户关闭拓展:

1
2
3
4
5
6
7
$ set -o noglob
# or
$ set -f
#重新打开拓展
$ set +o noglob
# or
$ set +f

拓展有许多种,当文件名中包含通配符的时候需要将文件名放在单引号或双引号里面

~目录拓展

~会自动拓展成当前用户的主目录:

1
2
$ echo ~
/home/ajsoabk

~user,表示拓展成用户user的主目录:

1
2
3
4
5
$ echo ~ajsoabk
/home/ajsoabk
#如果后面的用户名是不存在的用户名,则拓展不起作用
$ echo ~notexist
~notexist

~+会拓展成当前所在的目录,等同于pwd命令

1
2
3
$ cd ~/foo
$ echo ~+
/home/ajsoabk/foo

?单字符拓展

?字符代表文件路径里面的任意单个字符,不包括空字符。

1
2
3
# 当前目录存在文件a.txt和b.txt
$ ls ?.txt
a.txt b.txt

如果文件不存在,则不进行拓展

*通配拓展

*字符代表文件路径里面的任意数量的任意字符。但不会匹配隐藏文件(以.开头的文件),如果需要匹配隐藏文件要使用.*

1
2
3
4
5
# 存在文件 a.txt、b.txt和 ab.txt
$ ls a*.txt
a.txt ab.txt
$ ls *b*
b.txt ab.txt

如果没有匹配的文件,则不进行拓展

[]匹配拓展

[]包裹的任意一个字符匹配则匹配成功:

1
2
3
# 存在文件 a.txt 和 b.txt
$ ls [ab].txt
a.txt b.txt

可以在左方括号后紧跟一个^!,表示匹配不在方括号里面的字符

1
2
3
# 存在 aaa、bbb、aba三个文件
$ ls ?[!a]?
aba bbb

如果需要匹配[字符,可以放在方括号内,如果需要匹配-,只能放在方括号内部的开头或结尾,比如[-aeiou],除此之外,-可以用来匹配一个连续的范围,如[a-d]等同于[abcd]

{}全部拓展

{}拓展表示分别拓展成大括号里面的所有值:

1
2
$ echo d{a,e,i,u,o}g
dag deg dig dug dog

大括号内部的逗号前后不能有空格,否则扩展失效

逗号前面可以没有值,表示拓展失效:

1
2
3
$ cp a.log{,.bak}
#等同于
# cp a.log a.log.bak

大括号可以嵌套,也可以与其他模式联用,并且总是先于其他模式进行拓展

{start..end}表示拓展成一个连续序列,并且支持逆序

这种简写形式还可以使用第二个双点号用来指定拓展的步长

1
2
$ echo {0..8..2}
0 2 3 4 6 8

$变量拓展

$开头的词元将被视为变量,变量名除了放在$后面,也可以放在${}里面

1
$ echo $SHELL

${|string*}${!string@}返回所有匹配string的变量名

1
2
$echo ${!S*}
SECONDS SHELL SHELLOPTS SHLVL SSH_AGENT_PID SSH_AUTH_SOCK

$()命令拓展

$()或反引号可以拓展成另一个命令的运行结果,可嵌套,但是结果中的连续空白字符将被替换成单个空格:

1
2
$ echo $(date)
Tue Jan 28 00:01:13 CST 2020

$(())算术拓展

$(())可以拓展成整数运算的结果:

1
2
$ echo $((2+2))
4

[[:class:]]类匹配拓展

  • [[:graph:]]:匹配A-Z、a-z、0-9和标点符号
  • [[:alnum:]]:匹配A-Z、a-z、0-9
  • [[:alpha:]]:匹配A-Z、a-z
  • [[:upper:]]:匹配A-Z
  • [[:lower:]]:匹配a-z
  • [[:digit:]]:匹配0-9
  • [[:punct:]]:匹配标点符号(除了A-Z、a-z、0-9的可打印字符)
  • [[:blank:]]:匹配空格和Tab
  • [[:space:]]:匹配空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)
  • [[:cntrl:]]:匹配ASCII码0-31的不可打印字符
  • [[:xdigit:]]:匹配A-F、a-f、0-9

在第一个方括号后面加上!表示否定

量词语法

量词语法用于控制模式匹配的次数

量词语法有以下五个

  • ?(pattern-list):匹配零个或一个模式
  • @(pattern-list):匹配一个模式
  • *(pattern-list):匹配零个或多个模式
  • +(pattern-list):匹配一个或多个模式
  • !(pattern-list):匹配给定模式外的任何内容
1
2
3
#当前目录下有abctxt和abc.txt
$ ls abc?(.)txt
abctxt abc.txt

shopt命令

shopt命令可以调整Bash的行为,它有好几个参数跟通配符拓展有关

-s参数打开后面跟着的参数,-u关闭后面跟着的参数

  • dotglob,打开后拓展结果可以包含隐藏文件,默认关闭

  • nullglob,打开后让通配符不匹配任何文件名时返回空字符,默认关闭

  • failglob,打开后让通配符不匹配任何文件名时直接报错而不交由命令处理,默认关闭

  • extglob ,打开后支持量词语法,默认打开

  • nocaseglob,打开后让通配符拓展不区分大小写,默认关闭

  • globstar,打开后可以用**匹配零个或多个子目录,默认关闭

    1
    2
    3
    $ shopt -s globstar
    $ ls **/*.txt
    a.txt sub1/b.txt sub1/sub2/c.txt

转义

某些字符在bash里面有特殊含义($,&,*),要想输出这些字符需要在它们前面加上反斜杠,时其变成普通字符,这就叫做转义

1
2
$ echo \$date
$date

反斜杠本身也是特殊字符

是否转义主要有两种区别,一个是包裹字符串的是单引号还是双引号,一个是否使用了-e参数

-e参数会强制转义,不管包裹字符串的是单引号还是双引号。

而在没有-e参数的情况下,单引号包裹的字符串全部原样打印,不会有任何转义或拓展:

1
2
$ echo 'a\\b\nc'
a\\b\nc

只有三个特殊字符在双引号中会保留特殊含义($,` ,\),因此双引号包裹的字符串会对特殊字符进行转义,但不会对不可打印字符进行转义,如:

1
2
3
4
5
6
$ echo "$SHELL"
/bin/bash
$ echo "`date`"
Mon Jan 27 13:33:18 CST 2020
$ echo "a\\b\nc"
a\b\nc

Here文档

Here文档用于输入多行字符串,格式如下:

1
2
3
<< token
text
token

它的格式分为开始标记和结束标记,结束标记必须顶格写

Here文档内部$\保留特殊含义,不支持通配符拓展

1
2
3
4
5
6
7
8
9
10
$ foo='Ajsoabk'
cat << _example_
$foo
"$foo"
'$foo'
_example

Ajsoabk
"Ajsoabk"
'Ajsoabk'

如果不希望发生变量替换,可以把Here文档的开始标记放在单引号之中

1
2
3
4
5
6
7
8
9
10
$ foo='Ajsoabk'
$ cat << '_example'
$foo
"$foo"
'$foo'
_example_

$foo
"$foo"
'$foo'

Here文档的本质是重定向,它将字符串重定向输出给某个命令,相当于包含了echo命令

1
2
3
4
5
$ command << token
string
token
# 等同于
$ echo string |command

所以,Here文档知识和哪些可以接受标准输入作为参数的命令,对于其他命令无效,比如echo命令就不能用Here文档作为参数

此外,Here文档也不能作为变量的值,只能用于命令的参数

Here字符串

Here文档还有一个变体,叫做Here字符串,使用<<<表示,它的作用是将字符串通过标准输入传递给命令

有些命令直接接受给定的参数与通过标准输入接受参数结果是不一样的,如:

1
2
3
$ cat <<< 'hi there'
# 等同于
$ echo 'hi there' | cat

而如果是直接将字符串放在cat命令后面的话则会被解释成字符串

变量

环境变量

环境变量是Bash环境自带的变量,进入Shell时已经定义好了,可以直接使用。

可以通过命令env, printenv来查看所有环境变量

下面是一些常见的环境变量

  • BASH,Shell名称,如:

    1
    BASH=/user/bin/bash
  • BASH_VERSION,Bash持有的shell版本,如:

    1
    BASH_VERSION=4.2.46(2)
  • COLUMNS,屏幕的列数,如:

    1
    COLUMNS=80
  • HOME为用户的主目录,如:

    1
    HOME=/home/ajsoabk
  • LOGNAME为日志的用户名,如:

    1
    LOGNAME=ajsoabk
  • USER当前用户的用户名,如:

    1
    USER=ajsoabk
  • LINUNO返回它在脚本里面的行号

  • FUNCNAME返回一个包含当前函数调用堆栈的数组

  • BASH_SOURCE返回当前脚本调用堆栈

自定义变量

set命令可以显示所有变量(环境变量和自定义变量)已经所有的Bash函数

变量可以重复赋值,后面的赋值会覆盖前面的赋值

如果变量的值本身也是变量,可以使用${!var} 的语法读取最终的值

1
2
3
$ myvar=USER
$ echo ${!myvar}
ajsoabk

export变量

export变量能够被子Shell读取,在子Shell中更改export变量不会影响父Shell,如:

1
2
3
4
5
6
7
8
9
10
$ export foo=bar
# 新建子Shell
$ bash
$ echo $foo
bar
$ foo=baz
# 退出子Shell
$ exit
$ echo $foo
bar

特殊变量

$?为上一个命令的退出码,用来判断上一个命令是否执行成功,若成功则为0,反之则为非零值:

1
2
3
4
$ ls doesnotexist
ls: doesnotexist: No such file or directory
$ echo $?
1

$$为当前Shell的进程ID,可以用来命令临时文件

1
LOGFILE=/t,p/output_log.$$

$_为上一个命令的最后一个参数

1
2
3
4
$ grep dictionary /usr/share/dict/words
dictionary
$ echo $_
/usr/share/dict/words

$0为当前Shell的名称或者脚本名

变量的默认值

1
2
3
4
5
6
7
8
9
10
${varname:-word}
#如果varname存在且不为空,则返回它的值,否则返回word
${varname:=word}
#如果varname存在且不为空,则返回它的值,否则将它设为word并且返回word
${varname:+word}
#如果varname存在且不为空,则返回word,否则返回空值
${varname:?word}
#如果varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行
filename=${a:?"filename missint."}
#在脚本中可以用数字1到9表示传入脚本的参数

declare命令

declare命令可以声明一些特殊类型的变量,比如只读类型和整数类型的变量,其语法形式为:

1
declare OPTION VARIABLE=value

其参数有:

  • -a,数组变量
  • -i,整数变量
  • -r,只读变量
  • -x,环境变量
  • -f,输出所有函数定义
  • -F,输出所有函数名
  • -l,变量值总是会被转为小写字母
  • -u,变量值总是会被转为大写字母
  • -p,输出变量信息

不带任何参数的declare命令等同于不带任何参数的set命令

declare命令如果用在函数中,声明的变量旨在函数内部有效,等同于local命令

注意,一个变量声明为整数以后,依然可以被改写为字符串

1
2
3
4
$ declare -i var=12
$ var=foo
$ echo $var
0

上面例子中,变量var声明为整数,覆盖以后,Bash不会报错,但会赋以不确定的值

readonly命令

readonly命令等同于declare -r,用来声明只读变量,有三个参数

  • -f,声明的变量为函数名
  • -p,打印出所有的只读变量
  • -a,声明的变量为数组

let命令

let命令声明变量时,可以直接执行算术表达式

1
2
3
$ let foo=1+2
$ echo $foo
3

let命令的参数表达式如果包含空格,就需要使用引用

命令行参数

命令行参数通过将数据以参数的形式传递给脚本,使脚本更具动态性,如:

1
./bash_script.sh parameter1 parameter2

如何使用命令行参数

  • $0,脚本名称
  • $1-9,存储前9个参数的名称
  • $#,传递给脚本的参数总数
  • $@, 以数组形式存储的参数列表
  • $*,所有参数连接在一起后的形式

Shift命令与参数移位

shift命令能够控制命令行参数整体左移,同时$#也会减少相应个数,默认移一位,如:

1
2
3
4
5
#!/bin/bash
while [ -n "$1" ];do
echo "$@"
shift
done

字符串

可以使用${#varname}获取字符串长度

提取子串

可以使用${varname:offset:length}提取子串,可以省略length

1
2
3
$ count=frogfootman
$ echo ${count:4:4}
foot

offsetlength都可以为负值

1
2
3
4
5
$ foo="This string is long."
$ echo ${foo: -5:2}
lo
$ echo ${foo: -5:-2}
lon

上面例子中,length-2时要排除从字符串末尾开始的2个字符,所以返回lon

搜索和替换

${string#pattern}${string##pattern}可以检查字符串的开头是否匹配并将匹配的部分删除,其中前者是最短匹配,后者是最长匹配

${string/#pattern/sub}${string/##pattern/sub}可以检查字符串的开头是否匹配并将匹配的部分替换,同样的一个最短一个最长

#换成%则是检查末尾,其余同上

${string/pattern/sub}替换第一个最长的匹配

${string//pattern/sub}替换所有最长匹配

1
2
3
4
5
6
#将路径分隔符:换成换行符
$ echo -e ${PATH//:/\\n'}
/usr/local/bin
/usr/bin
/bin
...

改变大小写

${string^^}可以把字符串转为大写

${string,,}可以把字符串转为小写

算术

(())语法可以进行整数的算术运算,并且会自动忽略内部的空格

这个命令在算术结果不为0的时候执行成功,算术结果为0的时候就算执行失败

1
2
3
$ ((3 - 3))
$ echo $?
1

这个命令本身不返回值,如果要读取算数运算的结果,需要在第一个左括号前面加上美元符号

支持的算数运算符包括加减乘除、求余、递增递减、位运算、逻辑运算(包括三目运算符)、赋值运算(包括一系列复杂赋值)、逗号运算以及指数运算(**),注意/的返回结果总是整数

++--的前缀后缀版本的区别与C语言中相同

在算术表达式中可以使用其它进制

  • 0number,八进制
  • 0xnumber,十六进制
  • bass#numberbase 进制的数

expr命令

expr命令支持算术运算,可以不使用(())语法

脚本入门

Shebang行

脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!Shebang字符开头

如果没有Shebang行,就只能手动将脚本传给解释器来执行

控制流语句

if[-elif-else]-fi

if语法如下:

1
2
3
4
5
6
7
if commands; then
commands
elif commands; then
commands...
else
commands
fi

if后面可以跟任意数量的命令,这时所有命令都会执行,但是判断真伪只看最后一个命令

case结构

case结构用于多值判断,跟包含多个elifif结构等价,但是语义更好

1
2
3
4
5
6
7
8
9
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
* )
commands ;;
esac

如果需要匹配多个条件,可以用;;&终止每个条件块

while循环

格式为:

1
2
3
while condition; do
commands
done

condition可以为命令

until循环

until循环与while循环恰好相反,只要不符合判断条件,就不断循环执行指定的语句,一旦符合判断条件就退出循环:

1
2
3
until condition; do
commands
done

until一般用得比较少,完全可以统一使用while

for...in循环

for...in循环用于遍历列表的每一项

1
2
3
for variable in list; do
commands
done

列表可以是以空格分隔的值(for i in word1 word2 word3; do...

也可以由通配符产生(for i in *.png; do

也可以通过子命令产生(for i in $(cat ~/.bash_profile

in list的部分可以省略,此时list 默认等于脚本的所有参数$@,但是为了可读性最好还是不要省略

for循环

1
2
3
for (( expression1; expression2; expression3 )); do
commands
done

它等同于

1
2
3
4
5
(( expression1 ))
while (( expression2 )); do
commands
(( expression3 ))
done

breakcontinue

与C中相同

select 结构

select结构用于给用户提供选择

1
2
3
4
5
select element
in list
do
commands
done

Bash会对select依次进行下面的处理

  1. select打印一个菜单,内容是列表list的每一项,并且每一项前面还有一个数字编号
  2. Bash提示用户选择一项,输入它的编号
  3. 用户输入以后,Bash会将该项的内容存在变量name,该项的编号存入环境变量REPLY。如果用户没有输入,就按回车键,Bash会重新输出菜单,让用户选择
  4. 执行命令体commands
  5. 执行结束后回到第二步,重复这个过程

下面是一个例子

1
2
3
4
5
#!bin/bash
select brand in Samsung Sony iphone
do
echo "Youve chosen $brand"
done

执行上面的脚本,Bash会输出一个品牌的列表让用户选择

1
2
3
4
1) Samsung
2) Sony
3) iphone
#?

select可以与case结合

test命令

1
2
3
4
5
test expression
# or
[ expression ]
# or
[[ expression ]]

第三种形式还支持正则判断。如果expression表达式为真则test执行成功

文件判断

test命令有许多参数可以用来判断文件状态:

  • [ -a file ],存在
  • [ -b file ],存在且是一个块(设备)文件
  • [ -c file ],存在且是一个字符(设备)文件
  • [ -d file ],存在且是一个目录
  • [ -e file ],存在
  • [ -f file ],存在且是普通文件
  • [ -g file ],存在且设置了组ID
  • [ -G file ],存在且设置了有效的组ID
  • [ -h file ],存在且是符号链接
  • [ -k file ],存在且设置了它的“sticky bit”
  • [ -L file ],存在且是符号链接
  • [ -N file ],存在且自上次读取后已被修改
  • [ -O file ],存在且属于有效的用户ID
  • [ -p file ],存在且是命名管道
  • [ -r file ],存在且当前用户可读
  • [ -s file ],存在且长度大于零
  • [ -S file ],存在且是一个socket
  • [ -t fd ],如果fd是一个文件描述符且重定向到终端则为真。可以判断是否重定向了标准输入输出/错误流
  • [ -u file ],存在且设置了setuid
  • [ -w file ],存在且当前用户可写
  • [ -x file ],存在且当前用户可执行
  • [ file1 -nt file2 ]file1file2的更新时间最近,或者file1存在而file2不存在
  • [ file1 -ot file2 ]file2file1的更新时间最近,或者file2存在而file1不存在
  • [ FILE1 -ef FILE2 ]FILE1FILE2引用相同的设备和inode编号

字符串判断

  • [ string ],不为空

  • [ -n string ],不为空

  • [ -z string ],为空

  • [ string1 = string2],判断等价

  • [ string1 == string2],判断等价

  • [ string1 '>' string2 ],判断string的字典序在string2之后

  • [ string1 '>' string2 ],判断string的字典序在string2之前

    1
    2
    3
    4
    $ str1=abc
    $ str2=abb
    $ if [ "$str1" '<' "$str2" ]; then echo "greater"; fi
    greater

整数判断

  • [ -z num1 ],判断为空
  • [ num1 -eq num2 ],判断等价
  • [ num1 -ne num2 ],判断不等价
  • [ num1 -le num2 ],判断小于等于
  • [ num1 -lt num2 ],判断小于
  • [ num1 -ge num2 ],判断大于等于
  • [ num1 -gt num2 ],判断大于

算术判断

可以包裹(())进行算术运算的判断

正则判断

[[ expression ]]这种判断形式支持正则表达式

[[ string1 =~ regex ]],其中=~是正则比较运算符:

1
2
3
4
5
6
7
8
9
#!/bin/bash
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
echo "INT is an integer."
exit 0
else
echo "INT is not an integer." >&2
exit 1
fi

逻辑判断

可以用&&||!-a-o来结合多个test判断表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!bin/bash
MIN_VAL=1
MAX_VAL=100
INT=50
if [["$INT" =~ ^-[0-9]+$ ]]; then
if [[ $INT -ge $MIN_VAL && $INT -le $MAX_VAL ]]; then
echo "$INT is within $MIN_VAL to $MAX_VAL."
else
echo "$INT is out of range."
fi
else
echo "INT is not an integer." >&2
exit 1
fi

使用!操作符时,最好用圆括号确定转义的范围,并且圆括号必须使用引号或者转义,否则会被Bash解释

脚本参数

调用脚本的时候,脚本文件名后面可以带有参数:

1
$ script.sh word1 word2 word3

脚本文件内部可以使用特殊变量引用这些参数

  • $1-9,脚本的第一个到第九个参数
  • $#,参数的总数
  • $@,以数组形式存储的全部的参数
  • $*,以$IFS值的第一个字符(默认为空格)分隔的全部的参数

如果脚本的参数多于9个,那么第十个参数可以用${10}的形式引用,以此类推

shift命令

shift 命令可以改变脚本参数,每次执行都会移出脚本的前几个参数(默认为1),并将后面所有的参数向前移相应位数,$#的值也会相应减少(如果原来有参数的话)

getopts命令

getopts命令用在脚本内部,可以解析复杂的脚本命令行参数,通常与while循环一起使用,去除脚本所有的带有前置-的参数,其形式为:

1
getopts opt name

第一个参数opt是字符串,给出脚本所有的连词线参数(不带连词线)。

比如某个脚本可以有灿哥配置项参数-lh-a,其中只有-a可以带有参数值,而-l-h是开关参数,那么getopts的第一个参数写成lha:,顺序不重要。注意,a后面有一个冒号表示该参数带有参数值

第二个参数name是一个变量名,用来保存当前取到的配置项参数,即lh、或a

以下是一个使用getopts获取参数的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while getopts 'lha:' OPTION; do
case "$OPTION" in
l)
echo "linuxconfig"
;;
h)
echo "h stands for h"
;;
a)
avalue="$OPTARG"
echo "The value provided is $OPTARG"
;;
?)
echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
exit 1
;;
esac
done
shift "$(($OPTIND - 1))"

变量OPTION保存的是当前处理的连词线参数(即lha)。如果用户输入了没有指定的参数,那么OPTION等于?

如果某个连词线参数带有参数值,那么$OPTARG保存的就是参数值

注意,只要遇到不带连词线的参数,getopts就会执行失败,从而退出while循环。另外,多个连词线参数写在一起的形式,比如command -lhgetopts也可以正确处理

变量OPTINDgetopts开始执行前是1,然后每次执行就会加1

配置项参数终止符--

---开头的参数,会被Bash当作配置项解释,但有时它们是实体参数,如果要确保某个参数不会被当作配置项解释,就要在它前面放上参数终止符--

1
$ ls -- $myPath

exit命令

exit 命令用于终止当前脚本的执行,并向Shell返回一个退出值(0表示正常,1表示发生错误,2表示用法不对,126表示不是可执行脚本,127表示命令没有发现

exit命令与return命令的区别是return命令是函数的退出

source命令

source命令用于执行一个脚本,但不像直接执行脚本时会新建一个子Shell,所以通常用于重新加载一个配置文件,而且source命令执行脚本时不需要export变量

source命令的另一个用途,是在脚本内部加载外部库,这样就可以在脚本里面使用这个外部库定义的函数

source命令有一个简写形式,可以使用一个点(.)来表示

alias命令

alias用于为一个命令指定一个更便于记忆的命令别名,其格式为:

1
alias NAME=DEFINITION

其中NAME是命令别名,DEFINITION是别名对应的原始命令,如:

1
alias search=grep

也可以用来为长命令指定一个更短的别名

1
2
3
$ alias today='date +"%A, %B %-d, %Y"'
$ today
星期日, 十一月 14, 2021

直接调用alias命令可以显示所有别名

unalias命令可以解除别名

获取用户输入

read命令格式如下:

1
read [-options] [variable...]

variable是用来保存输入数值的一个或多个变量名。如果没有提供变量名,环境变量REPLY会包含用户输入的一整行数据

如果用户的输入项少于read命令给出的变量数目,那么额外的变量值为空

如果用户的输入项多于read命令给出的变量数目,那么多余的输入项会包含到最后一个变量中

read命令除了读取键盘输入也可以用来读入文件:

1
2
3
4
5
6
7
#!/bin/bash
filename='/etc/hosts'
while read myline
do
echo "$myline"
done < $filename
# 定向符<将文件内容导向read命令

参数

-t参数设置等待的秒数,环境变量TMOUT也可以起到同样作用

-p参数设置提示信息:

1
read -p "Enter one or more values > "

-a参数把用户的输入赋值给一个数组,下标从0开始:

1
2
3
4
$ read -a people
alice duchess dodo
$ echo ${people[2]}
dodo

-n参数指定读取字符的个数:

1
2
3
4
$ read -n 3 letter
abcdefg
$ echo $letter
abc

-e参数允许用户输入的时候使用readline库提供的快捷键,比如自动补全

-d delimiter将终止符设定为delimiter而不是\n

-r,raw模式,表示不把用户输入的反斜杠字符解释为转义字符

-s,使用户的输入不显示在屏幕上,用于信息保密

-u fd:使用文件描述符fd作为输入

IFS(Internal Field Separator)环境变量

read 命令读取的值默认以空格分隔。可以通过自定义环境变量IFS来修改分隔标志。

IFS的默认值是空格、tab\n,通常取第一个

在文件读取中经常把IFS定义成:;

可以把IFS的赋值命令和read命令写在一行,这样的话IFS的改变仅对后面的命令生效,不然就要用到OLD_IFS变量来恢复IFS

使用内置的read命令:

1
2
3
4
5
6
7
8
#!/bin/bash
echo "Enter your name:"
read user_name
echo "Your name is $user_name"
echo
echo "Enter your age, phonenumber and email: "
read age phonenumber email
echo "Your age is :$age. Your phone is: $phonenumber. Your email is $email"

使用-p参数

Bash函数

Bash函数定义的语法是:

1
2
function fn() {
}

其中function可以省略

下面是一个多行函数的例子,显示当前日期时间

1
2
3
4
today() {
echo -n "Today's date is: "
date +"%A, %B %-d, %Y"
}

删除一个函数可以使用unset命令

1
unset -f functionName

查看当前Shell已经定义的所有函数的详细信息可以使用declare命令

1
$ declare -f

参数变量

函数体内可以使用参数变量,与脚本参数变量是一致的

return命令

return命令可以返回一个值给调用者。如果命令行直接执行函数,下一个命令可以用$?获取返回值

local命令

Bash函数体内直接声明的变量(包括函数体内声明的普通变量)都属于全局变量,整个脚本都可以读取

如果希望声明局部变量,需要使用local命令:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# test.sh
fn (){
local foo
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"

其运行结果为:

1
2
3
$ bash test.sh
fn: foo = 1
global: foo =

数组

数组是一个包含多个值的变量,成员的编号从0开始

数组可以采用一次性赋值的方式创建

1
2
3
4
5
6
7
8
9
10
array=(val1 val2 ... valn)
# 也可以
array[0]=val1
array[1]=val2
...
# 也可以
array=([2]=val3 [0]=val1 [1]=val2)
#也可以只为某些值指定位置
names=(hater [5]=duchess alice)
#上面例子中alice是6号位

成员读取

数组的成员读取方式为${arr[n]}。如果没有花括号,Bash会读取arr的第一个元素,再把[n]原样输出

可以用${arr[@]}${arr[*]}获取数组的所有成员

将其放在双引号中时,后者会将数组元素合并成一个字符串返回

所以拷贝一个数组的最方便方法就是:

1
$ hobbies=( "${activities[@]}" )

提取数组序号

${!arr[@]}${!arr[*]}可以返回数组的成员序号

因此可以使用这种语法循环数组的下标

1
2
3
4
arr=(a b c d)
for i in ${!arr[@]};do
echo ${arr[i]}
done

提取数组成员

${arr[@]:pos:len}的语法可以提取一个区间内的成员

1
2
3
$ food=( apples bananas cucunmbers dates eggs fajitas grapes )
$ echo ${food[@]:1:3}
bananas cucumbers dates

如果省略length则返回从指定位置开始的所有成员

追加数组成员

可以使用+=赋值运算符追加数组成员

删除数组

使用unset命令删除数组或数组成员

关联数组

declare -A可以声明关联数组

1
2
3
delcare -A colors 
colors["red"]="#ff0000"
colors["green"]="#00ff00"

访问关联数组成员的方法几乎与整数索引数组相同

set命令与shopt命令

set命令可以帮助你写出更安全的Bash脚本

直接运行set会显示所有的环境变量和Shell函数

-u

设置了-u参数之后,脚本遇到不存在的变量就会报错并停止执行

-u参数和-o nounset是等价的

-x

设置了-x参数之后,脚本每运行一条命令就会在终端输出,用于调试,与-o xtrace等价

如果要关闭命令输出,可以使用set +x

-e

设置了-e参数之后,脚本只要发生错误(返回值非0),就会终止执行

有些命令的非零返回值可能不表示失败,这时可以使用command || true使得该命令即使执行失败也不会终止执行

等价于-o errexit

-o pipefail

set -e不适用于管道命令

管道命令指多个子命令通过管道运算符|组合成一个大的命令。Bash会把最后一个子命令的返回值作为整个命令的返回值。

set -o pipefail用来保证只要一个子命令失败整个管道命令就失败

-E

设置了-e参数会导致函数内的错误不会被trap命令捕获,加上-E参数后可以使得函数也能继承trap命令

1
2
3
4
5
6
7
8
9
#!/bin/bash
set -Eeuo pipefail
trap "echo ERR trap fired!" ERR
myfunc()
{
# 'foo'是一个不存在的命令
foo
}
myfunc

执行上面这个脚本就可以看到trap命令生效了

1
2
3
$ bash test.sh
test.sh:行9: foo:未找到命令
ERR trap fired!

-n

等同于-o noexec不运行命令,只检查语法是否正确

-f

等同于-o noglob表示不对通配符进行文件名拓展

-v

等同于-o verbose,表示打印Shell接收到的每一行输入

总结

上面介绍的set命令的几个参数,一般都放在一起使用:

1
set -Eeuxo pipefail

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

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

shopt命令

shopt命令用来调整Shell 的参数,跟set命令的作用类似

直接输入shopt可以查看所有参数以及其状态,也可以单独查询某个参数的状态

-s用来打开某个参数

-u用来关闭某个参数

-q也用来查询某个参数是否打开,但不直接输出查询结果,而是通过shopt命令的返回结果$?表示查询结果:

1
2
3
$ shopt -1 globstar
$ echo $?
1

mktemptrap

Bash脚本有时需要创建临时文件或临时目录,常见的做法是在/tmp目录里面创建文件或目录,但这样做有很多问题,使用mktemp命令是最安全的做法

临时文件的安全问题

/tmp目录是所有人可读写的,如果攻击者知道临时文件的文件名就可能导致严重的安全问题。

而且脚本意外退出时往往会糊涂清理临时文件

生成临时文件淫应当遵循下面的规则

  • 创建前检查文件是否已经存在
  • 确保临时文件已成功创建
  • 临时文件必须有权限限制
  • 临时文件名要不可预测
  • 脚本退出时要删除临时文件

mktemp

直接运行mktemp命令就能生成一个临时文件,其文件名随机并且只有用户本人可读写

为了保证脚本退出时临时文件被删除,可以使用trap命令指定退出时的清除操作

1
2
3
4
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

-d参数可以创建一个临时目录

-p参数可以指定临时文件所在的目录,默认使用$TMPDIR环境变量指定的目录,如果这个变量没设置则使用/tmp目录

-t参数可以指定临时文件的文件名模板,模板末尾至少包含索格连续的X字符表示随机字符,默认的文件名模板是tmp.后接十个随机字符

trap命令

trap命令用来在Bash脚本响应系统信号,其形式为:

1
$ trap [action] [signal1] [signal2] ...

其中action是一个Bash命令,常用的信号有

  • EXIT,编号0,退出脚本时产生
  • HUP,编号1,脚本与所在的终端脱离联系
  • INT,编号2,用户按下ctrl + C,意图让脚本停止运行
  • QUIT,编号3,用户按下ctrl+/,意图退出脚本

注意,trap命令必须放在脚本的开头,否则它上方的任何命令导致脚本退出,都不会被它捕获

文件权限

可以通过ls -l命令来罗列出所有文件和目录

img

上图七列分别是:

  • 文件类型(-常规文件,c特殊档案,p命名管道,b块设备,s套接字,d目录,l链接)及超级用户、用户组与其他用户的权限(r阅读权限,w写入权限,x执行权限,-占位)
  • 存储块的数量
  • 文件的所有者或具有管理权限的超级用户
  • 所有者、筹集用户组
  • 文件大小
  • 文件的最后修改日期
  • 文件或目录的名称

chmod命令可以更改不同用户类型的文件权限,其基本形式为

1
2
3
chmod [class][operator][permission] filename
#具体为
chmod [ugoa][+or-][rwx] filename

u超级用户g用户组o其他用户a所有类型

+添加-删除权限

r读取w修改x运行

重定向

输出重定向

>会以命令中的写入内容覆盖原文件内容,如果指定的文件不存在,那么它将会创建一个以指定文件名命名的新文件

1
2
3
4
5
#!/bin/bash
writefile=WriteFile.txt
$ echo "Ajsoabk:www.ajsoabk.xyz" > $writefile
#使用cat命令打印文件内容
$ cat $writefile

>>则是内容附加,如果文件不存在也会新建

输入重定向

<,与<<

深入理解

  • stdin:标准输入文件,其文件描述符为0,默认由此读取数据
  • stdout:标准输出文件,其文件描述符为1,默认向它输出数据
  • stderr:标准错误文件,其文件描述符为2,默认向它写入错误信息
1
2
#将stdin重定向到file1,将stdout重定向到file2
$ command < file1 > file2

文件相关

过于简单:cdmkdir,statrmdir, rm(删除目录或文件)

touch命令可以用来修改文件读取时间和修改时间,-t参数自定义修改时间

cat 命令

cat的全拼是concatenate,主要有三大功能

显示整个文件

1
$cat filename

从键盘创建新文件

1
2
#将stdout重定向到filename
$cat > filename

文件合并

1
$cat file1 file2 > file

参数:

  • -nor-number输出带行数
  • -bor-number-nonblank,输出带行数,但对空行不编号
  • -sor-squeeze-blank,当遇到有两行以上的空行,压缩为一行空行
  • -vor-show-nonprinting