pondělí 18. ledna 2021

Jak si přizpůsobit prompt a barvy v terminálu

Dnes něco, co jistě každý zná přinejmenším zběžně, ale jsou jisté detaily, které si můžeme upřesnit. Když spustíte terminál, zobrazí se vám výzva k zadání příkazu, tedy prompt. Prompt Bashe, který interpretuje zadané příkazy v terminálu Ubuntu, se v základu skládá ze jména uživatele, hostname, cesty k aktuálnímu pracovnímu adresáři a končí $, nebo # pro rozlišení terminálu běžného uživatele a roota. To ale není pevně dáno, jako prompt můžete zobrazovat i další užitečné informace, barvit je, spouštět příkazy, jejichž výstup se zobrazí, nebo ovlivní prompt, nebo provede nějakou jinou akci. Co si tam třeba dát hodiny, místo barvení promptu obarvit zadávané příkazy, či část promptu barvit podle návratové hodnoty předchozího spuštěného příkazu?


Jak bude prompt vypadat, řídí následující proměnné, jejich obsah se expanduje (krom PS3) a výstupní řetězec použije jako prompt, v různých situacích:

  • PS1 - základní podoba promptu na řádku terminálu, čekající na zadání příkazu.
  • PS2 - sekundární prompt, použije se v případě rozdělení příkazu na každém dalším řádku, výchozím je znak ">".
  • PS3 - používá se jako prompt příkazu select (interaktivní menu, součást BASHe) nemá výchozí hodnotu, select jako výchozí zobrazuje znaky "#?". Nepodporuje expanzi, escape sekvence, ani speciální proměnné, jako ostatní PS, co tam bude, to přesně zobrazí.
  • PS4 - používá se v debug režimu na začátku každého řádku s právě prováděným příkazem a opakováním prvního znaku indikuje hloubku expanze. Tento debug režim v BASHi aktivujete mapř. pomocí příkazu set -x (vypnete ho set +x), nebo spuštěním skriptu příkazem bash -x <cesta/ke/skriptu>.
  • PS0 - obsah se expanduje a zobrazí po tom, co příkaz v terminálu odentrujete (pokud je již celý a nebude na dalším řádku čekat na doplnění s PS2 promptem), ještě před jeho spuštěním. Ale pozor, neprovádí se pokud odentrujete prázdný řádek. Výchozí obsah je prázdný. 
  • PROMPT_COMMAND - může obsahovat příkaz/y, které se provedou před tím, než se zobrazí PS1 prompt. Neměla by se používat pro tisk znaků, ale je to možné. Já například tuto proměnnou plním příkazem history -a, čímž se před každým zobrazením promptu zapíše neuložená historie terminálu do souboru ~/.bash_history a nemůže se mi tak stát, že mi terminál spadne bez uložení své historie, kterou jinak drží v paměti a zapisuje až při jeho ukončení.

Pokud se chcete podívat, jak vypadá zápis vašeho aktuálního promptu, můžete si jednoduše vypsat danou proměnnou:

echo $PS1
${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$

Ta debian_chroot část je jen interpretace výstupu předchozího skriptu v ~/.bashrc a umožňuje vám identifikovat, zda se nacházíte v chrootovaném systému a jakém.

Zde může být zajímavá ta konstrukce:

${debian_chroot:+($debian_chroot)}

Bash toto interpretuje tak, že se podívá, zda má proměnná (v tomto případě debian_chroot) nějaký obsah a pokud ano, vypíše to, co se nachází za znakem +. Pokud bude prázdná, nevypíše nic. Místo + můžete použít - ,budete-li chtít zobrazit v případě existence obsahu proměnné onen obsah, nebo v opačném případě to, co je za -.

Dále jsou použity řídící znaky \u pro uživatele, \h pro hostname, \w pro aktuální pracovní adresář a sekvence číselných kódů začínající \033 pak barví a stylizuje jednotlivé části promptu, význam viz dále. Terminál Ubuntu již nějakou dobu používá pro mě ne extra přitažlivou kombinaci barviček (barva pozadí ale není dána promptem, nýbrž nastavením terminálu), vypadá to zhruba takto:

gdh@gdh:~/.local$

V PS proměnných můžete použít celou řadu dalších řídících znaků:

\e    ASCII escape znak (033), lze používat přímo \033
\u    jméno uživatele
\!    pořadí příkazu v historii
\#    pořadí příkazu aktuální konzole
\H    FDQN hostname
\h    část hostname před první "." (tečkou)
\n    nový řádek
\w    jméno aktuálního adresáře
\W    jméno aktuálního adresáře, ale bez cesty k němu
\a    systémový zvonek (příkaz beep)
\d    datum ve formátu den_v_týdnu měsíc den_v_měsíci (TT MMM DD)
\l    číslo konzole
\r    návrat kurzoru na začátek řádku
\s    jméno příkazového interpretu (basename $0)
\t    aktuální čas HH:MM:SS 24 hodin formát (čas v době interpretace)
\t    čas HH:MM:SS 12 hodin formát
\A    aktuální čas HH:MM:SS 24 hodin formát
\@    aktuální čas HH:MM 12 hodin formát
\D{formát}  časový údaj formátovaný pomocí strftime. Pokud formát vynecháte (složené závorky být musí), použije se formát času podle lokálního nastavení
\v    verze Bashe
\V    verze Bashe + patch level
\\    zpětné lomítko
\[    začátek speciální sekvence netisknutelných znaků
\]    konec speciální sekvence netisknutelných znaků

Krom těchto znaků můžete použít výstup z jakéhokoli skriptu, či programu, nebo sekvence příkazů, jen to prostě z promptu zavoláte, stejně jako když výstup přiřazujete proměné, tedy příkazy obalíte `` (apostrofy), nebo vložíte do $().

Barvy a styly

Základní metodou definování barev a stylů v prostředí terminálu jsou sekvence ANSI escape sekvence s SGR (Set Graphic Rendition) kódy. Jako obvykle je to pěkně zdokumentováno na Arch Wiki. Formát zápisu je následující:

\[\033[<kód1>;<kód2>;...;<kódX>m\]

V BASHi můžete \033 nahradit jednodušším \e. To m na konci určuje, že jde právě o sekvenci zmíněných SGR kódů. Některé kódy potřebují i vstupní argumenty, ty se v sekvenci nijak nerozlišují, píší se za sebou stejným způsobem, k tomu se ale dostanu dále.

Barvy popředí, tedy písma, pak můžete vybírat z následující základní nabídky:

30   Černá
31   Červená
32   Zelená
33   Žlutá
34   Modrá
35   Fialová
36   Tyrkysová
37   Světle šedá

Barvy pozadí jsou to samé +10, tedy začínají 4 místo 3.
Dále můžete použít světlé odstíny těchto barev, ty začínají pro popředí 9 a pro pozadí 10, tedy 91,92,... a 101,102,.. . Světlá šedá (97/107) odpovídá bílé.

Styly můžete využívat následující:

01    Tučné       (21 pro ukončení)
02    Tmavší      (22 pro ukončení)
03    Kurziva     (23 pro ukončení)
04    Podtržené   (24 pro ukončení)
05    Blikající   (25 pro ukončení)
07    Inverze     (27 pro ukončení)
08    Neviditelné (28 pro ukončení)
09    Přeškrtnuté (29 pro ukončení)

00    Resetuje všechny atributy

Rozšíření nabídky barev

Z repertoáru SGR kódů můžeme výběr barev rozšířit (pokud grafika a terminál podporuje, což je dnes na běžném desktopu standard) na 8-bit paletu, tedy 256 odstínů, nebo rovnou na 24-bit RGB paletu s něco přes 16 milióny odstínů.

8-bit paleta

Pro výběr barvy z této palety pořebujete použít kódy, pro popředí:

\[\033[38;5;<barva 0-255>m\]

pro pozadí:

\[\033[48;5;<barva 0-255>m\]

Přičemž 0-15 jsou základní barvy jako v předchozích případech 3x,4x,9x,10x,

16-231 - barevná paleta od černé, přes modrou, po žlutou a bílou

232-255 - odstíny šedé

Zde právě kód 38/48 potřebuje další parametry, v tomto případě 5, určující, že půjde o barvu z 8-bit tabulky, a číslo barvy.

24-bit paleta

Popředí:

\[\033[38;2;<R>;<G>;<B>m\]

Pozadí:

\[\033[48;2;<R>;<G>;<B>m\]

Dvojka opět určuje způsob zadání barvy, přičemž R,G,B jsou hodnoty z rozsahu 0-255 pro červenou, zelenou a modrou složku. RGB kódy barvy si případně můžete vygenerovat v nějakém gragickém editoru, nebo i online nástroji.

Zadávání escape sekvencí příkazem tput

Další možností barvení promptu je program tput, který umí vracet hodnoty z terminfo databáze, jenž popisuje možnosti terminálu. Ta ale nemusí obsahovat úplně vše, co terminál skutečně podporuje. Obsah terminfo databáze si můžete vypsat příkazem:

infocmp

Najdete tam jména vlastností a přiřazené escape sekvence.  tput tak umí vygenerovat výše popsané ANSI escape sekvence trochu lidštější syntaxí. Z PS proměnných lze tput spouštět podle potřeby, místo přímého zadávání escape sekvencí.

Příklady použití:

Možnosti barev (značení barev jako u escape sekvencí výš):

tput setab [0-7] – Nastaví barvu pozadí
tput setaf [0-7] – Nastaví barvu popředí

Možnosti stylu:

tput bold  – tučné
tput dim   – tmavší
tput smul  – začátek podtrženého textu
tput rmul  – ukončení podtrženého textu
tput rev   – inverze
tput smso  – inverze, stejné jako rev
tput rmso  – ukončení inverze
tput sgr0  – resetuje všechny nastavené atributy

Definice PS proměnných

Jak si tedy nastavit s výše nabytými vědomostmi prompt podle svých potřeb. Předně je si třeba uvědomit, jak BASH zachází s řetězci a expanduje je. Součástí kódu promptu mohou být i příkazy a proměnné, které chcete expandovat (provést) až ve chvíli, kdy se prompt zobrazuje, aby byly aktuální. Pak musíte escapovat i speciální znaky BASHe ($ ` \ atd.), nebo výraz uzavřít do jednoduchých uvozovek. 

Současně je naopak vhodné některé příkazy a proměnné expandovat hned, aby pak zbytečně nezdržovaly a nezatěžovaly systém, když je jejich výstup vždy stejný, jako třeba sekvence z tput.

Příklad - zobrazení času vykonání příkazu v promptu

Takový nápad mě napad, aby to nebylo tisíckrát viděné jinde. Občas optimalizuji skript a měřím čas běhu pomocí time. Což tak nechat měřit čas přímo prompt? Úvaha je taková, že pomocí PS0 si uložím čas spuštění příkazu v milisekundách a PS1 mi pak ten čas odečte od aktuálního a vypíše do řádku. Čas si musím někde ukládat, proměnná je mimo hru, ta by výstup ze subshellu nepřežila, tak použiji soubor, a aby to bylo rychlé, tak to chce soubor v RAM. Já se spokojím s adresářem /dev/shm, který je součástí systému, je v RAM, je určen pro výměnu dat mezi aplikacemi, jen s jednou drobnou nevýhodou - může být při nedostatku paměti odswapován. Nicméně, pro účely tohoto použití a s téměř nulovou velikostí použitých souborů to nemá žádný efekt a nemá cenu ani soubory mazat, stejně nepřežijí restart systému. Protože při odklepnutí prázdného řádku nedojde k interpretaci PS0, a tudíž zapsání nového času, tak se v tomto případě zobrazí čas od posledního spuštění příkazu, nikoliv od stisku Enteru do promptu - to nestojí ya to řešit.

Nejprve jak získat časové razítko. Příkaz date umí zobrazit vteřiny od počátku času (1.1.1970 UTC) a k tomu zbývající nanosekundy. Milisekundy bych tedy měl získat následovně:

date +"%s%3N"

%s je formátovací znak pro čas v sekundách a %3N pro nanosekundy zaokrouhlené na 3 místa, tedy výsledkem jsou milisekundy, které přilepím k sekundám a tím dostanu aktuální čas v ms.

Další komplikací, pokud budu chtít tento nový prompt využívat na více terminálech, je potřeba ukládat časové razítko pro každý terminál zvlášť, tedy pro každý vytvořit unikátní jméno souboru. Jednodušší způsob, než použití příkazu tty a přidání jména/čísla terminálu/konzole do jména souboru mě nenapadá.

Zplodil jsem tedy následující, samozřejmě s barvičkami:

PS0='$(t=$(tty);echo $(date +"%s%3N") > /dev/shm/${t##*/}ptt)\[\e[0m\]'
PS1='\[\e[m\]$( [[ "$?" -ne 0 ]] && echo "\[\e[91m\]")$(($(date +"%s%3N")-$(t=$(tty);cat /dev/shm/${t##*/}ptt))) ms^ \d \t \u@\h:\[\e[0m\]\[\e[1m\]\w\n\[\e[m\]\$ \[\e[1;92m\]'
PS2='\[\e[m\]>\[\e[1;92m\]'

Určitě jste si všimli, že jsem tam přidal ještě jednu klasickou vychytávku a to obarvení promptu (na červeno) v případě, že předchozí příkaz skončil s exit kódem jiným, než 0, tedy pravděpodobně selhal. Také jsem prompt rozdělil na dva řádky, abych psal příkazy od kraje, což se hodí, když je zobrazovaná cesta k pracovnímu adresáři delší a/nebo používám užší terminál.

Pro vyzkoušení stačí tyto příkazy postupně zadat do terminálu (v tomto pořadí, aby se vytvořil soubor s časem, než se bude číst), čímž se modifikuje prompt pouze dané instance shellu a po zavření terminálu zmizí, další terminál se otevře opět s promptem výchozím.

Na mém systému s Core i5-3450 na 3,16 GHz a RAM plnou webu, je běžná prodleva promptu cca 3-5 ms, mění se podle zátěže systému, není to jen kódem samotného promptu. Pokud měřím výkonnost nějakého programu, musím to dělat na řádově dalších časech, kde se taková hodnota ztratí.

Update 21.1.2021: Na ubuntím fóru mi uživatel/ka singularis nabídla lepší řešení, které eliminuje potřebu zapisovat čas do souboru. Chytře využívá funkci trap Bashe, která umožňuje definovat příkaz, jenž se spustí po tom, co proces obdrží definovaný signál.

PROMPT_TIMESTAMP=$(date +%s%3N)
trap 'PROMPT_TIMESTAMP=$(date +%s%3N)' SIGUSR2
PS0='$(kill -SIGUSR2 $$)\[\e[0m\]'
PS1='\[\e[m\]$( [[ "$?" -ne 0 ]] && echo "\[\e[91m\]")$(($(date +"%s%3N")-$PROMPT_TIMESTAMP)) ms^ \d \t \u@\h:\[\e[0m\]\[\e[1m\]\w\n\[\e[m\]\$ \[\e[1;92m\]'
PS2='\[\e[m\]>\[\e[1;92m\]'
 
Tento skript nasourcovaný do vašeho aktuálního terminálu nastaví proměnnou PROMPT_TIMESTAMP s časovým razítkem pro první aplikaci PS1, abz nedošlo k počítání s prázdným řetězcem. Následně definuje trap funkci na obnovení časového razítka v proměnné po obdržení signálu SIGUSR2. Obsah PS0 určuje, že při svém provedení, před spuštěním příkazu edentrovaného z příkazového řádku, pošle pomocí příkazu kill zmíněný signál, který vyvolá spuštění trap funkce a která aktualizuje proměnnou s časovým razítkem. Protože se přiřazení do proměnné děje stále pod tím samým shellem, je aktualizovaná proměnná k dispozici i kódu prováděném z PS1, jenž spočítá rozdíl mezi razítky a vytiskne výsledek do promptu. Není to zásadně rychlejší řešení ale definitivně elegantnější. a bez odpadu v souborovém systému.

Prompt podle potřeby aktivovaný příkazem

Promptů si můžete vytvořit více a používat je podle potřeby, já jsem si např. vytvořil skript jménem .timeprompt s definicí proměnných a do souboru ~/.bash_aliases si přidal alias:

alias tpt=". ~/.timeprompt"

který zajistí, že se skript po použití aliasu nebude klasicky spouštět, ale pomocí příkazu . (tečka) se provede v rámci aktuálního shellu, aby se proměnné změnily v něm a ne v subshellu.

Trvalé nastavení modifikace promptu

Výchozí nastavení promptu uživatele najdete v souboru:

~/.bashrc

root má samozřejmě svůj v

/root/.bashrc

Najdete to někde kolem řádku 60. Buď můžete svou definici proměnných PS zapsat do tohoto souboru, nebo lépe využít soubor

~/.bash_aliases

který se načítá z .bashrc souboru až po definici promptu, takže ho tu můžete přepsat a držet si to spolu s aliasy odděleně.

Žádné komentáře:

Okomentovat

Zkuste prosím při komentováni používat místo volby Anonymní volbu Název/adresa URL, kde vyplníte nějakou přezdívku, adresu zadávat nemusíte. Vědět, které příspěvky jsou od jednoho člověka, je fajn. Díky.

Pokud by se vám náhodou odeslaný komentář na stránce nezobrazil, vytáhnu ho z koše hned jak si toho všimnu. I Google spam filter se občas sekne.