Customizando o Prompt de Comando (Bash e Zsh)

Olá a todos. Gostaria de compartilhar uns estudos que tenho feito nos últimos dias a respeito do prompt de comando, também conhecido com PS1.

Contexto

Eu estava usando o zshrc do GRML no Arch Linux há mais de um ano. Para quem não sabe, o sistema operacional do GRML (base Debian) tem uma configuração pro z-shell que trás a tona muitas funcionalidades avançadas e produz um visu legal. É triste comparar ele com os temas do Oh-My-Zshell pois ele é muito melhor mantido, tem uma excelente documentação, estável com pequenas atualizações, pode rodar com versões de zsh bem antigas, tem uma longa história e não tende a dar problemas…

Durante esse tempo, tenho estudo o z-shell e tenho ficado mais familiar com suas funções. Muito man page, guia do zsh (escrito por Paul Falstad), lista de e-mails e comp.unix.shell.

Depois de estudar o /etc/zshrc de 4000 linhas do GRML instalado aqui várias vezes (do pacote grml-config no Arch), decidi encarar e tentar me livrar dele.
Muitas funcionalidades eu nunca usei, outras são específicas para o GRML e o Debian e, vamos falar a verdade, é um rc enorme… Além de limpar a linguiça, o desafio me ajudaria a aprender as funções dessa excelente shell.

O que eu não esperava é ter tido tanta diversão ao configurar os meus prompts, então é disso que vamos falar aqui.

Prompts de comando

prompt expedição
PS0 quando a linha de comando é lida e antes da execução
PS1 primária, no começo da linha de comando e geralmente # quando o usuário é privilegiado e $ do contrário
RPS1 primária, expedido à direita da linha (Zsh)
PS2 quando é detectado que a edição comando de múltiplas linhas ainda não terminou, ex definir uma função na linha de comando
RPS2 quando o prompt PS2 é expedido, fica a direita da linha (Zsh)
PS3 quando o loop select é acionado e espera por uma resposta do usuário
PS4 quando a shell é configurada a opção XTRACE de debug, por exemplo set -x
SPROMPT quando forem sugeridas correções de grafia (Zsh)

Dentre os prompts, os mais fáceis de se configurar são o PS2 ao PS4.

Bash

Há várias formas de setar o PS1 tanto no bash quanto no zsh e isso pode confundir a pessoa que está tentando aprender como sofisticar seus prompts.

$COMMAND_PROMPT

Essa variável é como um hook (gancho) do Bash. Nela, pode-se inserir um comando que será executado toda vez após a linha de comando ser executada. Alternativamente, pode-se promover essa variável a um array em que cada elemento será um comando executado em série. Para um exemplo de como usar o $COMMAND_PROMPT, veja a seção de adendo deste tutorial.

Muitas pessoas, erroneamente, utilizam o $COMMAND_PROMPT para configurar o seu PS1 porém há muitas outras maneiras de conseguir informações variáveis em uma string de PS1 fixa!

$PS1

Basta configurar a variável PS1 uma única vez com alguns caracteres especiais
para que a informação seja atualizada. Os caracteres especiais no Bash são
escapados com uma barra invertida \, como \H para o nome do hóspede, \w o nome do diretório atual, \! o número do histórico do comando atual (para o povo que roda histórico de bang) e \$ insere # se o usuário tiver privilégios ou $ ao contrário. Veja a seção PROMPTING em man bash para a lista completa de caracteres especiais.

Por exemplo, eu uso muito o gerenciador de arquivos vifm e acabo entrando na shell em vários diretórios diferentes dentro do vifm e acabo esquecendo isso e abro o vifm novamente até que começo a ter problemas pois estou em uma subshell muito elevada dentro… Portanto, é interessante um aviso para saber quando eu estiver em um nível de subshell maior que 2. Note que o nível padrão de subshell vai depender muito do sistema de janelas que estiver usando, e poderá ser que depois de logado na sua área de trabalho do X, esteja em um subnível de shell já maior que 1, e portanto precisa considerar esse fato e configurar o nível de acordo.

Para isso, podemos verificar a variável de ambiente $SHLVL e se ela for maior que 2 (ou o valor que seja adequado na sua configuração), adicionar um sinal de positivo “+” ao prompt. É importante neste exemplo entendermos que essa verificação só é necessária uma única vez e não precisamos verificá-la novamente toda vez que o prompt for expedido.

Para sermos um pouco mais didáticos, vamos fazer a verificação e, caso verdadeiro, definir uma variável. Essa variável fará parte da definição do $PS1 e se estiver vazia não ocupará nenhum espaço.

prompt_ssl_max=2
prompt_ssl=$( ((SHLVL>prompt_ssl_max)) && echo "+" )

PS1="${prompt_ssl}\$"

Até aqui tudo comum, só substituímos $prompt_ssl na definição do PS1 uma única vez.

É importante entendermos que uma vez que $PS1 foi definida, ela passará por expansão dos caracteres especiais e de variáveis uma vez antes da expedição do prompt. Assim, podemos aproveitar essa regra para incluirmos o nome de uma variável em $PS1 que não será expandida na sua definição, mas somente quando $PS1 for expandida antes da expedição do prompt!

Agora poderemos definir algumas outras variáveis com comandos que serão substituídas por uma (outra) substituição de comando toda vez que o prompt for expedido. Para isso, então, devemos escapar essas variáveis especiais de acordo para que elas não sejam prontamente substituídas na mesma hora da definição da variável $PS1.

Por exemplo, para emitirmos o código de saída da última linha de comando executada quando for maior que 0, vamos checar a variável de ambiente $? e só imprimir o código de saída se a condição for encontrada. Neste caso, vamos escapar com aspas simples todo o comando ou, se quisermos incluir códigos de cores, deveremos escapar especificamente a variável $?, assim como o cifrão da sintaxe de substituição de comando $() e as aspas duplas do comando echo.

prompt_exit='$( ((e=$? , e)) && echo "${e} " )'

PS1="${prompt_exit}\$"

Ou substituindo variáveis com código de cores:

endc='\[\033[00m\]'
bred='\[\033[01;31m\]'
prompt_exit="\$( ((e=\$? , e)) && echo \"${bred}\${e}${endc} \" )"

Uma outra variável especial que poderia ser implementada seria para mostrar a quantidade de e-mails não lidos, mas isso depende se o seu programa de e-mails disponibiliza um arquivo com esse número para poder ser lido…

Cores

No Bash, pode-se escrever os códigos de cores manualmente no $PS1 porém isso vai acabar deixando a string difícil de ler ou usar variáveis, como nos exemplos acima, porém atenção na hora de fazer os escapes pois essas sequencias de cores precisam ser substituídas nas strings o quanto antes.

Repare que o Bash precisa saber que as sequências de caracteres para cores, por exemplo, não vão ocupar espaço no prompt, e portanto devem estar contidas em \[ e \].

Truncando elementos

Com isso, falta sabermos de uma outra variável de ambiente que o Bash lê chamada $PROMPT_DIRTRIM que controla o truncamento do caminho do diretório de \w e \W. Assim, se o caminho para o diretório atual for muito longo, pode-se configurar quantos componentes mostrar antes da notação ‘…’. No exemplo a seguir, configuramos essa variável para 3, assim ~/Downloads/dir/subdir1/subdir2 será encurtado para .../subdir1/subdir2.

Prompt completo

# ~/.bashrc

#prompt do PS1
#cores
endc='\[\033[00m\]'
bwhite='\[\033[01;37;40m\]'
bcyan='\[\033[01;36m\]'
bgreen='\[\033[01;32m\]'
bpink='\[\033[01;35m\]'
bred='\[\033[01;31m\]'
yellow='\[\033[00;33m\]'

#usuário comum
c0="$bgreen"
#usuário raiz
((EUID)) || c0="$bpink"

#widgets (escape ninja pode ser necessário)
#checar nível de subshell (só precisa drodar uma única vez)
prompt_ssl_max=2
prompt_ssl=$( ((SHLVL>prompt_ssl_max)) && echo "${yellow}+${endc}" )

#integração básica com git (imprime nome do ramo)
prompt_git="\$( branch=\$(git rev-parse --abbrev-ref HEAD 2>/dev/null) && echo \"[${yellow}\${branch}${endc}] \" )"

#checar código de saída da última linha de comando
prompt_exit="\$( ((e=\$? , e)) && echo \"${bred}\${e}${endc} \" )"

#número de componentes de diretório para reter em \w e \W
PROMPT_DIRTRIM=3

#setar a string do ps1
PS1="${prompt_exit}${c0}\u${bwhite} \h:${bcyan}\w${endc} ${prompt_git}(\!)${prompt_ssl}\$ "

#manter o ambiente limpo
unset end bwhite bcyan bgreen bpink bred yellow c0 prompt_ssl_max prompt_ssl prompt_git prompt_exit


Figura 1. Prompt do Bash.

Zsh

precmd() e $precmd_functions

Similarmente ao $COMMAND_PROMPT do Bash, o array $precmd_functions ou a função especial precmd() podem ser utilizadas para o mesmo fim.

Da mesma forma não devem ser utilizados para configurar o PS1 a não ser que estiver fazendo algo muito diferente com o seu PS1, mas explanarei brevemente sobre eles aqui.

A função precmd() é um hook do Zsh, ou seja, uma vez definida essa função será executada toda vez antes da expedição do prompt de comando PS1. Assim, pode-se simplesmente definir a função de precmd() que ela será executada antes de todos outros hooks.

function precmd ()
{
	vcs_info
}

Em casos mais específicos, pode-se definir o array psvars e usar os valores do índice do array com %v dentro do PS1, por exemplo se psvars=($? exemplo) for definido como primeiro comando dentro da função precmd(), %1v terá o mesmo valor que %?, e %2v terá o valor de exemplo.

Em configurações muito complexas, pode ser que seja necessário adicionar novos hooks e neste caso pode-se utilizar a função do add-zsh-hook (carregada com autoload -Uz add-zsh-hook) que não descreverei aqui.

Para nosso caso, se precisarmos de mais de uma única função de hook, podemos incluí-las em um array chamado $precmd_functions que elas serão executadas depois da função precmd() (se houver definida), em série.

$PS1

O Zsh usa alguns parâmetros especiais com sinal de porcentagem que são substituídos similarmente aos caracteres especiais do Bash, e podem ainda conter um número.

O Zsh tem uma quantidade muito grande de parâmetros especiais de prompt, por exemplo %h substitue o número do comando no histórico, %~ o nome do diretório atual com sinal de ~ para home, %# imprime # caso seja o superusuário ou % do contrário equivalente a %(!.#.%%) e etc. Veja todos na seção SIMPLE PROMPT ESCAPES em man zshmisc.

O Zsh ainda oferece uma contrução de expressões ternária em parênteses que deixam as coisas mais divertidas.

Por exemplo, podemos verificar o nível de subshell com %L. Em uma expressão ternária a seguir, checamos se o subnível for no mínimo 2, então imprimimos um sinal de +, do contrário vazio:

PS1='%(2L.+.)%# '

Para imprimir o código de saída da última linha de comando se for maior que 0:

PS1='%(?..(%?%))%# '

Diferentemente do Bash, o $PS1 do Zsh não sofre substituição de parâmetros comuns por padrão (somente parâmetros de porcentagem).

Integração básica com git (VCS)

Existe um módulo do Zsh que pode ser usado para identificar diretórios que sigam os protocolos de controle de versões (VCS), como o git.
O módulo, depois de carregado, irá definir uma variável $vcs_info_msg_0_ de acordo com os estilos configurados e poderá ser substituída em PS1 ligando a opção de shell PROMPT_SUBST.

Apesar deste módulo ser um pouco difícil de se trabalhar, achei na própria documentação do módulo um exemplo de código para integração básica (exibe o nome do protocolo e ramo).

Como já dito, precisaremos ligar a opção de shell PROMPT_SUBST e também configurar um hook de precmd para que o módulo do zsh sempre possa atualizar a variável $vcs_info_msg_0_ após cada comando. Note que $vcs_info_msg_0_ deve ser escapado para que não sofra substituição assim que a string de $PS1 for definida!

#informação de controle de versões
autoload -Uz vcs_info
zstyle ':vcs_info:*' actionformats '%F{5}(%f%s%F{5})%F{3}-%F{5}[%F{2}%b%F{3}|%F{1}%a%F{5}]%f '
zstyle ':vcs_info:*' formats '%F{5}(%f%s%F{5})%F{3}-%F{5}[%F{2}%b%F{5}]%f '
zstyle ':vcs_info:(sv[nk]|bzr):*' branchformat '%b%F{1}:%F{3}%r'

#armar o hook do módulo de vcs
precmd() { vcs_info }

PS1="\${vcs_info_msg_0_}%# "

RPS1

Prompt PS1 do lado direito é automaticamente removido quando a digitação chega perto dele.

Se os códigos de escape de cores não tiver sido feito corretamente (delimitando-os com %{ e %}), poderá estar fora da margem da linha!

Aqui vou copiar o $RPS1 do GRML descaradamente. Caso a saída da linhha de comando anterior for maior que 0, desenhar uma carinha triste.

Cores

Há três maneiras de configurar cores no zsh. Primeira maneira é usando os códigos de cores manualmente; segunda é carregando e o módulo e rodando a função de colors() do módulo de cores (autoload colors && colors) e utilizar os arrays de cores; terceira o método padrão e mais arcaico de definição de cores de prompt com parâmetros de porcentagem.

Se for utilizar os códigos de cores ou os arrays de cores, é necessário escapar os códigos com %{ e %} para o zsh entender que esses caracteres não utilizam espaço no prompt (são somente caracteres especiais), caso contrário o zle poderá ter dificuldade em redesenhar a linha de comando em algumas situações.

O método padrão entende nomes das oito cores primárias, por exemplo $F{red}, ou o número de uma cor %F{2} e também aceita os efeitos visuais, como %B (começar negrito) e %b (terminar negrito).

Recomendo usar o método padrão, mas pode-se escolher o que for mais fácil visto que o módulo colors está disponível no zsh há muito tempo também…

Truncando elementos

O zsh usa uma notação do tipo %10<...<%~, em que %<< é a forma básica de truncamento. O 10 depois de % diz que qualquer coisa que se seguir é limitado a 10 caracteres e os caracteres ... são impressos sempre quando o prompt fosse mais longo de outra maneira.

Pode-se desligar o truncamento com %<<. Mudando o sinal de < para >, ele trunca para o outro lado.

Prompt completo

Abaixo, vamos montar um prompt PS1 completo:

# ~/.zshrc

#Prompting
autoload -U promptinit
promptinit
#prompt oliver
setopt PROMPT_SUBST

#configurar a função de informação de sistemas de controle de versões (CVS)
autoload -Uz vcs_info
zstyle ':vcs_info:*' actionformats '%F{5}(%f%s%F{5})%F{3}-%F{5}[%F{2}%b%F{3}|%F{1}%a%F{5}]%f '
zstyle ':vcs_info:*' formats '%F{5}(%f%s%F{5})%F{3}-%F{5}[%F{2}%b%F{5}]%f '
zstyle ':vcs_info:(sv[nk]|bzr):*' branchformat '%b%F{1}:%F{3}%r'

#comandos para rodar antes de expedir ps1
precmd() {
	vcs_info
}

#cores
#usuário raiz
c0=red
#usuário comum
c1=cyan

#subníveis de shell máximo
max_ssl=2

#setar ps1
PS1="%F{red}%B%(?..%? )%b%F{%(!.${c0}.${c1})}%n%F{white}@%m %40<...<%B%~%b%<< \${vcs_info_msg_0_}%f(%!)%F{yellow}%(${max_ssl}L.+.)%f%# "

#setar rps1
RPS1='%(?.%(t.Ding!.%D{%K:%M}).:()'

unset c0 c1 max_ssl


Figura 2. Prompt do Zsh.

Adendo: definindo título na barra da janela do XTerm

É uma ótima ideia colocar o diretório corrente na barra de título do terminal pois daí não teremos problemas caso o caminho do diretório seja muito longo, e não ocupará espaço no prompt de comando.

Como eu uso o Tmux, ele configura a barra de títulos para mim, mas aqui neste exemplo vamos definir uma função _set_xterm_title() e ela será adicionada ao $COMMAND_PROMPT para execução.

Porém, deixemos bem claro que isso também poderia ser feito com uma função customizada do comando cd facilmente (até mesmo preferível). Além disso, se estiver usando o Tmux, ele já tem uma opção para setar o título da janela do XTerm automaticamente set-titles.

# ~/.bashrc

#setar o título do XTerm para o diretório atual
_set_xterm_title()
{
	[[ -t 1 ]] || return
	[[ -z "$TMUX" ]] || return
	case $TERM in
		(sun-cmd)
			printf "\e]l%s\e\\" "${PWD/#$HOME/\~}"
			;;
		(*xterm*|rxvt|dtterm|kterm|Eterm)
			printf  "\e]2;%s\a" "${PWD/#$HOME/\~}"
			;;
	esac
}

#comandos para rodar antes da expedição do prompt
PROMPT_COMMAND=( _set_xterm_title )

A mudança de título no XTerm é feita através de sinais de escape, porém não queremos emití-los caso estejamos em um ambiente interativo (por isso o [[ -t 1 ]] é checado), e nem se estivermos dentro do Tmux.

No Zsh, há um módulo zftp titlebar que pode ser utilizado para mudar o título das janelas do XTerm ou, se estiver usando Tmux, verifique a opção set-title.

De qualquer forma, como exemplo no Zsh, basta definirmos a função especial chpwd() (que também é um hook) que ela será sempre executada quando o diretório mudar.

A sintaxe do print no zsh é um pouco diferente, também.

# ~/.zshrc

#set XTerm title
chpwd() {
	[[ -t 1 ]] || return
	case $TERM in
		(sun-cmd) print -Pn "\e]l%~\e\\"
		;;
		(*xterm*|rxvt|(dt|k|E)term) print -Pn "\e]2;%~\a"
		;;
	esac
}

Algumas referências

10 curtidas

Eu sinceramente prefiro a “maneira errada” de colocar informações variáveis no PS1. O amontado de escapes, necessidade ter em mente quando cada expansão ocorre e o uso de one-liners complexos dentro de $() prejudica bastante a legibilidade de código (e os dois vêm somados nesse caso).

Comparado com uma função no PROMPT_COMMAND, é possível usar local (evitando o unset gigante no final, que eu pessoalmente acho feio) e externalizar \$()s complexos para ifs na função (onde não há necessidade de escapes).

1 curtida

Pode ser, mas meramente reproduzi o que o Paul Falstad escreveu no Guia sobre não usar $precmd (ou $COMMAND_PROMPT por similaridade) para essas coisas básicas…

Por que na verdade, os caracteres especiais no bash (ou variáveis de porcentagem no zsh) foram desenvolvidas para serem usadas especificamente no prompt, para retornarem informações variáveis em uma string PS1 fixa!

Além do que é um desperdício usar o $COMMAND_PROMPT para redefinir o $PS1 toda hora, sendo que um PS1 simples é para definir somente uma vez (teoricamente)…

Concordo com você que a legibilidade da cadeia de caracteres do $PS1 pode ficar ilegível… Mas essa vontade de deixar tudo legível demais tem seu preço em termos de rapidez… Talvez não em um caso simples como esse do $PS1 mas com certeza no desenvolvimento de software sempre tem essas trocas…

Mas então queria saber qual sua opinião sobre expressões ternárias aninhadas? Acho elas bem interessantes e admiráveis pela simplicidade, mas são terrríveis de fazer debug. Algumas empresas até banem o uso delas por completo no desenvolvimento de código! Mas daí vai da forma que cada um usa a linguagem para se expressar, uns mais convolutos, uns mais diretos.


Por exemplo, agora eu estou achando melhor brincar um pouco com expressões ternárias e setar o RPS1 do Zsh da seguinte forma:

RPS1='%(?.%(t.Ding!.%D{%K:%M}).:()'

Há duas expressões ternárias aninhadas. A mais exterior que começa com %(? ou %(0? testa se a linha de comando saiu com código maior que 0. Quando sair com valor maior que zero, imprime um carinha triste :(. Se for igual a zero, vai testar a segunda expressão ternária aninhada, vai testar se %(t minutos é igual a 0 e vai imprimir Ding!, do contrário mostrará um horário em formato mais convencional…

Pode-se verificar as expansões dos parâmetros de porcentagem com a opção -P do print, assim:

print -P '%(?.%(t.Ding!.%D{%K:%M}).:()'

Tem mais um prompt que eu acabei de fazer. Inspirado na bandeira do Canada do Gerald Oskoboiny (link abaixo), que por sua vez diz ter se inpirado na bandeira da França de um tal Renaud, do qual não consigo achar traços na internet…
https://impressive.net/people/gerald/1998/10/19/bashrc-stupidity/bashrc-canada

Então eu bolei esse código para abandeira do Brasil… Se alguém tiver uma ideia melhor, queria saber!

#bash
PS1='\[\e[1;33;42m\]<\[\e[1;36;42m\]•\[\e[1;33;42m\]>\[\e[m\]\$ '
# zsh
PS1='%K{green}%F{yellow}%B<%F{blue}•%F{yellow}>%b%f%k%# '

⁕
*
•
1 curtida