欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-11 12:05:23
...

一、前言

在刚学习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"  ] && [email protected] || :
  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 "[email protected]"; #以列表的方式一次性打印所有参数
echo "$*"; #类似于[email protected],但是所有参数被视为单个实体
return 0; #返回值
}

传入脚本的参数可以通过下列形式访问。
$0是脚本名称。
$1是第一个参数。
$2是第二个参数。
$n是第n个参数。
[email protected]被扩展成$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
	[email protected] && return
	done
}

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

6.2 一种更快的做法

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

repeat() { while :; do [email protected] && 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 [email protected] && 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