Bash 4

Poradnik Bash Bushidō cz. IV – historia, skróty klawiszowe i The Fuck

Każdy z nas ma swoją historię. Na szczęście nie będziemy się zajmować trudnymi, zawiłymi i nie zawsze moralnie jednoznacznymi ludzkimi historiami. Naszą historią będzie dzisiaj historia w Bashu, czyli biblioteka oraz plik tekstowy prosto z projektu GNU, z której Bash korzysta. Będziemy sterować zachowaniem tej biblioteki poprzez odpowiednie ustawienia zmiennych w powłoce.

Prawie dwa miesiące przyszło nam czekać na następny artykuł z mojej ulubionej serii Bash Bushidō. Wynika to z prostego faktu, iż artykuły techniczne na naszym blogu piszą administratorzy, architekci i programiści, którzy czasem mają inne zadania.

Po krótkim usprawiedliwieniu – zaczynajmy.

Każdy z nas ma historię

Na szczęście nie będziemy się zajmować trudnymi, zawiłymi i nie zawsze moralnie jednoznacznymi ludzkimi historiami. Naszą historią dzisiaj będzie historia w Bashu, czyli biblioteka oraz plik tekstowy prosto z projektu GNU, z której Bash korzysta. Będziemy sterować zachowaniem tej biblioteki poprzez odpowiednie ustawienia zmiennych w powłoce.

Mimo iż jestem fanem man, to prawdziwą skarbnicą wiedzy o projektach prowadzonych przez GNU są strony info. Do ich czytania służy narzędzie o tej samej nazwie info. By zwiększyć naszą wiedzę à propos samej biblioteki historii, możemy wywołać info history.

Podobnie w celu znalezienia zmiennych używanych przez Basha polecam używanie info bash. Ze względu na fakt, iż manuale mają płaską strukturę (jedna strona), przy występowaniu odnośników do innych sekcji są one mniej czytelne niż strony info.

Gdzie mamy historię?

Sama historia w Bashu jest po prostu plikiem (jak prawie wszystko w Unixie). By znaleźć plik, do którego historia jest zapisywana, należy wpisać wartość zmiennej HISTFILE:

[Alex@SpaceShip ~]$ echo $HISTFILE
/home/Alex/.bash_history

Jak widać, zmienna HISTFILE jest zmienną automatycznie przypisywaną przez Basha. Aby zmienić miejsce zapisywania historii, zmieniamy wartość zmiennej HISTFILE.

Poniżej przykład z exportem zmiennej.

[Alex@SpaceShip ~]$ bash # Odpalenie basha w bashu :)
[Alex@SpaceShip ~]$ export HISTFILE=~/REMOVE_ME_TEST
[Alex@SpaceShip ~]$ echo "Ta jedyna ;)"
Ta jedyna ;)
[Alex@SpaceShip ~]$ exit # Zakończenie podpowłoki
exit
[Alex@SpaceShip ~]$ cat ~/REMOVE_ME_TEST #
export HISTFILE=~/REMOVE_ME_TEST
echo "Ta jedyna ;)"
exit

Permanentne rozwiązanie tworzymy, dodając export naszej zmiennej HISTFILE do .bashrc

[Alex@SpaceShip ~]$ echo "export HISTFILE=~/.remove_test_hist" >> ~/.bashrc

Najczęściej używane komendy

W celu wyświetlenia najczęściej używanych komend możemy wykonać następującego jednolinijkowca.

[Alex@SpaceShip ~]$ history | awk '{print $2}' | sort | uniq -c | sed "s/^[ \t]*//" | sort -nr | head -10
74 ls
64 vim
59 echo
38 ../xxx.sh
37 cd
32 git
28 bash
23 shellcheck
19 ./xxx.sh
14 history

Dla większości ludzi znających Basha ten oneliner jest zrozumiały. Jedynym miejscem, które może sprawić trudności, jest sed "s/^[ \t]*//". Został on skopiowany z internetu napisany w celu usunięcia znajdujących się na początku linii białych znaków. Znaki te są generowane przez uniq -c.

Skasujmy naszą historię

By wyczyścić historię, możemy wykorzystać history -c.

[vagrant@localhost ~]$ history
    1  w
    2  df ­h
    3  uname ­a
    ...
    70 history
[vagrant@localhost ~]$ history ­c
[vagrant@localhost ~]$ history
    1  history
[vagrant@localhost ~]$

Warto zauważyć, że plik $HISTFILE wciąż będzie zawierał stare wpisy. Historia zapisuje się, gdy kończymy naszą sesję.

Historia z datą

Za format, jak i występowanie daty w pliku historii odpowiada zmienna HISTTIMEFORMAT. W celu rozpoczęcia procesu zapisywania historii z datą wykonania komendy musimy ją odpowiednio ustawić.
export HISTTIMEFORMAT="%Y-%m-%d %T "

Zauważmy, że jeśli poprzednie polecenia nie posiadały swojego czasu wykonania, to za datę tych wpisów zostanie ustawiona data wykonania pierwszego polecenia z ustawioną zmienną historii.

[vagrant@localhost ~]$ export HISTTIMEFORMAT="%Y-%m-%d %T "
[vagrant@localhost ~]$ echo 'export HISTTIMEFORMAT="%Y-%m-%d %T "' >> .bashrc 
[vagrant@localhost ~]$ history
    1  2018-07-31 09:58:42 echo "This is great"
    2  2018-07-31 09:58:46 df -h 
    3  2018-07-31 09:58:57 sudo yum update -y
    4  2018-07-31 09:59:15 uname -a 
    5  2018-07-31 09:59:23 cat /etc/grub2.cfg 
    6  2018-07-31 09:59:25 sudo cat /etc/grub2.cfg 
    7  2018-07-31 09:59:30 history
    8  2018-07-31 09:59:47 export HISTTIMEFORMAT="%Y-%m-%d %T"
    9  2018-07-31 10:00:07 echo 'export HISTTIMEFORMAT="%Y-%m-%d %T"' >> .bashrc 
   10  2018-07-31 10:00:10 history
   11  2018-07-31 10:00:19 export HISTTIMEFORMAT="%Y-%m-%d %T "
   12  2018-07-31 10:00:23 echo 'export HISTTIMEFORMAT="%Y-%m-%d %T "' >> .bashrc 
   13  2018-07-31 10:00:25 history

W powyższym przypadku widać kilka wpisów z historią. Chciałbym zaznaczyć, że jednak posiadają one swój rzeczywisty czas wykonania. Wynika to z faktu, iż wydarzyły się w tej samej sesji co ustawienie zmiennej HISTTIMEFORMAT.

Należy zauważyć, iż spacja na końcu linii HISTTIMEFORMAT nie jest przypadkowa. Bez niej parametr %T „zleje się” z wywołaną komendą.

Jeśli posiadamy własne skrypty działające cyklicznie na historii Basha, konieczna może być ich aktualizacja. Przykładowo do znalezienia najczęściej używanych komend użyliśmy wybrania słowa komendy przy pomocy awk '{print $2}. Teraz będziemy musieli zmienić $2 na $4.

Ustawienia ignorowania historii

Jak już wspominałem w poprzednich częściach, możemy sprawić, by Bash nie zapisywał części naszych komend, np. jeśli pierwszym znakiem na linii poleceń jest spacja.

W tym celu musimy odpowiednio ustawić zmienną HISTCONTROL.

HISTCONTROL może mieć następujące wartości:

1. Nieustawiona zmienna lub ustawiona na niepoprawny ciąg znaków – zapisywane jest wszystko.
2. ignorespace – linie zaczynające się od spacji nie są zapisywane.
3. ignoredups – ignoruje duplikaty.
4. erasedups – usuwa poprzednie wpisy, jeśli wystąpiły duplikaty.
5. ignoreboth – ignorespace + ignoredups.

Najczęściej używa się ignoreboth.

Wpływ zmiennej HISTCONTROL prezentuje poniższy przykład.

[root@localhost ~]# echo "my_new_pass" | passwd --stdin root
Changing password for user root.
passwd: all authentication tokens updated successfully.
[root@localhost ~]#  echo "my_new_pass2" | passwd --stdin root
Changing password for user root.
passwd: all authentication tokens updated successfully.
[root@localhost ~]# history
    1  2018-07-31 11:25:55 echo "my_new_pass" | passwd --stdin root
    2  2018-07-31 11:26:04  echo "my_new_pass2" | passwd --stdin root
    3  2018-07-31 11:26:08 history
[root@localhost ~]# echo "export HISTCONTROL=ignoreboth" >> .bashrc
[root@localhost ~]# . .bashrc
[root@localhost ~]#  echo "my_new_pass3" | passwd --stdin root
Changing password for user root.
passwd: all authentication tokens updated successfully.
[root@localhost ~]# history
    1  2018-07-31 11:25:55 echo "my_new_pass" | passwd --stdin root
    2  2018-07-31 11:26:04  echo "my_new_pass2" | passwd --stdin root
    3  2018-07-31 11:26:08 history
    4  2018-07-31 11:26:36 echo "export HISTCONTROL=ignoreboth" >> .bashrc
    5  2018-07-31 11:26:43 . .bashrc
    6  2018-07-31 11:26:53 history

histchars – sterujmy historią

Kolejną ciekawostką, którą chciałbym przedstawić, jest możliwość zmiany domyślnych znaków specjalnych historii poprzez ustawienie zmiennej histchars.

Przy pomocy histchars ustawiamy trzy znaki, które odwołują się kolejno do:

1. Event designator – domyślnie używamy !.
2. Szybka podmiana – domyślnie używany znak to ‘^’. Uruchamiana tylko wtedy, gdy jej znak jest pierwszym znakiem na linii poleceń.
3. Komentarz – istotne jest tutaj zrozumienie, że chodzi o komentarz dla ekspansji z historii, a nie o zakomentowanie z linii poleceń (parser linii poleceń znajduje komentarze poprzez #). W związku z tym faktem jest praktycznie bezużyteczna.

Przykładowe wywołania:

[Alex@SpaceShip ~]$ echo "Bash jest super"
Bash jest super
[Alex@SpaceShip ~]$ !!
echo "Bash jest super"
Bash jest super
[Alex@SpaceShip ~]$ ^Bash^Fish
echo "Fish jest super"
Fish jest super
[Alex@SpaceShip ~]$ # To jest komentarz

Poniżej dokładnie to samo przy zmienionym ustawieniu histchars.

[Alex@SpaceShip ~]$ histchars="+=@"
[Alex@SpaceShip ~]$ echo "Bash jest super"
Bash jest super
[Alex@SpaceShip ~]$ ++
echo "Bash jest super"
Bash jest super
[Alex@SpaceShip ~]$ =Bash=Fish
echo "Fish jest super"
Fish jest super
[Alex@SpaceShip ~]$ @ To jest komentarz
bash: @: command not found...

Poniższy przykład pokazuje także, że rzeczywiście zmiana znaku komentarza dla parsera linii poleceń z # na @ nie występuje.

Stworzenie własnego skrótu klawiszowego

Jak już wiemy z poprzednich części, za to, jak zachowuje się nasza linia komend, odpowiada biblioteka readline. W celu ustawienia własnego skrótu klawiszowego użyjemy programu bind.

Jednak, by móc to robić, musimy się dowiedzieć, jakie kody będą wysyłane przez naszą klawiaturę do konsoli. By to sprawdzić, proponuję użyć następującego triku:

ctrl + v potem wcisnąć szukaną kombinację klawiszy (np F9).

By zobaczyć, za co „pod maską” odpowiada ctrl+v, możemy wywołać:

[Alex@SpaceShip ~]$ bind -P | grep '\C-v'
display-shell-version can be found on "\C-x\C-v".
quoted-insert can be found on "\C-q", "\C-v", "\e[2~".

Dla nas interesująca jest linia „quoted-insert”, co można luźno przetłumaczyć jako „cytowane wstawienie”. Jego sensem jest wstawienie znaku takim, jakim go dostaje linia poleceń (bez interpretowania). Inną nazwą na takie zachowanie konsoli jest „verbatim insert”.

Wiedząc już co pod spodem, możemy stworzyć własny skrót klawiszowy. Poniższy przykład tworzy nam powiązanie dla klawisza F9. Wykonuje ono komendę „date”.

[Alex@SpaceShip ~]$ # znalezione przy pomocy ctrl-v ^[[20~
[Alex@SpaceShip ~]$ bind '"\e[20~":"date\n"'
[Alex@SpaceShip ~]$ date # F9
Tue Jul 31 15:17:53 CEST 2018

Zauważmy zmienione podstawienie – zamiast ^[[20~ użyliśmy \e[20~.

Innym przykładem może być nadpisanie ctrl + q (jak już wiemy nadmiarowo powiązanego do funkcji quoted-insert[ctrlv naprawdę nam wystarczy, poza tym jest dużo bardziej oczywistym skrótem]).

[Alex@SpaceShip ~]$ # znalezienie po ctrl + v ^Q
[Alex@SpaceShip ~]$ bind '"\C-q":"date\n"`
> ^C
[Alex@SpaceShip ~]$ bind '"\C-q":"date\n"'
[Alex@SpaceShip ~]$ date # ctrl + q
Tue Jul 31 15:40:22 CEST 2018
[Alex@SpaceShip ~]$

Jeśli chcielibyśmy dowiązać nasze wywołanie komendy do alt + q, powinniśmy:

[Alex@SpaceShip ~]$ # znalezione po ctrl-v ^[q
[Alex@SpaceShip ~]$ bind '"\eq":"date\n"'
[Alex@SpaceShip ~]$ date # alt + q
Tue Jul 31 15:41:51 CEST 2018

Zmiana zapisu kodu klawisza, który zwróciła nam konsola względem tego, jakiego użyliśmy, wynika z tego, w jaki sposób zinterpretuje go nasza ulubiona biblioteka readline. Tłumaczy to poniższa tabelka.

<
Zapis
Interpretacja
/e
The Escape key. Używany do innych przypisań, takich jak znaki specjalne czy związane z klawiszem Meta (na większości klawiatur alt). Używamy, gdy przed znakiem (kodem klawisza) mamy ^[
/C-
Reprezentuje przytrzymanie klawisza ctrl . Używamy, gdy przed znakiem (kodem klawisza) mamy ^

The Fuck mały pomocnik na linii komend

The Fuck jest projektem stworzonym z myślą o administratorach, którym często zdarzają się literówki. Narzędzie to automatycznie wyszukuje najbliższą poprawną formę i sugeruje możliwe rozwiązanie. Oczywiście zdaję sobie sprawę, że jest to blog firmowy, nie mniej ja takiej nazwy nie wybrałem. Proszę więc o wybaczenie. Po drodze postaram się także spolszczyć nazwę.

The Fuck instalacja na systemach z rodziny Enterprise Linux w wersji 7

fuck wymaga Pythona w wersji 3.4 lub wyższej. Do instalacji The Fuck użyjemy zaś menadżera pakietów pythonowych pip.

By zainstalować na minimalnym wariancie systemu EuroLinux, wywołujemy następujące komendy:

[vagrant@localhost ~]$ sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
[vagrant@localhost ~]$ sudo yum install python34-devel python34-pip gcc
[vagrant@localhost ~]$ sudo pip3 install thefuck

No fucks given

Konfiguracja narzędzia fuck odbywa się poprzez dwukrotne wpisanie fuck i ponownie zaczytanie konfiguracji powłoki.

[vagrant@localhost ~]$ fuck
Seems like fuck alias isn't configured!
Please put eval $(thefuck --alias) in your ~/.bashrc and apply changes with source ~/.bashrc or restart your shell.
Or run fuck second time for configuring it automatically.
More details - https://github.com/nvbn/thefuck#manual-installation
[vagrant@localhost ~]$ fuck
fuck alias configured successfully!
For applying changes run source ~/.bashrc or restart your shell.
[vagrant@localhost ~]$ . ~/.bashrc
[vagrant@localhost ~]$ fuck
No fucks given

Po udanej konfiguracji dostaniemy informację o „no fucks given”, co można przetłumaczyć dwojako: dosłownie jako „OCENZUROWANO” lub „nie znaleziono żadnych niepoprawnych komend” :)

The Fuck drobne spolszczenie

Wielki programista Linus Torvalds, znany między innymi z dyplomatycznego języka i skromności, powiedział:

Nobody actually creates perfect code the first time around, except me. But there's only one of me.

Co można przetłumaczyć w bardzo swobodnym tłumaczeniu.

Nikt w rzeczywistości nie tworzy doskonałego kodu za pierwszym razem, nie licząc mnie. Ale ja jestem jedyny w swoim rodzaju.

Jako że nie jestem Linusem Torvaldsem, a moje ego nie jest wielkości małej planety, to często zdarza mi się popełniać błędy. Czasem sprawiają one, że używam języka i słów, których nie wypada użyć w artykule. Dlatego pozwolę sobie zamienić je na słowo “kurczę”.

W związku z tym skonfigurujemy thefuck tak, byśmy mogli go używać w polskim kontekście.

[vagrant@localhost ~]$ echo "eval $(thefuck --alias kurcze)" >> ~/.bashrc
[vagrant@localhost ~]$ . ~/.bashrc
[vagrant@localhost ~]$ kurcze
Nothing found

Jak widać, dla naszego własnego aliasu nie jest nam zwracany komunikat `No fucks given`.

[vagrant@localhost ~]$ mkdir new_repo
[vagrant@localhost ~]$ cd new_repo/
[vagrant@localhost new_repo]$ git int
git: 'int' is not a git command. See 'git --help'.

Did you mean this?
  init
[vagrant@localhost new_repo]$ kurcze
git init [enter/↑/↓/ctrl+c]
Initialized empty Git repository in /home/vagrant/new_repo/.git/

Warto zauważyć, że „the fuck” przy wyświetleniu listy potencjalnych komend do poprawy, pozwala na używanie j jako i k jako . Identycznie sytuacja ma się z ctrl+n i ctrl + p.

Podsumowanie i podziękowania

Wreszcie udało nam się dotrzeć do ustawień historii, które w naszej serii ze względu na długość artykułów odkładaliśmy. Poznaliśmy także ciekawe narzędzie „The Fuck”.

W następnej części omówimy narzędzie ShellCheck, które pomaga nam pisać lepsze skrypty.

Autorzy

Artykuły na blogu są pisane przez osoby z zespołu EuroLinux. 80% treści zawdzięczamy naszym developerom, pozostałą część przygotowuje dział sprzedaży lub marketingu. Dokładamy starań, żeby treści były jak najlepsze merytorycznie i językowo, ale nie jesteśmy nieomylni. Jeśli zauważysz coś wartego poprawienia lub wyjaśnienia, będziemy wdzięczni za wiadomość.