03 执行过程和原理
执行
-
shell脚本一般以.sh结尾,也可以没有,这是一个约定;第一行需要指定用什么命令解释器来执行
#! /bin/bash#! /user/bin/env bash -
启动 Shell 脚本的三种方式
# 直接以文件名运行,前提是文件名要有可执行权限(子进程执行) ./filename.sh # 解释器跟上文件路径来运行(子进程执行) bash ./filename.sh # 使用source运行(当前进程中执行) source ./filename.sh
执行过程
-
字符解析
- 识别换行符、分号(;)做行的分割
- 识别命令连接符( || && 管道)做命令的分割
- 识别空格、tab符,做命令和参数的分割
-
shell展开,例如 {1...3} 解析为1 2 3
-
重定向,将标准输入输出 stdin、stdout、stderr 的文件描述符的指向进行变更
-
执行命令
- builtin直接执行
- 非builtin使用$PATH查找,然后启动子进程执行
-
收集状态并返回给脚本
Shell展开
Shell 展开是指在执行命令之前,Shell 预处理命令行中的各种特殊字符,将它们替换为实际的值或执行相应的操作。
1. 大括号展开
大括号展开(Brace Expansion):使用大括号 {} 将一组字符串包含在命令行中,Shell 将展开大括号内的所有字符串,并将其作为命令行的一部分。例如:echo file{1..3}.txt 将展开为 file1.txt file2.txt file3.txt。
一般由三部分构成,前缀、一对大括号、后缀,大括号内可以是逗号分割的字符串序列,也可以是序列表达式 {x..y[..incr]}
# 字符串序列
a(b,c,d)e => abe ace ade
# 表达式序列,(数字可以使用incr调整增量,字母不行)
{1..5} => 1 2 3 4 5
# 也可以改变展开的增量,比如下方展开1~5,增量为2,所以就是1 3 5
{1..5..2} => 1 3 5
# 字母也可以展开,但是不可以调整增量
{a..e} => a b c d e
2. 波浪号展开
波浪号展开(Tilde Expansion):使用波浪号 ~ 将用户名或路径名包含在命令行中,Shell 将展开波浪号并将其替换为相应的用户名或路径名。例如:cd ~/Documents 将进入当前用户的 Documents 目录。
# 当前用户主目录
~ => $HOME
~/fo => $HOME/foo
# 指定用户的主目录
-fred/foo => 用户fred的SHOEM/foo
# 当前工作目录
~+/foo => $PWD/foo
# 上一个工作目录
~-/foo => ${$OLDPWD-'~-')/foo
3. 参数展开
参数展开(Parameter Expansion):使用 $ 将变量名包含在命令行中,并使用一些特殊符号来修改变量的值或进行字符串操作。例如:${var:-default} 表示如果变量 $var 未定义,则使用默认值 default。
-
间接参数扩展
${!parameter},其中引用的参数并不是parameter而是parameter的实际的值parameter="var" # 定义一个名为 parameter 的变量,其值为字符串 "var" var="hello" # 定义一个名为 var 的变量,其值为字符串 "hello" echo ${!parameter} # 使用参数展开的形式,将变量 $parameter 的值 "var" 替换为 $var,从而得到变量 $var 的值 "hello",并将其输出到标准输出 #输出 hello -
参数长度
${#parameter}par=cd echo $(#par) # 输出2 -
空参数处理
${parameter:-word} # 为空替换
{parameter:=word} # 为空替换,并将值赋给parameter变量
$parameter:?word) # 为空报错
${parameter:+word} # 不为空替换
a=1 # 定义一个名为 a 的变量,其值为整数 1 echo ${a:-word} #1 # 输出变量 $a 的值,如果 $a 未定义或为空,则输出 "word"。由于 $a 已经定义并赋值为 1,因此输出为 1。 echo $(b:-word) #word # 执行命令替换,执行命令 $b 并将其输出作为结果,如果 $b 未定义或为空,则输出 "word"。由于 $b 未定义,因此输出为 "word"。 echo $(par:=word) #word # 执行命令替换,将变量 $par 的值设置为 "word",并将其输出作为结果。由于 $par 未定义,因此输出为 "word"。 echo $(par:-hello) #word # 执行命令替换,输出变量 $par 的值,如果 $par 未定义或为空,则输出 "hello"。由于 $par 未定义,因此输出为 "word"。 echo $(par:+foo) #foo # 执行命令替换,如果 $par 已经定义且非空,则输出 "foo",否则不输出任何内容。由于 $par 未定义,因此不输出任何内容。 #输出分别为:1 word word word foo -
参数切片
${parameter:offset)
${parameter:offset:length}
# 这个和 JS 数组的 slice 方法比较像,这里就不举例了 -
参数部分删除,表示对参数进行掐头去尾的操作
${parameter%word} # 最小限度从后面截取word
${parameter%%word} # 最大限度从后面截取word
${parameter#word} # 最小限度从前面截取word
${parameter##word} # 最大限度从前面截取word
#!/bin/sh str=abcdefg # 定义一个名为 str 的变量,其值为字符串 "abcdefg" spl=$(str##*d) # 执行命令替换,删除变量 $str 开头到最后一个 "d" 之前的所有字符,并将剩余的字符串赋值给变量 $spl。由于变量 $str 的值为 "abcdefg",因此 $spl 的值为 "efg"。 # * 是通配符的一种,表示匹配任意数量的任意字符(包括 0 个字符)。具体来说,*d 表示匹配以字母 “d” 结尾的字符串,并将匹配到的字符串删除。而 ## 表示从变量的开头开始匹配,删除最长的匹配项。 sp2=${str%%d*} # 执行参数展开,删除变量 $str 最后一个 "d" 以及之后的所有字符,并将剩余的字符串赋值给变量 $sp2。由于变量 $str 的值为 "abcdefg",因此 $sp2 的值为 "abc"。 echo $spl #输出efg # 输出变量 $spl 的值,即字符串 "efg"。 echo $sp2 #输出abc # 输出变量 $sp2 的值,即字符串 "abc"。 # echo的两个输出分别如下:efg abc
4. 命令替换
命令替换(Command Substitution):使用反引号或 $() 将一个命令包括起来,Shell 将执行该命令并将其输出作为命令行的一部分。例如:echo "The date is $(date)" 将输出当前日期和时间。
在子进程中执行命令,并用得到的结果替换包裹的内容,形式上有两种:$(...) 或 ...
#! /bin/bash
echo ${whoimi} # 输出变量 $whoimi 的值。由于 $whoimi 未定义,因此输出为空。
foo(){ # 定义一个名为 foo 的函数
echo "asdasd" # 在函数中输出字符串 "asdasd"
}
a=`foo` # 执行命令替换,执行函数 foo 并将其输出作为结果,将结果赋值给变量 $a。由于函数 foo 的输出为 "asdasd",因此变量 $a 的值为 "asdasd"。
5. 数学计算
数学计算(Arithmetic Expansion):使用 $(( )) 将一个算术表达式包含在命令行中,Shell 将计算该表达式并将其替换为计算结果。例如:echo $((1+2)) 将输出 3。
使用 $(()) 包裹数学运算表达式,得到结果并替换
#! /bin/bash
echo $((1+2)) # 3
6. 文件名展开
文件名展开(Filename Expansion):使用通配符 *、?、[] 等来匹配文件名或路径名。Shell 将展开通配符并将其替换为匹配的文件名或路径名。例如:ls *.txt 将列出当前目录下所有扩展名为 .txt 的文件。
当有单词没有被引号包裹,且其中出现了 *, ?, 和 [ 字符,则shell会去按照正则匹配的方式查找文件名进行替换,如果没找到则保持不变
#! /bin/bash
$ echo D*
# 输出当前目录下所有以 D字母开头的目录、文件