title: shell学习笔记
date: 2021-04-25 15:59:05
[TOC]
0. 前言
鉴于实习中经常使用 shell 语言,因此趁此机会好好学习一下“强大”的 shell 语言。
参考:
1. 简介
shell 直译为壳。在操作系统(Linux)中,被称为外壳,通常与 kernel 内核相对立。在没有图形界面的时代,shell 是用户与操作系统交互的接口。
shell 本身是一个程序。在操作系统实验课程中,老师曾让我们实现一个微型 shell ,代码见:https://github.com/99MyCql/OS_pratice。它包括 shell 的许多基本功能:命令提示符、运行命令/程序(如:cat、echo、ls等)、重定向输入输出、管道。完成实验时,成就感满满,几乎与真实的 shell 无疑。
但,当时的我可能忽略了 shell 的另一个“身份”——解释器。shell 解释器可运行 shell 脚本语言,它支持变量、条件判断、循环等等语法。这让 shell 具备了可编程性,而失去这一大功能的 shell 只能称为 mini shell 。
关于解释型语言的定义,以及与编译型语言的区别,就不在此赘述了。
shell 分为很多种,包括:Bourne Shell(sh)、Bourne Again shell(bash)、Z Shell(zsh)等等,它们的本质基本相同,本文将主要基于 Bash Shell 。
接触过 python 的hxd应该知道,python 既可以在解释器中一行一行地敲,也可以写在一个文件中再运行文件。
shell 也是同理,既可以运行在命令行,也可以写入脚本文件再执行。shell 脚本语言的代码文件以 .sh
结尾:
- 可以通过执行解释器执行:
bash test.sh
- 或者直接使用当前命令行的解释器执行:相对路径执行
./test.sh
或绝对路径/home/test/test.sh
(需为脚本添加可执行权限chmod +x test.sh
)
在 shell 脚本文件中,需要在第一行添加如下内容,指定解释器,如下指定 bash shell 为解释器:
#!/bin/bash
# 或者, env 命令(这个命令总是在 /usr/bin 目录),返回 Bash 可执行文件的位置
#!/usr/bin/env bash
shell 中注释为 #
。
2. 使用命令
在 shell 命令行中,可以输入cat、echo、grep等命令(这些命令本质是一个可执行文件),去执行对应的程序。
同样,在 shell 脚本中,也可以使用命令,包括内部命令和外部命令(在我看来 shell 语言中的命令就相当于其他语言中的库函数)。比如:
#!/bin/bash
echo "hello world"
同时还包括:
- 管道
|
- 重定向
<
>
- 命令结束符
;
。使用:Command1 ; Command2
允许单行多个命令,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。 - 命令组合符
&&
||
。使用:Command1 && Command2
第一个命令运行成功,才继续运行第二个命令;Command1 || Command2
第一个命令运行失败,才继续运行第二个命令。
更多 Linux 命令可以参见我的另一篇笔记:linux命令学习笔记。
3. 变量
定义变量
var=value
var='value'
var="value"
变量名:由字母、数字和下划线字符组成;首字符必须是字母下划线,不能是数字。
变量值:没有数据类型的概念,都是字符串,如果值中包含空格,需使用引号包围。
注意:赋值符号附近不能有空格。
同时,可以将命令执行的结果赋给变量:
var=`command`
var=$(command) # 更推荐这种表示方式
也可以将运算结果赋给变量:
var=$((5 * 7))
使用变量
两种方式:
$var
${var} # 更推荐这种表示方式
花括号 {}
用于区分变量边界。比如:在如下代码中,不使用花括号,会把 varScript
当成变量。
var="Java"
echo "I am good at $varScript" # 错误
如果变量值包含连续空格或制表符,使用变量时应用双引号 ""
包围起来,因为 Shell 会将多个空格合为一个:
var="1 2 3"
echo $var # 1 2 3
echo "$var" # 1 2 3
注意:当使用单引号 ''
将包围变量时,变量将不会解析,而是会被当成普通字符串:
var="1 2 3"
echo $var # 1 2 3
echo "$var" # 1 2 3
echo '$var' # $var
修改变量值
重新赋值即可:
var="hello world" # hello world
echo $var
var="hello world!!!" # hello world!!!
echo $var
删除变量
var="hello world"
echo $var # hello world
unset var
echo $var #
shell 中不存在的变量一律等于空字符串,所以即使unset命令删除了变量,还是可以读取这个变量(值为空字符串)。而且,被删除的变量可再次使用。
环境变量
用户创建的变量仅用于当前 Shell,子 Shell (在当前shell中运行的shell)默认读取不到父 Shell 定义的变量。
使用 export
命令可以设置变量为环境变量,使子 shell 可以读取该变量。
测试脚本 test.sh
如下:
#!/bin/bash
echo $test_export
echo $test_noexport
export test_export2="export"
运行:
$ export test_export="export" # 设为环境变量,子 shell 可读
$ test_noexport="no export"
$ ./test.sh
export
$ echo $test_export2 # 显然,父 shell 也读不到子 shell export 的变量
注意:子 Shell 如果修改环境变量,不会影响父 Shell 。
常用的环境变量有:
-
HOME
:用户的主目录。 -
HOST
:当前主机的名称。 -
PATH
:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。 -
PWD
:当前工作目录。 -
USER
:当前用户的用户名。 -
LINENO
:返回它在脚本中的行号。 -
FUNCNAME
:返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数。 -
BASH_SOURCE
:返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本
只读变量
readonly
命令指示变量只读,不可修改。
readonly var="hello"
var="hello world" # var: readonly variable
echo $var # hello
变量默认值
-
${var:-word}
如果变量 var 为空或已被删除(unset),那么返回 word,但不改变 var 的值。 -
${var:=word}
如果变量 var 为空或已被删除(unset),那么返回 word,并将 var 的值设置为 word。 -
${var:?message}
如果变量 var 为空或已被删除(unset),那么将消息 message 送到标准错误输出,并将脚本停止运行,可以用来检测变量 var 是否可以被正常赋值。 -
${var:+word}
如果变量 var 被定义,那么返回 word,但不改变 var 的值。
特殊变量
-
$0
当前脚本的文件名。 -
$n
传递给脚本或函数的参数,$1
表示第一个参数,$2
表示第二个参数。 -
$#
传递给脚本或函数的参数个数。 -
$*
传递给脚本或函数的所有参数。 -
$@
传递给脚本或函数的所有参数。被双引号""
包含时,$*
会将所有参数作为一个整体,而$@
会分开。 -
$?
上个命令的退出状态,或函数的返回值。 -
$$
当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。
4. 字符串
字符串是 shell 最基本的数据类型。
拼接字符串(推荐使用 {}
):
str="world"
echo "hello $str!" # hello world!
echo "hello ${str}!" # hello world!
获取字符串长度(变量使用必须要加 {}
):
str="hello"
echo ${#str} # 5
提取字符串(offset
默认为0,length
默认到结尾):
${str:offset:length}
字符转义:
- 经典的转义字符
\n
\t
转义; - 对于 shell 中的特殊字符,如
$
*
&
等,需要转义; - 使用单引号
''
时,转义字符都会被当成普通字符串
字符串匹配并删除:
${str#pattern}
从字符串首字符开始,删除最短匹配的部分,返回剩余字符串。pattern
支持*
、?
、[]
等通配符。-
${str##pattern}
从字符串首字符开始,删除最长匹配(贪婪匹配)的部分,返回剩余字符串。str=/home/root/shell/study str=${str#/*/} # root/shell/study echo $str echo ${str##/*/} # root/shell/study str=/home/root/shell/study echo ${str##/*/} # study
${str%pattern}
从字符串尾字符开始,删除最短匹配的部分,返回剩余字符串。${str%%pattern}
从字符串尾字符开始,删除最长匹配的部分,返回剩余字符串。
更高级的匹配建议使用:grep
、awk
。
5. 数组
定义数组
arr=(value0 value1 value2 value3)
或
arr=(
value0
value1
value2
value3
)
或单独定义(可以不使用连续的下标,而且下标的范围没有限制):
arr[0]=value0
arr[1]=value1
arr[3]=value3
追加
使用 +=
可追加元素:
arr=(a b c d)
echo $arr # a b c d
arr+=(e f)
echo ${arr[@]} # a b c d e f
使用数组
单个元素:
value=${arr[i]}
全部元素:
${arr[*]}
${arr[@]}
注意:
- 默认
${arr} = ${arr[0]}
而非全部元素。 -
${arr[@]}
"${arr[@]}"
${arr[*]}
"${arr[*]}"
有不同效果,详情见:读取所有成员,推荐使用"${arr[@]}"
多个元素:
${arr[@]:offset:length}
获取数组长度:
${#arr[*]}
${#arr[@]}
获知数组哪个位置上有值,即获取数组中存在值的元素的索引(提取数组索引):
unset arr
arr[1]=a
arr[3]=b
echo ${!arr[@]} # 1 3
6. 运算表达式
语法:使用 (( ))
包裹,或者使用 expr
命令。更推荐前一种。
获取表达式的结果:$(( ))
。
在表达式中可以使用变量,且不需要加$
。若变量为空,则当作 0 。
在表达式中,可以使用进制:默认十进制、0num
八进制、0xnum
十六进制、base#num
base进制
算术运算
-
+
加法 -
-
减法 -
*
乘法 -
/
除法(整除) -
%
余数 -
**
指数 -
++
自增运算(前缀或后缀) -
--
自减运算(前缀或后缀)
i=0
echo $((++i)) # 1
echo $(((1+2) * 3)) # 9
位运算
与 C 语言一致:
-
<<
左移 -
>>
右移 -
&
与 -
|
或 -
~
按位取反 -
^
异或
echo $((16>>2)) # 4
逻辑运算
与 C 语言一致:
-
<
小于 -
>
大于 -
<=
小于或相等 -
>=
大于或相等 -
==
相等 -
!=
不相等 -
&&
逻辑与 -
||
逻辑或 -
!
逻辑否 -
expr1 ? expr2 : expr3
三元条件运算
如果逻辑表达式为真,返回1,否则返回0:
echo $((3 > 2)) # 1
赋值运算
支持直接赋值 =
,也支持 +=
*=
|=
等等。
i=1
echo $((i+=1)) # 2
7. 条件判断 if
if commands
then
commands
[elif commands
then
commands...]
[else
commands]
fi
# 或
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
if
后面所接的判断条件是一个命令,命令返回成功(0)则为真,返回失败(非0)则为假。
test
if
判断条件通常使用 test
命令,它是一个用于判断的命令,它有三种形式:
# 写法一
test expr
# 写法二
[ expr ]
# 写法三
[[ expr ]]
注意:
- 中括号
[ ]
与表达式之间必须包含空格 - 第二种形式与第三种形式,在某些场景(比如逻辑判断)有所不同,详情参考:bash中 [ ] 与 [[ ]] 的区别
由于 test
是一个命令,它支持很多选项:
1) 文件判断
-
[ -a $file ]
:如果 file 存在,则为true。 -
[ -b $file ]
:如果 file 存在并且是一个块(设备)文件,则为true。 -
[ -c $file ]
:如果 file 存在并且是一个字符(设备)文件,则为true。 -
[ -d $file ]
:如果 file 存在并且是一个目录,则为true。 -
[ -e $file ]
:如果 file 存在,则为true。 -
[ -f $file ]
:如果 file 存在并且是一个普通文件,则为true。
更多见:条件判断
2) 字符串判断
-
[ $str ]
:如果str不为空(长度大于0),则判断为真。 -
[ -n $str ]
:如果字符串str的长度大于零,则判断为真。 -
[ -z $str ]
:如果字符串str的长度为零,则判断为真。 -
[ $str1 = $str2 ]
:如果str1和str2相同,则判断为真。 -
[ $str1 == $str2 ]
:等同于[ str2 ]。 -
[ $str1 != $str2 ]
:如果str1和str2不相同,则判断为真。 -
[ $str1 '>' $str2 ]
:如果按照字典顺序str1排列在str2之后,则判断为真。 -
[ $str1 '<' $str2 ]
:如果按照字典顺序str1排列在str2之前,则判断为真。
注意:test命令内部的>
和<
,必须用引号括起来(或者是用反斜杠转义),否则它们会被 shell 解释为重定向操作符。
3) 整数判断
由于 >
<
会被误解为重定向操作法,所以有专门的整数判断指令。
-
[ $int1 -eq $int2 ]
:如果int1等于int2,则为true。 -
[ $int1 -ne $int2 ]
:如果int1不等于int2,则为true。 -
[ $int1 -le $int2 ]
:如果int1小于或等于int2,则为true。 -
[ $int1 -lt $int2 ]
:如果int1小于int2,则为true。 -
[ $int1 -ge $int2 ]
:如果int1大于或等于int2,则为true。 -
[ $int1 -gt $int2 ]
:如果int1大于int2,则为true。
4) 逻辑判断
-
[[ $expr1 && $expr1 ]]
/[ $expr1 ] && [ $expr1 ]
-
[[ $expr1 || $expr1 ]]
/[ $expr1 ] || [ $expr1 ]
-
[ ! $expr1 ]
/[ ! \( $expr1 && $expr2 \) ]
注意:test命令内部使用(
和)
必须使用引号或转义。
运算表达式
if
判断条件也可以使用运算表达式 (( ))
。
但注意:运算表达式返回非0 ((1))
表示真,返回0 ((0))
表示假。
echo $((2 > 1)) # 1
if ((2>1)); then
echo true # true
else
echo false
fi
普通命令
if
判断条件可以直接使用命令,命令返回成功(0)则为真,返回失败(非0)则为假。
当然,也可以使用管道、重定向、命令结束符;
、命令组合符&&
||
等。
比如:
if mkdir temp && cd temp; then
echo "enter in temp/"
fi
8. case
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac
pattern
支持基本的模式匹配,比如:
echo -n "输入一个字母或数字 > "
read character
case $character in
[[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
;;
[0-9] ) echo "输入了数字 $character"
;;
* ) echo "输入不符合要求"
esac
9. 循环
while
while commands; do
commands
done
判断条件与 if
一样。
until
until commands; do
commands
done
for
遍历列表每一项:
for variable in ${arr[@]}; do
commands
done
或:
for (( expr1; expr2; expr3 )); do
commands
done
比如:
for ((i=0; i<10; i++)); do
echo $i
done
for i in $(seq 0 9); do
echo $i
done
continue
提前终止本轮循环,进行下一轮循环。
10. 函数
定义:
# 第一种
func() {
}
# 第二种
function func() {
}
调用:
func # 直接调用无参数
func param1 param2 # 传入参数
参数
$1~$9
:函数的第1个到第9个的参数。
$0
:函数所在的脚本名。
$#
:函数的参数总数。
$@
:函数的全部参数,参数之间使用空格分隔。
$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
return
函数返回,可指定返回值,调用者通过 $?
获取。
local 局部变量
shell 中定义变量属于全局变量,在函数中声明局部变量需使用 local
,比如:
func1() {
foo1=1
echo $foo1 # 1
}
func1
echo $foo1 # 1
func2() {
local foo2
foo2=2
echo $foo2 # 2
}
func2
echo $foo2 # 空
其它
主要介绍在脚本中使用较多,而在命令行中使用较少的命令。
set
命令行下不带任何参数,直接运行 set
,会显示所有的环境变量和 Shell 函数。
常用选项:
-
set -u
遇到不存在的变量则报错(默认会跳过) -
set -x
在运行命令前,先输出该命令,常用于调试。set -x
开启,set +x
关闭。 -
set -e
遇到错误则终止执行(默认命令执行出错会忽略)。set -e
有一个例外情况,就是不适用于管道命令(多个子命令通过管道符组合,Bash 会把最后一个子命令的返回值,作为整个命令的返回值)。 -
set -o pipefail
用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。
使用示例:
# set -x # 调试时再开启
set -euo pipefail
注意:
- 使用
set -e
后,如果调用函数,函数返回了非零值,程序也会退出!
read
read [-options] [var...]
输入由回车结束,用户输入将被保存到变量 var
,多个输入项通过空格区分。
若未提供变量名,环境变量 REPLY
会包含用户输入的一整行数据。
若提供的输入项少于变量数目,则剩余变量为空。
常用选项:
-
p
指定提示信息。
read -p "Enter your input:"
echo $REPLY
-
a
把用户的输入赋值给一个数组,从零号位置开始。
read -a arr
echo ${arr[@]}
-
n
指定只读取若干个字符作为变量值,而不是整行读取。
read -n 3 var
echo $var
read 还可用于读文件:
filename='xxx'
while read line; do
echo "$line"
done < $filename
exit
用于退出当前执行的 Shell ,并返回一个值,返回 0 代表成功,返回 非0 代表失败。
source
用于执行一个脚本文件,但不同于直接执行(会新建子 shell ),source
会在当前 shell 执行。
类似于加载外部库。