shell学习笔记


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} 从字符串尾字符开始,删除最长匹配的部分,返回剩余字符串。

更高级的匹配建议使用:grepawk

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 ]:等同于[ str1 =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 执行。

类似于加载外部库。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容