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编程》

 

posted @ 2019-04-10 22:56  99度的水  阅读(606)  评论(0编辑  收藏  举报