Linux_Shell脚本学习第一章-小试牛刀(下)

一、前言

在刚学习shell后不久便利用暑假去实习了一段时间,体验了一下嵌入式BSP开发,刚开学不久,继续开始艰苦的Linux学习之旅。

二、调试脚本

2.1 启用shell脚本的跟踪调试功能

2.1.1 使用选项-x,启用shell脚本的跟踪调试功能

$ bash -x script.sh

运行带有-x选项的脚本可以打印出所执行的每一行命令以及当前状态。

2.1.2 使用set -x和set +x对脚本进行部分调试

如下程序

  1 #!/bin/bash
  2 
  3 for i in {1..6};
  4 do
  5         set -x
  6         echo $i
  7         set +x
  8 done
  9 echo "Script executed"

输出如下

+ echo 1
1
+ set +x
+ echo 2
2
+ set +x
+ echo 3
3
+ set +x
+ echo 4
4
+ set +x
+ echo 5
5
+ set +x
+ echo 6
6
+ set +x
Script executed

2.1.3 自定义调试信息

前面介绍的调试方法是Bash内建的。它们以固定的格式生成调试信息。但是在很多情况下,我们需要使用自定义的调试信息。可以通过定义 _DEBUG环境变量来启用或禁止调试及生成特定形式的信息。
代码如下:

  1 #!/bin/bash
  2 
  3 function DEBUG()
  4 {
  5         [ "$__DEBUG" == "on"  ] && $@ || :
  6 }
  7 
  8 for i in {1..10}
  9 do
 10         DEBUG echo "I is $i"
 11 done

可以将调试功能设置为on来运行上面的脚本:

$ _DEBUG=on ./script.sh

我们在每一条需要打印调试信息的语句前加上DEBUG。如果没有把 _DEBUG=on传递给脚本,那么调试信息就不会打印出来。在Bash中,命令:告诉shell不要进行任何操作。

三、函数和参数

函数和别名乍一看很相似,不过两者在行为上还是略有不同。最大的差异在于函数参数可以在函数体中任意位置上使用,而别名只能将参数放在命令尾部。

3.1 函数的定义

函数的定义包括function命令、函数名、开/闭括号以及包含在一对花括号中的函数体。
函数可以这样定义:

function fname()
{
statements;
}

或者

fname()
{
statements;
}

甚至是这样(对于简单的函数):

fname() { statement; }

3.2 函数的调用

只需使用函数名就可以调用函数:

$ fname ; #执行函数

3.3 函数的参数访问

函数参数可以按位置访问,$1是第一个参数,$2是第二个参数,以此类推:

fname arg1 arg2 ; #传递参数

以下是函数fname的定义。在函数fname中,包含了各种访问函数参数的方法。

fname()
{
echo $1, $2; #访问参数1和参数2
echo "$@"; #以列表的方式一次性打印所有参数
echo "$*"; #类似于$@,但是所有参数被视为单个实体
return 0; #返回值
}

传入脚本的参数可以通过下列形式访问。
$0是脚本名称。
$1是第一个参数。
$2是第二个参数。
$n是第n个参数。
$@被扩展成$1 $2 $3等。
$*被扩展成$1c$2c$3,其中c是IFS的第一个字符。

3.4 函数与别名的比较

3.4.1 下面的这个别名通过将ls的输出传入grep来显示文件子集。别名的参数添加到命令的尾部,因此lsg txt就被扩展成了ls | grep txt:

$> alias lsg='ls | grep'
$> lsg txt
file1.txt
file2.txt
file3.txt

b.如果想获得/sbin/ifconfig文件中设备对应的IP地址,可以尝试这样做:

$> alias wontWork='/sbin/ifconfig | grep'
$> wontWork eth0
eth0 Link encap:Ethernet HWaddr 00:11::22::33::44:55

c.grep命令找到的是字符串eth0,而不是IP地址。如果我们使用函数来实现的话,可以将
设备名作为参数传入ifconfig,不再交给grep:

$> function getIP() { /sbin/ifconfig $1 | grep 'inet '; }
$> getIP eth0
inet addr:192.168.1.2 Bcast:192.168.255.255 Mask:255.255.0.0

3.5 导出函数

函数也能像环境变量一样用export导出,如此一来,函数的作用域就可以扩展到子进程中:

export -f fname
$> function getIP() { /sbin/ifconfig $1 | grep 'inet '; }
$> echo "getIP eth0" >test.sh
$> sh test.sh
sh: getIP: No such file or directory
$> export -f getIP
$> sh test.sh
inet addr: 192.168.1.2 Bcast: 192.168.255.255 Mask:255.255.0.0

四、将一个命令的输出发送给另一个命令

4.1 预备知识

命令输入通常来自于stdin或参数。输出可以发送给stdout或stderr。当我们组合多个命令时,通常将stdin用于输入,stdout用于输出。我们使用管道(pipe)连接每个过滤器,管道操作符是|。例如:

$ cmd1 | cmd2 | cmd3

这里我们组合了3个命令。cmd1的输出传递给cmd2,cmd2的输出传递给cmd3,最终的输出(来自cmd3)会出现在显示器中或被导入某个文件。

4.2 实战演练

4.2.1 组合两个命令

$ ls | cat -n > out.txt

ls(列出当前目录内容)的输出被传给cat -n,后者为通过stdin所接收到的输入内容加上行号,然后将输出重定向到文件out.txt。

4.2.2 将命令序列的输出赋给变量

cmd_output=$(ls | cat -n)
echo $cmd_output

或者

cmd_output=`ls | cat -n`
echo $cmd_output

4.3 利用子shell生成一个独立的进程

子shell本身就是独立的进程。可以使用()操作符来定义一个子shell。

$> pwd
/
$> (cd /bin; ls)
awk bash cat...
$> pwd
/

当命令在子shell中执行时,不会对当前shell造成任何影响;所有的改变仅限于该子shell内。例如,当用cd命令改变子shell的当前目录时,这种变化不会反映到主shell环境中。

假设我们使用子shell或反引用的方法将命令的输出保存到变量中,为了保留输出的空格和换行符(\n),必须使用双引号。例如:

$ cat text.txt
1
2
3
$ out=$(cat text.txt)
$ echo $out
1 2 3 # 丢失了1、2、3中的\n
$ out="$(cat text.txt)"
$ echo $out
1
2
3

五、在不按下回车键的情况下读入n 个字符

5.1 read命令

5.1.1 下面的语句从输入中读取n个字符并存入变量variable_name:

read -n number_of_chars variable_name

例如:

$ read -n 2 var
$ echo $var

5.1.2 用无回显的方式读取

read -s var

5.1.3 使用read显示提示信息

read -p "Enter input:" var

5.1.4 在给定时限内读取输入

read -t timeout var

例如:

$ read -t 2 var
#在2秒内将键入的字符串读入变量var

5.1.5 用特定的定界符作为输入行的结束

read -d delim_char var

例如:

$ read -d ":" var
hello: #var被设置为hello

六、持续运行命令直至执行成功

有时候命令只有在满足某些条件时才能够成功执行。例如,在下载文件之前必须先创建该文件。这种情况下,你可能希望重复执行命令,直到成功为止。

6.1 repeat函数

定义如下函数:

repeat()
{
	while true
	do
	$@ && return
	done
}

函数repeat()中包含了一个无限while循环,该循环执行以函数参数形式(通过$@访问)传入的命令。如果命令执行成功,则返回,进而退出循环。

6.2 一种更快的做法

在大多数现代系统中,true是作为/bin中的一个二进制文件来实现的。这就意味着每执行一次之前提到的while循环,shell就不得不生成一个进程。为了避免这种情况,可以使用shell的内建命令:,该命令的退出状态总是为0:

repeat() { while :; do $@ && return; done }

6.2 加入延时

假设你要用repeat()从Internet上下载一个暂时不可用的文件,不过这个文件只需要等一会就能下载。一种方法如下:

repeat wget -c http://www.example.com/software-0.1.tar.gz

如果采用这种形式,会产生很多发往www.example.com的流量,有可能会对服务器造成影响。(可能也会牵连到你自己;如果服务器认为你是在向其发起攻击,就会把你的IP地址列入黑名单。)要解决这个问题,我们可以修改函数,加入一段延时:

repeat() { while :; do $@ && return; sleep 30; done }

这样命令每30秒才会运行一次。

七、字段分隔符与迭代器

7.1 内部字段分隔符IFS

内部字段分隔符(Internal Field Separator,IFS)是shell脚本编程中的一个重要概念。在处理文本数据时,它的作用可不小。

作为分隔符,IFS有其特殊用途。它是一个环境变量,其中保存了用于分隔的字符。它是当前shell环境使用的默认定界字符串。

考虑一种情形:我们需要迭代一个字符串或逗号分隔型数值(Comma Separated Value,CSV)中的单词。如果是前者,可以使用IFS=" “;如果是后者,则使用IFS=”,"。

7.2 IFS实例

考虑CSV数据的情况:

data="name, gender,rollno,location"

我们可以使用IFS读取变量中的每一个条目。

oldIFS=$IFS
IFS=, #IFS现在被设置为,
for item in $data;
do
echo Item: $item
done
IFS=$oldIFS

输出如下:

Item: name
Item: gender
Item: rollno
Item: location

IFS的默认值为空白字符(换行符、制表符或者空格)。
当IFS被设置为逗号时,shell将逗号视为一个定界符,因此变量$item在每次迭代中读取由逗号分隔的子串作为变量值。
如果没有把IFS设置成逗号,那么上面的脚本会将全部数据作为单个字符串打印出来。

7.3 IFS的另一种用法

在文件/etc/passwd中,每一行包含了由冒号分隔的多个条目。该文件中的每行都对应着某个用户的相关属性。
考虑这样的输入:root❌0:0:root:/root:/bin/bash。每行的最后一项指定了用户的默认shell。
可以按照下面的方法巧妙地利用IFS打印出用户以及他们默认的shell:

#!/bin/bash
#用途: 演示IFS的用法
line="root:x:0:0:root:/root:/bin/bash"
oldIFS=$IFS;
IFS=":"
count=0
for item in $line;
do
[ $count -eq 0 ] && user=$item;
[ $count -eq 6 ] && shell=$item;
let count++
done;
IFS=$oldIFS
echo $user's shell is $shell;

输出为:

root's shell is /bin/bash

7.3 多种类型的循环

7.3.1 面向列表的for循环

for var in list;
do
	commands; #使用变量$var
done

list可以是一个字符串,也可以是一个值序列。
我们可以使用echo命令生成各种值序列:

echo {1..50}; #生成一个从1~50的数字序列
echo {a..z} {A..Z}; #生成大小写字母序列

同样,我们可以将这些方法结合起来对数据进行拼接(concatenate)。
下面的代码中,变量i在每次迭代的过程里都会保存一个范围在a到z之间的字符:

for i in {a..z}; do actions; done;

7.3.2 迭代指定范围的数字

for((i=0;i<10;i++))
{
	commands; #使用变量$i
}

7.3.2 循环到条件满足为止

当条件为真时,while循环继续执行;当条件不为真时,until循环继续执行。

while condition
do
	commands;
done

用true作为循环条件能够产生无限循环。

7.3.2 until循环

在Bash中还可以使用一个特殊的循环until。它会一直循环,直到给定的条件为真。例如:

x=0;
until [ $x -eq 9 ]; #条件是[$x -eq 9 ]
do
	let x++; echo $x;
done

七、比较与测试

7.1 if else

7.1.1 if条件

if condition;
then
	commands;
fi

7.1.1 else if和else

if condition;
then
	commands;
else if condition; then
	commands;
else
	commands;
fi
	 

7.2 逻辑运算符

[ condition ] && action; # 如果condition为真,则执行action
[ condition ] || action; # 如果condition为假,则执行action

&&是逻辑与运算符,||是逻辑或运算符。编写Bash脚本时,这是一个很有用的技巧。

7.3 算术比较

比较条件通常被放置在封闭的中括号内。一定要注意在[或]与操作数之间有一个空格。如果忘记了这个空格,脚本就会报错。

[$var -eq 0 ] or [ $var -eq 0]

对变量或值进行算术条件测试:

[ $var -eq 0 ] #当$var等于0时,返回真
[ $var -ne 0 ] #当$var不为0时,返回真

其他重要的操作符如下。

● -gt:大于。
● -lt:小于。
● -ge:大于或等于。
● -le:小于或等于。

-a是逻辑与操作符,-o是逻辑或操作符。可以按照下面的方法结合多个条件进行测试:

[ $var1 -ne 0 -a $var2 -gt 2 ] #使用逻辑与-a
[ $var1 -ne 0 -o $var2 -gt 2 ] #逻辑或-o

7.4 文件系统相关测试

我们可以使用不同的条件标志测试各种文件系统相关的属性。
● [ -f $file_var ]:如果给定的变量包含正常的文件路径或文件名,则返回真。
● [ -x $var ]:如果给定的变量包含的文件可执行,则返回真。
● [ -d $var ]:如果给定的变量包含的是目录,则返回真。
● [ -e $var ]:如果给定的变量包含的文件存在,则返回真。
● [ -c $var ]:如果给定的变量包含的是一个字符设备文件的路径,则返回真。
● [ -b $var ]:如果给定的变量包含的是一个块设备文件的路径,则返回真。
● [ -w $var ]:如果给定的变量包含的文件可写,则返回真。
● [ -r $var ]:如果给定的变量包含的文件可读,则返回真。
● [ -L $var ]:如果给定的变量包含的是一个符号链接,则返回真。
例如:

fpath="/etc/passwd"
if [ -e $fpath ]; then
echo File exists;
else
echo Does not exist;
fi

7.5 字符串比较

进行字符串比较时,最好用双中括号,因为有时候采用单个中括号会产生错误。

7.5.1 测试两个字符串是否相同

● [[ $str1 = $str2 ]]:当str1等于str2时,返回真。也就是说,str1和str2包
含的文本是一模一样的。
● [[ $str1 == $str2 ]]:这是检查字符串是否相同的另一种写法。

7.5.2 测试两个字符串是否不同

● [[ $str1 != $str2 ]]:如果str1和str2不相同,则返回真。

7.5.3 找出在字母表中靠后的字符串

字符串是依据字符的ASCII值进行比较的。例如,A的值是0x41,a的值是0x61。因此,A小于a,AAa小于Aaa。
● [[ $str1 > $str2 ]]:如果str1的字母序比str2大,则返回真。
● [[ $str1 < $str2 ]]:如果str1的字母序比str2小,则返回真。

7.5.4 测试空串

● [[ -z $str1 ]]:如果str1为空串,则返回真。
● [[ -n $str1 ]]:如果str1不为空串,则返回真。

7.5.5 组合测试

使用逻辑运算符 && 和 || 能够很容易地将多个条件组合起来:

if [[ -n $str1 ]] && [[ -z $str2 ]] ;
then
	commands;
fi

例如:

str1="Not empty "
str2=""
if [[ -n $str1 ]] && [[ -z $str2 ]];
then
echo str1 is nonempty and str2 is empty string.
fi

输出如下:

str1 is nonempty and str2 is empty string.

7.5 test命令

test命令可以用来测试条件。用test可以避免使用过多的括号,增强代码的可读性。之前讲过的[]中的测试条件同样可以用于test命令。例如:

if [ $var -eq 0 ]; then echo "True"; fi

也可以写成:

if test $var -eq 0 ; then echo "True"; fi

八、使用配置文件定制bash

你在命令行中输入的绝大部分命令都可以放置在一个特殊的文件中,留待登录或启动新的bash会话时执行。将函数定义、别名以及环境变量设置放置在这种特殊文件中,是一种定制shell的常用方法。
放入配置文件中的常见命令如下:

# 定义ls命令使用的颜色
LS_COLORS='no=00:di=01;46:ln=00;36:pi=40;33:so=00;35:bd=40;33;01'
export LS_COLORS
# 主提示符
PS1='Hello $USER'; export PS1
# 正常路径之外的个人应用程序安装目录
PATH=$PATH:/opt/MySpecialApplication/bin; export PATH
# 常用命令的便捷方式
function lc () {/bin/ls -C $* ; }

8.1 用户登录时执行的文件

当用户登录shell时,会执行下列文件:

/etc/profile, $HOME/.profile, $HOME/.bash_login, $HOME/.bash_profile /

注意,如果你是通过图形化登录管理器登入的话,是不会执行/etc/profile、
~/.profile和$HOME/.bash_profile这3个文件的。这是因为图形化窗口管理器并不会启动shell。当你打开终端窗口时才会创建shell,但这个shell也不是登录shell。
如果.bash_profile或.bash_login文件存在,则不会去读取.profile文件

8.2 启动交互式shell时执行

交互式shell(如X11终端会话)或ssh执行单条命令(如ssh 192.168.1.1 ls /tmp)时,会读取并执行以下文件:

/etc/bash.bashrc $HOME/.bashrc

8.3 调用shell处理脚本文件时执行的

如果运行如下脚本:

$> cat myscript.sh
#!/bin/bash
echo "Running"

不会执行任何配置文件,除非定义了环境变量BASH_ENV:

$> export BASH_ENV=~/.bashrc
$> ./myscript.sh

使用ssh运行下列命令时:

ssh 192.168.1.100 ls /tmp

会启动一个bash shell,读取并执行/etc/bash.bashrc和$HOME/.bashrc,但不包括/etc/profile或.profile。

如果调用ssh登录会话:

ssh 192.168.1.100

这会创建一个新的登录bash shell,该shell会读取并执行以下文件:

/etc/profile
/etc/bash.bashrc
$HOME/.profile or .bashrc_profile
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章