Linux Shell 脚本编程入门
一、 Shell 基础知识
1. 登录Shell
(1)UNIX系统在逻辑上被划分为两个不同的部分:内核和实用工具(Utility)。Shell属于实用工具,它作为登录过程的一部分被载入内存中。
(2)本地终端登录
① UNIX系统启动,init程序会在每个终端端口自动启动一个getty程序,getty是一个设备驱动程序,它调用login程序完成登录过程
注:对于系统上的每个物理终端,都会激活一个getty程序
② login会对比文件/etc/passwd中相应的条目来验证登录名和密码,每个用户在该文件都有条目,包含登录名、主目录及登录后要启动的程序(使用哪种Shell)。
root:x:0:0:root:/root:/bin/bash kevin:x:1000:1000:Kevin,,,:/home/kevin:/bin/bash
注:如果最后一个冒号之后没有内容,则使用标准Shell:/bin/sh
(3)远程系统登录
① 远程登录工具:ssh、telnet、rlogin
② init程序会针对网络连接运行类似于getty的程序(如,sshd、telnetd、tlogind会响应来自ssh、telnet和rlogin的连接请求)
③ sshd、telnetd、tlogind会将用户的shell连接到伪终端上
(4)Shell执行命令过程
① 搜索磁盘,查找环境变量PATH中指定的所有目录,知道找到相应程序(命令)
② Shell将自己复制一份(生成子Shell),用相应程序 (命令)替换,自己进入休眠,等待子Shell执行
③ 当命令执行完毕,子shell从内存小时,控制权又交给登录Shell,等待输入下条命令
注1:Shell的命令提示符,普通用户一般为$,root用户为#
注2:Shell只是一个程序
(5)Shell的命令行:每次输入一行,Shell就会分析该行,决定执行什么操作
program-name arguments
注:Shell使用一些特殊字符来确定程序名称及每个参数的起止,这些字符叫做空白字符,包括:空格符、水平制表符合行尾符(换行符)
(6)Shell中的变量:使用变量时候前面需要加$
a=10 echo "$a"
(7)Shell命令行中文件名替换(Shell Pattern Matching)
(*):匹配零个或多个字符
(?):匹配任意单个字符(正则表达式中使用(.)实现相同的效果)
[...]:匹配包含在中括号中的任意字符
# ls [T]* Test1.txt Test.sh Test.txt
注:Pattern Matching 与 Regular Expression(正则表达式)不同,不要搞混
2. 正则表达式(Shell Regular Expression)
① (.): 匹配任意单个字符
② (^): 匹配行首
③ ($): 匹配行尾
④ [...]: 匹配字符组
[0-9]: 匹配0到9的数字
[A-Za-z]: 匹配所有大小写字母
[^0-9]: 匹配除了数字的字符
注: [...]是匹配单个字符
⑤ (*): 匹配零次或多次出现在其之前的正则表达式元素
X*:匹配0个或者多个X
XX*:匹配1个或者多个X
⑥ \{...\}: 匹配固定字数的子模式
X\{1-5\}:匹配1-5个X
X\{3\}:匹配3个X
⑦ \(...\): 保存已匹配的字符
^\(.\):匹配行首的第一个单词
^\(.\)\1:匹配行首的两个相同的单词
注:\(.\)的作用是匹配到1个任意字符,并把它放入寄存器,引用使用\n(n的取值为1-9)
注:{}和()都是特殊字符,需要用\转义
3. Shell常用工具:cut、 paste、 sed、 tr、 grep、 uniq、 sort
(1)cut: 从数据文件或命令中提取各种字段
① 一般用法:
cut -cchars file
② 常用选项
-c:以字符为单位提取字段
-b:以字节为单位提取字段
-d:指定分隔符
-f:指定字段
--complement:补偿输出,输出指定字段以外的字段
--output-delimiter=STRING:用STRING作为输出的分隔符,默认使用输入的分隔符
③ 示例
cut -c5- Test.txt 提取每行第5个到行尾的字符
cut -c1-8 Test.txt 提取每行第1-8个字符
cut -d: -f1,2 /etc/passwd 以':'为分隔符提取第1,2段字符,默认是制表符'\t'作为分隔符
cut -f1 --output-delimiter=: --complement ./PhoneBook.txt
提取每行除了第1字段的字符,并以':'作为分隔符输出
注:sed -n l file 可以查看文件的特殊字符
(2)paste: 可以将多个文件按行合并,也可以把一个文件的多行合并为一行
① 一般用法:
paste files
② 常用选项:
-d(--delimiters=LIST):指定分隔符
-s(--serial):只粘贴一个文件,而不是多个文件并行粘贴
③ 示例
paste -d'\t' PhoneBook.txt Address.txt 两个文件以'\t'为分隔符按行合并
paste -d"+-" PhoneBook.txt Address.txt Age.txt
第1,2个文件以'+'为分隔符,第2,3个文件以'-'为分隔符,按行合并
paste -s PhoneBook.txt
将一个文件的多行合并为一行
ls | paste -d' ' -s -
将当前目录的文件名输出(等效于:echo *)
注:'-'表示来自标准输入
(3)sed:用来在管道或命令序列中编辑数据,全称为stream editor
① 一般用法:sed command file
注:sed会将指定的命令应用在输入的每一行上,并将结果写入标准输出
② 示例
sed 's/Kevin/KEVIN/g' PhoneBook.txt
替换PhoneBook.txt中的字符串"Kevin"为"KEVIN"(s为替换命令,g为全局选项) who | sed 's/ .*$//g'
删除第一个空格之后的内容(等价于命令:who | cut -c1-8)
sed -n '1,2p' PhoneBook.txt
只打印1,2行(-n告诉sed默认不打印任何行)
sed -n '/Kevin/p' PhoneBook.txt
只打印包含"Kevin"的行 sed -n 'l' PhoneBook.txt
打印PhoneBook.txt中所有的行,将不可打印的字符显示为\nn(nn是字符的8进制值),制表符显示为\t
sed '1,2d' PhoneBook.txt
删除PhoneBook.txt的第1,2行
sed '/Kevin/d' PhoneBook.txt
删除PhoneBook.txt中所有包含Kevin的行
(4)tr:转换标准输入中的字符
① 一般用法:
tr from-chars to-chars
注:from-chars 和 to-chars可以是一个或多个字符,也可以是字符组
② 常用选项
-s:“压缩”(squeeze)to-chars中连续出现的字符,将多个连续的字符替换成1个
-d:删除输入流中某一字符
③ 示例
tr K k < PhoneBook.txt
将PhoneBook.txt中的K替换为k tr Kevin KEVIN < PhoneBook.txt 字符依次对应替换
tr [a-z] [A-Z] < PhoneBook.txt 小写字母替换为大写字母
date | tr ' ' '\12' tr [a-zA-Z] [A-Za-z] < Test.txt tr -s X x < Test.txt tr -d [0-9] < Test.txt tr -d ' ' < Test.txt
(5)grep:在一个或多个文件中搜索指定的模式
① 一般用法
grep pattern files
② 常用参数
-i:忽略大小写
-v:获得不匹配的行
-l:只打印包含内容的文件名
-n:匹配的每一行前加行号
③ 示例
grep Kevin PhoneBook.txt grep [A-Z]...[a-z] ./Test.txt grep -i kevin PhoneBook.txt grep -l main *.c grep -in kevin PhoneBook.txt grep -in main *.c
(6)sort:提取指定输入文件每一行,并按升序排序
① 一般用法
sort file
② 常用选项
-u:消除输出中重复的行(--unique)
-r:逆序排列(--reverse)
-o:指定输出文件(--output=FILE)
-n:将文件中的字段视为数字,然后排序(--numeric-sort)
-k:从指定字段开始排序(--key=KEYDEF)
-k2n:从第二字段开始排序
-t:指定分隔符(--field-separator=SEP)(默认以空格或制表符作为分隔符)
③ 示例
sort -n Data.txt sort -k2n Data.txt sort -k3n -t: /etc/passwd
(7)uniq:查找或删除文件中的重复行
① 一般用法
uniq in_file out_file
② 常用选项
-d:列出重复行(--repeated )
-c:统计出现的次数(--count )
-u:打印没有重复的行(--unique)
③ 示例
二、 Shell 脚本编程
1. 脚本是一个包含一系列命令序列的文本文件,当运行这个脚本文件时,文件中包含的命令序列将得到执行。
(1) 脚本主要由两部分组成:脚本解释器和命令序列
#!/bin/bash
echo Hello World
注:#!/bin/bash 指明脚本解释器为Bash Shell
2. Shell脚本允许用户设置和使用自己的变量,用户无需指定其类型,也无需在使用前定义。
(1)变量名以字母或下划线(_)开头,后面可以跟上0个或多个字母、数字字符或下划线(_)
(2)定义时无需加"$",使用时需要加"$"
(3)赋值“=”左右不能有空格
(4)可以使用"{}"隔离其他字符:${PATH}X
注:只有变量名最后一个字符后面跟的是字母、数字或下划线的时候,才需要使用"{}"隔离
(5)注释用“#”
(6)文件名替换与变量
x=*
echo $x
在执行echo $x时,严格的操作步骤如下
① Shell扫描命令行,将x替换为*
② Shell重新扫描命令行,遇到*,使用当前目录的所有文件名替换*
③ Shell执行ehco,将文件列表作为参数传入
注:Shell先进行变量替换,然后是文件名替换,接着将命令行解析
3. 所有现代UNIX和Linux中所包含的POSIX标准Shell提供了一种叫做算术扩展(arithmetic expansion)的机制
(1)算术扩展格式:
$((expression))
注1:expression是包含shell变量和操作符的算术表达式
注2:双括号内a前可以不加$,因为Shell知道出现在算术扩展中的有效元素只有操作符、数字和变量
(2)示例(计算从1加到100的值)
#!/bin/bash
#for a in `seq 1 100`
for a in {1..100}
do
result=$((result + a))
done
echo $result
注1:$(())支持的操作符包括,基本的6种:+、-、*、/、%、 **(乘方符),复杂记法:+= 、-=、 *=、 /=,以及自增(variable++)和自减(variable--)
注2:$(())可以处理不同的进制,甚至在进制之间进行转换,例如
echo $((8#100)) echo $((2#10101100))
注3:双括号内部的空格是可选的
4. Shell中的引用
(1)Shell能够识别四种不同的引用字符
① 单引号 ': 告诉Shell忽略引用的所有特殊字符
a=10 echo '$a' output:$a
② 双引号 ": 忽略大部分特殊字符,除了美元符号$、反引号`、反斜线\
a=10 echo "$a" output: 10
③ 反斜线 \: 可以对紧随其后的字符进行转义(反斜线相当于在单个字符两侧放置单引号)
a=10 echo \$a output: $a
④ 反引号 ':告诉Shell将其中的命令使用命令输出代替 ($(...)有相同的功能)
echo `pwd`
echo $(pwd)
(2)所有现代的UNIX、Linux以及其他POSIX兼容的Shell都支持一种更新、更可取的命令替换写法:$(...)
① 方便阅读
② 易于嵌套
#/SambaShare/Test/Test.c ===>> ^SambaShare^Test^Test.c
fileName=/SambaShare/Test/Test.c fileName=$(echo $fileName | tr "$(echo $fileName | cut -c1)" "^") echo $fileName output: ^SambaShare^Test^Test.c
(3)老一代的Shell使用数学等式解算器(mathematical equation solver)expr进行整数运算
① expr并不删除解析等式,所以操作数和操作符之间必须以空格分离
expr 1 + 2
② expr能够识别常用的算术操作符:+、-、*、/、%
expr "17 * 6" expr 17 \* 6
③ 类似于Shell内建的算术功能,expr只能够求值整数算术表达式(浮点数运算可以使用awk或bc)
i=1 i=$(expr $i + 1) echo $i
④ expr其他操作符用的最多的是:操作符,以第二个操作数中的正则表达式匹配第一个操作数,返回匹配到的字符个数
expr "$file" : ".*"
5. 同C程序一样,Shell脚本也可以使用命令行参数
(1)$#:传入脚本的命令行参数个数
(2)$*:所有命令行参数值,在各个参数值之间留有空格
(3)$0:第0个命令行参数,命令行本身(Shell文件名)
(4)$1:第1个命令行参数
(5)${n}:第n个命令行参数(访问第10个及以后的参数需要加"{}",例如${10})
(6)shift命令:可以向左移动位置参数
#!/bin/bash shift echo $# echo $* echo $1 echo $2 echo ${10}
6. 条件语句
(1)if 条件语句可以测试某种条件,然后根据测试结果改变程序执行流程
① 一般格式:
if command(test) then command command ... fi
② 示例
#!/bin/bash user="$1" if test "$user" = "root" then echo "$user is logged on" fi
(2)程序执行完成,会向Shell提供一个返回状态码,为0表示运行成功,非0表示运行失败,这个状态码保存在变量$?中
who | grep root echo $? who | grep kevin echo $?
(3)test命令
① 一般格式:
test expression
② 功能:test会对expression求值,如果为真,返回0;如果为假,返回非0
#!/bin/bash user="$1" if test "$user" = "root" then echo "$user is logged on" fi
注1:操作符"="用来测试两个值是否一样
注2:test命令操作符和操作数必须是独立的参数,它们彼此之间必须有一个或多个空白字符分隔
③ test另一种格式
[ expression ]
#!/bin/bash user="$1" if [ "$user" = "root" ] then echo "$user is logged on" fi
注1:"[" 、"]"两侧都必须加空格
注2:"[" 、"]"其实也是命令名,命令名不一定非要是字母或数字
④ 字符串操作符
操作符 | 如果满足下列条件,则返回真(退出状态码为0) |
string1 = string2 | string1 等于 string2 |
string1 != string2 | string1 不等于 string2 |
string | string 不为空 |
-n string | string 不为空(test必须能欧识别出作为参数的string,最好在两边加上双引号) (nonzero) |
-z string | string 为空(test必须能欧识别出作为参数的string,最好在两边加上双引号) (zero) |
#!/bin/bash user="$1" if [ -n "$user" ] then if [ "$user" = "root" ] then echo "$user is logged on" fi else echo "Usage: ./Test.sh param" fi
⑤ 整数操作符
操作符 | 如果满足下列条件,则返回真(退出状态码为0) |
int1 -eq int2 | int1 等于 int2 (equal) |
int1 -ge int2 | int1 大于或等于 int2 (greater than or equal ) |
int1 -gt int2 | int1 大于 int2 (greater than) |
int1 -le int2 | int1 小于或等于 int2 (less than or equal) |
int1 -lt int2 | int1 小于 int2 (less than) |
int1 -ne int2 | int1 不等于 int2 (not equal) |
#!/bin/bash Test1="005" if [ "$Test1" = 5 ] then echo "equal" else echo "no equal" fi if [ "$Test1" -eq 5 ] then echo "equal" else echo "no equal" fi output: no equal equal
注:在使用整数操作符时,将变量视为整数的是test命令,而非Shell。所以操作符=是字符串比较,而"-eq"是数值比较
⑥ 文件操作符
操作符 | 如果满足下列条件,则返回真(退出状态码为0) |
-d file | file是一个目录 |
-e file | file存在 |
-f file | file是一个普通文件 |
-r file | file可由进程读取 |
-s file | file不是空文件 |
-w file | file可由进程写入 |
-x file | file是可执行的 |
-L file | file是一个符号链接 |
#!/bin/bash if [ -f "/SambaShare/TestShell/Test.c" ] then echo "Normal file" else echo "Not a normal file" fi
⑦ 逻辑否定操作符 !:可以放置在任意的test表达式之前,否定该表达式的求值结果
[ ! -f "/SambaShare/TestShell/Test.c" ] [ ! "$x1" = "$x2"]
⑧ 逻辑“与”操作符 -a : 操作符 -a 在两个表达式之间执行“与”运算,仅当这两个表达式都为真时,才返回真(返回状态码0)
[ ! -f "$file" -a $(who > $file) ]
注:操作符 -a 符合短路规则,当前面的表达式不为真时,则立即返回,不会计算后面的表达式
⑨ 括号:可以在test表达式中使用括号"()"改变求值顺序
[ \( "$count" -gt 0 \) -a \( "$count" -lt 10 \) ] ====> (0 < count < 10)
注1:括号"()"对Shell有特殊意义,必须转义
注2:括号"()"两边必须要有空格,因为test要求条件语句中的每一个元素都是独立的参数
⑩ 逻辑“或”操作符 -o:两个表达式中只要其中一个为真,表达式就为真
[ -n "$mailopt" -o -r $HOME/mailfile ]
注1:操作符-o符合短路规则,如果前面表达式为真,后面将不会执行
注2:操作符-o的优先级比-a的低
"$a" -eq 0 -o "$b" -eq 2 -a "$c" -eq 10 <====> "$a" -eq 0 -o \( "$b" -eq 2 -a "$c" -eq 10 \)
(4)else语句
① 一般格式
if command(test) then command command ... else command command ... fi
② 简洁解释方式:
if condition then statements-if-true else statements-if-false fi
③ 示例
#!/bin/bash user=$1 if who | grep "^$user " > /dev/null then echo "$user is logged on" else echo "$user is not logged on" fi
(5)Shell内建的 exit 命令可以立即终止Shell程序的执行,其一般格式为
exit n
注:n为要返回的退出状态码,如果没有指定,返回exit之前那条命令的退出状态码(exit $?)
(6)elif一般格式
if command(test1) then command ... elif command(test2) then command ... else command ... fi
注:elif <====> else if condition
(7)case命令
① case一般格式
case value in pattern1) command ... command;; pattern2) command ... command;;
... esac
② case命令中的pattern属于通配符(Shell Pattern Matching)(不要与正则表达式(Shell Regular expression)搞混)
?:指定任意单个字符
*:指定零个或多个字符
[...]:指定中括号中出现的任意单个字符
③ 示例
#!/bin/bash if [ $# -ne 1 ] then echo Usage: CType char exit 1 fi char=$1 case "$char" in [0-9] ) echo Digit;; [a-z] ) echo Lowercase Letter;; [A-Z] ) echo Uppercase Letter;; ? ) echo Special Charcter;; * ) echo Please input a single character;; esac
④ 调试选项-x:在执行命令前加上sh -x(或者bash -x)可以跟踪执行过程
bash -x ./Greetings.sh
⑤ 符号 | 用于两个模式之间,效果等同于逻辑“或”
#!/bin/bash hour=$(date +%H) case "$hour" in 0? | 1[01] ) echo Good morning;; 1[2-7] ) echo Good atfternoon;; 1[89] | 2[0-3] ) echo Good evening;; * ) echo Invalid parameter esac
(8)空命令":":如果case对应的分支什么都不做,可以利用Shell内置的空命令(Null command)来实现
(9)&&和||
① command1 && command2:如果command1的退出状态码为0,则执行第二条命令;否则直接退出而不执行第二条命令
[ -z "$Editor" ] && Editor=/bin/ed
② command1 || command2:如果command1的退出状态码不为0(为假),则执行第二条命令;否则直接退出而不执行第二条命令
grep "$name" phonebook || echo "Couldn't find $name"
③ && 和 || 可以用在if中,效果类似于逻辑“与”和逻辑“或”
#!/bin/bash index=$1
#if pwd || touch Test"$index" if pwd && touch Test"$index" then echo "true" else echo "false" fi
7. 循环:for、 while、 until
(1)for命令
① 基本格式
for var in word1 word2 ... word n do command command ... done
② 示例
#!/bin/bash for index in 1 2 3 do echo $index done
③ 不使用列表的for命令
for var do command command ... done
注1:等价于for var in $@
注2:$@也是所有参数列表,与$*有细微差异
$@:"1" "2" "3"
$*:“1 2 3”
(2)while命令
① 基本格式
while command(test) do command command
... done
② 示例
#!/bin/bash i=1 while [ "$i" -le 5 ] do echo $i i=$((i+1)) done
③ while循环经常和shift命令搭配使用,用于处理命令行中数量不定的参数
#!/bin/bash while [ "$#" -gt 0 ] do echo "$1" shift done
(3)until命令:直到测试条件为真才会停止执行(与while相反)
① 一般格式
until command(test) do command command ... done
② 示例:until命令适合编写那种需要等待特定时间发生的程序
#!/bin/bash until cat ./Test.txt | grep "Kevin" > /dev/null do sleep 3 done echo "End"
(4)break命令:跳出循环
break n
注:n表示跳出n层循环
#!/bin/bash param=$1 count=1 for file in {1..10} do echo "$file" echo "$param" while [ $count -lt 10 ] do if [ -n "$param" ] then echo "parame is not null" break 2 else echo "param is null" fi done done echo "End"
(5)continue命令:跳过循环中余下的命令
continue n
注:n表示跳过最内侧n个循环的命令
#!/bin/bash param=$1 for file in {1..5} do echo "for circle start, $file" while true do echo " while circle start" if [ "$param" = "continue" ] then continue 2 else break 2 fi echo " while circle end" done echo "for circle end, $file" done echo "End"
(6)在后台执行循环
for var in 2 3 4 > do > echo $var > done &
(7)循环上的I/O重定向
for var in 2 3 4 > do > echo $var > done > Test10.txt
(8)将数据导入及导出循环
for var in 2 3 4 > do > echo $var > done | wc -l
(9)单行循环
for var in 2 3 4; do echo $var; done
(10)getopts命令:处理命令行参数
①一般格式:
getopts options variable
② 示例
#!/bin/bash while getopts mt: option do case "$option" in m ) echo "-m";; t ) echo "-t $OPTARG";; esac done
③ getopts 命令详解
* 专门用于循环中执行
* 每次循环,getopts都会检查下一个命令行参数,是否以 “-“ 开头,且是否在options指定的字符中
* 如果没问题,getopts会把匹配到的选项放入 variable 中,返回退出码0
* 如果 “-” 之后的字符没有在options中,将?放入varibale中,返回退出码0(此时会向标准错误输出报错信息)
④ 特殊变量
* OPTIND:存放下一个要处理的命令行参数的序号(初始值为1,序号为在整个命令行列表中的序号)
* OPTARG:存放带参数命令行选项的参数
(11) 综合示例
#!/bin/sh -e while [ $# -gt 0 ] ; do echo "----argv[1] = $1" case "$1" in --) shift ; break ;; -a) shift ; APPEND=yes ;; -n) shift ; BOARD_NAME=$1 ; shift ;; *) break ;; esac echo "argv[1] = $1" done echo "APPEND=${APPEND}" echo "BOARD_NAME=${BOARD_NAME}"
8. 数据的读取及打印
(1)read 命令
① 一般形式
read variables
② 命令解析:执行read命令时,Shell会从标准输入读取一行,将第一个单词分配给variables中第一个变量,第二个单词分配给第二个变量,以此类推(如果命令行单词数量多于变量,则多出的单词赋值给最后一个变量)
③ 示例
read x y 10 20 30
echo $x 10echo $y 20 30
(2)变量$$和临时文件($$储存PID)
① 两个重要概念
* 计算机实现多任务的方式是在处理器上来回切换程序(将当前进程的寄存器保存在内存中,然后将要切换进程的寄存器值从内存加载到寄存器)
* 多个线程访问同一个资源时会产生竞争(需要加金城所锁来避免竞争)
② 同名临时文件被多个线程创建或访问时会产生竞争,简单的解决方案是:在临时文件名上加当前进程的ID
grep -v "$name" phonebook > /tmp/phonebook$$
(3)read的退出状态:除非是碰到了文件结尾(end-of-file condition),read都会返回为0的退出状态
#!/bin/bash while read n1 n2 do echo $(($n1 + $n2)) done
(4)printf命令:格式化输出
① 一般格式
printf "format" arg1 arg2 ...
② printf的格式规范字符
字符 | 功能 |
%d | 整数 |
%u | 无符号数 |
%o | 八进制数 |
%x | 十六进制数,使用a~f |
%X | 十六进制数,使用A~F |
%c | 单个字符 |
%s | 字符串字面量 |
%b | 包含转义字符的字符串 |
%% | 百分号 |
printf "Hello world" printf "a = 0x%x\n" 10
(5)转换规范的一般格式
%[flags][width][.precision]type
① 有效的flags
“-”: 将输出的数值左对齐(默认是右对齐)
“+”: 使得printf在整数前面加上+或-号(默认只有负数才输出符号)
“#”: 使得printf在八进制数前加上0,在十六进制数前加上0x或0X(分别使用%#x或%#X来指定)
“ ”: 空格使得printf在整数前面加上一个空格,在负数前面加上-,以起到对齐的作用
# printf "%-5d\n" 12 12 # printf "%5d\n" 12 12 # printf "%d\n" 12 12 # printf "%+d\n" 12 +12 # printf "%#x\n" 18 0x12 # printf "%#o\n" 0x12 022 # printf "% d\n" 12 12 # printf "% d\n" -12 -12
② 修饰符width是一个整数,用来指定输出参数时的最小字段宽度(对应的参数使用右对齐的形式,除非使用-)
# printf "%5d\n" 12 12 # printf "%-5d\n" 12 12
③ 修饰符.precision是一个正数,
* 对于%d %u %o %x及%X所显示出的最小数位个数(如果不足,前面用0填充,而width是用空格填充)
* 对于字符串,指定要显示出的字符串最大字符数(如果字符串长度大于precision,右边的会被截断)
* 对于浮点数,指定要显示的小数位个数,不足的补0,多的去除
# printf "%.4s\n" Hello Hell # printf "%5.4s\n" Hello Hell # printf "%5.4d\n" 1220 1220 # printf "%5.4d\n" 122 0122 # printf "%5.4d\n" 12 0012 # printf "%5.8d\n" 12 00000012
# printf "%.4f\n" 12.23
12.2300
# printf "%.1f\n" 12.23
12.2
④ 如果将width和precision中的数字替换为*,那么待显示的值之前的参数必须是一个数字,该数字分别用作宽度和精度
# printf "%*d\n" 5 12 12 # printf "%*.*d\n" 5 3 12 012
⑤ printf格式规范修饰符总结
修饰符 | 含义 |
Flags | |
- | 左对齐值 |
+ | 在整数前面加上+或- |
(space) | 在正数前面加上空格 |
# | 在八进制数前加上0,在十六进制数前加上0x或0X |
width | 字段最小宽度;*表示使用下一个参数作为宽度 |
precision | 显示整数时使用的最小位数;显示字符串时使用的最大字符数;*表示使用下一个参数作为精度 |
#!/bin/bash # Align cat
cat $* | while read number1 number2 do printf "%12d %12d\n" $number1 $number2 done
9. 环境
(1)登录Shell和子Shell
① 当你登录到一个系统时,得到一套登录Shell的副本,这个登录Shell维护着你所处的环境--一套每个用户各不相同的配置(环境变量)
② 当登录Shell执行某一命令时,它会启动一个新的Shell来执行该程序,即子Shell
③ 登录Shell和子Shell有各自一套的环境变量(局部变量)
(2)导出变量
① 使用export命令可以将变量导出
export variables
② 当变量被导出后,它会在之后的子Shell中保持导出状态
③ 修改父Shell中的导出变量的值会影响到子Shell中的值,而在子Shell中无法修改父Shell中的值(父Shell给子Shell一个导出变量的副本,而不是共用一个导出变量)
④ 打印所有导出变量的值:
export -p
(3)Shell中常用的环境变量(导出变量)
① PS1:命令提示符
② PS2:辅助提示符
③ HOME:主目录,用户登录系统时所处位置
④ PATH:命令搜索目录(从前往后)
⑤ CDPATH:cd命令默认搜索目录
(4)再谈子Shell
① . 命令(dot命令):在当前Shell中执行file内容
. file
② 创建一个子Shell:/bin/bash
#!/bin/bash Test="Diags Env" export Test echo :$Test: /bin/bash
③ exec命令:将当前进程替换为另一个程序的进程(PID不变)
exec program exec < infile exec > report
④ (...)和(...;)
(...):命令组,在子Shell中执行
{ ...; }:命令组,在当前Shell中执行
#!/bin/bash x=50 (x=100; ls) echo $x { x=20; pwd; } echo $x
注1:多条命令以 ";" 隔开
注2:{}中的命令都写在一行,做花括号后一定要有空格,且最后一条命令后必须加 ";"
⑤ 另一种将变量传递给子Shell的方法:在命令行上,把一个或多个变量赋值放到命令行前
# x=10 y=100 ./Test.sh
(5).profile文件
① 登录Shell会在系统中查找并读取两个特殊文件
/etc/profile:由系统管理员所设置
~/.profile:用户命令行环境配置
(6)TERM变量:指定终端
(7)TZ变量:当前时区(date命令和一些C标准库函数使用TZ变量决定当前时区)
10. 再谈参数
(1)参数包括传递给程序的参数(位置参数)、特殊的Shell变量($#、$?)及普通变量(关键字参数)
# a=10 b=100 ./Test.sh x y
注:x、y 为位置参数,a、b 为关键字参数
(2)参数替换:参数替换最简单的形式是在参数前加上美元符号
① ${}:如果参数名后的字符可能会造成名字冲突,可以把参数名放进花括号内
② ${parameter:-value}:如果parameter不为空,则使用它的值;否则就是用value
示例1:echo Using editor ${EDITOR:-/bin/vim}
其效果等同于
if [ -n "$EDITOR" ] then echo Using editor $EDITOR eles echo Using editor /bin/vim fi
示例2:${EDITOR:-/bin/bim} /tmp/edfile
注:这种写法并不会改变变量的值,如果EDITOR之前为空,执行完语句之后依然为空
③ ${parameter:=value}:如果parameter为空,不仅会使用value,还会将value分配给parameter(不能使用这种方式给位置变量赋值,因为parameter不能是数字)
典型用法:测试某个导出变量是否已经设置,如果没有,则为其分配默认值
${PHONEBOOK:=$HOME/phonebook}
注:上面例子不能单独作为命令,因为执行完替换操作之后,Shell会尝试执行替换结果;想要作为单独命令,需要使用空命令操作符 ":"
: ${PHONEBOOK:=$HOME/phonebook}
#!/bin/bash a=10 : ${a:=100} : ${b:=200} echo $a echo $b
④ ${parameter:?value}:如果parameter不为空,Shell会替换它的值;否则,Shell将value写入标准错误,然后退出
注:这种写法主要用来检查程序所需的变量是否已经设置且不为空
: ${TOOLS:?} ${EXPTOOLS:?} ${TOOLBIN:?}
⑤ ${parameter:+value}:如果parameter不为空,则替换成value;否则,不进行任何替换(它的效果和 ":-" 相反)
# a=10 # echo a=${a:+100} a=100
注:以上所有写法中,value部分都可以使用命令替换
WORKDIR=${DBDIR:-$(pwd)}
⑥ 模式匹配(Pattern Matching):
* POSIX Shell提供了4种能够执行模式匹配的参数替换形式
* 模式匹配接受两个参数:变量名(或参数数量)和模式
* 这里使用的模式一次和文件名替换与case语句的模式匹配相同
*:匹配零个或多个字符
?:匹配任意单个字符
[...]:匹配指定字符组中的任意单个字符
[!...]:匹配不在字符组中的任意单个字符
* 四种模式匹配
${variable%pattern}:检查varibale是否以pattern结束,如果是,则使用variable的内容并从右侧删除pattern所能匹配到的最短结果
${variable%%pattern}:检查varibale是否以pattern结束,如果是,则使用variable的内容并从右侧删除pattern所能匹配到的最长结果(pattern中有*时候%和%%才不一样,否则%和%%效果一样)
${varibale#pattern}:检查varibale是否以pattern结束,如果是,则使用variable的内容并从左侧删除pattern所能匹配到的最短结果
${varibale##pattern}:检查varibale是否以pattern结束,如果是,则使用variable的内容并从左侧删除pattern所能匹配到的最长结果
示例1
# echo $var testcase # echo ${var%e} testcas # echo ${var%se} testca
示例2:判读文件名是否以.o结尾
if [ ${file%.o} != $file ] then echo "file is ending as '.o'" fi
示例3:删除第一个位置参数的最后一个"/"以及它之前的所有字符
echo ${1##*/}
注:等价于 echo $(basename $1)
⑦ ${#variable}:返回变量中的字符个数
(3)$0:Shell会自动将程序名保存在特殊变量$0
(4)set 命令:主要有两个作用,设置Shell选项以及重新为位置参数$1、$2...赋值
① set -x:打开跟踪模式,命令会打印到标准错误中。(被跟踪的命令前会有一个+)
② set +x:关闭跟踪模式
注1:跟踪选项不会沿用到子Shell
注2:跟踪模式也可以这样实现 bash -x Test.sh
③ 无参数的set:打印当前环境的局部变量和导出变量
④ 使用set为位置参数重新赋值
#!/bin/bash set 100 200 echo $1 echo $2
echo $#
注:set常用来“解析”从文件或终端中读取的数据
#!/bin/bash read line
set $line
echo $#
⑤ set --选项:告诉set对于后续出现的"-"不再视为选项
⑥ set -e:如果有命令执行失败,或退出码不为0,则退出
(5)readonly命令:指定在程序随后的执行过程中、值都不会反生改变的变量
readonly PATH HOME
注1:指明PATH和HOME为只读变量,如果后面试图修改,Shell将报错
注2:变量的只读属性不会传给子Shell
注3: 一个变量一旦被设为只读,就无法再设置回普通变量
注4:查看所有只读变量 readonly -p
(6)unset命令:删除某个变量
unset x
注:不能对只读变量使用unset,一些特殊的变量如IFS、MAILCHECK、PATH、PS1、PS2也不能使用unset
11. 扩展内容
(1)eval命令
① 一般格式:
eval command-line
② 功能:如果把eval放在命令行前,Shell会对其进行二次扫描,然后执行
# pipe="| # eval ls $pipe wc -l 11
注1:Shell是在变量替换之前处理管道和I/O重定向的
注2:Shell第一次扫描时,将pipe替换为"|",eval会使得Shell进行第二次扫描,从而识别出"|"为管道
③ 在Shell程序中,eval常用于从变量中构造命令行。如果变量中包含任何必须由Shell解释的字符,就必须用eval(命令终止符(; | &)、I/O重定向(< >)以及引号)
#!/bin/bash eval echo \$$#
④ eval命令还可以用来创建指向变量的指针
# x=100 # ptrx=x # eval echo $$ptrx 3096ptrx # eval echo \$$ptrx 100 # eval $ptrx=50 # echo $x 50
(2)wait命令:等待某一进程结束(一般是子进程)
wait process-id
(3)$!:最后移入后台的进程
wait $! # 等待最后移入后台的进程结束
(4)trap命令
① 一般格式
trap commands signals
② 功能:指定进程接收到信号的处理方式。commands是接收到由signals指定的信号时要执行的一个或多个命令
③ 示例:
# trap "echo Hello world" INT # ^CHello world
④ 常用信号编号
信号 | 助记名 | 产生原因 |
0 | EXIT | 退出Shell |
1 | HUP | 挂起 |
2 | INT | 中断(如按下DELETE键或Ctrl+c) |
15 | TERM | 软件终止信号(默认由kill命令发送) |
⑤ 不适用参数的trap:会显示你定义过或修改过的所有trap处理程序
# trap trap -- 'echo Hello world' SIGINT #
⑥ 忽略信号
trap "" SIGINT
⑦ 重置信号
trap HUP INT
⑧ 列出所有信号
trap -l
(5)再谈I/O
① 在程序中明确的向标准错误写入
command >&2
注:>&指明输出重定向到与指定文件描述符相关的文件中(>&跟的是文件描述符,>跟的是文件名)
② 将标准输出(strout)和标准错误(stderr)都重定向到同一个文件中
# 正确写法 command > foo 2>>foo <==> command > foo 2>&1 # 错误写法 command 2>&1 1>foo
注:Shell在命令行是从左到右处理重定向的,第二种写法是:将标准错误重定向标准输出,将标准输出重定向foo文件
③ 可以使用exec命令实现标准输入或标准输出动态重定向
exec < datafile
exec > /tmp/output
exec 2> /tmp/erro
④ <&-与>&-:关闭标准输入、关闭标准输出
⑤ 行内输入重定向
command <<word
注:Shell会使用后面的行作为命令输入,知道碰到只包含word的行
[root@localhost ~]# wc -l <<END > Hello > world > ni > hao > END 4 [root@localhost ~]#
<<\:不让Shell解释输入行中的内容
<<-:删除前导制表符
⑥ Shell归档文件(行内输入重定向的应用)
(6)函数
① 一般格式
name () { command; ... command; }
② 示例
#!/bin/bash Test () { ls; pwd; echo $1;} Test "Hello"
注1:函数可以在命令行直接调用
注2:函数后面的参数依次会分配给位置参数$1、$2...
③ 函数多行定义:可省略";"
#!/bin/bash Test () { ls pwd echo $1 } Test "Hello"
④ 删除函数
unset -f function
⑤ return命令
return n
注1:n为该函数的返回状态,如果忽略,则返回最后一条命令的状态
注2:exit命令不仅会退出函数,还会结束Shell进程,而return仅仅结束函数
(7)type命令:返回命令类型(函数、Shell内建函数、标准UNIX命令或Shell别名)
# type pwd pwd is a shell builtin # type ls ls is aliased to `ls --color=auto' # type cat cat is /usr/bin/cat # type Test Test is a function Test () { ls --color=auto; pwd; echo $1 }
12. 交互式与非标准Shell
(1)使用正确的Shell
① #!:指定文件的解释器(用哪种Shell)
#!/bin/bash
(2)ENV文件
① 当启动Shell时,它会首先查看ENV变量是否为空,如果不为空,则执行ENV所指定的文件(创建Shell环境)
(3)命令行编辑
① 行编辑模式模仿了两种全屏编辑器:vi和emacs(POSIX标准Shell可以模仿vi,Bash和Korn Shell还支持emacs)
set -o vi
(4)命令历史
① 默认情况下,命令历史记录以文件形式保存在用户目录下,文件名为.sh_history(bash下是.bash_history)
HISTFILE:指定历史记录文件路径名
HISTSIZE:指定能保存的最大命令条数
(5)vi行编辑模式
(6)emacs行编辑模式
(7)访问历史记录的其他方法
① history命令:history 10(打印最近10条命令)
② fc命令:
fc -l 400 405 fc -n -l -10 fc -n -l -1 > runx
(8)函数
① Bash和Korn Shell函数都可以拥有局部变量,这使得编写递归函数成为可能。局部变量使用typeset定义
typeset i j
注:如果已经存在同名变量,该同名变量的值在执行typeset保存,待函数返回时恢复
(9)整数算术
① Bash和Korn Shell都支持不使用算术扩展的情况下求值算术表达式(注意与算术扩展的区别$((...)))
((...))
# x=10 # ((x = x * 12)) # echo $x 120
注:因为不会执行扩展,这种写法本身可以作为命令使用
② 这种写法的真正价值在于可以将算术表达式用于if、while和until命令中()
if (( i == 100 )) then echo "i=100" fi
if [ "$i" -eq 100 ] then echo "i=100" fi
注:上面两种写法等价,但第一种写法在测试的同时还可以执行算术运算
if (( i / 10 != 0 )) then echo "" fi
#!/bin/bash x=0 result=0 while (( x++ < 100 )) do (( result = result + x)) done echo $result
③ 整数类型
typeset -i variable
注1:这样做的好处:相较于非整数值,((...))对整数执行算术运算效率更高
注2:只能将整数值或整数表达式赋值给整数类型变量,否则会报错
typeset -i i i=100
④ 不同基数的数字
base#number
注:base为进制数,number为数值
typeset -i i=8#100
typeset -i j=16#100
注:Bash并不保存变量的基数,对于整数类型变量都是按十进制显示
(10)alias命令:自定义别名
① 一般格式
alias name=string
alias ll='ls -l'
② 如果别名以空格结束,则会对其之后的单词执行别名替换
alias pt='/usr/bin/echo '
pt ll
③ 列出别名name对应的值
alias name
④ 删除别名
alias name
alias -a
(11)数组
① Korn和Bash Shell都提供了有限的数组功能
② 数组元素可以通过下标访问,下标元素是一个值为整数的表达式,以0作为起始
③ [*]可以作为下标,用来生成数组所有元素
# arr[0]="hello" # arr[1]="world" # arr[2]="Test" # echo ${arr[*]} hello world Test
④ 可以使用 “typeset -i” 指定数组名来声明一个整数数组
# array[0]=100 # array[1]=50 # (( array[2] = array[0] + array[1] )) # echo ${array[2]} 150 # echo ${array[*]} 100 50 150
⑤ Korn 和 Bash中数组写法
写法 | 含义 |
${array[i]} | 替换为元素i的值 |
$array | 替换为数组第一个元素的值(array[0]) |
${array[*]} | 替换为所有数组元素的值 |
${#array[*]} | 替换为元素个数 |
array[i]=val | 将val保存到array[i] |
#!/bin/bash typeset -i array array[0]=0 array[1]=10 array[3]=30 echo ${array[0]} echo $array echo ${array[*]} echo ${#array[*]} echo ${array}
注:数组下标不一定是连续的,这样的数组称为稀疏数组
(12)作业控制
① Shell提供了在命令行中可以直接控制作业的功能,作业可以使Shell中任何命令或命令序列
# who | wc & [1] 4594 1 5 44 # [1]+ Done who | wc
② jobs命令可以打印尚未完成的作业状态
# jobs [1] Running ./Test.sh & [2]- Running ./Test1.sh & [3]+ Running ./Test2.sh &
注:+当前作业;-上一个作业
③ Shell内建的kill命令可以用来终止后台作业,可以接收的参数为进程ID,或者是以%开头的作业编号、+(当前作业)、-(上一个作业)、%%(当前作业另一种写法)
# jobs [1] Running ./Test.sh & [2]- Running ./Test1.sh & [3]+ Running ./Test2.sh & # kill %1 # jobs [1] Terminated ./Test.sh [2]- Running ./Test1.sh & [3]+ Running ./Test2.sh &
注:命令序列的前几个字符也可以用来引用作业
④ 作业的停止与恢复
* Ctrl+z可挂起在前台工作的作业(该作业会停止执行stopped)
* 继续执行该作业可以使用fg或bg命令:fg命令会在前台恢复执行当前作业,bg命令会在后台继续执行当前作业
# ./Test.sh 0 0 0 10 30 3 0 ^Z [1]+ Stopped ./Test.sh # jobs [1]+ Stopped ./Test.sh # bg [1]+ ./Test.sh & # jobs [1]+ Running ./Test.sh &
注:可以为fg和bg命令指定作业编号、管道的前几个字符、+、-或%%
(13)其他特性
① "cd -"切换到上一个目录
② 波浪符替换:如果“~”是单词中的唯一字符或者紧挨着“/”,会使用HOME变量的值来替换
③ 在命令行中输入命令后Shell的搜索次序
* 检查命令是否为保留字(for或者do)
如果不是保留字且没有被引用,检查别名列表
如果找到匹配的别名,执行替换,如果是空格结尾,尝试对下一个单词执行别名替换
针对替换后的结果,再次检查保留字列表,如果不是保留字,进行下一步
* 针对命令检查函数列表,如果找到,执行其中的同名函数
* 检查是否为内建命令
* 最后搜索PATH来定位命令
* 如果没找到,输出错误信息command not found
参考书籍:《UNIX/Linux/OS X中的Shell编程》