Bash Guide for Beginners is a good book.

《 Bash Guide for Beginners 》

1. Bash 与 Bash 脚本

空白字符:空格或制表符

1.1 常用的 shell 程序

UNIX 的 shell 程序可以解释用户的命令,无论是用户直接输入的,还是从 shell 脚本读取的。

Shell 脚本是 解释 型的,而不是编译型的。Shell 从脚本的每一行读取命令,并在系统中搜索这些命令。

脚本中可以使用可执行文件。

除了向内核传送命令之外,shell 的主要任务是提供一个用户环境,该环境可用 shell 配置文件来单独配置。

shell 类型

sh

即 Bourne Shell。它是最早的 shell,现在仍在使用。

它是基本的 shell,特性不多。虽不是标准的 shell,但为了 UNIX 程序的兼容性,仍存在于每个 Linux 系统中。

bash

Bourne Again shell。标准的 GNU shell,直观而又灵活。

初学者最明智的选择,同时对高级和专业用户来说也是一个强有力的工具。在 Linux 上,bash 是普通用户的标准 shell。这个 shell 因此称为 Bourne shell 的超集,一套附件和插件。

bash 与 sh 是兼容的:在 sh 中可以工作的命令,在 bash 中也能工作,反之则不然。

csh

C shell。语法类似于 C 语言,某些时候程序员会使用。

tcsh

Turbo C shell。普通 C shell 的超集,加强了的用户友好度和速度。

ksh

Korn shell。有时被有 UNIX 背景的人所赏识。

Bourne shell 的一个超集,有着对初学者来说就是一场恶梦的标准配置。

相关文件

/etc/shells 文件中保存了当前系统存在的 shell。

/etc/passwd 文件中,每个用户都有自己默认 shell 的设置。

shell 的切换

要从一个 shell 转换到另外一个,只需在当前终端输入新 shell 的文件名。

系统在 PATH 变量设置的目录中查找。

新的 shell 会启用新的提示符,因为每个 shell 都有自己的外观。

1.2 Bash 的优势

GNU shell

GNU 计划为类 UNIX 系统管理提供遵守 UNIX 标准的免费软件。

Bash 是兼容 sh 的 shell,而且从 Korn shell (ksh) 和 C shell (csh) 整合了一些有用的特性。它遵循 IEEE POSIX P1003.2/ISO 9945.2 Shell 和工具标准。提供了基于 sh 的编程和交互的功能改进;其中包括命令行编辑,无限制的历史命令,作业控制,shell 函数和别名,无大小限制的索引数组,和以 2 到 64 为基础的整数算法。Bash 可以不经修改地运行多数 sh 脚本。

和其他的 GNU 项目一样,Bash 主动开始保留、保护和促进使用、学习、拷贝、修改和再发布软件的自由。普遍认为这样的情况激发了创造力。这也是 Bash 程序可以而许多其他 shell 无法提供的额外特性的缘由。

Bash 独有的特性

Bash 启动脚本

启动脚本是指当 Bash 启动时会读取并执行的脚本。

以交互登陆 shell 调用,或者使用 ‘–login’

读取的文件:

  • /etc/profile:系统全局启动文件,通常不予修改,而是在 /etc/profile.d/ 目录中自定义所需脚本。
  • ~/.bash_profile:或 ` ~/.bash_login~/.profile`,读取第一个存在的可读取的文件
  • ~/.bash_logout:退出 bash 时运行的脚本

如果配置文件存在但无法读取,将会显示错误消息。一个文件不存在,Bash 会查找下一个。

以交互非登陆 shell 调用

读取的文件:

  • ~/.bash_profile

此文件通常指向 :~/.bashrc

$ cat .bash_profile

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH
非交互调用

所有的脚本都使用非交互 shell。这些脚本通常仅仅是为了完成特定的任务。

读取的文件:

  • 由变量 BASH_ENV 定义

PATH 无法查找脚本文件,所以运行脚本时,最好使用完整的路径及文件名。

以 sh 命令调用

Bash 会在尽量尝试与 sh 相似的行为的同时,也遵循 POSIX 标准。

读取的文件:

/etc/profile

~/.profile

当以交互方式调用时,环境变量 ENV 能指向额外的启动信息。

POSIX 模式

本选项在使用内建的命令 set 时会启用:

set -o posix

或用 --posix 选项来调用 bash 时也会启用 POSIX 模式。

Bash 会尽可能遵循 POSIX 的 shell 标准。 设置 POSIXLY_CORRECT 变量可以达到目的。

读取的文件:

由变量 ENV 定义。

远程调用

以 rshd 调用时读取的文件:

~/.bashrc

【 不要使用 r 系列工具 】使用 rlogin, telnet, rsh 和 rcp 这类工具存在一定的危险。由于他们在网络上传输数据是 未经加密 的,所以他们本质上是不安全的。如果你需要远程执行和文件传输之类的工具,推荐使用 SSH。

UID 不等于 EUID 时调用

这种情况不会读取任何启动脚本。

交互式 shell

交互式 shell 通常可从终端读取用户的输入,也可以把输出写到终端:输入和输出都与终端相连。

如果 bash 命令不带任何选项地启动,就会启动 Bash 的交互行为。

判断方法

如何判断当前 shell 是不是交互式的:

$ echo $-
himBH

只要返回的结果中包含 i 即代表是交互式的 shell。

另外,非交互式 shell 不需要命令提示符,所以变量 PS1 未设置。

交互式 shell 的行为特征
  • bash 会读取启动脚本
  • 默认启用作业控制
  • 默认设置命令提示符。同时会启用 PS1PS2 变量,PS2 为多行命令的提示符,当用户命令不完整时,回车后也会出现。
  • 默认使用 readline 命令从命令行读取命令
  • 读取命令时,bash 如果收到 EOF,它会先检查 ignoreeof 选项的值,如果它没有设置才会退出
  • 默认会启用命令历史和历史扩展,shell 退出时会把命令历史保存到 HISTFILE 变量所代表的文件中,默认为 ~/.bash_history
  • 默认启用别名扩展
  • 如果没有设置 trap,SIGTERM 信号会被忽略
  • 如果没有设置 trap,SIGINT 信号会被捕捉并处理。^C 会中断某些内建的命令,但不会导致交互 shell 的退出
  • 如果启用了 huponexit 选项,则退出 shell 时会向所有作业发送 SIGHUP 信号
  • bash 读取到命令会立即执行
  • bash 会定期检查邮件
  • bash 可以配置成遇到未引用的变量就退出,该行为在交互模式下是禁用的
  • shell 内建命令遇到重定向错误时,不会导致 shell 的退出
  • 特殊的内建命令在 POSIX 模式返回错误时,不会导致 shell 的退出
  • 如果 exec 执行失败,不会导致 shell 的退出
  • 解析到语法错误,不会导致 shell 的退出
  • 默认对内建命令 cd 的参数开启简单的拼写检查
  • 默认情况下,超过 TMOUT 变量指定的时间,bash 会自动退出,即超时
条件表达式

可以在 [[ ]]test[ ] 中使用条件表达式。

表达式可以是 一元二元 的。

一元条件表达式 经常用来检验文件的状态:

只需要一个对象,如一个文件,就能执行操作。

二元条件表达式 需要两个对象来执行操作:

shell 运算

shell 可以对算术表达式求值,使用 shell 扩展或内建命令 let 来完成。

赋值时需用等宽整数,不会进行溢出检查,除以 0 会被捕获,并标记为错误。

别名

可以用一个简单的字符串来代替另一堆相对复杂的字符串,通过一张别名列表来管理,相关命令为 aliasunalias

bash 需要把 一行命令读取完毕,才会开始执行其中的命令。而别名的扩展是发生在 读取命令时,而非执行命令时。因此,要想让别名生效,至少要从下一行起。

alias l='ls -l'; l
bash: l: command not found...
# 别名未生效

alias l='ls -l'
l
total 2
drwxrwxr-x. 5 neo neo  36 May 17 16:41 aa
drwxrwxr-x. 5 neo neo  36 May 17 16:41 bb
# 读取完一整行之后,别名生效

对于函数来说,因为函数相当于 一个 复合的命令,因此同样地,需要等到整个函数的定义全部读取完毕,别名才被扩展。

数组

bash 支持一维数组变量。

任何变量都可以用在数组中。

delcare 内建命令来显示声明数组。

数组没有大小的限制,成员无需索引,无需连续赋值。

目录栈

目录栈用于保存最近访问的目录的列表。

pushd dir1 内建命令用于把指定目录加入列表

popd dir2 内建命令用于把指定目录移出列表

dirs 命令可查看列表内容

命令提示符

交互式 bash 使用提示符,可以自定义。

bash 受限模式

如果调用 rbash,或调用 bash 时使用 --restricted 选项,会发生以下事情:

  • 内建命令 cd 被禁用
  • 无法修改 SHELLPATHENVBASH_ENV 这些变量的值
  • 命令中禁止包含斜线
  • source 命令(.)后面的文件名禁止包含斜线
  • hash -p 内建命令不接受斜线
  • 禁止在启动时导入函数
  • 启动时会忽略 SHELLOPTS 变量
  • 禁止使用 >>|><>&&>>> 进行输出重定向
  • 禁用内建命令 exec
  • 内建命令 enalbe-f-d 选项被禁用
  • 受限模式无法关闭

1.3 命令的执行

Bash 会判断要执行的程序的类型。

普通程序为编译好的系统命令,运行时会产生新的进程,因为 bash 生成了一个自己的副本。该子进程拥有相同的环境,只不过 PID 不同。该过程称为 forking。

forking 之后,子进程的地址空间被新的数据覆盖,是通过 exec 系统调用实现的。

fork-and-exec 机制把旧命令转化成新命令,而新程序的环境仍然与原来的相同,包括输入输出设备的配置、环境变量和优先级。这种机制用来创建所有的进程。

shell 内建命令

内建命令包含于 shell 自身。如果内建程序的名称在一个简单命令中处于最前面,shell 会直接执行该命令,无需生成新的进程。

bash 支持三种内建命令:

sh 内建命令

:

.

break

cd

continue

eval

exec

exit

export

getopts

hash

pwd

readonly

return

set

shift

test

[

times

trap

umask

unset

bash 内建命令

alias

bind

builtin

command

declare

echo

enable

help

let

local

logout

printf

read

shopt

type

typeset

ulimit

unalias

特殊内建命令

bash 以 POSIX 模式运行时,特殊内建命令主要表现为以下三方面的不同:

  • 命令查找期间,特殊内建命令会先于 shell 函数被找到
  • 如果特殊内建命令返回一个错误状态码,非交互式 shell 会退出
  • 命令完成后,在命令之前执行的赋值语句在 shell 环境中仍然有效

特殊内建命令:

:

.

break

continue

eval

exec

exit

export

readonly

return

set

shift

trap

unset

从脚本中执行程序

如果被执行的程序是一个脚本,bash 会用 fork 创建一个新的 bash 进程,该子 shell 会逐行读取脚本,加以解释、执行,和直接从键盘上输入命令的效果是一样的。

在子 shell 逐行处理脚本时,父进程会等着它,处理完脚本所有行,子 shell 就终止了,父 shell 苏醒,并显示提示符。

1.4 Shell 的标准组件

Bash 是 GNU shell,兼容 sh,并从其他 shell 中吸取了许多有用的功能。

shell 启动时会读取其配置文件。最重要的几个:

/etc/profile

~/.bash_profile

~/.bashrc

Bash 在交互模式下的行为有所不同, 具有 POSIX 兼容模式和受限模式。

shell 命令分为:shell 的函数、shell 内建命令以及系统中某目录中的命令。

Shell 脚本由这些命令组成,并按 shell 的句法规定排列。

脚本是按行读取和执行的,应该有一个逻辑结构。

Shell 句法

如果输入没有被注释掉,shell 会把读取的输入 分割 成文字和操作符,用 引用规则 来定义每个字符的含义。这些文字和操作符于是被 转化为命令 和其它成分,命令会返回一个退出状态。

只有在 shell 解析完输入之后,该 fork-and-exec 机制才会起作用。解析过程如下:

  • shell 从文件、字符串或终端 读取输入

  • 输入按引用规则被 拆分成 文字和操作符,这些 记号(token)用元字符(metacharacter)来分隔,同时进行了别名扩展。

  • shell 把这些记号 解析成 简单或复合的 命令

  • Bash 进行多种 shell 扩展,把扩展后的记号转换成文件名列表、命令和参数。

  • 需要时进行 重定向,把重定向操作符和被重定向的对象从参数列表中移除。

  • 执行命令。

  • 需要时,shell 会等待命令的完成,并收集其退出状态。

shell 命令

简单的 shell 命令由 命令参数 组成,由 空格 分隔。

再复杂的命令也是由多个简单的命令以某种方式组合在一起的,如管道、循环、条件结构等。

shell 函数

使用 shell 函数,可以把多个命令组合在一起,便于稍后用一个名字来执行。函数执行起来就像一个普通的命令,如果把函数名作为命令来运行,与该函数名关联的命令列表就会被执行。

shell 函数是在当前 shell 执行的,不会产生新进程

shell 参数

参数是可以保存值的实体。它可以是名字、数字或特殊值。

对于 shell 来说,变量是个参数,用于保存名字。一个变量可以有值,以及零或多个属性。变量可通过内建命令 declare 创建。

如果没有给变量赋值,会给变量分配一个 空字符串

变量只能通过内建命令 unset 来删除。

shell 扩展

每个命令行被分割成记号以后,会进行 shell 扩展:

  • 括号扩展
  • ~ 扩展
  • 参数和变量扩展
  • 命令替换
  • 算术扩展
  • 单词分割
  • 文件名扩展

重定向

在执行命令之前,借助一个特殊符号,其输入和输出可以被重定向。

重定向也可用于在当前执行环境中打开或关闭文件。

命令的执行

执行命令时,解析器标记为 变量赋值(命令名前面)的单词和 重定向 将被 保存 以供后面引用,不属于变量赋值或重定向功能的词将被 扩展;扩展后的第一个剩余单词被视为 命令 的名称,其余的是该命令的 参数。然后执行 重定向 功能, 将分配给变量的字符串扩展开。如果没有找到命令名,变量会在当前 shell 环境继续生效。

shell 众多任务中最重要的,就是查找命令。以下为查找的流程:

  • 检查命令中是否包含斜线。如果没有,先从函数列表里面找。
  • 如果命令不是函数,再从内建命令中找。
  • 如果都不是,从 PATH 变量值中定义的各个路径中找。bash 使用哈希表(内存中的数据存储区块)来保存可执行文件的完整路径,可以避免粗放的 PATH 查找。
  • 如是还没找到,bash 打印错误消息,返回退出状态码 127。
  • 如果找到了,或如果命令包含斜线,shell 会在单独的执行环境中执行该命令。
  • 如果由于文件非可执行而执行失败,且文件不是目录,则假定其是 shell 脚本。
  • 如果命令没有异步地启动,shell 会等待命令执行完毕,然后收集其退出状态。

shell 脚本

调用 bash 时,如果脚本被用作第一个非选项参数(不带 -c 或者 -s,这两个参数会创建非交互 shell),该 shell 首先会查找脚本文件当前目录,如果没找到,则对环境变量 PATH 进行查找。

1.5 开发好脚本

好脚本的特质

  • 运行无错误
  • 能完成预期的任务
  • 程序的逻辑清晰、明确
  • 不做不必要的工作
  • 可被重用

脚本的结构

脚本的结构非常灵活。即使在 bash 中有很大的自由度可以发挥,仍然要确保正确的逻辑、流控制和高效,以便用户可以轻松正确地执行脚本。

开始着手编写脚本时,问自己几个问题:

  • 我需要从用户或用户环境中获取任何信息吗?
  • 怎么保存这些信息?
  • 要创建文件吗?保存在哪?权限?所有人?
  • 要用什么命令?如果脚本在其它系统运行,上面有这些命令的所需版本吗?
  • 用户要接收通知消息吗?什么时候发送?为什么发送?

术语

常用编程术语:

术语 说明
命令控制 测试命令的退出状态,以判断这部分代码是否应该执行
条件分支 代码中的逻辑点,由条件决定下一步该发生什么
逻辑流程 程序的总体设计。确定任务的逻辑顺序,以确保得到成功且可控的结果
循环 这部分代码会被执行 0 或多次
用户输入 程序运行时,由外部源提供的信息,需要时可保存或回调

关于顺序与逻辑

为了加速开发的进程,程序的 逻辑顺序 应提前考虑充分。这是开发脚本的第一步。

有多种方法可供使用,最常用的就是使用列表。逐项列出程序中涉及的 任务列表 可帮助描述每段流程,可以用项目编号来引用单个的任务。

用口语化的语言来 标记 程序中的各项任务,有助于建立便于理解的 程序框架。之后,就可以用 shell 的语言和结构来替换。

下面的例子示范了这种逻辑流程的设计。描述了日志文件的轮替。

设计一个重复的循环,用轮替的日志文件数量来控制:

  1. 你想轮替日志吗? a. 如果是: i. 输入要轮替日志所在的目录 ii. 输入日志文件基准文件名 iii. 输入日志需要保存的天数 iv. 修改用户的 crontab 文件 b. 如果不是,跳到第 3 步
  2. 你要轮替另一组日志吗? a. 如果是:重复第 1 步 b. 如果不是:跳到第 3 步
  3. 退出

用户要提供信息,才能让程序做点什么。必须要获取用户的输入并且保存下来。有必要提醒用户他的 crontab 文件将会被修改。

2. 脚本的编写与调试

2.1 创建脚本并运行

编写与命名

shell 脚本是一个重复使用的命令序列,通常在命令行中输入脚本的名称来执行。或者,也可以借助 cron 让脚本实现自动化的任务。脚本的另一个用途是在系统启动和关机过程中,在初始化脚本中可以定义守护进程和服务的操作。

要创建 shell 脚本,请在编辑器中打开一个新的空文件,放里面放几个命令,然后再起个一看就明白的名字,但不要与现有命令名冲突。为了方便,脚本文件通常加后缀 .sh。即使这样,也有可能同一系统中不同位置存在同名的脚本,可用以下命令帮助确认:

which -a script_name
whereis script_name
locate script_name

执行脚本

脚本的所有人要想运行,必须给脚本赋予可执行权限。

一般情况下,用 ./script_name 的方式来运行脚本。如果当前路径被加入了 PATH 变量,则可以直接用脚本名 script_name 来运行。

可以把脚本做为参数来运行 bash

bash -x script_name.sh

但通常我们只在需要获取特殊行为时才会执行此操作,例如检查脚本是否可以与另一个 shell 一起工作,或打印用于调试的追踪。指定的 shell 将启动一个当前 shell 的子 shell 来执行脚本。若想让脚本以特定选项启动,或想在特定条件下来启动时,可以用这种方法。

如果不希望启动一个子 shell 执行脚本,可以用 source 来运行:

source script_name.sh

source 是内建命令,与 . 的作用相同。. 最早是 sh 命令。此时脚本不需要可执行权限,命令是在当前 shell 执行的,因此,由此对环境变量造成的任何改变,随后都可以看到。

$ source script1.sh
--output ommited--

$ echo $VALUE
9

2.2 脚本基础

2.2.1 用哪个 shell 来运行脚本

在子 shell 运行脚本时,需要指明用哪个 shell 来运行。你用来编写脚本的 shell 类型有可能不是当前系统的默认 shell,因此如果用错误的 shell 执行时,你输入的命令可能会造成错误。

脚本的第一行确定要启动的 shell。第一行的前两个字符应该是 #!,然后就是用来解释后面命令的 shell 的路径。空白行也被视为行,所以不要用空行启动脚本。

#!/bin/bash 将会使用在 /bin 目录中找到的 bash 可执行文件来执行脚本。

2.2.2 添加注释

应该清楚一点,你不会是唯一一个会阅读你写的代码的人,许多用户和系统管理员都会运行别人写的代码。清楚明白的注释会帮助别人了解你是如何编写脚本的。

注释也会让自己更轻松一些。比如说,为了实现一个特定的结果,你要在脚本中使用某些命令,因此不得不阅读大量的帮助文档。过了几周或几个月,你可能就不记得这些命令是如何工作的了,除非加了充足的注释。

在一个正式的脚本中,第一行通常注释的内容是该脚本的简介,会发生什么。之后,根据需要,为每一大块命令添加必要的注释。

2.3 调试 bash 脚本

2.3.1 调试整个脚本

如果脚本没有按预期的工作,就要判断是什么导致了脚本的失败。

bash 提供了调试功能,最常用的是用 -x 选项来启动子 shell,会以调试模式来运行整个脚本。在每条命令被扩展后、被执行之前,对该命令和参数的追踪都会打印到标准输出。

2.3.2 调试部分脚本

要想对脚本的局部代码进行调试,可以使用内建命令 set,以普通模式运行,来查看局部代码的调试信息。

简写 长形 作用
set -f set -o noglob 禁止使用元字符(通配)来扩展文件名
set -v set -o verbose 在 shell 读取输入时,同时显示出来
set -x set -o xtrace 执行命令之前,显示命令的追踪

用减号 - 来激活调试,用加号 + 停止调试。

set 包围局部代码

例如,我们不确定某个脚本文件中 w 命令是如何运行的,则可以在代码中把 w 命令用 set 包围起来:

set -x		# 从此处激活调试
w
set +x		# 从此处停止调试

输出将是这样的:

$ script1.sh
The script starts now.
Hi, willy!

I will now fetch you a list of connected users:

+ w
  5:00pm  up 18 days,  7:00,  4 users,  load average: 0.79, 0.39, 0.33
USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU  WHAT
root     tty2     -                Sat 2pm  5:47m  0.24s  0.05s  -bash
willy    :0       -                Sat 2pm   ?     0.00s   ?     -
+ set +x

你可以在脚本中随意地启用、停止调用。

在命令行中运行 set
$ set -v
$ ls
ls
aaaaaaaaa.sh   bbb.sh

$ set +v
set +v

$ ls *
aaaaaaaaa.sh   bbb.sh

$ set -f

$ ls *
ls: *: No such file or directory

$ touch *

$ ls
*   commented-scripts.sh    script1.sh

$ rm *

$ ls
commented-scripts.sh    script1.sh
在脚本开头添加选项

可以把要使用的选项直接添加在 #! 行后面:

#!/bin/bash -xv
找到问题代码之后

一旦找到了脚本中出问题的部分代码,可以在每条不太确定的命令之前用 echo 加上调试声明,这样会看的更清楚,哪儿不正常,为什么了。

echo "debug message: now attempting to start w command"; w

在更高级的脚本中,可以插入 echo 来显示不同阶段同一变量的内容,以便检测出错误:

echo "Variable VARNAME is now set to $VARNAME."

3. bash 环境

3.1 shell 初始化文件

3.1.1 系统范围的配置文件

/etc/profile

如果 bash 是使用 --login 选项被调用的,或是做为 sh 被调用的,它会读取 /etc/profile 脚本,其中通常会设置变量 PATHUSERMAILHOSTNAMEHISTSIZE

在某些系统中,umask 的值是在 /etc/profile 中设定的,其它系统中是在该文件中保有指向其它配置文件的指针,如:

  • /etc/inputrc,系统范围的 Readline 初始化文件,用于设定命令行提示音
  • /etc/profile.d 目录,其中的文件用于配置特定程序系统的范围的行为

如果希望设定的用户环境能 影响到所有用户,就应该在 /etc/profile 中设定。

该文件会设定一些基础的 shell 环境变量,以及用户在网页浏览器中运行 Java 或 Java 程序时所需要的一些变量。

/etc/bashrc

如果系统支持多种 shell,建议使用该文件来进行针对 bash 的配置,因为 /etc/profile 文件也会被其他 shell 所读取,比如 Bourne shell。如果其他 shell 不理解 bash 的语法就会产生错误,所以建议把针对不同类型 shell 的配置文件分割开。这种情况下,用户的 ~/.bashrc 有可能指向 /etc/bashrc,目的是为了在登陆 shell 初始化期间将 /etc/bashrc 脚本包含进来。

/etc/profile 仅仅维护 shell 的环境变量和程序的自动启动设置,而 /etc/bashrc 包含了系统范围的对 shell 函数别名 的定义。

/etc/bashrc 可能会在 /etc/profile 或单用户初始文件中 被引用

3.1.2 单用户配置文件

这些单用户配置文件有可能默认不存在,需要时可以创建。

~/.bash_profile

要想单独为特定用户配置 shell 环境,最好就用 ~/.bash_profile 文件 。

source ~/.bashrc
source ~/.bash_login
......
~/.bash_login

通常只有在需要登陆系统的时候才会执行该文件。如果 shell 找不到 ~/.bash_profile,就会读取该文件。

~/.profile

如果找不到 ~/.bash_profile~/.bash_login,就会读取 ~/.profile。该文件可以保存相同的配置,也可以被其它 shell 访问。

~/.bashrc

现在经常会用到非登陆 shell,比如在图形界面里,用 X 终端登陆时。打开这个窗口时,用户无需输入用户名和密码,也不会进行认证。

~/.bash_logout

退出时会执行该文件。

3.1.3 修改 shell 配置文件

如果以上文件发生了修改,用户要么需要重新连接到系统,要么通过 source script_name 来使其生效。对文件的修改会应用到当前 shell 会话。

许多 shell 脚本会在私有环境下执行:变量只有从父进程导出,才会被子进程所继承。因此,source 一个脚本不仅会应用其对环境的修改,也会应用其对变量的设置。

如果需要的话,为了便于了解哪些设置来自于哪个文件,可以在配置文件中用 echo 添加一些声明文字,如:

echo "Now executing .bash_profile.."

或:

echo "Now setting PS1 in .bashrc:"
export PS1="[some value]"
echo "PS1 is now set to $PS1"

这样,当某个配置文件被应用时,你随时都会了解到它做了些什么。

3.2 变量

3.2.1 变量的种类

按照习惯,shell 变量通常使用大写字母来表示。bash 会保留变量的列表,其中有两类变量:

全局变量

全局变量或环境变量在所有 shell 中可见。可以用 envprintenv 命令来查看环境变量。

局部变量

局部变量只在当前 shell 可见。使用内建命令 set 不带选项运行,可以查看所有变量和函数的列表。输出会根据当前语系进行排序,以可重用的格式来显示。

按变量内容区分

如果按变量内容来区分变量:

  • 字符变量
  • 整数变量
  • 常量
  • 数组变量

3.2.2 创建变量

变量是区分大小写的,默认都用大写。有时候习惯性用小写字母来命名局部变量。然而,你可以随意使用大小写来命名变量。变量名可以包含数字,但不能在首位。如 number1 是合法的名字,但 1number 就不是。

要在 shell 中设置一个变量,使用如下格式:

VARNAME="value"

等号两边与变量和值之间都不能有空格,否则会出错。为变量赋值时,建议养成 引用 内容字符串的好习惯。这会减小出错的机率。

unset 删除变量之后,该变量就不再可用。

$ unset MYVAR1
$ echo $MYVAR1

3.2.3 导出变量

创建变量之后只是在当前 shell 可见,是局部变量,当前 shell 的子进程看不到。要想把变量传递给子 shell,需要使用 export 命令来将其导出,被导出之后,变量就会当作环境变量来看待了。

export VARNAME="value"

子 shell 可以修改其继承的变量,但所作的修改不会影响父进程。

3.2.4 保留变量

Bourne shell 的保留变量

bash 有一些变量是与 Bourne shell 相同的,某些情况下,bash 会给这些变量赋予默认值。

变量 用途
CDPATH 用冒号分隔的目录列表,用于为 cd 查找路径
HOME 当前用户的家目录,cd 的默认参数,该变量的值也用于 ~ 扩展
IFS 一个字符列表,这些字符用于分隔字段,在扩展中用于分割单词
MAIL 如果该变量用作一个文件名的参数,而且 MAILPATH 变量没有设置,bash 会检查该文件,如果其中有新的邮件就提醒用户
MAILPATH 用冒号分隔的文件列表,shell 会定期检查新邮件
OPTARG getopts 命令处理的、最近的选项参数的值
OPTIND getopts 命令处理的、最近的选项参数的索引号
PATH 用冒号分隔的目录列表,shell 用于查找命令
PS1 主要的命令提示符,默认值为 \s-\v\$
PS2 次要的命令提示符,默认值为 >,用于多行
Bash 的保留变量

以下变量为 bash 专有的变量,但对于其它 shell 就是普通的变量名,不会特殊对待它们。

变量 用途
auto_resume 控制 shell 如何与用户交互,作业控制
BASH 用于执行当前 Bash 实例的全路径
BASH_ENV 调用 bash 执行脚本时,如果该变量已设置,执行脚本前,会将变量值扩展,作为启动文件来读取
BASH_VERSION 当前 bash 实例的版本号
BASH_VERSINFO 只读数组变量,其成员保存该 bash 实例的版本信息
COLUMNS 打印列表时,内建命令 select 使用该变量来判断终端的宽度,接到 SIGWINCH 信号时会自动设置
COMP_CWORD 当前光标下输入的单词位于 COMP_WORDS 数组中的索引
COMP_LINE 当前命令行中输入的完整命令
COMP_POINT 相对于当前命令起始处的当前光标位置
COMP_WORDS 数组变量,保存当前命令行输入所有的单词
COMPREPLY 用于生成补全列表的数组
DIRSTACK 数组变量,保存目录栈的内容
EUID 当前用户的有效 UID
FCEDIT 当内建命令 fc 使用 -e 选项时,其使用的默认编辑器
FIGNORE 指定多个文件扩展名,用冒号分隔,自动补全时会忽略这些扩展名
FUNCNAME 数组变量,包含了整个调用链上所有函数的名字。${FUNCNAME[0]} 代表 shell 脚本当前正在执行的函数的名字,而变量 ${FUNCNAME[1]} 则代表调用函数 ${FUNCNAME[0]} 的函数的名字,依此类推
GLOBIGNORE 设置要忽略的模式匹配文件,多个模式用冒号分隔,文件名扩展时会忽略匹配这些模式的文件
GROUPS 数组变量,包含当前用户的所有的组
histchars 用于控制历史记录展开、快速替换和标记化的字符,最多有 3 个字符
HISTCMD 当前命令执行完后,它在历史命令中的排列编号
HISTCONTROL 用于指定是否要把某个命令加入历史列表中
HISTFILE 保存历史命令的文件名,默认为 ~/.bash_history
HISTFILESIZE 历史命令文件可以保存的最大行数,默认为 500 行
HISTIGNORE 指定哪些命令保存到历史文件中,哪些忽略
HISTSIZE 历史列表中可保存的命令的数量,默认为 500 个
HOSTFILE /etc/hosts
HOSTNAME 指定主机名要保存在哪个文件中,如 /etc/hosts。shell 需要时会读取。
HOSTTYPE 用于描述运行 bash 的当前主机硬件平台
IGNOREEOF 如果整个输入是一个 EOF 字符,shell 应该如何对待
INPUTRC Readlin 初始化文件的名称,用来覆盖默认值 /etc/inputrc
LANG 排除那些以 LC_ 开头的变量所设置的语系,该变量用于设置其余类别的语系
LC_ALL 设置语系,用于覆盖 LANGLC_ 开头的所有变量
LC_COLLATE 文件名扩展之后,用该变量指定的语系来排序,并确定了字符整理和字符串整理的规则,这些规则控制着范围、同等类以及多字符整理元素的行为。
LC_CTYPE 指定用于 LC_CTYPE 类别信息的语言环境。LC_CTYPE 类别确定了字符处理的规则,这些规则控制着文本数据字符(即单字节和多字节字符)的字节序列的解释、字符的分类(如,字母、数字等)以及字符类的行为。
LC_MESSAGES 指定用于 LC_MESSAGES 类别信息的语言环境。LC_MESSAGES 类别确定了控制肯定和否定响应的规则,以及控制用于消息和菜单的语言环境(语言)的规则。
LC_NUMERIC 指定用于 LC_NUMERIC 类别信息的语言环境。LC_NUMERIC 类别确定了控制非货币数字格式的规则。
LINENO 脚本中当前执行的行号
LINES select 命令用该变量来决定打印时的宽度
MACHTYPE 记录系统的硬件结构,格式为标准的 GNU 格式:CPU-COMPANY-SYSTEM
MAILCHECK MAILPATHMAIL 变量所指定的邮件文件,该变量指定多少秒检查一次邮件文件
OLDPWD cd 命令设置的上一个工作目录
OPTERR 如果设为 1,bash 会显示 getopt() 函数产生的错误
OSTYPE 当前操作系统类型
PIPESTATUS 数组变量,保存最近在前台执行的管道进程的退出状态码的列表
POSIXLY_CORRECT bash 启动时,如果该变量在环境变量中,shell 会进入 POSIX 模式
PPID shell 的父进程的 ID
PROMPT_COMMAND 如果设置该变量,变量值会被解释为一个命令,在每次打印主要提示符 PS1 之前执行
PS3 变量值用于作为 select 命令的提示符,默认为 #?
PS4 在 bash 的调试模式中,每行命令都会被回显,该变量的值可以在每条回显的最前面显示
PWD cd 命令设置的当前工作目录
RANDOM 该变量每次做为参数被引用时,都会随机产生一个 0 ~ 32767 的整数。为变量赋值可以为随机生成器设定种子
REPLY read 命令的默认值
SECONDS 该变量会扩展为 shell 到现在运行的秒数
SHELLOPTS 所有已启用的 shell 选项,用冒号分隔
SHLVL 启动一个新的 bash 实例,该变量就会加 1
TIMEFORMAT 此参数的值用作格式字符串, 用于指定应如何显示带有 time 的管道的计时信息
TMOUT 如果设置的值大于 0,TMOUT 会做为 read 命令超时的默认值。在交互式 shell 中,出现提示符以后,变量值代表等待输入的秒数,如果超过这个时间还没有输入,bash 就会终止
UID 当前用户的真实 UID

3.2.5 特殊参数

有几个参数 shell 会特殊对待,这些参数只可以被引用,不可以赋值。

参数 用途
$* 扩展为位置参数,从 1 开始。如果扩展发生在双引号中,该变量会被扩展成一个词,该词由若干个参数组成,参数之间用 IFS 的第一个字符分隔。
$@ 扩展为位置参数,从 1 开始。如果扩展发生在双引号中,其中的每一个参数都会被单独扩展成为一个词。
$# 扩展成十进制的位置参数
$? 扩展成为最近在前台执行的管道的退出状态
$- 用于保存当前 shell 所使用的所有选项,这些选项都是由 set 命令设置的
$$ 扩展为 shell 的进程 ID
$! 扩展为最近后台执行的进程的 ID
$0 扩展为 shell 的名称或 shell 脚本的名称
$_ 保存前一个命令最后一个参数的变量值
$* vs $@

现实使用中 $* 经常会带来问题,因此经常使用 $@

位置参数

位置参数是指脚本名后面跟的词,它们会放到 $1$2$3 等变量中。需要时,可以把变量加入一个内部数组。

$# 保存了参数的总个数。

3.2.6 变量让脚本更易于重用

为了让得到脚本的他人能更快速地修改、使用,建议尽可能地用变量代替常量。

3.3 对字符的引用

3.3.1 为什么要引用字符

引用是为了清除字符或词的特殊含义。

引用可以 禁用 shell 对某些特殊字符的 特殊对待,可以让 shell 忽略其保留的词,从而对它们 禁用参数的扩展

3.3.2 转义符

转义符用于移除 单个字符 的特殊含义。没被引用的 \ 在 bash 中是一个转义符。它会保持其后面这个字符的原始值,换行符除外。如果 \ 后面紧跟着换行符,代表下一行的内容与本行是连续的,输入流中的这个 \ 会被移除并忽略,从而 \ 在此起了连接两行的作用。

3.3.3 单引号

' ' 单引号用于保持被引用内容中 所有字符 的原始值。

单引号不能嵌套使用,哪怕加了转义符也不可以。

3.3.4 双引号

" " 双引号用于保持被引用内容中 大部分 字符的原始值,除了 $` `\

$` ` 在双引号中保持其原义,即变量替换和命令替换。

\ 只有在 $` `" "\换行符 前面时才会保持原义。在双引号中,如果 \ 后面跟的是这些符号之一,\ 会从输入流中被删除。

双引号可以嵌套使用,但被引用的双引号需被转义。

echo "I'd say: \"Go for it!\""
I'd say: "Go for it!"

ANSI-C 引用

bash 还有一种引用的机制:在字符中使用类 ANSI-C 的转义序列,语法为:

$'string'

string 中如果含有以下转义序列,会被转义:

转义码 含义
\" 双引号
\' 单引号
\\ 反斜线
\a 终端警告字符(提示音)
\b 回退
\e 转义
\f 馈页
\n 换行
\r 光标到行首
\t 水平制表符
\v 垂直制表符
\cx 一个 ctrl-x 字符,如 $'\cZ' 会输出 ctrl-z 的控制序列

3.3.6 语系

$"string"

这种引用称为 I18N。

如果对该字符串存在可用的翻译,会用其译文来代替原有的文字。如果没有,或如果语系设置为 CPOSIX,该 $ 符号会被忽略,结果就变成普通的双引号引用字符串。

如果字符串被译文所替换,则结果是双引号引用。

3.4 shell 扩展

简介

命令被拆分成记号以后,这些记号或词汇会被扩展或解析。共可进行 8 类扩展,在所有扩展完成之后,会进行引用的移除。

3.4.2 大括号扩展

大括号扩展是一种能够生成任意字符串的机制。

大括号扩展的模式是这样的:

  • 有一个可选的 前缀
  • 然后是一组 字符串 或表达式,它们包含在大括号中,用 逗号 分隔
  • 最后是一个可选的 后缀
前缀{字,符,串}后缀
前缀{表,达,式}后缀

大括号扩展允许嵌套。每个扩展字符串的结果是不会排序的,依然按照从左到右的顺序依次扩展。

$ echo sp{el,il,al}l
spell spill spall

大括号扩展是先于其他扩展进行的,其他扩展眼中的特殊字符都会被保留下来,这个过程是严格的文本性质的。对于扩展的内容和括号内的文本,bash 绝对不会应用任何的语法解释。为了避免与参数扩展造成冲突,大括号扩展不会识别字符串中的 ${

正确的形式:大括号扩展必须包含未引用的成对的大括号,以及至少一个未引用的逗号。

如果大括号扩展的形式错误,扩展之后不会产生任何变化。

3.4.3 ~ 扩展

如果词汇的开头是个未引用的波浪线 ~,所有的字符一直到第一个未引用斜线,或所有的字符(如果没有未引用斜线)被看作波浪线 前缀

~neo

前缀中如果没有字符被引用,则其中紧随 ~ 后面的字符串被看作有可能是登陆名。

如果该登陆名是空字符串,~ 会被替换成 HOME 变量的值。

如果 HOME 未设置,则用执行 shell 的用户的家目录替换,否则用指定登陆名的家目录替换。

如果登陆名无效或扩展失败,则命令不会受影响,不会变。

~+

如果前缀是 ~+,用变量 PWD 的值替换前缀。

~-

如果前缀是 ~-,用变量 OLDPWD 的值,如果预先设置的话,就进行替换。

~6

如果前缀是个数字 n,则把 ~n 替换成目录堆栈的第 n 个目录。

$PATH:~/test

给变量赋值时,如果值是以 = 开头,也会进行扩展,因此可以用 ~ 和文件名的组合来为 PATHMAILPATHCDPATH 赋值,shell 会把扩展后的值赋给变量。

export PATH=$PATH:~/test

~/test 会先扩展为 $HOME/test,如果 $HOME/home/franky,则 /home/franky/test 会加入 PATH 变量的内容中。

3.4.4 shell 参数与变量扩展

🍎 本节要特别感谢 astrotycoon 的文章!

参数的概念

在 shell 编程中,参数是个 大概念,也是个笼统的概念,它是个实体,其中存储着各式各样的值。

可以通过三类方式来引用参数,从而得到参数中存储的值:

  • 通过 名称 来引用参数,这样的参数称之为 变量

    一个变量拥有自己的值和诸多属性,属性可以通过 declare 来设定,可以通过 unset 来取消一个变量。

  • 通过 数字 来引用参数,这样的参数称之为 位置参数

    位置参数在脚本被调用时自动初始化为传递给脚本的参数。脚本中调用函数时,位置参数会临时替换成传递给函数的参数。可以用 set 命令来改变位置参数的值,但无法通过赋值语句来改变。

  • 还有一类参数,被称之为 特殊参数

    只能通过 shell 内部预定义的特殊符号来引用它们,并且只能引用,不能用赋值语句来重新赋值。预定义的特殊符号包括:* @ $ ? ! - $ 0

参数扩展

参数扩展就是通过符号 $ 获得参数中存储的值。在获得最终结果之前,要对参数及数值进行一系列的操作,如删除、截取、替换等。

参数扩展最简单的形式为:$参数${参数}

被扩展的参数名可以用大括号括起来,大括号是可选的,但 建议总是加上

  • 使用大括号时,关闭的大括号不能被转义、被引用,也不能处于算术表达式、命令替换、参数替换中。
  • 大括号中参数的值将被替换。
  • 如果参数是大于一位的位置参数,如 $15,则必须要使用大括号,即 ${$15},否则 $$15 会被认为是 $1 后面跟着一个 5。
WORD=car
echo ${WORD}s and $WORDs
cars and

所以如果需要把变量名与紧随的字符区分开,就一定要使用大括号。

常用的参数扩展方法
表达式 说明
${var} 变量 var 的值,同 $var
   
${var-$DEFAULT} 如果 var 未设置,表达式求值结果为 $DEFAULT *
${var:-$DEFAULT} 如果 var 未设置或为空,表达式求值结果为 $DEFAULT *
   
${var=$DEFAULT} 如果 var 未设置,表达式求值结果为 $DEFAULT *
${var:=$DEFAULT} 如果 var 未设置或为空,表达式求值结果为 $DEFAULT *
   
${var+$OTHER} 如果 var 已设置,表达式求值结果为 $OTHER,否则为空
${var:+$OTHER} 如果 var 已设置,表达式求值结果为 $OTHER,否则为空
   
${var?$ERR_MSG} 如果 var 未设置,打印 $ERR_MSG,退出脚本,退出状态为 1。*
${var:?$ERR_MSG} 如果 var 未设置,打印 $ERR_MSG,退出脚本,退出状态为 1。*
   
${!varprefix*} 匹配之前所有以 varprefix 开头声明的变量
${!varprefix@} 匹配之前所有以 varprefix 开头声明的变量

* 如果 var 已设置,表达式求值结果为 $var

间接扩展

间接扩展也称间接引用,是指用一个变量的值来传递另一个变量的名字。

NAME="VAR1"
VAR1=42
echo ${!NAME}
42

如果参数的第一个字符是个感叹号 !,则参数其余的字符做为变量名,但该变量扩展成变量值以后,该变量值代表另一个变量的名字。

子串扩展

${parameter:offset}

${parameter:offset:length}

从 offset 位置开始,截取长度为 length 的子字符串,如果没有提供 length,则是从 offset 开始到结尾。

  • 如果 offset 是负值,开始位置是从字符串末尾开始算起,取长度为 length 的子串。
  • 如果 length 是负值,则 length 不再代表字符串长度,而代表另一个 offset,位置从字符串末尾开始,扩展的结果是 offset ~ length 之间的子串。
  • 如果 parameter 是 @,即所有的位置参数时,offset 必须从 1 开始。

当 offset 是负值时,负号 - 与前面的冒号 : 必须用空格分开,或者把负数用括号括起来:

echo ${MYSTRING: -34:13}
echo ${MYSTRING:(-34):13}
查找并替换

${parameter/pattern/string}

${parameter//pattern/string}

${parameter/pattern}

${parameter//pattern}

匹配后的子串会用 string 替换掉。

  • parameter 之后如果是 /,则只替换匹配到的 第一个 子串;parameter 之后如果是 //,则替换 所有 匹配到的子串。
  • 当 string 为空时,则相当于将匹配的子串 删除
  • 特殊符号 #% 在这种情况下分别锚定字符串的 开始结尾
  • 如果 bash 的 nocasematch 选项是打开的,则匹配的过程大小写是不敏感的。
$ MYSTRING="Be liberal in what you accept, and conservative in what you send"
$ echo ${MYSTRING/in/by}
Be liberal by what you accept, and conservative in what you send
$ echo ${MYSTRING//in/by}
Be liberal by what you accept, and conservative by what you send
$ echo ${MYSTRING/conservative/}
Be liberal in what you accept, and in what you send

$ MYSTRING=xxxxxxxxxxxxxxx
$ echo ${MYSTRING}
xxxxxxxxxxxxxxx
$ echo ${MYSTRING/#x/y}
yxxxxxxxxxxxxxx
$ echo ${MYSTRING/%x/y}
xxxxxxxxxxxxxxy

bash 的这个查找替换功能跟 sed 的很像,不同的是这里的 pattern 不是正则表达式。

查找并删除

${parameter#pattern}

${parameter##pattern}

${parameter%pattern}

${parameter%%pattern}

删除匹配到的子串。其中操作符 #% 的作用为:

  • # 行首起,匹配第一个
  • ## 行首起,匹配所有
  • % 行尾起,匹配第一个
  • %% 行尾起,匹配所有

例如:file=/dir1/dir2/dir3/my.file.txt 定义变量

${file#*/}dir1/dir2/dir3/my.file.txt

${file##*/}my.file.txt ,相当于 basename ${file}

${file#*.}file.txt

${file##*.}txt

${file%/*}/dir1/dir2/dir3,相当于 dirname ${file}

${file%%/*} :空值

${file%.*}/dir1/dir2/dir3/my.file

${file%%.*}/dir1/dir2/dir3/my

获取参数长度

${#parameter}

返回 parameter 值的长度。

大小写转换

${parameter^} :将第一个字符转成大写

${parameter^^} :将所有字符转成大写

${parameter,} : 将第一个字符转成小写

${parameter,,} : 将所有字符转成小写

3.4.5 命令替换

可以用一个命令的输出来替换命令本身。

$(command)`command`

扩展时,bash 先执行该命令,然后用其输出来替换命令本身,末尾的换行符会被删除,中间的换行符不会删。

  • 如果使用 `command` 形式:如果 \ 后面是 $`\,会将其转义,否则就是 \ 本身。
  • 如果使用 $(command) 形式:括号里的所有字符都正常处理。
  • 命令替换可嵌套,如果用反引号形式,里面的那层反引号需要用 \ 转义。

如果命令替换出现在双引号里面,不会对结果进行词汇分割和文件名扩展。

3.4.6 算术表达式

算术表达式会对表达式进行计算,并用计算结果进行替换。

基本形式: $(( EXPRESSION ))

表达式中的所有记号都会经历参数扩展、命令替换、去除引用。

算术表达式可以嵌套。

计算表达式时会用等宽整数,不会进行溢出检查,除以 0 会被捕获,并标记为错误。

运算符 含义
VAR++ VAR-- 后缀自增、自减
++VAR --VAR 前缀自增、自减
+ - 加、减
* / % 乘、除、余数
** 求幂
! 逻辑否
&& 逻辑与
|| 逻辑或
== != 等于、不等于
< <= > >= 比较运算符
~ 按位取反
<< >> 按位左移、按位右移
& 按位与
| 按位或
^ 按位异或
expr1 ? expr2 : expr3 条件计算
= *= /= %= += -= <<= >>= &= ^= |= 赋值

shell 变量也可作为运算对象。计算前会先进行参数扩展。在表达式中,shell 变量也可以由名称引用,而不使用参数扩展语法。变量的值在引用时作为算术表达式求值。一个 shell 变量不需要开启它的整数属性就可以用于表达式。

进制表示

由 0 开头的常数被看作八进制数字。

由 0x 或 0X 开头的为十六进制。

否则就使用 [BASE'#']N 的形式:BASE 是一个 2~64 的二进制数字,代表是什么进制,N 是该进制的数字。如果 BASE'#' 被省略,则认为是十进制的数字。大于 9 的数字依次用小写字母、大写字母、@_ 来表示。如果 BASE 小于等于 36,大小写可以相互混用,来代表 10~35 的数字。

运算符按优先顺序进行计算,括号中的子表达式最先计算,可以覆盖上面的规则。

只要可能,就建议尽量使用这种形式的表达式:

$[ EXPRESSION ]

但这种形式只能计算,不能进行条件的测试。

3.4.7 进程替换

用管道将一个命令的标准输出传递给另一个命令的标准输入,这是个强大的技术。但是,如果要用管道来传递 多个命令 的标准输出该怎么做?这时候进程替换就派上用场了。进程替换可以把一个(或多个)进程的输出送到另一个进程的标准输入。

原理

进程替换是 进程间通讯 的一种形式,允许一个命令的输入或输出看上去 像个文件 一样。bash 会将 命令 在线 替换 成一个 文件名,借助这种方法,原来只能接受文件做为参数的命令,现在可以直接从另一个程序读取输入,或把输出直接写到另一个程序。

进程替换是 重定向 的一种形式,一个进程的输入或输出显示为一个 临时文件

进程替换与参数扩展、命令替换和算术扩展是 同时 进行的。

语法

格式: >(command_list)<(command_list)

<> 与括号之间没有空格,加上空格或报错。

括号中的命令执行以后,其:

  • 形式为 <( ) 的标准输出文件描述符 或
  • 形式为 >( ) 的标准输入文件描述符

被连接到一个 FIFO 文件上,或 /dev/fd/ 目录中的某个文件上。然后,文件描述符所连接的文件名被用来替换 <( )>( ) 结构。

有些程序只支持从文件获取输入,不支持从标准输入获取,因此无法从管道获取输入。借助进程替换,这些程序就可以接收来自于其它程序的数据了。

范例

这个例子最便于理解进程替换格式的含义:

$ cat <(ls)
aa
bbb
cccc

$ echo <(ls)
/dev/fd/63

<(ls) 改变了 ls 的输出定向。原本 ls 的输出是到标准输出的,但 <(ls) 把输出指向了一个临时文件,用描述符 /dev/fd/63 来表示。

文件名作为 cat 的参数时,会将文件内容打印出来,即 ls 的文件列表。

文件名作为 echo 的参数时,只会把*文件名本身打印出来。

$ cat /usr/share/dict/linux.words | wc
 479828  479828 4953680
$ wc <(cat /usr/share/dict/linux.words)
 479828  479828 4953680 /dev/fd/63
$ diff -u <(ps) <(ps -e)
diff -u .bashrc <(ssh remote cat .bashrc)
$ rsync -arv --log-file=>(grep -vF .tmp >log.txt) src/ host::dst/
$ ps -ef | tee >(awk '$1=="tom"' >toms-procs.txt) \
               >(awk '$1=="root"' >roots-procs.txt) \

3.4.8 字段分割

Word Splitting

如果 IFS 的值为空(null),就不会进行字段分割。

如果没有发生扩展,也不会进行字段分割。

在参数扩展、命令替换及算术扩展之后,shell 扫描得到的结果,找到双引号,对里面的内容进行字段分割。

shell 把 $IFS 的每一个字符都做为分隔符,在这些分隔符上进行分割,把其它扩展的结果分割成单独的字段。

  • 如果 IFS 没有设置,或其值是 <space><tab><newline> 这个默认值,则 IFS 字符的任何排列都可以用来切割字段。
  • 如果 IFS 中含有非默认值,而且 IFS 中包含空白字符(空格或制表符),则词汇开头和结尾的空白字符序列会被忽略。
  • IFS 中的非空白字符与空白字符的组合也可以用来分割。
  • IFS 空白字符的序列可以用来分割。
范例

这东西必须得有实例才能直观地理解。

x='   one , tow,      three    '
printf "<%s>\n" $x
<one>
<two>
<three>
#  IFS 没有设置,使用默认值
#+ 无论这些空白是纯空格,纯制表符,
#+ 还是空格与制表符的组合,都看作单一分隔符


IFS=','
x='  one , tow,three  '
printf "<%s>\n" $x
<  one >
< two>
<three  >
# IFS 只有逗号时,空格也被看作普通字符


IFS=' ,'
x='  one , tow,three  '
printf "<%s>\n" $x
<one>
<two>
<three>
# IFS 为空格和逗号时,空格与逗号的任何组合都被看作单一的分隔符

3.4.9 文件名扩展

set -f 会禁止 bash 使用通配符做文件名。

如果 bash 没有开启 -f 选项,词汇分割之后,它就会在字段中查找 *?[ 这些字符。如果找到,该字段即被当作文件名 匹配模板

  • 如果有匹配的文件名:文件列表会按字母排序,然后替换掉模板本身。
  • 如果没有匹配的文件名:nullglob 选项没有开启,该字段就不做任何改动;若 nullglob 选项开启,该字段会被删除。
  • 如果 nocaseglob 选项开启,匹配时会忽略大小写。
  • 如果选项 dotglob 没有开启,文件名开头的 ../ 必须显式匹配:* 不可匹配 .file,只有 .* 才能匹配;同理,只有 ./* 才能匹配 ./file
  • 在匹配文件名的过程中,/ 始终要显式匹配:* 不能匹配 /dir,只有 /* 才能匹配。
  • GLOBIGNORE 变量可以用来限制文件名如何匹配模板:如果设置了该变量,匹配的文件名中,如果同时也匹配该变量,该文件名会被移除。文件名 ... 不受该变量的限制,会被忽略。
  • 设置 GLOBIGNORE 变量相当于开启了 dotglob 选项,于是以 . 开头的文件名也会被通配符匹配:* 可以匹配 .file

3.5 别名

3.5.1 别名简介

别名就是用简短的字段来代替一个通常是更复杂的字符串。

shell 平时维护了一个 别名列表。用 aliasunalias 命令来添加和删除其中的条目。

alias 不加参数运行,可以查看该列表内容。

别名的用途
  • 同一个命令在系统中有多个版本,用别名可以简化不同版本的调用
  • 设置命令的默认选项
  • 纠正易发的错误拼写
别名的扩展
  • 在非交互式 shell 中不会发生别名扩展,除非使用 shopt 命令来启用 expand_aliases 选项。
  • 简单命令中的 第一个字段 如果 没被引用,会被检查是否是别名。如果是,则用其原始内容替换。'll' /tmp 不会被扩展。
  • 除了别名中不能包含 = 以外,别名与原始内容可以包含任何有效的 shell 输入,包括 shell 元字符。
  • 原始内容 中的第一个字段会被 再次 检查,看是不是其它别名。但同一个别名只会扩展一次:如把 ls -F 做名别名 ls,bash 会正常执行 ls -F,而不会没完没了地递归扩展下去。
  • 默认情况下,bash 只会检查一行命令中的 第一个字段 是不是别名。但是,如果第一个别名的原始内容其 最后一个字符是空白字符,则会检查紧挨着的 第二个字段 是否别名: 如:sudo ll,bash 默认只会检查 sudo 是否别名,而不会检查 ll。要想让 ll 别名被检查,可以重新给 sudo 加个别名:

      alias sudo='sudo '
    

    别名 sudo 的原始内容为 sudo ,其最后一个为空白字符,因此跟随它后面的命令也会被检查。

3.5.2 创建与删除别名

alias ll='ls -l'

unalias ll

bash 总是要把一行命令完整地读取完毕,然后才会执行其中的命令。因此,别名的扩展发生于 读取命令时,而非执行命令时。

因此,创建别名时,不要指望在同一行马上就使用:

alias ll='ls -l'; ll
bash: ll: command not found...

刚创建的别名,至少要在创建别名的这一行命令的下一行才能开始生效。

因此,要把创建别名的命令单独放在一行,不建议放到复合命令中去。

别名不会被子进程所继承。

别名扩展是在执行函数之后进行的,因此相对来说解析的稍慢一些。虽然别名更容易理解,但还是建议 尽量使用函数 而不使用别名。

3.6 bash 其它选项

3.6.1 查看选项

set -o 可以查看所有 shell 选项。

3.6.2 修改选项

shell 的这些选项,可以在调用 shell 的时候设定,也可以在 shell 运行期间修改,还可以把选项直接写到启动脚本里。

调用 shell 时设定
$ bash --posix script.sh
运行期间修改

要想临时修改当前环境,或想在脚本中修改选项,最好就是用 set 命令,- 选项 为启用,+ 选项 为禁用。