bash study

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.txtb.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]可以匹配ab,前提是确实存在相应的文件。

方括号扩展属于文件名匹配,即扩展后的结果必须符合现有的文件路径。如果不存在匹配,就会保持原样,不进行扩展。

# 不存在文件 a.txt 和 b.txt
$ ls [ab].txt
ls: 无法访问'[ab].txt': 没有那个文件或目录

上面例子中,由于扩展后的文件不存在,[ab].txt就原样输出了,导致ls命名报错。

方括号扩展还有两种变体:[^...][!...]。它们表示匹配不在方括号里面的字符,这两种写法是等价的。比如,[^abc][!abc]表示匹配除了abc以外的字符。

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

上面命令中,[!a]表示文件名第二个字符不是a的文件名,所以返回了ababbb两个文件。

注意,如果需要匹配[字符,可以放在方括号内,比如[[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]*:所有以abc字符之一开头的文件名。
  • 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.txtb.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 变量名区分大小写,HOMEhome是两个不同的变量。

查看单个环境变量的值,可以使用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}的变量的设置默认值语法混淆。这时还可以指定lengthlength可以是正值,也可以是负值。

查找子字符串

查找字符 io 的位置(哪个字母先出现就计算哪个):

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#numberbase进制的数。

下面是一些例子。

$ 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是一个变量名,用来保存当前取到的配置项参数,即lha

#!/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))"

变量$OPTINDgetopts开始执行前是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

ifthen写在同一行时,需要分号分隔。分号是 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 ]:如果string1string2相同,则判断为真。
  • [ string1 == string2 ] 等同于[ string1 = string2 ]
  • [ string1 != string2 ]:如果string1string2不相同,则判断为真。
  • [ 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结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elifif结构等价,但是语义更好。它的语法如下。

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

匹配语句的模式是*,这个通配符可以匹配其他字符和没有输入字符的情况,类似ifelse部分。

#!/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):匹配ab
  • [[: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
  1. select生成一个菜单,内容是列表list的每一项,并且每一项前面还有一个数字编号。
  2. Bash 提示用户选择一项,输入它的编号。
  3. 用户输入以后,Bash 会将该项的内容存在变量name,该项的编号存入环境变量REPLY。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。
  4. 执行命令体commands
  5. 执行结束后,回到第一步,重复这个过程。
#!/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、减号-)

命令执行完会在屏幕上打印相应的数据,但这些数据可能要经过几道手续之后才能成为我们所想要的格式。而中间经过的这几道手续就与我们所要用的管道命令有关了。

管道命令与“连续执行命令”是不一样的!!

管道命令使用的界定符号是“|”

管道命令的要求

  1. 管道命令仅能处理经由前面一个命令传来的正确信息,也就是standard output的信息,对于standard error并没有直接处理的能力。
  2. 在每个管道后面接的第一个数据必定是“命令”,而且这个命令必须要能够接收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
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值