十一、函数
shell函数就是按照上面SHELL GRAMMAR的描述定义的保存着一系列等待稍后执行的命令。当shell函数名被当做一个简单命令名使用时,被这个函数名关联的一系列命令都被执行。函数在当前shell的上下文环境中被执行;不会创建新的进程来解释它们(这与shell脚本的执行形成了对比)。当执行函数时,函数的参数成为执行过程中的位置参数。特殊参数#被更新以反映这个变化。特殊参数0不会改变。函数执行时,FUNCNAME变量的第一个元素被设置为函数的名称。 shell执行环境的所有其他方面是在函数和其调用者之间是完全相同的,但下述情况除外:DEBUG捕获器和RETURN捕获器(查看下面SHELL BUILTIN COMMANDS的内置命令捕获器的相关描述)是非继承关系的,除非函数已经被设定trace属性(查看下面内置命令declare的相关描述)或者shell的内置命令set的-o functrace选项被启用(在这种情况下,函数会继承DEBUG捕获器和RETURN捕获器),同时ERR捕获器也是非继承的,除非启用了-o errtrace选项。 函数中的局部变量可以使用内置命令local来声明。通常情况下,变量和它们的值在函数和其调用者之间是共享的。 变量FUNCNEST,如果设置为一个大于0的数值类型的值的话,就定义了函数最大的嵌套等级。超出此限制的函数调用将导致全部命令被取消。 如果内置命令return函数中被执行,那么函数执行结束并且以函数调用后的下一个命令重新开始执行。任何跟RETURN捕获器有关联的命令都会在函数重新执行之前被执行。当一个函数执行结束,位置变量和特殊变量#的值会被恢复到函数执行之前的值。 函数名和定义可以使用内置命令declare或typeset加上-f参数来列出。如果在declare或typeset命令中使用-F选项将只列出函数名(如果shell的extdebug选项开启,还有源文件和行号)。函数可以使用内置命令export加上-f参数导出,使得子shell中它们被自动定义。函数定义也可以用内置命令unset并使用-f选项删除。注意在环境变量传递给子shell过程中,带有相同的shell函数和变量名可能会导致多重同名条目。 函数可以是递归的。变量FUNCNEST可以被用于限制函数调用栈的深度以及限制函数调用数。默认情况下,对于递归调用的次数没有硬性限制。十二、算术运算 在一定的环境下,shell允许算术表达式做数值计算(参见内置命令let和declare以及算术表达式)。求值计算使用没有溢出校验的固定宽度的整数来完成,但是除以0仍然是受限制的并且被打上错误标记。操作符及其优先级和聚合程度与C语言中相同。下列操作符的列表按照操作数等价优先级来分组。优先级的顺序是递减的。 id++ id-- 变量后自增和后自减 (自增自减在赋值后) ++id --id 变量先自增和先自减 (自增自减在赋值前) - + (单目的) 取负/取正 ! ~ 逻辑和位取反 ** 乘方 * / % 乘,除,模运算(取余) + - 加,减 << >> 左/右位移 <= >= < > 比较 == != 相等/不等 & 位与 (AND) ^ 位异或 (exclusive OR) | 位或 (OR) && 逻辑与 (AND) || 逻辑或 (OR) expr?expr:expr 条件操作 = *= /= %= += -= <<= >>= &= ^= |= 赋值 expr1 , expr2 逗号表达式 允许使用shell变量作为操作数;在表达式求值之前会进行参数展开。在一个表达式中,可以用名称引用shell变量,不必使用参数展开的语法,此时,shell变量为空或取消计算将值设置为0.变量的值是在变量作为算术表达式被引用时或者使用declare -i选项给变量赋予一个整数属性的值时,对该变量进行求值。空值当作0来计算。shell变量用于表达式中时,不必启用整数属性。 以0为前导的常量被当作八进制数,以0x或0X作为前导代表是十六进制。否则,数字将采用这样的形式:[base#]n,这里base是可选择的基数,是2到64之间的一个代表算数基数的十进制数字,n是在这个基数中的数字。如果忽略了base#,将以10为基数。大于9的数字依次以小写字母,大写字母,@和_表示。如果base小于或等于36,在表示10与35之间的数字时小写字母和大写字母可以交换使用。 操作数按照优先级顺序进行求值。圆括号中的子表达式被最先求值,可能会超越上面的优先级规则。十三、条件表达式 条件表达式用于[[复合命令以及内置命令test和[中,用来测试文件属性,进行字符串和算术比较。表达式由下面的单目或二由单目或双目操作符构成。如果某操作的任何file参数的形式是/dev/fd/n,那么将校验文件描述符n。如果某操作的file参数是/dev/stdin,/dev/stdout或者/dev/stderr之一,将分别检验文件描述符0,1和2。 除非特别说明,对文件进行的主要操作是跟随符号链接,在链接的目标上进行操作,而不是链接本身。 当使用[[的时候,<和>操作符会使用当前语言环境按照词典编撰的顺序排序。test命令使用ASCII编码顺序。 -a file 如果file存在则为真。 -b file 如果file存在且为块设备则为真。 -c file 如果file存在且为字符设备则为真。 -d file 如果file存在且是一个目录则为真。 -e file 如果file存在则为真。 -f file 如果file存在且为普通文件则为真。 -g file 如果file存在且是设置组ID的 (sgid) 则为真。 -h file 如果file存在且为符号链接则为真。 -k file 如果file存在且设置了 ``sticky'' 位 (粘滞位) 则为真。 -p file 如果file存在且是一个命名管道 (FIFO) 则为真。 -r file 如果file存在且可读则为真。 -s file 如果file存在且大小大于零则为真。 -t fd 如果文件描述符 fd 是打开的且对应一个终端则为真。 -u file 如果file存在且是设置用户ID的 (suid) 则为真。 -w file 如果file存在且可写则为真。 -x file 如果file存在且可执行则为真。 -O file 如果file存在且为有效用户ID所拥有则为真。 -G file 如果file存在且为有效组ID所拥有则为真。 -L file 如果file存在且为符号链接则为真。 -S file 如果file存在且为套接字则为真。 -N file 如果file存在且上次读取后被修改过则为真。 file1 -nt file2 如果file1比file2要新(根据修改日期),或者如果file1存在而file2不存在,则为真。 file1 -ot file2 如果file1比file2更旧,或者如果file1不存在而file2存在,则为真。 file1 -ef file2 如果file1和file2指的是相同的设备和inode号则为真。 -o optname 如果启用了shell选项optname则为真。参见下面对内置命令set的-o选项的描述中的选项列表。 -z string 如果string的长度为0则为真 -n string 如果string的长度非0则为真。 string1 == string2 string1 = string2 如果字符串相等则为真。= 可以用于test命令来兼容POSIX规范。 string1 != string2 如果字符串不相等则为真。 string1 < string2 如果string1在当前语言环境的字典顺序中排在string2之前则为真。 string1 > string2 如果string1在当前语言环境的字典顺序中排在string2之后则为真。 arg1 OP arg2 OP是-eq,-ne,-lt,-le,-gt,或-ge之一。这些算术二目操作符,如果arg1与arg2分别是相等,不等,小于,小于或等于,大于,大于或等于关系,返回值为真。arg1和arg2可以是正整数或负整数。十四、简单命令展开 当执行一个简单命令时,shell会从左到右进行下列展开,赋值和重定向。 1.分析器标记为与变量赋值(在命令名之前的)和重定向有关的字被保存等待稍后处理。 2.并非变量赋值或重定向的字被展开。如果展开后仍然有字保留下来,第一个字被作为命令名,其余字是参数。 3.重定向按照重定向的规则进行。 4.每个变量赋值中"="之后的文本在赋予变量之前要经过波浪线展开,参数展开,命令替换,算术展开和引用去除。 如果没有得到命令名,变量赋值影响当前shell环境。否则,变量被加入被执行的命令的环境中,不影响当前shell环境。任何试图为只读变量赋值的行为,将导致出错,命令以非零状态值退出。 如果没有得到命令名,重定向仍会进行,但是不影响当前shell环境。重定向出错将使命令以非零状态值退出。 如果展开后有命令名保留下来,那么执行过程如下所示。否则,命令退出。如果在任何展开中包含命令替换,那么整个命令的退出状态是被执行的最后一个命令替换的退出状态。如果没有进行命令替换,命令以状态零退出。十五、执行命令 命令被拆分为字之后,如果结果是一个简单命令和可选的参数列表,将执行下面的操作。 如果命令名中不包括斜杠,shell试图定位命令位置。如果存在同名的shell函数,函数将被执行。如果名称不是一个函数,shell从内置命令中查找它。如果找到对应命令,它将被执行。 如果名称既不是shell函数也不是一个内置命令,并且没有包含斜杠,bash查找PATH变量的每个元素,通过名称查找包含该可执行文件的目录。Bash使用散列表(hash表)来储存可执行文件的完整路径名。只有当hash表中没有找到该命令的时候才会对PATH变量中保存的元素目录进行完全查找。如果查找不成功,shell就去找一个已经被定义的名为command_not_fount_handle的shell函数。如果该函数存在,它就会使用源命令以及源命令的参数作为参数被调用执行,此函数的退出状态就会成为shell的退出状态。如果该函数未定义,shell输出错误消息,返回退出状态值127。 如果查找成功,或者命令中包含一个或多个斜杠,shell使用单独的执行环境来执行这个程序。参数0被设置为所给名称,命令的其他参数被设置为所给的参数,如果有的话。 如果因为文件不是可执行格式而执行失败,并且此文件不是目录,就假定它是一个shell script,一个包含shell命令的文件。此时将启动一个子shell来执行它。子shell重新初始化自身,效果就好像是 调用了一个新的shell来处理脚本一样,但是被父shell缓存的命令位置仍然被子shell保留。 如果程序是以"#!"开头的文件,那么第一行的其余部分指定了这个程序的解释器。shell在不会自行处理这种可执行文件格式的操作系统上执行指定的解释器。如果有的话,解释器的参数可由下面三部分组成: 程序第一行中紧跟在解释器名称之后的一个可选参数, 程序的名称,命令行参数。十六、命令执行环境 shell有执行环境的概念,由下列内容组成: 打开的文件在启动shell时被继承,通过内置命令exec提供的重定向来修改。 当前工作目录使用cd、pushd或者popd命令来设置,或是由shell在启动时继承得到。 文件创建模式掩码使用umask命令来设置或是从shell的父进程中继承得到。 当前捕获器,用trap设置。 shell参数使用变量赋值或者set命令来设置,或者是从父进程的环境中继承得到。 shell函数,在执行过程中被定义或者是从父进程的环境中继承得到。 选项在调用时激活(可以是默认的,也可以是用命令行参数给出的)或者是用set命令来设置。 用shopt激活选项。 用alias定义的shell别名。 各种PIDs,包含后台作业的进程号,$$的值,以及$PPID的值。 当一个非shell函数或内置命令的简单命令执行时,它在包含由下述内容组成的单独的执行环境中启动。除非另外说明,否则其值都是从shell中继承的。 shell打开的文件,加上对命令使用重定向修改和添加的文件。 当前工作目录。 文件创建模式掩码。 标记为导出的shell变量和函数,以及传递到环境中为这个命令导出的变量。 shell捕捉的捕获器被重置为从shell的父进程中继承的值,被shell忽略的捕获器也被忽略。 在单独的环境中启动的命令不能影响shell的执行环境。 命令替换,用圆括号分组的命令以及异步命令都在shell环境复制来的子shell环境中执行,除非shell捕捉到的捕获器s被重置为shell从其父shell启动是继承来的值。作为管道的一部分来执行的内置命令也在一个子shell环境中被执行。对子shell环境所作修改不能影响到原有shell的执行环境。 子shell生成一个执行命令替换继承的父shell的-e选项的值。如果不是posix模式,bash会在这样的子shell中清除-e选项。 如果命令后面是&并且没有启用作业控制,命令的默认标准输入将是空文件/dev/null。否则,被执行的命令继承被重定向修改的所谓shell的文件描述符。十七、环境 当一个程序被调用时,它被赋予一个称为环境字符串数组。它是一个名称-值对的列表,形式是name=value。 shell提供了多种操作环境的方法。启动时,shell扫描自身的环境,为每个找到的名字创建一个参数,自动地为导出到子进程而标记之。被执行的命令继承了这个环境。export和declare -x命令允许参数和函数被加入到环境中或从环境中删除。如果环境中参数的值被修改,新值替换了旧值成为环境的一部分。任何被执行的命令继承的环境包括shell的初始环境,其值可能在shell中被修改过,减去被unset命令删除的,加上通过export和declare -x命令添加的部分。 可以通过以参数赋值的方式设置前缀以暂时增强任何简单命令或函数的环境。这些赋值语句制造这个命令的环境中生效。 如果使用内置命令set设置了-k选项,所有的变量赋值都将在命令的环境中被替换,不仅是在命令名前面的那些。 当bash执行一个外部命令时,变量"_"被设置为命令的完整文件名,然后被传递给该环境中的命令。十八、退出状态 一个被执行的命令的退出状态是通过waitpid系统调用或相应函数的返回值。退出状态在0到255之间,但是,需要解释的是,shell可能使用125之前的数值。shell的内置命令和复合命令的退出状态也会被限制到这个范围。在某些情况下,shell会使用特殊值提示特殊的错误模式。 从shell的角度看,使用退出状态是0来退出的命令意味着成功执行。退出状态是0表明成功。非零状态值表明失败。当命令收到致命的信号N而结束时,bash使用128+N作为它的退出状态。 如果没有找到命令,为执行它而创建的子进程返回127的状态。如果找到了命令但是文件不可执行,返回状态是126。 如果命令因为一个在展开或重定向时的错误而使得执行失败,退出状态大于零。 shell内置命令如果成功返回0(true)状态,如果执行时出错则返回非零(false)状态。所有内置命令返回2的退出状态来指明用法不正确。 Bash自身返回最后执行的命令的退出状态,除非发生了语法错误,这种情况它返回非零值。十九、信号 如果bash是交互的,没有任何捕获器,它忽略SIGTERM(这样kill 0不会结束掉交互的shell),SIGINT被捕获并处理(从而使内置命令wait可以中断)。在所有情况下,bash忽略SIGQUIT。如果作业控制正在发挥作用,bash忽略SIGTTIN, SIGTTOU和SIGTSTP. 通过bash运行的非内置命令都有从其父shell继承了相应值的信号处理器。当作业控制器失效时,异步命令忽略SIGINT和SIGQUIT,除了这些继承来的处理器。以一个命令替换的结果运行的命令忽略键盘生成的作业控制信号SIGTTIN,SIGTTOU和SIGTSTP。 默认的,在收到一个SIGHUP信号时shell会退出。在退出之前,交互式shell重新发送信号SIGHUP给所有作业,不管时正在运行的还是已经停止的。已经停止的作业发送SIGCONT信号以确保他们收到了SIGHUP信号。为了防止shell向一个独立的作业发送信号,应该使用内置命令disown将它从作业表中移除或者使用disown -h标记它不能收到SIGHUP。 如果使用shopt命令设置了huponexit选项,当交互式登录的shell在退出时,bash向所有作业发送一个SIGHUP信号。如果bash正在等待命令执行结束并且收到一个设置了捕获器的信号,捕获器直到命令结束才会执行。当bash通过内置命令wait等待异步命令时,已经被设置了捕获器信号的接收,将使得内置命令wait立即以大于128的状态值返回。接着,捕获器将立即被执行。二十、作业控制 作业控制指的是可以选择停止(或挂起)进程执行,并且可以在之后继续(或恢复)进程的执行。用户通常通过交互式接口由操作系统内核终端驱动器和bash联合提供的这种功能。 shell将每个管道关联到一个作业。它保存一个当前正在执行的可以用jobs命令来列出的作业表。当bash异步地启动一个的作业时(后台执行),它输出这样的一行: [1] 25647 这代表这个作业的作业号是1,与作业关联的管道中最后一个进程的进程ID是25647。单一管道中的所有进程都是同一作业的成员。 Bash使用作业概念作为作业控制的基础。 为了简化用户接口的实现而进行作业控制,操作系统维护着一个"当前终端的进程组标识"的概念。这个进程组的成员(进程组ID等于当前终端进程组ID的进程)可以收到由键盘产生的信号,诸如SIGINT。这些进程被称为前台的进程。后台的进程是那些进程组ID与终端不同的进程;这些进程不会受到键盘产生的信号的影响的。只有前台进程可以从终端读或向终端写。后台进程试图读/写终端时,将收到由内核终端驱动程序发送的SIGTTIN(SIGTTOU)信号。这个信号如果没有加以捕捉,将挂起这个进程。 如果有bash在运行的操作系统支持作业控制,bash会包含使用它的设施。在一个进程正在运行的时候键入suspend挂起字符(通常是 ^Z, Control-Z)将导致这个进程停止同时将控制权还给bash。输入delayed suspend字符(通常是^Y,Control-Y)将导致这个进程在试图从终端读取输入时停止并将控制权还给bash。用户接下来可以控制此作业的状态,使用bg命令使它在后台继续运行,fg命令使它在前台继续运行,或kill命令将它杀死。^Z会立即生效,并且还有其他方面的效果,导致在等待输出的过程中和键入字符之前就被放弃。 有很多方法来指代shell中的作业。字符%可以引入作业名。编号为n的作业可以用%n的形式来指代。作业也可以用启动它的名称的前缀,或者命令行中的子字符串来指代。例如,%ce指代一个暂停的ce作业。如果前缀匹配多于一个作业,bash报错。另一方面,使用%?ce,可以指代任何命令行中包含字符串ce的作业。如果子字符串匹配多于一个作业,bash报错。符号%%和%+指代shell当前作业的概念,也就是前台被暂停的最后一个作业,或者是在后台启动的作业。前一作业可以使用%-来指代。在有关作业的输出信息中(例如,jobs命令的输出),当前作业总是被标记为+,前一作业标记为-。 简单命名一个作业就可以把它放到前台:%1是"fg %1"的同义词,将作业1从后台放到前台。类似的,"%1 &"在后台恢复作业1,与"bg %1"等价。 当某个作业改变状态时,shell立即可以得知。通常,bash等待即将要输出一个提示符时,才会报告作业的状态变化,从而不会打断其他输出。如果内置命令set启用了-b选项,bash将立即报告这些变化。任何SIGCHLD信号的捕获器都会在每个子进程退出时执行。 如果在作业停止时(或者如果使用内置命令shopt的checkjobs选项开启并运行)试图退出bash,shell会显示一条警告信息,并且当checkjobs选项开启时,列表作业和他们的状态。jobs命令可以用来检测这些作业的状态。如果没用中间命令再次尝试退出,shell不会显示其他警告消息,所有停止的作业会被终止。二十一、提示符 在交互执行时,bash在准备好读入一条命令时显示主提示符PS1,当它为了完成命令需要更多信息输入的时候,会显示辅助命令提示符PS2,bash允许通过输入一些使用反斜杠逃逸的特殊字符来自定义这些提示符字符串,如下所示: \a 一个ASCII响铃字符(07) \d 日期,格式是 "星期 月份 日" (例如,"Tue May 26") \D{format} format被传递给strftime(3),结果被插入到提示字符串中;空的format将使用语言环境特定的时间格式。花括号是必需的 \e 一个ASCII 转义字符(033) \h 主机名,第一个"."之前的部分 \H 完整的主机名 \j shell当前管理的作业数量 \l shell的终端设备名的基本部分 \n 换行符 \r 回车 \s shell的名称,$0的基名部分 \t 当前时间,采用24小时制的HH:MM:SS格式 \T 当前时间,采用12小时制的HH:MM:SS格式 \@ 当前时间,采用12小时制上午/下午(am/pm)格式 \A 当前时间,采用24小时制上午/下午格式 \u 当前用户的用户名 \v bash的版本 \V bash的发行编号,版本号加补丁级别 \w 当前工作目录 \W 当前工作目录的基本部分 \! 此命令的历史编号 \# 此命令的命令编号 \$ 如果有效UID是0,就是#, 其他情况下是$ \nnn 对应八进制数nnn的字符 \\ 一个反斜杠 \[ 一个不可打印字符序列的开始,可以用于在提示符中嵌入终端控制序列 \] 一个不可打印字符序列的结束 命令编号和历史编号通常是不同的:历史编号是命令在历史列表中的位置,可以包含从历史文件中恢复的命令,而命令编号是在当前shell会话中执行的命令的序号。字符串被解码之后,它将进行展开,要经过参数展开,命令替换,算术展开和引用去除等展开操作,最后还要经过shell选项promptvars处理。