快,学会 shell

故事扮演 提交于 2020-04-07 23:47:04

题图:https://pixabay.com/illustrations/hacker-cyber-crime-internet-2300772/

本文分成入门篇和基础篇。基础篇包括变量、字符串处理、数学运算三部分。基础篇包括流控制、函数和函数库三部分。主要是基于例子进行讲解,其中有 4 个复杂一点的脚本,看懂了也就入门了。

我们先来聊一聊 shell 和 shell script 的概念。计算机的运行离不开硬件,我们通过操作系统(OS,Operating System)操作硬件,而我们所说的 linux 严格来说是操作系统(OS)的核心部分——内核(Kernel)。我们无法直接操作 kernel,需要借助于 kernel 外的一层壳 shell 才能与 kernel 进行交互。如果把操作系统(OS)看做是一家公司,shell 就是前台,kernel 就是董事会。当我们访问公司的时候,先和前台(shell)打个招呼,前台通知董事会(kernel),董事会来控制公司(OS)。

俗话说“铁打的营盘流水的兵”,就是公司人来人往,都不会影响公司的运转。对于操作系统也一样,我们可以替换操作系统的前台(shell),甚至董事会(kernel)。如果你想知道你的系统中用到的是什么 shell 可以访问 /etc/shells 文件。,我的电脑上就有下面几种 shell:

# /etc/shells: valid login shells
/bin/sh
/bin/dash
/bin/bash
/bin/rbash
/bin/zsh
/usr/bin/zsh

小结: shell 是 kernel 外的一层壳,是操作系统与用户之间的桥梁.我们通常所说的 shell 并不是 shell 本身,而是 shell script(shell 脚本),一般是 .sh 结尾的文件.怎么写一个 shell 脚本是本文探讨的话题,而不是 shell 本身.

入门篇

#!/bin/bash
for ((i=0; i<10; i++));
do
    echo ${i}
done

直接来看一个例子吧。创建一个名为 shell001.sh 的文件,写上上面几行代码:

  • 第 1 行,指定 shell 的解释器。shell 脚本就和 python 或者 jsp 需要用解释器来解析,第 1 行就是用于指定解释器,也就是之前提到的 /etc/shells 下面列出来的。
  • 第 2 行,循环语句,共循环 10 次,会在后面流控制章节讲解,加上 do 和 done 是 for 循环中的语法规则,for、do、done 是 shell script 中的关键字
  • 第 4 行,打印 i 这个变量

那么怎么运行这个脚本呢?既然是运行我们既要给它赋予可执行权限chmod +x shell001.sh,接着用 ./shell001.sh 执行这个脚本。脚本运行起来将会在终端输出 0 到 9 这几个数字。

变量

前面没有解释第 4 行echo ${i}。echo 是一个简单的 linux 命令,它会将输入从到标准输出(stdout)上,然后在终端中显示出来,这里显示的就是${i}这个变量的值。

在 shell 中定义变量的规则如下:

  • 变量和等号之间不能有空格
  • 变量名称由字母、数字和下划线组成
  • 变量名称的第一个字符必须是字母或者下划线
  • 变量名称中不允许空格和标点

比如说一个变量为name="shuiyj",那么使用变量就要加上$符号,打印这个变量就使用echo ${name}。此外变量除了显示地赋值,还可以使用语句给变量赋值:

# 获取该文件夹下后缀为 jpg 结尾的列表
for image in `ls *.jpg`

变量匹配

我们会定义和使用一个变量了,接下来我会介绍几个使用的处理变量的方法。

现在一张图片的名字叫做 cat.jpg,我想要获取文件的名称,即 cat。当然这有很多的中方法,这里介绍一种实用的方法——变量匹配。

语法 说明
${变量名#匹配规则} 从变量开头进行规则匹配,将符合最短的数据删除
${变量名##匹配规则} 从变量开头进行规则匹配,将符合最长的数据删除
${变量名%匹配规则} 从变量结尾进行规则匹配,将符合最短的数据删除
${变量名%%匹配规则} 从变量结尾进行规则匹配,将符合最长的数据删除
${变量名/旧字符串/新字符串} 变量中符合规则的第一个旧字符串将会被旧字符串代替
${变量名//旧字符串/新字符串} 变量中符合规则的所有旧字符串将会被旧字符串代替

回到最开始的需求,就可以使用%来实现

cat="cat.jpg"
echo ${cat} # cat.jpg
echo ${cat%.*} # cat

首先定义一个变量 cat 并为其赋值,接着用$获取 cat 变量的值并打印出来,最后使用变量替换截取字符串。

变量匹配在 shell 中会被高频使用,要记住这些规则。

特殊变量的含义

shell 中有一些特殊的变量,它们有很多实用的功能,比如说校验输入的参数,允许追加更多参数,判断上一条命令是否执行成功等。

变量 含义
$0 当前脚本的文件名
$n 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2
$# 传递给脚本或函数的参数个数。
$* 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。被双引号(" ")包含时,与 $* 稍有不同,下面将会讲到。
$? 上个命令的退出状态,或函数的返回值。
$$ 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。

比如说我们要对 shell 中的输入参数进行校验,可以添加这样一段

#!/bin/bash
if [ $# != 2 ];then
    echo "Usage: $0 <change ID> <target ID>"
    exit -1
fi
...

其中$#表示输入的参数数量,我们通过条件判断在程序的输入参数不为 2 的时候将会进行提示,并退出程序。其中$0一般是可执行文件的名称。

字符串

变量的话题就先讲到这里,接下来讲 shell 中处理字符串的一些注意事项和技巧。无论学习哪一门编程语言,字符串的处理都是一个绕不开的话题,并且在 shell 编程中用的最多的就是字符串。

单引号与双引号的区别

字符串可以用单引号,也可以用双引号,还可以不用引号,我们要注意它们之间的区别。

# 单引号
str='this is a string'

# 双引号
your_name='qinjx'
str="Hello, I know your are \"$your_name\"! \n"

单引号

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的
  • 单引号字串中不能出现单引号(对单引号使用转义符后也不行)

双引号

  • 双引号里可以有变量
  • 双引号里可以出现转义字符

想要了解更多它们之间的区别可以看这篇文章:shell十三问之4:""(双引号)与''(单引号)差在哪?

拼接字符串

shell 中的字符串拼接只要直接连在一起就可以。

first_name="yujie"
last_name="shui"
greeting="hello,${first_name} ${last_name}"

echo ${greeting}

计算字符串的长度

shell 中有两种方法可以计算字符串长度。

str="abcde"
echo ${#str} # 5
expr length "${str}" # 5	

获取子串在字符串中的索引

expr index ${string} ${substring}

抽取子串

语法 说明
${string:position} 从string中的position开始
${string:position} 从position开始,并指定长度为length
${string:-position} 从右边开始匹配
${string:(postion)} 从左边开始匹配
expr substr $string $position $length 从position开始,匹配长度为length

命令替换

命令替换指的是 shell执行命令并将命令替换部分替换为执行该命令后的结果(先执行该命令,然后用结果代换到命令行中),共有有两种实现命令替换的方式:

# 方法一
`command`
# 方法二
$(command)

还记得之前讲变量的时候提到变量既可以直接获取,也可以从语句获取么?

# 获取该文件夹下后缀为 jpg 结尾的列表
for image in `ls *.jpg`

可以看到 Image 获取到的是ls *.jpg的返回值,也就是一个文件的列表。这里就用到命令替换的符号``即两个反引号。

再比如获取系统的所有用户并输出

index=1 
for user in `cat /etc/passwd | cut -d ":" -f 1` 
do 
	echo "This is ${index} user: ${user}" 
	index = $(($index + 1)) 
done 

cat /etc/passwd | cut -d ":" -f 1 将会截取用户名,由于使用了命令替换,其执行结果会返回给 user 变量,此时的 user 就是一个包含用户名称的列表。

最后再举一个使用$()的例子,比如获取系统时间计算今年或明年

echo "This is $(date +%Y) year" 
echo "This is $(($(date +%Y) + 1)) year" 

数学运算

shell 中就两种变量,字符串和数字,数字又要按照整型和浮点型分开进行处理,处理它们的函数是不同的。整型运算需要使用expr $num1 operator $num2或者$(($num1 operator $num2)),浮点型运算则需要使用bc

任然是通过案例的方式进行说明,假设现在有这样一个需求:提示用户输入一个正整数 num,然后计算 1+2+3+…+num 的值,并且必须对 num 是否为正整数做判断,不符合应该允许再次输入。

#!/bin/bash
#
while true
do
    read -p "please input a positive number: " num
    expr $num + 1 &> /dev/null
    if [ $? -eq 0 ];then
        if [ `expr $num \> 0` -eq 1 ];then
            for((i=1;i<=$num;i++))
            do
                sum=`expr $sum + $i`
            done    
            echo "1+2+3+....+$num = $sum"
            exit
        fi
    fi
    echo "error,input enlegal"
done

这个脚本优点复杂但是不用着急,我们先关注于数学运算expr $num + 1 这一部分,其中关于if判断的部分会在下一节讲解。

expr $num + 1 意思就是做一次整数运算,将 num 和 1 相加。做这个操作的目的是判断 num 是不是一个整数,因为 expr 只能应用在整数运算上,所以执行expr $num + 1之后,如果 num 是整数退出状态就是正常的 $? = 0,否则 $? ≠ 0 ,并且我们并不需要返回结果,可以将结果重定向到/dev/null中,即expr $num + 1 &> /dev/null

注:在特殊变量的含义这一节可以了解$?的含义。退出状态指的是命令执行完毕之后像操作系统返回的值,成功则为 0。

expr 支持整数运算,不支持浮点数运算,要做浮点数运算那就要用到 bc。bc 是 bash 内建的运算器,支持浮点数运算,使用方法如下所示:

echo "23.3+30" | bc
53.3
echo "scale=4;23.3/3.2" | bc
7.2812

基础篇

在前面的入门篇,我们了解了变量、字符串和数学运算,接下来我将会介绍 shell 中流程控制的语法规则,以及 shell 中如何使用函数以及函数库。当我们掌握以上这些内容,shell 就可以算是入门了,那么就一起开始吧。

流控制

流控制就是用判断语句,循环语句来控制程序执行的逻辑,就从我们在上一节数学运算中的那个脚本讲起吧,它既包含了if又包含了while循环,是一个很好的例子。

IF 控制语句

#!/bin/bash
#
while true
do
    read -p "please input a positive number: " num
    expr $num + 1 &> /dev/null
    if [ $? -eq 0 ];then
        if [ `expr $num \> 0` -eq 1 ];then
            for((i=1;i<=$num;i++))
            do
                sum=`expr $sum + $i`
            done    
            echo "1+2+3+....+$num = $sum"
            exit
        fi
    fi
    echo "error,input enlegal"
done

上一节中的脚本中expr $num + 1 &> /dev/null是关于数学运算的部分,紧跟着的if就是一个控制语句,我们抛开无关部分,开看一下关于if的骨架

expr $num + 1 &> /dev/null

if [ $? -eq 0 ];then
		...
fi

这里首选要进一步解释退出状态的含义。之前已经说了,退出状态指的是命令(包括脚本和函数)在执行完毕之后,向操作系统返回的值。这个值是一个 0~255 的整数,用来表示命令执行成功还是失败,其中 0 代表命令执行成功。参数$?则用于保存这个返回值。

由此可以看出if在这里做的就是判断expr $num + 1 &> /dev/null是否执行成功。

此外,我们也可以使用if...elif..else的形式,如下:

if condition1
then
	command1
elif condition2
	command2
else
	commandN
fi

此外,在实际开发过程中还经常会对文件状态进行判断,比如说判断这是不是一个文件夹、是不是一个文本文件等;或者会对字符串进行判断,比如说字符串是否为空,字符串长度是否符合要求;还会对数值进行比较操作,就像例子中提到到值是不是为 0 等。

判断表达式

if test     #表达式为真
if test !   #表达式为假
test 表达式1 –a 表达式2     #两个表达式都为真
test 表达式1 –o 表达式2     #两个表达式有一个为真
test 表达式1 ! 表达式2       #条件求反

文件表达式

test File1 –ef File2    #两个文件是否为同一个文件,可用于硬连接。主要判断两个文件是否指向同一个inode。
test File1 –nt File2    #判断文件1是否比文件2新
test File1 –ot File2    #判断文件1比是否文件2旧
test –b file   #文件是否块设备文件
test –c File   #文件并且是字符设备文件
test –d File   #文件并且是目录
test –e File   #文件是否存在 (常用)
test –f File   #文件是否为正规文件 (常用)
test –g File   #文件是否是设置了组id
test –G File   #文件属于的有效组ID
test –h File   #文件是否是一个符号链接(同-L)
test –k File   #文件是否设置了Sticky bit位
test –b File   #文件存在并且是块设备文件
test –L File   #文件是否是一个符号链接(同-h)
test –o File   #文件的属于有效用户ID
test –p File   #文件是一个命名管道
test –r File   #文件是否可读
test –s File   #文件是否是非空白文件
test –t FD     #文件描述符是在一个终端打开的
test –u File   #文件存在并且设置了它的set-user-id位
test –w File   #文件是否存在并可写
test –x File   #文件属否存在并可执行

字符串表达式

test string #string不为空
test –n 字符串    #字符串的长度非零
test –z 字符串    #字符串的长度是否为零
test 字符串1=字符串2       #字符串是否相等,若相等返回true
test 字符串1==字符串2      #字符串是否相等,若相等返回true
test 字符串1!=字符串2      #字符串是否不等,若不等反悔false
test 字符串1>字符串2				# 在排序时,string1 在 string2 之后
test 字符串1<字符串2				# 在排序时,string1 在 string2 之前

整数表达式

test 整数1 -eq 整数2    #整数相等
test 整数1 -ge 整数2    #整数1大于等于整数2
test 整数1 -gt 整数2    #整数1大于整数2
test 整数1 -le 整数2    #整数1小于等于整数2
test 整数1 -lt 整数2    #整数1小于整数2
test 整数1 -ne 整数2    #整数1不等于整数2

以上表达式摘自test - shell环境中测试条件表达式工具,test 是测试条件表达式的工具,test 后面部分的内容可以用于if条件判断中。

WHILE 和 UNTIL 循环

再接之前的脚本来讲解 while 的用法

#!/bin/bash
#
while true
do
    read -p "please input a positive number: " num
    expr $num + 1 &> /dev/null
    if [ $? -eq 0 ];then
        if [ `expr $num \> 0` -eq 1 ];then
            for((i=1;i<=$num;i++))
            do
                sum=`expr $sum + $i`
            done    
            echo "1+2+3+....+$num = $sum"
            exit
        fi
    fi
    echo "error,input enlegal"
done

前面也说过,这个脚本的目的是接收一个 num,如果输入的 num 不是一个整数就一直让用户输入,直到输入的 num 是一个整数为止。这里就用到了 while 循环,并且将循环条件设置为 true,也就是一个永久的循环。循环不能终止,按照逻辑我们要在用户输入整数并完成计算之后推出程序,所以这里通过exit来退出程序,这里也可以使用break来跳出循环。我们还可以配合使用continue表示继续执行,这里没有举例说明。

while命令退出状态不为0时终止循环,而until命令则刚好相反。除此之外,until命令与while命令很相似。until循环会在接收到为0的退出状态时终止。在while-count脚本中,循环会一直重复到count变量小于等于5。使用until改写脚本也可以达到相同的效果。

#!/bin/bash
# until-count: display a series of numbers

count=1

until [ $count -gt 5 ]; do
     echo $count
     count=$((count + 1))
done
echo "Finished."

将测试表达式改写为count –gt 5 until就可以在合适的时刻终止循环。选择使用while还是until,通常取决于哪种循环能够允许程序员写出最明了的测试表达式。

case 分支

前面讲了使用if做条件判断,在其他语言比如 C++ 或者 Java 等中都存在switch..case..这样的语句,shell 也提供了case这个多选项符合命令,它的命令格式是这样的:

case word in
    [pattern [| pattern]...) commands ;;]...
esac

在这里我想举一个做算数运算的例子,这和例子与下一节讲解的函数相关,其中用到了case,但是即使不了解函数怎么使用,也不会对理解case的运用造成影响。

#!/bin/bash
#
function calcu
{
    case $2 in
        +)
            echo "`expr $1 + $3`"
            ;;
        -)
            echo "`expr $1 - $3`"
            ;;
        \*)
            echo "`expr $1 \* $3`"
            ;;
        /)
            echo "`expr $1 / $3`"
            ;;
    esac
}
calcu $1 $2 $3

这个脚本希望做的是一次算数运算,根据操作符是+ - * /来进行运算。

for 循环

现在到了流控制的最后一节了,for 循环其实在文章一开始我们就见过了,在文章最开始我举了两个例子:

# 获取该文件夹下后缀为 jpg 结尾的列表
for image in `ls *.jpg`
do
	....
done

# 输出 0-9 共 10 个数字
for ((i=0; i<10; i++));
do
    echo ${i}
done

第一种是传统的形式,和 python 中的 for 循环很像,我们可以像这样for i in A B C D;do echo $i; done 使用 for 循环,可以将循环的内容就当成 python 中的一个列表,也可以像for i in {A..E};do echo $i; done这样创建字符列表。

第二种方式就是 C 语言的形式了for ((i=0; i<10; i++)),比较常规也没什么值得讲的。

函数

在上一节中,我们介绍了 shell 中的流控制的语法:if, while, until, case和 for。再之前我们讲了变量、字符串和数学运算。到这一节就可以讲一下函数这个话题了。

了解了上面这些内容理论上已经能够写出任何的程序的,不过写程序的过程中会有许多类似的代码,如果全部重新写一遍程序就会显得冗长,所以一般的做法就是将可以复用的代码抽取出来形成函数。shell 自然也支持函数的使用,接下里就看看在 shell 中怎么定义和使用函数。

首先看 shell 中函数是怎么定义的。shell 中的函数有两种定义格式,使用任意一种都可以。

name()
{
	command1
  command2
  ...
  commandn
}
function name
{
	command1
  command2
  ...
  commandn
}

我比较习惯用第二种形式,你可以选择任何一种方式,不过我接下来的例子是按照第二种定义方式。

我们知道了函数定义的架构,但是如果你用过其他编程语言会发现它没有参数列表,也没有返回值,这在一开始也让我觉得很困惑,但是我们在变量那一节学过特殊变量的含义,其中有一个变量是$0表示函数的名称,shell 中的变量是通过命令行键入,再用$1 $2 $3读取的。这就和 C++ 或者 Java 中的主函数读取参数一个道理,char** argvString[] args就是由命令行键入的参数列表。

下面就用一个具体函数的例子进行说明,这个例子在 流控制——case 这一节也讲过,但是没有讲完:

#!/bin/bash
#
function calcu
{
    case $2 in
        +)
            echo "`expr $1 + $3`"
            ;;
        -)
            echo "`expr $1 - $3`"
            ;;
        \*)
            echo "`expr $1 \* $3`"
            ;;
        /)
            echo "`expr $1 / $3`"
            ;;
    esac
}

calcu $1 $2 $3
echo ""calcu $1 $2 $3""

首先这个脚本的文件名为 calcu,其中定义了一个函数 calcu,采用的是第二种函数定义方式,在函数体中我们利用case做了一个多条件判断。

在脚本的结尾我们调用了 calcu 这个函数,并将输入了三个参数,紧接着为了给大家看到三个参数分别是什么我选择将其打印出来。调用函数的过程是这样的:

ubuntu@VM-0-2-ubuntu:/tmp$ ./calcu.sh 5 + 3
8
calcu 5 + 3

可以看到在这里$1 = 5, $2 = +, $3 = 3,这就是 shell 中传递参数的方式。

再看 shell 中返回值这个问题,shell 有两种返回值的方式,一种是使用return,一种是使用echo

  • 使用return
    • 使用 return 返回值,只能返回 1-255 的整数
    • 函数使用 return 返回值。通常只能用来供其他地方调用获取状态,因此通常仅返回0或1;0表示成功,1表示失败
  • 使用echo
    • 使用 echo 可以返回任何字符串结果
    • 通常用于返回数据,比如一个字符串值或者列表值

echo的内容作为返回值可以看一下上面这个例子,这里再举一个return来返回值的例子。

#!/bin/bash
#
this_pid=$$
function is_nginx_running
{
    ps -ef | grep nginx | grep -v grep | grep -v $this_pid &> /dev/null
    if [ $? -eq 0 ];then
        return
    else
        return 1
    fi
}
is_nginx_running && echo "Nginx is running" || echo "Nginx is stoped"

和前面说过的退出状态一样,return 也是 0 表示函数执行成功,其他表示执行失败。这里举的例子是通过查看 Nginx 的进程来来确认 Nginx 是否运行。

首先是用$$来接收这个 shell 脚本的 Pid,因为脚本名字中带有 Nginx 就需要将其利用 Pid 过滤掉。还记得之前讲的的退出状态么,如果 ps 命令找到了 nginx 进程退出状态就会是 0,表示成功。最后就通过return来返回 nginx 是否正常运行。

之后可以用后台挂起的形式运行这个脚本,将其作为 nginx 的守护进程:nohup sh nginx.sh & ,使用tail -f nohup.out 来查看监听结果。

局部变量和全局变量

讲到函数就还有一个作用域的问题需要讨论,shell 中的局部变量、全局变量和一般编程语言没什么区别。

#!/bin/bash
#
var1="Hello world"
function test
{
    local var2=87
}
test
echo $var1
echo $var2

这里给到一个脚本自行体会一下即可。

函数库

基础篇的最后一部分就用来介绍一下 shell 中的函数库。函数库是每一门编程语言中非常重要的一部分,比如说 C++ 中的标准库,Java 中的 JDK,正式这些优秀的库存在才让编程变得更加高效。

shell 也是可以封装自己的库的,比如我现在下一个你要加加减乘除封装成一个函数库,作为之前 calcu 脚本的升级版,该函数库实现以下几个函数:

  1. 加法函数 add
  2. 减法函数 reduce
  3. 乘法函数 multiple
  4. 除法函数 divide
  5. 打印系统运行情况的函数 sys_load,该函数显示内存运行情况,
function add
{
    echo "`expr $1 + $2`"
}
function reduce
{
    echo "`expr $1 - $2`"
}
function multiple
{
    echo "`expr $1 \* $2`"
}
function divide
{
    echo "`expr $1 / $2`"
}

function sys_load
{
    echo "Memory Info"
    echo
    free -m
    echo
    
    echo "Disk Usage"
    echo
    df -h
    echo
}

我们将上面文件封装成一个函数库 lib/base_function。

  • 经常使用的重复代码封装成函数文件
  • 一般不直接执行,而是由其他脚本调用

接着用一个 shell 脚本 calculate.sh 调用函数库中的函数

#!/bin/bash
#
. /root/lesson/3.5/lib/base_function
add 12 23
reduce 90 30
multiple 12 12
divide 12 2

编写函数库文件的建议

  • 库文件名的后缀是任意的,但一般使用 .lib
  • 库文件通常没有可执行选项
  • 库文件无序和脚本在同级目录,只需在脚本中引用时指定
  • 第一行一般使用 #/bin/echo, 输出警告信息,避免用户执行

总结

最后来做一个总结:

  • 在文章开头我们分辨了 shell 和 shell script 的概念
  • 入门篇中主要介绍了变量,字符串和数学运算
  • 基础篇中主要介绍了流程控制的语法,以及函数的使用
  • 本文中有 4 个相对复杂的 shell 脚本,涵盖了本文所有知识点

当然 shell 的功能是很强大的,本文也只是介绍其非常基础的使用方法,希望对大家有帮助.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!