自定义 Bash 主题

对于广为人知的 fish、Oh-My-Zsh,尽管他们内置了很多功能,但是实际上大部分人只用到了两部分:

  • 漂亮的主题
  • Tab 补全

在这里,我们针对主题部分进行自定义修改。

转义字符

对于终端中,输入命令前面的部分,可以使用PS1环境变量设置。

export PS1="\u@\H:\w"

PS1="\u@\H:\w"PS1="\u@\H:\w"

就是将前面的部分设置为[用户名]@[主机名]:[当前目录]

相应的转义字符可见表

符号 含义 示例
\h 当前主机名(第一部分) mycomputer
\H 当前主机名(全称) mycomputer.example
\u 当前用户名 mario
\v 命令解释器 bash
\l 终端 ttys02
\w 目录 /usr/local/src
\W 文件夹 src
\A 时间(HH:MM) 14:23
\t 时间(HH:MM:SS) 14:23:52
\@ 时间(HH:MM AM/PM) 07:23 AM
\T 时间(12小时) 02:23:52
\D 日期 Mon Feb 22
$? 前一条命令退出状态 0

首先,限定下面的需求:

  1. 需要可以查看主机名和用户名,这样如果有多个服务器也不会混乱
  2. 需要可以看到绝对路径,这样可以确定及时存在同名目录也可以确保在正确的目录执行程序
  3. 带时间,可以记录一下每个命令结束时间(每一行的时间实际上是上一行结束时间)

可以使用

export PS1="\$ \t \u@\H:\w >"

PS1="\$ \t \u@\H:\w>"PS1="\$ \t \u@\H:\w >"

颜色配置

上面虽然符合功能要求,但是并不“好看”,一个好看的主题,起码应该是有多种颜色的。

这里,可以使用 ANSI 颜色进行配置,使用\[\e[font;fg;bgm\]可以对后面的文字输出进行样式管理

export PS1='$ \[\e[0;95;40m\]\t\[\e[0m\] \[\e[0;33;40m\]\u\[\e[0m\]@\[\e[0;96;40m\]\H\[\e[0m\]:\[\e[0;36;40m\]\w\[\e[0m\] \[\e[1;37;40m\]>\[\e[0m\] '

带颜色的自定义配置带颜色的自定义配置

上面的部分看起来非常凌乱,但是如果分开看的话,还是可以对照下面的部分看懂的。

字体

font为字体编码,包括加粗、斜体、闪烁等各种特效,具体编码见表

编码 含义
1 加粗
2 半透明
3 斜体
4 下划线
5 闪烁
6 快速闪烁
7 反色
8 隐藏
9 删除线

前景色

fg为前景色编码,根据具体环境不同,有三种格式可以选择:

  • 基本颜色: [颜色编码]
  • 256 颜色: 38;5;[编码]
  • rgb 颜色: 38;2;[红色];[绿色];[蓝色]
编码 含义
30 黑色
31 红色
32 绿色
33 黄色
34 蓝色
35 品红
36 青色
37 白色
90 浅黑
91 浅红
92 浅绿
93 浅黄
94 浅蓝
95 淡紫
96 淡青
97 浅白

背景色

bg为背景色编码,根据具体环境不同,有三种格式可以选择:

  • 基本颜色: [颜色编码]
  • 256 颜色: 48;5;[编码]
  • rgb 颜色: 48;2;[红色];[绿色];[蓝色]
编码 含义
40 黑色
41 红色
42 绿色
43 黄色
44 蓝色
45 品红
46 青色
47 白色
100 浅黑
101 浅红
102 浅绿
103 浅黄
104 浅蓝
105 淡紫
106 淡青
107 浅白

带脚本的主题

如果需要更多的功能,则可以使用 bash 本身的功能来实现,如我们需要以下功能:

  • 如果上一行成功执行,则开始的$为绿色,否则为红色
  • 如果当前目录是一个 Git 项目,则显示当前的分支

首先,我们先不考虑PS1,在 Bash 中如何实现上面的功能呢?

前者实际上只是一个判断,判断$?是否为0即可

if [ $? -eq 0 ]; then 
    echo -e '\[\e[1;32;40m\]Success\[\e[0m\]'; 
else 
    echo -e '\[\e[1;31;40m\]Fail\[\e[0m\]';
fi

这里\[\]在 echo 中未被转义,不过并不影响结果。

命令是否执行成功判断命令是否执行成功判断

而后者

branch=$(git status 2>/dev/null | head -n 1 | cut -d " " -f 3 2>/dev/null); \
if [[ -n ${branch} ]]; then
    echo -e "@\[\e[1;91m\]${branch}\[\e[0m\]";
fi

git 分支提取git 分支提取

综上所述,如果PS1的部分代码本身使用$()或反引号包裹,那么将会实时执行,从而实现对动态内容的展示。

最后的代码合起来为

export PS1='$(if [ $? -eq 0 ]; then echo -ne "\[\e[1;32m\]$\[\e[0m\]"; else echo -ne "\[\e[1;31m\]$\[\e[0m\]"; fi) \[\e[95m\]\t\[\e[0m\] \[\e[33m\]\u\[\e[0m\]@\[\e[96m\]\H\[\e[0m\]:\[\e[36m\]\w\[\e[0m\]$(branch=$(git status 2>/dev/null | head -n 1 | cut -d " " -f 3 2>/dev/null);if [[ -n ${branch} ]]; then echo -ne "@\[\e[1;91m\]${branch}\[\e[0m\]";fi) \[\e[1;37m\]>\[\e[0m\] '

更优雅的生成方式

上述内容在使用上没有任何问题,但是在维护上,问题很大。更好的办法是使用脚本生成PS1语句,这样会有更高的可维护性。

通过对各种颜色的显示进行封装,每部分单独处理,最后拼接到一起,这样可以使得项目维护性更高。

下面的代码可以在source后,在当前终端配置PS1,同时会输出一份可以复制的脚本,可以粘贴到.bashrc中,以便后续自动生成

fg_color() {
    case "$1" in
        black)          echo 30;;
        red)            echo 31;;
        green)          echo 32;;
        yellow)         echo 33;;
        blue)           echo 34;;
        magenta)        echo 35;;
        cyan)           echo 36;;
        white)          echo 37;;
        lightBlack)     echo 90;;
        lightRed)       echo 91;;
        lightGreen)     echo 92;;
        lightYellow)    echo 93;;
        lightBlue)      echo 94;;
        lightMagenta)   echo 95;;
        lightCyan)      echo 96;;
        lightWhite)     echo 97;;
        orange)         echo 38\;5\;166;;
        *)              echo $1;;
    esac
}

bg_color() {
    case "$1" in
        black)          echo 40;;
        red)            echo 41;;
        green)          echo 42;;
        yellow)         echo 43;;
        blue)           echo 44;;
        magenta)        echo 45;;
        cyan)           echo 46;;
        white)          echo 47;;
        orange)         echo 48\;5\;166;;
        lightBlack)     echo 100;;
        lightRed)       echo 101;;
        lightGreen)     echo 102;;
        lightYellow)    echo 103;;
        lightBlue)      echo 104;;
        lightMagenta)   echo 105;;
        lightCyan)      echo 106;;
        lightWhite)     echo 107;;
        *)              echo $1;;
    esac;
}

text_effect() {
    case "$1" in
        reset)      echo 0;;
        bold)       echo 1;;
        weak)       echo 2;;
        italic)     echo 3;;
        underline)  echo 4;;
        blink)      echo 5;;
        quickBlink) echo 6;;
        reverse)    echo 7;;
        hide)       echo 8;;
        del)        echo 9;;
        *)          echo $1
    esac
}

color() {
    font=$(text_effect $1)
    fg=$(fg_color $2)
    bg=$(bg_color $3)

    code=""
    first=0
    for c in $font $fg $bg
    do 
        if [[ $c -ne "" ]]; then
            if [[ $first -ne 0 ]]; then
                code+=";"
            fi
            code+="$c"
            first=1
        fi
    done

    
    text=$4

    echo "\[\e[${code}m\]${text}\[\e[0m\]"
}


# export PS1='`a=$?; if [ $a -eq 0 ]; then echo -ne "\[\e[32;1m\]\$\[\e[0m\]"; else echo -ne "\[\e[31;1m\]\$\[\e[0m\]"; fi` \[\e[35;1m\]\t\[\e[0m\] \[\e[1;41m\]▶\[\e[0m\] \[\e[1;41m\]\u@\H:\w\[\e[0m\]  \[\e[1;41m\]▶\[\e[0m\]\$?'

prefix_ok="$(color bold green "" \$)"
prefix_fail="$(color bold red "" \$)"

prefix='$(if [ $? -eq 0 ]; then echo -ne '"\"${prefix_ok}\""'; else echo -ne '"\"${prefix_fail}\""'; fi)'
tm="$(color "" lightMagenta "" \\t)"
host="$(color "" lightCyan "" \\H)"
user="$(color "" yellow "" \\u)"
dir="$(color "" cyan "" \\w)"

git='$(branch=$(git status 2>/dev/null | head -n 1 | cut -d " " -f 3 2>/dev/null);if [[ -n ${branch} ]]; then echo -ne "@\[\e[1;91m\]${branch}\[\e[0m\]";fi)'

P="${prefix} ${tm} ${user}@${host}:${dir}${git} $(color bold white "" \>) "

echo "export PS1='${P}'"
echo "export PS1='${P}'" > theme.bash
export PS1=$P

参考资料