Shell 编程基础

获取脚本的参数

1
2
3
4
5
6
# $# 代表传入函数的参数个数
echo "$# 个参数"
# 位置参数 。从参数 0 开始
echo "$1 $2"
# $@ 代表所有参数的内容
echo "$@"

将脚本加上可执行权限:

1
chmod -x ./2.sh

以 > 改变标准输出:

1
2
# 以 < 改变标准输入(这条命令将会复制/tmp/a.txt 文件到/tmp/b.txt)
cat < /tmp/a.txt > /tmp/b.txt

以 >> 追加文件:

1
cat 2.sh >> 3.sh

文件描述符

理解文件描述符、系统文件表和内存索引节点表

  1. 文件描述符表
    用户区的一部分,除非通过使用文件描述符的函数,否则程序无法对其进行访问。对进程中每个打开的文件,文件描述符表都包含一个条目。

  2. 系统文件表
    为系统中所有的进程共享。对每个活动的 open, 它都包含一个条目。每个系统文件表的条目都包含文件偏移量、访问模式(读、写、或读-写)以及指向它的文件 描述符表的条目计数。每个进程的文件表在系统文件表中的区域都不重合。理由是,这种安排使每个进程都有 它自己的对该文件的当前偏移量。

  3. 内存索引节点表
    对系统中的每个活动的文件(被某个进程打开了),内存中索引节点表都包含一个条目。几个系统文件表条目可能对应于同一个内存索引节点表(不同进程打开同一个文件)。

习惯上,标准输入(Standard Input)的文件描述符是 0,标准输出(Standard Output)是 1, 标准错误(Standard Error)是 2。这也是当我们重定向标准错误时,使用(2>)的原因。

特殊文件的妙用

/dev/null

我们可以把/dev/null 想象为一个“黑洞”。它类似于一个只写文件。所有写入它的内容都不可读取。但是,对于命令行和脚本来说,/dev/null 却非常有用。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 读取/tmp/b.txt 文件,但是将读取的内容输出到/dev/null
cat /tmp/b.txt >/dev/null
# 检索/etc 下所有包含 alloy 字符串的文件行,但是如果有错误信息,则输出到/dev/null
grep "alloy" /etc/* 2> /dev/null
# 下面的命令不会产生任何输出
# 如果 b.txt 文件存在,则读取的内容输出到/dev/null
# 如果 b.txt 文件不存在,则错误的信息输出到/dev/null
cat /tmp/b.txt >/dev/null 2>/dev/null
# 这个命令和上一条命令是等效的
cat /tmp/b.txt &>/dev/null
# 清空 messages 和 wtmp 文件中的内容,但是让文件依然存在并且不改变权限
cat /dev/null > /var/log/messages
cat /dev/null > /var/log/wtmp

如果是重定向标准输出,直接使用>就可以了,或者也可以用(1>)表示,而如果是重新向标准错误,则用 2>。如果是标准输入呢?那就要用(0<)表示。而(&>)则代表标准输出和标准错误。

/dev/zero

类似于/dev/null,/dev/zero 也是一个伪文件,但事实上它会产生一个 null 流(二进制的 0 流,而不是 ASCII 类型)。如果你想把其他命令的输出写入/dev/zero 文件的话,那么写入的内容会消失,而且如果你想从/dev/zero 文件中读取一连串 null 的话,也非常的困难,/dev/zero 文件的主要用途就是用来创建一个指定长度,并且初始化为空的文件,这种文件一 般都用作临时交换文件。

/dev/tty

/dev/tty 是一个很实用的文件。当程序打开这个文件时,UNIX/Linux 会自动将它重定向到当
前所处的终端。输出到此的信息只会显示在当前工作的终端显示器上。在某些时候例如,设定了 脚本输出到/dev/null 时,而你又想在当前终端上显示一些很重要的信息,你就可以调用这个设备, 写入重要信息。这样做可以强制信息显示到终端。

1
2
3
4
5
6
7
printf“Enter new passwd:”               # 提示输入 
stty–echo # 关闭自动打印输入字符的功能
read pass < /dev/tty # 读取密码
printf“Enter again”
read pass2< /dev/tty # 再读一次,以便确认
stty echo # 记得重新打开自动打印输入字符功能
...

一切皆文件\

Linux 文件类型常见的有:普通文件、目录、字符设备文件、块设备文件、符号链接文件等。

  1. 普通文件
    我们用 ls-lh 来查看某个文件的属性,可以看到有类似-rw-r–r– ,值得注意的,它的第一个符 号是-,这样的文件在 Linux 中就是普通文件。这些文件一般是用一些相关的应用程序创建,例如 图像工具、文档工具、归档工具或 cp 工具等。这类文件的删除方式是用 rm 命令。

  2. 目录
    当我们在某个目录下执行命令,看到有类似 drwxr-xr-x 命令时,这样的文件就是目录,目录 在 Linux 是一个比较特殊的文件。注意,它的第一个字符是 d。创建目录可以用 mkdir 命令或 cp 命令。cp 可以把一个目录复制为另一个目录。删除目录用 rm 或 rmdir 命令。

  3. 字符设备或块设备文件
    如果进入/dev 目录,列一下文件,会看到类似如下的格式:

    1
    2
    3
    4
    5
    alloy@ubuntu:~/LinuxShell/ch2$ ls-la /dev/tty
    crw-rw-rw-1 root tty 5, 0 5 月 14 16:47 /dev/tty
    crw-rw-rw-1 root tty 5, 0 04-19 08:29 /dev/tty
    alloy@ubuntu:~/LinuxShell/ch2$ ls-la /dev/sda1
    brw-rw----1 root disk 8, 1 5 月 14 11:39 /dev/sda1

    看到 /dev/tty 的属性是 crw-rw-rw-。注意,前面第一个字符是 c,表示字符设备文件,如猫等串口设备。
    看到/dev/sda1 的属性是 brw-r—–。注意,前面的第一个字符是 b,表示块设备,如硬盘、光驱等设备。
    这种文件,是用 mknode 来创建,用 rm 来删除。目前,在最新的 Linux 发行版本中,一般不用自己来创建设备文件,因为这些文件是和内核是相关联的。

  4. 套接口文件
    当我们启动 MySQL 服务器时,会产生一个 mysql.sock 的文件。

    1
    alloy@ubuntu:~/LinuxShell/ch2$ ls-lh /var/lib/mysql/mysql.sock

    注意,这个文件的属性的第一个字符是 s。我们了解一下就行了。

  5. 符号链接文件

1
2
alloy@ubuntu:~/LinuxShell/ch2$ ls-lh setup.log
lrwxrwxrwx 1 root root 11 5月14 11:39 setup.log-> install.log

当我们查看文件属性时,会看到有类似 lrwxrwxrwx 的命令。注意,第一个字符是 l,这类文件是链接文件。是通过 ln-s 源文件产生新文件名。这和 Windows 操作系统中的快捷方式有点相似。

编程的基础元素

字符串操作符

  1. 替换运算符
变量运算符 替换
${varname:-word} 如果 varname 存在且非 null,则返回 varname 的值;否则,返回 word。用途:如果变量未定义,则返回默认值范例:如果 loginname 未定义,则${loginname:-ollir}的值为 ollir
${varname:=word} 如果 varname 存在且非 null,则返回 varname 的值;否则将其置为 word,然后返回其值。用途:如果变量未定义,则设置变量为默认值 word。范例:如果 loginname 未定义,则${loginname:-ollir}的值为 ollir,并且 loginname 被设 置为 ollir
${varname:?message} 如果 varname 存在且非 null,则返回 varname 的值;否则打印message,并退出当前脚 本。省如果省略 message 的话,Shell 返回 parameter null or not set。用途:用于捕捉由于变量未定义而导致的错误。范例:如果 loginname 未定义,则${loginname:”undefined!”}则显示 loginname:undefined!,然后退出
${varname:+word} 如果 varname 存在且非 null,则返回 word;否则返回 null。用途:用于测试变量存在。范例:如果 loginname 已定义,则${loginname:+1}返回 1
  1. 模式匹配运算符
变量运算符 替换
${varname#pattern} 如果模式匹配变量取值的开头处,则删除匹配的最短部分,并返回剩下部分。范例:${path#/*/}为 prince/desktop/long.file.name 这个范例删除了字符串开头/的部分
${varname##pattern} 如果模式匹配变量取值的开头处,则删除匹配的最长部分,并返回剩下部分。范例:${path#/*/}为 long.file.name这个范例提取了文件路径中的文件名
${varname%pattern} 如果模式匹配变量取值的结尾处,则删除匹配的最短部分,并返回剩下部分。范例:${path%.*}为/home/prince/desktop/long.file 这个范例去除文件路径中最后一个点号(.)之后的部分
${varname%%pattern} 如果模式匹配变量取值的结尾处,则删除匹配的最长部分,并返回剩下部分。范例:${path%.*}为/home/prince/desktop/long 这个范例去除范例中第一个点号(.)之后的部分
${varname/pattern/string} ${varname//pattern/string} 将 varname 中匹配模式的最长部分替换为 string。第一种格式中,只有匹配的第一部分 被替换;第二种格式中,varname 中所有匹配的部分都被替换。如果模式以#开头,则 必须匹配varname 的开头,如果模式以%开头,则必须匹配 varname 的结尾。如果 string 为空,匹配部分被删除。如果 varname 为@或*,操作被依次应用于每个位置参数 并且扩展为结果列表。范例:${path//prince/ollir}则为:/home/ollir/desktop/long.file.name 这个范例将字符串 prince 替换成 ollir

${varname//pattern/string} 例子:

1
2
# PATH 以换行符展示
echo ${PATH//:/'\n'}-e # -e 选项允许 echo 将\n 解释为一个 LINEFEED

位置变量

比较多得是 $n,$#,$0,$?。如例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh
# 判断运行参数个数,如果不等于 2,显示使用“用法帮助”,其中 $0 表示就是脚本自己。
# NOTE 用中括号做判断时 “[“ 后和 ”]” 前的空格是必须加的
if [ $#-ne 2 ] ;
then
echo "Usage: $0 string file";
exit 1;
fi
# 用 grep 在 $2 文件中查找 $1 字符串
grep $1 $2;

# 判断前一个命令运行后的返回值(一般成功都会返回 0, 失败都会返回非 0)
if [ $?-ne 0 ];
then
echo "Not Found \"$1\" in $2";
exit 1;
fi
# 如果没有成功则显示没找到相关信息,否则显示找到了。
# 其中 \" 表示转义,在 "" 里面还需要显示 " 号,则需要加上转义符 \"。
echo "found \"$1\" in $2";

Shell 内置了一个 shift 命令,shift 命令可以“截去”参数列表最左端的一个。执行了 shift 命 令后,$1 的值将永远丢失,而$2 的旧值会被赋值给$1,依此类推。

条件测试

字符串比较

操作符 如果…则为真
Str1 = str2 str1 匹配 str2
Str1 != str2 str1 不匹配 str2
Str1 < str2 str1 小于 str2
Str1 > str2 str1 大于 str2
-n str1 str1 为非 null(长度大于 0)
-z str1 str1 为 null(长度为 0)

文件属性检查

操作符 如果…则为真
-b file file 为块设备文件
-d file file 为目录
-e file file 存在
-f file file 为一般文件
-r file file 可读
-w file file 可写
-x file file 可执行
-s file file 非空
-O file 你是 file 的所有者
file1 -nt file2 file1 比 file2 新
file1 -ot file2 file1 比 file2 旧

case

语法如下:

1
2
3
4
5
6
7
8
9
case expression in
pattern1)
statements;;
pattern2)
statements;;
pattern3 | pattern4)
statements;;
...
esac

case 语句常常被用于对 单个参数有大量判断语句的情形。一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 判断文件后缀,然后根据文件后缀选择不同的读取方式。
case $1 in
*.jpg)
gqview $1;;
*.txt)
gvim $1;;
*.avi | *.wmv)
mplayer $1;;
*.pdf)
acroread $1;;
*)
echo $1: Don't know how to read this file;;
esac

for 循环

语法如下:

1
2
3
4
for name [in list]           # 遍历list中的所有对象
do
... # able to use $name,执行与$name相关的操作
done

一个例子:

1
2
3
4
5
# 遍历当前目录中所有 mp3 文件,mpg123 时命令行程序,播放mp3文件
for file in `find .-iname
do
mpg123 $file
done

注意,本例中 list 上的两个反单引号(``)。执行反单引号之间的命令,引用结果作为字符串。
在 for 循环中,如果 in list 被省略,则默认为 in “$@”,即命令行参数的引用列表。

while/until 循环

语法如下:

1
2
3
4
5
6
while condition
do
statements...
done
# 至于 until 语句,语法几乎和 while 一样:
# until condition

while 语句与 until 语句唯一不同的地方在于,如何判断 condition 的退出状态。在 while 语句中,当 condtion 的退出状态为真时,循环继续运行,否则退出循环。而在 until 中,当 condition 的退出状态为真时,循环退出,否则继续执行循环体。一个例子:

1
2
3
4
5
6
7
8
# 遍历 PATH 路径
path=$PATH: # 将$PATH 复制到一个参数 path 中,并在末尾加上一个冒号

while [ -n $path ]; # 当path不为空时
do
ls-ld ${path%%:*} # 我们使用ls-ld列出显示path中的第一个目录
path=${path#*:} # 在这里,我们截去 path 中的第一个目录和冒号
done

综合例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/bin/sh
author=false
list=false
file=""

while [ $#-gt 0 ]
do
case $1 in
-f)
file=$2 # 将 -f 参数的下一个参数(file)获取至 file 变量 #截去下一个参数
shift
;;
-l)
list=true
;;
-a)
author=true
;;
--) # 传统上,以 -- 结束选项
shift
break
;;
-*)
echo $0: $1: unrecognized option
;;
*)
break # 无选项参数时,在循环中跳出
;;
esac
shift # 参数偏移
done

在 Shell 中,有 getopt 命令,可以简化选项处理。使用 getopt 重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh
author=false
list=false
file=""

# 它的第一个参数是一个字符串,每个字符是命令的一个选项。如果参数后还需要跟其 他参数,则该字符后面接一个冒号(:),而紧跟的参数则会放入 $OPTARG 变量中。
while getopt alf: opt
do
case $opt in
f) # 将 -f 参数的下一个参数(file)获取至 file 变量 #截去下一个参数
file=$OPTARG
;;
l)
list=true
;;
a)
author=true
;;
esac
done
shift ${{OPTIND–1}} #删除选项,留下参数,变量 OPTIND 包含下一个要处理的参数的索引值。Shell 会 把它初始化为 1

可以明显看出简化了很多。首先,在 case 中对 $opt 的测试仅仅是字母,开头的 - 被去除了;然后,循环中的 shift 也被 getopt 处理了,不需要自己控制;再次,– 的 case 也不见了,getopt 自动处理;最后,针对不合法选项的处理默认下 getopt 也会显示错误信息。

正则表达式

一般字符

一般字符包括文字和数字字符、空白字符和 标点符号字符。一般字符匹配的就是它们自身。

转义的 meta 字符

当 meta 字符无法表示自己而我们需要这些字符时,转义符号的作用就体现出来了:在字符前置一个反斜杠 ()。例如,.只表示一个点,而不是任意字符;[匹配左方括号,而\表示反斜杠本身。如果将转义字符置于一般字符前,则转义字符会被忽略。

.(点号)字符

.(点号)字符 点号字符表示“任一字符”。例如,”.hina”正则表达式匹配 china,也匹配 China,但是它也同时匹配 dhina

方括号表达式

例如,[cC]hina 只匹配 china 和 China。这是最简单的方括号表达式的用法,即直接将字符列表置于方括号中。如果将^符号至于方括号的开头([^abc]),就是取反的意思。即不在方括号中出现的任意字符。例如,[^abd]hina 匹配除了 abd 三个小写字母外的任意字母,加上 hina。

星号 meta 字符的应用

ab*c 正则表达式匹配如下字符串:ac,abc,abbc,abbbc…你一定看出来了,星号 meta 字符匹配零个或多 个星号前面的单个字符。注意,匹配零个或多个字符并不是任意字母,例如,ab*c 就不匹配 adc。

a.*c 当点号和星号一起用时是表示字母 a 和 c 中匹配任意长度的字符串,例如,ac, abc, adc, abbc, acccc 等。

a.c 它的含义是字母 a 和字母 c 之间匹配任意一个字母,但是只能是一个,不能多也不能少。例如,acc, abc, aac, a!c等。

区间表达式的应用

ab{3}c a 字母和 c 字母之间的 b 字母重现 3 次,即,ab{3}c 正则表达式匹配 abbbc。

ab{3,}c a 字母和 c 字母之间的 b 字母重现至少 3 次,即,ab{3}c 正则表达式匹配 abbbc,abbbbc, abbbbbc…

ab{3,5}c a 字母和 c 字母之间的 b 字母重现 3~5 次,即,ab{3}c 正则表达式匹配 abbbc,abbbbc, abbbbbc。

ab?c 只匹配两种:ac 和 abc。

ab+c 匹配 abc,abbc,abbbc。。。但不匹配 ac。

^abc 匹配字符串开头的 3 个字母 abc,例如,abcxxxABCabcxxxefg。

efg$ 匹配结尾处的 efg。和开头一样,$符号锚定了字符串的结尾,即 abcxxxABCabcxxxefg。

如果将字符^和$一起使用,则两者之间的正则表达式就匹配了整个或整行正则表达式。有时 我们使用^$来匹配空的字符串或者空行。