Shell脚本执行过程

一、#!有什么用

脚本程序几乎都开始于#! 符号。这个符号的作用是声明解释器。

#! 就是告诉系统本文件要由哪个解释器执行。比如写shell脚本,第一行是 #!/bin/bash或 #!/bin/sh。如果写Python脚本,第一行就是#!/usr/bin/python。

大多数脚本语言都是将#后面出现的字符当作是注释,在脚本中并不起作用。这个 #! 和这个注释的规则不冲突么?

不冲突,因为第一行的 #! 其实是内核读取的,而不是脚本解释器读取的。为什么 #! 一定要写在第一行的前两个字符,因为这是在内核里写死的,它就检查前两个字符。当内核帮你选好了脚本解释器之后,调用解释器,后续的工作就都交给解释器做了。脚本的所有内容也都会原封不动的交给解释器再次解释,包括#!。但是由于对于解释器来说,#开头的字符串都是注释,并不生效,所以解释器会忽略第一行。


二、bash如何执行shell命令?

当第一行是 #!/bin/bash的时候,实际上内核给我们启动了一个bash进程,然后把脚本内容都传递给bash执行。

(一)第一层解析

bash会以一些特殊字符作为分隔符,把文本分段解析。最主要的分隔符无疑就是回车,类似功能的分隔符还有分号";"。所以在bash脚本中是以回车或者分号作为一行命令结束标志的。

这基本上就是第一层级的解析,主要目的是将大段的命令行分段。

(二)第二层解析

这一层级主要是区分所要执行的命令。这一层级主要解析的字符是管道"|",&&、||这样的可以起到连接命令作用的特殊字符。

这一层级解析完后,就得到最基本的一条条命令了。


(三)第三层解析

这一层主要是区分出要执行的命令和其参数,主要解析的是空格和tab字符。

这一层解析完,得到一个个token或者叫单词。

(四)第四层解析

解析每个token,主要是替换

下面是整个替换流程:

1.{}替换

Brace Expansion (Bash Reference Manual)

2.~替换

Tilde Expansion (Bash Reference Manual)

3.参数和变量替换

Shell Parameter Expansion (Bash Reference Manual)

4.命令替换

Command Substitution (Bash Reference Manual)

5.算术表达式替换

Arithmetic Expansion (Bash Reference Manual)

6.重定向替换

Process Substitution (Bash Reference Manual)

7.单词分割

Word Splitting (Bash Reference Manual)

8.文件名扩展(路径替换)

Filename Expansion (Bash Reference Manual)

9.引用移除

Quote Removal (Bash Reference Manual)

在上面所有步骤完成之后,所有不是上面替换得到的,未被引用的'"\,通通都移除掉。

只有大括号扩展、单词拆分、文件名扩展可以增加单词个数。

其它扩展将单个单词扩展为单个单词。唯一的例外是"$@"和$*,以及"${name[@]}"和${name[*]}的展开。

(五)判断第一个token的类型

优先顺序是:

别名:alias

关键字或者叫保留字:keyword

函数:function

内建命令:built in

哈希索引:hash

外部命令:command

先判断token是不是别名,如果不是则下一步

判断token是不是关键字,如果不是则下一步

判断token是不是函数,如果不是则下一步

判断token是不是内建命令,如果不是则下一步

判断token是不是在hash表里,如果不是则下一步


哈希索引:hash
hash功能实际上是针对外部命令做的一个功能。外部命令放在 $PATH路径中。bash在执行一个外部命令时所需要做的操作是:如果发现这个命令是个外部命令就按照 $PATH变量中按照目录路径的顺序,在每个目录中都遍历一遍,看看有没有对应的文件名。如果有,就fork、exec、wait。
$PATH变量包含的路径可能很多,目录中的文件数量也可能会很多。于是,遍历这些目录去查询文件名的行为就可能比较耗时。于是bash提供了一种功能,就是建立一个hash表,在第一次找到一个命令的路径之后,对其命令名和对应的路径建立一个hash索引。这样下次再执行这个命令的时候,就不用去遍历所有的目录了,只要查询索引就可以更快的找到命令路径,以加快执行程序的速度。

判断token是不是外部命令,如果不是则就报告命令不存在

就是在 $PATH路径下找命令,找到之后fork、exec、wait。如果没有这个可执行文件名,就报告命令不存在。

(六)调用命令

第一个token作为命令,其余token作为参数。调用命令。

三、脚本的退出

一个bash脚本的退出一般有多种方式,比如使用exit退出或者所有脚本命令执行完之后退出。无论怎么样退出,脚本都会有个返回码,而且返回码可能不同。
任何命令执行完之后都有返回码,主要用来判断这个命令是否执行成功。在交互bash中,我们可以使用 $? 来查看上一个命令的返回码。

返回码逻辑上有两类,0为真,非零为假。就是说,返回为0表示命令执行成功,非零表示执行失败。返回码的取值范围为0-255。其中错误返回码为1-255。bash为我们提供了一个内建命令exit,通过这个命令可以人为指定退出的返回码是多少。

if、while语句的条件判断实际上就是判断命令的返回值,而不是输出。

考虑到程序退出可能性的各种可能,系统将错误返回码设计成1-255,这其中还分成两类:

(1)程序退出的返回码:1-127。这部分返回码一般用来作为给程序员自行设定错误退出用的返回码,比如:如果一个文件不存在,ls将返回2。如果要执行的命令不存在,则bash统一返回127。返回码125和126有特殊用处,一个是程序命令不存在的返回码,另一个是命令的文件在,但是不可执行的返回码。

(2)程序被信号打断的返回码:128-255。这部分系统习惯上是用来表示进程被信号打断的退出返回码的。一个进程如果被信号打断了,其退出返回码一般是128+信号编号的数字。

比如说,如果一个进程被2号信号打断的话,其返回码一般是128+2=130。如:

sleep 1000
^C
echo $?
130


在执行sleep命令的过程中,我使用Ctrl+c中断了进程的执行。此时返回值为130。

可以用内建命令kill -l查看所有信号和其对应的编号:

kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值