《Linux程序设计(第四版)》---第二章:shell程序设计

目录

重定向

重定向输出

重定向输入

管道

作为程序设计语言的shell

交互式程序

创建脚本

将脚本设置为可执行

shell的语法

变量

扫描二维码关注公众号,回复: 6788132 查看本文章

使用引号

环境变量

参数变量

条件

控制结构

if

elif

与变量有关的问题

for

while

until

case

命令列表

语句块

函数

命令

break命令

:命令

continue命令

.命令

echo命令

eval命令

exec命令

exit n命令

export命令

expr命令

printf命令

return命令

set命令

shift命令

trap命令

unset命令

另外两个有用的命令和正则表达式

命令的执行

算术扩展

参数扩展

here文档

调试脚本程序

迈向图像化:dialog工具

综合应用

CD唱片应用程序

应用程序说明

小结


shell执行shell程序,这些程序通常被称为脚本,是在运行时解释执行的,使得调试工作易于进行,因为可以逐行地执行指令,节省了重新编译的时间。但是shell不适合完成时间紧迫性和处理器忙碌型的任务。


ls -al | more

上述命令利用ls与more工具通过管道实现了文件列表的分屏显示。


shell作为用户与Linux系统间接口的程序,允许用户向操作系统输入需要执行的命令。

shell的功能比Windows的命令行工具更为强大。如:可以使用<和>对输入输出进行重定向,使用|在同时执行的程序之间实现数据的管道传输,使用$(...)获取子进程的输出


/bin/bash --version

上述命令查看shell的版本


重定向

重定向输出

ls -l > lsoutput.txt

上述命令将ls的输出保存到lsoutput.txt中。通过>操作符把标准输出重定向到一个文件,默认如果文件存在,则内容会被覆盖。可以使用set -o no clobber(或 set -C)设置noclobber选项,阻止重定向操作对已有文件的覆盖。可以使用set +o noclobber命令取消该选项

文件描述符0代表一个程序的标准输入,1代表标准输出,2代表标准输出错误

可以使用>>操作符输出内容附加到一个文件中:

ps >> lsoutput.txt

上述命令会将ps命令的输出附加到指定文件的尾部。

若想对标准错误输出进行重定向,需要将向重定向的文件描述符编号加在>操作符的前面,即使用2>操作符,这个方法可以在需要丢弃错误信息并阻止错误信息显示在屏幕上时使用。

当命令kill想终止一个进程时,可能在执行kill之前,需要终止的进程已经结束了,这时kill将向标准错误输出写一条错误信息,默认这条信息会显示在屏幕上,通过对标准输出和标准错误输出都进行重定向,可以阻止kill命令向屏幕上写任何内容。

kill -HUP 1234 >killout.txt 2>killer.txt

上述命令将标准输出与标准错误输出分别重定向到不同的文件中

kill -1 1234 >killouter.txt 2>&1

上述命令将两组输出都重定向到一个文件中,使用的是>&操作符来结合两个输出。含义是将标准输出重定向到killouter.txt中,然后再将标准错误输出重定向到与标准输出相同的地方。

kill -l 1234 >/dev/null 2>&1

上述命令使用Linux的通用回收站/dev/null来有效丢弃所有的输出信息。

重定向输入

more < killout.txt

上述命令实现了重定向输入,但是意义不大


管道

使用管道操作符|来连接进程

在Linux下通过管道连接的进程可以同时运行,而且随着数据流在它们之间的传递可以自动地进行协调。

可以使用sort命令对ps命令(Linux ps命令用于显示当前进程 (process) 的状态。)的输出进行排序

如果不使用管道,必须分步骤来完成任务:

ps > psout.txt
sort psout.txt > pssort.out

下述代码使用管道来连接进程:

ps | sort > sort.out

上述代码使用|将ps与sort两个进程连接起来,将内容重定向到sort.out

ps | sort | more

上述代码又连接了more进程,使得在屏幕上分页显示结果

ps -xo comm | sort | uniq | grep -v sh | more

上述代码首先按字母排序ps命令的输出,再用uniq命令去掉名字相同的进程,再用grep -v sh 命令删除名为sh的进程,最终将结果分页显示在屏幕上。

注意:若有一系列命令需要执行,相应的输出文件是在这一组命令被创建的同时立刻被创建或写入的,因此不要在命令流中重复使用相同的文件名


作为程序设计语言的shell

交互式程序

当shell期待进一步的输入时,正常的$shell提示符将变为>,可以一直输入,由shell判断何时输入完毕并且立刻执行脚本程序。

shell提供通配符扩展:

  • 匹配一个字符串
  • ? 匹配单个字符
  • [set] 允许匹配方括号中任意一个单个字符
  • [^set] 对方括号里的内容取反,即匹配任何没有出现在给出的字符集中的字符
  • {}(只能在部分shell中使用,包括bash)允许将任意的字符串放在一个集合中,以供shell扩展
ls my_{finger, toe}s

上述代码将列出文件my_fingers和my_toes,使用shell检查当前目录下的每个文件

创建脚本

首先必须用文本编辑器来创建一个包含命令的文件first,如下

# !/bin/sh

# first
# This file looks through all files in the
# current directory for the string POSIX, and then prints the name of
# those files to the standard output.

for file in *
do
  if grep -q POSIX $file
  then
    echo $file
  fi
done

exit 0

上述代码中,#!bin/shell 告诉系统之后的参数是执行文件的程序

最后的exit在打算从另一个脚本程序里调用此脚本并且查看是否成功,返回一个合理的退出码很重要,在shell中,0表示成功

Linux很少使用文件扩展名来决定文件的类型,使用file命令可以检查文件是否为脚本程序,如file first。

将脚本设置为可执行

运行脚本文件

/bin/sh first

也可以使用下面代码,实现直输入程序名就调用它的目的:

chmod +x first

然后使用下面代码调用

first

可能会收到错误信息,因为shell环境变量PATH未设置为在当前目录下查找执行的命令。解决方法:第一种是在命令行上直接输入PATH=$PATH:.或者编辑.bash_profile文件,将path的这行代码添加到文件结尾,然后退出登录后再重新登录进来。第二种方法是在保存脚本程序的目录中输入./first,作用是把程序的完整的相对路径告诉shell。

用./能够保证不会意外执行系统中与自己的文件同名的另一个命令。这是一个很好的习惯!!!


shell的语法

变量

使用变量不需要提前声明,通过使用(如赋初值)时创建。默认所有变量以字符串存储。shell会在需要时把数值型字符串转换为对应的数值以对变量操作。

可以在变量名前加$访问其内容。为变量赋值时,只需要使用变量名,变量会自动创建。

一种检查变量内容的方式是在变量名前加一个$,再echo将内容输出。

可以通过设置和检查变量salutation的不同值查看salutation

salutation=Hello
echo $salutation

输出:

Hello

注意:若字符串包含空格,需要使用引号。等号两边不能有空格

可以使用read将用户的输入赋值给一个变量。命令需要准备读入用户数据的变量名,之后会等待用户输入数据。按回车则read命令结束。从终端读取一个变量时,不需要引号,如:

输入

read salutation
My name is Barry Allen.
echo $salutation

输出

My name is Barry Allen.

使用引号

若想在一个参数中包含多个空白符,需要给参数加上引号。

若把$变量表达式放在双引号中,执行到这一行会将变量替换为它的值;单引号则不会发生替换。可以在$字符前加一个\以取消其特殊含义。

字符串通常存放在双引号中,防止变量被空白字符分开,同时又允许$扩展。

输入

#!/bin/sh

myvar="Hi there"

echo $myvar

输出

Hi there

输入

echo "$myvar"

输出

Hi there

输入

echo '$myvar'

输出

$myvar

输入

echo \$myvar

输出

$myvar

输入

echo Enter some txt

输出

Enter some txt

输入

read myvar

需要用户输入,再输入

Hello World

再输入

echo '$myvar' now equals $myvar
exit 0

输出

'$myvar' now equals Hello World

上述所有代码的解释:myvar在创建时赋值Hi there。使用echo显示内容。$后加变量名可以得到内容。使用双引号可以使变量名替换为内容,使用单引号与\不能替换。使用read从用户读取一个字符串。

环境变量

环境变量

说明

$HOME

家目录

$PATH

以冒号分隔的用来搜索的目录列表

$PS1

命令提示符,通常是$字符。再bash中,可以使用更加复杂的值,如[\u@\h\W]$,给出了用户名、机器名、当前目录名

$PS2

二级提示符、提示后续的输入,通常是>字符

$IFS

输入域分隔符。当shell读取输入时,给出用来分隔单词的一组字符,一般是空格、制表符、换行符

$0

shell脚本的名字

$#

传递给脚本的参数个数

$$

shell脚本的进程号,用于生成一个唯一的临时文件,如/tmp/temfile_$$

参数变量

程序在调用时有参数,一些额外的变量就会被创建。即使没有传递任何参数,环境变量$#也存在,为0。

下面是参数列表

参数变量 说明
$1, $2,…… 程序的参数
$* 在变量中列出所有参数,各个参数用环境变量IFS中的第一个字符分割开,若IFS被修改,$*将命令行分隔为参数的方式就随之改变
$@ 是$*的变体,不使用IFS环境变量,故即使IFS为空,参数也不会挤在一起

下面的代码可以展示$@和$*的区别:

IFS=''
set foo bar bam
echo "$@"

输出

foo bar bam

输入

echo "$*"

输出

foobarbam

输入

unset IFS
echo "$*"

上述代码中,双引号里的$@把各个参数扩展为彼此分开的域,不受IFS的影响。通常使用$@来访问脚本程序的参数。可以使用read读取变量内容。

下面的代码演示了简单的变量操作。当输入内容并保存为try_var后,用chmod +x try_var将其设为可执行。

#!/bin/sh

salutation="Hello"
echo $salutation
echo "The program $0 is now running"
echo "The second parameter was $2"
echo "The first parameter was $1"
echo "The parameter list was $*"
echo "The user's name directory is $HOME"

echo "Please enter a new greeting"
read salutation

echo $salutation
echo "The script is now complete"
exit 0

使用下面的代码执行程序

./try_var foo bar baz

输出

Hello
The program ./try_var is now running
The second parameter was bar
The first parameter was foo
The parameter list was foo bar baz
The user's name directory is /root
Please enter a new greeting

需要继续输入,我输入的是

Naruto

输出

Naruto
The script is now complete

上述代码中,创建变量salutation并显示其内容,之后显示个参数以及环境变量$HOME都已存在并有了适当的值。


条件

shell可以对任意可以从命令行上调用的命令的退出码进行测试,这也是在末尾加exit的原因。

shell的布尔判断命令[或test。使用[时,以]结尾。

which 名称,可以查看系统中是否有一个[名称]的外部命令,如which test可以检查执行的是哪一个test命令。

使用./test可以确保执行的是当前目录下的程序。

test -f <filename>用于查看文件是否存在:

if test -f fred.c
then
……
fi

if [ -f fred.c]
then
……
fi

test的退出码(表示条件是否满足)决定是否需要执行后面的代码。

注意:必须在[和被检查的条件之间留出空格。

如果想把then和if放在一行,需要分号将test和then分隔如下:

if [ -f fred.c ]; then
……
fi

test可使用的条件类型如下三种:字符串比较、算术比较、文件条件测试

字符串比较
字符串比较 结果
string1 = string2

相同则为真

string1 != string2

不同则为真

-n string 字符串非空则为真
-z string 为null(空串)则为真
算术比较
算术比较 结果
expression1 -eq expression2 相等则为真
expression1 -ne expression2 不等则为真
expression1 -gt expression2 大于则为真
expression1 -ge expression2 大于等于则为真
expression1 -lt expression2 小于则为真
expression1 -le expression2 小于等于则为真
! expression 表达式为假则结果为真,反之亦然
文件条件测试
文件条件测试 结果
-d file 文件是目录则为真
-e file 文件存在则为真,-e不可移植,通常使用-f
-f file 是普通文件则为真
-g file 文件的set-group-id为被设置,则为真
-r file 可读则为真
-s file 大小不为0则为真
-u file 文件的set-user-id位被设置则为真
-w file 可写则为真
-x file 可执行则为真

set-group-id授予程序拥有者的访问权限而不是使用者的访问权限

set-user-id授予程序所在组的访问权限。

上述两个特殊位是通过chmod的选项u和g设置的,二者对shell不起作用,只对二进制文件有用。

下面的代码测试/bin/bash文件状态:

#!/bin/sh

if [ -f /bin/bash ]
then
  echo "file /bin/bash exist"
fi

if [ -d /bin/bash ]
then
  echo "/bin/bash is a directory"
else
  echo "/bin/bash is NOT a directory"
fi

控制结构

if

if condition
then
  statements
else
  statements
fi

一个例子:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ $timeofday = "yes" ]; then
  echo "Good morning"
else
echo "Good afternoon"
fi

exit 0

elif

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ $timeofday = "yes" ]; then
  echo "Good morning"
elif [ $timeofday = "no" ]; then
  echo "Good afternoon"
else
  echo "Sorry, $timeofday not recognized. Enter yes or no"
  exit 1
fi

exit 0

与变量有关的问题

上述代码中,如果直接输入回车,会报错

b: 6: [: =: unexpected operator
b: 8: [: =: unexpected operator
Sorry,  not recognized. Enter yes or no

问题在第一个if,测试时,包含一个空字符串,使得if变成

if [ = "yes" ]

上述代码不是合法的条件,因此为了避免出现这种情况,要给变量加引号,如下

if [ "$timeofday" = "yes" ]

若想使echo命令去掉每行的换行符,可以使用以下语法:

echo -n "Is it morning? Please answer yes or no: "

for

for variable in values
do
  statements
done

下面的代码是一个例子,循环值通常是字符串:

#!/bin/sh

for foo in bar fud 43
do
  echo $foo
done
exit 0

输出是

bar
fud
43

shell默认所有变量包含的都是字符串。

实验:使用通配符扩展的for循环

for经常与shell的文件名扩展一起使用。这意味着字符串的值中使用通配符,并由shell在程序执行时填写出所有的值。

若想打印当前目录中所有以f开头的文件,且假设都以.sh结尾:

#!/bin/sh

for file in $(ls f*.sh); do
  lpr $file
done
exit 0

上述代码中演示了$(command)语法的用法,for的参数表包括在$()中的命令的输出结果。shell扩展f*.sh给出所有匹配此模式的名字。

注意:shell中的变量扩展均是在执行时而非编写时完成,故变量声明的语法错误只有执行时才会发现。

while

while condition; do
  statements
done

下面的代码是一个密码检查程序:

#! /bin/sh

echo "Enter password"
read trythis

while [ "$trythis" != "secret" ]; do
  echo "Sorry, try again"
  read trythis
done
exit 0

until

until condition
do
  statements
done

上述代码中,循环将反复执行知道条件为真

注意:一般需要循环至少执行一次,使用while;若可能根本不需要执行循环,使用until。

下面的代码是一个例子,设置一个警报,当特定用户登录,会启动警报,通过命令行将用户名传递给脚本程序:

#!/bin/sh

until who | grep "$1" > /dev/null
do
  sleep 60
done

# now ring the bell and announce the expected user.

echo -e '\a'
echo "**$1 has just logged in **"

exit 0

上述代码中,若用户已经登录,则循环不需要执行

case

case variable in
  pattern [ |pattern] ...) statements;;
  pattern [ |pattern] ...) statements;;
  ...
esac

上述代码中,case允许通过复杂的方式将变量的内容和模式进行匹配,之后根据匹配的模式执行不同的代码,比多条if执行多个条件检查要更简单。

注意:每个模式行均以;;结尾,因为可以在前后模式之间放置多条语句,故需要一个分号标记前一个语句的结束和后一个模式的开始。在case使用*这样的通配符要小心,因为将使用第一个匹配的模式,即使后面的模式有更精确的匹配。

下面的代码是一个例子,用户输入:可以使用case编写输入测试程序,使其更具有选择性并且对非预期输入也更宽容:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

cese "$timeofday" in
    yes) echo "Good morning";;
    no ) echo "Good afternoon";;
    y  ) echo "Good morning";;
    n  ) echo "Good afternoon";;
    *  ) echo "Sorry, answer not recognized";;
esac

exit 0

上述代码中,case会对比较的字符串进行正常的通配符扩展,故可以指定字符串的一部分并且在其方面加一个*通配符。使用单独的*表示匹配任何可能的字符串,故总是在其他匹配值之后加一个*以确保若无匹配,case也会执行某个默认动作。case不会找最佳匹配,只会按顺序匹配首个字符串。

下面的代码是一个例子,合并匹配模式:

#!/bin/sh


echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
    yes | y | Yes | YES ) echo "Good morning";;
    n* | N* ) echo "Goodafternoon";;
    * ) echo "Sorry, answer not recognized";;
esac

exit 0

简单易懂,不做解释

下面的代码是一个例子,执行多条语句:为了使程序具有重用性,需要在默认模式时给出另外一个退出码,如下

#!/bin/sh
                                                                                                                                                                                                                                                                                                                                                echo "Is it morning? Please answer yes or no"                                                                                                                           read timeofday                                                                                                                                                                                                                                                                                                                                  case "$timeofday" in                                                                                                                                                                        
    yes | y | Yes | YES )                                                                                                                                                       
        echo "Good morning"                                                                                                                                                                             
        echo "Up bright and early this morning"                                                                                                                                 
        ;;                                                                                                                                                                      
    [nN]* )                                                                                                                                                                     
        echo "Goodafternoon"                                                                                                                                                    
        ;;                                                                                                                                                                  
    * )                                                                                                                                                                                   
        echo "Sorry, answer not recognized"                                                                                                                                     
        echo "Please answer yes or no"                                                                                                                                          
        exit 1                                                                                                                                                                      
        ;;                                                                                                                                                              esac                                                                                                                                                                                                                                                                                                                                            

exit 0

上述代码中,若最后一个case模式是默认模式则省略最后一个双分号是可以的,因为方后面没有其他的case模式需要考虑。

为了让case的匹配功能更加强大,可以使用

[yY] | [yY] [Ee] [Se] )

上述代码中,限制了允许出现的字母。

命令列表

有时,需要将几条命令连接成一个序列。如:想在执行某条语句之前同时满足好几个不同的条件,如下:

if [ -f this_file ]; then
    if [ -f that file ]; then
        if [ -f the_other_file ]; then
            echo "All files present, and corrent"
        fi
    fi
fi

或者,希望至少存在这一序列中有一个为真,如:

if [ -f this_file ]; then
    foo="True"
elif [ -f that_file ]; then
    foo="True"
elif [ if the_other_file ]; then
    foo="True"
else
    foo="False"
if
if[ "$foo" = "Ture" ]; then
    echo "One of the files exists"
fi
  • AND列表

语法:statement1 && statement2 && statement3 ...

从左往右顺序执行每条命令,若一条命令的返回是True,右边的下一条命令才能够执行,&&的作用是检查前一条命令的返回值

下面的代码是一个例子,执行touch file_one(检查文件是否存在,不存在则创建它),并删除file_two文件,之后用AND列表检查每个文件是否存在并通过echo命令给出相应的指示:

#!/bin/sh

touch file_one
rm -f file_two

if [-f file_one] && echo "hello" && [-f file_two] && echo " there"
then
    echo "in if"
else
    echo "in else"
fi

exit 0

输出

hello
in else
  • OR列表

语法:statement1 || statement2 || statement3 ...

OR列表结构允许持续执行命令直到有一条命令成功,其后的命令将不再执行,如果本条命令是Flase,下一条指令才能执行,如此持续直到有一条命令返回true,或者列表中的所有命令均执行结束

下面的代码是一个例子:

#!/bin/sh

rm -f file_one

if [-f file_one] || echo "hello" || echo " there"
then
    echo "in if"
else
    echo "in else"
fi

exit 0

输出

hello
in if

上面两种列表:AND和OR,称为短路求值

语句块

若想在只允许单个语句的地方使用多条语句,可以将其用括号{}来构造一个语句块。


函数

在一个脚本程序中执行另外一个脚本比执行一个函数慢得多

语法

function_name(){
    statements
}

下面的代码是一个例子:

#!/bin/sh

foo(){
    echo "Function foo is executing"
}

echo "script starting"
foo
echo "script ended"

exit 0

输出:

script starting
Function foo is executing
script ended

必须在调用函数之前定义。当函数被调用时,脚本程序的位置参数($*、$@、$#、$1、$2等)会被替换为函数的参数,这也是传递给函数参数的方法。函数执行完以后这些参数会恢复为原先的值。

可以使用return让函数返回数值。让函数将字符串保存在变量中,在函数结束之后调用,从而让函数返回字符串。还可以echo一个字符串并且捕获结果,如下:

foo(){ echo JAY; }

...

result="$(foo)"

可以在shell中使用local声明局部变量,仅在函数作用范围内有效。

当局部变量与全局变量同名时,前者会覆盖后者,但是仅限于函数内,下面的代码是一个例子:

#!/bin/sh

sample_text="global variable"

foo(){
    local sample_text="local variable"                                                                                                                                      
    echo "Function foo is executing"
    echo $sample_text                                                                                                                                                   }

echo "script starting"
echo "$sample_text"

foo

echo "script ended"
echo "$sample_text"

exit 0

输出如下:

script starting
global variable
Function foo is executing
local variable
script ended
global variable

下面的代码是一个例子,下一个程序my_name演示了函数的参数传递,以及如何返回一个true和false的值,使用一个参数调用该程序,从函数中返回一个值:

#!/bin/sh

yes_or_no(){
    echo "Is your name $* ?"
    while true
    do
        echo -n "Enter yes or no: "
        read x
        case "$x" in
            y | yes ) return 0;;
            n | no ) return 1;;
            * ) echo "Answer yes or no"
        esac
    done
}

echo "Original parameters are $*"

if yes_or_no "$1"
then
    echo "Hi $1, nice name"
else
    echo "Never Mind"
fi
exit 0

输出时,注意是./(程序名) 参数:如上述代码名是test,运行时输入:./test Tom:

Original parameters are Tom
Is your name Tom ?
Enter yes or no: yes
Hi Tom, nice name

命令

两类命令:普通命令即外部命令(external command和内置命令即内部命令(internal command)。内置命令是在shell内部实现的,不能作为外部程序被调用。但是大多数内部命令也提供了独立运行的程序版本——这是POSIX规范的一部分。内部命令的执行效率更高

break命令

可以在控制条件未满足之前退出for,while,until循环,可以为break提供额外的数字来表明需要跳出的循环次数,一般只跳出一层循环,下面的代码是一个例子:

#!/bin/sh

rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4

for file in fred*
do
    if [ -d "$file"]; then
        break;
    fi
done

echo first directory starting fred was $file
rm -rf fred*
exit 0

:命令

是空命令。用于简化条件逻辑,相当于true的一个别名,运行比true块,但是输出可读性差。

while : 实现了无限循环,代替了while true

也会用在变量的条件设置,如

: ${var:value}

如果没有:,shell将试图把$var当作一条命令处理。下面的代码是一个例子:

#!/bin/sh

rm -f fred
if [ -f fred ]; then
    :
else
    echo file fred did not exist
fi

exit 0

输出如下:

file fred did not exist

continue命令

使得for、while、until循环跳到下一次循环继续执行,循环变量取循环列表的下一个值:

#!/bin/sh

rm -rf fred*
echo > fred1
echo >fred2
mkdir fred3
echo > fred4

for file in fred*
do
    if [ -d "$file" ]; then
        echo "skipping directory $file"
      continue
    fi
    echo file is $file
done

rm -rf fred*
exit 0

输出如下:

file is fred1
file is fred2
skipping directory fred3
file is fred4

.命令

. ./shell_script

点命令用于在当前shell中执行命令

当脚本执行外部命令或者程序时,会创建新的环境(子shell),命令会在新环境执行,之后,环境会被丢弃,留下退出码回给父shell,此时对环境变量所作的修改都会丢失。但是外部的source命令和.命令在执行脚本中的命令时,使用的是调用该脚本的同一个shell。而.命令允许执行的脚本改变当前环境变量。当要把脚本当作“包裹器”来为后续执行的其他命令设置环境时,这个命令就很有用。

在shell中,.命令的作用类似于c的#include命令,尽管没在字面意义上包含脚本,但是会在上下文执行命令,所以可以使用它将变量和函数定义结合进脚本程序。

下面的代码是一个例子,假设有两个包含环境设置的文件,分别针对两个不同的开发环境。为了设置老的、经典命令的环境,可以使用classic_set,内容如下:

#!/bin/sh

version=classic
PATH=/usr/local/old_bin:/usr/bin:/bin:.
PS1="classic> "

对于新命令,使用文件latest_set:

#!/bin/sh

version=latest
PATH=/usr/local/new_bin:/usr/bin:/bin:.
PS1=" latest version> "

可以通过将这些脚本和点命令结合来设置环境,如下:

$ . ./latest_set
latest version> echo $version
latest
$ . ./classic_set
classic> echo $version
classic

上述代码中,脚本使用点命令执行,故每个脚本都是在当前shell中执行的,使得脚本可以改变当前shell的环境设置,即使脚本执行结束,改变依然有效。

echo命令

Linux解决输出换行符取消的方法:

echo -n "string to output"
echo -e "string to output\c"

第二种方法echo -e确保启用了反斜线转义字符(\c代表去掉换行符,\t代表制表符,\n代表回车)的解释。

eval命令

允许对参数进行求值,为内置命令,不会以单独命令存在,下面的代码是一个例子:

foo=10
x=foo
y='$'$x
echo $y

输出如下:

$foo

foo=10
x=foo
eval y='$'$x
echo $y

输出为10,因此eval类似一个额外的$,给出一个变量的值的值。eval允许代码被随时生成和运行。虽然增加了脚本测试的复杂度。

exec命令

有两种用法,第一种是将当前的shell替换为一个不同的程序,如:

exec wall "Thanks for all the fish"

上述代码中会用wall命令替换当前的shell,脚本中exec后面的代码不会执行,因为执行这个脚本的shell已经不存在了。

第二种用法是修改当前文件描述符:

exec 3< afile

上述代码中,使得文件描述符3被打开以便从afile中读取数据,这种用法比较少见,一般不用。

exit n命令

使程序以退出码n结束运行。如果脚本在退出时不指定一个退出状态,那么该脚本最后一条被执行命令的状态将被用作为返回值。

退出码为0表示成功,1~125是脚本可以使用的错误代码,其余代码如下:

退出码 说明
126

文件不可执行

127 命令未找到
128及以上 出现一个信号

下面的代码是一个例子,如果当前目录下存在一个.profile的文件,就返回0表示成功

#!/bin/sh

if [ -f .profile ]; then
    exit 0
fi

exit 1

export命令

将作为它参数的变量到处到子shell中,并使之在子shell中有效。在shell中创建的变量默认在这个shell调用的下级(子)shell中是不可用的。export把自己的参数创建为一个环境变量,而这个环境变量可以被当前程序调用的其他脚本和程序看见。被导出的变量构成从该shell衍生的任何子进程的环境变量。

下面的代码是一个例子,先列出脚本export2:

#!/bin/sh

echo "$foo"
echo "$bar"

下面是脚本export1,在此脚本结尾调用了export2:

#!/bin/sh

foo="The first meta-syntactic variable"
export bar="The second meta-syntactic variable"

export2

运行export1(事实上,我运行不了,不知道为什么):

./export1

输出如下:


The second meta-syntactic variable

上述代码中,export2只是回显两个变量的值。export1设置两个变量,但是只导出bar,所以向后调用export2时,foo的值已经丢失,但是bar的值已经被导出到第二个脚本中。脚本输出的第一个空行是因为$foo在export2中无定义,回显一个null变量将输出空行。变量被shell导出以后,可被该shell调用的任意脚本使用,也可以被后续依次调用的任何shell调用。若export2调用了另外一个脚本,bar的值对新脚本来说仍然有效。

set -a或set -o allexport将导出它之后声明的所有变量。

expr命令

将参数当作表达式求值。最简单的就是进行下面的数学运算:

x=`expr $x + 1`

反引号``使x取值为expr $x+1的执行结果,也可以使用$()替换``:

x=$(expr $x + 1)

printf命令

语法:

printf "format string" parameter1 parameter2 ...

不支持浮点数,shell中的算术运算均按照整数来计算,格式字符由各种可打印字符、转义序列和字符转换限定符组成。格式化字符串中除了%和\之外的所有字符都要按原样输出,下面的表格列出了支持的转义序列:

字符转换限定符由%和一个转换字符组成,如下:

格式化字符串用来解释printf后续参数的含义并输出结果,如:

printf "%s\n" hello

输出如下:

hello

输入:

printf "%s %d\t%s" "Hi there" 15 people

输出如下:

Hi there 15     people

return命令

作用是使函数返回,有一个数字参数,作为函数返回值。如果未指定参数,默认返回最后一条命令的退出码

set命令

作用是为shell设置参数变量。许多输出是以空格分隔的值,若需要使用输出结果中的某个域,就会用这个命令。

若想在脚本中使用当前月份的名字,系统提供了一个date命令,输出包含字符串形式的月份名称。但是若想将其与其他区域分开,可以将set和$(...)结合起来执行date命令,返回想要得到的结果。data的输出将月份字符串作为它的第二个参数:

#!/bin/sh

echo the date is $(date)
set $(date)
echo The month is $2

exit 0

输出如下:

the date is Sat Jun 29 08:53:33 CST 2019
The month is Jun

上述代码中,将date的输出设置为参数列表,然后通过位置参数$2获得月份。

在实际中,用使用date + %B提取月份。

还可以通过set和参数控制shell的执行方式。常用的是set -x,让脚本跟踪显示它当前执行的命令

shift命令

把所有参数变量左移一个位置,使$2变成$1,使$3变成$2,$1丢弃,而$0不变。调用shift时指定了一个数字参数在,者代表所有参数将向左移的次数。$*、$@、$#等也将根据参数的安排做出变动。

如果程序需要10个或10个以上的参数,需要使用shift来访问第十个及后面的参数,下面的代码是一个例子,依次扫描所有的位置参数:

#!/bin/sh

while [  "$1" != "" ]; do
    echo $1
    shift
done

exit 0

trap命令

用于指定在接收到信号后将采取的行动,常用于在脚本中断时完成清理工作。历史上,shell用数字代表信号,但是新的脚本应该使用定义在头文件signal.h的信号的名字,使用信号名时需要省略SIG前缀,可以在命令提示符输入trap -l查看信号的编号及关联的名称。

trap -l

输出如下:

 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

信号是被异步发送到一个程序的事件,默认通常会终止一个程序的运行。

trap的参数有两个:1)收到指定信号要采取的行动 2)处理的信号名

trap command signal

必须在想保护的代码之前指定trap命令。

若想重置某个信号的处理方式到默认值,只需要将command设置为-。要忽略某个信号,把command设置为空字符串""。不带参数的trap命令将列出当前设置的信号及行动的清单。下面是能够捕获的比较重要的一些信号(数字是对应的信号编号):

下面的代码是一个例子,演示了一些信号处理方法:

#!/bin/sh

trap 'rm -f /tmp/my_tmp_file_$$' INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$

echo "press interrupt (CTRL-C) to interrupt ..."
while [ -f /tmp/my_tmp_file_$$ ]; do
    echo File exists
    sleep 1
done
echo The file no longer exists

trap INT

trap INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$

echo "press interrupt (CTRL-C) to interrupt ..."
while [  -f /tmp/my_tmp_file_$$ ]; do
    echo File exists
    sleep 1
done

echo we never get here
exit 0

运行上述脚本,在每次循环按下CTRL+C,将得到:

creating file /tmp/my_tmp_file_150
press interrupt (CTRL-C) to interrupt ...
File exists
File exists
File exists
^CThe file no longer exists
creating file /tmp/my_tmp_file_150
press interrupt (CTRL-C) to interrupt ...
File exists
File exists
File exists
^C

上述代码中,先用trap安排它出现在一个INT(中断)信号时执行rm -f /tmp/my_tmp_file_$$删除临时文件,脚本程序然后进入一个while循环,只要临时文件存在,循环就一直池逊下去,按下CTRL+C后,脚本会执行rm -f /tmp/my_tmp_file_$$,然后继续下一循环,因为临时文件已经删除了,所以第一个while循环正常退出。接下来,再次调用trap,这次指定当INT出现时不执行任何命令,脚本重新创建临时文件进入第二个while循环。当用户安下CTRL+C,无语句执行,采取默认处理,即终止脚本程序,因为程序被终止,所以echo和exit语句不会执行。

unset命令

从环境中删除变量或者函数。不能删除shell本身定义的只读变量(如IFS)。并不常用。

下面的代码是一个例子,第二次输出Hello World,第二次输出换行符:

#!/bin/sh

foo="Hello World"
echo $foo

unset foo
echo $foo

输出如下:

Hello World

使用foo=语句的效果与unset命令类似,但是不等同,前者将foo设置为空,但是foo仍然存在,unset foo的效果是把变量foo从环境中删除。

另外两个有用的命令和正则表达式

  • find命令,用于搜索文件。

下面的代码是一个例子,在本地查找名为test的文件,为确保有搜索整个机器的权限,以root用户身份来执行命令:

find / -name test -print

上述代码中,从根目录查找test文件,输出文件的完整路径。

若指定-mount选项,则find不会搜索挂载在其他文件系统的目录,下面的代码是一个例子:

find / -mount -name test -print

find完整语法:find [path] [option] [tests] [actions]

path部分可以指定绝对路径如:/bin,可以指定相对路径,如:.,也可以指定多个路径,如:find: /var /home

find命令有许多选项可用,如下:

下面是测试部分,每种测试返回的结果有两种:true或者false。find执行时,按顺序将每种测试一次应用到搜索到的每个文件。若一个测试返回false,则停止处理当前找到的文件,继续搜索。若返回true,将继续下一个测试或对当前文件采取行动,下表为最常用的测试:

可以使用操作符来组合测试,有两种格式:短格式与长格式,如下:

可以使用圆括号来强制测试和操作符的优先级,由于对shell来说圆括号有特殊含义,必须使用反斜线引用圆括号。此外,若在文件名处使用的是匹配模式,则必须在模板上使用引号以确保模式没有被shell扩展,而是直接传递给find命令。

下面是测试“搜索的文件比文件X要新,或者文件名以下划线开头”:

\(-newer X -o -name "_*"\)

下面的代码是一个例子,使用带测试的find命令,在当前目录搜索比文件while2要新的文件:

find . -newer while2 -print

下面的代码是一个例子,查找以下划线开头或者比while2更新的文件,但是这两种情况下都需要是普通文件,使用括号强制优先级,使用\引用括号,对多组测试进行组合:

find . \(-name "_*" -or -newer while2 \) -type f -print

下面看看在发现匹配指定条件的文件之后,可以执行的操作:

-exec和-ok将命令行上后续的参数作为自己参数的一部分,直到被\;序列中止,执行的是嵌入式命令,所以必须以一个转义的分号结束,使得find可以决定什么时候可以继续查找用于它自己的命令行选项。魔术字符串{}是-exec和-ok的一个特殊类型的参数,将被当前文件的完整路径取代。

下面的代码是一个例子,使用命令ls:

find . -newer a -type f -exec ls -l {} \;

输出如下:

-rw------- 1 root root 9086 Jul  3 23:35 ./.bash_history
-rw------- 1 root root 9250 Jul  3 09:41 ./.viminfo
-rw-rw-rw- 1 root root 0 Jul  3 08:55 ./a.out
  • grep命令,通用正则表达式解析器(General Regular Expression Paraser)

使用find在系统中搜索文件,使用grep在文件中搜索字符串,常用的用法是在使用find时,将grep作为传递给-exec的一条命令。

grep使用一个选项、一个要匹配的模式和要搜索的文件,语法如下:

grep [options] PATTERN [FILES]

若未提供文件名则grep将搜索标准输入。下面是grep的一些主要选项:

下面的代码是一个例子,使用grep的简单例子:

首先,a.txt的内容如下:

Hello world!

My name is Xue Wei.

Hello my friend, what a wonderful world!

b.txt的内容如下

The world is colorful.

Enjoy our world.

输入:

grep world a.txt

输出如下:

Hello world!
Hello my friend, what a wonderful world!

上述代码中未使用选项,只是在a.txt中搜索world字符串,然后输出匹配的行,文件名没有输出是因为只在一个文件中进行搜索。

输入:

grep -c world a.txt b.txt

输出如下:

a.txt:2
b.txt:2

上述代码中,在两个文件中计算匹配行的数目,这种情况下,文件名被输出。

输入如下:

grep -c -v world a.txt b.txt

输出如下:

a.txt:3
b.txt:1

上述代码中使用-v对搜索取反,在两个文件中计算不匹配行的数目。

  • 正则表达式

允许实现更复杂的匹配。在正则表达式的使用中,一些字符是以特定方式进行处理的。最常用的特殊字符如下:

若想使用上述字符串作普通字符,需要在前面加上\ 。如,使用$时,要写为\$。

在[]中还可以使用一些有用的特殊匹配模式,如下:

若指定了用于扩展匹配的-E选项,则用于控制匹配完成的其他字符可能会遵循正则表达式的规则,如下,对于grep来说,还需要在这些字符之前加上\字符:

下面的代码是一个例子,查找以字母d结尾的行,需要使用特殊字符$,如下:

首先,a.txt的内容如下:

Hello world                                                                                                                                                             
My name is Xue Wei                                                                                                                                                      
Hello my friend, what a wonderful world

输入:

grep d$ a.txt

输出如下:

Hello world
Hello my friend, what a wonderful world

上述代码找到了以d结尾的行。

下面的代码是一个例子,查找以s为结尾的单词,需要使用方括号括起来的特殊匹配符,在本例使用[[:blank:]],用来测试空格或者制表符:

grep s[[:blank:]] a.txt

输出如下:

My name is Xue Wei
Hello my friends what a wonderful world

下面的代码是一个例子,查找以Th开头的由3个字母组成的词,需要使用[[:space:]]来划定单词的结尾,还需要用字符(.)来匹配一个额外的字符:

首先,b.txt的内容如下:

The king of uiverse

The fight

OK

The world

输入如下:

grep Th.[[:space:]] b.txt

输出如下:

The king of uiverse
The fight
The world.

下面的代码是一个例子,用扩展模式搜索只有3个字符串长的全部由小写字母组成的单词,通过指定一个匹配字母a到z的字符范围和一个重复3次的匹配来完成:

输入:

grep -E [a-z]\{3\} b.txt

输出如下:

The king of uiverse
The fight
The world.

命令的执行

在脚本里执行命令的比较老的语法形式时,使用反引号``,单引号的作用是防止变量扩展。

新脚本程序应该使用$(...)形式,目的是为避免在使用反引号执行命令时,处理内部的$、`、\等字符需要应用的相当复杂的规则。

$(command)的结果是其中命令的输出。这不是该命令的退出状态,而是字符串形式的输出结果,下面的代码是一个例子:

#!/bin/sh

echo The current directory is $PWD
echo The current users are $(who)

exit 0

若想将命令的结果放在一个变量中,可以赋值,如下:

whoisthere=$(who)
echo $whoisthere

算术扩展

将表达式置于$((...))中能够更有效地完成简单的算术运算,如下:

#!/bin/sh

x=0
while [ "$x" -ne 10 ]; do # x的值不等于10
    echo $x
    x=$(($x+1))
done

exit 0

输出如下:

0
1
2
3
4
5
6
7
8
9

两对圆括号用于算术替换,一对圆括号用于命令的执行和获取输出。

参数扩展

下面的代码是一个例子:

#!/bin/sh

for i in 1 2
do
    my_secret_process ${i}_tmp
done

exit 0

上述代码中i的值替换掉了${},从而给出了正确的文件名,即把参数的值替换进了一个字符串。可在shell中采用多种参数替换的方法,下面是一些常见的参数扩展方法:

下面的代码是一个例子,演示了各种参数匹配操作符的用法:

#!/bin/sh

unset foo
echo ${foo:-bar}

foo=fud
echo ${foo:-bar}

foo=/usr/bin/X11/startx
echo ${foo#*/}
echo ${foo##*/}

bar=/usr/local/etc/local/networks
echo ${bar%local*}
echo ${bar%%local*}

exit 0

输出如下:

bar
fud
usr/bin/X11/startx
startx
/usr/local/etc/
/usr/

上述代码中,若语句是${foo:=bar},这变量$foo就会被赋值,这个操作符的作用是检查foo是否存在且不为空。若非空则返回该值,否则将foo赋值为bar并且返回这值。${foo:?bar}将在foo不存在或者设置为空的情况下,输出foo:bar并异常终止脚本程序。${foo:+bar}将在foo存在且不为空的情况下返回bar。

{foo#*/}仅仅匹配并且删除最左面的/。{foo##*/}匹配并且删除尽可能多的字符,所以输出了最右面的/及签名的所有字符。

{bar%local*}匹配从右面起到第一次出现local(及跟在它后面的所有字符)。{bar%%local*}从右面起尽可能多的匹配字符,直到遇见最左面的local。

下面的代码是一个例子,使用cjpeg将gif文件转换为jpeg文件:

cjpeg image.gif > image.jpg

若希望对大量的文件执行此操作,输入以下代码:

#!/bin/sh

for image in *.gif
do
    cjpeg $image > ${image%%gif}jpg
done

here文档

向一条命令传递输入的一种特殊方法是使用here文档,允许命令在获得输入数据时就好像是在读取一个文件或键盘一样,实际上是从脚本程序中得到输入数据。以<<开头,紧跟着一个特殊的字符序列,该序列将在文档的结尾处再次出现。<<是shell的标签重定向符,这里强制命令的输入是here文档,特殊字符序列的作用像一个标记,告诉shell here文档结束的位置。由于标记序列不能出现在传递给命令的文档内容中,故应该尽量使其简单而不寻常。

下面的代码是一个例子,使用here文档给cat命令提供输入数据:

#!/bin/sh

cat << !FUNKY! # "cat <<"给cat提供输入数据
hello
this is a here document
!FUNKY!

输出如下:

hello
this is a here document

here文档可以用来调用交互式的程序,比如编辑器,并且向它提供一些事先定义好的输入。更常见的用途是在程序中输出大量的文本,而不需要echo输出每一行,可以在标识符的两侧都使用!来确保不会混淆哦。

下面的代码是一个例子,讲述here文档的另一个用法:

首先a.txt的文件内容如下:

That is line 1
That is line 2
That is line 3
That is line 4

可以通过结合here文档和ed编辑器来编辑这个文件:

#!/bin/sh

ed a.txt << !NARUTO!
3
d
.,\$s/is/was/
w
q
!NARUTO!

exit 0

输出如下:

60
That is line 3
46

文本内容是:

That is line 1
That is line 2
That was line 4

上述代码中调用ed编辑器并且向它传递命令,想让它移动到第三行,然后删除该行,再把当前行(为原来的最后一行)中的is替换成was。完成操作的ed命令来自脚本程序的here文档——在标记!NARUTO!之间的内容。

在here文档中使用\来防止被shell扩展。\的作用是对$继续转义,让shell不用试着$s/is/was扩展为它的值,而它确实也没值。shell把\$传递为$,再由ed编辑器对它进行解释。

调试脚本程序

可以添加echo语句来显示变量的内容,可以通过在shell中交互地输入代码片段来对他们进行测试。跟踪脚本中复杂错误的主要方法是设置shell选项。可以在调用shell时加上命令行选项或者使用set命令。下图是各种选项:

可以使用-o启用set命令的选项标志,+o取消设置。可以使用xtrace得到一份简单的执行跟踪报告。

使用下面的命令启用xtrace选项:

set -o xtrace

再使用下面的命令关闭xtrace选项:

set +o xtrace

变量扩展的层次默认由每行代码前的+号个数指出。

在shell中,还可以通过捕获EXIT,在程序退出时查看程序的状态。具体是在程序的开头添加类似下面的语句:

trap "echo Exiting: critical variable = $critical_variable" EXIT

迈向图像化:dialog工具

dialog的整体思想:一个带有各种参数和选项的程序,可以显示各种类型的图形框,范围涵盖从最简单的yes/no框到输入框,甚至菜单选项。通常在用户执行某类型的输入后返回,返回结果可以通过退出状态获得,或在用户输入文本时,通过标准错误流来获取。

下面的代码是一个例子,创建了一个消息框:

dialog --msgbox "Hello World" 9 18

执行后会显示一个图像化的消息框,可以通过OK对话框关闭,如下图:

下面是可以创建的对话框的类型:

若想获得任何类型的允许文本输入或进行选择的对话框的输出,你必须捕获标准错误流,通常是把它指向某个临时文件以便后续处理。要想获得yes/no对话框的输出结果,只需查看退出码,0表示成功,1表示失败。

所有对话框都有各种控制的参数,比如控制显示的对话框的大小和形状,如下:

所有的对话框都有几个相同的选项,如--title:指定对话框的标题,--clear完成清屏操作。

下面的代码是一个例子,将创建一个标题位check me的复选框,包含一条提示信息pick numbers。复选框高15字符,宽25字符。每个选项高3字符,最后列出要显示的选项并且设置了默认的开关选择:

dialog --title "check me" --checklist "pick numbers" 15 25 3 1 "one" "off" 2 "two" "on" 3 "three" "off"

输出如下:

上述代码中--checklist用于创建一个复选框,--title将标题设置为check me,下一个参数是提示信息pick numbers,然后是对话框的大小,高15行,宽25字符,3行被用于菜单。选项的设置有三个值:编号、文本、状态。第一个菜单项的编号是1,显示的文本是one,状态设置为off,后面依次自己看吧。

下面的代码是一个例子,这个程序关注用户的响应:

(1)首先通过对话框告诉用户发生的事情。不需要获得返回值或者任何用户的输入:

#!/bin/sh

# Ask some questions and collect the answer

dialog --title "FAQ" --msgbox "welcome to bilibili" 9 18

输出如下:

(2)然后使用一个yes/no对话框来询问用户是否要继续操作。使用环境变量$?来检查是否选择了yes(返回码为0),若不想继续操作,使用一个简单的信息框显示信息,信息框在退出之前不需要用户输入:

dialog --title "confirm" --yesno "Are you willing to take part?" 9 18
if [ $? != 0 ]; then
    dialog --infobox "thank you anyway" 5 20
    sleep 2
    dialog --clear
    exit 0
fi

输出如下:

(3)使用一个输入框来询问用户的姓名。重定向错误标准流2到临时文件——1.txt,然后再将它放到变量Q_NAME中:

dialog --title "FAQ" --inputbox "Your name" 9 30 2>_1.txt
Q_NAME=$(cat _1.txt)

输出如下:

(4)现在显示一个菜单,有4个选项,再次重定向标准错误流并且把它转载到一个变量中:

dialog --menu "$Q_NAME, what music do you like best?" 15 30 4 1 "classic" 2 "jazz" 3 "country" 4 "other" 2>_1.txt
Q_MUSIC=$(cat _1.txt)

输出如下:

(5)选择的编号将保存到_1.txt中,而且结果被后置到变量Q_MUSIC中,便于对结果测试:

if [ "$Q_MUSIC" = "1" ]; then
    dialog --title "like classical" --msgbox "good choice" 12 25
else
    dialog --title "doesn't like classical" --msgbox "shame" 12 25
fi

输出如下:

(6)最后,清除对话框并且退出程序:

sleep 2
dialog --clear
exit 0

综合应用

上述两个文件通过编号结合在一起。需要决定的事情是如何分隔数据项,本例采用csv文件(使用逗号)。下面是将使用的函数:

get_return()
get_confirm()
set_menu_chioce()
insert_title()
insert_track()
add_record_track()
add_records()
find_cd()
update_cd()
count_cds()
remove_records()
list_tracks()

CD唱片应用程序

(1)版权信息

#!/bin/bash

# Very simple example shell script for managing a CD collection.
# Copyright (C) 1996-2007 Wiley Publishing Inc.
# This program is free software; you can redistribute is and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your option) any later version.

# This program is distributed in the hopes that it will be useful, but
# WITHOUT ANY WARRANTY ; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOS   E. See the GNU General
# Public License for more details.

# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.

(2)首先设置脚本中将用到的全局变量,包括标题文件、曲目文件和一个临时文件。还设置了ctrl+c组合键的中断处理,以确保程序在用户中断脚本程序时删除临时文件:

menu_choice=""
current_cd=""
title_file="title.cdb"
tracks_file="tracks.cdb"
temp_file=/tmp.cdb.$$
trap "rm -f $temp_file" EXIT

(3)定义函数。最开始的两个函数是简单的工具型函数:

get_return(){
    echo -e "Press return \c"
    read x
	return 0
}

get_confirm(){
	echo -e "Are you sure? \c"
	while true
	do
		read x
		case "$x" in
			y | yes | Y | Yes | YES )
				return 0;;
			n | no | N | No | NO )
				echo
				echo "Cancelled"
				return 1;;
			* ) echo "Please enter yes or no" ;;
		esac
	done
}

(4)主菜单函数set_menu_choice。菜单内容是动态变化的,当选择了某张CD后,主菜单会多出几个选项:

set_menu_choice() {
	clear
	echo "Option :-"
	echo
	echo "	a) Add new CD"
	echo "	f) Find CD"
	echo "	c) Count the CDs and tracks in the catalog"
	if [ "$cdcatnum" != "" ]; then
		echo "	l) List tracks on $cdtitle"
		echo "	r) Remove $cdtitle"
		echo "	u) Update track information for $cdtitle"
	fi
	echo "	q) Quit"
	echo
	echo -e "Please enter choice then press return \c"
	read menu_choice
	return
}

(5)两个函数inset_title和insert_track,用于向数据库文件里添加数据,然后是函数add_record_tracks,使用模式匹配确保用户未输入逗号(逗号为数据段的分隔符),使用算术操作符在用户输入曲目时递增当前曲目的编号:

insert_title(){
	echo $* >> $title_file
	return
}

insert_track(){
	echo $* >> $tracks_file
	return
}

add_record_tracks(){
	echo "Enter tracks information for this CD"
	echo "When no more tracks enter q"
	cdtrack=1
	cdttitle=""
	while [  "$cdttitle" != "q" ]
	do
		echo -e "Track $cdtrack, track title? \c"
		read tmp
		cdttitle=${tmp%%,*}
		if [ "$tmp" != "$cdttitle" ]; then
			echo "Sorry, no commas allowed"
			continue
		fi
		if [ -n "$cdttitle" ]; then
			if [ "$cdttitle" != "q" ]; then
				insert_track $cdcatnum, $cdtrack, $cdttitle
				fi
			else
				cdtrack=$((cdtrack-1))
			fi
		cdtrack=$((cdtrack+1))
	done
}

(6)add_records用于输入新的CD唱片的标题信息:

add_records(){
	# prompt for the initial information
	
	echo -e "Enter catalog name \c"
	read tmp
	cdcatnum=${tmp%%,*}
	
	echo -e "Enter title \c"
	read tmp
	cdtitle=${tmp%%,*}
	
	echo -e "Enter type \c"
	read tmp
	cdtype=${tmp%%,*}
	
	echo -e "Enter artist/composer \c"
	read tmp
	cdac=${tmp%%,*}
	
	# Check that they want to enter the informatioon
	
	echo About to add new entry
	echo "$cdcatnum $cdtitle $cdtype $cdac"
	
	# If comfirmed then append it to titles file
	
	if get_confirm ; then
		insert_title $cdcatnum, $cdtitle, $cdtype, $cdac
		add_record_track
	else
		remove_records
	fi
	
	return
}

(7)find_cd的作用是使用grep在CD唱片标题文件中查找CD唱片有关的资料。需要知道查询字符串在标题文件中出现的次数,但是grep命令只能输出该字符串是匹配了零次还是多次。采取的方法是把grep的输出保存到临时文件中,文件的每一行对应一次匹配,然后再统计该文件的次数。

动词统计命令wc在其输出中使用空格符分隔被统计文件中的行数、单词数和字符个数。使用$(wc -l  $tmp_file)标记从wc命令的输出结果中提取第一个参数,并且赋值给linesfound。若要用到wc命令输出中的其他参数,可以利用set命令把shell参数变量设置为wc命令的输出结果。将IFS(内部数据字段分隔符)设置为一个逗号,此时,可以读取以逗号分隔的数据字段了,另一个可选择的命令是cut:

find_cd(){
	if [ "$1" = "n" ]; then
		asklist=n
	else
		asklist=y
	fi
	cdcatnum=""
	echo -e "Enter a string to search for in the CD titles \c"
	read searchstr
	if [ "$searchstr" ="" ]; then
		return 0
	fi
	
	grep "$searchstr" $title_file > $temp_file
	
	set $(wc -l $temp_file)
	
	linesfound=$1
	
	case "$linesfound" in
	0 )	echo "Sorry, nothing found"
		get_return
		return 0
		;;
	1 )	;;
	2 ) echo "Sorry, not unique."
		echo "Found the following"
		cat $temp_file
		get_return
		return 0
	esac
	
	IFS=","
	read cacatnum cdtitle cdtype cdac < $temp_file
	IFS=" "
	
	if [ -z "$cdcatnum" ]; then # 长度为0则为真
		echo "Sorry, could not ectract catalog field from $temp_file"
		get_return
		return 0
	fi
	echo
	echo Catalog number: $cacatnum
	echo Title: $cdtitle
	echo Type: $cdtype
	echo Artists/Composer: $cdac
	echo
	get_return
	
	if [ "$asklist" = "y" ]; then
		echo -e "View tracks for the CD? \c"
			read x
		if [ "$x" = "y" ]; then
			echo
			list_tracks
			echo
		fi
	fi	
	return 1
}

(8)update_cd用于重新输入CD唱片的资料。搜索的行(使用grep命令)是以$cdcatnum开头(通过标志^)并且其后跟着一个逗号,因此需要把$cdcatnum的扩展放在一对{}里,这样就可以搜索紧跟在CD目录编号之后的逗号。此函数还在get_confirm返回true时,使用{}将要执行的多个语句组成一个语句块:

update_cd(){
	if [ -z "$cdcatnum" ]; then
		echo "You must select a CD first"
		find_cd n
	fi
	if [ -n "$cdcatnum" ]; then
		echo "Current tracks are : -"
		list_tracks
		echo
		echo "This will re-enter the tracks for $cdtitle"
		get_confirm && {
			grep -v "^${cdcatnum}, " $tracks_file > $temp_file
			mv $temp_file $tracks_file
			echo
			add_record_tracks
		}
	fi
	return
}

(9)count_cds用于统计CD唱片个数与曲目总数:

count_cds(){
	set $(wc -l $title_file)
	num_titles=$1
	set $(wc -l $track_file)
	num_tracks=$1
	echo found $num_titles CDs, with a total of $num_tracks tracks
	get_return
	return
}

(10)remov_records用于从数据库文件中删除数据项,通过grep -v删除所有匹配的字符串,注意必须使用临时文件来完成。

若输入:

grep -v "^$cdcatnum" > $title_file

$title_file就会在grep开始执行前被>重定向操作设置为空文件,导致grep从空文件读取数据:

remove_records(){
	if [ -z "$cdcatnum" ]; then
		echo You must select a CD first
		find_cd n
	fi
	if [ -n "$cdcatnum" ]; then
		echo "You are about to delete $cdtitle"
		get_confirm &&{
			grep -v "^${cdcatnum}," $title_file > $temp_file
			mv $temp_file $title_file
			grep -v "^${cdcatnum}," $tracks_file > $temp_file
			mv $temp_file $tracks_file
			cdcatnum=""
			echo Entry removed
		}
		get_return
	fi
	return
}

(11)list_tracks使用grep找出想要找的行,通过cut命令来访问想要的字段,通过more提供按页输出:

list_tracks(){
	if [ "$cdcatnum" = ""]; then
		echo no CD selected yet
		return
	else
		grep "^{cdcatnum}," $tracks_file > $temp_file
		num_tracks=$(wc -l $temp_file)
		if [ "$num_tracks" = "0" ];then
			echo no tracks found for $cdtitle
		else {
			echo
			echo "$cdtitle : -"
			echo
			cut -f 2- -d, $temp_file
			echo
		} | ${PAGER:-more}
		fi
	fi
	get_return
	return
}

(12)函数均已经定义好,下面是主程序,开头的几行先确保需要的文件处于一个已知的状态,然后调用主菜单函数set_menu_choice,根据输出进行相应的操作。若选择退出,程序先删除临时文件,再显示结束信息,最后成功退出(退出码为0):

rm -f $temp_file # -f 即使原档案属性设为唯读,亦直接删除,无需逐一确认。
if [ ! -f $title_file ]; then
	touch $title_file
fi
if [ ! -f $tracks_file ]; then
	touch $tracks_file
fi

# Now the application proper

clear
echo
echo
echo "Mini CD manager"
sleep 1

quit=n
while [ "$quit" != "y" ];
do
	set_menu_choice
	case "$menu_choice" in
		a) add_records;;
		r) remove_records;;
		f) find_cd y;;
		u) update_cds;;
		c) count_cds;;
		l) list_tracks;;
		b)
			echo
			more $title_file
			echo
			get_return;;
		q | Q) quit=y;;
		*) echo "Sorry, choice not recognized";;
	esac
done

# Tidy up and leave

rm -f $temp_file
echo "Finished"
exit 0

应用程序说明

脚本程序的trap用于设置在用户按下ctrl+c组合键的时候的中断处理,根据终端的不同,ctrl+c组合键可能引发EXIT或INT信号。

bash提供的select结构是专门处理菜单选择的结构,但是程序移植性比较差。还可以使用here文档来实现为用户提供多行信息。

实现的程序中,当添加一个CD唱片记录时,程序并没有检查主键,新代码只是忽略使用相同主键的后续唱片标题,但是把它们的曲目添加到第一个使用该主键的CD唱片的曲目清单中。如下:

1 First CD Track 1

2 First CD Track 2

1 Another CD

2 With the same CD key


小结

shell本身就是一种强大的程序设计语言,可以轻松调用其他程序确保对其输出进行处理,这种能力使得shell成为完成文本和文件处理任务的理想工具

终于看完第二章了,长舒一口气,还有很长的路要走,我一定会把这本600多页的书看完的!

猜你喜欢

转载自blog.csdn.net/qq_40061206/article/details/93190709