bash
————
————
Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。
Shell 是一个程序,提供一个与用户对话的环境。这个环境只有一个命令提示符,让用户从键盘输入命令,所以又称为命令行环境(command line interface,简写为 CLI)。Shell 接收到用户输入的命令,将命令送入操作系统执行,并将结果返回给用户。
Shell 是一个命令解释器,解释用户输入的命令。它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 命令写出各种小程序,又称为脚本(script)。这些脚本都通过 Shell 的解释执行,而不通过编译。
Linux 的 Shell 种类众多,常见的有:
- Bourne Shell(/usr/bin/sh或/bin/sh)
- Bourne Again Shell(/bin/bash)
- C Shell(/usr/bin/csh)
- K Shell(/usr/bin/ksh)
- Shell for Root(/sbin/sh)
- ……
#!/bin/bash
#!
告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序。
可以查看当前运行的 Shell:
$ echo $SHELL
查看当前的 Linux 系统安装的所有 Shell:
$ cat /etc/shells
$
是命令行环境的提示符,用户只需要输入提示符后面的内容
运行 Shell 脚本的两种方法
1、作为可执行程序
将上面的代码保存为 test.sh,并 cd 到相应目录:
chmod +x ./test.sh #使脚本具有执行权限
./test.sh #执行脚本
注意,一定要写成 ./test.sh,而不是 test.sh,运行其它二进制的程序也一样,直接写 test.sh,linux 系统会去 PATH 里寻找有没有叫 test.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 test.sh 是会找不到命令的,要用 ./test.sh 告诉系统说,就在当前目录找。
2、作为解释器参数
这种运行方式是,直接运行解释器,其参数就是 shell 脚本的文件名,如:
/bin/sh test.sh
/bin/php test.php
这种方式运行的脚本,不需要在第一行指定解释器信息,写了也没用。
基本语法
命令行提示符
进入命令行环境以后,用户会看到 Shell 的提示符。提示符往往是一串前缀,最后以一个美元符号$
结尾,用户可以在这个符号后面输入各种命令。
[user@hostname] $
上面例子中,完整的提示符是[user@hostname] $
,其中前缀是用户名(user
)加上@
,再加主机名(hostname
)。比如,用户名是bill
,主机名是home-machine
,前缀就是bill@home-machine
。
根用户(root)的提示符,不以美元符号($
)结尾,而以井号(#
)结尾,用来提醒用户,现在具有根权限,可以执行各种操作
进入命令行环境以后,一般就已经打开 Bash 了。如果你的 Shell 不是 Bash,可以输入bash
命令启动 Bash。
$ bash
退出 Bash 环境,可以使用exit
命令,也可以同时按下Ctrl + d
。
$ exit
Bash 的基本用法就是在命令行输入各种命令,非常直观。作为练习,可以试着输入pwd
命令。按下回车键,就会显示当前所在的目录。
echo 命令
echo
命令的作用是在屏幕输出一行文本,可以将该命令的参数原样输出。
$ echo hello world
hello world
上面例子中,echo
的参数是hello world
,可以原样输出。
如果想要输出的是多行文本,即包括换行符。这时就需要把多行文本放在引号里面。
$ echo "<HTML>
<HEAD>
<TITLE>Page Title</TITLE>
</HEAD>
<BODY>
Page body.
</BODY>
</HTML>"
默认情况下,echo
输出的文本末尾会有一个回车符。-n
参数可以取消末尾的回车符,使得下一个提示符紧跟在输出内容的后面
$ echo -n a;echo b
ab
分号(;
)是命令的结束符,使得一行可以放置多个命令,上一个命令执行结束后,再执行第二个命令。使用分号时,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。
-e
参数会解释引号(双引号和单引号)里面的特殊字符(比如换行符\n
)。如果不使用-e
参数,即默认情况下,引号会让特殊字符变成普通字符,echo
不解释它们,原样输出。
$ echo "Hello\nWorld"
Hello\nWorld
# 双引号的情况
$ echo -e "Hello\nWorld"
Hello
World
# 单引号的情况
$ echo -e 'Hello\nWorld'
Hello
World
&&
和||
Bash 还提供两个命令组合符&&
和||
,允许更好地控制多个命令之间的继发关系。
Command1 && Command2
如果Command1
命令运行成功,则继续运行Command2
命令。
Command1 || Command2
上面命令的意思是,如果Command1
命令运行失败,则继续运行Command2
命令。
$ cat filelist.txt ; ls -l filelist.txt
只要cat
命令执行结束,不管成功或失败,都会继续执行ls
命令。
$ cat filelist.txt && ls -l filelist.txt
上面例子中,只有cat
命令执行成功,才会继续执行ls
命令。如果cat
执行失败(比如不存在文件flielist.txt
),那么ls
命令就不会执行。
$ mkdir foo || mkdir bar
只有mkdir foo
命令执行失败(比如foo
目录已经存在),才会继续执行mkdir bar
命令。如果mkdir foo
命令执行成功,就不会创建bar
目录了。
read
#!/bin/bash
echo -n "输入一些文本 > "
read text
echo "你的输入:$text"
read
可以接受用户输入的多个值。
#!/bin/bash
echo Please, enter your firstname and lastname
read FN LN
echo "Hi! $LN, $FN !"
如果用户的输入项少于read
命令给出的变量数目,那么额外的变量值为空。如果用户的输入项多于定义的变量,那么多余的输入项会包含到最后一个变量中。
如果read
命令之后没有定义变量名,那么环境变量REPLY
会包含所有的输入。
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
上面脚本的运行结果如下。
$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'
read
命令除了读取键盘输入,可以用来读取文件。
#!/bin/bash
filename='/etc/hosts'
while read myline
do
echo "$myline"
done < $filename
上面的例子通过read
命令,读取一个文件的内容。done
命令后面的定向符<
,将文件内容导向read
命令,每次读取一行,存入变量myline
,直到文件读取完毕。
read
命令的-t
参数,设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行。
#!/bin/bash
echo -n "输入一些文本 > "
if read -t 3 response; then
echo "用户已经输入了"
else
echo "用户没有输入"
fi
环境变量TMOUT
也可以起到同样作用,指定read
命令等待用户输入的时间(单位为秒)。
$ TMOUT=3
$ read response
-p
参数指定用户输入的提示信息。
read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
上面例子中,先显示Enter one or more values >
,再接受用户的输入。
-a
参数把用户的输入赋值给一个数组,从零号位置开始。
$ read -a people
alice duchess dodo
$ echo ${people[2]}
dodo
-n
参数指定只读取若干个字符作为变量值,而不是整行读取。
$ read -n 3 letter
abcdefghij
$ echo $letter
abc
IFS
read
命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS
(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
IFS
的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。
如果把IFS
定义成冒号(:
)或分号(;
),就可以分隔以这两个符号分隔的值,这对读取文件很有用。
#!/bin/bash
# read-ifs: read fields from a file
FILE=/etc/passwd
read -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<< "$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
上面例子中,IFS
设为冒号,然后用来分解/etc/passwd
文件的一行。IFS
的赋值命令和read
命令写在一行,这样的话,IFS
的改变仅对后面的命令生效,该命令执行后IFS
会自动恢复原来的值。如果不写在一行,就要采用下面的写法。
OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"
另外,上面例子中,<<<
是 Here 字符串,用于将变量值转为标准输入,因为read
命令只能解析标准输入。
如果IFS
设为空字符串,就等同于将整行读入一个变量。
#!/bin/bash
input="/path/to/txt/file"
while IFS= read -r line
do
echo "$line"
done < "$input"
上面的命令可以逐行读取文件,每一行存入变量line
,打印出来以后再读取下一行。
type 命令
type
命令用来判断命令的来源。
$ type echo
echo is a shell builtin
$ type ls
ls is hashed (/bin/ls)
如果要查看一个命令的所有定义,可以使用type
命令的-a
参数。
$ type -a echo
echo is shell builtin
echo is /usr/bin/echo
echo is /bin/echo
上面代码表示,echo
命令既是内置命令,也有对应的外部程序。
type
命令的-t
参数,可以返回一个命令的类型:别名(alias),关键词(keyword),函数(function),内置命令(builtin)和文件(file)。
快捷键
Ctrl + L
:清除屏幕并将当前行移到页面顶部。Ctrl + C
:中止当前正在执行的命令。Shift + PageUp
:向上滚动。Shift + PageDown
:向下滚动。Ctrl + U
:从光标位置删除到行首。Ctrl + K
:从光标位置删除到行尾。Ctrl + D
:关闭 Shell 会话。↑
,↓
:浏览已执行命令的历史记录。
模式扩展
波浪线扩展
波浪线~
会自动扩展成当前用户的主目录。
$ echo ~
/home/me
?
字符扩展
?
字符代表文件路径里面的任意单个字符,不包括空字符。比如,Data???
匹配所有Data
后面跟着三个字符的文件名。
# 存在文件 a.txt 和 b.txt
$ ls ?.txt
a.txt b.txt
上面命令中,?
表示单个字符,所以会同时匹配a.txt
和b.txt
。
如果匹配多个字符,就需要多个?
连用。
*
字符扩展
*
字符代表文件路径里面的任意数量的任意字符,包括零个字符。
# 存在文件 a.txt、b.txt 和 ab.txt
$ ls *.txt
a.txt b.txt ab.txt
上面例子中,*.txt
代表后缀名为.txt
的所有文件。
如果想输出当前目录的所有文件,直接用*
即可。
$ ls *
*
可以匹配空字符,下面是一个例子。
# 存在文件 a.txt、b.txt 和 ab.txt
$ ls a*.txt
a.txt ab.txt
$ ls *b*
b.txt ab.txt
注意,*
不会匹配隐藏文件(以.
开头的文件),即ls *
不会输出隐藏文件。
如果要匹配隐藏文件,需要写成.*
。
# 显示所有隐藏文件
$ echo .*
如果要匹配隐藏文件,同时要排除.
和..
这两个特殊的隐藏文件,可以与方括号扩展结合使用,写成.[!.]*
。
$ echo .[!.]*
*
只匹配当前目录,不会匹配子目录。
# 子目录有一个 a.txt
# 无效的写法
$ ls *.txt
# 有效的写法
$ ls */*.txt
上面的例子,文本文件在子目录,*.txt
不会产生匹配,必须写成*/*.txt
。有几层子目录,就必须写几层星号。
Bash 4.0 引入了一个参数globstar
,当该参数打开时,允许**
匹配零个或多个子目录。因此,**/*.txt
可以匹配顶层的文本文件和任意深度子目录的文本文件。详细介绍请看后面shopt
命令的介绍。
方括号扩展
方括号扩展的形式是[...]
,只有文件确实存在的前提下才会扩展。如果文件不存在,就会原样输出。括号之中的任意一个字符。比如,[aeiou]
可以匹配五个元音字母中的任意一个。
# 存在文件 a.txt 和 b.txt
$ ls [ab].txt
a.txt b.txt
# 只存在文件 a.txt
$ ls [ab].txt
a.txt
上面例子中,[ab]
可以匹配a
或b
,前提是确实存在相应的文件。
方括号扩展属于文件名匹配,即扩展后的结果必须符合现有的文件路径。如果不存在匹配,就会保持原样,不进行扩展。
# 不存在文件 a.txt 和 b.txt
$ ls [ab].txt
ls: 无法访问'[ab].txt': 没有那个文件或目录
上面例子中,由于扩展后的文件不存在,[ab].txt
就原样输出了,导致ls
命名报错。
方括号扩展还有两种变体:[^...]
和[!...]
。它们表示匹配不在方括号里面的字符,这两种写法是等价的。比如,[^abc]
或[!abc]
表示匹配除了a
、b
、c
以外的字符。
# 存在 aaa、bbb、aba 三个文件
$ ls ?[!a]?
aba bbb
上面命令中,[!a]
表示文件名第二个字符不是a
的文件名,所以返回了aba
和bbb
两个文件。
注意,如果需要匹配[
字符,可以放在方括号内,比如[[aeiou]
。如果需要匹配连字号-
,只能放在方括号内部的开头或结尾,比如[-aeiou]
或[aeiou-]
。
[start-end] 扩展
方括号扩展有一个简写形式[start-end]
,表示匹配一个连续的范围。比如,[a-c]
等同于[abc]
,[0-9]
匹配[0123456789]
。
# 存在文件 a.txt、b.txt 和 c.txt
$ ls [a-c].txt
a.txt
b.txt
c.txt
# 存在文件 report1.txt、report2.txt 和 report3.txt
$ ls report[0-9].txt
report1.txt
report2.txt
report3.txt
...
下面是一些常用简写的例子。
[a-z]
:所有小写字母。[a-zA-Z]
:所有小写字母与大写字母。[a-zA-Z0-9]
:所有小写字母、大写字母与数字。[abc]*
:所有以a
、b
、c
字符之一开头的文件名。program.[co]
:文件program.c
与文件program.o
。BACKUP.[0-9][0-9][0-9]
:所有以BACKUP.
开头,后面是三个数字的文件名。
这种简写形式有一个否定形式[!start-end]
,表示匹配不属于这个范围的字符。比如,[!a-zA-Z]
表示匹配非英文字母的字符。
$ echo report[!1–3].txt
report4.txt report5.txt
上面代码中,[!1-3]
表示排除1、2和3。
大括号扩展
大括号扩展{...}
表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔。比如,{1,2,3}
扩展成1 2 3
。
$ echo {1,2,3}
1 2 3
$ echo d{a,e,i,u,o}g
dag deg dig dug dog
$ echo Front-{A,B,C}-Back
Front-A-Back Front-B-Back Front-C-Back
注意,大括号扩展不是文件名扩展。它会扩展成所有给定的值,而不管是否有对应的文件存在。
$ ls {a,b,c}.txt
ls: 无法访问'a.txt': 没有那个文件或目录
ls: 无法访问'b.txt': 没有那个文件或目录
ls: 无法访问'c.txt': 没有那个文件或目录
上面例子中,即使不存在对应的文件,{a,b,c}
依然扩展成三个文件名,导致ls
命令报了三个错误。
另一个需要注意的地方是,大括号内部的逗号前后不能有空格。否则,大括号扩展会失效。
$ echo {1 , 2}
{1 , 2}
上面例子中,逗号前后有空格,Bash 就会认为这不是大括号扩展,而是三个独立的参数。
逗号前面可以没有值,表示扩展的第一项为空。
$ cp a.log{,.bak}
# 等同于
# cp a.log a.log.bak
大括号可以嵌套。
$ echo {j{p,pe}g,png}
jpg jpeg png
$ echo a{A{1,2},B{3,4}}b
aA1b aA2b aB3b aB4b
大括号也可以与其他模式联用,并且总是先于其他模式进行扩展。
$ echo /bin/{cat,b*}
/bin/cat /bin/b2sum /bin/base32 /bin/base64 ... ...
# 基本等同于
$ echo /bin/cat;echo /bin/b*
上面例子中,会先进行大括号扩展,然后进行*
扩展,等同于执行两条echo
命令。
大括号可以用于多字符的模式,方括号不行(只能匹配单字符)。
$ echo {cat,dog}
cat dog
由于大括号扩展{...}
不是文件名扩展,所以它总是会扩展的。这与方括号扩展[...]
完全不同,如果匹配的文件不存在,方括号就不会扩展。这一点要注意区分。
# 不存在 a.txt 和 b.txt
$ echo [ab].txt
[ab].txt
$ echo {a,b}.txt
a.txt b.txt
上面例子中,如果不存在a.txt
和b.txt
,那么[ab].txt
就会变成一个普通的文件名,而{a,b}.txt
可以照样扩展。
{start…end} 扩展
大括号扩展有一个简写形式{start..end}
,表示扩展成一个连续序列。比如,{a..z}
可以扩展成26个小写英文字母。
$ echo {a..c}
a b c
$ echo d{a..d}g
dag dbg dcg ddg
$ echo {1..4}
1 2 3 4
$ echo Number_{1..5}
Number_1 Number_2 Number_3 Number_4 Number_5
这种简写形式支持逆序。
$ echo {c..a}
c b a
$ echo {5..1}
5 4 3 2 1
注意,如果遇到无法理解的简写,大括号模式就会原样输出,不会扩展。
$ echo {a1..3c}
{a1..3c}
这种简写形式可以嵌套使用,形成复杂的扩展。
$ echo .{mp{3..4},m4{a,b,p,v}}
.mp3 .mp4 .m4a .m4b .m4p .m4v
大括号扩展的常见用途为新建一系列目录。
$ mkdir {2007..2009}-{01..12}
上面命令会新建36个子目录,每个子目录的名字都是”年份-月份“。
这个写法的另一个常见用途,是直接用于for
循环。
for i in {1..4}
do
echo $i
done
上面例子会循环4次。
如果整数前面有前导0
,扩展输出的每一项都有前导0
。
$ echo {01..5}
01 02 03 04 05
$ echo {001..5}
001 002 003 004 005
这种简写形式还可以使用第二个双点号(start..end..step
),用来指定扩展的步长。
$ echo {0..8..2}
0 2 4 6 8
上面代码将0
扩展到8
,每次递增的长度为2
,所以一共输出5个数字。
多个简写形式连用,会有循环处理的效果。
$ echo {a..c}{1..3}
a1 a2 a3 b1 b2 b3 c1 c2 c3
变量扩展
Bash 将美元符号$
开头的词元视为变量,将其扩展成变量值,详见《Bash 变量》一章。
$ echo $SHELL
/bin/bash
变量名除了放在美元符号后面,也可以放在${}
里面。
$ echo ${SHELL}
/bin/bash
${!string*}
或${!string@}
返回所有匹配给定字符串string
的变量名。
$ echo ${!S*}
SECONDS SHELL SHELLOPTS SHLVL SSH_AGENT_PID SSH_AUTH_SOCK
上面例子中,${!S*}
扩展成所有以S
开头的变量名。
子命令扩展
$(...)
可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。
$ echo $(date)
Tue Jan 28 00:01:13 CST 2020
上面例子中,$(date)
返回date
命令的运行结果。
还有另一种较老的语法,子命令放在反引号之中,也可以扩展成命令的运行结果。
$ echo `date`
Tue Jan 28 00:01:13 CST 2020
$(...)
可以嵌套,比如$(ls $(pwd))
。
算术扩展
$((...))
可以扩展成整数运算的结果,详见《Bash 的算术运算》一章。
$ echo $((2 + 2))
4
字符类
[[:class:]]
表示一个字符类,扩展成某一类特定字符之中的一个。常用的字符类如下。
[[:alnum:]]
:匹配任意英文字母与数字[[:alpha:]]
:匹配任意英文字母[[:blank:]]
:空格和 Tab 键。[[:cntrl:]]
:ASCII 码 0-31 的不可打印字符。[[:digit:]]
:匹配任意数字 0-9。[[:graph:]]
:A-Z、a-z、0-9 和标点符号。[[:lower:]]
:匹配任意小写字母 a-z。[[:print:]]
:ASCII 码 32-127 的可打印字符。[[:punct:]]
:标点符号(除了 A-Z、a-z、0-9 的可打印字符)。[[:space:]]
:空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)。[[:upper:]]
:匹配任意大写字母 A-Z。[[:xdigit:]]
:16进制字符(A-F、a-f、0-9)。
请看下面的例子。
$ echo [[:upper:]]*
上面命令输出所有大写字母开头的文件名。
字符类的第一个方括号后面,可以加上感叹号!
,表示否定。比如,[![:digit:]]
匹配所有非数字。
$ echo [![:digit:]]*
上面命令输出所有不以数字开头的文件名。
字符类也属于文件名扩展,如果没有匹配的文件名,字符类就会原样输出。
# 不存在以大写字母开头的文件
$ echo [[:upper:]]*
[[:upper:]]*
上面例子中,由于没有可匹配的文件,字符类就原样输出了。
注意点
(1)通配符是先解释,再执行。
Bash 接收到命令以后,发现里面有通配符,会进行通配符扩展,然后再执行命令。
$ ls a*.txt
ab.txt
上面命令的执行过程是,Bash 先将a*.txt
扩展成ab.txt
,然后再执行ls ab.txt
。
(2)文件名扩展在不匹配时,会原样输出。
文件名扩展在没有可匹配的文件时,会原样输出。
# 不存在 r 开头的文件名
$ echo r*
r*
(3)只适用于单层路径。
所有文件名扩展只匹配单层路径,不能跨目录匹配,即无法匹配子目录里面的文件。或者说,?
或*
这样的通配符,不能匹配路径分隔符(/
)。
如果要匹配子目录里面的文件,可以写成下面这样。
$ ls */*.txt
(4)文件名可以使用通配符。
Bash 允许文件名使用通配符,即文件名包括特殊字符。这时引用文件名,需要把文件名放在单引号或双引号里面。
$ touch 'fo*'
$ ls
fo*
上面代码创建了一个fo*
文件,这时*
就是文件名的一部分。
量词语法
量词语法用来控制模式匹配的次数。它只有在 Bash 的extglob
参数打开的情况下才能使用,不过一般是默认打开的。下面的命令可以查询。
$ shopt extglob
extglob on
如果extglob
参数是关闭的,可以用下面的命令打开。
$ shopt -s extglob
量词语法有下面几个。
?(pattern-list)
:匹配零个或一个模式。*(pattern-list)
:匹配零个或多个模式。+(pattern-list)
:匹配一个或多个模式。@(pattern-list)
:只匹配一个模式。!(pattern-list)
:匹配给定模式以外的任何内容。
$ ls abc?(.)txt
abctxt abc.txt
上面例子中,?(.)
匹配零个或一个点。
$ ls abc?(def)
abc abcdef
上面例子中,?(def)
匹配零个或一个def
。
$ ls abc+(.txt|.php)
abc.php abc.txt
上面例子中,+(.txt|.php)
匹配文件有一个.txt
或.php
后缀名。
$ ls abc+(.txt)
abc.txt abc.txt.txt
上面例子中,+(.txt)
匹配文件有一个或多个.txt
后缀名。
$ ls a!(b).txt
a.txt abb.txt ac.txt
上面例子中,!(b)
表示匹配单个字母b
以外的任意内容,所以除了ab.txt
以外,其他文件名都能匹配。
量词语法也属于文件名扩展,如果不存在可匹配的文件,就会原样输出。
# 没有 abc 开头的文件名
$ ls abc?(def)
ls: 无法访问'abc?(def)': 没有那个文件或目录
上面例子中,由于没有可匹配的文件,abc?(def)
就原样输出,导致ls
命令报错。
引号与转义
转义
想要原样输出这些特殊字符,就必须在它们前面加上反斜杠,使其变成普通字符。这就叫做“转义”(escape)。
$ echo \$date
$date
反斜杠本身也是特殊字符,如果想要原样输出反斜杠,就需要对它自身转义,连续使用两个反斜线(\\
)。
反斜杠除了用于转义,还可以表示一些不可打印的字符。
\a
:响铃\b
:退格\n
:换行\r
:回车\t
:制表符
想要在命令行使用这些不可打印的字符,可以把它们放在引号里面,然后使用echo
命令的-e
参数。
$ echo a\tb
atb
$ echo -e "a\tb"
a b
命令行直接输出不可打印字符\t
,Bash 不能正确解释。必须把它们放在引号之中,然后使用echo
命令的-e
参数。
换行符是一个特殊字符,表示命令的结束,Bash 收到这个字符以后,就会对输入的命令进行解释执行。换行符前面加上反斜杠转义,就使得换行符变成一个普通字符,Bash 会将其当作空格处理,从而可以将一行命令写成多行。
$ mv \
/path/to/foo \
/path/to/bar
# 等同于
$ mv /path/to/foo /path/to/bar
如果一条命令过长,就可以在行尾使用反斜杠,将其改写成多行。这是常见的多行命令的写法。
引号
单引号用于保留字符的字面含义,各种特殊字符在单引号里面,都会变为普通字符,比如星号(*
)、美元符号($
)、反斜杠(\
)等。
$ echo '*'
*
$ echo '$USER'
$USER
$ echo '$((2+2))'
$((2+2))
$ echo '$(echo foo)'
$(echo foo)
由于反斜杠在单引号里面变成了普通字符,所以如果单引号之中,还要使用单引号,不能使用转义,需要在外层的单引号前面加上一个美元符号($
),然后再对里层的单引号转义。
# 不正确
$ echo it's
# 不正确
$ echo 'it\'s'
# 正确
$ echo $'it\'s'
更合理的方法是改在双引号之中使用单引号。
$ echo "it's"
it's
大部分特殊字符在双引号里面,都会失去特殊含义,变成普通字符。
三个特殊字符除外:美元符号($
)、反引号(```)和反斜杠(\
)。这三个字符在双引号之中,依然有特殊含义,会被 Bash 自动扩展。
$ echo "$SHELL"
/bin/bash
$ echo "`date`"
Mon Jan 27 13:33:18 CST 2020
美元符号用来引用变量,反引号则是执行子命令。
$ echo "I'd say: \"hello\""
I'd say: "hello"
$ echo "\\"
\
可以利用双引号,在命令行输入多行文本。
$ echo "hello
world"
hello
world
双引号的另一个常见的使用场合是,文件名包含空格。这时就必须使用双引号(或单引号),将文件名放在里面。
$ ls "two words.txt"
双引号还有一个作用,就是保存原始命令的输出格式。
# 单行输出
$ echo $(cal)
九月 2021 日 一 二 三 四 五 六 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
# 原始格式输出
$ echo "$(cal)"
九月 2021
日 一 二 三 四 五 六
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
here 文档
Here 文档(here document)是一种输入多行字符串的方法,格式如下。
<< token
text
token
它的格式分成开始标记(<< token
)和结束标记(token
)。开始标记是两个小于号 + Here 文档的名称,名称可以随意取,后面必须是一个换行符;结束标记是单独一行顶格写的 Here 文档名称,如果不是顶格,结束标记不起作用。两者之间就是多行字符串的内容。
下面是一个通过 Here 文档输出 HTML 代码的例子。
$ cat << _EOF_
<html>
<head>
<title>
The title of your page
</title>
</head>
<body>
Your page content goes here.
</body>
</html>
_EOF_
Here 文档内部会发生变量替换,同时支持反斜杠转义,但是不支持通配符扩展,双引号和单引号也失去语法作用,变成了普通字符。
$ foo='hello world'
$ cat << _example_
$foo
"$foo"
'$foo'
_example_
hello world
"hello world"
'hello world'
here string
command <<< string
command 是 Shell 命令,string 是字符串(它只是一个普通的字符串,并没有什么特别之处)。将字符串通过标准输入,传递给命令。
这种写法告诉 Shell 把 string 部分作为命令需要处理的数据。例如,将小写字符串转换为大写:
[mozhiyan@localhost ~]$ tr a-z A-Z <<< one
ONE
Here String 对于这种发送较短的数据到进程是非常方便的,它比 Here Document 更加简洁。
[mozhiyan@localhost ~]$ var=two
[mozhiyan@localhost ~]$ tr a-z A-Z <<<"one $var there"
ONE TWO THERE
[mozhiyan@localhost ~]$ tr a-z A-Z <<<'one $var there'
ONE $VAR THERE
[mozhiyan@localhost ~]$ tr a-z A-Z <<<one${var}there
ONETWOTHERE
有些命令直接接受给定的参数,与通过标准输入接受参数,结果是不一样的。所以才有了这个语法,使得将字符串通过标准输入传递给命令更方便,比如cat
命令只接受标准输入传入的字符串。
$ cat <<< 'hi there'
# 等同于
$ echo 'hi there' | cat
变量
Bash 变量分成环境变量和自定义变量两类。
环境变量
环境变量是 Bash 环境自带的变量,进入 Shell 时已经定义好了,可以直接使用。它们通常是系统定义好的,也可以由用户从父 Shell 传入子 Shell。
env
命令或printenv
命令,可以显示所有环境变量。
HOSTNAME=A23605341
HARDWARE_PLATFORM=x86_64
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=10.68.35.239 59096 22
CONDA_SHLVL=0
SSH_TTY=/dev/pts/2
USER=webmnt
LD_LIBRARY_PATH=:/home/webmnt/software/python-2.7.16/lib:/home/webmnt/go/src/webmnt_redesign/vendor/Link-c/lib/
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
CONDA_EXE=/home/webmnt/anaconda3/bin/conda
_CE_CONDA=
MAIL=/var/spool/mail/webmnt
PATH=/home/webmnt/anaconda3/condabin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/java8//bin:/usr/local/go/bin:/home/webmnt/go/bin:/home/webmnt/.local/bin:/home/webmnt/bin:/home/webmnt/Documents/alexliu_code/vtp-micro-go/bin
PWD=/home/webmnt/Documents/yanghuamei/test
JAVA_HOME=/usr/java8/
LANG=zh_CN.UTF-8
_CE_M=
HISTCONTROL=ignoredups
SHLVL=1
HOME=/home/webmnt
CONDA_PYTHON_EXE=/home/webmnt/anaconda3/bin/python
LOGNAME=webmnt
CLASSPATH=./usr/java8//lib/dt.jar:/usr/java8//lib/tools.jar
XDG_DATA_DIRS=/home/webmnt/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share
SSH_CONNECTION=10.68.35.239 59096 10.230.5.50 22
GOPATH=/home/webmnt/go
LESSOPEN=||/usr/bin/lesspipe.sh %s
OLDPWD=/home/webmnt/Documents/yanghuamei
_=/usr/bin/env
常见的环境变量。
BASHPID
:Bash 进程的进程 ID。BASHOPTS
:当前 Shell 的参数,可以用shopt
命令修改。DISPLAY
:图形环境的显示器名字,通常是:0
,表示 X Server 的第一个显示器。EDITOR
:默认的文本编辑器。HOME
:用户的主目录。HOST
:当前主机的名称。IFS
:词与词之间的分隔符,默认为空格。LANG
:字符集以及语言编码,比如zh_CN.UTF-8
。PATH
:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。PS1
:Shell 提示符。PS2
: 输入多行命令时,次要的 Shell 提示符。PWD
:当前工作目录。RANDOM
:返回一个0到32767之间的随机数。SHELL
:Shell 的名字。SHELLOPTS
:启动当前 Shell 的set
命令的参数,参见《set 命令》一章。TERM
:终端类型名,即终端仿真器所用的协议。UID
:当前用户的 ID 编号。USER
:当前用户的用户名。
环境变量很少发生变化,而且是只读的,可以视为常量。由于它们的变量名全部都是大写,所以传统上,如果用户要自己定义一个常量,也会使用全部大写的变量名。
注意,Bash 变量名区分大小写,HOME
和home
是两个不同的变量。
查看单个环境变量的值,可以使用printenv
命令或echo
命令。
$ printenv PATH
# 或者
$ echo $PATH
注意,printenv
命令后面的变量名,不用加前缀$
自定义变量
自定义变量是用户在当前 Shell 里面自己定义的变量,仅在当前 Shell 可用。一旦退出当前 Shell,该变量就不存在了。
set
命令可以显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数。
创建变量
变量名必须遵守下面的规则。
- 字母、数字和下划线字符组成。
- 第一个字符必须是一个字母或一个下划线,不能是数字。
- 不允许出现空格和标点符号。
变量声明的语法如下。
variable=value
等号左边是变量名,右边是变量。注意,等号两边不能有空格。
如果变量的值包含空格,则必须将值放在引号中。
myvar="hello world"
Bash 没有数据类型的概念,所有的变量值都是字符串。
a=z # 变量 a 赋值为字符串 z
b="a string" # 变量值包含空格,就必须放在引号里面
c="a string and $b" # 变量值可以引用其他变量的值
d="\t\ta string\n" # 变量值可以使用转义字符
e=$(ls -l foo.txt) # 变量值可以是命令的执行结果
f=$((5 * 7)) # 变量值可以是数学运算的结果
如果同一行定义多个变量,必须使用分号(;
)分隔。
$ foo=1;bar=2
读取变量
读取变量的时候,直接在变量名前加上$
就可以了。
$ foo=bar
$ echo $foo
bar
每当 Shell 看到以$
开头的单词时,就会尝试读取这个变量名对应的值。
如果变量不存在,Bash 不会报错,而会输出空字符。
由于$
在 Bash 中有特殊含义,把它当作美元符号使用时,一定要非常小心,
$ echo The total is $100.00
The total is 00.00
Bash 将$1
解释成了变量,该变量为空,因此输入就变成了00.00
。所以,如果要使用$
的原义,需要在$
前面放上反斜杠,进行转义。
$ echo The total is \$100.00
The total is $100.00
读取变量的时候,变量名也可以使用花括号{}
包围,比如$a
也可以写成${a}
。这种写法可以用于变量名与其他字符连用的情况。
$ a=foo
$ echo ${a}_file
foo_file
如果变量的值本身也是变量,可以使用${!varname}
的语法,读取最终的值。
$ my=USER
$ echo ${my}
USER
$ echo ${!my}
webmnt
删除变量
unset
命令用来删除一个变量。
不存在的 Bash 变量一律等于空字符串,所以即使unset
命令删除了变量,还是可以读取这个变量,值为空字符串。
所以,删除一个变量,也可以将这个变量设成空字符串。
$ foo=''
$ foo=
输出变量
用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。为了把变量传递给子 Shell,需要使用export
命令。这样输出的变量,对于子 Shell 来说就是环境变量。
export
命令用来向子 Shell 输出变量。
# 输出变量 $foo
$ export foo=bar
# 新建子 Shell
$ bash
# 读取 $foo
$ echo $foo
bar
# 修改继承的变量
$ foo=baz
# 退出子 Shell
$ exit
# 读取 $foo
$ echo $foo
bar
子 Shell 修改了继承的变量$foo
,对父 Shell 没有影响。
特殊变量
$?
为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0
,表示上一个命令执行成功;如果是非零,上一个命令执行失败。
[webmnt@A23605341 test]$ echo $?
0
[webmnt@A23605341 test]$ echo $$
12050
$$
为当前 Shell 的进程 ID。
变量的默认值
${varname:-word}
如果变量varname
存在且不为空,则返回它的值,否则返回word
。它的目的是返回一个默认值
${varname:=word}
如果变量varname
存在且不为空,则返回它的值,否则将它设为word
,并且返回word
。它的目的是设置变量的默认值
${varname:+word}
如果变量名存在且不为空,则返回word
,否则返回空值。它的目的是测试变量是否存在
${varname:?message}
如果变量varname
存在且不为空,则返回它的值,否则打印出varname: message
,并中断脚本的执行。如果省略了message
,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,
declare
declare
命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。
它的语法形式如下。
declare OPTION VARIABLE=value
declare
命令的主要参数(OPTION)如下。
-a
:声明数组变量。-f
:输出所有函数定义。-F
:输出所有函数名。-i
:声明整数变量。-l
:声明变量为小写字母。-p
:查看变量信息。-r
:声明只读变量。-u
:声明变量为大写字母。-x
:该变量输出为环境变量。
declare
命令如果用在函数中,声明的变量只在函数内部有效,等同于local
命令。
不带任何参数时,declare
命令输出当前环境的所有变量,包括函数在内,等同于不带有任何参数的set
命令。
readonly
readonly
命令等同于declare -r
,用来声明只读变量,不能改变变量值,也不能unset
变量。
$ readonly foo=1
$ foo=2
bash: foo:只读变量
$ echo $?
1
更改只读变量foo
会报错,命令执行失败。
readonly
命令有三个参数。
-f
:声明的变量为函数名。-p
:打印出所有的只读变量。-a
:声明的变量为数组。
let
let
命令声明变量时,可以直接执行算术表达式。
$ let foo=1+2
$ echo $foo
3
let
命令的参数表达式如果包含空格,就需要使用引号。
$ let "foo = 1 + 2"
let
可以同时对多个变量赋值,赋值表达式之间使用空格分隔。
$ let "v1 = 1" "v2 = v1++"
$ echo $v1,$v2
2,1
字符串
创建字符串
可以用单引号,也可以用双引号,也可以不用引号。
单引号
str='this is a string'
单引号字符串的限制:
- 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
- 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。
双引号
your_name="runoob"
str="Hello, I know you are \"$your_name\"! \n"
echo -e $str
输出结果为:
Hello, I know you are "runoob"!
双引号的优点:
- 双引号里可以有变量
- 双引号里可以出现转义字符
拼接字符串
your_name="runoob"
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1
# 使用单引号拼接
greeting_2='hello, '$your_name' !'
greeting_3='hello, ${your_name} !'
echo $greeting_2 $greeting_3
输出结果为:
hello, runoob ! hello, runoob !
hello, runoob ! hello, ${your_name} !
获取字符串长度
string="abcd"
echo ${#string} #输出 4
提取子字符串
以下实例从字符串第 2 个字符开始截取 4 个字符:
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo
注意:第一个字符的索引值为 0。
这种语法不能直接操作字符串,只能通过变量来读取字符串,并且不会改变原始字符串。变量前面的美元符号可以省略。
# 报错
$ echo ${"hello":2:3}
上面例子中,"hello"
不是变量名,导致 Bash 报错。
$ foo="This string is long."
$ echo ${foo: -5}
long.
$ echo ${foo: -5:2}
lo
$ echo ${foo: -5:-2}
lon
负值,表示从字符串的末尾开始算起。注意,负数前面必须有一个空格, 以防止与${variable:-word}
的变量的设置默认值语法混淆。这时还可以指定length
,length
可以是正值,也可以是负值。
查找子字符串
查找字符 i 或 o 的位置(哪个字母先出现就计算哪个):
string="runoob is a great site"
echo `expr index "$string" io` # 输出 4
注意: 以上脚本中 ` 是反引号,而不是单引号 ',不要看错了哦。
搜索与替换
改变大小写
$ foo=heLLo
$ echo ${foo^^}
HELLO
$ echo ${foo,,}
hello
数组
bash支持一维数组(不支持多维数组),并且没有限定数组的大小。
类似于 C 语言,数组元素的下标由 0 开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于 0
创建数组
用括号来表示数组,数组元素用"空格"符号分割开。定义数组的一般形式为:
数组名=(值1 值2 ... 值n)
array_name=(
value0
value1
value2
value3
)
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen
单独定义数组的各个分量:
读取数组
读取数组元素值的一般格式是:
${数组名[下标]}
例如:
valuen=${array_name[n]}
使用 @ 符号可以获取数组中的所有元素,例如:
echo ${array_name[@]}
echo $array_name[0]
数组的第一个元素是a
。如果不加大括号,Bash 会直接读取$array
首成员的值,然后将[0]
按照原样输出。
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
这两个特殊索引配合for
循环,就可以用来遍历数组。
for i in "${names[@]}"; do
echo $i
done
@
和*
放不放在双引号之中,是有差别的。
$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
$ for act in ${activities[@]}; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing
上面的例子中,数组activities
实际包含5个成员,但是for...in
循环直接遍历${activities[@]}
,导致返回7个结果。为了避免这种情况,一般把${activities[@]}
放在双引号之中。
$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: surfing
上面例子中,${activities[@]}
放在双引号之中,遍历就会返回正确的结果。
拷贝一个数组的最方便方法,就是写成下面这样。
$ hobbies=( "${activities[@]}" )
也可以用来为新数组添加成员。
$ hobbies=( "${activities[@]}" diving )
上面例子中,新数组hobbies
在数组activities
的所有成员之后,又添加了一个成员。
默认位置
读取数组成员时,没有读取指定哪一个位置的成员,默认使用0
号位置。
$ declare -a foo
$ foo=A
$ echo ${foo[0]}
A
上面例子中,foo
是一个数组,赋值的时候不指定位置,实际上是给foo[0]
赋值。
引用一个不带下标的数组变量,则引用的是0
号位置的数组元素。
$ foo=(a b c d e f)
$ echo ${foo}
a
$ echo $foo
a
上面例子中,引用数组元素的时候,没有指定位置,结果返回的是0
号位置。
获取数组的长度
获取数组长度的方法与获取字符串长度的方法相同,例如:
# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}
提取数组序号
${!array[@]}
或${!array[*]}
,可以返回数组的成员序号,即哪些位置是有值的。
$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23
利用这个语法,也可以通过for
循环遍历数组。
arr=(a b c d)
for i in ${!arr[@]};
do
echo ${arr[i]}
done
提取数组成员
${array[@]:position:length}
的语法可以提取数组成员。
$ food=( apples bananas cucumbers dates eggs fajitas grapes )
$ echo ${food[@]:1:1}
bananas
$ echo ${food[@]:1:3}
bananas cucumbers dates
如果省略长度参数length
,则返回从指定位置开始的所有成员。
$ echo ${food[@]:4}
eggs fajitas grapes
追加数组成员
数组末尾追加成员,可以使用+=
赋值运算符。它能够自动地把值追加到数组末尾。否则,就需要知道数组的最大序号,比较麻烦。
$ foo=(a b c)
$ echo ${foo[@]}
a b c
$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f
删除数组
删除一个数组成员,使用unset
命令。
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
$ unset foo[2]
$ echo ${foo[@]}
a b d e f
将某个成员设为空值,可以从返回值中“隐藏”这个成员。
$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${foo[@]}
a c d e f
注意,这里是“隐藏”,而不是删除,因为这个成员仍然存在,只是值变成了空值。
unset ArrayName
可以清空整个数组。
算术运算
算术表达式
((...))
语法可以进行整数的算术运算。自动忽略内部的空格,所以下面的写法都正确,得到同样的结果。
读取算术运算的结果,需要在((...))
前面加上美元符号$((...))
,使其变成算术表达式,返回算术运算的值。
$ ((2+2))
$ (( 2+2 ))
$ (( 2 + 2 ))
不返回值,命令执行的结果根据算术运算的结果而定。只要算术结果不是0
,命令就算执行成功。
$ (( 3 + 2 ))
$ echo $?
0
$ ((4-4))
$ echo $?
1
((...))
语法支持的算术运算符如下。
+
:加法-
:减法*
:乘法/
:除法(整除)%
:余数**
:指数++
:自增运算(前缀或后缀)--
:自减运算(前缀或后缀)
注意,除法运算符的返回结果总是整数,比如5
除以2
,得到的结果是2
,而不是2.5
。
++
和--
这两个运算符有前缀和后缀的区别。作为前缀是先运算后返回值,作为后缀是先返回值后运算。
$ i=0
$ echo $i
0
$ echo $((i++))
0
$ echo $i
1
$ echo $((++i))
2
$ echo $i
2
如果在$((...))
里面使用字符串,Bash 会认为那是一个变量名。如果不存在同名变量,Bash 就会将其作为空值,因此不会报错。
$ echo $(( "hello" + 2))
2
$ echo $(( "hello" * 2))
0
变量foo
的值是hello
,而hello
也会被看作变量名。这使得有可能写出动态替换的代码。
$ foo=hello
$ hello=3
$ echo $(( foo + 2 ))
5
数值的进制
默认都是十进制,但是在算术表达式中,也可以使用其他进制。
number
:没有任何特殊表示法的数字是十进制数(以10为底)。0number
:八进制数。0xnumber
:十六进制数。base#number
:base
进制的数。
下面是一些例子。
$ echo $((0xff))
255
$ echo $((2#11111111))
255
位运算
$((...))
支持以下的二进制位运算符。
<<
:位左移运算,把一个数字的所有位向左移动指定的位。>>
:位右移运算,把一个数字的所有位向右移动指定的位。&
:位的“与”运算,对两个数字的所有位执行一个AND
操作。|
:位的“或”运算,对两个数字的所有位执行一个OR
操作。~
:位的“否”运算,对一个数字的所有位取反。^
:位的异或运算(exclusive or),对两个数字的所有位执行一个异或操作。
十进制4在内存中排列如下:00000100
$ echo $((4<<2))
16
$ echo $((4>>2))
1
位与运算(&)是将两个整数写出二进制形式,然后同位置相比较,只有对于位置的二进制值都为1结果才是1,否则0
00000100 #4
00001000 #8
00000000 #8&4
$ echo $((8&4))
0
位或运算(|),将两个整数写出二进制形式,然后同位置相比较,只要对应的位置有1则结果就为1
00000100 #4
00001000 #8
00001100 #8|4
$ echo $((8|4))
12
异或运算将两个整数写出二进制形式,只要对应的位置同为1或者同为0结果就是0,否则为1。
00001010 #10
00000011 #3
00001001 #10^3
$ echo $((10^3))
9
位非(~)有快捷的公式进行计算 ~a 值位"-(a+1)"
逻辑运算
$((...))
支持以下的逻辑运算符。
<
:小于>
:大于<=
:小于或相等>=
:大于或相等==
:相等!=
:不相等&&
:逻辑与||
:逻辑或!
:逻辑否expr1?expr2:expr3
:三元条件运算符。若表达式expr1
的计算结果为非零值(算术真),则执行表达式expr2
,否则执行表达式expr3
。
如果逻辑表达式为真,返回1
,否则返回0
。
$ echo $((3 > 2))
1
$ echo $(( (3 > 2) || (4 <= 1) ))
1
三元运算符执行一个单独的逻辑测试。它用起来类似于if/then/else
语句。
$ a=0
$ echo $((a<1 ? 1 : 0))
1
$ echo $((a>1 ? 1 : 0))
0
# 第一步执行成功,才会执行第二步
cd $some_directory && rm *
# 第一步执行失败,才会执行第二步
cd $some_directory || exit 1
赋值运算
$((...))
支持的赋值运算符,有以下这些。
parameter = value
:简单赋值。parameter += value
:等价于parameter = parameter + value
。parameter -= value
:等价于parameter = parameter – value
。parameter *= value
:等价于parameter = parameter * value
。parameter /= value
:等价于parameter = parameter / value
。parameter %= value
:等价于parameter = parameter % value
。parameter <<= value
:等价于parameter = parameter << value
。parameter >>= value
:等价于parameter = parameter >> value
。parameter &= value
:等价于parameter = parameter & value
。parameter |= value
:等价于parameter = parameter | value
。parameter ^= value
:等价于parameter = parameter ^ value
$ foo=5
$ echo $((foo*=2))
10
$ echo $(( a<1 ? (a+=1) : (a-=1) ))
求值运算
逗号,
在$((...))
内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值。
$ echo $((foo = 1 + 2, 3 * 4))
12
$ echo $foo
3
expr
计算字符串的长度
[root@localhost shell]# string="hello,everyone my name is xiaoming"
[root@localhost shell]# echo ${#string}
34
[root@localhost shell]# expr length "$string"
34
抓取第一个字符数字串出现的位置
expr index stringsubstring
索 引 命 令 功 能 在 字 符 串 string substring索引命令功能在字符串stringsubstring索引命令功能在字符串string上找出substring中字符第一次出现的位置,若找不到则expr index返回0或1。
[root@localhost shell]$ string="hello,everyone my name is xiaoming"
[root@localhost shell]$ expr index "$string" my
11
[root@localhost shell]$ expr index "$string" nihao
1
匹配
expr match $string substring
命令在string字符串中匹配substring字符串,然后返回匹配到的substring字符串的长度,若找不到则返回0。
[webmnt@A23605341 test]$ expr match "$string" my
0
[webmnt@A23605341 test]$ expr match "$string" hell.*
34
[webmnt@A23605341 test]$ expr match "$string" hell
4
[webmnt@A23605341 test]$ expr match "$string" small
0
对字符串中字符的抽取
第一种是从position位置开始抽取直到字符串结束,第二种是从position位置开始抽取长度为length的子串。
[webmnt@A23605341 test]$ echo ${string:10}
yone my name is xiaoming
[webmnt@A23605341 test]$ echo ${string:10:5}#以0开始编号
yone
[webmnt@A23605341 test]$ echo ${string:10:10}
yone my na
[webmnt@A23605341 test]$ expr substr "$string" 10 5 #以1开始编号
ryone
整数计算
空格!
[webmnt@A23605341 test]$ expr 2 + 2
4
[webmnt@A23605341 test]$ expr 2+2
2+2
[webmnt@A23605341 test]$ val=`expr 2 + 2`
[webmnt@A23605341 test]$ echo "两数之和为: ${val}"
两数之和为: 4
脚本入门
shebang行
#!
后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh
或/bin/bash
。
#!/bin/sh
# 或者
#!/bin/bash
Shebang 行不是必需的,但是建议加上这行。如果缺少该行,就需要手动将脚本传给解释器。举例来说,脚本是script.sh
,有 Shebang 行的时候,可以直接调用执行。
$ ./script.sh
如果没有 Shebang 行,就只能手动将脚本传给解释器来执行。
$ /bin/sh ./script.sh
# 或者
$ bash ./script.sh
执行权限与路径
只要指定了 Shebang 行的脚本,可以直接执行。这有一个前提条件,就是脚本需要有执行权限。可以使用下面的命令,赋予脚本执行权限。
# 给所有用户执行权限
$ chmod +x script.sh
# 给所有用户读权限和执行权限
$ chmod +rx script.sh
# 或者
$ chmod 755 script.sh
# 只给脚本拥有者读权限和执行权限
$ chmod u+rx script.sh
脚本的权限通常设为755
(拥有者有所有权限,其他人有读和执行权限)或者700
(只有拥有者可以执行)。
注释
以 # 开头的行就是注释,会被解释器忽略。
多行注释还可以使用以下格式:
:<<EOF
注释内容...
注释内容...
注释内容...
EOF
EOF 也可以使用其他符号:
:<<'
注释内容...
注释内容...
注释内容...
'
传参
创建 test.sh
[webmnt@A23605341 test]$ vim test.sh
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com
echo "Shell 传递参数实例!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";
为脚本设置可执行权限,并执行脚本
[webmnt@A23605341 test]$ chmod +x test.sh
[webmnt@A23605341 test]$ ./test.sh 1 2 3
Shell 传递参数实例!
执行的文件名:./test.sh
第一个参数为:1
第二个参数为:2
第三个参数为:3
参数处理 | 说明 |
---|---|
$# | 传递到脚本的参数总数 |
$* | 以一个单字符串显示所有向脚本传递的参数。 如"$* “用「”」括起来的情况、以"$1 $2 … $n" 的形式输出所有参数。 |
$$ | 脚本运行的当前进程ID号 |
$! | 后台运行的最后一个进程的ID号 |
$@ | 与$* 相同,但是使用时加引号,并在引号中返回每个参数。 如"$@ “用「”」括起来的情况、以"$1" "$2" … "$n " 的形式输出所有参数。 |
$- | 显示Shell使用的当前选项,与set命令功能相同。 |
$? | 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。 |
$0 | 脚本文件名,即test.sh 。 |
$1-$9 | 对应脚本的第一个参数到第九个参数。如果脚本的参数多于9个,那么第10个参数可以用${10} 的形式引用,以此类推。 |
shift
shift
命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1
),使得后面的参数向前一位,即$2
变成$1
、$3
变成$2
、$4
变成$3
,以此类推。
while
循环结合shift
命令,也可以读取每一个参数。
#!/bin/bash
echo "一共输入了 $# 个参数"
while [ "$1" != "" ]; do
echo "剩下 $# 个参数"
echo "参数:$1"
shift
done
[webmnt@A23605341 test]$ chmod +x shift_study.sh
[webmnt@A23605341 test]$ ./shift_study.sh q w e r t
一共输入了 5 个参数
剩下 5 个参数
参数:q
剩下 4 个参数
参数:w
剩下 3 个参数
参数:e
剩下 2 个参数
参数:r
剩下 1 个参数
参数:t
shift
命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为1
。
shift 3
上面的命令移除前三个参数,原来的$4
变成$1
。
getopts
getopts
命令用在脚本内部,可以解析复杂的脚本命令行参数,通常与while
循环一起使用,取出脚本所有的带有前置连词线(-
)的参数。
getopts optstring name
optstring
是字符串,给出脚本所有的连词线参数。比如,某个脚本可以有三个配置项参数-l
、-h
、-a
,其中只有-a
可以带有参数值,而-l
和-h
是开关参数,那么getopts
的第一个参数写成lha:
,顺序不重要。注意,a
后面有一个冒号,表示该参数带有参数值,getopts
规定带有参数值的配置项参数,后面必须带有一个冒号(:
)。
name
是一个变量名,用来保存当前取到的配置项参数,即l
、h
或a
。
#!/bin/bash
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))"
变量$OPTIND
在getopts
开始执行前是1
,然后每次执行就会加1
。等到退出while
循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1
就是已经处理的连词线参数个数,使用shift
命令将这些参数移除,保证后面的代码可以用$1
、$2
等处理命令的主参数。
配置项参数终止符
-
和--
开头的参数,会被 Bash 当作配置项解释。但是,有时它们不是配置项,而是实体参数的一部分,比如文件名叫做-f
或--file
。
$ cat -f
$ cat --file
上面命令的原意是输出文件-f
和--file
的内容,但是会被 Bash 当作配置项解释。
配置项参数终止符--
,它的作用是告诉 Bash,在它后面的参数开头的-
和--
不是配置项,只能当作实体参数解释。
$ cat -- -f
$ cat -- --file
如果要确保某个变量不会被当作配置项解释,就要在它前面放上参数终止符--
。
如果想在文件里面搜索--hello
,这时也要使用参数终止符--
。
$ grep -- "--hello" example.txt
上面命令在example.txt
文件里面,搜索字符串--hello
。这个字符串是--
开头,如果不用参数终止符,grep
命令就会把--hello
当作配置项参数,从而报错。
source
source
命令用于执行一个脚本,通常用于重新加载一个配置文件。
source
命令最大的特点是在当前 Shell 执行脚本,不像直接执行脚本时,会新建一个子 Shell。所以,source
命令执行脚本时,不需要export
变量。
#!/bin/bash
# test.sh
echo $foo
上面脚本输出$foo
变量的值。
# 当前 Shell 新建一个变量 foo
$ foo=1
# 打印输出 1
$ source test.sh
1
# 打印输出空字符串
$ bash test.sh
source
命令的另一个用途,是在脚本内部加载外部库。
#!/bin/bash
source ./lib.sh
function_from_lib
上面脚本在内部使用source
命令加载了一个外部库,然后就可以在脚本里面,使用这个外部库定义的函数。
source
有一个简写形式,可以使用一个点(.
)来表示。
$ . .bashrc
alias
alias
命令用来为一个命令指定别名,这样更便于记忆。下面是alias
的格式。
alias NAME=DEFINITION
上面命令中,NAME
是别名的名称,DEFINITION
是别名对应的原始命令。注意,等号两侧不能有空格,否则会报错。
alias
也可以用来为长命令指定一个更短的别名。下面是通过别名定义一个today
的命令。
$ alias today='date +"%A, %B %-d, %Y"'
$ today
星期四, 九月 9, 2021
$ alias echo='echo It says: '
$ echo hello world
It says: hello world
只能为命令定义别名,为其他部分(比如很长的路径)定义别名是无效的。
直接调用alias
命令,可以显示所有别名。
$ alias
unalias
命令可以解除别名。
$ unalias echo
条件判断
if
if commands; then
commands
elif commands; then
commands...
else
commands
fi
if
和then
写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号。
if true
then
echo 'hello world'
fi
if false
then
echo 'it is false' # 本行不会执行
fi
除了多行的写法,if
结构也可以写成单行。
$ if true; then echo 'hello world'; fi
hello world
$ if false; then echo "It's true."; fi
if
关键字后面也可以是一条命令,该条命令执行成功(返回值0
),就意味着判断条件成立。
$ if echo 'hi'; then echo 'hello world'; fi
hi
hello world
test
if
结构的判断条件,一般使用test
命令,有三种形式。
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]]#支持正则判断
expression
是一个表达式。这个表达式为真,test
命令执行成功(返回值为0
);表达式为伪,test
命令执行失败(返回值为1
)。注意,第二种和第三种写法,[
和]
与内部的表达式之间必须有空格。
# 写法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi
# 写法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi
# 写法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi
判断表达式
if
关键字后面,跟的是一个命令。命令的返回值为0
表示判断成立,否则表示不成立。因为这些命令主要是为了得到返回值,所以可以视为表达式。
文件判断
[ -a file ]
:如果 file 存在,则为true
。[ -b file ]
:如果 file 存在并且是一个块(设备)文件,则为true
。[ -c file ]
:如果 file 存在并且是一个字符(设备)文件,则为true
。[ -d file ]
:如果 file 存在并且是一个目录,则为true
。[ -e file ]
:如果 file 存在,则为true
。[ -f file ]
:如果 file 存在并且是一个普通文件,则为true
。[ -g file ]
:如果 file 存在并且设置了组 ID,则为true
。[ -G file ]
:如果 file 存在并且属于有效的组 ID,则为true
。[ -h file ]
:如果 file 存在并且是符号链接,则为true
。[ -k file ]
:如果 file 存在并且设置了它的“sticky bit”,则为true
。[ -L file ]
:如果 file 存在并且是一个符号链接,则为true
。[ -N file ]
:如果 file 存在并且自上次读取后已被修改,则为true
。[ -O file ]
:如果 file 存在并且属于有效的用户 ID,则为true
。[ -p file ]
:如果 file 存在并且是一个命名管道,则为true
。[ -r file ]
:如果 file 存在并且可读(当前用户有可读权限),则为true
。[ -s file ]
:如果 file 存在且其长度大于零,则为true
。[ -S file ]
:如果 file 存在且是一个网络 socket,则为true
。[ -t fd ]
:如果 fd 是一个文件描述符,并且重定向到终端,则为true
。 这可以用来判断是否重定向了标准输入/输出/错误。[ -u file ]
:如果 file 存在并且设置了 setuid 位,则为true
。[ -w file ]
:如果 file 存在并且可写(当前用户拥有可写权限),则为true
。[ -x file ]
:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true
。[ file1 -nt file2 ]
:如果 FILE1 比 FILE2 的更新时间最近,或者 FILE1 存在而 FILE2 不存在,则为true
。[ file1 -ot file2 ]
:如果 FILE1 比 FILE2 的更新时间更旧,或者 FILE2 存在而 FILE1 不存在,则为true
。[ FILE1 -ef FILE2 ]
:如果 FILE1 和 FILE2 引用相同的设备和 inode 编号,则为true
。
#!/bin/bash
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 1
fi
上面代码中,$FILE
要放在双引号之中。这样可以防止$FILE
为空,因为这时[ -e ]
会判断为真。而放在双引号之中,返回的就总是一个空字符串,[ -e "" ]
会判断为伪。
字符串判断
[ string ]
:如果string
不为空(长度大于0),则判断为真。[ -n string ]
:如果字符串string
的长度大于零,则判断为真。[ -z string ]
:如果字符串string
的长度为零,则判断为真。[ string1 = string2 ]
:如果string1
和string2
相同,则判断为真。[ string1 == string2 ]
等同于[ string1 = string2 ]
。[ string1 != string2 ]
:如果string1
和string2
不相同,则判断为真。[ string1 '>' string2 ]
:如果按照字典顺序string1
排列在string2
之后,则判断为真。[ string1 '<' string2 ]
:如果按照字典顺序string1
排列在string2
之前,则判断为真。
注意,test
命令内部的>
和<
,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。
#!/bin/bash
ANSWER=$1
if [ -z "$ANSWER" ]; ten
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" = "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi
重定向
将shell命令的执行结果存储到具体文件中,那么我们就需要使用shell中输入/输出的重定向功能。
文件描述符
0表示标准输入,默认从键盘获得输入
1表示标准输出,默认输出到屏幕(即控制台)
2表示标准错误输出,默认输出到屏幕(即控制台)
> 默认为标准输出重定向,与 1> 相同
2>&1 意思是把 标准错误输出 重定向到 标准输出.
&>file 意思是把 标准输出 和 标准错误输出 都重定向到文件file中
输出重定向
命令格式 代表的含义
command > filename 把标准正确输出重定向到指定文件中
command 1 > filename 同上
command >> filename 把标准正确输出追加到指定文件中
command 1 >> filename 同上
command 2 > filename 把标准错误输出重定向到指定文件中
command 2 >> filename 同上
“>” 或者 “>>” 符号表示对标准输出(包括正确的和错误的输出)进行重定向。
这两个符号的左边表示文件描述符,如果没有的话表示1,也就是标准正确输出,符号的右边可以是一个文件,也可以是一个输出设备。
当使用">" 符号时,会判断右边的文件存不存在,如果存在的话就先删除,然后创建一个新的文件,不存在的话则直接创建。
当使用 “>>” 符号进行追加时,则不会删除原来已经存在的文件。
输入重定向
命令格式 代表的含义
command < filename 以filename文件作为标准输入
command 0 < filename 同上
command << filename 以filename文件作为标准输入
command 0 << filename 同上
command <<delimiter 从标准输入中读入,直到遇到delimiter分隔符
我们使用"<" 和 “<<” 符号对输入做重定向,如果符号左边没有写值,那么默认就是0
将利用输入重定向,将我们在键盘上敲入的字符写入到文件中。我们需要使用ctrl+c来结束输入:
$ cat >a.txt
123
text
^C
$ cat a.txt
123
text
[webmnt@A23605341 test]$ cat > b.txt <a.txt
[webmnt@A23605341 test]$ cat b.txt
123
text
整数判断
[ integer1 -eq integer2 ]
:如果integer1
等于integer2
,则为true
。[ integer1 -ne integer2 ]
:如果integer1
不等于integer2
,则为true
。[ integer1 -le integer2 ]
:如果integer1
小于或等于integer2
,则为true
。[ integer1 -lt integer2 ]
:如果integer1
小于integer2
,则为true
。[ integer1 -ge integer2 ]
:如果integer1
大于或等于integer2
,则为true
。[ integer1 -gt integer2 ]
:如果integer1
大于integer2
,则为true
。
#!/bin/bash
INT=-5
if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
正则判断
[[ expression ]]
这种判断形式,支持正则表达式。
[[ string1 =~ regex ]]
上面的语法中,regex
是一个正则表示式,=~
是正则比较运算符。
#!/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
test 判断的逻辑运算
AND
运算:符号&&
,也可使用参数-a
。OR
运算:符号||
,也可使用参数-o
。NOT
运算:符号!
。
#!/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
使用否定操作符!
时,最好用圆括号确定转义的范围。
if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
$ command1 && command2
$ command1 || command2
对于&&
操作符,先执行command1
,只有command1
执行成功后, 才会执行command2
。对于||
操作符,先执行command1
,只有command1
执行失败后, 才会执行command2
。
case结构
case
结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif
的if
结构等价,但是语义更好。它的语法如下。
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac
上面代码中,expression
是一个表达式,pattern
是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号(;
)结尾。
#!/bin/bash
echo -n "输入一个1到3之间的数字(包含两端)> "
read character
case $character in
1 ) echo 1
;;
2 ) echo 2
;;
3 ) echo 3
;;
* ) echo 输入不符合要求
esac
匹配语句的模式是*
,这个通配符可以匹配其他字符和没有输入字符的情况,类似if
的else
部分。
#!/bin/bash
OS=$(uname -s)
case "$OS" in
FreeBSD) echo "This is FreeBSD" ;;
Darwin) echo "This is Mac OSX" ;;
AIX) echo "This is AIX" ;;
Minix) echo "This is Minix" ;;
Linux) echo "This is Linux" ;;
*) echo "Failed to identify this OS" ;;
esac
上面的例子判断当前是什么操作系统。
case
的匹配模式可以使用各种通配符
a)
:匹配a
。a|b)
:匹配a
或b
。[[:alpha:]])
:匹配单个字母。???)
:匹配3个字符的单词。*.txt)
:匹配.txt
结尾。*)
:匹配任意输入,通过作为case
结构的最后一个模式。
#!/bin/bash
echo -n "输入一个字母或数字 > "
read character
case $character in
[[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
;;
[0-9] ) echo "输入了数字 $character"
;;
* ) echo "输入不符合要求"
esac
Bash 4.0之前,case
结构只能匹配一个条件,然后就会退出case
结构。Bash 4.0之后,允许匹配多个条件,这时可以用;;&
终止每个条件块。
#!/bin/bash
# test.sh
read -n 1 -p "Type a character > "
echo
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
[[:digit:]]) echo "'$REPLY' is a digit." ;;&
[[:graph:]]) echo "'$REPLY' is a visible character." ;;&
[[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;&
[[:space:]]) echo "'$REPLY' is a whitespace character." ;;&
[[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac
Type a character > w
'w' is lower case.
'w' is alphabetic.
'w' is a visible character.
循环
while
while
循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。
while condition; do
commands
done
关键字do
可以跟while
不在同一行,这时两者之间不需要使用分号分隔。
while true
do
echo 'Hi, while looping ...';
done
while
的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果。
$ while true; false; do echo 'Hi, looping ...'; done
上面代码运行后,不会有任何输出,因为while
的最后一个命令是false
。
until
until
循环与while
循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
until condition; do
commands
done
关键字do
可以与until
不写在同一行,这时两者之间不需要分号分隔。
until condition
do
commands
done
$ until false; do echo 'Hi, until looping ...'; done
Hi, until looping ...
Hi, until looping ...
Hi, until looping ...
^C
#!/bin/bash
number=0
until [ "$number" -ge 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
上面例子中,只要变量number
小于10,就会不断加1,直到number
大于等于10,就退出循环。
until
的条件部分也可以是一个命令,表示在这个命令执行成功之前,不断重复尝试。
until cp $1 $2; do
echo 'Attempt to copy failed. waiting...'
sleep 5
done
until
循环都可以转为while
循环,只要把条件设为否定即可
for in
for variable in list
do
commands
done
for variable in list; do
commands
done
for
for
循环还支持 C 语言的循环语法。
for (( expression1; expression2; expression3 )); do
commands
done
expression1
用来初始化循环条件,expression2
用来决定循环结束的条件,expression3
在每次循环迭代的末尾执行,用于更新值。
for (( i=0; i<5; i=i+1 )); do
echo $i
done
break、continue
break
命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。
continue
命令立即终止本轮循环,开始执行下一轮循环。
select
select
结构主要用来生成简单的菜单。它的语法与for...in
循环基本一致。
select name
[in list]
do
commands
done
select
生成一个菜单,内容是列表list
的每一项,并且每一项前面还有一个数字编号。- Bash 提示用户选择一项,输入它的编号。
- 用户输入以后,Bash 会将该项的内容存在变量
name
,该项的编号存入环境变量REPLY
。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。 - 执行命令体
commands
。 - 执行结束后,回到第一步,重复这个过程。
#!/bin/bash
# select.sh
select brand in Samsung Sony iphone symphony Walton
do
echo "You have chosen $brand"
done
[webmnt@A23605341 test]$ bash select.sh
1) Samsung
2) Sony
3) iphone
4) symphony
5) Walton
#? 1
You have chosen Samsung
用户没有输入编号,直接按回车键。Bash 就会重新输出一遍这个菜单,直到用户按下Ctrl + c
,退出执行。
函数
函数(function)与别名(alias)的区别是,别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令
函数定义
Bash 函数定义的语法有两种。
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
调用时,就直接写函数名,参数跟在函数名后面。
删除一个函数,可以使用unset
命令。
unset -f functionName
return
return
命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。
function func_return_value {
return 10
}
函数将返回值返回给调用者。如果命令行直接执行函数,下一个命令可以用$?
拿到返回值。
全局变量与局部变量
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。
# 脚本 test.sh
fn () {
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
上面脚本的运行结果如下。
$ bash test.sh
fn: foo = 1
global: foo = 1
上面例子中,变量$foo
是在函数fn
内部声明的,函数体外也可以读取。
函数体内不仅可以声明全局变量,还可以修改全局变量。
#! /bin/bash
foo=1
fn () {
foo=2
}
fn
echo $foo
上面代码执行后,输出的变量$foo
值为2。
函数里面可以用**local
命令声明局部变量。**
#! /bin/bash
# 脚本 test.sh
fn () {
local foo # 局部变量
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
$ bash test.sh
fn: foo = 1
global: foo =
上面例子中,local
命令声明的$foo
变量,只在函数体内有效,函数体外没有定义。
$ bar=1
$ foo=2
bn() {
> local bar
> bar=a
> foo=b
> echo $bar
> echo $foo
> }
$ bn
a
b
$ echo $foo;echo $bar
b
1
set
Bash 执行脚本时,会创建一个子 Shell。
$ bash script.sh
上面代码中,script.sh
是在一个子 Shell 里面执行。这个子 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。
set
命令用来修改子 Shell 环境的运行参数,即定制环境。
如果命令行下不带任何参数,直接运行set
,会显示所有的环境变量和 Shell 函数。
set -u
执行脚本时,如果遇到不存在的变量,Bash 默认忽略它。
#!/usr/bin/env bash
echo $a
echo bar
上面代码中,$a
是一个不存在的变量。执行结果如下。
$ bash script.sh
bar
可以看到,echo $a
输出了一个空行,Bash 忽略了不存在的$a
,然后继续执行echo bar
。大多数情况下,这不是开发者想要的行为,遇到变量不存在,脚本应该报错,而不是一声不响地往下执行。
set -u
就用来改变这种行为。脚本在头部加上它,遇到不存在的变量就会报错,并停止执行。
#!/usr/bin/env bash
set -u
echo $a
echo bar
运行结果如下。
$ bash script.sh
bash: script.sh:行4: a: 未绑定的变量
可以看到,脚本报错了,并且不再执行后面的语句。
-u
还有另一种写法-o nounset
,两者是等价的。
set -o nounset
set -x
默认情况下,脚本执行后,只输出运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。
set -x
用来在运行结果之前,先输出执行的那一行命令。
#!/usr/bin/env bash
set -x
echo bar
执行上面的脚本,结果如下。
$ bash script.sh
+ echo bar
bar
可以看到,执行echo bar
之前,该命令会先打印出来,行首以+
表示。这对于调试复杂的脚本是很有用的。
-x
还有另一种写法-o xtrace
。
set -o xtrace
脚本当中如果要关闭命令输出,可以使用set +x
。
#!/bin/bash
number=1
set -x
if [ $number = "1" ]; then
echo "Number equals 1"
else
echo "Number does not equal 1"
fi
set +x
上面的例子中,只对特定的代码段打开命令输出。
+ '[' 1 = 1 ']'
+ echo 'Number equals 1'
Number equals 1
+ set +x
bash错误处理
如果脚本里面有运行失败的命令(返回值非0
),Bash 默认会继续执行后面的命令。
Bash 只是显示有错误,并没有终止执行。
这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法。
command || exit 1
上面的写法表示只要command
有非零返回值,脚本就会停止执行。
如果停止执行之前需要完成多个操作,就要采用下面三种写法。
# 写法一
command || { echo "command failed"; exit 1; }
# 写法二
if ! command; then echo "command failed"; exit 1; fi
# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi
另外,除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法。
command1 && command2
set -e
set -e
: 脚本只要发生错误,就终止执行。
set -e
根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e
,该命令执行结束后,再重新打开set -e
。
set +e
command1
command2
set -e
set -o pipefail
set -e
有一个例外情况,就是不适用于管道命令。
管道命令,就是多个子命令通过管道运算符(|
)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e
就失效了。
set -o pipefail
用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。
#!/usr/bin/env bash
set -eo pipefail
foo | echo a
echo bar
运行后,结果如下。
$ bash script.sh
a
script.sh:行4: foo: 未找到命令
上面重点介绍的set
命令的几个参数,一般都放在一起使用。
# 写法一
set -Eeuxo pipefail
# 写法二
set -Eeux
set -o pipefail
这两种写法建议放在所有 Bash 脚本的头部。
另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数。
$ bash -euxo pipefail script.sh
shopt
shopt
命令用来调整 Shell 的参数,跟set
命令的作用很类似set
是从 Ksh 继承的,属于 POSIX 规范的一部分,而shopt
是 Bash 特有的。直接输入shopt
可以查看所有参数,以及它们各自打开和关闭的状态。
[webmnt@A23605341 test]$ shopt
autocd off
cdable_vars off
cdspell off
checkhash off
checkjobs off
checkwinsize on
cmdhist on
compat31 off
compat32 off
compat40 off
-s
用来打开某个参数。
$ shopt -s optionNameHere
-u
用来关闭某个参数。
$ shopt -u optionNameHere
-q
-q
的作用也是查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态($?
)表示查询结果。如果状态为0
,表示该参数打开;如果为1
,表示该参数关闭。
这个用法主要用于脚本,供if
条件结构使用。下面例子是如果打开了这个参数,就执行if
结构内部的语句。
if !(shopt -q globstar); then
...
fi
管道命令
概述
常见管道命令的使用(cut/grep、sort/wc/uniq、tee、tr/col/join/paste/expand、xargs、减号-)
命令执行完会在屏幕上打印相应的数据,但这些数据可能要经过几道手续之后才能成为我们所想要的格式。而中间经过的这几道手续就与我们所要用的管道命令有关了。
管道命令与“连续执行命令”是不一样的!!
管道命令使用的界定符号是“|”
管道命令的要求
- 管道命令仅能处理经由前面一个命令传来的正确信息,也就是standard output的信息,对于standard error并没有直接处理的能力。
- 在每个管道后面接的第一个数据必定是“命令”,而且这个命令必须要能够接收standard input的数据。
选取命令
选取命令:就是将一段数据经过分析后,取出我们所想要的;或者是经由分析的关键字,取得我们所想要的那一行。
一般说来,选取信息通常是针对“行”来分析的,并非整篇信息分析的。
cut
将一段信息的某一段“切”出来,处理的信息是以“行”为单位的。主要的用途在于将同一行里面的数据进行分解(在一行信息中取出某部分我们想要的)。
分隔字符:cut -d ‘分隔字符’ -f fields //-f会依据-d的分隔字符将一段信息切割成为数段,用-f取出第几段的意思
排列整齐的信息:cut -c 字符范围 //-c会以字符的单位取出固定字符区间
题目1将PATH变量取出,找出第五个路径。
命令:echo $PATH | cut -d ‘:’ -f 5
题目2:将PATH变量取出,找出第三、五个路径。
命令:echo $PATH | cut -d ‘:’ -f 3,5
题目3:在export输出的信息中,取每一行的第12以后的字符
命令:export | cut -c 12-
题目4:在export输出的信息中,取每一行的第12-20字符
命令:export | cut -c 12-20
题目5:用last在显示的登录者信息中仅留下用户名
命令:last | cut -d ’ ’ -f 1
grep
分析一行信息,若当中有我们所需要的信息,就将该行拿出来.可以解析一行文字,取得关键字,若该行存在关键字,就会整行列出来
grep [-cinv] [–color=auto] ‘查找字符串’ filename //-c计算找到’查找字符串’的次数;-i忽略大小写;-n输出行号;-v反向选择
题目1:将last当中有出现root的那一行取出来
命令:last | grep ‘root’
题目2:与题目1相反,只要没有root的就取出
命令:last | grep -v ‘root’
题目3:在last的输出信息中,只要有root就取出,并且仅取该行的第一列
命令:last | grep ‘root’ | cut -d ’ ’ -f 1
题目4:取出/etc/man.config内含MANPATH的那几行
命令:grep ‘MANPATH’ /etc/man.config
排序命令
sort
可以依据不同的数据类型来排序。例如,数字与文字的排序就不一样。排序的字符与语系的编码有关,因此,如果你需要排序时,建议使用LANG=C来让语系统一。
sort [-bfntk] [file/stdin]
//-b忽略最前面的空格符部分、-f忽略大小写、-n使用纯数字进行排序(默认是以文字类型来排序的)、-t后接分隔符(默认是Tab)、-k为第k个区间来进行排序
题目1:个人账户都记录在/etc/passwd下,请将账号进行排序
命令:cat /etc/passwd | sort
题目2:/etc/passwd内容是以:来分隔的,我想以第三列来排序,该如何办?
命令:cat /etc/passwd | sort -t ‘:’ -k 3
题目3:利用last将输出的数据仅取账号,并加以排序
命令:last | cut -d ’ ’ -f1 | sort
uniq
排序完成后,将重复的数据仅列出一个显示。(即需要配合排序过的文件来处理)
uniq [-c] //-c进行计数
题目1:使用last将账号列出,仅取出账号列,进行排序后仅取出一位
命令:last | cut -d ’ ’ -f 1| sort | uniq
题目2:承题目1,我还想知道每个人的登录总次数
命令:last | cut -d ’ ’ -f 1| sort | uniq -c
wc
计算文件内容的一个工具组。计算输出的信息的整体数据,如计算一个文件里有多少字/行/字符
wc [-lwm] //-l仅列出行、-w仅列出多少字(英文单字)、-m列出有多少字符
题目1:计算/etc/man.config里面到底有多少相关字、行、字符数
命令:cat /etc/man.config | wc
题目2:我知道使用last可以输出登陆者,但是last最后两行并非账号内容,那么我该如何以一行命令串取得这个月份登录系统的总人次?
命令:last | grep [a-zA-Z] | grep -v ‘wtmp’ | wc -l
双向重定向
我们之前使用>将stdout导入文件或设备,除非我们去读取该文件或设备,否则就无法继续利用这个stdout。
tee会同时将数据流送与文件与屏幕,而输出到屏幕的就是stdout,可以让下个命令继续处理。
tee可以让stdout转存一份到文件内并将同样的数据继续送到屏幕去处理。
这样,除了可以让我们同时分析一份数据并记录下来之外,还可以作为处理一份数据的中间暂存盘记录之用。
tee [-a] file //-a以累加的方式,将数据加入file
示例1:last | tee last.list | cut -d " " -f1
解释:让我们将last的输出存一份到last.list文件中
示例2:ls -l /home | tee ~/homefile | more
解释:将ls的数据存一份到~/homefile,同时屏幕也有输出信息
示例3:ls -l / | tee -a ~/homefile | more
解释:tee后接的文件会被覆盖,所以我们加上-a参数来将信息累加
字符转换命令
tr
tr [-ds] SET1 … //-d为删除信息当中的SET1这个字符串;-s为替换掉重复的字符串
【示例】
题目1:将last输出的信息中所有的小写字符变成大写字符
命令:last | tr ‘[a-z]’ ‘[A-Z]’ 或 last | tr [a-z] [A-Z]
题目2:将/etc/passwd输出的信息中的冒号(:)删除
命令:cat /etc/passwd | tr -d ‘:’
题目3:将/etc/passwd转存成dos断行到/root/passwd中,再将^M符号删除
命令:cp /etc/passwd /root/passwd && UNIX2dos /root/passwd; cat /root/passwd | tr -d ‘\r’ > /root/passwd.linux
col
将[tab]按键替换成为空格键;将man page转存为纯文本文件以方便查阅
【用法】
col [-xb] //-x为将tab键转换成对等的空格键、-b则是在文字内有/时,仅保留/最后接的那个字符
【示例】
题目1:利用cat -A显示出所有的特殊按键,最后以col将[tab]转成空白
命令:cat -A /etc/man.config; cat /etc/man.config | col -x | cat -A | more
题目2:将col的man page转存成为/root/col.man的纯文本文件
命令:man col > /root/col.man; man col | col -b > /root/col.man
join
处理两个文件之间的数据 ,而且主要是将两个文件当中有相同数据的那一行加在一起。需要注意的是,在使用join之前,需处理的文件应该要事先经过排序处理,以免略过某些项目。
join [-t12] file1 file2
-t后接分隔符(默认为空格),并且对比“第一字段”的数据,如果两个文件相同,则将两条数据连成一行,且第一个字段放在第一个;
-1代表file1要用哪个字段来分析,-2代表file2要用哪个字段来分析的意思。
题目1:用root的身份,将/etc/passwd与/etc/shadow相关数据整合成一行
命令:join -t ‘:’ /etc/passwd /etc/shadow
评讲:将两个文件的第一字段相同者整合成一行,第二个文件的相同字段并不会显示(因为两个文件中的相同字段会在第一列显示)
题目2:我们知道/etc/passwd的第四个字段是GID,那个GID记录在/etc/group当中的第三个字段,该如何将两个文件整合
命令:join -t ‘:’ -1 4 /etc/passwd -2 3 /etc/group
评讲:相同的字段部分被移动到最前面
paste
paste就直接将两行贴在一起,且中间以[tab]键隔开而已
paste [-d] file1 file2 //-d后面可以接分隔字符(默认为Tab)、如果文件部分写成-,表示来自standard input的数据的意思
【示例】
题目1:将/etc/passwd和/etc/shadow同一行粘贴在一起
命令:paste /etc/passwd /etc/shadow
题目2:先将/etc/group读出,然后与题目1粘贴在一起,且仅取出前三行
命令:cat /etc/group | paste /etc/passwd /etc/shadow - | head -n 3
评讲:重点是-的使用,这经常代表stdin 。
expand
【用途】
将[Tab]按键转成空格键
【用法】
expand [-t] file //-t后接数字,一般是8,但你也可以自行定义一个[Tab]按键代表多少个字符
【示例】
题目1:将/etc/man.config内行首为MANPATH的字样取出,仅取前三行
命令:grep ‘^MANPATH’ /etc/man.config | head -n 3
题目2:承上,如果我想要将所有的符号都列出来的话?
命令:grep ‘^MANPATH’ /etc/man.config | head -n 3 | cat -A
题目3:承上,我将[Tab]按键设置成6个字符的话?
命令:grep ‘^MANPATH’ /etc/man.config | head -n 3 | expand -t 6 - | cat -A
切割split
将一个大文件依据文件大小或行数来切割成为小文件
【用法】
split [-bl] file 切割文件的前导文字 //-b后接欲切割成的文件大小(可带单位,如b,k,m等)、-l为以行数来进行切割
【示例】
题目1:我的/etc/term文件有700多KB,若想要分为300KB一个文件时怎么办?
命令:cd /tmp; split -b 300k /etc/term term
评讲:term是自己设置的前导文字,那么得到的3个小文件的名字就分别为termaa, termab, termac
题目2:如何将上面的三个小文件合成一个文件,文件名为termback
命令:cat term* >> termback
题目3:将使用ls -al /输出的信息中,每10行记录成一个文件
命令:ls -al / | split -l 10 - lsroot
评讲:重点在于-,一般来说,-会被当成stdin或stdout
参数代换xargs
xargs就是在产生某个命令的参数的意思。
xargs可以读入stdin的数据,并且以空格符或断行字符进行分辨,将stdin的数据分隔成为arguments。
因为是以空格符作为分隔,所以,如果有一些文件名或者是其他意义的名词内含有空格符的时候,xargs可能就会误判了。
xargs [-epn] command //未接任何参数时,默认是以echo来进行输出
-e:这个是EOF的意思,后面接一个字符串,当xargs分析到这个字符串时,就会停止继续工作
-p:在执行每个命令的参数时,都会询问用户的意思
-n:后面接次数,每次command命令执行时,要使用几个参数的意思。
题目1:将/etc/passwd内的第一列取出,仅取三行,使用finger这个命令将每个账号的内容显示出来。
命令:cut -d ‘:’ -f 1 /etc/passwd | head -n 3 | xargs finger
说明:我们利用cut取出账号名称,用head取出三个账号,最后由xargs将三个账号的名称变成finger后面需要的参数
题目2:同上,但是每次执行finger时,都要询问用户是否操作。
命令:cut -d ‘:’ -f 1 /etc/passwd | head -n 3 | xargs -p finger
题目3:将所有的/etc/passwd内的账号都以finger查阅,但一次仅查阅五个账号
命令:cut -d ‘:’ -f 1 /etc/passwd | head -n 3 | xargs -p -n 5 finger
说明:一般来说,某些命令后面可以接的参数是有限制的,不能无限制地累加,此时我们可以利用-n来帮助我们将参数分成数个部分,每个部分分别再以命令来执行。
题目4:同上,但是分析到lp就结束这串命令
命令:cut -d ‘:’ -f 1 /etc/passwd | head -n 3 | xargs -p -e ‘lp’ finger
题目5:找出/sbin下面具有特殊权限的文件名,并使用ls -l 列出详细属性
错误命令:find /sbin -perm +7000 | ls -l //因为ls -l并非管道命令
正确命令:find /sbin -perm +7000 | xargs ls -l //xargs产生ls命令的参数(即find找到的文件名)
减号-
在管道命令中,经常会使用到前一个命令的stdout作为这次的stdin,某些命令需要用到文件名(例如tar)来进行处理时,该stdin与stdout可以利用“-”来替代。
举例:tar -cvf - /home | tar -xvf -
将/home里面的文件打包,但打包的数据不是记录到文件,而是传送到stdout;经过管道后,将tar -cvf - /home传送给后面的tar -xvf -。后面的这个-这是取用前一个命令的stdout,因此我们就不需要使用文件了。
awk
AWK 是一种处理文本文件的语言,是一个强大的文本分析工具。
awk [选项参数] 'script' var=value file(s)
或
awk [选项参数] -f scriptfile var=value file(s)
选项参数说明:
- -F fs or --field-separator fs
指定输入文件折分隔符,fs是一个字符串或者是一个正则表达式,如-F:。 - -v var=value or --asign var=value
赋值一个用户定义变量。 - -f scripfile or --file scriptfile
从脚本文件中读取awk命令。 - -mf nnn and -mr nnn
对nnn值设置内在限制,-mf选项限制分配给nnn的最大块数目;-mr选项限制记录的最大数目。这两个功能是Bell实验室版awk的扩展功能,在标准awk中不适用。 - -W compact or --compat, -W traditional or --traditional
在兼容模式下运行awk。所以gawk的行为和标准的awk完全一样,所有的awk扩展都被忽略。 - -W copyleft or --copyleft, -W copyright or --copyright
打印简短的版权信息。 - -W help or --help, -W usage or --usage
打印全部awk选项和每个选项的简短说明。 - -W lint or --lint
打印不能向传统unix平台移植的结构的警告。 - -W lint-old or --lint-old
打印关于不能向传统unix平台移植的结构的警告。 - -W posix
打开兼容模式。但有以下限制,不识别:/x、函数关键字、func、换码序列以及当fs是一个空格时,将新行作为一个域分隔符;操作符和=不能代替和=;fflush无效。 - -W re-interval or --re-inerval
允许间隔正则表达式的使用,参考(grep中的Posix字符类),如括号表达式[[:alpha:]]。 - -W source program-text or --source program-text
使用program-text作为源代码,可与-f命令混用。 - -W version or --version
打印bug报告信息的版本。
用法一:行匹配语句 awk ‘’ 只能用单引号
awk '{[pattern] action}' {filenames} # 行匹配语句 awk '' 只能用单引号
# 每行按空格或TAB分割,输出文本中的1、4项
$ awk '{print $1,$4}' log.txt
# 输出
2 a
3 like
This's
10 orange,apple
#log.txt
2 this is a test
3 Are you like awk
This's a test
10 There are orange,apple mongo
# 格式化输出
$ awk '{printf "%-8s %-10s\n",$1,$4}' log.txt
# 输出
2 a
3 like
This's
10 orange,apple
用法二:-F相当于内置变量FS, 指定分割字符
awk -F #-F相当于内置变量FS, 指定分割字符
# 使用","分割
$ awk -F, '{print $1,$2}' log.txt
2 this is a test
3 Are you like awk
This's a test
10 There are orange apple mongo
# 或者使用内建变量
$ awk 'BEGIN{FS=","} {print $1,$2}' log.txt
2 this is a test
3 Are you like awk
This's a test
10 There are orange apple mongo
用法三:设置变量
awk -v # 设置变量
$ awk -va=1 '{print $1,$1+a}' log.txt
2 3
3 4
This's 1
10 11
用法四:脚本
awk -f {awk脚本} {文件名}
实例:
$ awk -f cal.awk log.txt
运算符 | 描述 |
---|---|
= += -= *= /= %= ^= **= | 赋值 |
?: | C条件表达式 |
|| | 逻辑或 |
&& | 逻辑与 |
~ 和 !~ | 匹配正则表达式和不匹配正则表达式 |
< <= > >= != == | 关系运算符 |
空格 | 连接 |
+ - | 加,减 |
* / % | 乘,除与求余 |
+ - ! | 一元加,减和逻辑非 |
^ *** | 求幂 |
++ – | 增加或减少,作为前缀或后缀 |
$ | 字段引用 |
in | 数组成员 |
使用正则表达式匹配
# 输出第二列包含 "th",并打印第二列与第四列
$ awk '$2 ~ /th/ {print $2,$4}' log.txt
this a
~ 表示模式开始。// 中是模式。
# 输出包含 "re" 的行
$ awk '/re/ ' log.txt
---------------------------------------------
3 Are you like awk
10 There are orange,apple mongo
忽略大小写
$ awk 'BEGIN{IGNORECASE=1} /this/' log.txt
---------------------------------------------
2 this is a test
This's a test
word.txt只包括小写字母和 ’ ’ 。 每个单词只由小写字母组成。 单词间由一个或多个空格字符分隔。
cat word.txt | tr -s ' ' '\n' | sort | uniq -c | sort -r
#cat word.txt | tr -s ' ' '\n' | sort | uniq -c | sort -r | awk '{print $2" "$1}'
tr -s ’ ’ ‘\n’ 将重复的空格替换为换行
uniq -c 统计重复次数 排序完成后,将重复的数据仅列出一个显示。
sort -r 降序排序 -n 以数字排序(默认字符)
see
send
{}扩展
~$ a=d{a..c}g
$ echo $a
d{a..c}g