概述
Shell是一个命令行解释器,接收来自应用程序或用户的命令,然后调用操作系统内核。
Shell也被称为一个编程语言。
查看Linux提供的Shell
解释器,示例代码:
1 | cat /etc/shells |
运行结果:
1 | /bin/sh |
解释一下bash
和sh
的关系,示例代码:
1 | ll /usr/bin/ | grep bash |
运行结果:
1 | -rwxr-xr-x. 1 root root 964536 Nov 25 2021 bash |
即sh
是bash
的连接。
查看CentOS默认的解释器,示例代码:
1 | echo $SHELL |
运行结果:
1 | /bin/bash |
即,CentOS的默认解释器为bash
。
开始
案例
创建一个Shell脚本helloworld.sh
,内容如下:
1 | !/bin/bash |
解释说明:#!/bin/bash
,指定解释器。
执行方式
sh 脚本
通过sh 脚本
的方式执行,该方式不需要对脚本文件有执行的权限。
举例:
sh 脚本的相对路径
,示例代码:运行结果:1
sh helloworld.sh
1
helloworld
sh 脚本的绝对路径
,示例代码:运行结果:1
sh /home/kaka/helloworld.sh
1
helloworld
- 通过bash执行,示例代码:运行结果:
1
bash helloworld.sh
1
helloworld
脚本路径
直接输入脚本路径执行,该方式需要对脚本文件本身有执行的权限。
举例:
- 相对路径,示例代码:运行结果:
1
./helloworld.sh
1
helloworld
- 绝对路径,示例代码:运行结果:
1
/home/kaka/helloworld.sh
1
helloworld
比较
第一种方法,bash/sh 脚本
,本质是调用bash解释器来读取文件,执行脚本,所以脚本本身不需要执行权限。
第二种方法,直接输入路径,本质是脚本需要自己执行,所以需要执行权限。
.或source 脚本路径
先看例子。
假设存在一个Shell脚本test.sh
,内容如下:
1 | !/bin/bash |
首先,我们采用sh
的方式进行执行,示例代码:
1 | sh test.sh |
运行结果:
1 | 5 |
这一步没有任何问题,然后我们echo $A
,会发现没有运行结果。
再用直接输入路径的方式,示例代码:
1 | ./test.sh |
运行结果:
1 | 5 |
同样,我们echo $A
,同样会发现没有运行结果。
第三种方法,我们通过.
来执行,示例代码:
1 | . test.sh |
运行结果:
1 | 5 |
然后我们echo $A
,示例代码:
1 | echo $A |
运行结果:
1 | 5 |
现在有运行结果了。
前两种方式都是在当前Shell中打开一个子Shell来执行脚本内容,当脚本内容结束,子Shell关闭。
第三种,也就是使用在脚本路径前加.
或者source
的方式,可以使脚本内容在当前Shell
里执行,而无需打开子Shell
。
这也是为什么我们每次要修改完/etc/profile
文件以后,需要source
一下的原因。
开子Shell
与不开子Shell
的区别就在于,环境变量的继承关系,在子Shell
中设置的环境变量,一般在父Shell
中是不可见的。
一般执行自己的脚本,不用第三种方法。
变量
系统预定义变量
常用系统变量:$HOME
$PWD
$SHELL
$USER
等
示例代码:
1 | echo $HOME |
运行结果:
1 | /home/kaka |
自定义变量
操作
定义变量
方法:
1 | 变量名=变量值 |
注意:=
号前后不能有空格。
撤销变量
方法:
1 | unset 变量名 |
声明静态变量
方法:
1 | readonly 变量 |
注意:静态变量不能被修改,也不能被unset
。
规则
- 变量名称由字母、数字和下划线组成,但是不能以数字开头,环境变量名建议大写。
- 等号两侧不能有空格
- 在
bash
中,变量默认类型都是字符串类型,无法直接进行数值运算。 - 变量的值如果有空格,需要使用双引号或单引号括起来。
例子
举例:
- 定义变量
A
,示例代码:运行结果:1
2A=5
echo $A1
5
- 给变量
A
重新赋值,示例代码:运行结果:1
2A=8
echo $A1
8
- 撤销变量
A
,示例代码:1
unset A
- 声明静态的变量,此时不能重新赋值,不能unset,示例代码:运行结果:
1
2
3
4readonly B=2
echo $B
B=3
unset B1
2
32
-bash: B: readonly variable
-bash: unset: B: cannot unset: readonly variable - 在bash中,变量默认类型都是字符串类型,无法直接进行数值运算。
示例代码:运行结果:1
2C=1+2
echo $C这种方式,同样无法进行数值运算,示例代码:1
1+2
运行结果:1
2
3
4d1=1
d2=2
d3=d1+d2
echo $d3如果需要相加,可以采用如下的方式。示例代码:1
d1+d2
运行结果:1
2C=$((1+2))
echo $C示例代码:1
3
运行结果:1
2d3=$((d1+d2))
echo $d31
3
- 变量的值如果有空格,需要使用双引号或单引号括起来。示例代码:运行结果:
1
D=123 456
示例代码:1
bash: 456: command not found...
运行结果:1
2D='123 456'
echo $D1
123 456
export
可把变量提升为全局环境变量,供其他Shell程序使用。
新建helloworld.sh
,内容如下,示例代码:执行1
2
3!/bin/bash
echo "helloworld"
echo $B./helloworld.sh
,运行结果:并没有打印输出变量B的值。1
helloworld
我们export B
,再执行./helloworld.sh
,示例代码:运行结果:1
2export B
./helloworld.sh(题外话,在《基于JavaScript的前端开发入门:4.Node.js》,有一个关键词1
2helloworld
2exports
(注意,有s
),作为有点类似,提升为"全局"。)
查看所有变量
set
:查看当前Shell的所有变量
特殊变量
$n
$n
:n为数字,$0
代表该脚本名称,$1
到$9
代表第一到第九个参数,十以上的参数需要用花括号括号包含,如${10}
。
parameter.sh
:
1 | echo $0 |
1 | sh parameter.sh a b c d e f g h i j k l m |
运行结果:
1 | parameter.sh |
$#
$#
:获取所有输入参数个数。
parameter.sh
:
1 | echo $# |
1 | sh parameter.sh 1 2 3 4 5 6 7 8 9 10 |
运行结果:
1 | 10 |
$* $@
$*
和$@
都表示传递给函数或脚本的所有参数
- 不被双引号
""
包含时,都以$1
、$2
、$n
的形式输出所有参数。 - 被双引号
""
包含时$*
会将所有的参数作为一个整体,以$1 $2 ... $n
的形式输出所有参数$@
会将各个参数分开,以$1
、$2
、...
、$n
的形式输出所有参数
示例代码:
1 | !/bin/bash |
1 | sh parameter.sh 1 2 3 4 5 |
运行结果:
1 | === $n === |
$?
$?
,有两个作用:
- 表示最后一次的命令的结果。
- 表示最后一次的命令是否正常结束,
0
代表没有问题(?),是正常结束的。非0
代表有问题。
我们执行sh parameter.sh 1 2 3 4 5
,再执行echo $?
,示例代码:
1 | echo $? |
运行结果:
1 | 0 |
如果不具有对parameter.sh
的执行权限,我们执行parameter.sh
,示例代码:
1 | ./parameter.sh 1 2 3 4 5 |
运行结果:
1 | -bash: ./parameter.sh: Permission denied |
再执行echo $?
,示例代码:
1 | echo $? |
运行结果:
1 | 126 |
运算符
语法:
$((运算式))
$[运算式]
示例代码:
1 | a1=$((1+2)) |
运行结果:
1 | 3 |
示例代码:
1 | a2=$[1+2+3] |
运行结果:
1 | 6 |
条件判断
有两种语法格式:
[ 条件表达式 ]
注意条件表达式
前后要有空格。test 条件表达式
其中最常见的还是第一种。
常用判断条件:
-
两个整数之间比较:
-eq
,等于-ne
,不等于-lt
,小于-le
,小于等于-gt
,大于-ge
,大于等于
-
字符串之间的比较:
=
,相等!=
,不相等
-
文件权限的判断
-r
,有读的权限-w
,有写的权限-x
,有执行的权限
-
文件类型的判断
-e
,文件存在-f
,文件存在并且是一个常规文件-d
,文件存在并且是一个目录
多条件判断:
&&
,上一条命令执行成功时,才执行。||
,上一条命令执行失败时,才执行。
举例:
test
格式的判断,条件为真的情况,示例代码:运行结果:1
2test 3 -ge 2
echo $?1
0
test
格式的判断,条件为假的情况,示例代码:运行结果:1
2test 1 -ge 2
echo $?1
1
[ ]
格式的判断,示例代码:运行结果:1
2[ 3 -ge 2 ]
echo $?1
0
- 判断对文件是否有写的权限,示例代码:运行结果:
1
2[ -w parameter.sh ]
echo $?1
0
- 判断对文件是否有执行的权限,示例代码:运行结果:
1
2[ -x parameter.sh ]
echo $?1
1
- 判断文件是否存在,示例代码:运行结果:
1
2[ -e parameter.sh ]
echo $?1
0
- 多条件判断,示例代码:运行结果:
1
[ kaka ] && echo ok || echo notOk
1
ok
- 示例代码:运行结果:
1
[ ] && echo ok || echo notOk
1
notOk
流程控制
if
单分支:
两种语法格式:
- 第一种,示例代码:
1
2
3if [ 条件判断式 ];then
程序
fi - 第二种,示例代码:
1
2
3
4if [ 条件判断式 ]
then
程序
fi
多分支:
语法格式:
1 | if [ 条件判断式 ] |
示例代码:
1 | !/bin/bash |
1 | sh if.sh 1 |
运行结果:
1 | 条件一命中 |
case
语法格式:
1 | case $变量名 in |
注意事项:
case
行尾必须为单词in
,每一个模式匹配必须以右括号)
结束。- 双分号
;;
表示命令序列结束,相当于Java中的break
。 - 最后的
*)
表示默认模式,相当于Java中的default
。
示例代码:
1 | !/bin/bash |
1 | sh case.sh 3 |
运行结果:
1 | 其他情况命中 |
for
两种用法:
- 用法一:
1
2
3
4for (( 初始值;循环控制条件;变量变化 ))
do
程序
done - 用法二:
1
2
3
4for 变量 in 值一 值二 值三 ...
do
程序
done
举例:
-
用法一,示例代码:
1
2
3
4
5
6!/bin/bash
for((i=0;i<=10;i++))
do
echo $i
done1
sh for1.sh
运行结果:
1
2
3
4
5
6
7
8
9
10
110
1
2
3
4
5
6
7
8
9
10 -
用法二,示例代码:
1
2
3
4
5
6!/bin/bash
for v in a b c
do
echo $v
done运行结果:
1
2
3a
b
c
while
语法格式:
1 | while [ 条件判断式 ] |
示例代码:
1 | !/bin/bash |
1 | sh while.sh 10 |
运行结果:
1 | 45 |
read
read
:读取控制台输入。
语法格式:read 选项 参数
常用选项:
-p
,指定读取值时的提示符-t
,指定读取值时等待的时间(秒),默认一直等待
参数:指定读取值的变量名
假设存在read.sh
,内容如下:
1 | !/bin/bash |
函数
调用命令
调用命令的方法为$(命令)
示例代码:
1 | !/bin/bash |
1 | sh func1.sh kaka |
运行结果:
1 | kaka_log_1658325297 |
有些资料把"调用命令"称为"调用系统函数",我个人认为不完全准确,部分命令并不属于系统函数,可能是额外安装的。
自定义函数
语法格式:
1 | [ function ] funname[()] |
[ ]
包裹的部分为可选。
注意:必须在调用函数地方之前,先声明函数,Shell脚本是逐行运行,不会像其它语言一样先编译。
示例代码:
1 | !/bin/bash |
我们执行脚本,依次输入1
、2
,打印结果3
。这个没有任何问题。
再来一个,输入100
、200
。
这就有问题了!
这是因为return的返回值只能在之间。
解决方法为我们用echo
返回,并对调用方式进行调整,从sum $n1 $n2;
改为sum=$(sum $n1 $n2)
。
示例代码:
1 | !/bin/bash |