Shell 简明教程

Shell 简介

Shell 是一个命令解释器。既可以在命令解释器上把命令一行一行敲出来执行,也可以把多行保存到一个文件(*.sh),再让命令解释器执行这个文件。

编写第一个 shell 脚本

新建一个 shell 脚本文件,文件名使用 .sh 后缀。

vim hello.sh

输入以下内容:

#!/bin/bash
Hello='Hello World'
echo $Hello

脚本说明如下:

  • 第 1 行 指定shell脚本的解释器
  • 第 2 行 声明一个名为 Hello 的变量
  • 第 3 行 执行 echo 命令。echo 命令用于将一个字符串在终端上打印出来。

保存上面的代码,并赋予它可执行权限

chmod +x hello.sh

执行 ./test.sh 运行脚本。

上面的脚本将会输出:

hello world

这和在命令行或者终端模拟器下输入 echo 'hello world' 并按下回车得到的结果是一样的。

代码注释

和所有的编程语言一样,shell 也有注释,在 shell 脚本中,# 号和它后面的内容来表示一个注释:

# Print a message
echo "I'm a shell script."

输出内容

echo 用于向输出流输出内容,例如:

echo "hello world"

输入内容

read 用于输入一条内容:

read input
echo $input

上面的代码中,read 命令从输入流读取一个值并赋予 input,然后使用 echoinput 的内容打印出来

1. shell 中的变量

1.1 定义变量和赋值

shell 中的变量只有一种数据类型,就是字符串,所以脚本语言的变量不需要声明,直接赋值即可。shell 变量的命名规则和 C 语言差不多,支持英文字母和下划线。例如:

var1='hello'
var2=90

注意:= 的两边绝对不能出现空格。因为 shell 脚本是逐句、直接进行解释,如果写了空格,就会被 shell 认为是一个命令。

1.2 读取变量

要读取一个变量的值,使用 $变量名 的格式。例如:

# 声明一个变量 var
var="hello"

# 读取 var 变量的值并输出
echo $var

也可以用 ${var} 方式访问到变量值,例如:

var="hello"
echo ${var}

访问变量的时候 $var${var} 是等效的,推荐后者来访问一个变量。

1.3 变量作用域

1.3.1 全局变量

没有任何命令修饰的变量是一个全局变量,全局变量在同一个 shell 会话中都是有效的。

function func(){
    a=90
}
func
echo $a

输出:

90

1.3.2 局部变量

local 命令用于声明一个局部变量。例如:

function func(){
    # 使用 local 声明一个局部变量
    local a=90
}
func
echo $a

输出:

空值

1.3.3 环境变量

export 命令修饰的变量称为环境变量,在父 shell 会话中声明一个环境变量,子 shell 中也能访问。

export path="/system/bin"

# 创建一个新的shell会话
bash
echo ${path}

1.3.4 特殊变量

变量含义
$0当前脚本的文件名
$n (n≥1)表示传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 1
$#表示传递给脚本或函数的参数个数
$*表示传递给脚本或函数的所有参数
$@表示传递给脚本或函数的所有参数
$?表示上个命令的退出状态,或函数的返回值
$$表示当前 Shell 进程的 ID。对于 Shell 脚本,就是该脚本所在的进程 ID

1.3.5 $*$@ 的区别

  • $* 得到的所有参数被当成字符串
  • $@ 得到所有参数都会被当成独立的参数

示例代码如下:

#!/bin/bash

for val in "$*"
do
    echo "\$@ : ${val}"
done

for val in "$@"
do
    echo "\$* : ${val}"
done

将上述代码保存为 test.sh,然后执行以下命令:

./test.sh 1 2 3

将输出:

$@ : 1 2 3
$* : 1
$* : 2
$* : 3

2. 获取一条命令的执行结果

2.1 用 ` 将一条命令包裹起来

` 符号表示反引号,其位置是在键盘的 Esc 键的下方。使用 ` 包含的字符串,可以直接当成 shell 命令执行:

ret=`pwd`
echo ${ret}

` 包裹起来的命令中,也可以访问到变量,如下所示:

path='/'
ret=`ls -l ${path}`
echo ${ret}

2.2 以 $(command) 方式执行命令

在 shell 脚本中,可以以 $(command) 的方式来执行命令:

ret=$(pwd)
echo ${ret}

同样的,用 $(command) 这种方式也可以访问到变量:

path='/'
ret=$(ls -l ${path})
echo ${ret}

上面的例子中,如果想打印命令结果中的换行符,则可以把输出的变量包含在双引号中:

path='/'
ret=$(ls -l ${path})
echo "${ret}"

注意:$(command) 仅在 Bash Shell 中有效,而 ` 反引号可在多种 Shell 中都可使用。

3. 字符串

3.1 字符串的表示

shell 有三种方式可以表示字符串:

  1. 变量名后直接跟上字符
  2. 使用单引号 '' 表示字符串
  3. 使用双引号 "" 表示字符串

3.1.1 变量名后直接跟上字符

例如:

str=hello
echo ${str}

输出:

hello

这种方式的字符串遇到空格将会被终止。

3.1.2 使用单引号表示字符串

例如:

str=hello
echo '${str}'

输出:

${str}

从上面的例子可以看出,使用单引号表示字符串时,单引号里面的字符将保持原样输出,不会对变量进行解析,也不会对特殊符号进行转义。

3.1.3 使用双引号表示字符串

例如:

str=shell
echo "${str}: \"hello wolrd\""

输出:

shell: "hello world"

双引号中可以访问变量以及可以对特殊符号进行转义。

3.2 获取字符串的长度

例如:

str="hello"
echo ${#str}

输出:

5

3.3 字符串拼接

将两个变量放在一起访问即可实现字符串的拼接。例如:

a='hello'
b='world'
c=${a}${b}
echo ${c}

输出:

helloworld

当然,还可以这样:

echo 'hello'"world"

3.4 字符串截取

3.4.1 从左边开始截取字符串

从左边开始截取字符串,格式为:${string: start :length}

其中:

  • string 表示要截取的字符
  • start 表示开始截取的位置
  • length 表示截取长度,可省略。当省略时,表示截取到字符串末尾。

例如:

# 截取字符串
msg="hello world"
echo ${msg: 6: 5}

输出:

world

3.4.2 截取 chars 后面的字符

格式为:${string#*chars}

其中:

  • string 表示要截取的字符
  • chars 是指定的字符(或者子字符串),是通配符的一种,表示任意长度的字符串。

该语法表达的意思是:忽略左边的所有字符,直到遇见 chars(chars 不会被截取)。

3.4.3 截取最后一次出现 chars 的位置后面的内容

格式为:${string##*chars}

3.4.4 使用 % 截取左边字符

使用百分号 % 可以截取指定字符(或者子字符串)左边的所有字符,格式为:${string%chars*}

这里需要注意星号 * 的位置,因为要截取 chars 左边的字符,而忽略 chars 右边的字符,所以星号 * 应该位于 chars 的右侧。其他方面 %# 的用法相同。

4. 运算符和流程控制

4.1 基本运算

运算符说明
+加(需要结合 expr 命令使用)
-减(需要结合 expr 命令使用)
*乘(需要结合 expr 命令使用)
/除(需要结合 expr 命令使用)
%求余(需要结合 expr 命令使用)
=赋值操作
==判断数值是否相等(需要结合 [] 使用)
!=判断数值是否不相等,需要结合 [] 使用

例子:

a=8
b=4
echo "a=$a,b=$b"
var=`expr ${a} + ${b}`
echo "加法结果:${var}"
var=`expr ${a} - ${b}`
echo "减法结果:${var}"

# 注意:乘号需要转义
var=`expr ${a} \* ${b}`
echo "乘法结果:${var}"
var=`expr ${a} / ${b}`
echo "除法结果:${var}"
var=`expr ${a} % ${b}`
echo "求余结果:${var}"
var=$[${a} == ${b}]
echo "是否相等:${var}"
var=$[${a} != ${b}]
echo "是否不相等:${var}"

输出:

a=8,b=4
加法结果:12
减法结果:4
乘法结果:32
除法结果:2
求余结果:0
是否相等:0
是否不相等:1

上面的例子中,调用 expr 命令和使用 [],得到表达式的值,并将它们输出。

注意:在 shell 中,表达式两边要有空格

4.2 关系运算

运算符说明
-eqEqual,判断两个数是否相等
-neNot equal,判断两个数是否不相等
-gtGreater than,判断前面那个数是否大于后面那个数
-ltLess than,判断前面那个数是否小于后面那个数
-geGreater equal than,判断前面那个数是否大于等于后面那个数
-leLess equal than,判断前面那个数是否小于等于后面那个数

4.3 布尔运算

运算符说明
!非运算
-o或运算
-a与运算

4.4 逻辑运算

运算符说明
&&逻辑与
||逻辑或

&& 表示逻辑与运算,用 && 连接起来的两个命令,前面的执行失败就不执行后面的命令。例如:

cd /bin && ls /bin

其实,shell 脚本和 C 语言差不多,只要前面的条件不满足,后面那个就不用去执行它了。

|| 表示逻辑或运算,用 || 连接起来的两个命令,前面的执行失败才会执行后面的命令。例如:

cd /bin || ls /bin

在这里顺便说一下 ; 这个运算符,该运算符用于连接多个语句,使多个语句能够在同一行。

; 连接起来的语句,不管前面的执行成不成功,都会执行后面的语句。例如:

mkdir ella;cd ella;pwd

4.5 文件判断

运算符说明
-e判断对象是否存在
-d判断对象是否存在,并且为目录
-f判断对象是否存在,并且为普通文件
-L判断对象是否存在,并且为符号链接
-h判断对象是否存在,并且为软链接
-s 判断对象是否存在,并且长度不为 0
-r判断对象是否存在,并且可读
-w判断对象是否存在,并且可写
-x判断对象是否存在,并且可执行
-O判断对象是否存在,并且属于当前用户
-G判断对象是否存在,并且属于当前用户组
-nt判断 file1 是否比 file2 新
-ot判断 file1 是否比 file2 旧

4.6 流程控制语句

4.6.1 if 语句

if 语句格式如下:

if <condition>
then
    #do something
elif <condition>
then
    #do something
else
    #do something
fi

如果想把 then 和 if 放同一行,需要在 then 前面使用 ; 分号:

if <condition> ; then
    #do something
elif <condition> ; then
    #do something
else
    #do something
fi

其中 elif 和 else 可以省略。

例子:

read file

if [ -f ${file} ] ; then
    echo 'This is normal file.'
elif [ -d ${file} ] ; then
    echo 'This is dir'
elif [ -c ${file} -o -b ${file} ] ; then
    echo 'This is device file.'
else
    echo 'This is unknown file.'
fi

逻辑判断也可以用 test 命令,它和 [] 的作用是一样的。如下所示:

#!/bin/bash
a=4
b=4

if test $[a+1] -eq $[b+2] 
then
   echo "表达式结果相等"
else
   echo "表达式结果不相等"
fi

输出:

表达式结果不相等

4.6.2 for 语句

for 语句格式如下:

for <var> in [list] 
do
    # do something
done

例子:

read input

for val in ${input} ; do
    echo "val:${val}"
done

输入:

1 2 3 4 5

输出:

val:1
val:2
val:3
val:4
val:5

4.6.3 while 语句

while 语句格式如下:

while <condition>
do
    #do something
done

例子:

a=1
sum=0

while [ ${a} -le 100 ] ;do
    sum=`expr ${sum} + ${a}`
    a=`expr ${a} + 1`
done

echo ${sum}

输出:

5050

5. 数组

5.1 定义和基本用法

shell 中也有数组,在 shell 中声明数组的方式是用英文小括号 () 把元素包裹起来,元素与元素之前用若干个分割符隔开(空格、制表符、换行符),这个分割符定义在 IFS 变量中,我们可以通过设置 IFS 变量自定义分隔符。

来看看如何定义一个数组:

# 使用空格作为数组元素的分隔符
array=(1 2 3 4 5 'hello')

也可以定义一个空数组

array=()

访问和修改数组元素的格式如下:

array[index]=value

和大多数编程语言一样,shell 的数组索引也是从 0 开始的,假如想要分别修改数组的第一个和第二个元素为 10 和 20,使用如下方法:

array[0]=10
array[1]=20

上面的代码,如果 array 为空,10 和 20 将被添加到数组中。

5.2 遍历数组

遍历数组是十分常见的操作,如果想对数组每个元素都进行特定的操作(如:访问、修改),就需要对数组进行遍历。

在 shell 中,有两个方式可以得到数组的全部元素:

  1. ${array_name[*]}
  2. ${array_name[@]}

有了这个知识,我们就遍历数组了。

#!/bin/bash

array=("My favoriate number is" 65 22 )
idx=0

for elem in ${array[*]}
do
    echo "Array element ${idx} is:${elem}"
    idx=$(expr $idx + 1)
done

输出:

Array element 0 is:My
Array element 1 is:favoriate
Array element 2 is:number
Array element 3 is:is
Array element 4 is:65
Array element 5 is:22

在上面的代码中,我们可能以为自己定义了一个字符串和两个数字在数组中,应该打印出一行字符串和两个数字。但是却不是这样的,只要有空白符,shell 会把它们当成数组的分隔符,这些被隔开的部分就会被当成数组的元素。

6. 函数

在 shell 脚本中,使用 function 关键字来定义一个函数。

函数调用及注意事项:

  1. 直接写一个函数名来调用一个无参数的函数
  2. 函数有参数,调用时,在函数名后面写上参数,多个参数用空格隔开
  3. 调用函数时传递参数,在函数体内部,通过 $1 表示第 1 个参数,$2 表示第2个参数...

6.1 函数的结构

function foo(){
    # do something...
}

6.2 函数的用法示例

function foo(){
    local name=$1
    local age=$2
    echo "My name is ${name},I'm ${age} years old."
}

foo "Ella" 29

输出:

My name is Ella,I'm 29 years old.

7. 重定向

重定向可以理解把一个东西传送到另个地方。

重定向符作用
output > file将输出流重定向到文件
output >> file将输出流追加到文件末尾
input < file将文件的内容重定向到输入流

7.1 输出到文件

例子:

echo 'hello' > out.txt
echo 'world' >> out.txt
cat out.txt

输出:

hello
world

在上面的例子中,首先使用 > 将字符串 hello 从输出流重定向文件,然后使用 >>world 字符串追加到 out.txt 文件尾,最后用 cat 命令读取并打印 out.txt 的文件内容到控制台。

重定向符还可以配合数字 0、1、2 使用,其中:

  • 0 代表标准输入流
  • 1 代表标准输出流(上面的例子没有指定数字,就是默认输出流)
  • 2 代表标准错误流

例如:

ls / 1 > out.txt
cat out.txt

执行上面的命令,会将根目录下的文件名和目录名输出到 out.txt 文件中。

ls /mybook 2 > out.txt
cat out.txt

执行上面的命令,会将执行 ls /mybook 命令时的错误信息输出到 out.txt 文件。

ls /;ls /luoye 2 > &1

执行上面的代码,将错误流重定向到输出流,这种做法在某些场合是很有用的。

7.2 特殊文件

7.2.1 /dev/null

所有重定向到这个文件的内容都会消失,常常同于忽略错误输出。

ls /mybook 2> /dev/null

如果不存在 /mybook 这个目录或者文件,就什么提示也没有。

7.2.2 /dev/zero

这个文件会不断产出空的数据,该文件常被 dd 命令使用:

dd if=/dev/zero of=out.txt bs=1 count=16

从 /dev/zero 输入,输出到 out.txt,生成一个大小为 16 字节的空文件。

8. 管道

管道是 Linux 中的一种跨进程通信的机制,和重定向不同,管道用做进程与进程之间传送数据。

作为 Linux 中默认的脚本语言,shell 中也是可以使用管道的,在 shell 脚本中,使用 | 表示管道。

8.1 使用管道筛选内容中包含 root 的行

ls -l / | grep root

这个例子中,ls 命令输出的内容传给了 grep 命令进行筛选。

8.2 同时用多个管道

在 shell 中,我们也可以同时使用多个管道。

下面是同时使用多个管道筛选数据并统计的一个示例:

ls -l / | grep root | wc -l

在这个例子中,首先使用 ls 命令列出根目录 / 的文件列表,然后把输出的内容传给了 grep 命令进行筛选,最后,再通过管道转给 wc 命令统计行数。

提示:本站提供了常用的 Linux 命令参考,请移步常用 Linux 命令工具进行参考。

为了更好的理解管道,写两个脚本来体验一下:

文件1:in.sh 文件

#! /bin/bash
read msg
echo "Receive :${msg}"

文件2:out.sh 文件

#! /bin/bash
echo 'hello'

然后,在命令行中执行以下命令:

./out.sh | ./in.sh

输出:

Receive :hello

从上面的输出结果看,代码执行结果符合我们预期,即字符串 hello 从 out.sh 传送到了 in.sh。

全文完,感谢阅读。

分享