什么是Shell?
什么是Shell脚本?
.sh
。Shell 编程跟 Java、Python 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。我们通常用 Vim 来编写 Shell 脚本,至于 Shell 解释器,其种类众多,常见的有
sh
:即 Bourne Shell,sh
是 Unix 标准默认的 shell。bash
:即 Bourne Again Shell,bash
是 Linux 标准默认的 shell。zsh
:功能强大的 shell 与脚本语言。我们可以使用如下命令来查看当前使用的 Shell
echo $SHELL
若要查看所有的 Shell,则可执行
cat /etc/shells
如果要切换 Shell,例如切换到 bash
,则可执行如下命令然后重启终端
chsh -s /bin/bash
⚠️ 由于
bash
是 Linux 标准默认的 Shell 解释器,所以本文接下来将主要关注bash
的使用。
在 shell 脚本中,#!
后跟的路径用来告诉系统此脚本应用什么解释器来执行,具体请见 Shebang。
接下来我们创建一个脚本文件
cd && touch demo.sh && vim demo.sh
向其中写入内容
#!/bin/bash
echo "Hello World"
之后保存并退出,然后执行该脚本
chmod u+x demo.sh && ./demo.sh
需要注意,创建的 demo.sh
默认是没有可执行权限的,所以我们需要为其添加可执行权限。此外,执行命令不可以直接写成 demo.sh
,否则系统会去 PATH 里寻找有没有叫 demo.sh
的命令。执行 ./demo.sh
后,系统会自动使用第一行所提到的 bash
解释器来执行该脚本。
📝 使用
#!/usr/bin/env 脚本解释器名称
是一种常见的在不同平台上都能正确找到解释器的办法,因为系统会自动在 PATH 环境变量中查找你指定的程序。
当然我们也不必在第一行指定解释器,更简洁地执行脚本的做法是(以 bash
为例)
bash demo.sh
此时即使在第一行指定了解释器也没用。
shell 有交互模式和非交互模式两种。
shell 的交互模式可以简单理解为在命令行中执行命令,如下
user@host:~$
此时我们可以输入一系列 Linux 指令,例如 ls
,cd
等。
shell 的非交互模式可以简单理解为执行 shell 脚本,例如 2.1 节中提到的例子。
shell 的注释分为单行注释和多行注释,其中单行注释和python相同,都以 #
开头,例如
# This is a comment.
echo "Hello World"
shell 的多行注释以 :<
EOF
结束,例如
:<<EOF
注释内容...
注释内容...
注释内容...
EOF
跟许多程序设计语言一样,我们可以在 bash 中创建变量。bash 中没有数据类型,bash 中的变量可以保存一个数字、一个字符、一个字符串等等,同时无需提前声明变量,给变量赋值会直接创建变量。
变量命名原则:
可以看出 bash 中的变量和 python 中的变量十分相似,但需要注意的是,在对变量进行赋值时,等号两边不能出现空格
var = 'apple' # 错误
var='apple' # 正确
在定义了变量后,又该如何去使用他们呢?例如如何用 echo
命令输出变量?
使用一个定义过的变量,只要在变量名前面加 $
即可:${var}
或 $var
。其中花括号加不加都行,加花括号是为了帮助解释器识别变量的边界,所以推荐加花括号。
例如
fruit='apple'
# 以下两者等价
echo ${fruit}
echo $fruit
有时候我们可能需要将变量和其他字符串一起输出,此时应用双引号(不能是单引号,后续会讲)将他们括住:
fruit='apple'
# 第一条语句输出变量fruit和字符串abc,最终结果是appleabc
echo "${fruit}abc"
# 第二条语句输出变量fruitabc,但因为没有这个变量所以不会输出任何东西(该变量的值为空)
echo "$fruitabc"
我们可以像 python 那样对已定义的变量重新赋值
fruit='apple'
fruit=123
echo ${fruit}
# 输出:123
如果不希望定义的变量后续被修改,可以使用 readonly
var=123
readonly var
var='abc'
此时会报错
demo.sh: line 3: var: readonly variable
删除一个变量可以使用 unset
命令
var=123
unset var
echo ${var}
因为删除了变量所以最终输出结果为空。
export
关键字,shell 脚本也可以定义环境变量。常见的环境变量:
变量 | 描述 |
---|---|
$HOME | 当前用户的家目录 |
$PWD | 当前工作目录 |
$PATH | 用分号分隔的目录列表,shell 会到这些目录中查找命令 |
$SHELL | 本机可以使用的 shell |
$RANDOM | 0 - 32767之间的随机整数 |
$UID | 当前用户的用户ID |
如需了解更多,请参考 Bash Guide for Beginners。
echo
命令用来输出字符串。
输出字符串
# 以下两者等价
echo "Hello World"
echo Hello World
输出转义字符
sentence="\"This is a sentence\""
echo ${sentence}
# 输出结果:"This is a sentence"
输出变量
read name # 从键盘中读取一行,并将其赋值给name变量
echo "How are you, ${name}?"
输出换行
echo -e "YES\nNO" # -e 代表开启转义
# 输出:
# YES
# NO
输出不换行
echo -e "YES\c"
echo "NO"
# YESNO
输出命令执行结果(注意不是单引号而是反引号)
echo `pwd`
将输出重定向至文件
echo "Hello" > test.txt
printf
命令模仿的是 C 语言里的 printf()
语句。默认情况下,printf
不会像 echo
一样自动添加换行符,如需换行可以手动添加 \n
。
以下两条语句等价
echo "Hello World"
printf "Hello World\n"
printf
的语法为
printf format-string [arguments...]
其中 format-string
为格式控制字符串,arguments
为参数列表。
与 C 语言类似,format-string
中的 %s
代表字符串,%c
代表字符,%d
代表整数,%f
代表浮点数。%-10s
表示一个宽度为10的字符串,其中前面的 -
表示左对齐,如果没有代表右对齐。
printf "%d %s\n" 1 "abc"
# 输出:1 abc
# 单引号也可以
printf '%d %s\n' 1 "abc"
# 输出:1 abc
# 没有引号也可以输出
printf %s abcdef
# 输出:abcdef
# 格式只指定了一个参数,但多出的参数仍然会按照该格式输出,format-string 被重用
printf %s abc def
# 输出:abcdef
printf "%s %s %s\n" a b c d e f g h i j
# 输出:
# a b c
# d e f
# g h i
# j
# 如果没有参数,那么 %s 用 NULL 代替,%d 用 0 代替
printf "%s and %d \n"
# 输出: and 0
shell 中的字符串可以用单引号,也可以用双引号,也可以不用引号。
单引号字符串的限制:
比起单引号,双引号可以识别变量,并且支持转义字符,因此推荐使用双引号。
string="abcde"
echo ${#string}
# 5
从第二个字符开始,往后截取三个字符
echo ${string:1:3}
# bcd
bash 只支持一维数组(不支持多维数组),数组中可以存放多个不同类型的值,初始化时不需要定义数组大小。数组下标从 0 开始,下标可以是整数或算术表达式,其值应大于或等于 0。
数组用括号来表示,元素用空格分隔开,语法格式如下
array=(value1 value2 ... valuen)
我们可以用上面提到的格式来创建一个数组
array=(1 "A" 3.2)
我们也可以采用赋值的方法来创建一个数组
array[0]=1
array[1]="A"
array[2]=3.2
访问数组中的单个元素的格式为:${array[index]}
,例如
nums=(3 2 1)
echo ${nums[2]}
# 1
若要访问数组的所有元素,则可将 index
改为 *
或 @
,具体如下
array=(A B C D)
echo ${array[*]}
# A B C D
echo ${array[@]}
# A B C D
我们同样可以像对待字符串那样对数组进行切片
nums=(3 4 6 1 9 2)
echo ${nums[*]:2:3}
# 6 1 9
nums=(3 4 6 1 9 2)
echo ${#nums[*]}
# 6
添加元素(在首、尾处添加1,5)
nums=(2 3 4)
nums=(1 ${nums[*]} 5)
echo ${nums[*]}
# 1 2 3 4 5
删除元素
nums=(2 3 4)
unset nums[1]
echo ${nums[*]}
# 2 4
shell 和其他编程语言一样,支持多种运算符,这里仅列出常用的几种:
原生 bash 不支持简单的数学运算,但是可以通过其他命令来实现,例如 expr
,它是一款表达式计算工具,使用它能完成表达式的求值操作。
一个简单的例子:
val=`expr 2 + 3`
echo "两数之和为: ${val}"
有两点需要注意
2+3
,2+ 3
,2 +3
都是不对的,必须写成 2 + 3
。``
包含(注意不是单引号)。一些例子
a=10
b=3
echo `expr $a + $b`
# 13
echo `expr $a - $b`
# 7
echo `expr $a \* $b` # 注意*需要转义
# 30
echo `expr $a / $b`
# 3
echo `expr $a % $b`
# 1
if [ $a == $b ]
then
echo "a等于b"
else
echo "a不等于b"
fi
# a不等于b
# 将b的值赋给a
a=$b
if [ $a != $b ]
then
echo "a不等于b"
else
echo "a等于b"
fi
# a等于b
最后两个例子涉及到了条件表达式,条件表达式要放在方括号之间,并且要有空格。例如 [ $a == $b]
,[$a == $b ]
,[$a == $b]
都是不对的,必须写成 [ $a == $b ]
。
条件表达式通常与 if
等结合,如果直接用 echo
输出不会得到像 python 那样的 True
或 False
的结果
echo [ $a == $b ]
# [ 10 == 3 ]
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。
这里仅介绍六个最常用的关系运算符。
关系运算符 | 描述 |
---|---|
-eq | 即 equal,用来判断两个数是否相等 |
-ne | 即 not equal,用来判断两个数是否不等 |
-gt | 即 greater than,用来判断左边的数是否大于右边的 |
-lt | 即 less than,用来判断左边的数是否小于右边的 |
-ge | 即 greater equal,用来判断左边的数是否大于等于右边的 |
-le | 即 less equal,用来判断左边的数是否小于等于右边的 |
这里以 -ge
为例,其他可类比
a=5
b=6
if [ $a -ge $b ]
then
echo "$a >= $b"
else
echo "$a < $b"
fi
# 5 < 6
布尔运算符 | 描述 |
---|---|
! | 非运算 |
-o | 或运算 |
-a | 与运算 |
一些例子
a=5
b=3
if [ ! $a -ge $b ]
then
echo "$a < $b"
else
echo "$a >= $b"
fi
# 5 >= 3
if [ $a -gt 3 -a $b -lt 4 ]
then
echo "$a > 3 且 $b < 4 为真"
else
echo "$a > 3 且 $b < 4 为假"
fi
# 5 > 3 且 3 < 4 为真
if [ $a -gt 6 -o $b -lt 2 ]
then
echo "$a > 6 或 $b < 2 为真"
else
echo "$a > 6 或 $b < 2 为假"
fi
# 5 > 6 或 3 < 2 为假
字符串运算符 | 描述 |
---|---|
== | 检查两个字符串是否相等 |
!= | 检查两个字符串是否不等 |
-z | 检查字符串长度是否为0 |
-n | 检查字符串长度是否不为0 |
$ | 检查字符串是否不为空 |
一些例子
a="abc"
b=""
if [ -z $a ]
then
echo "字符串a的长度为0"
else
echo "字符串a的长度不为0"
fi
# 字符串a的长度不为0
if [ $b ]
then
echo "字符串b不为空"
else
echo "字符串b为空"
fi
# 字符串b为空
if
的语法格式
if condition; then
# Command...
fi
if-else
的语法格式
if condition; then
# Command...
else
# Command...
fi
if-elif-else
的语法格式
if condition1; then
# Command...
elif condition2; then
# Command...
else
# Command...
fi
例如
a=15
b=13
if [ $a == $b ]; then
echo "a等于b"
elif [ $a -gt $b ]; then
echo "a大于b"
elif [ $a -lt $b ]; then
echo "a小于b"
else
echo "???"
fi
# a大于b
如果你需要面对很多情况,分别要采取不同的措施,那么使用 case
会比嵌套的 if
更有用。
case ... esac
为多选择语句,与其他语言中的 switch ... case
语句类似,是一种多分支选择结构,每个 case
分支用右圆括号开始,用两个分号 ;;
表示 break
,即执行结束,跳出整个 case ... esac
语句。
语法如下
case value in
pattern1)
# Command;
;;
pattern2)
# Command;
;;
esac
取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一模式匹配,使用星号 *
捕获该值,再执行后面的命令。
例如
read -p "输入1-4之间的整数: " num
case $num in
1) echo "你选择了1" ;;
2) echo "你选择了2" ;;
3) echo "你选择了3" ;;
4) echo "你选择了4" ;;
*) echo "你没有输入1-4之间的数字" ;;
esac
这里仅介绍最常用的三种:for
、while
和 until
。
格式如下
for var in item1 item2 ... itemN; do
# Command...
done
例如
for num in 1 2 3 4 5; do
echo "The number is $num"
done
# The number is 1
# The number is 2
# The number is 3
# The number is 4
# The number is 5
我们还可以利用 for
循环进行批量移动文件。假设当前目录下有 A
和 B
两个目录,现在需要将目录 A
中的所有 .txt
文件都移动到 B
中,则可以这样做
for file in $PWD/A/*.txt; do
mv $file $PWD/B
done
格式如下
while condition; do
# Command...
done
例如
x=0
while [ $x -lt 5 ]; do
echo $x
x=`expr $x + 1`
done
# 0
# 1
# 2
# 3
# 4
若想使用无限循环,只需将 condition
改为 :
或 true
即可。
until
循环跟 while
循环正好相反。它跟 while
一样也需要检测一个条件,但不同的是,只要该条件为 false
就一直执行循环。
格式如下
until condition; do
# Command...
done
例如
x=0
until [ $x -ge 5 ]; do
echo $x
x=`expr $x + 1`
done
# 0
# 1
# 2
# 3
# 4
shell 的 break
和 continue
的功能与其他语言一样,具体请参考下面的例子。
x=0
while :; do
if [ $x -gt 10 ]; then
echo $x
break
else
x=`expr $x + 1`
fi
done
# 11
再例如,输出1-10之间的所有偶数
for x in {1..10}; do
if [ `expr $x % 2` -eq 0 ]; then
echo $x
else
continue
fi
done
📝 有关括号的使用请参考这篇文章 shell 中各种括号的作用()、(())、[]、[[]]、{}。
shell 中函数的定义格式如下
[function] funname(){
# Code...
[return int]
}
其中方括号的内容为可选。
如果函数有返回值的话,则返回值必须是0-255之间的整数。如果不加 return
语句,shell 将默认以最后一条命令的运行结果作为函数的返回值。
定义一个最简单的函数
# 定义函数
demo(){
echo "Hello World"
}
# 执行函数
demo
计算两数之和
twoSum(){
read -p "请输入第一个数: " a
read -p "请输入第二个数: " b
return `expr $a + $b`
}
twoSum
echo "两数之和为: $?"
函数返回值在调用该函数后通过 $?
来获得。
位置参数表:
变量 | 描述 |
---|---|
$0 | 脚本名称 |
$1 、$2 、…、$9 | 第1个到第9个参数 |
${10} 、…、${N} | 第10个到第N个参数 |
$* 或 $@ | 除了 $0 以外的所有位置参数 |
$# | 不包括 $0 在内的位置参数的个数 |
例如
func(){
echo "第一个参数为 $1"
echo "第二个参数为 $2"
echo "第十个参数为 ${10}"
echo "共有 $# 个参数"
echo "输入的所有参数: $*"
}
func 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 第一个参数为 3
# 第二个参数为 4
# 第十个参数为 12
# 共有 14 个参数
# 输入的所有参数: 3 4 5 6 7 8 9 10 11 12 13 14 15 16
[1] 一篇文章让你彻底掌握 shell 语言
[2] Shell 教程 | 菜鸟教程