Shell 快速入门教程

Posted by 陈树义 on 2019-11-22

基础概念

说起 Shell 语言,其实很多人习惯的叫法是叫做 Shell 脚本。或许你在还没学习之前就听说过,Shell 脚本、Shell 语言、Shell 环境等等名词。但是它们到底是什么,它们之间有什么区别?下面我们就逐个来解答一下。

Shell

我们知道对于所有计算机设备来说,它们都是由 CPU、内存、硬盘等硬件做成的。但是单单有这些硬件,我们还没办法使用,因为我们不知道怎么和这些冷冰冰的硬件沟通。

为了让这些硬件听懂我们的话,计算机的先辈们费尽了九牛二虎之力,写了一个程序来把我们的话翻译给机器听。这个翻译程序可厉害了,只要你输入具体的命令,它就会把它翻译给机器听,这样你就可以和机器沟通了。例如,我们输入ifconfig可以查看 IP 地址,翻译程序就会将这条命令翻译给硬件,告诉它我们要查看 IP 地址。其实这里的翻译程序就是 Shell,而具体的命令就是Linux命令。

简单地说,Shell 就是一套位于硬件和用户之间的程序,我们一般称之为解释器。

Shell语言

有了 Shell 这个解释器,硬件就能听懂我们输入的命令了。但当我们要做一些复杂的逻辑操作时,例如筛选出占用 CPU 最高的进程ID。这时候就不仅仅只是一个命令那么简单了,我们需要进行逻辑判断等一系列操作。

所以说 Shell 语言就是一系列语言规范的集合,只要你按照这些规范将你的 Linux 命令组合起来,那么 Shell 就可以正确解析出你的意图。

简单地说,Shell 语言其实就是一系列的语法规范。

Shell 脚本

简单地说,由 Linux 命令和 Shell 语言规范组成而成的一系列字符,我们就称之为 Shell 脚本。

Shell 环境

Unix 类系统经过了长时间的发展,衍生出了很多不同的版本,这些不同版本的 Shell 不太一致,其支持的 Linux 命令有有所不同。现存的 Shell 有下面这几个:

  • Bourne Again Shell(/bin/bash)
  • Bourne Shell(/usr/bin/sh或/bin/sh)
  • C Shell(/usr/bin/csh)
  • K Shell(/usr/bin/ksh)

所以说 Shell 环境就是指的就是拥有这些 Shell 的环境。在这些 Shell 环境中,Bash 由于易用和免费,在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的 Shell。

环境搭建

对于使用 Mac OSX 系统或者 Linux 系统的朋友来说,因其自带了基本的 Shell 环境,我们无需做任何操作即可进行 Shell 脚本的编写。但在 Windows 系统中,要运行 Shell 脚本,则需要做一些额外的工作。

在 Windows 中运行 Linux 命令,我们可以通过 GitHub 提供的一个 Git For Windows 软件来实现。首先到下面的地址下载合适的版本:

Git for Windows

下载成功后直接按照默认安装。默认安装成功,在桌面或任何文件目录中,点击右键菜单中会有Git Bash Here选项。

点击Git Bash Here之后会打开命令行窗口,在窗口中就可以运行 Linux 命令。

虽然在 Windows 下也可以通过这种方式运行 Shell 脚本,但是总归不方便。所以有条件的话还是直接在 Unix 系统中进行 Shell 语言学习吧。

文件结构

一个 Shell 脚本都以.sh为文件名后缀,并且其文件结构都有一定的特点。一个标准的 Shell 脚本组成结构如下:

  • 文件标识。第一行的#!是一个特殊标记(一般称为Hashbang),标识该 Shell 脚本将使用/bin/bash这个 Shell 对这个文件内容进行解析。
  • 文件注释。这部分是对这个文件业务逻辑的注释,一个好的注释可以让别人一眼看懂该文件的目的。
  • Shell代码。这部分就是具体的业务代码了。
#!/bin/bash
# author:陈树义
# site:http://shuyi.me
echo "Hello Shell"

在注释部分,建议分为两个部分:

  • 顶层注释。顶层注释主要是对其内容进行简要概述。版权声明和作者信息是可选的。例如:
#!/bin/bash
#
# 备份数据库User表
  • 功能注释。其他人通过阅读注释就能够学会如何使用你的程序或库函数,而不需要阅读代码。例如:
#######################################
# 连接指定数据数据库
# 全局变量:
#   BACKUP_DIR
#   ORACLE_SID
# 参数:
#   None
# 返回值:
#   None
#######################################
connect(){

}

综合以上的建议,整理了一个 Shell 脚本的文件结构范例。下次你要写 Shell 脚本的时候可以直接拷贝过去,修改相关注释以及函数名就可以,即规范又简便。

#!/bin/bash
#
# 备份数据库User表

#######################################
# 连接指定数据数据库
# 全局变量:
#   BACKUP_DIR
#   ORACLE_SID
# 参数:
#   None
# 返回值:
#   None
#######################################
connect(){
}

运行 shell 脚本

运行 Shell 脚本有两种方式,一种是通过 bash 命令,一种是作为可执行程序执行。

我们写了下面这样一个 Shell 脚本,并将其保存为hello.sh

#!/bin/bash
echo "Hello Shell."

如果我们用bash命令运行,那么是这样的:bash hello.sh

运行后会输出:Hello Shell

如果想用可执行程序方式运行,那么需要先修改文件的权限,使其具有执行权限,之后再执行脚本。

chmod +x ./hello.sh  #使脚本具有执行权限
./hello.sh  #执行脚本

同样地,运行后会输出:Hello Shell

如何标记语句的结束

在 Shell 中标记语句的结束有两种方式:分号和换行。

例如下面的 Shell 脚本:

a=10
if [ $a = 10 ]; then 
  echo "the value of a is 10"
fi

也可以写成:

a=10
if [ $a = 10 ]
then 
  echo "the value of a is 10"
fi

即上面将 if 表达式后面的分号换成换行符了。但是不推荐这种写法,因为这样不利于阅读。因为最好的阅读方式就是 if 和 fi 对齐,中间是执行的语句,就像上面一开始的例子一样。

基本数据类型

Shell 语言是一门弱类型的语言,它并没有数据类型上的概念。无论你输入的是字符串、数字,在 Shell 中都按照字符串类型来存储。至于具体是什么类型,Shell 根据上下文去确定。 例如下面当你尝试对一个字符串进行加一操作时,Shell 运行时便会报错。

#!/bin/bash
num="Hello"
echo `expr $num + 1`	//expr: not a decimal number: 'Hello'
num=1
echo `expr $num + 1`	//2

这是因为虽然 Shell 语言是弱语言类型,但其解释执行的时候会进行语法检查。**意识到 Shell 在数据类型上的特殊性很重要,这样你就不会犯一些基础错误了。**例如下面的例子:

result="false"
if $result
then
	echo "true."
else 
	echo "false."	# 输出false
fi

虽然上面的 result 变量是一个字符串,但是实际上在运行时,Shell 是将其当成一个布尔型的值进行比较的。当你将 result 改成 true 之后,结果便会输出 true。

变量的使用

因为 Shell 语言是一门弱语言类型,所以变量可以无须定义便可直接使用。在 Shell 语言中,引用变量有几种方式。

第一种,直接使用 $ 符号引用。

str="Hello"
echo $str	//Hello

第二种,使用 ${} 符号引用。

str="Hello"
echo ${str} //Hello

一般来说,如果不会引起变量的阅读困难,那么可以使用第一种变量引用方式。但如果在较为复杂的环境,会引起阅读和理解困难,那还是使用第二种引用方式。例如:

#!/bin/sh
echo "What is your name?"
read USER_NAME
echo "Hello $USER_NAME"
echo "I have create an email $USER_NAME@gmail.com for you"

上面的例子非常简单,输入你的名字,为你创建一个邮箱。虽然上面的例子能正常运行,但是却不易阅读。特别最后一行,你需要很仔细端详,才知道原来是引用了$USER_NAME这个变量,而不是引用了$USER_NAME@gmail.com这个变量。在这种情况下,使用花括号来引用变量是更为合适的:

#!/bin/sh
echo "What is your name?"
read USER_NAME
echo "Hello $USER_NAME"
echo "I have create an email ${USER_NAME}@gmail.com for you"

虽然说我们可以加上大括号,或者不加,但是业界统一的规范还是加上大括号。因为这样比较统一,便于阅读。因此树义也建议大家使用第二种方式,即使用加上大括号的方式。

如何打印字符串

在 Shell 脚本中,我们可以使用 echo 命令或者 printf 命令来打印字符串。echo 适合用于简单的打印,而 printf 则适用于统一缩进的复杂打印。

echo命令

在 Shell 语言中,一般使用 echo 命令来打印字符串。而 echo 命令后面跟着的字符串有好几种形式:裸奔、单引号、双引号。

第一种,裸奔。

所谓裸奔就是后面什么引用符号都不用加,直接写上要输出的字符串。

echo Hello, My Name is chenshuyi!

这种方式会直接输出 echo 命令后的所有字符,例如上面会输出:Hello, My Name is chenshuyi!。但这种方式有个缺陷,就是无法输出分号;。因为当读到分号的时候,程序会认为这一行代码结束了。

echo Hello; I am chenshuyi

上面的命令的输出结果是:

Hello
-bash: I: command not found

可以看到程序只输出了 Hello,并把后面的I当成了一个命令。

第二种,单引号的引用方式。

str='Hello ! My Name is chenshuyi';
echo $str	

上面的语句成功输出:Hello ! My Name is chenshuyi。但这种方式的缺陷是无法在字符串中引用变量。

NAME="chenshuyi"
str='Hello ! My Name is $NAME';
echo $str

上面的输出结果是:Hello ! My Name is $NAME。可以看到我们无法打印出 NAME 变量的值。

第三种,双引号的引用方式。

NAME="chenshuyi"
str="Hello! My Name is $NAME";
echo $str

输出结果:Hello! My Name is chenshuyi。可以看到在双引号的引用方式下,我们可以成功打印出 NAME 变量的值。但是这种方式也有其缺陷,就是无法直接打印出特殊字符,需要把特殊进行转义。

简单地说:如果你打印的语句没有任何变量,那么直接和特殊字符,直接裸奔也未尝不可。但如果有一些特殊字符,那么使用单引号可能更好。如果又有特殊字符,又需要引用变量,那么只能使用双引号了。

printf命令

使用 printf 命令可以对齐打印字符串,对于阅读比较友好。

#!/bin/bash
# author:陈树义
# site:www.shuyi.me
 
printf "%-10s %-8s %-4s\n" 姓名 年龄 存款K  
printf "%-10s %-8s %-4.2f\n" 郭靖 30 52.20
printf "%-10s %-8s %-4.2f\n" 杨过 25 26.32
printf "%-10s %-8s %-4.2f\n" 郭芙 27 15.20

在这样一个符号中%-10s,百分号是个标识符,-表示左对齐,数字10表示保留10位的长度,s表示其实一个字符串。

  • 对应的%-8s表示左对齐、保留 8 位,是字符串。
  • 对应的%-4.2f表示左对齐、保留 4 位、小数点保留两位,是个浮点型数字。

在 printf 中的格式替代符一共有下面四个:

  • d: Decimal 十进制整数
  • s: String 字符串
  • c: Char 字符
  • f: Float 浮点

如何进行数学运算

在 Shell 中进行数据运算是一件很头疼的事情,因为 Shell 的数学运算和我们高级语言中的语法完全不一样。例如在 Java 中下面一个简单的算术:

int a = 10 + 5;

你以为是这么写的:

#!/bin/bash
a=10+5
echo $a

但实际上上面的输出是:10+5。我们在第四小节的时候说过,Shell 中把所有东西都当成是一个字符串,所以这里它并不知道我们要它进行数学运算。实际上在 Shell 中你要进行这样的数学运算,你应该这么写:

#!/bin/bash
a=`expr 10 + 5`
echo $a

当然了,你还可以这么写:

#!/bin/bash
let a=10+5
echo $a

这两种方式都可以实现简单的数学运算。但相比这两种,我更推荐用下面这种形式:

(( a = 10 + 5 ))
echo $a

即将需要运算的内容放在两对中括号里面。因为这种方式更加简洁,并且功能也是最为完整,也是最为接近高级语言的写法。

如何进行数值比较

学会了如何进行数学运算,但怎么进行数值运算呢。在 Java 中,我们进行数值运算是这样的:

int a = 15;
int b = 10;
if (a >= b){
  System.out.println("a > b");
} else {
  System.out.println("a < b");
}

但在 Shell 中,如果你写成下面这样:

#!/bin/bash
a=15
b=10
if [ $a >= $b ]
then 
  echo "a >= b"
else
  echo "a < b"
fi

运行结果会报错:-bash: [: a: unary operator expected。这是因为 Shell 无法识别 >= 这个符号。但如果我们使用下面这种形式,我们就可以顺利执行了。

#!/bin/bash
a=15
b=10
if (( a >= b ))
then 
  echo "a >= b"
else
  echo "a < b"
fi

没错,用的就是和数学运算一样的两对括号。将需要运算的表达式放在这里面,Shell 就会计算出一个最终的结果,true 或者 false。在括号中的表达式与我们用 Java 等高级语言的语法非常一致,我们可以使用 && 或者 || 符号,非常方便。

#!/bin/bash
a=15
if (( a > 10 && a < 20 ))
then 
	echo "10 < a < 20"	
else
	echo "a <= 10 or a >= 20"
fi

输出结果:10 < a < 20

如何进行字符串比较

在 Shell 中进行字符串比较有专门的六个操作符,他们分别是:

  • =:检测两个字符串是否相等,相等返回 true。
  • !=:检两个字符串是否相等,不相等返回 true。
  • -z:检测字符串长度是否为0,为 0 返回 true。
  • -n:检测字符串长度是否为0,不为 0 返回 true。
  • str:检测字符串是否为空,不为空返回 true。

要记得操作符左右两边都要加空格,否则会报语法错误。

这六个操作符可以进行字符串的非空比较,长度比较,相等比较等。下面的这个例子完整列出了这六个字符串操作符的使用方法:

#!/bin/bash
# author:陈树义
# site:http://www.shuyi.me

a="abc"
b="efg"
# 字符串是否相等
if [ $a = $b ]
then
   echo "$a = $b : a 等于 b"
else
   echo "$a = $b: a 不等于 b"
fi
if [ $a != $b ]
then
   echo "$a != $b : a 不等于 b"
else
   echo "$a != $b: a 等于 b"
fi
# 字符串长度是否为0
if [ -z $a ]
then
   echo "-z $a : 字符串长度为 0"
else
   echo "-z $a : 字符串长度不为 0"
fi
if [ -n "$a" ]
then
   echo "-n $a : 字符串长度不为 0"
else
   echo "-n $a : 字符串长度为 0"
fi
# 字符串是否为空
if [ $a ]
then
   echo "$a : 字符串不为空"
else
   echo "$a : 字符串为空"
fi

输出结果为:

abc = efg: a 不等于 b
abc != efg : a 不等于 b
-z abc : 字符串长度不为 0
-n abc : 字符串长度不为 0
abc : 字符串不为空

条件结构怎么写

与其他语言一样 Shell 也有 IF-ELSE 以及 IF-ELSE-IF-ELSE 的选择结构。

IF ELSE 结构

Shell 语言中的 IF - ELSE 选择结构语法格式如下:

if condition
then
    command1 
    command2
    ...
    commandN
else
    command
fi

例如:

a=10
b=10
if [ $a = $b ]
then
  echo "a equals b"	# 输出这里
fi

IF ELSE-IF ELSE 结构

Shell 语言中的 IF ELSE-IF ELSE 结构语法格式如下:

if condition1
then
    command1
elif condition2 
then 
    command2
else
    commandN
fi

例如:

a=9 
if [ $a = 10 ]
then
  echo "a == 10"
elif [ $a -lt 10 ]
then
  echo "a < 10"	# 输出这里
else 
  echo "a > 10"
fi

**特别需要注意的是 if 后面的表达式,其左右两边都要留有一个空格,这是 Shell 的语法。**如果没有空格,那么 Shell 执行的时候会报错。例如下面的 Shell 就会报错:

a=10 
if [$a = 10 ]
then
  echo "a is $a"
fi

执行之后,输出错误:-bash: [9: command not found。出错原因就是因为在 if 后面的表达式$a前面少了一个空格。

循环结构怎么写

Shell 中的循环结构有 for、while、until、case 这四种,但是 while 和 until 的作用基本类似,所以我们重点关注 for、while、case 这三种循环结构即可。

for 循环结构

for 循环结构的语法如下:

for var in item1 item2 ... itemN
do
    command1
    command2
    ...
    commandN
done

例如,顺序输出当前列表中的数字:

for loop in 1 2 3 4 5
do
  echo "The value is: $loop"
done

输出结果:

The value is: 1
The value is: 2
The value is: 3
The value is: 4
The value is: 5

例如,顺序输出字符串中的字符:

for str in 'This is a string'
do
  echo $str
done

输出结果:

This is a string

while 循环结构

whie 循环结构语法格式如下:

while condition
do
    command
done

以下是一个基本的while循环,测试条件是:如果 a 小于等于5,那么条件返回真。int 从 0 开始,每次循环处理时,int 加 1。运行上述脚本,返回数字 1 到 4,然后终止。

#!/bin/sh
a=1
while [ $a -lt 5 ]
do
    echo $a
    let "a++"
done

运行脚本,输出:

1
2
3
4

使用中使用了 Bash let 命令,它用于执行一个或多个表达式,变量计算中不需要加上 $ 来表示变量。

那如果我要实现一个无线循环呢?

#!/bin/bash
a=1
while true
do
  echo "infinite loop $a"
  ((a++))
done

运行上面的程序,记得按下ctrl+c结束掉。

case 语句

Shell case 语句为多选择语句。可以用case语句匹配一个值与一个模式,如果匹配成功,执行相匹配的命令。case语句格式如下:

case 值 in
模式1)
    command1
    command2
    ...
    commandN
    ;;
模式2)
    command1
    command2
    ...
    commandN
    ;;
*)
	  command1
    ....
    commandN
	  ;;
esac

case 语句取值后面必须为单词 in,每一模式必须以右括号结束。取值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;。取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。case 语句使用 esac 作为结束标志。

下面的脚本提示输入1到4,与每一种模式进行匹配:

#!/bin/bash
echo '输入 1 到 4 之间的数字:'
echo '你输入的数字为:'
read aNum
case $aNum in
    1)  echo '你选择了 1'
    ;;
    2)  echo '你选择了 2'
    ;;
    3)  echo '你选择了 3'
    ;;
    4)  echo '你选择了 4'
    ;;
    *)  echo '你没有输入 1 到 4 之间的数字'
    ;;
esac

温馨提示:将上面的文本保存为一个 shell 脚本运行,直接复制到命令行运行会出错。

输入不同的内容,会有不同的结果,例如:

输入 1 到 4 之间的数字:
你输入的数字为:
3
你选择了 3

跳出循环

在循环过程中,有时候需要在未达到循环结束条件时强制跳出循环,Shell使用两个命令来实现该功能:break 和 continue。

break 命令允许跳出所有循环,这将终止执行后面的所有循环。

下面的例子中,脚本进入死循环直至用户输入数字大于5。要跳出这个循环,返回到shell提示符下,需要使用break命令。

#!/bin/bash
while true
do
    echo -n "输入 1 到 5 之间的数字:"
    read aNum
    case $aNum in
        1|2|3|4|5) echo "你输入的数字为 $aNum!"
        ;;
        *) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
            break
        ;;
    esac
done

执行以上代码,输出结果为:

输入 1 到 5 之间的数字:3
你输入的数字为 3!
输入 1 到 5 之间的数字:7
你输入的数字不是 1 到 5 之间的! 游戏结束
continue
continue命令与break命令类似,只有一点差别,它不会跳出所有循环,仅仅跳出当前循环。

对上面的例子进行修改:

#!/bin/bash
while :
do
    echo -n "输入 1 到 5 之间的数字: "
    read aNum
    case $aNum in
        1|2|3|4|5) echo "你输入的数字为 $aNum!"
        ;;
        *) echo "你输入的数字不是 1 到 5 之间的!"
            continue
            echo "游戏结束"
        ;;
    esac
done

运行代码发现,当输入大于5的数字时,该例中的循环不会结束,语句 echo 「游戏结束」永远不会被执行。

函数的使用

Linux Shell 可以用户定义函数,然后在shell脚本中可以随便调用。

函数定义

Shell 中的函数定义与 JavaScript 类似,格式如下:

function hello()
{
    echo "Hello SHell.";
	  return 1;
}
echo "method begin..."
hello
echo "method end..."

输出结果:

method begin...
Hello SHell.
method end...

上面定义了一个名为 hello 的函数,并返回了一个整数。

在 Shell 中,参数返回可以显示加 return 返回。如果不加,将以最后一条命令运行结果,作为返回值。返回值只能正整数,并且范围在 0 - 255。

函数参数

在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数...

带参数的函数示例:

funWithParam(){
    echo "第一个参数为 $1 !"
    echo "第二个参数为 $2 !"
    echo "第十个参数为 $10 !"
    echo "第十个参数为 ${10} !"
    echo "第十一个参数为 ${11} !"
    echo "参数总数有 $# 个!"
    echo "作为一个字符串输出所有参数 $* !"
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73

输出结果:

第一个参数为 1 !
第二个参数为 2 !
第十个参数为 10 !
第十个参数为 34 !
第十一个参数为 73 !
参数总数有 11 个!

作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 34 73 !

注意,$10 不能获取第十个参数,获取第十个参数需要${10}。当n>=10时,需要使用$来获取参数。

另外,还有几个特殊字符用来处理参数:

[image:B92B80B9-81E1-4760-8153-0FEDD6F34A32-315-000099F2DAFA9951/E57EB315-DFE4-4D10-AF30-E42268F31FEA.png]

输出重定向

在写 Shell 脚本的时候,我们经常会想将命令的输出结果保存到文件中,或者将命令的执行结果保存到日志记录中。这时候就需要把命令的输出结果重定向。而要进行重定向,就要了解 Linux 的输入输出流。

在 Linux 中有三个经常用到的输入输出流,他们分别是:

  • 标准输入(stdin)
  • 标准输出(stdout)
  • 标准错误(stderr)

在 Linux 系统中,系统保留了 0(标准输入)、1(标准输出)、2(标准错误) 三个文件描述符分别代表它们。

**标准输入指的是从键盘这些标准输入设备读取到的数据。**一般情况下标准输入重定向的很少用到,因此我们就不展开说。

**标准输出则是通过屏幕输出的这些数据。**我们可以通过标准输出重定向来让数据保存到文件里。例如:

YuRongChandeMacBook-Pro:chenshuyi yurongchan$ echo "hello shell, I'm shuyi" > out.txt
YuRongChandeMacBook-Pro:chenshuyi yurongchan$ cat out.txt
hello shell, I'm shuyi

可以看到 echo 命令的输出并没有在屏幕上打印出来,而是保存在了 out.txt 文件中。

其实上面这种方式和echo "hello" 1> out.txt这条命令的结果是一样的。或许是因为标准输出重定向比较频繁,所以就把数字 1 省略了。

**标准错误是指输出的错误信息。**例如当我们运行一条错误的指令时,控制台会提示凑无信息,这些就是错误信息。如果我们要重定向错误信息到文件中,我们可以用2>这个操作符。例如:

YuRongChandeMacBook-Pro:chenshuyi yurongchan$ ls +
ls: +: No such file or directory
YuRongChandeMacBook-Pro:chenshuyi yurongchan$ ls + 2> error.txt
YuRongChandeMacBook-Pro:chenshuyi yurongchan$ cat error.txt
ls: +: No such file or directory

通过2>这个操作符,我们将标准错误重定向到了 error.txt 文件中了。

说到这里,输入输出重定向就说完了。其实这一章很简单,就是通过1>2>这两个操作符达到输出重定向的目的。

文件操作

文件测试运算符用于检测文件的各种状态和属性,目前支持的运算符如下:

  • -b file:是否块设备文件
  • -c file:是否字符设别文件
  • -d file:是否目录
  • -f file:是否普通文件
  • -g file:文件是否设置了 SGID 位
  • -k file:是否设置了粘着位
  • -p file:文件是否有名管道
  • -u file:文件是否设置了 SUID 位
  • -r file:文件是否可读
  • -w file:文件是否可写
  • -x file:文件是否可执行
  • -s file:文件是否为空(文件大小是否大于0),不为空返回 true。
  • -e file:文件(包括目录)是否存在

要特别注意的是-s file判断文件是否为空时,不为空才返回true。

变量 file 表示文件 /User/chenshuyi/hello.sh,它的大小为 52 字节,具有 rwx 权限。下面的代码,将检测该文件的各种属性:

#!/bin/bash
# author:陈树义
# site:http://www.shuyi.me

file="/User/chenshuyi/hello.sh"
if [ -r $file ]
then
   echo "文件可读"
else
   echo "文件不可读"
fi
if [ -w $file ]
then
   echo "文件可写"
else
   echo "文件不可写"
fi
if [ -x $file ]
then
   echo "文件可执行"
else
   echo "文件不可执行"
fi
if [ -f $file ]
then
   echo "文件为普通文件"
else
   echo "文件为特殊文件"
fi
if [ -d $file ]
then
   echo "文件是个目录"
else
   echo "文件不是个目录"
fi
if [ -s $file ]
then
   echo "文件不为空"
else
   echo "文件为空"
fi
if [ -e $file ]
then
   echo "文件存在"
else
   echo "文件不存在"
fi

输出结果为:

文件可读
文件可写
文件可执行
文件为普通文件
文件不是个目录
文件不为空
文件存在

如何使用数组

Shell 中有数组这个概念,数组中可以存放多个值。但 Shell 只支持一维数组,不支持多维数组,初始化时不需要定义数组大小。与大部分编程语言类似,数组元素的下标由0开始。

Shell 数组用括号来表示,元素用「空格」符号分割开,语法格式如下:

array_name=(value1 ... valuen)

例如:

#!/bin/bash
# author:陈树义
# site:http://www.shuyi.me

my_array=(A B "C" D)

我们也可以使用下标来定义数组:

array_name[0]=value0
array_name[1]=value1
array_name[2]=value2

读取数组元素值的一般格式是:${array_name[index]}

#!/bin/bash
# author:陈树义
# site:http://www.shuyi.me

my_array=(A B "C" D)

echo "第一个元素为: ${my_array[0]}"
echo "第二个元素为: ${my_array[1]}"
echo "第三个元素为: ${my_array[2]}"
echo "第四个元素为: ${my_array[3]}"

执行脚本,输出结果如下所示:

$ chmod +x test.sh 
$ ./test.sh
第一个元素为: A
第二个元素为: B
第三个元素为: C
第四个元素为: D

获取数组中的所有元素
使用@ 或 * 可以获取数组中的所有元素,例如:

#!/bin/bash
# author:陈树义
# site:http://www.shuyi.me

my_array[0]=A
my_array[1]=B
my_array[2]=C
my_array[3]=D

echo "数组的元素为: ${my_array[*]}"
echo "数组的元素为: ${my_array[@]}"

执行脚本,输出结果如下所示:

$ chmod +x test.sh 
$ ./test.sh
数组的元素为: A B C D
数组的元素为: A B C D

获取数组长度的方法与获取字符串长度的方法相同,例如:

#!/bin/bash
# author:陈树义
# site:http://www.shuyi.me

my_array[0]=A
my_array[1]=B
my_array[2]=C
my_array[3]=D

echo "数组元素个数为: ${#my_array[*]}"
echo "数组元素个数为: ${#my_array[@]}"

执行脚本,输出结果如下所示:

$ chmod +x test.sh 
$ ./test.sh
数组元素个数为: 4
数组元素个数为: 4

特殊符号的使用

在 Shell 语言中,经常会看到中括号和括号组成的特殊标识,例如:[][[]](())$(())()。这些符号经常使我们非常迷惑,弄清楚它们之间的作用和区别非常必要。

在开始之前,我们先来学习一个 test 命令。

test命令

test 命令主要是用来做表达式的判断,其语法结构如下:

test {EXPRESSION}

例如:

if test "a" == "a"
then 
	echo match!	
fi

[]

其实 [] 符号的作用与 test 命令一样,都用于判断表达式的真假。只不过 [] 将表达式括起来了,更加易读。上面的例子用 [] 重写就会变成这样:

if [ "a" == "a" ]
then 
	echo match!	
fi

让我们来看一个更加复杂点的例子:

a=12
if [ $a -gt 10 -a $a -lt 15 ]
then 
	echo match!	
fi

[[]]

[[]] 符号与 [] 符号的区别是,在 [[]] 符号里,我们引用变量时可以不再用 $ 符号了,而且还可以使用 && 和 || 运算符。

像上面判断变量 a 的范围,我们在 [] 符号中,只能使用 -gt-a-lt等操作符。但如果用[[]]实现,我们就可以用上&&||操作符:

a=12
if [[ a -gt 10 && a -lt 15 ]]
then 
	echo match!	
fi

但你会发现我们做算术比较还需要写-lt-gt之类的东西,非常恶心。那么我们可以用下面这个符号。

let 命令

我们在进行算术运算时,我们可以使用 let 命令进行运算:

a=10
let b=a+10
echo $b

在 let 命令中的变量,不需要使用 $ 符号就可以使用。像上面的 a 变量,其实一个变量,但是在第 2 行的 let 语句不需要使用 $ 符号也能成功运算。

(())

这组符号的作用与 let 指令相似,用在算数运算上,是 bash 的内建功能。所以,在执行效率上会比使用 let指令要好许多。

在这个符号里,我们可以进行整数运算,它的作用和 let 命令一样。

a=10
(( b = a + 10 ))
echo $b

或者我们可以将计算结果作为表达式,如果结果是 0 表示假,其余全部是真。

a=10
if (( a + 10 ))
then
	echo true
fi

又或者是:

a=10
if (( a <= 12 && a > 0))
then 
  echo great than 10
fi

$(())

这玩意和上面的差不多,但是不会像命令一样有返回值,而是会像变量一样把运算结果替换出来。例如:

a=10
b=$(( a <= 12 && a > 0))
echo $b

输出:1

a=10
b=$(( a <= 12 && a < 0))
echo $b

输出:0

因此如果要让它作为一个表达式的话,就要结合 [] 符号。例如:

a=10
if [ $(( a <= 12 && a > 0)) -eq 1 ]
then 
  echo great than 10
fi

对于 (()) 符号而言只有 bash 这个 Shell 有,而 $(()) 则是所有 Shell 都有,更为通用。

()

() 符号表示括号中作为一个子 Shell 运行,运行结果不干扰外层的 Shell。

看看下面这个例子:

a=2
(a=1)
echo $a

输出是:2

因为括号括起来是一个子 Shell,不影响外层 Shell 的运行,所以对 a 赋值为 1 不影响外层结果,外层的 a 变量还是 2。

利用上面子 Shell 这个特性,我们在写 Shell 脚本的时候可以做到不切换当前目录而在其他目录干点事儿。例如:

(cd hello; echo "Hello Shell" > hello.txt); pwd; cat hello/hello.txt

上面我进入了子目录 hello,并创建了一个 hello.txt 文件。输出结果是:

/Users/yurongchan/Yosemite/shell-practice/practice
Hello Shell

可以看到我当前目录没有改变,但是文件已经创建成功了。

{ } 大括号 (Block of code)

这种用法与上面介绍的指令群组非常相似,但有个不同点,它在当前的 shell 执行,不会产生 subshell。 单纯只使用大括号时,作用就像是个没有指定名称的函数一般。

a=2
{ a=1; }
echo $a

上面输出:1

这个用法和 () 用法的区别有两个:

  • 大括号 {} 里的运算是在当前 Shell 运行,会影响外层结果,而括号 ()的不会。
  • 大括号里最后一个语句必须要用 ; 分号结束,否则出错,而括号 () 的并没有这个要求。

逻辑运算符的使用

逻辑运算符有三个,分别是:非运算、或运算、与运算。

  • !:非运算符。
  • -o:或运算符。
  • -a:与运算符。

因为 Shell 中并没有布尔类型,所以非运算符主要是对表达式取反。

#!/bin/bash
# author:陈树义
# site:www.chenshuyi.com
a=10
b=20
# 非运算
if !(( a == b ))
then
   echo "a is not equal to b" 
fi
# 或运算
if [ $a -lt 100 -o $b -gt 100 ]
then
   echo "a < 100 or b > 100" 
fi
# 与运算
if [ $a -lt 100 -a $b -gt 15 ]
then
   echo "a < 100 and b > 15" 
fi

值得注意的是,因为 Shell 语言并没有布尔型。所以如果你尝试在非运算符后面跟上一个「布尔值」,那么你会得到错误的结果。

#!/bin/bash
# author:陈树义
# site:http://www.shuyi.me

result=true
if [ !$result ]
then 
  echo "Hello"
fi

按照我们的理解,上面的例子应该不会打印出 Hello 字符,但实际结果是会打印。这是因为 Shell 中根本就没有布尔类型的值,所以 if 表达式中的字符串会被当成是一个字符串,字符串肯定就是 true 了,所以就会打印 Hello。其实如果我们随便输入一串字符,结果还是会输出 Hello。

关系运算符的使用

有时候我们需要比较两个数字的大小关系,这时候就要用到关系运算符。关系运算符只支持数值运算,不支持字符运算。

#!/bin/bash
# author:陈树义
# site:www.chenshuyi.com

a=10
b=20

if [ $a -gt $b ]
then
   echo "a great than b"
else
   echo "a not great than b"
fi

上面输出:a not great than b

除了支持大于运算符,Shell 语言还支持下面这些关系运算符:

  • -eq:检测两个数是否相等,相等返回 true。
  • -ne:检测两个数是否不相等,相等返回 true。
  • -gt:检测左边的数是否大于右边的,如果是返回 true。
  • -lt:检测左边的数是否小于右边的,如果是返回 true。
  • -ge:检测左边的数是否大于等于右边的,如果是返回 true。
  • -le:检测左边的数是否小于等于右边的,如果是返回 true。

算术运算符的使用

在 Shell 中,利用 expr 命令再加上算术运算符可以实现数值运算。目前支持的算术运算符有:

  • +:加法
  • -:减法
  • *:乘法
  • /:除法
  • %:取余
  • =:赋值
  • ==:相等
  • !=:不相等

在 Java 中,我们可以这样实现一个算术运算:

int a = 12;
int b = 10;
int c = a + b;

但在 Shell 中,我们不能这么操作。因为在 原生 Bash 不支持简单的数学运算,所以在 Shell 语言中的运算都是通过其他命令来实现的,其中最常用的就是 expr 命令。

#!/bin/sh
val=`expr 2 + 2`
echo "Total value : $val"

输出结果:Total value : 4

上面的 ` 符号叫做反引号,作用是将符号内的命令结果赋值给左边的变量。