得之我幸 失之我命

when someone abandons you,it is him that gets loss because he lost someone who truly loves him but you just lost one who doesn’t love you.

探索 Bash 中的 Prompt

在开始探索之前,还是得再次夸一夸 Arch 的 wiki,虽然自己日常用的是 macOS 和 debian,但是好几次终端有关的问题,却都是通过 Arch 的 wiki 找到了我想要的答案,这次,兜兜转转,也还是让又出了一份力

本文围绕的东西有:PS1、PS2、PS3、PS4、PROMOTE_COMMAND、ANSI escape code

P 字辈

Bash 有四个可以定制的提示符:

  • PS1 是在每个命令前都显示的主要提示符,大部分用户都是定制这个值

  • PS2 命令需要输入时的第二提示符(比如多行命令)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 修改 PS2 前,"> " 作为默认提示符
    $ myisamchk --silent --force --fast --update-state \
    > --key_buffer_size=512M --sort_buffer_size=512M \
    > --read_buffer_size=4M --write_buffer_size=4M \
    > /var/lib/mysql/bugs/*.MYI
    # 自定义 PS2 后
    $ export PS2="continue-> "
    $ myisamchk --silent --force --fast --update-state \
    continue-> --key_buffer_size=512M --sort_buffer_size=512M \
    continue-> --read_buffer_size=4M --write_buffer_size=4M \
    continue-> /var/lib/mysql/bugs/*.MYI
  • PS3 不常用,Bash 的内置 select 显示交互菜单时使用. 和其它提示符不一样,它不扩展 Bash escape sequences. 通常在使用包含 select 的脚本时会需要定制此提示符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    # 一个带 select 的例子
    $ cat ps3.sh
    select i in mon tue wed exit
    do
    case $i in
    mon) echo "Monday";;
    tue) echo "Tuesday";;
    wed) echo "Wednesday";;
    exit) exit;;
    esac
    done

    # 修改 PS3 前,缺省的提示符是 "#? "
    $ ./ps3.sh
    1) mon
    2) tue
    3) wed
    4) exit
    #? 1
    Monday
    #? 4

    # 在 ps3.sh 中设置了 PS3 变量后
    $ cat ps3.sh
    PS3="Select a day (1-4): "
    select i in mon tue wed exit
    do
    case $i in
    mon) echo "Monday";;
    tue) echo "Tuesday";;
    wed) echo "Wednesday";;
    exit) exit;;
    esac
    done

    # 执行的时候提示符已经变成了 "Select a day (1-4): "
    $ ./ps3.sh
    1) mon
    2) tue
    3) wed
    4) exit
    Select a day (1-4): 1
    Monday
    Select a day (1-4): 4
  • PS4 也不常用,在调试 bash 脚本时显示缩进级别。第一个字符的重复次数表示缩进级别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    # 一个带调试的例子
    $ cat ps4.sh
    set –x
    echo "PS4 demo script"
    ls -l /etc/ | wc –l du -sh ~

    # 修改 PS4 前,使用 sex -x 跟踪输出时的提示符默认为 "++ "
    $ ./ps4.sh
    ++ echo 'PS4 demo script'
    PS4 demo script
    ++ ls -l /etc/
    ++ wc –l
    243
    ++ du -sh /home/ramesh
    48K /home/ramesh

    # 在 ps4.sh 中设置了 PS4 变量后
    $ cat ps4.sh
    export PS4='$0.$LINENO+ '
    # $0 显示当前的脚本名
    # $LINENO 显示的当前的行号

    set -x
    echo "PS4 demo script"
    ls -l /etc/ | wc -l
    du -sh ~

    $ ./ps4.sh
    ../ps4.sh.3+ echo 'PS4 demo script'
    PS4 demo script
    ../ps4.sh.4+ ls -l /etc/
    ../ps4.sh.4+ wc -l
    243
    ../ps4.sh.5+ du -sh /home/ramesh
    48K /home/ramesh

而 PROMPT_COMMAND 则是作为一个普通的 bash 命令执行的,只是执行时间是在 bash 显示 prompt 之前

回过头来

设置 PS1 的时候,用到了 '\[\e]0;\u: \w\a\]\[\e[33;1m\]\u:\[\e[32;1m\]\w\$ ',这次,暂时先不看 OSC 转义序列,看 \[\e[33;1m\]\u:\[\e[32;1m\]

  • 为啥有 \[\]

    不难发现,两端颜色转义序列的两边都有 \[\],这是为啥呢? 参看 1 2

    因为 \e[33;1m 是不可打印字符,而 \[ \] 有助于 Bash 忽略不可打印字符,以便正确计算提示符的大小,如果没有 \[ \],bash 会认为构成颜色代码的转义序列的字节实际上会占用屏幕上的空间,导致 readline 无法知道光标的实际位置,可能会导致提示符被覆盖等奇奇怪怪的问题

  • 但是别高兴的太早,\[\] 的坏心眼儿

    知道了上面 \[\] 的作用,为了让 PS1 根据不同的场景显示不同的颜色,是不是可以直接用写个函数,通过 echo -e 设置动态的 PS1 了?

    如果你是这么想的,那你就正好落入了 \[ \] 的坑,事实上,\[ \] 除了直接对 PS 系列变量设置时有效,对于其他采用输出方式设置 PS 系列变量的情境下都不生效(例如 printf or echo -e

    这时候,就需要用 \001\002 替换 \[\]

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # this function runs when the prompt is displayed
    active_prompt () {
    local blue=$(tput setaf 4)
    local reset=$(tput sgr0)
    # printf '\[%s\]%s\[%s\]' "$blue" "$PWD" "$reset" # no effect
    printf '\001%s\002%s\001%s\002' "$blue" "$PWD" "$reset"
    }

    PS1='$(active_prompt)\$ '

    再举个例子:

    1
    2
    3
    4
    5
    6
    $ PS1='\[\e[32;1m\]\u:\[\e[0m\] '
    $ echo -e $PS1
    \[\]\u:\[\]
    $ PS1='\001\e[32;1m\002\u:\001\e[0m\002 '
    $ echo -e $PS1
    \u:
  • \[\] 替换成 \001\002原因

    readline 接受 \001 和 \002 (ASCII SOH and STX) 作为不可打印的文本分隔符,所以只要用 readline 这个库,就支持这个分隔符

    bash 源码中 lib/readline/display.c:

    1
    2
    3
    4
    5
    6
    /* Current implementation:
    \001 (^A) start non-visible characters
    \002 (^B) end non-visible characters
    all characters except \001 and \002 (following a \001) are copied to
    the returned string; all characters except those between \001 and
    \002 are assumed to be `visible'. */

be slow to promise and quick to perform.