准备工作

前言

开篇主要介绍什么是 Shell,Shell 运行环境,Shell 基本语法和调试技巧。

什么是 Shell

首先让我们从下图看看 Shell 在整个操作系统中所处的位置吧,该图的外圆描述了整个操作系统(比如 Debian/Ubuntu/Slackware 等),内圆描述了操作系统的核心(比如 Linux Kernel),而 ShellGUI 一样作为用户和操作系统之间的接口。

Shell和GUI用户接口

GUI 提供了一种图形化的用户接口,使用起来非常简便易学;而 Shell 则为用户提供了一种命令行的接口,接收用户的键盘输入,并分析和执行输入字符串中的命令,然后给用户返回执行结果,使用起来可能会复杂一些,但是由于占用的资源少,而且在操作熟练以后可能会提高工作效率,而且具有批处理的功能,因此在某些应用场合还非常流行。

Shell 作为一种用户接口,它实际上是一个能够解释和分析用户键盘输入,执行输入中的命令,然后返回结果的一个解释程序(Interpreter,例如在 linux 下比较常用的 Bash),我们可以通过下面的命令查看当前的 Shell

$ echo $Shell
/bin/bash
$ ls -l /bin/bash
-rwxr-xr-x 1 root root 702160 2008-05-13 02:33 /bin/bash

既然该程序可以解释具有一定语法结构的文件,那么我们就可以遵循某一语法来编写它,它有什么样的语法,如何运行,如何调试呢?下面我们以 Bash 为例来讨论这几个方面。

搭建运行环境

为了方便后面的练习,我们先搭建一个基本运行环境:在一个 Linux 操作系统中,有一个运行有 Bash 的命令行在等待我们键入命令,这个命令行可以是图形界面下的 Terminal (例如 Ubuntu 下非常厉害的 Terminator),也可以是字符界面的 Console (可以用 CTRL+ALT+F1~6 切换),如果你发现当前 Shell 不是 Bash,请用下面的方法替换它:

$ chsh $USER -s /bin/bash
$ su $USER

或者是简单地键入Bash:

$ bash
$ echo $SHELL  # 确认一下
/bin/bash

有了基本的运行环境,那么如何来运行用户键入的命令或者是用户编写好的脚本文件呢 ?

假设我们编写好了一个 Shell 脚本,叫 test.sh

第一种方法是确保我们执行的命令具有可执行权限,然后直接键入该命令执行它:

$ chmod +x /path/to/test.sh
$ /path/to/test.sh

第二种方法是直接把脚本作为 Bash 解释器的参数传入:

$ bash /path/to/test.sh

$ source /path/to/test.sh

$ . /path/to/test.sh

基本语法介绍

先来一个 Hello, World 程序。

下面来介绍一个 Shell 程序的基本结构,以 Hello, World 为例:

#!/bin/bash -v
# test.sh
echo "Hello, World"

把上述代码保存为 test.sh,然后通过上面两种不同方式运行,可以看到如下效果。

方法一:

$ chmod +x test.sh
$ ./test.sh
 ./test.sh
 #!/bin/bash -v

 echo "Hello, World"
 Hello, World

方法二:

$ bash test.sh
Hello, World

$ source test.sh
Hello, World

$ . test.sh
Hello, World

我们发现两者运行结果有区别,为什么呢?这里我们需要关注一下 test.sh 文件的内容,它仅仅有两行,第二行打印了 Hello, World,两种方法都达到了目的,但是第一种方法却多打印了脚本文件本身的内容,为什么呢?

原因在该文件的第一行,当我们直接运行该脚本文件时,该行告诉操作系统使用用#! 符号之后面的解释器以及相应的参数来解释该脚本文件,通过分析第一行,我们发现对应的解释器以及参数是 /bin/bash -v,而 -v 刚好就是要打印程序的源代码;但是我们在用第二种方法时没有给 Bash 传递任何额外的参数,因此,它仅仅解释了脚本文件本身。

Shell 程序设计过程

Shell 语言作为解释型语言,它的程序设计过程跟编译型语言有些区别,其基本过程如下:

  • 设计算法
  • 用 Shell 编写脚本程序实现算法
  • 直接运行脚本程序

可见它没有编译型语言的"麻烦的"编译和链接过程,不过正是因为这样,它出错时调试起来不是很方便,因为语法错误和逻辑错误都在运行时出现。下面我们简单介绍一下调试方法。

调试方法介绍

可以直接参考资料:Shell 脚本调试技术 或者 BASH 的调试手段

echo 命令

默认情况下,echo在每次调用后都会添加一个换行符

chu888chu888@ubuntul-dev:~$ echo "Welcome to Bash"
Welcome to Bash
chu888chu888@ubuntul-dev:~$ echo Welecome to Bash
Welecome to Bash
chu888chu888@ubuntul-dev:~$ echo 'Welcome to Bash'
Welcome to Bash
chu888chu888@ubuntul-dev:~$

一些特殊字符在输出的时候我们需要转义,那么就不要把他放到双引号中,我们需要转义一下

chu888chu888@ubuntul-dev:~$ echo "cannot include exclamation -! within double quotes"
cannot include exclamation -! within double quotes

chu888chu888@ubuntul-dev:~$ echo 'cannot include exclamation - ! within double quotes'
cannot include exclamation - ! within double quotes

在默认情况下,echo会将一个换行符追加到输出文本的尾部,可以使用选项-n来忽略结尾的换行符.echo同样接受双引号字符串内的转义符作为参数.如果需要使用转义序列,则采用echo -e "包括转义序列的字符串"这种形式.

chu888chu888@ubuntul-dev:~$ echo -n "我没有换行"
我没有换行chu888chu888@ubuntul-dev:~$ 

chu888chu888@ubuntul-dev:~$ echo -e "我支持转义符 \n1\t2\t3"
我支持转义符 
1    2    3

在终端中打印颜色是比较常用的方法,主要是为了让用户注意.比如重置=0 黑色=30 红色=31 绿色=32 黄色=33 蓝色=34 洋红色=35 青色=36 白色=37.

要打印彩色文本,可输入如下命令:

chu888chu888@ubuntul-dev:~$ echo -e "\e[1;31m This is red text \e[0m"
 This is red text 

我们来解释一下这段脚本.
chu888chu888@ubuntul-dev:~$ echo -e "\e[1;31m \e[0m" 为主体
\e[1;31m将颜色设为红色 \e[0m为将颜色置回 .只需要替换31就可以了.

printf 命令

printf使用引用文本或由空格分隔的参数.我们可以在printf中使用格式化字符串,还可以指定字符串的宽度/左右对齐方式等.

#!/bin/bash
printf "%-5s %-10s %-4s\n" No Name Mark
printf "%-5s %-10s %-4.2f\n" 1 Sarath 80.3456
printf "%-5s %-10s %-4.2f\n" 2 James 90.9989
printf "%-5s %-10s %-4.2f\n" 3 Jeff 77.564

chu888chu888@ubuntul-dev:~$ bash printf.sh 
No    Name       Mark
1     Sarath     80.34
2     James      90.99
3     Jeff       77.56

我们来解释一下上面的例子是如何实现的. %s %c %d %f都是格式替换符,其所对应的参数可以置于带引号的格式字符串之后.

%-5s指明了一个格式为左对齐宽度为5的字符串替换(-表示左对齐).如果不用-指定对齐方式,字符串就采用右对齐形式.宽度指定了保留给某个变量的字符数.对Name而言,保留宽度为10.因此任何Name字段的内容都会被显示在10字符宽的保留区域内,如果内容不足10个字符,余下的则以空格符填充.

%-4.2f,其中.2指定保留2个小数位.注意,在每行格式字符串后都有一个换行符(\n)

环境变量

变量是任何一种编程语言都必不可少的组成部分,用于存放各类数据.脚本语言通常不需要在使用变量之前声明其类型.只需要直接赋值就可以了.在Bash中,每一个变量的值都是字符串.无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储.有一些特殊的变量会被Shell环境与操作系统用来存储一些特别的值.这类变量被称为环境变量.

#!/bin/bash
fruit=apple
count=5
echo "we have $count ${fruit}(s)"

chu888chu888@ubuntul-dev:~$ bash test.sh
we have 5 apple(s)

我们可能通过export命令导入环境变量

chu888chu888@ubuntul-dev:~$ HTTP_PROXY=192.168.1.23:3128
chu888chu888@ubuntul-dev:~$ export HTTP_PROXY
chu888chu888@ubuntul-dev:~$ echo $HTTP_PROXY
192.168.1.23:3128

获取字符串长度

chu888chu888@ubuntul-dev:~$ var=1234567890
chu888chu888@ubuntul-dev:~$ echo ${#var}
10

识别当前的使有物shell

chu888chu888@ubuntul-dev:~$ echo $SHELL
/bin/bash
chu888chu888@ubuntul-dev:~$ echo $0
-bash

检查用户是否为超级用户

UID是一个重要的环境变量,可以用于监测当前脚本是以超级用户还是普通用户的身份运行的.例如:

#!/bin/bash
if [ $UID -ne 0 ];then
        echo "你不是root用户,请运行在root用户下面!"
else
        echo "你是root用户"
fi

修改Bash提示字符串(username@hostname:-$)

当我们打开终端或是运行Shell时,会看到类似于user@hostname:/home/$的提示字符串.不同GNU/Linux发行版中的颜色及提示略有不同.我们可以利用PS1的环境变量来定制提示文本.默认的Shell提示文本是在文件~/.bashrc中的某一行设置的.

可以使用如下命令列出设置变量PS1的那一行:

chu888chu888@ubuntul-dev:~$ cat ~/.bashrc |grep PS1
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
    PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"

文件描述符与重定向

文件描述符是与文件输入/文件输出相关取的整数,它们用来跟踪已打开的文件.最常见的文件描述符是stdin stdout stderr.我们甚至可以将某个文件描述符的内容重定向到另一个文件描述符中.

预备知识

在编写脚本的时候会频繁的使用标准输入(stdin) 标准输出(stdout) 标准错误(stderr) .通过内容过滤将输出重定向到文件是我们平日里基本任务之一.当命令输出文本时,这些输出文本有可能是错误信息,也有可能是正常的输出信息.

文件描述符是与某个打开的文件或数据流相关取的整数.文件描述符0 1 2是系统预留的

整数描述 内容
0 标准输入
1 标信输出
2 标准错误

实战演练

1 下面的方法可以将输出文本重定向或保存到一个文件中:

$echo "this is sample test 1">temp.txt

2 下面的方法可以将输出文件追击到目标文件中:

$echo "this is sample test 2">>temp.txt

3 如果有错误信息的话,我们可以采用这种方法

要注意2与1后面要紧紧的跟着空格

$ ls + 2>stderr.txt 1>stdout.txt

将文件重定向到命令

数组与关联数组

数组是shell脚本非党重要的组成部分,它借助索引将多个独立的数据存储为一个集合.普通数组只能使用整数作为数组索引.Bash也支持关联数组,它可以使用字符串作为数组索相.在很多情况下,采用字符串索引更容易理解,这时候关取数组就派上用场了.

1 定义数组的方法有许多.可以在单行中使用一列值来定义一个数组

#!/bin/bash

#定义数组
array_var=(1 2 3 4 5)
array_var1[0]="test1"
array_var1[1]="test2"

#打印数组
echo ${array_var[0]}
index=1
echo ${array_var1[$index]}

#以清单的形式打印数组
echo ${array_var[*]}

echo ${array_var[@]}

#打印数组的长度
echo ${#array_var[*]}

2 定义关联数组

在关联数组中,我们可以用任意的文本作为数组索引,首先,需要使用声明语句将一个变量名声明为关联数组.像下面这样:

$declare -A ass_array

利用内嵌"索相-值"列表的方法,提供一个"索引-值"列表:

ass_array=([index1]=var1 [index2]=var2)

使用独立的索相值进行赋值:

ass_array[index1]=var1
ass_array[index2]=var2

简单的例子:

#!/bin/bash
declare -A fruits_value
fruits_value=([apple]='100' [orange]='150')
echo "Apple costs ${fruits_value[apple]}"

使用别名

别名就是一种快捷方式,以省去用户输出一长串命令序列的麻烦.

为安装命令apt-get install 创建一个别名: 这样一来,我们就可以用install pidgin代替sudo apt-get install pidgin了

$alias install='sudo apt-get install'

alias命令的作用只是暂时的,一旦关闭当前终端,所有设置过的别名就失效了.为了使别名设置一直保持作用,可以将它放入~/.bashrc中.因为每当一个新的shell进程生成时,都会执行~/.bashrc中的命令.

$echo 'alias cmd="command seq"'>>~/.bashrc

如果需要删除别名,只用将其对应的语句从~/.bashrc中删除,或者使用unalias命令.或者使用alias example=,这会取消名为example的别名.

我们可以创建一个别名rm,它能够删除原始文件,同时在backup目录中保留副本.

$ alias rm='cp $@ ~/backup && rm $@'

获取终端信息

编写命令行Shell脚本时,总是避免不了大量处理当前终端的相关信息,比如行数/列数/光标位置/密码字段等.

1 tput和stty是两个处理终端的工具.

#获取当前终端的行数与列数
chu888chu888@ubuntul-dev:~$ tput cols
178
chu888chu888@ubuntul-dev:~$ tput lines
24
chu888chu888@ubuntul-dev:~$ 

#打印当前终端名
chu888chu888@ubuntul-dev:~$ tput longname
xterm with 256 colorschu888chu888@ubuntul-dev:~$ 


#将光标移到坐标(100,100)处
chu888chu888@ubuntul-dev:~$ tput cup 100 100

#设置终端背景颜色

2 在脚本中生成延时

在下面的例子中,变量count初始化为0,随后每循环一次便增加1.echo语句打印出count的值.我们用tput sc存储光标位置.在每次循环中,通过恢复之前存储的光标位置,在终端中打印出新的count值.恢复光标位置的命令是tput rc.tput ed清除从当前光标位置到行尾之间的所有内容,使得旧的count值可以被清除并写入新值.循环内1秒钟延时是通过sleep命令来实现的.

#!/bin/bash
echo -n Count:
tput sc
count=0;
while true;
do
        if [ $count -lt 4 ];
        then
                let count++;
                sleep 1;
                tput rc
                tput ed
                echo -n $count;
        else exit 0;
        fi
done

调试脚本

调试功能是每一种编程语言都应该实现的重要特性之一,当出现一些始料未及的情况时,用它来生成脚本运行信息.调试可以帮你弄清楚是什么原因使得程序发生崩溃或行为异常.每位系统程序员都应该了解BASH提供的调试选项.

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

$bash -x script.sh

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

命令 作用
set -x 在执行时显示参数与命令
set +x 禁止调试
set -v 当命令进行读取时显示输入
set +v 禁止打印输入
#!/bin/bash
for i in {1..6};
do
        set -x
        echo $i
        set +x

done
echo "script executed"

函数和参数

我们可以创建执行特定任务的函数,也可以创建能够接受参数的函数.

1 定义函数

#!/bin/bash
function funname1()
{
        echo "funname1"
}
funname2()
{
        echo "funname2"
}
funname1;
funname2;

2 传递参数

例子

#!/bin/bash
echo "文件名: $0"
echo "第一参数 : $1"
echo "第二个参数 : $2"
echo "以列表方式一次性打印所有参数: $@"
echo "类似于$@,但是参数被作为单个实体: $*"
echo "几个参数 : $#"

执行

chu888chu888@ubuntul-dev:~$ ./test5.sh Zara Ali
文件名: ./test5.sh
第一参数 : Zara
第二个参数 : Ali
以列表方式一次性打印所有参数: Zara Ali
类似于Zara Ali,但是参数被作为单个实体: Zara Ali
几个参数 : 2

小结

Shell 语言作为一门解释型语言,可以使用大量的现有工具,包括数值计算、符号处理、文件操作、网络操作等,因此,编写过程可能更加高效,但是因为它是解释型的,需要在执行过程中从磁盘上不断调用外部的程序并进行进程之间的切换,在运行效率方面可能有劣势,所以我们应该根据应用场合选择使用 Shell 或是用其他的语言来编程。

results matching ""

    No results matching ""