bash的配置与自定义
1、bash启动选项
你想搞清楚 bash 的各种启动选项,但是 bash --help 也没能帮上忙。
除了 bash --help,还可以试试 bash -c “help set” 和bash -c help,如果正在使用 bash shell,直接使用 helpset 和 help 就行了。
bash 有时可以采用不同的方式设置相同的选项,这就是一个例子。你可以在启动时设置某个选项(如 bash -x),随后使用 set +x 以交互形式关闭该选项。
2、自定义提示符
默认的 bash 提示符通常以 $ 结尾,但信息不够丰富,传达不出太多的东西。你希望能够自定义提示符,以展现出更多实用的信息。
根据需要自定义 $PS1 和 $PS2 变量。
默认的提示符随系统而异。bash 本身会显示其主版本号和次版本号(\s-\v\$),例如,bash-3.00$。不过,你的操作系统可能有自己的默认提示符,例如,有些版本的Fedora 使用的是 [user@host~]$ ([\u@\h\W]\$)。这里我们给出了 8 种基本提示符和 3 种花哨的提示符。
-
基本提示符:以下这 8 种更实用的提示符适用于 bash 1.14.7 及以上版本。如果有效 UID 为 0(root 用户),结尾的 $显示为 #;否则显示为 $。
用户名 @ 主机名、日期和时间,以及当前工作目录。$ export PS1='[\u@\h \d \A] \w \$ ' [jp@freebsd Wed Dec 28 19:32] ~ $ cd /usr/local/bin/ [jp@freebsd Wed Dec 28 19:32] /usr/local/bin $用户名 @ 长格式主机名、ISO 8601 格式的日期和时间,以及当前工作目录的基本名称(\W)。$ export PS1='[\u@\H \D{%Y-%m-%d %H:%M:%S%z}] \W \$ ' [jp@freebsd.jpsdomain.org 2005-12-28 19:33:03-0500] ~ $ cd /usr/local/ [jp@freebsd.jpsdomain.org 2005-12-28 19:33:06-0500] local $用户名 @ 主机名、bash 版本号,以及当前工作目录(\w)。$ export PS1='[\u@\h \V \w] \$ ' [jp@freebsd 3.00.16] ~ $ cd /usr/local/bin/ [jp@freebsd 3.00.16] /usr/local/bin $换行符、用户名 @ 主机名、基本 PTY、shell层级、历史编号、换行符,以及完整的工作目录名称($PWD)。
PTY 是你所连接的伪终端(Linux 术语)的编号。当你有多个会话并且试图跟踪各个会话时,这就能派上用场了。shell 层级是你所在子 shell 的深度。首次登录时,这个值是 1,随着不断地生成子进程(如 screen),该值也会不断增加,因此,运行 screen 之后,值就变成了 2。历史编号是当前命令在命令历史记录中的编号。$ export PS1='\n[\u@\h \l:$SHLVL:\!]\n$PWD\$ ' [jp@freebsd ttyp0:3:21] /home/jp$ cd /usr/local/bin/ [jp@freebsd ttyp0:3:22] /usr/local/bin$用户名 @ 主机名、上一个命令的退出状态,以及当前工作目录。注意,只要在提示符下执行任何命令,退出状态就会被重置(因此也就失去了作用)。$ export PS1='[\u@\h $? \w \$ ' [jp@freebsd 0 ~ $ cd /usr/local/bin/ [jp@freebsd 0 /usr/local/bin $ true [jp@freebsd 0 /usr/local/bin $ false [jp@freebsd 1 /usr/local/bin $ true [jp@freebsd 0 /usr/local/bin $换行符、用户名 @ 主机名,以及 shell 当前管理的作业数。如果运行了大量的后台作业,又把这事给忘了,该提示符就能发挥作用了。$ export PS1='\n[\u@\h jobs:\j]\n$PWD\$ ' [jp@freebsd jobs:0] /tmp$ ls -lar /etc > /dev/null & [1] 96461 [jp@freebsd jobs:1] /tmp$ [1]+ Exit 1 ls -lar /etc >/dev/null [jp@freebsd jobs:0] /tmp$换行符、用户名 @ 主机名、终端、shell 层级、历史编号、作业数量、bash 版本号,以及完整的工作目录。$ export PS1='\n[\u@\h t:\l l:$SHLVL h:\! j:\j v:\V]\n$PWD\$ ' [jp@freebsd t:ttyp1 l:2 h:91 j:0 v:3.00.16] /home/jp$换行符、用户名 @ 主机名、代表终端的 T、代表 shell 层级的 L、代表命令编号的 C,以及 ISO 8601 格式的日期和时间。
这属于那种让人非爱即恨的提示符。它清晰无误地显示了谁在什么时间什么地点做了什么,非常适合在回滚缓冲区(scrollbackbuffer)中通过简单地复制和粘贴来记录执行某项任务的步骤,但有些人认为这种提示符的形式太杂乱,容易分散注意力。$ PS1='\n[\u@\h:T\l:L$SHLVL:C\!:\D{%Y-%m-%d_%H:%M:%S_%Z}]\n$PWD\$ ' [jp@freebsd:Tttyp1:L1:C337:2006-08-13_03:47:11_EDT] /home/jp$ cd /usr/local/bin/ [jp@freebsd:Tttyp1:L1:C338:2006-08-13_03:47:16_EDT] /usr/local/bin$
-
花哨的提示符:下列 3 种花哨的提示符要么使用 ANSI 转义序列来显示颜色,要么在 xterm 中设置标题栏的内容,但要注意,这些做法未必总能奏效。
系统设置、xterm 仿真,以及 SSH 和 Telnet 客户端中都存在大量让人摸不着头脑的变量,它们都可能影响到这些提示符。
另外还要注意,转义序列应该出现在 [ 和 ] 的内部,以此告知 bash 其中的字符不可打印。否则,bash(从技术上来说其实是 readline)会搞不清楚行的长度,在错误的位置上折行。
用户名 @ 主机名、浅蓝色的当前工作目录(实体书上看不出颜色)。$ export PS1='\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] ' [jp@freebsd:~]$ [jp@freebsd:~]$ cd /tmp [jp@freebsd:/tmp]$在 xterm 的标题栏和提示符中同时显示用户名 @ 主机名、当前工作目录。如果你使用的不是 xterm,则可能会在提示符中产生一些乱七八糟的东西。$ export PS1='\[\033]0;\u@\h:\w\007\][\u@\h:\w]\$ ' [jp@ubuntu:~]$ [jp@ubuntu:~]$ cd /tmp [jp@ubuntu:/tmp]$同时更新颜色和 xterm。$ PS1='\[\033]0;\u@\h:\w\007\]\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] ' [jp@ubuntu:~]$ [jp@ubuntu:~]$ cd /tmp [jp@ubuntu:/tmp]$
为了让你在逐个尝试时免去烦人的输入,所有这些提示符都可以在本书的 GitHub 仓库中找到(文件 ./ch16/prompts)。下例展示了该文件的内容。
# 实例文件:prompts
# 用户名@短格式主机名、日期和时间,以及当前工作目录(CWD):
export PS1='[\u@\h \d \A] \w \$ '
# 用户名@长格式主机名、ISO 8601格式的日期和时间、当前工作目录的基本名称(\W):
export PS1='[\u@\H \D{%Y-%m-%d %H:%M:%S%z}] \W \$ '
# 用户名@短格式主机名、bash版本号,以及当前工作目录(\w):
export PS1='[\u@\h \V \w] \$ '
# 换行符、用户名@短格式主机名、基本PTY、shell层级、历史编号、换行符,以及完整的工作
目录名称($PWD):
export PS1='\n[\u@\h \l:$SHLVL:\!]\n$PWD\$ '
# 用户名@短格式主机名、上一个命令的退出状态,以及当前工作目录:
export PS1='[\u@\h $? \w \$ '
# 换行符、用户名@短格式主机名,以及后台作业数:
export PS1='\n[\u@\h jobs:\j]\n$PWD\$ '
# 换行符、用户名@短格式主机名、终端、shell层级、历史编号、作业数量、bash版本号,以
及完整的工作目录:
export PS1='\n[\u@\h t:\l l:$SHLVL h:\! j:\j v:\V]\n$PWD\$ '
# 换行符、用户名@短格式主机名、代表终端的T、代表shell层级的L、代表命令编号的C,以及
ISO 8601格式的日期和时间:
export PS1='\n[\u@\h:T\l:L$SHLVL:C\!:\D{%Y-%m-%d_%H:%M:%S_%Z}]\n$PWD\$ '
# 用户名@短格式主机名、浅蓝色的当前工作目录:
export PS1='\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] '
# 在xterm的标题栏和提示符中同时显示用户名@短格式主机名、当前工作目录:
export PS1='\[\033]0;\u@\h:\w\007\][\u@\h:\w]\$ '
# 同时更新颜色和xterm:
export PS1='\[\033]0;\u@\h:\w\007\]\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] '
注意,export 命令只需要使用一次,就可以标记出要导入子进程的变量。
假设已经设置 shell 选项 promptvars(默认情况),提示字符串会被解析和扩展(通过变量扩展、命令替换以及算术扩展)、引号会被移除并显示最终结果。提示字符串分别为 $PS0、
P
S
1
、
‘
PS1、`
PS1、‘PS2、
P
S
3
‘
、
‘
PS3`、`
PS3‘、‘PS4`。
- $PS0 仅存在于 bash 4.4 及以上版本。下一节将详细讨论这个“执行前”(preexecution)提示符。
- $PS1 是命令提示符。
- $PS2 是辅助提示符,在 bash 需要更多信息来结束命令时,它会出现。该提示符默认为 >,但你也可以改用其他字符。
- $PS3 是 select 提示符(参见 3.7 节和 6.16 节),其默认为 #?。
- PS4 是 xtrace(调试)提示符,其默认为 +。注意,$PS4 的首个字符会根据需要重复多次,以表明当前所执行命令的嵌套级数。
$ export PS2='Secondary> '
$ for i in *
Secondary> do
Secondary> echo $i
Secondary> done
cheesy_app
data_file
hard_to_kill
mcd
mode
$ export PS3='Pick me: '
$ select item in 'one two three'; do echo $item; done
1) one two three
Pick me: ^C
$ export PS4='+ debugging> '
$ set -x
$ echo $( echo $( for i in *; do echo $i; done ) )
+++ debugging> for i in '*'
+++ debugging> echo cheesy_app
+++ debugging> for i in '*'
+++ debugging> echo data_file
+++ debugging> for i in '*'
+++ debugging> echo hard_to_kill
+++ debugging> for i in '*'
+++ debugging> echo mcd
+++ debugging> for i in '*'
+++ debugging> echo mode
++ debugging> echo cheesy_app data_file hard_to_kill mcd mode
+ debugging> echo cheesy_app data_file hard_to_kill mcd mode
cheesy_app data_file hard_to_kill mcd mode
由于仅以交互方式运行 bash 时才有用,因此 $PS1 提示符的最佳设置位置是在全局文件 /etc/bashrc 或者本地文件 ~/.bashrc 中。
从风格角度而言,我们建议将空格作为 $PS1 字符串的最后一个字符。把提示字符串与用户输入的命令隔开,更易于阅读屏幕上的内容。有鉴于此,又因为提示字符串中可能包含其他空格或特殊字符,所以最好使用双引号或单引号将其引用起来,再赋给 $PS1。
至少有 3 种简单易行的方式可以在提示符中显示当前工作目录(CWD):\w、\W、$PWD。\W 输出基本名称,或者说是目录路径的最后一部分,\w 则输出整个路径。注意,当你位于主目录时,两者输出的都是 ~,而非 $HOME的值。这让有些用户着实抓狂,因此,要想输出完整的当前工作目录,可以使用 $PWD。输出完整的当前工作目录会改变提示符的长度,在比较深的目录层次结构中,这会使得提示符折行。这又让另一些用户抓狂了。对于 bash 4或以上版本,使用 $PROMPT_DIRTRIM 并配合 \w 或 \W即可(不会影响 $PWD)。Bash Reference Manual 中对该变量的描述如下。
如果设置为大于 0 的数字,那么在扩展 \w 和 \W提示字符串转义时,将该值作为要保留的结尾目录组件的数量…… 被删除的那些字符以省略号替换。
如果能使用 $PROMPT_DIRTRIM,那就用;如果无法使用,下例中给出了一个可用于截断工作目录的函数及其用法提示。
# 实例文件:func_trunc_PWD
function trunc_PWD {
# $PWD截断代码改编自The Bash Prompt HOWTO:
# 11.10. Controlling the Size and Appearance of $PWD
# $PWD中有多少个字符应该保留
local pwdmaxlen=30
# 表明目录已被截断的指示符
local trunc_symbol='...'
# PWD的临时变量
local myPWD=$PWD
# 用'~'替换$PWD中匹配$HOME的部分
# 如果想使用完整路径,可以将下面这条语句注释掉!
myPWD=${PWD/$HOME/~}
if [ ${#myPWD} -gt $pwdmaxlen ]; then
local pwdoffset=$(( ${#myPWD} - $pwdmaxlen ))
echo "${trunc_symbol}${myPWD:$pwdoffset:$pwdmaxlen}"
else
echo "$myPWD"
fi
}
接下来演示一下用法。
$ source file/containing/trunc_PWD
[jp@freebsd ttyp0:3:60]
~/this is a bunch/of really/really/really/long directories/did I mention really/
really/long$export PS1='\n[\u@\h \l:$SHLVL:\!]\n$(trunc_PWD)\$ '
[jp@freebsd ttyp0:3:61]
...d I mention really/really/long$
你可以注意到,这里的提示字符串出现在单引号内,因此,$ 和其他特殊字符会按照其字面意义被对待。提示字符串在显示期间求值,其中的变量会如期被扩展。双引号也不是不能用,但这样的话,你必须对 shell 元字符进行转义,例如,使用 $ 代替 $。
命令编号和历史编号通常不是一回事:命令的历史编号是其在历史列表中的位置,列表可能包含从历史文件中恢复的命令;而命令编号是在当前 shell 会话期间所执行的一系列命令中的位置。
另外还有一个特殊变量 $PROMPT_COMMAND,该变量(如果设置的话)会被解释为在求值并显示 $PS1 之前要执行的命令。此处以及在 $PS1 中使用命令替换的问题是,每次显示提示符时都会执行这些命令,这太频繁了。例如,你可以在提示符中嵌入 $(ls-l | wc-l) 这样的命令替换,以此获得当前工作目录内的文件数量。但对于比较陈旧或者使用率颇高的系统来说,如果位于包含大量文件的目录中,可能会导致在看到提示符并能够着手工作前出现明显的延迟。提示符最好简短明了(尽管本节中展示的部分提示符挺吓人的)。如有需要,可以定义函数或别名,别搞得你的提示符又乱又慢。
为了解决提示符中出现的 ANSI 或 xterm 转义序列在不被支持的情况下产生乱七八糟的内容,你可以在 rc 文件中加入下列操作。
case $TERM in
xterm*) export \
PS1='\[\033]0;\u@\h:\w\007\]\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\]' ;;
*) export PS1='[\u@\h:\w]\$ ' ;;
esac
在我们刚刚讨论的 ANSI 例子中,1;34m 代表“将字符属性设置为浅色,将字符颜色设置为蓝色”。0m 代表“清除所有属性,不设置颜色”。这些代码可参见 A.3 节。结尾的 m 表示颜色转义序列。
下例中的脚本展示了所有可能的组合。如果你的终端显示不出颜色,则表明未启用或不支持 ANSI 颜色。
#!/usr/bin/env bash
# 实例文件:colors
#
# Daniel Crisman的ANSI颜色图表脚本取自
# The Bash Prompt HOWTO: 6.1. Colours
#
# 该文件向终端回显了一批颜色代码,以演示可用的颜色。
# 每行分别选择17种前景色颜色代码(默认 + 16个转义序列)中的一种,
# 然后在所有9种背景色上测试该颜色(默认 + 8个转义序列)
#
T='gYw' # 测试文本
echo -e "\n 40m 41m 42m 43m\
44m 45m 46m 47m";
for FGs in ' m' ' 1m' ' 30m' '1;30m' ' 31m' '1;31m' ' 32m' \
'1;32m' ' 33m' '1;33m' ' 34m' '1;34m' ' 35m' '1;35m' \
' 36m' '1;36m' ' 37m' '1;37m'; do
FG=${FGs// /}
echo -en " $FGs \033[$FG $T "
for BG in 40m 41m 42m 43m 44m 45m 46m 47m; do
echo -en "$EINS \033[$FG\033[$BG $T \033[0m";
done
echo;
done
echo
3、在程序运行前出现的提示符
你想让提示符在程序运行前出现,而不只是在结束之后。这样可以方便地标记起止时间。
这个解决方案仅适用于 bash 4.4 或以上版本。这些版本中引入了 $PS0 提示符。如果设置的话,该提示字符串会在命令执行前被求值并输出。
接下来我们用 $PS0 和 $PS1 来显示命令运行的起止时间。
PS0=' \t\n'
PS1='----------------------------------------------- \t\n\! \$ '
P S 0 是执行前提示符。它会在 s h e l l 执行命令前显示。那些前导空白字符是为了将输出往右边挪动;如果觉得还不够靠右或想再往左边些,随意添加或删除空白字符即可。与此类似, PS0 是执行前提示符。它会在 shell 执行命令前显示。那些前导空白字符是为了将输出往右边挪动;如果觉得还不够靠右或想再往左边些,随意添加或删除空白字符即可。与此类似, PS0是执行前提示符。它会在shell执行命令前显示。那些前导空白字符是为了将输出往右边挪动;如果觉得还不够靠右或想再往左边些,随意添加或删除空白字符即可。与此类似,PS1 中的连字符也是为了将时间戳右移,同时在命令之间划定界限,以便更好地观察。你也可以根据需要添加更多的连字符(或改用空格)。
这两个提示符中的关键在于 \t。它会转换成时间戳。\n 代表换行符,作用是更好地格式化。如果按照上述方法设置好提示符,然后执行一个得花费些时间的命令(如 sleep5),可以看到如下结果。
1037 $ echo 'sleep...' ; sleep 5; echo 'awake!'
21:36:59
sleep...
awake!
----------------------------------------------- 21:37:04
1038 $
如果想让 $PS0 提示符和键入的命令出现在同一行,那就在 \t 前面加上 \e[A。为了让时间戳更往右移,你可能还得添加更多空格。
要想在设置之前先测试提示符,可以利用另一个仅 适用于 bash 4.4 或以上版本的特性。将提示字符串赋给某个变量,然后使用 @P 运算符输出该变量的值。例如:
$ MYTRY=' \! \h \t\n'
$ echo "${MYTRY}"
\! \h \t\n
$ echo "${MYTRY@P}"
1015 monarch 14:07:45
$
如果不使用 @P,输出的则是所键入的字符;加上@P 的话,则会按照提示字符串解释其中的特殊字符序列。如果显示效果如你所愿,就将其赋给$PS0。
时间戳的解析度只能到秒,并不适用于精确的性能测量,但在回顾一系列命令的来龙去脉(尤其离开屏幕去拿咖啡的情况下)以及哪些命令运行时间较长时非常有用。
4、永久修改$PATH
你需要永久性修改命令路径。
首先,你得找出在哪里设置路径,然后再修改。对于本地账户,可能是在~/.profile 或 ~/.bash_profile 中。用 grep-l PATH ~/.[^.]* 找出该文件并使用你喜欢的编辑器进行修改。接着对文件使用 source 命令,使改动立即生效。
如果你是 root 用户,则需要设置整个系统的路径,基本操 作过程是一样的,但 /etc 目录中可供设置 $PATH 的文件不尽相同,具体取决于你的操作系统和版本。最有可能的文件是 /etc/profile,但也有可能是/etc/bashrc、/etc/rc、/etc/default/login、~/.ssh/environment 以及 PAM 的 /etc/environment。
某些系统中有一个名为 /etc/profile.d 的目录,其中包含了系统启动时会运行的 shell 脚本。你可以修改该目录中已有的脚本或添加新脚本。里面的各色脚本只是组织或模块化多种设置的一种方式,这好过将它们一股脑地放进一个大文件中。
由于 shell 通配符扩展的性质以及 . 和 … 目录的存在,grep -l PATH ~/.[^.]* 命令值得注意。
$PATH 中列出的位置会带来安全隐患,尤其作为 root 用户时。如果 root 用户的路径中有一个人皆可写的目录出现在其他典型目录(/bin、/sbin)之前,那么本地用户就可以创建出让 root 执行的文件,从而对系统为所欲为。这也是当前目录(.)不应该出现在 root 用户路径中的原因。
为了避免这个问题:
- 令 root 用户的路径尽可能简短,绝不要使用相对路径避免在 root 用户的路径中出现人皆可写的目录
- 考虑在 root 用户执行的 shell 脚本中明确设置路径
- 对于 root 用户执行的 shell 脚本中用到的实用工具,考虑硬编码其绝对路径
- 将用户或应用程序目录放在 $PATH 的末尾,只用于非特权用户
5、临时修改$PATH
你想在当前会话期间向 $PATH 中添加(或删除)一个目录。
这个问题的解决方案不止一种。
可以使用 PATH="newdir:$PATH" 或PATH="$PATH:newdir" 将新目录放在前面或后面,不过你得确保该目录不会在 $PATH 中重复出现。
如果需要编辑路径中间的部分,可以通过 echo 命令将路径输出到屏幕,然后使用终端的 kill 和 yank(剪切和粘贴)功能 3 将其复制到新行并进行编辑。你也可以从readline 文档中添加“便于 shell 交互的宏”([m]acrosthat are convenient for shell interaction)。尤其是:
# 编辑路径
"\C-xp": "PATH=${PATH}\e\C-e\C-a\ef\C-f"
# [...]
# 在当前行编辑变量
"\M-\C-v": "\C-a\C-k$\C-y\M-\C-e\C-a\C-y="
按下 Ctrl-X P,当前行上会显示出 $PATH 的内容,输入任 何变量名称,然后按下 Meta-Ctrl-V,就会显示出该变量的内容,以供编辑。这非常方便。
对于一些简单的情况,你也可以使用下例中的函数(取自 Red Hat Linux 的 /etc/profile,略有改编)。
# 实例文件:func_pathmunge
# 改编自Red Hat Linux版本
function pathmunge {
if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
if [ "$2" = "after" ] ; then
PATH="$PATH:$1"
else
PATH="$1:$PATH"
fi
fi
}
egrep 模式 (^|:) 1 ( 1( 1(|:) 用于在 $PATH 字符串中的各个可能位置查找 $1 的值。我们在自己编写的函数func_tweak_path 中选择使用 case 语句,强制在路径的开头和结尾加上了 :。因为用的是 shell 内建命令,所以这种做法在理论上速度会更快,不过 Red Hat 版本要更简洁。我们的版本也很好地演示了 if 命令能够处理退出码,因此第一个 if 可以使用 grep 所设置的退出码,第二个 if则需要使用测试运算符([])。
对于需要进行大量错误检查的复杂情况,可以读入并使用下例中给出的更为通用的函数。
# 实例文件:func_tweak_path
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# 在路径开头或结尾处添加目录(只要该目录尚不存在)
# 不考虑符号链接!
# 返回值:1或者设置好新的$PATH
# 调用方式:add_to_path <directory> (pre|post)
function add_to_path {
local location=$1
local directory=$2
# 确保有可处理的对象
if [ -z "$location" -o -z "$directory" ]; then
echo "$0:$FUNCNAME: requires a location and a directory to add" >&2
echo "e.g. add_to_path pre /bin" >&2
return 1
fi
# 确保不是相对目录
if [ $(echo $directory | grep '^/') ]; then
:echo "$0:$FUNCNAME: '$directory' is absolute" >&2
else
echo "$0:$FUNCNAME: can't add relative directory '$directory' to \$PATH" >&2
return 1
fi
# 确保要添加的目录确实存在
if [ -d "$directory" ]; then
: echo "$0:$FUNCNAME: directory exists" >&2
else
echo "$0:$FUNCNAME: '$directory' does not exist--aborting" >&2
return 1
fi
# 确保要添加的目录尚不存在于$PATH之中
if [ $(contains "$PATH" "$directory") ]; then
echo "$0:$FUNCNAME: '$directory' already in \$PATH--aborting" >&2
else
:echo "$0:$FUNCNAME: adding directory to \$PATH" >&2
fi
# 判断要执行的操作
case $location in
pre* ) PATH="$directory:$PATH" ;;
post* ) PATH="$PATH:$directory" ;;
* ) PATH="$PATH:$directory" ;;
esac
# 清理新路径并设置$PATH
PATH=$(clean_path $PATH)
} # 函数add_to_path定义完毕
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# 从路径中删除目录(如果存在的话)
# 返回值:设置好新的$PATH
# 调用方式:rm_from_path <directory>
function rm_from_path {
local directory=$1
# 从$PATH中删除所有的$directory实例
PATH=${PATH//$directory/}
# 清理新路径并设置$PATH
PATH=$(clean_path $PATH)
} # 函数rm_from_path定义完毕
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# 删除开头/结尾或重复的':',删除重复的目录项
# 返回值:显示“经过清理的”路径
# 调用方式:cleaned_path=$(clean_path $PATH)
function clean_path {
local path=$1
local newpath
local directory
# 确保有可处理的对象
[ -z "$path" ] && return 1
# 删除重复的目录项(如果存在的话)
for directory in ${path//:/ }; do
contains "$newpath" "$directory" && newpath="${newpath}:${directory}"
done
# 删除开头的':'分隔符
# 删除结尾的':'分隔符
# 删除重复的':'分隔符
newpath=$(echo $newpath | sed 's/^:*//; s/:*$//; s/::/:/g')
# 返回新路径
echo $newpath
} # 函数clean_path定义完毕
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# 判断路径是否包含指定的目录
# 如果目标符合模式,返回1;否则,返回0
# 调用方式:contains $PATH $dir
function contains {
local pattern=":$1:"
local target=$2
# 比较时区分大小写,除非设置过nocasematch选项
case $pattern in
*:$target:* ) return 1;;
* ) return 0;;
esac
} # 函数contains定义完毕
用法如下所示:
$ source chpath
$ echo $PATH
/bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin:/home/jp/bin
$ add_to_path pre foo
-bash:add_to_path: can't add relative directory 'foo' to the $PATH
$ add_to_path post ~/foo
-bash:add_to_path: '/home/jp/foo' does not exist--aborting
$ add_to_path post '~/foo'
-bash:add_to_path: can't add relative directory '~/foo' to the $PATH
$ rm_from_path /home/jp/bin
$ echo $PATH
/bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin
$ add_to_path /home/jp/bin
-bash:add_to_path: requires a location and a directory to add
e.g. add_to_path pre /bin
$ add_to_path post /home/jp/bin
$ echo $PATH
/bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin:/home/jp/bin
$ rm_from_path /home/jp/bin
$ add_to_path pre /home/jp/bin
$ echo $PATH
/home/jp/bin:/bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin
关于这个问题以及例中给出的函数,有 4 个值得注意的地方。
首先,如果尝试在 shell 脚本中修改 $PATH 或其他环境变量,起不到任何效果,因为脚本在子 shell 中运行,脚本结束后,子 shell 也随之终止,修改过的环境变量全部会失效。因此,我们将函数读入当前 shell 环境并在其中运行。
其次,你可能注意到 add_to_path post ~/foo 会返回“does not exist”,而 add_to_path post’~/foo’ 返回的是“can’t add relative directory”。原因是,在函数运行之前,shell 会将 ~/foo 扩展成 /home/jp/foo。没考虑到 shell 扩展是一个常见错误。可以用 echo 命令查看shell 到底将什么传给了脚本和函数。
接下来,你可能还会注意到 echo"$0:$FUNCNAME:requires a location and a directory toadd" >&2 这样的代码行。$0:$FUNCNAME 可以方便地识 别出错误消息究竟来自何处。$0 始终保存的是当前程序的名称(本例中为 -bash,其他情况下是脚本或程序的名称)。加上函数名便于在调试时定位错误。>&2 将输出发送到 STDERR,运行期间产生的用户反馈(尤其是警告或错误信息)都应该出现在这里。
最后,你可以认为这些函数的接口不一致,因为add_to_path 和 remove_from_path 实际上设置了$PATH,而 clean_path 显示的是经过清理的路径,contains 返回真或假。我们在生产环境中并不会这么做,这里只是为了让示例更加有趣,展现不同的处理方式。鉴于这些函数的用途,我们觉得这些接口也说得过去。
6、设置$CDPATH
你希望能够更轻松地在不同位置的多个目录之间进行切换。
恰当地设置 $CDPATH。你的常用目录可能就是那么几个,假设你要花费大量时间来处理 init 的 rc 目录:
/home/jp$ cd rc3.d
bash: cd: rc3.d: No such file or directory
/home/jp$ export CDPATH='.:/etc'
/home/jp$ cd rc3.d
/etc/rc3.d
/etc/rc3.d$ cd rc5.d
/etc/rc5.d
/etc/rc5.d$ cd games
bash: cd: games: No such file or directory
/etc/rc5.d$ export CDPATH='.:/etc:/usr'
/etc/rc5.d$ cd games
/usr/games
/usr/games$
根据 Bash Reference Manual,$CDPATH 是“一个以冒号为分隔符的目录列表,用作内建命令 cd 的搜索路径”。可以将它看作 cd 的 $PATH。其用法有点微妙,但着实非常方便。
如果 cd 的参数以斜线开头,则用不到 $CDPATH。如果使用了 $CDPATH,则会向 STDOUT 输出新目录的绝对路径,如上面的示例所示。
如果是在 POSIX 模式下运行 bash(例如,/bin/sh 或使用 --posix 选项),那可要小心了。Bash Reference Manual 中提到过:
如果使用了 $CDPATH 中的非空目录名,或者第一个参数是 -,而且成功切换了目录,新工作目录的绝对路径会被写入标准输出。
换句话说,几乎每次使用 cd 时都会向 STDOUT 回显新路径,这并非标准行为。
$CDPATH 中常见的目录包括:
.:当前目录(可选,因为这是隐含的)~/:主目录..:父目录../..:祖父目录~/.dirlinks:隐藏目录,只包含指向其他常用目录的符号链接
因此可以得到:
export CDPATH='.:~/:..:../..:~/.dirlinks'
7、当找不到命令时
你希望能够更好地处理找不到命令的情况,或者只是输出更好的错误消息。
将下列函数添加到脚本的起始部分或者 rc 文件中,后者效果更佳:
function command_not_found_handle ()
{
echo "Sorry. $0: $1 not there."
return 1
}
在 bash 4.3 及其后续版本中,如果找不到你想要运行的可执行文件,shell 会调用一个特殊的函数command_not_found_handle,你可以根据自己的用途(重新)定义该函数。本例只是让其输出 shell 的名称以及无法找到的命令。
重要的是,函数要能返回非 0 值来表明调用命令失败。脚本的其他部分或脚本的调用者有可能依赖此信息。
有些管理员将函数 command_not_found_handle 的定义放在了系统范围的 bashrc 文件(例如,/etc/profile等)。在函数内部,查找 /usr/lib 或 /usr/share 下的 Python 脚本 command-not-found(注意是连字符,不是下划线)。该脚本会查找刚刚调用失败的命令所对应的软件包,看看能否建议安装,以提供缺失的命令。这种做法在某些情况下的确有帮助,但如果只是简单地输错了命令,那这就是添乱了。
8、缩短或修改命令名称
你想将经常用到的那些冗长或复杂的命令变得简短些,或者重新命名难以记忆、不易输入的命令。
不要手动改名或移动可执行文件,因为 Unix 和 Linux 的不少地方都依赖于某些位置上的某些文件;你应该使用别名、函数以及符号链接。
根据 Bash Reference Manual,“别名允许将简单命令的第一个单词替换成字符串。shell 维护着一系列别名,可以使用内建命令 alias 和 unalias 进行设置和删除”。这意味着你可以在一个别名中列出多个命令,从而重命名命令或创建宏;例如,alias copy='cp' 或 alias ll.='ls -ld .*'。
别名只能扩展一次,因而得以在不陷入死循环的情况下改变命令的工作方式,比如 alias ls=‘ls -F’。大多数情况下,别名扩展仅检查命令行的第一个单词,而且别名属于严格的文本替换,本身无法使用参数。也就是说,你不能定义别名 alias mkcd='mkdir $1 && cd $1'。
函数有两种用法。首先,它们可以被读入当前的交互式shell 环境,成为常驻内存的脚本。函数占用内存少,由于已经载入内存,运行速度非常快,而且是在当前进程中执行,而非其子进程。其次,函数可以在脚本中作为子程序使用。另外,函数还可以接受参数,如下例所示。
# 实例文件:func_calc
# 简单的命令行计算器
function calc {
# 仅适用于整数!--> echo The answer is: $(( $* ))
# 浮点数
awk "BEGIN {print \"The answer is: \" $* }";
} # 函数calc定义完毕
对于个人或系统范围的用途而言,使用别名或函数来重命名或调校命令可能更好,但符号链接允许单个命令同时出现在多个位置,这一点非常实用。例如,Linux 系统基本上都是使用 /bin/bash,而其他系统使用的可能是/usr/bin/bash、/usr/local/bin/bash 或者/usr/pkg/bin/bash。虽然有更好的办法处理这个问题,但符号链接可能是一种通用的解决方式。我们不推荐使用硬链接,因为不大容易看出来它们是否就是你要找的对象,而且硬链接更容易被操作不当的编辑器等破坏。符号链接更加显眼和直观。
在进行别名扩展时,通常只检查命令行的第一个单词。但如果别名值的最后一个字符是空白字符,则接下来的单词也会被检查。这在实践中很少出现什么问题。
由于 bash 中的别名无法使用参数(不同于 csh),如果的确需要传递参数,你得使用函数。别名和函数都位于内存中,因此差别不大。
除非设置了 shell 选项 expand_aliases,否则别名不会在非交互式 shell 中扩展。编写脚本的最佳实践要求不使用别名,因为其他系统中未必存在同样的别名。你还需要在脚本中定义函数,或在使用前明确地将其读入。因此,定义函数的最佳位置是在全局的/etc/bashrc 或本地的~/.bashrc 中。
9、调整shell行为及环境
你希望根据自己的工作方式、物理位置、语言等调整 shell环境。
参见 A.6 节、A.7 节、A.8 节。
有 3 种方法可以调整 shell 环境的方方面面。set 是 POSIX 标准,使用单字母选项。shopt 针对 bash 的 shell 选项。由于历史原因,再加上要与很多第三方应用程序兼容,在用的环境变量数量着实不少。如何调整以及在哪里调整让人倍感困惑。附录 A 中的表格可以帮你理清头绪,由于篇幅过多,这里就不再赘述。
10、用.inputrc调整readline的行为
你想要调整 bash 处理输入的方式,尤其是命令补全。例如,你希望能够忽略大小写。
根据情况,编辑或创建适合的 ~/.inputrc 或 /etc/inputrc文件。有大量的选项可供你根据喜好调整。要想 readline在初始化时使用你所提供的文件,可以设置 $INPUTRC, 例如,INPUTRC=~/.inputrc。要想重新读取文件并应用设置,或修改后进行测试,可以使用 bind -f filename。
我们推荐你研究一下 readline 的文件以及 bind 命令,尤其是 bind -v、bind -l、bind -s、bind -p,最后一个命令涉及的内容颇多,而且晦涩难懂。
有关 readline 的更多配置信息,参见 A.18 节。对于来自其他环境(尤其是 Windows)的用户比较有用的配置是:
# 这是inputrc设置中值得注意的一部分,更长的示例参见16.22节
# 要想重新读取(并应用对该文件做出的改动),可以使用:
# bind -f $SETTINGS/inputrc
# 首先,纳入/etc/inputrc中所有的系统范围绑定和变量赋值
#(如果文件不存在,则静默失败)
$include /etc/inputrc
$if Bash
# 进行命令补全时忽略大小写
set completion-ignore-case on
# 在补全的目录名后添加斜线
set mark-directories on
# 如果补全的名称是指向目录的符号链接,在其后添加斜线
set mark-symlinked-directories on
# 在补全的名称后添加与ls -F相同的文件类型符号
set visible-stats on
# 遍历模棱两可的补全,而不是将其列出
"\C-i": menu-complete
# 将铃声设置为能够听到
set bell-style audible
# 列出可能的补全结果,而不是响铃
set show-all-if-ambiguous on
# 取自readline文档
# 方便shell交互使用的一些宏
# 编辑路径
"\C-xp": "PATH=${PATH}\e\C-e\C-a\ef\C-f"
# 为输入带引号的单词做准备——插入左双引号和右双引号,
# 然后将光标移动到左双引号之后
"\C-x\"": "\"\"\C-b"
# 插入反斜线(在字符序列和宏中测试反斜线转义)
"\C-x\\": "\\"
# 引用当前或前一个单词
"\C-xq": "\eb\"\ef\""
# 添加绑定以刷新未绑定的行
"\C-xr": redraw-current-line
# 编辑当前行上的变量
#"\M-\C-v": "\C-a\C-k$\C-y\M-\C-e\C-a\C-y="
"\C-xe": "\C-a\C-k$\C-y\M-\C-e\C-a\C-y="
$endif
不妨尝试一下这些及其他配置。另外注意,$include 引入了系统配置,但要确保你能够根据需要进行修改。
很多人并没有意识到 GNU readline 库具有多么出色的可定制性,更别提其强大的功能和灵活性了。虽说如此,但并不存在“一劳永逸”的方法。你应该制定适合自己需求和习惯的配置。
注意,第一次调用 readline 时会进行其正常的启动文件处理,包括查看 $INPUTRC,如果该变量未设置,则默认为~/.inputrc。
11、通过添加~/bin来存放个人工具
你有一些要使用的个人工具,但你并非系统的 root 用户,无法将这些工具放入正常的位置(如 /bin 或/usr/local/bin),或者出于其他原因,需要将这些工具隔离开。
创建~/bin 目录,将个人工具放入其中并将该目录添加到路径:
PATH="$PATH:~/bin"
你需要在某个 shell 初始化文件(如 ~/.bashrc)中执行上述更改。有些系统已经默认将 $HOME/bin 作为非特权用户账户的最后一个目录,因此最好先检查一下。
作为一名绝对合格的 shell 用户(毕竟你已经买了这本书嘛),你肯定创建了不少脚本。使用完整路径名调用脚本实在不方便。通过将脚本汇集到 ~/bin 目录中,至少可以让你的脚本看起来就像普通的 Unix 程序那样。
出于安全考虑,不要将你的 bin 目录放在路径开头。这样很容易覆盖系统命令,如果是无心之举(我们也干过),这会很不方便;如果是恶意为之,那可就危险了。
1158



被折叠的 条评论
为什么被折叠?



