neděle 16. února 2014

Bash - aliasy s automatickým doplňováním argumentů klávesou Tab

Konečně jsem se rozhodl udělat si nějaké ty vlastní aliasy pro práci v Bash terminálu. A hned jsem samozřejmě narazil na problém s automatickým doplňováním argumentů takovýchto výtvorů, bash-completion je totiž pochopitelně sám od sebe nezná. Je tedy třeba mu je představit a když už jsem řešil tohle, vzal jsem ty aliasy trochu podrobněji, je to další téma, o kterém se všechny články, které jsem našel, jen tak trochu otřou. ...



K čemu jsou aliasy dobré

Pro jistotu začnu od začátku. Příkazem alias si můžete nadefinovat zkratky pro delší příkazy, případně přerazit původní příkazy nějakou jejich jinou variantou. Je daleko rychlejší napsat například u, místo sudo apt-get update, i když se to většinou dá zjednodušit použitím historie - Ctrl+R a up mi většinou stačí. Jsou ale i zajímavější zkratky, klasicky třeba grepování procesů příkazem ps aux|grep -i něco. Často se mi stává, že to napíšu bez roury i grepu a hledaný výraz mastím rovnou. S aliasem psx je po problémech.

Definice aliasu vypadá tak, jak byste čekali, např.:
alias psx='ps aux|grep -i'

Alias má vždy přednost před spustitelnými soubory v adresářích v $PATH, proto je jím možné nahradit i stejnojmenný příkaz.

Samotný příkaz alias vám vypíše všechny aktuální aliasy, to se může hodit, když cokoliv zapomenete.

A je tu ještě jedna finta. Chcete-li použít alias, ale příkaz, kteý zastupuje potřebujete před spuštěním nějak modifikovat, můžete po jeho napsání do terminálu stisknout Ctrl+Alt+E a Bash vám ho na řádku okamžitě expanduje. Třeba to pro vás bude rychlejší, než to napsat celé normálně.

Aliasem může být jen první slovo příkazu, pokud nepřidáte mezeru

Bash normálně hledá aliasy jen pro první slovo příkazu a to rekurzivně, pokud se slovo neshoduje s nahrazovaným aliasem. Pokud ale na konec definice aliasu přidáte mezeru, bude Bash alias hledat i pro slovo následující. Viz můj dřívější zápisek sudo alias.. proč to ten alias najednou nevidí?

Aliasy nefungují ve skriptech

Výchozím chováním je fungování aliasů pouze v interaktivním režimu Bashe. Když si definujete alias se stejným jménem, jako má nějaký existující příkaz, který sice vůbec nepoužíváte samostatně, ale nějaký skript by ho používat mohl, nenastane žádná nechtěná záměna při běhu skriptů. I toto výchozí chování je možné změnit pomocí příkazu shopt -s expand_aliases přímo ve skriptu, kde je to třeba, obecně to ale je hloupý nápad.

Kam nemůže alias, nastrčíte funkci

V těle příkazu aliasu není možné použít argument, který aliasu dáte. To znamená, že pokud potřebujete dodaný argument použít někde uprostřed příkazu, alias vám je k ničemu. Pak je na pořadu dne stará dobrá funkce, v té si můžete číst argumenty sami a nacpat je kam je třeba. Funkce také nemusí mít jeden řádek, nacpete do ní celý skript. Jednoduchý příklad:

sshu (){ ssh -X $1@192.168.1.36; }

Místo $1 bude dosazen první argument zapsaný za sshu při pozdějším volání funkce.
U definice funkcí pozor na některé náležitosti - jednak musíte za první složenou závorku vložit mezeru, nebo nový řádek a za poslední příkaz zase středník, nebo nový řádek. Při definici můžete, ale nemusíte, použít klíčové slovo function.

Oproti aliasům jsou předem definované funkce běžně použitelné i ze skriptů, v Bashi se totiž dají exportovat pomocí export -f jméno_funkce, takže mohou být k dispozici i ve všech dalších instancích Bashe, které jsou odtud spuštěny, včetně těch neinteraktivních. Nebudou ale samozřejmě dostupné v Dashi (/bin/sh).

Kam aliasy a funkce zapsat, aby byly vždy k dispozici

Aliasy Bashe je možné zapsat přímo do souboru
~/.bashrc
ten se spouští s každou novou interaktivní instancí Bashe (prostě když si otevřete terminál) daného uživatele, ale ~/.bashrc v Ubuntu přímo podporuje aliasy v souboru
~/.bash_aliases
který, pokud existuje, automaticky sám načte.

Pokud mají být globální, pro všechny uživatele, tedy i pro roota, můžete využít soubor
/etc/bash.bashrc

Je tu i možnost využít souboru
/etc/skel/.bash_aliases
který se nakopíruje do $HOME každému nově založenému uživateli. Existující uživatele to tedy nijak neovlivní. Z tohoto adresáře se kopíruje do $HOME nového uživatele úplně všechno, takže sem můžete umístit i jiné soubory a adresářové struktury, které by měl mít každý uživatel v základní výbavě.

Aliasy můžete zapsat i do souborů ~/.profile (/etc/profile, /etc/profile.d/*), jejichž obsah je spouštěn pouze s login shellem a jsou určeny i pro systémový shell Dash (/bin/sh), což znamená, že tam nemůžete použít nic, co Dash neumí, to byste se pak nemuseli vůbec přihlásit do systému. Dash má omezení třeba v tom, že neumí exportovat funkce, takže je pak nemůžete použít jinde, než právě v login shellu, potomci je nezdědí. Můžete si ale vytvořit přímo soubor ~/.bash_profile, při jeho existenci ho Bash načte místo ~/.profile.

Pokud tedy pracujete v pseudoterminálu v grafickém prostředí, museli byste místo ~/.profile použít ~/.bash_profile, místo aliasů funkce a ty poctivě všechny exportovat. Využíváte-li klasickou konzoli (Ctrl+Alt+F[1-6]), nebo třeba ssh, může vám profile stačit.

Z nějakého podivného důvodu Bash v Debianu / Ubuntu nepoužívá něco jako /etc/bash.bashrc.d/, kam byste si mohli strkat skripty doplňující soubor /etc/bash.bashrc, tak jak to má /etc/profile. Je to to první, co mě napadlo hledat, ale fakt s tím není počítáno. Nejsem sám, koho to napadlo, nechápe to i řada jiných, kteří o tom diskutují na bugs.debian.org, například zde. Není samozřejmě takový problém si dopsat do bash.bashrc řádek a adresář vytvořit, ale není to zrovna systémové řešení, vzhledem k tomu, že se ten soubor může změnit při nějaké aktualizaci.

Jak naučit Bash doplňovat argumenty aliasů - bash-completion

Aliasy jsou super, ale jakmile si vyrobíte alias na příkaz, který žere nějaké ty argumenty, zjistíte, že přestalo fungovat jejich automatické doplňování pomocí klávesy Tab. Tohle doplňování je zajišťováno systémem funkcí bash-completion, které se původně umisťovaly všechny (pro každý příkaz vlastní soubor) do adresáře /etc/bash_completion.d/, ale s příchodem verze 2.0 (přinejmenším od Ubuntu 13.10) začal bash-completion používat pro výchozí sadu doplňovacích funkcí adresář /usr/share/bash-completion/. Ten v /etc/ je nadále funkční a některé programy ho pro své doplňovačky stále používají.

V aliasech běžně využíváme příkazy, které již tyto funkce mají definovány, stačí tedy napsat wrapper, který na ně nasměruje i nové aliasy. Tenhle wrapper už někdo napsal, takže ho můžeme využít. Zdroj zde.

# License: Whatever
# Wraps a completion function
# make-completion-wrapper <actual completion function> <name of new func.> <alias>
#                         <command name> <list supplied arguments>
# eg.
#  alias agi='apt-get install'
#  make-completion-wrapper _apt_get _apt_get_install apt-get install
# defines a function called _apt_get_install (that's $2) that will complete
# the 'agi' alias. (complete -F _apt_get_install agi)
#
function make-completion-wrapper () {
        local comp_function_name="$1"
        local function_name="$2"
        local alias_name="$3"
        local arg_count=$(($#-4))
        shift 3
        local args="$@"
        local function="
function $function_name {
        COMP_LINE=\"$@\${COMP_LINE#$alias_name}\"
        let COMP_POINT+=$((${#args}-${#alias_name}))
        ((COMP_CWORD+=$arg_count))
        COMP_WORDS=("$@" \"\${COMP_WORDS[@]:1}\")

        local cur words cword prev
        _get_comp_words_by_ref -n =: cur words cword prev
        $comp_function_name
        return 0
        }"
        eval "$function"
}


Tento skript si buď můžete uložit přímo do .bashrc, nebo lépe společně s vašimi aliasy do ~/.bash_aliases.

Díky tomuto wrapperu můžete definovat completion funkce pro své aliasy, jak je naznačeno na začátku skriptu. Důležité je, že musíte znát jméno completion funkce příkazu, který máte v aliasu, v zásadě to bývá jméno příkazu uvozené podtržítkem. Za běžných okolností se dá zjistit příkazem:

completion -p příkaz

Pokud vám to nic nevypíše, nepanikařte a podívejte se na další odstavec.
Definice aliasu s automatickým doplňováním tedy vypadá následovně:

alias i="sudo apt-get install"
make-completion-wrapper _apt_get _apt_get_install i apt-get install
complete -F _apt_get_install i

Argumenty funkce make-completion-wrapper jsou následující:
  1. jméno doplňovací funkce příkazu apt-get
  2. jméno doplňovací funkce pro váš alias (libovolné, běžně začínající podtržítkem, nesmí kolidovat s jinými příkazy)
  3. jméno aliasu
  4. celý příkaz, kterému je třeba argumenty doplňovat
A protože doplňování argumentů vůbec není vázáno na to, zda napsaný příkaz existuje, či nikoliv, není problém si napsat completion funkci i pro funkce.

Jak si napsat zcela novou completion funkci naleznete třeba v článku Petra Krčmáře na rootu (až na konci):
http://www.root.cz/clanky/bash-completion-inteligentni-doplnovani-prikazu/

Bash-completion načítá od verze 1.99 doplňovací funkce dynamicky, až při prvním použití

Na Ubuntu 13.10 a 14.04 jsem, oproti 12.04, narazil na to, že se completion funkce načítají dynamicky až tehdy, když jsou třeba, proto dojde k chybě, když se je snažíte použít pro alias před inicializací. Je to nová fíčura bash-completion od verze 1.99 a dává to smysl, není třeba brzdit start shellu načítám miliardy funkcí, z kterých většinu vůbec nepoužijete.

Ideálním řešením by bylo zkopírovat si potřebné funkce do nových completion souborů speciálně pro dané aliasy, ty by pak fungovaly nativně a také by se načítaly dynamicky. Také to ale můžete udělat po staru, že si prostě sourcnete potřebné completion skripty sami, v rámci definice vašich aliasů, stále to bude řádově rychlejší, než jak je to ve starších verzích, kde se tak dopředu načítalo všechno.

Soubor s  doplňovačkou pro konkrétní příkaz můžete vyhledat třeba takto:

locate -r "completion.*/příkaz$"

Například pro apt-get a apt-cache si k aliasům dopíšete:

. /usr/share/bash-completion/completions/apt-get
. /usr/share/bash-completion/completions/apt-cache

Pozn. - v Ubuntu 12.04 je ještě celý apt v jednom souboru, v nových verzích již takto zvlášť.

Pár mých nových aliasů

Krom zmíněného completion wrapperu mám momentálně v ~/.bash_aliases následující:

alias si="sudo -i"
alias loc="locate -i"
alias px="ps aux | egrep -i"
alias u="sudo apt-get update"
alias g="sudo apt-get dist-upgrade"

alias i="sudo apt-get install"
make-completion-wrapper _apt_get _apt_get_install i apt-get install
complete -F _apt_get_install i

alias ri="sudo apt-get install --reinstall"
make-completion-wrapper _apt_get _apt_get_reinstall ri apt-get install
complete -F _apt_get_reinstall ri

alias p="sudo apt-get purge"
make-completion-wrapper _apt_get _apt_get_purge p apt-get purge
complete -F _apt_get_purge p

alias r="sudo apt-get remove"
make-completion-wrapper _apt_get _apt_get_remove r apt-get remove
complete -F _apt_get_remove r

alias ac="apt-cache show"
make-completion-wrapper _apt_cache _apt_cache_show ac apt-cache show
complete -F _apt_cache_show ac

alias acs="apt-cache search"

alias ap="apt-cache policy"
make-completion-wrapper _apt_cache _apt_cache_policy ap apt-cache policy
complete -F _apt_cache_policy ap

alias dmr="sudo service lightdm restart"
alias nmr="sudo stop network-manager && sudo start network-manager"
alias lirc="sudo service lirc restart"
alias rb="sudo reboot"
alias sd="sudo shutdown now"
alias ds="du -Sm | sort -nr | less" # vypíše adresáře od aktuálního níž a seřadí je podle velikosti v MB
alias fatrace="sudo fatrace"


Pár kydů na konec

Doposud jsem si žádné aliasy nedefinoval, ze začátku jsem si říkal, že je lepší si zažít příkazy tak jak jsou, když pak nebudu u svého Bashe, bude to výhoda, nebudu muset nad ničím přemýšlet. Je tu také problém s volbou vhodného jména, které si člověk musí zapamatovat a zároveň by nemělo kolidovat s dalšími příkazy. Už to vidím, jak budu měsíc upravovat jména aliasů, aby to bylo co nejlepší, jen za ty čtyři dny jsem to změnil třikrát. Tak jsem si zvykl žít bez aliasů (jen ty ls varianty, co jsou přímo v Ubuntu, využívám) a psal ty romány stále dokola celé, maximálně jsem používal vyhledávání v historii. Po těch letech jsou ale příkazy, které už můžu zapomenout jedině díky opravdu velké ráně do hlavy s následným pobytem v komatu, a tak když jsem před pár dny na nějaký alias návod narazil, řekl jsem si, že si jich konečně taky pár udělám a uvidím. Nakonec, jsou příkazy komplikované, kvůli kterým se vždy musím dívat do manuálu a vymýšlet je znovu, nebo sem na blog, kde jsem je popsal a aliasy a funkce jsou praktickou nápovědou k okamžitému použití. Už teď mám v hlavě dalších pár kandidátů, které si postupně přidám, aby byly po ruce. Že mi to ale trvalo..
A klidně se podělte se svými aliasy, jsem zvědavej.

6 komentářů:

  1. Zdar, líbí se mi, jak píšeš, dobře se to čte, i když se jedná o návody nebo technické věci. Občas taky něco napíšu, tak vím, jak je to těžké a kolik to zabere času.

    bash_completion je super věc, ale hrozně neintuitivní na nastavení; tak před dvěma lety jsem se do toho ponořil, snad naučil, a teď jsem zjistil, že už si nepamatuju nic, tak do toho už nerýpu.

    aliasy jsou mnohem lepší, ale z těch, co jsem kdy používal (cca 20) ti zůstane 4-5, u kterých to má smysl, ty ostatní člověk zapomene, a taky se blbě hledají v historii. Kdysi jsem hodně používal
    alias logy='cd /var/log'
    a podobně, než jsem narazil na autojump, ten nemá chybu: pamatuje si, ve kterých adresářích jsi nejčastěji, a tam tě pošle, např.
    j log

    Dále používám na debugování češtiny v aplikacích
    alias cesky='LANGUAGE=cs_CZ:cs LANG=cs_CZ.utf8'

    výpis nejnovějších souborů v adresáři (ani nevím, proč to potřebuju, ale používám to často)
    alias new='ls -AlFtc | head'

    pak různé převodníky, např. hex→dec
    x() { echo $((0x$1)); }

    arrange

    OdpovědětSmazat
    Odpovědi
    1. Hola, fakt jsem rád, že se to dá číst, taková informace rozhodně potěší.
      Tak jsem nainstaloval autojump, myslíš, že jsem použil ten alias? A nakonec mám na to vlastně i extension v GNOME Shellu, takže mi stačí stisknout super místo aliasu. Teď jsem si na to vzpoměl..
      Myslím, že jestli mi něco vydrží, tak to budou ty základní grepovací aliasy, roury zdržujou a blbě je trefuju. Zvyknu si asi i na jednopísmenné apty.

      Ještě jsem dneska narazil na tohle:
      https://github.com/dvorka/hstr

      Smazat
    2. Já aliasy na roury taky používal, ale pak zjistil, že potřebuji něco trochu upravit, nebo si prostě nevzpomenu na tu zkratku, a jsem v pytli. Ty apty jsou myslím lepší nápad. Takže většinou stejně Ctrl+r a šup do historie.

      hstr jsem nainstaloval (tedy ručně ze zdroje, ppa mi hlásilo 404), a vypadá to perfektně, je to rychlé, tak uvidíme, jak se to osvědčí, díky. Ještě by to mohlo umět hledat ve více slovech (zadám "locate grep", a chci hledat 'locate.*grep', jak jsem zvyklý u klasického file search, ale nic to nenajde), zkusím to doprogramovat.

      Smazat
    3. "zkusím to doprogramovat"
      Tohle je na tom to nejlepší, že můžeš! :)

      Smazat
    4. doprogramovat, nebo obšlohnout ;) Teď jsem zase zjistil, že to není ani tolerantní k chybám v textu (rsznc místo rsync) nebo aspoň vynechaným písmenkům (rsnc), takže nějakou úpravu by to chtělo...

      Smazat
    5. Vzhledem k povaze skriptu, jsem tato sofistikovanou funkčnost neočekával, snad maximálně nějaké to základní sed -r 's/(.)/\1.*/g' ...

      Smazat

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.