Potęga powłoki tekstowej. Jak po nią sięgnąć?

Pracuję z komputerami od dawna. W tym czasie związałem się w dużym stopniu z wieloma narzędziami, które najlepiej odpowiadają moim oczekiwaniom. Co ciekawe, jeśli miałbym wybrać najistotniejsze narzędzie pracy, nie będzie to ani język programowania (pomimo sympatii dla Common Lispa, jestem otwarty na inne języki, rozumiem, że czasem trzeba odstąpić od swoich sympatii), system operacyjny (Linux FTW :)) czy edytor tekstu/IDE (tak, tak, vim — szkaradny, ale robi robotę). To wiersz polecenia jest narzędziem, które postrzegam jako najważniejsze.

Pierwsze 4 rozdziały są dedykowane początkującym, którzy nie używali wcześniej powłoki. Zaawansowani mogą być zainteresowani przejrzeniem ich, by zobaczyć czy jakaś ciekawa właściwość im nie umknęła. Rutynowanym użytkownikom polecam przejść od razu do rozdziału 5: Praktyka, jestem przekonany, że większość znajdzie jakieś ciekawe rozwiązanie, o jakim nigdy nie pomyśleli czy program konsolowy, którego nigdy nie używali.

w tym artykule:

  1. Dlaczego wiersz poleceń?
  2. Jak zacząć?
  3. Anatomia powłoki
  4. Skrypty
  5. Praktyka
  6. Podsumowanie

Dlaczego wiersz poleceń?

Powodów jest wiele, przede wszystkim powłoki używam cały czas, niezależnie czy programuję, piszę artykuł, wrzucam pliki na stronę, zarządzam komputerem, robię to w konsoli. Tylko niektóre zadania wymagają bardziej graficznego podejścia. Konsolę mam więc uruchomioną praktycznie cały czas. Dlaczego w konsoli? Lubię w konsoli to, że mam dostępny cały zapis swojej pracy w postaci komend i ich wyników. Zawsze mogę wrócić do tego co zrobiłem przed chwilą. Jeśli zrobię gdzieś błąd, z łatwością mogę wrócić do wcześniej wykonanej komendy. Graficzne środowisko zaś jest bardzo ulotne i chwilowe. Widząc za każdym razem komendy, które wpisuję, z łatwością spostrzegam powtarzalne wzorce, które mogę przełożyć na skrypty, które oszczędzą mi pracy. W trybie graficznym, przygotowany interfejs użytkownika jest nieubłagany, nie daje takich możliwości dostosowania do swoich potrzeb.

Oczywiście, korzyści mają swoją cenę. Pracować w konsoli jest znacznie trudniej niż obsługiwać interfejs graficzny, zaprojektowany tak, żeby każdy umiał się w nim zorientować metodą prób i błędów. Powiem więcej, uczącym się obecnie jest, prawdopodobnie, znacznie trudniej niż mi. Ja mam to szczęście, że uczyłem się komputera na DOSie, wtedy nie było innego wyboru, trzeba było znać komendy, tak więc ucząc się linuksowej powłoki było to dla mnie bardziej naturalne. Bardziej cieszyłem się tym, że bash nie jest tak toporny jak DOS, podczas gdy osobie wychowanej na interfejsie graficznym brakuje tego, że cały czas ma przed sobą możliwości spośród których może wybrać. Taka możliwość jest z pewnością bardzo dogodna, jednak tylko do pewnego momentu.

Osiągnąwszy biegłość, znamy cały proces na wylot, od samego początku wiemy co chcemy zrobić — wówczas interfejs graficzny staje się przeszkodą, bo pracując w konsoli wpisuję polecenia i wystarczy patrzeć tylko w jedno miejsce, żeby kontrolować czy wszystko idzie zgodnie z planem. W interfejsie graficznym muszę poświęcić swoją uwagę by kliknąć we właściwym miejscu. Ekrany dotykowe trochę ułatwiają sprawę, jednak w sytuacji, w której chcę wybrać plik, na ogół szybciej jest wpisać jego nazwę (bo przecież mamy autouzupełnianie i globy). Pod warunkiem, że wiemy czego chcemy. Podobnie jest z komendami, programami, itd.

Co więcej, wygląda na to, że będąc przywiązanym do takiego środowiska, odcinamy sobie jedną ścieżkę rozwoju. Świadomość możliwości ma silny wpływ na naszą świadomość. Spójrzcie jak Google zmieniło nasz sposób myślenia, jak spadła wartość wiedzy samej sobie, a jak wzrosło znaczenie umiejętności jej znajdowania.

Bardzo mocno to odczułem ucząc się vima. Przyzwyczajenie do edycji tekstu opartej o ciągłe widzenie swojego kodu odcinały mi możliwości płynące z pewności, że od samego początku wiem co chcę zrobić, co gdzie chcę zmienić, itd. Okazało się bardzo pomocnym skonfigurować go tak, żeby trudno było go używać jak zwykły edytor, żeby zmusić się do innego sposobu myślenia, poprzez zabranie sobie możliwości chwytania się wygodnych, ale niezbyt efektywnych nawyków (w pewnym momencie nawet, naumyślnie, pracowałem w trybie ex).

Bynajmniej, powłoka nie jest tylko dla doświadczonych. Uważam, że tryb graficzny może blokować rozwój doświadczonego programisty i to w sposób całkiem podstępny, bo może utwierdzać w przekonaniu, że dalej to już można się tylko nowej technologii nauczyć czy poznać nowy wzorzec projektowy, zamiast nabrać lepszej biegłości w abstrakcji. Natomiast sposób myślenia, który tryb tekstowy wymusza, powinien być owocny dla każdego. Również automatyzacja pracy, jaką umożliwia proste medium, jakim jest tekst, wraz z nawykiem — najpierw wiem, potem robię — pomaga eliminować ze swej pracy, elementy, które wcale naszej uwagi nie potrzebują.

I wreszcie szczegół, który ma znacznie tylko dla niektórych. W konsoli przeważnie dzieje się jedna rzecz na raz, co pomaga się skupić, nic nie rozprasza. Dla niektórych ludzi jest to bardzo ważne. Poza tym, to nie znaczy, że konsola ustępuje graficznym interfejsom złożonością, jaką pozwala ogarnąć. Raczej chodzi o to, że konsola motywuje by porządkować rzeczy, zamienić kilka strumieni informacji w jeden, spójny.

Jak zacząć?

Jeśli używasz Linuksa, *BSD, Maca (ogólnie piszemy: *nix, system uniksopodobny), sprawa jest prosta — te systemy nie istnieją bez powłoki. Uruchamiając terminal na każdym z tych systemów otrzymamy bardzo podobne środowisko wzorowane na systemie UNIX. Warto zaznaczyć, że również na Androidzie można zainstalować taki terminal, w końcu Android jest oparty o Linuksa.

Jest kilka implementacji powłoki, najbardziej popularny obecnie jest bash. Na tym środowisku oprę przykłady w tym artykule, choć postaram się zaznaczyć cechy, które są typowe dla basha.

W Windows, sprawy mają się trochę inaczej, bo tu podstawową powłoką jest nieszczęsne cmd, będące nieco udoskonalonym wierszem poleceń DOSa. Na szczęście Microsoft zrozumiał potrzebę dobrej powłoki systemowej i stworzył PowerShell — powłokę opartą o środowisko .NET. Później nastąpiła odwilż Open Source i nawet Steve Balmer powiedział, że kocha Linuksa, więc w nowszych wersjach Windows jest tryb zgodności z Linuksem (w tym powłoka).

Wcześniej istniały różne implementacje uniksowej powłoki dla Windows. Ja zwykle korzystałem z Cygwina, który, pomimo słabej wydajności, dawał bardzo szerokie wsparcie dla standardu. Cygwin to nie tylko powłoka, można w nim skompilować programy pisane pod Linuksa, nawet w assemblerze, choć oczywiście z pewnymi ograniczeniami. Cygwin zawiera też całkiem pokaźną kolekcję oprogramowania open source do wyboru przy instalacji. Znacznie prostszym i szybszym sposobem na uzyskanie uniksowej powłoki w Windows jest git-bash dostarczany z Gitem dla Windows (jednak nie wszystkie polecenia na nim zadziałają).

Znak zachęty

Gdy otworzymy okienko konsoli, możemy zobaczyć różne rzeczy, na niektórych systemach przywita nas losowy aforyzm, powiadomienie, że mamy nieprzeczytaną pocztę. Jednak co do reguły wyświetlony zostanie znak zachęty, mówiący, że powłoka jest gotowa przyjąć polecenie. Na ogół, po wydaniu komendy, znak zachęty pojawi się dopiero po zakończeniu pierwszej. Chyba, że uruchomimy komendę w tle.

Nie ma reguły, jak ten znak zachęty wygląda, jest to w pełni konfigurowalne, jednak standardowe rozwiązanie to:

user@host:~/$

Mamy tu nazwę użytkownika, nazwę komputera i bieżący katalog. $ oznacza, że jesteśmy na zwykłym koncie, co do zasady, na koncie administratora (root), zamiast niego będzie #. ~/ to katalog domowy użytkownika.

Początkowo może dziwić, że akurat takie informacje się w nim pojawiają, jakbym nie wiedział, że pracuję na komputerze, który mam przed sobą? Terminal to, w dużej mierze, narzędzie administratora, który często loguje się zdalnie na inne komputery przez ssh. Wówczas taki, zróżnicowany, znak zachęty pomaga być na bieżąco, na jakim komputerze obecnie jesteśmy. Z resztą, nie tylko administratorzy używają ssh. Myślę, że ssh używają ci, którzy wiedzą, że mogą i wiedzą jak. Zdarzało mi się z niego korzystać w pracy i do własnych celów. Loguję się tak na moje Raspberry Pi, serwer WWW, nawet na telefon. Na maszynie, której używamy tylko bezpośrednio ma sens ograniczenie znaku zachęty do bieżącego katalogu.

Jest również poboczny znak zachęty, który oznacza, że podana wcześniej linia nie jest dokończona (np. jeśli nie zamknęliśmy cudzysłowu lub postawiliśmy znak ucieczki na końcu linii). Domyślnie jest to >, ale w przykładach będzie przyjęty pusty, aby można było łatwo wklejać w okienko terminala.

Zmienne, znaki ucieczki

Znak zachęty jest konfigurowany przez zmienne środowiskowe PS1 i PS2 (właściwie jest ich jeszcze więcej, można doczytać w podręczniku). Zmieniając je, zmieniamy znak zachęty:

lwik@cia-ThinkPad-T410:~/tekst$ PS1="\n\w> "

~/tekst> echo $PS1
\n\w>

~/tekst>

Na początku był standardowy znak zachęty Minta (podobny schemat mamy w Ubuntu), przypisuję nową wartość i już wita mnie nowiutki znak zachęty. Wartość zmiennej przypisujemy operatorem = (uwaga, nie może być odstępu po nazwie zmiennej), aby wypisać wartość zmiennej możemy użyć polecenia echo (bo powtarza cokolwiek dostanie w parametrze). Znak $ natomiast ma tę funkcję, że wstawia wartość zmiennej (stąd, nota bene, wzięły się dolary w nazwach zmiennych w perlu i, potem, PHP).

W ten sposób ustawiona zmienna obowiązuje tylko w bieżącej sesji i tylko dla tej powłoki. Jeżeli chcemy, żeby wywoływane przez nas programy miały do nich dostęp (jest to jeden ze sposobów konfiguracji), musimy użyć polecenia export:

export FOO="bar"

Jeśli chcemy, by zmiana miała zasięg globalny, musimy dopisać ją do skryptu startowego. W przypadku tej akurat zmiennej słowo polecenie export nie ma wielkiego sensu, chyba że uruchomimy powłokę z opcjami --norc --noprofile aby upewnić się, że zmienna nie zostanie ustawiona przez skrypt startowy (co zwykle ma miejsce jeśli uruchamiamy w powłokę w trybie interaktywnym).

Nie będę tłumaczył, w szczegółach, składni tworzenia znaku zachęty, jednak winienem objaśnić znak \ zwany znakiem ucieczki (escape), gdyż pozwala uciec od zwykłego znaczenia w wyrażeniach. Ten pomysł również się przyjął na dobre i w większości współczesnych języków programowania piszemy \n gdy mamy na myśli znak nowej linii. Między innymi, znak ten pozwala nam uciec od znaczenia dolara, jako wstawienia wartości zmiennej:

~/tekst> echo \$PS1
$PS1

Na wypadek dłuższych wyrażeń, które mają być traktowane dosłownie, mamy do dyspozycji pojedynczy cudzysłów:

~/tekst> echo '$PS1\n'
$PS1\n

~/tekst> echo \''$PS1\n'\'
'$PS1\n'

Składnia polecenia, przełączniki, autouzupełnianie

UNIX powstał dawno temu, więc jego powłoka ma bardzo prostą składnię. Najpierw pojawia się nazwa polecenia, potem następują parametry oddzielane odstępami (spacja, tab, znak nowej linii poprzedzony \). Dajmy na to polecenie:

~/tekst> uname -a
Linux cia-ThinkPad-T410 4.15.0-20-generic #21-Ubuntu SMP Tue Apr 24 06:16:15
 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Dzięki niemu możemy się dowiedzieć na jakim systemie operacyjnym pracujemy i na jakiej architekturze procesora. uname to nazwa polecenia, a -a to przełącznik, skrót od all, mówiący że chcemy wszystkie informacje.

Nazwa polecenia jest nazwą pliku wykonywalnego, który implementuje jego działanie. Pisanie za każdym razem całej ścieżki dostępu byłoby bardzo niewygodne, więc zdefiniowana jest zmienna PATH, która wymienia katalogi w których szukane są polecenia (oddzielone znakiem :).

~/tekst> echo $PATH
/home/lwik/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbi
n:/bin:/usr/games:/usr/local/games

Istnieje kilka wyjątków od tej reguły, bo niektóre, najbardziej podstawowe polecenia są zaimplementowane w programie powłoki, więc nigdzie nie znajdziemy ich pliku wykonywalnego. Na przykład trudno byłoby zaimplementować polecenie cd (zmień bieżący katalog) jako oddzielny program. Poza tym, są jeszcze funkcje i aliasy.

Inny przykład, wypisanie zawartości katalogu:

~/tekst> ls
a.html  bak  b.html  docs  Gad-toga.htm  home  index.html  index.txt  it  Ma
kefile  make.sh  nowe  out  x.txt

~/tekst> ls ~
'A Science Odyssey.mkv'   hello.pl    Muzyka      quicklisp 
 body.txt                 jsons.txt   Obrazy      Steam     
 code                     lekturka    Pobrane     Szablony
 code.tar                 lisp        praca       tekst
 dane-ssh                 Mail        Publiczny   tekst.tar
 Dokumenty                muzyka      Pulpit      test.py

~/tekst> ls /
bin  cdrom  etc   initrd.img      lib    lost+found  mnt  proc  run   srv  
sys  usr    vmlinuz
boot  dev   home  initrd.img.old  lib64  media       opt  root  sbin  swapf
ile  tmp  var

W zależności od parametru, wypisuję zawartość różnych katalogów (nic nie stoi na przeszkodzie wpisać kilka na raz). Dajmy na to, że chciałbym obejrzeć film A Science Odyssey, który mam w katalogu domowym. Nazwa pliku zawiera spacje, więc musiałbym użyć znaku ucieczki. Jednak są też inne sposoby. Po pierwsze, mogę użyć zwykłego cudzysłowu, wówczas mogę, m.in, używać zmiennych:

~/tekst> mplayer "$HOME/A Science Odyssey.mkv"

mplayer nie jest standardowym poleceniem lecz programem, którego przeważnie używam do odtwarzania multimediów. Może być nieobecny w twoim systemie. Mógłbym też użyć globu:

~/tekst> mplayer ~/A*mkv

Jednak najpewniej użyję autouzupełniania. Wpiszę: mplayer ~/A i wcisnę TAB, a powłoka uzupełni resztę. To jak ten mechanizm zachowa się, w przypadku niejednoznacznoności, zależy od powłoki i aktualnej konfiguracji. Przeważnie uzupełni tyle ile jest wspólne dla wszystkich możliwości i wypisze je poniżej. Innym typowym zachowaniem jest użycie pierwszej możliwości, wówczas kolejne wciśnięcia TAB wywołują kolejne. To są zachowania najbardziej podstawowe. Różne powłoki mogą oferować mniej lub bardziej rozbudowane rozwiązania (o ile dobrze pamiętam, zsh przoduje w tego typu sprawach).

Parametry, pomoc (man, help)

Większość programów oferuje mniej lub więcej przełączników, pozwalających dostosować działanie programu:

~/tekst> ls -F
a.html  bak/  b.html  docs/  Gad-toga.htm  home/  index.html  index.txt  it/
Makefile  make.sh*  nowe/  out  x.txt

~/tekst> ls -lh
total 204K
-rw-r--r-- 1 lwik lwik  53K lip  8 17:26 a.html
drwxr-xr-x 2 lwik lwik 4,0K cze 16 14:06 bak
-rw-r--r-- 1 lwik lwik  52K lip  8 17:25 b.html
drwxr-xr-x 2 lwik lwik 4,0K lip  8 15:22 docs
-rw-r--r-- 1 lwik lwik  50K cze 18 16:31 Gad-toga.htm
drwxr-xr-x 3 lwik lwik 4,0K lip  9 22:36 home
-rw-r--r-- 1 lwik lwik 3,0K lip 10 00:18 index.html
-rw-r--r-- 1 lwik lwik  356 lip  8 15:22 index.txt
drwxr-xr-x 4 lwik lwik 4,0K lip  8 18:32 it
-rw-r--r-- 1 lwik lwik  147 lip  8 15:22 Makefile
-rwxr-xr-x 1 lwik lwik  127 cze 21 22:53 make.sh
drwxr-xr-x 3 lwik lwik 4,0K lip  8 18:32 nowe
-rw-r--r-- 1 lwik lwik 1,8K lip  2 16:15 out
-rw-r--r-- 1 lwik lwik  413 lip  6 18:45 x.txt

~/tekst> ls -a
.  ..  a.html  bak  b.html  docs  Gad-toga.htm  .git  home  index.html  inde
x.txt  it  Makefile  make.sh  nowe  out  x.txt

Warto też zaznaczyć, że używając zmiennej w linii poleceń, jest ona interpretowana przed podziałem na parametry. Dlatego w ten sposób możemy wstawić kilka parametrów, a jeśli zmienna nie ma wartości, nie doda żadnego parametru. Jeśli chcemy aby zmienna była traktowana jako jeden parametr, musimy wstawić ją w cudzysłów:

~/tekst> par="-lah /usr/src"

~/tekst> ls $par
total 16K
drwxr-xr-x  4 root root 4,0K cze 26  2018 .
drwxr-xr-x 10 root root 4,0K cze 26  2018 ..
drwxr-xr-x 27 root root 4,0K cze 26  2018 linux-headers-4.15.0-20
drwxr-xr-x  8 root root 4,0K cze 26  2018 linux-headers-4.15.0-20-generic

~/tekst> ls "$par"
ls: invalid option -- ' '
Try 'ls --help' for more information.

Oczywiście nikt nie jest chodzącą encyklopedią, pamiętamy tylko najczęściej używane przełączniki, gdy nie pamiętamy, używamy systemu pomocy, polecenia man (od manual czy podręcznik), np.

~/tekst> man ls

Warto zauważyć, że w man możemy znaleźć znacznie więcej, opisy funkcji standardowych C, systemowego API, niektórych plików systemowych:

man stdio # standardowa biblioteka C stdio
man fopen # funkcja C fopen()
man fork  # funkcja systemowa fork()
man fstab # plik systemowy /etc/fstab

Nic nie stoi na przeszkodzie, aby stworzyć własną stronę man. Można je pisać ręcznie w formacie groff albo wygenerować, np. przy użyciu pandoc.

Dla funkcji wbudowanych powłoki, z reguły nie ma stron man, należy użyć polecenia help:

~/tekst> help cd
cd: cd [-L|[-P [-e]] [-@]] [dir]
    Change the shell working directory.

[…]

Przeglądarka tekstu — less, edytor nano

To polecenie przeważnie używa przeglądarki tekstu (np. less), w związku z czym możemy przewijać tekst, używając strzałek. Możemy poszukać frazy naciskając /. Potem wpisujemy frazę i zatwierdzamy enterem. Odpowiednio n i N przechodzą do następnego i poprzedniego wystąpienia. Gdy wciśniemy q, przeglądarka zamknie się i nie będzie po niej śladu. Podobnie możemy przejrzeć dowolny plik, np:

~/tekst> less /etc/fstab

~/tekst> 

Ten akurat plik, przechowuje konfigurację używanych domyślnie partycji w systemie. less należy rozumieć jako pokaż mi mniej (na raz). Jednocześnie, jest to również żart (uniksowi hakerzy mają specyficzne poczucie humoru), bo jest to rozszerzona wersja more, który nie pozwala przewijać do tyłu. Poza tym less wyświetla, w takim sensie mniej, że po zamknięciu cała zawartość pliku znika, w przypadku more, po zakończeniu cała zawartość pliku pozostaje, jakby to było polecenie cat.

Do edycji tekstu, na początek, polecam nano, powinien być w większości systemów. Prawdziwi hakerzy używają vima lub emacsa, bo mają olbrzymie możliwości konfiguracji. Na temat drugiego nawet krąży żart: emacs to świetny system operacyjny, ale ma jedną wadę, brak mu dobrego edytora tekstu. Jeśli jednak jesteś nowy, polecam spróbować edytora atom, jest nowocześniejszy (ale nie ma wersji konsolowej). Szczerze, vim to syf, ale nawyk to nawyk. :)

Możemy również użyć polecenia cat, aby po prostu wypisać treść pliku:

~/tekst> cat /etc/fstab
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
# / was on /dev/sda3 during installation
UUID=d77703fa-dbbf-48b1-804d-c130b7af6ccd /               ext4    errors=rem
ount-ro 0       1
/swapfile                                 none            swap    sw        
      0       0

To polecenie również może być użyte do pisania do pliku (za pomocą przekierowania do pliku), więc uruchomienie go bez parametru nie jest błędem, będzie wtedy wypisywać na wyjściu to co dostanie na wejściu. Są dwa sposoby by go wówczas zamknąć: CTRL-D na początku nowej linii oznacza znak EOF (end of file) i spowoduje normalne zamknięcie programu; CTRL-C wysyła sygnał zamknięcia programu, wówczas program zakończy się błędem (choć nie anuluje to zmian, jeśli pisaliśmy do pliku).

Warto zaznaczyć też, że cat to skrót od concatenate (połącz), bo można podać wiele plików i wszystkie zostaną wyświetlone po kolei.

Bieżący katalog

W znaku zachęty mamy podany bieżący katalog. Byłoby wysoce uciążliwe cały czas pisać pełne ścieżki dostępu. Dlatego mamy bieżący katalog, oznaczany kropką (.). Jeśli ścieżka dostępu nie zaczyna się od ~ lub / to znaczy że jest ścieżka dostępu podana względem katalogu bieżącego.

Oprócz tego, że jest wypisywany w znaku zachęty (przeważnie), można go uzyskać na jeszcze parę sposobów (przydatne w skryptach):

~/tekst> pwd
/home/lwik/tekst

~/tekst> echo $PWD
/home/lwik/tekst

W tych przypadkach są podawane pełne ścieżki, bez skrótowca ~. Są dwa główne sposoby zmiany katalogu:

~/tekst> cd ..

~> cd ~/tekst/home/

~/tekst/home> pushd /usr/src
/usr/src ~/tekst/home

/usr/src> ls
linux-headers-4.15.0-20  linux-headers-4.15.0-20-generic

/usr/src> popd
~/tekst/home

~/tekst/home> cd ..

~/tekst>

Polecenie cd zmienia bieżący katalog — kropka. Natomiast pushd jest niestandardowym (działa w bashu) sposobem na zmianę katalogu, dopóki nie użyjemy popd by wrócić do katalogu w którym byliśmy wywołując pushd. Jak nazwy wskazują, działają one na zasadzie stosu, innymi słowy możemy te wywołania zagnieżdżać.

Najważniejszy skrypt

Zanim zaczniesz pracować w powłoce, proponuję ci zacząć od najważniejszego, moim zdaniem kodu, który należy umieścić w skrypcie startowym twojej powłoki. W bashu (program powłoki można sprawdzić poleceniem echo $SHELL), ~/.bashrc. Możesz użyć do tego nano, a możesz zrobić tak:

echo '
RCFILE="$HOME/.bashrc"
EDITOR="nano"

shrc() {
    $EDITOR $RCFILE
    source $RCFILE
}' >> ~/.bashrc

To polecenie dopisze kawałek kodu na koniec danego pliku. Przydaje się to w pisaniu skryptów, ale też w pisaniu poradników. :) Jak widzicie, również, powłoka nie ma nic przeciwko łamaniu linii wewnątrz cudzysłowów (w odróżnieniu od Javy czy C++). Operator >> oznacza dopisywanie do pliku.

Co ten skrypt robi? Definiuje funkcję shrc, którą można wywołać jako polecenie. Otworzy ona plik $RCFILE edytorem $EDITOR (dostosuj wartości tych zmiennych), a następnie użyje polecenia source, które załaduje ponownie plik. Swoją drogą, jeśli chcesz zmienić znak zachęty, to jest właściwe miejsce, żeby tam umieścić właściwą definicję (zmienne środowiska wygasają po zamknięciu powłoki).

Czemu jest to najważniejsze polecenie? Używanie nieskonfigurowanej powłoki jest mało wygodne, warto mieć pod ręką łatwy sposób na dodanie czegoś do konfiguracji. Któż chciałby pamiętać te wszystkie nazwy i przełączniki? Jeśli nadamy im swoje nazwy, z pewnością łatwiej je zapamiętamy. Wspomniałem już o tym jak świadomość możliwości kształtuje zachowanie. Dlatego warto sobie pomóc i stworzyć szybki sposób dopisywania nowych linii w skrypcie startowym.

Chociaż, jest też druga strona medalu. Jeśli za bardzo przyzwyczaimy się do swoich aliasów, możemy mieć problem jeśli ich zabraknie. Dopóki chodzi o komputer osobisty i swoje konto na zdalnym komputerze, da się to łatwo obejść, choćby trzymając swoją konfigurację na githubie bądź pendrivie i instalując je na każdym nowym koncie. Jednak im bardziej stajemy się doświadczeni, rośnie prawdopodobnieństwo, że będziemy pomagać młodszemu koledze na jego komputerze. I co wtedy? Z pewnością warto zachować tu równowagę. Z pewnością można się wspomóc na samym początku własną konfiguracją, lecz warto też pamiętać, żeby po przełamaniu pierwszych lodów, poznać również standardowe metody.

Aliasy

Możemy pisać funkcje, ale jeszcze prościej zrobić alias:

~/tekst> alias ll="ls -lh"

~/tekst> ll
total 224K
drwxr-xr-x 2 lwik lwik 4,0K lip  8 15:22 docs
drwxr-xr-x 3 lwik lwik 4,0K lip 10 01:12 home
-rw-r--r-- 1 lwik lwik 3,0K lip 12 21:44 index.html
-rw-r--r-- 1 lwik lwik  356 lip  8 15:22 index.txt
drwxr-xr-x 4 lwik lwik 4,0K lip  8 18:32 it
-rw-r--r-- 1 lwik lwik  147 lip  8 15:22 Makefile
-rwxr-xr-x 1 lwik lwik  127 cze 21 22:53 make.sh
drwxr-xr-x 3 lwik lwik 4,0K lip  8 18:32 nowe

Zasadnicza różnica jest taka, że alias działa na zasadzie prostego podstawienia w miejscu wystąpienia, więc ewentualne parametry zostaną dopisane na końcu. Funkcja uzyskuje dostęp do parametrów za pomocą zmiennych, takich samych jak w przypadku parametrów wywołania skryptu.

Świetnie. Zrobiłeś pierwszy krok, żeby zostać konsolowym wymiataczem. Tak trzymaj! Ktoś mógłby oczekiwać, że teraz pojawi się wykaz najpopularniejszych poleceń: ls, cp, mv, rm, ln, find, whereis, kill, grep. Jednak tego nie zrobię, sprawdzenie tych poleceń w man to zadanie domowe, a czego nie wiesz, znajdziesz w internecie, np: how to copy a file in bash, how to list all processes in bash, itd. Natomiast ja przejdę do rzeczy bardziej istotnych, typowych dla języka powłoki samego w sobie.

Anatomia powłoki

Gdy masz już masz uruchomioną powłokę i znasz absolutne podstawy, możemy przejść do cech, które czynią uniksową powłokę tak potężnym narzędziem.

Strumienie: wejście, wyjście, błędy; kod powrotu

Gdy otwieramy terminal, mamy zapis pracy w konsoli. Postrzegamy to jako tekst wprowadzany i tekst wypisywany przez programy. Jednak należy sobie zdawać sprawę, że jest to tylko domyślne zachowanie. Każdy proces w *nix ma trzy strumienie: stdin, stdout, stderr, aby program nie musiał martwić się skąd brać dane wejściowe, gdzie zapisać wynik, a gdzie zapisywać błędy, tym ma zajmować się powłoka, lub inny proces nadrzędny. Szkoda, że duża część materiałów dot. języków programowania utożsamia standardowe wejście z klawiaturą i standardowe wyjście z ekranem, bo takie traktowanie sprawy odbiera elastyczność jaką daje nam przekierowywanie strumieni.

Dzięki temu, możemy nie tylko używać programów ręcznie, ale możemy też pisać programy, które komponują inne programy w większą całość. Poza tym, wiele programów w open-source, jako podstawa, są konsolowe i mają wiele programów, które implementują graficzny interfejs dla nich. Praktycznie każdy współczesny język programowania pozwala przekierowywać strumienie, jest też systemowe API na poziomie C i assemblera. Jednak język powłoki pozwala zrobić to łatwiej i szybciej, jeśli nie chcemy tego mieszać ze skomplikowaną logiką.

Oczywiście, posługiwanie się strumieniami wymaga wprawy i nawet doświadczony programista potrzebuje wypróbować polecenia bez przekierowań, zanim ułoży skrypt. Tak więc, bez obaw. Czas robi swoje, to wymaga wprawy, zobaczyć jak działają różne polecenia i jak można je ze sobą łączyć. Dlatego też zachęcam, żeby w dalszych przykładach sprawdzać jakie wyjście generują używane przeze mnie programy. Żeby nie rozwlekać, nie zamieszczam ich.

Oprócz tego, każdy program zwraca wartość liczbową. Wartość 0 oznacza, że wykonał się prawidłowo, pozostałe oznaczają różne warianty błędów. Kto pisał w C/C++, pamięta return 0; na końcu funkcji main(). Kod powrotu ostatniego wywołanego programu znajduje się w zmiennej $?. Domyślnie, zakończenie programu niepowodzeniem nie ma żadnych szczególnych konsekwencji. Jeśli jednak uruchomimy powłokę z przełącznikiem -e, powłoka zamknie się jeśli którekolwiek polecenie nie powiedzie się, czasami chcemy takiego zachowania w skryptach.

Przekierowanie do pliku

Mamy kilka sposobów przekierowania strumieni wyjścia do pliku, oto one (w powłoce, linie zaczynające się od # to komentarze):

# przekierowanie strumienia wyjścia (stdout), nadpisując plik:
echo foo > plik

# stdout, dopisując na końcu pliku:
echo bar >> plik

# zobacz wynik:
cat plik

# Dla strumienia błędów analogicznie (2>> jest rozszerzeniem basha):

bash -c "nieznana komenda" 2> plik
bash -c "inna komenda" 2>> plik

cat plik

# Dla obu strumieni:

bash -c "echo foo && bar" &> plik
bash -c "cat /file/does/not/exist" &>> plik

cat plik

Możesz spokojnie zapisać do pliku i uruchomić:

~/tekst> bash -x x.sh
+ echo foo
+ echo bar
+ cat plik
foo
bar
+ bash -c 'nieznana komenda'
+ bash -c 'inna komenda'
+ cat plik
bash: nieznana: command not found
bash: inna: command not found
+ bash -c 'echo foo && bar'
+ bash -c 'cat /file/does/not/exist'
+ cat plik
foo
bash: bar: command not found
cat: /file/does/not/exist: No such file or directory

Dzięki temu, że użyłem przełącznika -x, każdy krok jest wypisywany. W skrypcie użyty jest też przełącznik -c, pozwalający wywołać jedną komendę w nowej powłoce. Operator && sprawia, że druga komenda będzie wykonana tylko jeśli pierwsza zakończy się sukcesem (kod powrotu 0).

Możemy też przekierować strumień wejściowy do pliku. Możemy przekierować oba strumienie na raz. Oto najprostszy możliwy test programu:

./program < input &> output
diff output expected

Bieżący katalog, domyślnie nie figuruje w zmiennej PATH, więc aby wywołać program w bieżącym katalogu, poprzedzam go ./. Polecenie diff porównuje dwa pliki i zwraca listę różnic (jeśli pliki są tekstowe.

Plik, czyli…

W tym miejscu trzeba zaznaczyć, że w filozofii *nix wszystko jest plikiem. To znaczy, że w systemie plików znajdziemy nie tylko tradycyjne pliki. Zasadniczo każde urządzenie ma swój plik (znajdują się one w katalogu /dev/). Na przykład, w Linuksie, pierwszy dysk twardy to /dev/hda lub /dev/sda (w zależności od typu dysku), a /dev/sr0 to pierwszy napęd CD/DVD. W ten sposób możemy pisać z i na dysk z pominięciem abstrakcji systemu plików. Na przykład, żeby zapisać MBR (pierwsze 512 bajtów) tego dysku możemy zrobić:

~/tekst> sudo dd if=/dev/sda of=mbr.bin count=1 bs=512
[sudo] password for lwik: 
1+0 records in
1+0 records out
512 bytes copied, 0,000197036 s, 2,6 MB/s

Polecenie dd pozwala przeczytać lub zapisać określoną ilość bajtów, w określonym miejscu w pliku. Ma bardzo dużo opcji. Komendę poprzedzam poleceniem sudo, ponieważ dostęp do tego urządzenia wymaga uprawnień administratora. W związku z tym, musiałem też wpisać swoje hasło, które się nie wyświetla w konsoli.

Oczywiście, sudo może być tak skonfigurowane, żeby dany użytkownik miał prawo uruchamiać tylko niektóre programy. Jednak na systemach desktopowych, przeważnie pierwszy utworzony użytkownik ma prawa do wszystkiego przez sudo. W podobny sposób mogę też zrobić obraz dysku, poszczególnej partycji, itd. Poza tym, zwykle jest grupa sudo, do której należy dodać użytkownika aby miał takie uprawnienia.

Ważnym plikiem specjalnym jest /dev/null, można do niego pisać i pisać i nic się nie zapisze. Używamy tego często aby pominąć mało istotne komunikaty. Na przykład:

~/tekst> rm /tmp/foo &> /dev/null

Nawet jeśli plik nie istnieje, nie zobaczymy żadnego komunikatu o błędzie. Jednak jeśli plik nie istnieje, operacja zakończy się niepowodzeniem, aby to zmienić, możemy napisać:

~/tekst> rm /tmp/foo &> /dev/null || true

Operator || oznacza — uruchom drugie polecenie jeśli pierwsze się nie powiedzie. Polecenie true zawsze się powodzi.

Inny ciekawy plik, /dev/zero można czytać bez końca i będzie cały czas wypisywać znak o kodzie zero. Możemy go użyć by stworzyć obraz dysku:

~/tekst> dd if=/dev/zero of=my.img bs=1024 count=40K
40960+0 records in
40960+0 records out
41943040 bytes (42 MB, 40 MiB) copied, 1,11794 s, 37,5 MB/s

~/tekst> mke2fs my.img > /dev/null
mke2fs 1.44.1 (24-Mar-2018)

I mamy 42MB system plików ext2 w pliku my.img.

Bardzo dużo ciekawych plików znajdziemy w katalogach /proc i /sys. Na przykład, poniższa komenda wprowadzi komputer w tryb uśpienia:

~/tekst> sudo bash -c "echo mem > /sys/power/state"

Nawet nie wspominam o dowiązaniach (odpowiednik skrótów z Windows), i takich systemach plików jak nfs, tmpfs czy squashfs, bo nie są niczym niezwykłym, choć nie każdy wie, że tak można Natomiast czymś typowym dla *nix są potoki nazwane (named pipes). Innymi słowy jeden program pisze do takiego potoku, a inny to czyta. Taka prosta alternatywa dla lokalnej komunikacji przez TCP/IP, o tyle wygodna, że nie wymaga żadnego szczególnego wsparcia, działa poprzez zwykłe czytanie i pisanie do pliku. Na przykład, używam tego aby uruchomić interpreter Lispa lub powłoki w tle i móc z niego korzystać w różnych programach.

Potoki

Potoki (pipes) są podstawowym sposobem kompozycji programów w *nix i biegły użytkownik często z nich korzysta. Potok oznacza, że wejście jednego programu jest przekierowane do wyjścia innego. Na przykład:

~/tekst> echo "Witaj Świecie" | hexdump -C
00000000  57 69 74 61 6a 20 c5 9a  77 69 65 63 69 65 0a    |Witaj ..wiecie.|
0000000f

Albo gdy zastanawia nas co nam zjada tyle RAMu:

~/tekst> ps aux | awk '$4 > 2 {print $3, $4, $11}'
4.1 3.9 cinnamon
0.7 6.8 /usr/lib/firefox/firefox
0.1 3.3 /usr/lib/firefox/firefox
0.2 2.7 /usr/lib/firefox/firefox

Polecenie ps z argumentem aux wypisuje wszystkie procesy wraz ze szczegółami zużycia zasobów. awk zaś jest językiem programowania stworzonym specjalnie na takie okazje. Wybieram wiersze z wartością 4. kolumny większą niż 2 (2% zużycia pamięci) i wypisuję kolumny 3 (CPU), 4 i 11 (nazwa programu). Szerszy opis języka w tym artykule.

Gdybym chciał wiedzieć jaki mam procesor:

~/tekst> lscpu | grep Model\ name
Model name:          Intel(R) Core(TM) i5 CPU       M 520  @ 2.40GHz

Polecenie lscpu wypisuje informacje o procesorach, polecenie grep wybiera linie pasujące do wzorca.

Nawet takie czary-mary:

~/tekst> echo '
#include <stdio.h>

int main() {
    printf("Hello world!");
    return 0;
}' > /tmp/x.c

~/tekst> sed 's/Hello world/Witaj świecie!/' /tmp/x.c \
                                  | gcc -o /tmp/x -xc -

~/tekst> /tmp/x
Witaj świecie!!

Polecenie sed wypisze na wyjściu plik /tmp/x.c ze zmienionym Hello world na Witaj świecie!. Jednak wynik przekierowuję do kompilatora gcc. Przełącznik -o definiuje nazwę pliku wyjściowego, a -xc informuje, że chcę użyć języka C. Ta opcja jest niezbędna z parametrem -, mówiącym, że kod będzie na wejściu, a nie w pliku.

Potok z przekierowaniem do pliku (tee)

Czasami jednocześnie zrobić przekierowanie do pliku i potoku, na przykład w celu prowadzenia dziennika zdarzeń (jak trwoga to do loga). Wówczas możemy użyć polecenia tee, które jednocześnie zapisuje strumień wejściowy do pliku i do strumienia wyjściowego. Mozna również użyć go do poszukiwania błędów w potokach, na przykład tak:

~/tekst> sudo netstat -lp | awk 'match($1, /(tcp|udp)/)' \
            | tee /dev/tty \
            | awk '{if ($6 == "LISTEN") print $7; else print $6}'
tcp        0   0 localhost:44303    0.0.0.0:*  LISTEN  881/containerd      
tcp        0   0 localhost:4242     0.0.0.0:*  LISTEN  9241/sbcl           
tcp        0   0 localhost:domain   0.0.0.0:*  LISTEN  409/systemd-resolve 
tcp        0   0 localhost:ipp      0.0.0.0:*  LISTEN  9048/cupsd          
tcp6       0   0 ip6-localhost:ipp  [::]:*     LISTEN  9048/cupsd          
udp    29952   0 localhost:domain   0.0.0.0:*          409/systemd-resolve 
udp        0   0 0.0.0.0:ipp        0.0.0.0:*          9049/cups-browsed   
udp        0   0 0.0.0.0:60147      0.0.0.0:*          636/avahi-daemon: r 
udp        0   0 0.0.0.0:mdns       0.0.0.0:*          636/avahi-daemon: r 
udp6       0   0 [::]:41958         [::]:*             636/avahi-daemon: r 
udp6   49664   0 [::]:mdns          [::]:*             636/avahi-daemon: r 
881/containerd
9241/sbcl
409/systemd-resolve
9048/cupsd
9048/cupsd
409/systemd-resolve
9049/cups-browsed
636/avahi-daemon:
636/avahi-daemon:
636/avahi-daemon:
636/avahi-daemon:

Polecenie netstat pozwala przejrzeć wszystkie dostępy do połączeń sieciowych i potoków nazwanych. Przełączniki -lp oznaczają odpowiednio filtr na odczyt (listening) i wypisywanie ID procesu (PID). Używam sudo, aby mieć dostęp do identyfikatorów procesów innych użytkowników.

Pierwsze awk wybierze tylko połączenia TCP i UDP (odsieje potoki). Drugie, wybierze PID z nazwą procesu. Pomiędzy nie wstawiłem tee, aby zobaczyć co zwraca pierwsze polecenie awk. Urządzenie /dev/tty, odpowiada konsoli na której pracuje program. Ono pozwala obejść standardowe wejście i wyjście, które są przekierowane.

Warto jeszcze zauważyć, że polecenie tee domyślnie nadpisuje plik. Aby dopisywał, należy uruchomić go z przełączynikiem -a (append).

Zadania

Powłoka *nix wspiera współbieżne procesy. Są dwa sposoby na umieszczenie procesu w tle. Można uruchomić program od razu w tle:

~/tekst> firefox &> /dev/null &
[4] 3724

W tym przypadku przeglądarka; od razu wycinam strumienie wyjściowe, żeby nie zaśmiecały mi konsoli. & na końcu oznacza, że program ma być uruchomiony w tle. W związku z tym, w następnej linii otrzymuję numer zadania [4] i numer procesu 3724 (dostępny również przez zmienną $!).

Jeśli chcę przerwać na chwilę pracę z jakimś programem i wrócić do wiersza poleceń, mogę wcisnąć CTRL+Z. Powłoka wyświetla mi:

[1]+  Stopped                 vim it/unix/shell.txt

Zadanie 1, vim jest zatrzymane, w tle. W przypadku vima nie ma to większego sensu, ale np. w przypadku jakiegoś serwera lub przeglądarki, mógłbym chcieć wznowić pracę w tle, wówczas napiszę:

~/tekst> bg 1
[1]+ vim it/unix/shell.txt &

Natomiast by przywrócić zadanie jako główne (tj. to, które dostaje wejście z terminala):

~/tekst> fg 1

Aby wyświetlić wszystkie uruchomione zadania:

~/tekst> jobs
[1]-  Running                 firefox it/unix/shell.html &
[2]+  Stopped                 vim it/unix/shell.txt

Jest również polecenie wait, pozwalające poczekać na proces potomny:

~/tekst> sleep 30 &
[3] 3554

~/tekst> wait $!
[3]-  Done                    sleep 30

Uruchamiam w tle polecenie sleep, żeby czekało 30 sekund. Potem czekam na ten proces. Przeważnie ma to sens w skryptach, podając więcej niż jeden identyfikator. Jeśli uruchomimy to polecenie bez parametru, czeka na wszystkie zadania.

Skrypty

Jak już wspomniałem, potęga powłoki, w większości wynika z dużej możliwości pisania skryptów. Mówiąc prosto, skrypt jest kawałkiem kodu w języku powłoki, który można wykonać jak program. Tak jak inne programy ma swoje strumienie wejściowe i wyjściowe, może przyjmować parametry i zwraca kod powrotu.

W zależności od potrzeb, możemy zdefiniować jako plik wykonywalny, bądź funkcję powłoki, dodaną do skryptu startowego.

Funkcje powłoki

Przykład takiej funkcji był przytoczony tutaj. Takie funkcje zachowują się jak odrębne skrypty, tak samo przyjmują parametry, tak samo można przerwać ich działanie poleceniem exit. Chociaż mają dostęp do zmiennych zadeklarowanych w środowisku wywołującym, jeśli je zmieniają, zmiana będzie widoczna tylko w obrębie funkcji. Podobnie, umieszczenie dyrektywy set w funkcji będzie działać tylko w jej obrębie. Tylko zmienna $0 podaje nazwę pliku, nie zaś funkcji, jak można byłoby się spodziewać.

Jako plik wykonywalny

Aby stworzyć taki skrypt należy po prostu umieścić kolejne polecenia w pliku tekstowym. Wówczas możemy go uruchomić podając nazwę pliku w parametrze wywołania programu powłoki. Możemy też uczynić plik wykonywalnym i uruchomić jak zwykły program. Wtedy jednak musimy w pierwszej linijce zdefiniować, jakiego programu należy użyć do uruchomienia programu (tak samo możemy zrobić z każdym innym językiem skryptowym jak perl czy python):

~/tekst> echo 'echo Hello World' > x.sh

~/tekst> bash x.sh
Hello World

~/tekst> echo '0a                      
#!/bin/bash

.
w
q' | ed x.sh &> /dev/null

~/tekst> cat x.sh
#!/bin/bash

echo Hello World

~/tekst> chmod +x x.sh

~/tekst> ./x.sh 
Hello World

Najpierw zapisuję prostą komendę do pliku. Mogę wywołać go pierwszym sposobem. Następnie używam edytora ed aby dopisać pierwszą linijkę z deklaracją interpretera skryptu. Wywołanie cat, żeby pokazać wynik działania ed. Polecenie chmod +x nadaje prawo wykonywania pliku.

ed to kawałek historii UNIXa, którą opisałem przy okazji języka awk. Może być niedostępny w niektórych systemach. W większości dystrybucji, powinien być dostępny w menedżerze pakietów.

Jako dodatkowa konfiguracja

Jeśli uruchamiamy skrypt w powyższy sposób, zastanie on uruchomiony w odrębnej sesji powłoki (zostanie uruchomiony nowy proces). W związku z tym, nie możemy za jego pomocą dodać zmiennej, funkcji ani aliasu. Aby uruchomić skrypt w bieżącej sesji można użyć polecenia source. Alternatywnie możemy użyć samej kropki, jednak lepiej nie stosować tego w skryptach (czytelność):

~/tekst> echo '
foo="bar"
alias f="echo $foo"
' > x.sh

~/tekst> bash x.sh

~/tekst> f
f: command not found

~/tekst> echo $foo


~/tekst> . x.sh

~/tekst> f
bar

Parametry

Wartości kolejnych parametrów można uzyskać przez zmienne $1, $2, itd. Jeśli chcemy umieścić wszystkie parametry w linii poleceń, możemy użyć zmiennej $@. Czasami, również przydaje się dostęp do nazwy pliku wykonywanego, poprzez zmienną $0. To ma sens jeśli chcemy wywołać skrypt poprzez pełną ścieżkę i chcemy mieć dostęp do innych plików w tym katalogu.

~/tekst> echo '
echo \$0: $0
echo \$1: $1
echo \$2: $2
echo \$@: $@
' > x.sh

~/tekst> bash x.sh foo bar baz lorem ipsum dolor amet
$0: x.sh
$1: foo
$2: bar
$@: foo bar baz lorem ipsum dolor amet

Czasami chcemy oddzielić kilka pierwszych parametrów, a resztę razem. Możemy użyć do tego polecenia shift, które przesuwa parametry w numeracji:

~/tekst> echo '
echo skrypt $0
echo 1: $1
echo 2: $2
shift 2
echo $@
echo $0
' > x.sh

~/tekst> bash x.sh
skrypt x.sh
1:
2:

x.sh

~/tekst> bash x.sh lorem ipsum dolor amet
skrypt x.sh
1: lorem
2: ipsum
dolor amet
x.sh

Możemy również uzyskać ilość parametrów za pomocą zmiennej $#.

~/tekst> echo '
echo $#
shift 2
echo $#
' > x.sh

~/tekst> bash x.sh foo bar baz
3
1

Globy

Aby wydajnie radzić sobie z dużymi ilościami plików, mamy do dyspozycji tak zwane globy, czyli uproszczone wyrażenia regularne do dopasowywania nazw plików. Najczęściej używamy tylko jednego specjalnego znaku: *, który oznacza dowolny ciąg znaków:

ls *.html     # pliki .html
ls docs/*.txt # pliki .txt w katalogu docs
ls */*.txt    # pliki .txt we wszystkich podkatalogach

Oprócz tego, standard wspiera jeszcze znak ? oznacza jeden dowolny znak i […] pozwalający wybrać jeden z wymienionych znaków, to jednak stosunkowo rzadkie rozwiązania. bash wprowadza kilka bardzo przydatnych udogodnień. Na przykład, glob rekursywny:

shopt -s globstar # uaktywnij rozszerzenie
ls **/*.html      # pliki .html we wszystkich podkatalogach
ls {.,**}/*.html  # włączając także bieżący katalog

Konstrukcja z klamerką pozwala wymienić jedną z kilku opcji:

ls *.{txt,html} # wybierz pliki .html i .txt

Uwaga, jeśli glob nie znajdzie dopasowania, zostaje uznany za właściwą nazwę pliku! Klamerka zaś ma to do siebie, że działa jakby wpisać tam wiele odrębnych globów, więc jeśli powyższą komendę uruchomimy w katalogu gdzie nie ma żadnego pliku txt lub html to parametrach pojawi nam się *.txt lub *.html. Jeśli nie ma ani takiego ani takiego to oba.

Konstrukcja if

Często chcemy sprawdzić warunek i w zależności od tego czy jest spełniony uruchomić różne polecenia. Na przykład:

if [ $# == 3 ]; then
    echo $@
else
    echo "zła ilość parametrów" > /dev/stderr
    exit 1
fi

klauzula else jest opcjonalna. Należy pamiętać o przerwie między if, [, warunkami i ]. Właściwie [ jest programem podobnym do test (może być nawet dowiązaniem do niego). Natomiast, zamiast warunku w [], po if może pojawić się dowolne polecenie. Wówczas sprawdzane jest czy polecenie zwróciło 0. Średnik jest operatorem podobnym do && i ||, oddziela polecenia od siebie (tak samo jak znak nowej linii). Ze względu na czytelność, zwykle tak oddzielamy słowo kluczowe then.

Często możemy spotkać podwójny nawias [[ … ]]. Jest to rozszerzenie basha. Modyfikuje nieco działanie testu. Ze względu na kompatybilność, dobrze jest używać go tylko wtedy gdy jest to konieczne.

Mamy do dyspozycji różne warunki:

[ -z "$1" ] && echo "pusty/niezdefiniowany łańcuch"
[ -e "$1" ] && echo "plik istnieje"
[ -d "$1" ] && echo "$1 jest katalogiem"
[ -x "$1" ] && echo "plik jest wykonywalny"
[ -p "$1" ] && echo "plik jest potokiem nazwanym"
[ "$1" -ot "$2" ] && echo "plik \$1 jest starszy niż \$2"

Zmienne parametrów należy umieszczać w cudzysłowach, gdyż gdyby nie istniały, spowodowałyby błąd (brak parametru). Warto zwrócić uwagę, że operatory > i < służą do porównywania leksykograficznego łańcuchów znaków. Do porównywania liczb służą -gt i lt.

[ 1 -gt 5 ] && echo '1 > 5'
[ 5 -lt 1 ] && echo '5 < 1'
[ 42 -gt 1 ] && echo '42 > 1'
[ foo > bar ] && echo '"foo" > "bar"'
[ 21 > 111 ] && echo '"21" > "111"'

# 42 > 1
# "foo" > "bar"
# "21" > "111"

Pętle: for i while

W języku powłoki są trzy rodzaje pętli. Najczęściej stosuje się pętlę for:

for x in $@
do
    echo $x
done

Dla każdego elementu tablicy (tu $@), wykonaj echo $x, gdzie x jest kolejną jej wartością. Jedyna zawiłość w tym, co dla powłoki jest tablicą.

Rzadziej spotkamy pętlę while

while true
do
    read foo
    echo $foo
done

Pętla nieskończona, wczytuje ze standardowego wejścia linię i zapisuje do zmiennej foo, potem ją wypisuje. Polecam zajrzeć do help read, polecenie ma sporo ciekawych opcji.

Oczywiście, może też się pojawić warunek, tak samo jak w if:

while [ -z "$foo" ]
do
    read foo
done

Tak długo, jak zmienna foo nie ma wartości, próbuj ją wczytać. Jeśli foo od początku ma wartość, nie rób nic.

Na sam koniec, raczej rzadka konstrukcja, for w stylu C:

for ((i=0; i<10; i++))
do
    echo $i
done

Żeby dobrze skojarzyć, dodam, że podwójny nawias jest również używany przy operacjach arytmetycznych. Moim skromnym zdaniem, i jedno i drugie, w tym języku jest raczej nadmiarowe.

Tablice

W standardzie, tablica to po prostu zwykły łańcuch wartości, pooddzielanych jednym z separatorów zdefiniowanych w zmiennej IFS (domyślnie spacja, tab lub znak nowej linii). Przy czym, zachowanie jest nieco nieintuicyjne, bo:

~/tekst> IFS=",:"

~/tekst> for x in "a,b:c,d e"
do
echo $x
done
a b c d e

~/tekst> a="a,b:c,d e"

~/tekst> for x in $a
do
echo $x
done
a
b
c
d e

Natomiast bash definiuje trochę bardziej rozbudowaną składnię dla definiowania tablic. Po pierwsze, jest dostęp do poszczególnych pól (przypisanie wartości pola do nieistniejącej tablicy utworzy ją automatycznie) oraz wszystkich na raz:

show() {
    echo -n "$1: "
    eval "echo $1"
}

a[0]="foo"
a[1]="bar"

IFS=","
show '${a[0]}'
show '${a[1]}'
show '"${a[0]}"'
show '"${a[1]}"'
show '"${a[*]}"'
show '"${a[@]}"'
show '${a[*]}'
show '${a[@]}'

# ${a[0]}: foo
# ${a[1]}: bar
# "${a[0]}": foo
# "${a[1]}": bar
# "${a[*]}": foo,bar
# "${a[@]}": foo bar
# ${a[*]}: foo bar
# ${a[@]}: foo bar

Jak widać, dosyć podchwytliwa sprawa, jakich niestety wiele. Taki urok starych języków pełnych szybkich, prowizorycznych rozwiązań. Na szczęście wystarczy rozumieć co się robi i wiedzieć czego nie robić. Gorzej jeśli pracujemy z czyimś kodem. Funkcję show polecam zapisać, będziemy jej jeszcze używać. Przy czym ostrzegam, gdy uruchamiamy skrypt, skrypt startowy nie jest wykonywany, więc musimy załadować plik, używając source.

Poza tym, jest możliwość zadeklarowania pustej tablicy i pobrania rozmiaru tablicy (zarówno ilości elementów jak i sumy rozmiarów elementów):

declare -a tab

show '${#tab[*]}'
show '${#tab}'

a[0]="foo"
a[1]="bar"

show '${a[0]}'
show '${a[1]}'

show '${#a}'
show '${#a[0]}'
show '${#a[*]}'

a[${#a[*]}]="baz"

show '${a[2]}'

# ${#tab[*]}: 0
# ${#tab}: 0
# ${a[0]}: foo
# ${a[1]}: bar
# ${#a}: 3
# ${#a[0]}: 3
# ${#a[*]}: 2
# ${a[2]}: baz

Niestety nie znam sposobu, aby napisać funkcję, która dodałaby nowy element na końcu tablicy. Niestety, eval nie pozwala ustawiać zmiennych.

Jeszcze kilka spraw. Możemy zdefiniować tablicę poprzez wymienienie wszystkich wartości w nawiasie (działa tylko przy przypisaniu wartości, nie działa jako literał). Poza tym, jeśli chcemy użyć tablicy w pętli for, tylko poprzez "${tab[@]}", inaczej uzyskamy nie to czego chcemy, a jest sporo rzeczy, jakie można zrobić i dają nieoczekiwane wyniki:

a=("foo bar" "rab oof")
for x in "${a[@]}"; do
    echo $x
done

echo

# foo bar
# rab oof

show '$a'

echo

# $a: foo bar

for x in ${a[*]}; do
    echo $x
done

echo

# foo
# bar
# rab
# oof

for x in $a; do
    echo $x
done

# foo
# bar

Innymi słowy, z tablicami ostrożnie, jednak są przydatne.

Dyrektywa set

Jest kilka modyfikatorów działania powłoki. Najpopularniejsze są -e — spowoduje zatrzymanie wykonywania skryptu jeśli któryś krok się nie powiedzie; oraz -x — spowoduje wypisanie każdego polecenia przed wykonaniem, przy czym polecenie nie jest tu równoznaczne z linią. Pominięte zostaną przekierowania do pliku, a każde polecenie w potoku będzie wypisane oddzielnie, itd. Więcej w help set. W przypadku -e i -x, znaczenie pokrywa się z parametrem uruchomienia powłoki.

Funkcje arytmetyczne

Jest podstawowe wsparcie dla funkcji arytmetycznych:

$((x-2)): 40
$((x*2)): 84
$((x/2)): 21

Jednak jest bardzo toporne, raczej niewygodne, nie wspiera liczb zmiennoprzecinkowych. Do większości zadań, znacznie lepiej użyć programu bc, który daje znacznie większe możliwości. Jako, że jest to program domyślnie interaktywny, mam skrypt na tę potrzebę:

do_math() {
    echo "scale=2;$@" | bc
}

set -x

do_math 2+2            #4
do_math 2.5/2          #1.25
do_math 1/3            #.33
do_math '2*((7*7)+52)' #202

Wyjście programu jako parametr

Czasem chcemy zapisać wyjście programu do zmiennej, lub od razu zastosować jako parametr do wywołania innego polecenia. Służy do tego konstrukcja $(…) (alternatywnie `…`, ale ta forma mało się wyróżnia, więc lepiej nie używać jej w skryptach):

while read -p "wyrażenie> " expr; do
    echo $(do_math $expr)
done

Używam funkcji z poprzedniego rozdziału aby obliczyć wyrażenie. Więc w pętli wczytuję wyrażenie do obliczenia. Pętla zakończy się jeśli nie wprowadzę wyrażenia lecz CTRL-D. Parametr -p "wyrażenie> " definiuje znak zachęty jaki wygeneruje polecenie read.

Grupowanie poleceń

Niekiedy chcemy połączyć kilka poleceń, aby wspólnie korzystały ze strumieni wejściowych i wyjściowych. Aby to zrobić, musimy je pogrupować klamerką, jak w tym podrozdziale(drugi przykład). Uwaga, to spowoduje uruchomienie tych poleceń w osobnej powłoce, więc nie mogą zmodyfikować zmiennej poza jej zakresem (pracują na ich kopiach).

Praktyka

Tak naprawdę to co tej pory opisałem, to są tylko narzędzia, których używamy pracując w konsoli. Natomiast najważniejszy jest sposób myślenia, cel, który dzięki tym środkom osiągamy. Dlatego chciałbym przedstawić kilka swoich skryptów, których używam. Przedstawić problem, który zauważam i jak proponuje go rozwiązać przy pomocy powłoki. Zacznę od najprostszych. Pierwsze dwa już podałem: shrc() i show().

Przypominajka

Jedna linijka w skrypcie startowym, jak dla mnie must-have:

[ -f "$HOME/.memo" ] && cat $HOME/.memo

Myślę, że można pozostawić to bez komentarza.

Katalog jako środowisko pracy

Pracuję z różnymi danymi i tak naprawdę katalog, w którym pracuję w dużej mierze definiuje to jak chcę pracować. Stworzyłem dwie funkcje, które na różnych poziomach rozwiązują ten problem.

Po pierwsze vim. Nie znoszę jego języka skryptowego, nawet nie chce mi się go tknąć. Z resztą jego możliwości rozpoznawania z czym pracują są ograniczone. Zrobienie tego dobrze w vim scripcie to katorga. Jest lepszy sposób.

v() {
    if [[ -e "./.vimrc" ]]; then
        vim -S ./.vimrc $@
    else
        vim $@
    fi
}

Kilka linijek skryptu, które robią świetną robotę. Jeśli w katalogu istnieje plik .vimrc (kropka na początku w *nix oznacza plik ukryty), to zostanie automatycznie załadowany jeśli istnieje. Nie muszę stosować żadnych czary mary, wykrywać typu pliku. Po prostu definiuję odpowiedniej skróty i komendy, dostosowane do problemu. Czy to piszę w LaTeXu, czy w Lispie czy C. Każdy język potrzebuje zupełnie innej konfiguracji środowiska. Vim jest dla mnie narzędziem by je tworzyć. Również mogę automatycznie utworzyć konkretne pliki, w odpowiednim układzie.

Uogólniając rozwiązanie, na poziomie powłoki:

s() {
    if [[ -x "./.start.sh" ]]; then
        ./.start.sh $@
    fi
}

Jedną literką zaczynam pracę, a plik .start.sh definiuje tę pracę. Jako, że tu pracujemy z językiem powłoki mogę przytoczyć przykład z pisania artykułu, który właśnie piszę:

#!/bin/bash

declare -a html
declare -a txt

IFS=":"

if [ "$#" == 0 ]; then
    patterns=(awk shell)
else
    patterns=$@
fi


for x in "${patterns[@]}"
do
    for name in $(find . -name '*.txt' \
        | awk /$x/' {sub(/\.txt$/, "");
                printf $0 ":"}')
    do
        txt[${#txt[*]}]="$name.txt"
        html[${#html[*]}]="$name.html"
    done
done

firefox ${html[*]} &> /dev/null &
vim -S .vimrc -p ${txt[*]}

Mam pliki źródłowe *.txt, które są generowane do plików *.html, które chcę oglądać. Chcę jednocześnie podglądać wynik i edytować źródło. Pod żadnym pozorem nie chcę pisać całych ścieżek do nazw artykułów. Raczej wpisuję wzorce do wyszukania. Póki co w kodzie jest założenia (początkowo nieświadome), że każdy wzorzec dopasuje jeden wynik, inaczej to nie zadziała — póki co się nie martwię. Polecenie find wypisze rekursywnie wszystkie pliki *.txt w bieżącym katalogu. Za pomocą awk wybieram te pasujące do wzorca i usuwam rozszerzenie. Używam polecenia printf, aby oddzielać ścieżki dwukropkiem (dlatego IFS=":"), żeby uniknąć kłopotliwych nazw.

Dodaję wyniki do odpowiednich tablic. Jeśli podałem parametry to parametry są wzorcami, jako domyślne mam artykuły nad którymi teraz pracuję. Po wygenerowaniu listy plików, otwieram przeglądarkę ze wszystkimi plikami .html i vima z plikami .txt. W tym drugim, przełącznik -S .vimrc ładuje konfigurację typową dla tego projektu, a przełącznik -p sprawia, że każdy plik jest w osobnej zakładce. Parę linijek, które może zrobić potężną robotę, choć oczywiście można pójść dalej, że jeśli nie podam parametru to zostaną wybrane te, które git wskaże jako już zmienione. Pomysłów może być wiele.

Przyrost kodu

Kiedyś interesowało mnie badanie, jak mój kod rośnie z czasem. Stąd taki skrypt locsPerCommit:

#!/bin/bash
#to be ran in repo with no uncommited changes!

set -e

readarray commits < <(git log --pretty=oneline | tac)

for commit in "${commits[@]}"; do
    sha1=$(echo $commit | awk '{print $1}')
    name=$(echo $commit | awk '{$1 = ""; print $0}')

    git checkout $sha1 &> /dev/null

    loc=$(find * -type f | xargs wc -l | tail -n1 | awk '{print $1}')
    echo $loc $name
done

Najpierw tworzę tablicę commits przy pomocy polecenia git log uzyskuję listę commitów w bieżącej gałęzi. Używam opcji --pretty=oneline żeby na każdy commit była tylko jedna linia. Pocenie tac sprawia odwrócenie listy (tac to odwrócone cat).

Dalej, dla każdego elementu, przy pomocy awk wyodrębniam tytuł i hash bieżącego commita. Potem robię checkout na danym commicie. Aby obliczyć liczbę linii, najpierw używam polecenia find. -type f oznacza tylko pliki. Polecenie xargs wywołuje polecenie wc -l (podaj ilość linii w danym pliku) z parametrami z kolejnych wyników find. wc działa tak, że najpierw wypisze pojedyncze wyniki, a na końcu poda sumę. Dlatego używam polecenia tail -n1 aby pozostawić tylko ostatnią linię. Jako, że wc zwraca wynik w postaci ilość-nazwa, używam polecenia awk by wybrać tylko ilość linii. Na koniec wypisuję wynik. Proste i działa.

Na przykład dla mojego generatora stron:

~/tekst/home> locsPerCommit 
368 read simple markdown-like format and store in a structure
370 Build simple html document from article
393 stylesheet!
392 separeate style, add footer parameter
579 apply oo design
602 make script
612 refactor: separate blank-line?, nonblank-line? and orf
613 smarter output file
629 refactor: separate chunk class
649 css adjustments
674 support %italics%
699 secondary headers (h2) and embedded audio
720 escape < and > in text
971 refactor: nice OO interface for line->chunk->html contversion
981 recognize title
988 refactor: separate string-iterator related stuff
1022 allow creating tables of content
1272 readme
1319 index, links, images
1346 enable poem blocks
1381 sort index by date
1396 recursively compile index-linked files
1426 make it possible to load from another directory
1434 titled links
1440 wrap lines in code section
1461 create 2 level TOCs
1462 ul style
1462 fix parent-direcory link
1611 regex facility
1613 regex: :nil symbol as standard ^ and $
1628 use regex instead of ~format
1640 better preparing line of code
1641 interpret & in input literaly
1647 cope with wrong/missing date in input file
1650 error message for failed spechar

Co to słowo znaczy?

Lubię się uczyć języków, na co dzień posługuję się trzema. Mimo wszystko, nawet po latach, czasem zbraknie słowa (zwłaszcza czytając poezję). Stworzyłem skrypt, który pobiera z ulubionego słownika definicję podanego słowa:

#!/bin/sh

word=$1
url='https://www.thefreedictionary.com'
if [ ! -z "$word_stdurl" ]; then
    url=$word_stdurl
fi

if [ -z "$word" ]; then
    echo "$0 <word>"
    echo "    look for a word in dictionary."
    echo "    designed for thefreedictionary"
    echo "    URL can be changed by exporting $word_stdurl"
    exit 1
fi

curl $url/$word \
    | grep 'div id="Definition"' \
    | sed -e 's/<a[^>]*>/;/g' \
          -e 's/<[^>]*>//g'     -e 's/[ \t]\+/ /g' \
          -e 's/&[^;]\+;/ /g'   -e 's/(*[0-9]\+)*/\n&>/g' \
    | grep -vP "^[\d\s]*$" \
    | less 

# SED code: not obvious, right?
#            1. put semicolon instead of <a> rendered they
#               separate using style.
#            2. remove all tags
#            3. compress all white space
#            4. remove HTML entities
#            5. break line for each number found
#
# GREP out all blank or digit-only lines.

Ten kod jest dostosowany do konkretnej strony (http://thefreedictionary.com). Pobieram linię w której znajduję interesujące mnie id. Usuwam wszystkie tagi i encje, pozostawiam sam tekst. Ostatecznie dostaję wyciąg. Nie jest to idealne, ale działa. Na pewno niedługo przygotuję bardziej rozbudowane rozwiązanie, bo będę potrzebował zrobić to masowo, prawdopodobnie dla setek słów (to niesamowite, że człowiek mówi biegle w jakimś języku, a nie zna tylu słów).

Warto zapamiętać curl. Najbardziej podstawowy klient HTTP. Bardzo przydatny w testowaniu WWW, REST, itd. Tu akurat nie ma żadnych parametrów, bo mam proste żądanie GET.

Nieczytelny internet (curl, wget, youtube-dl)

Nie lubię większości współczesnych stron internetowych. Zauważyłem, że niekiedy potrafię olać dobry artykuł tylko ze względu na złe formatowanie strony. Dlatego powstał readablize:

#!/bin/sh

# download page and make it more readable

url="$1"
name="$2.html"

if [[ -z "$1" ]]; then
    echo "Usage: readabilize url [name]"
    echo
    echo "downloads site from url to READABILIZE_DIR and"
    echo "inject readabilize.css to it's code. File can"
    echo "be found with used font in extas/readabilize."
    echo "It (or your version) should be placed in READABILIZE_DIR"
    echo "If given name, file will be stored ass \$name.html"
    echo "In the end page is run in browser of choice"
fi

if [[ -z "$READABILIZE_DIR" ]] || [[ -z "$BROWSER" ]]; then
    echo "please make sure that following evironment values are set
    READABILIZE_DIR – directory where documents are downloaded to
        and where style data is stored.
    BROWSER – command used to run web browser"

    exit 1
fi

if [[ -z $2 ]]; then
    name=$(echo $url | sed 's#^.*/##')
fi

head_regx='</head'
css_include='<link rel="stylesheet" type="text/css" href="readabilize.css">'

curl "$1" \
    | sed -e 's#<link[^>]+rel="stylesheet"[^>]\+>##' \
          -e "s#$head_regx#$css_include$head_regx#" \
    > "$READABILIZE_DIR/$name"

$BROWSER "$READABILIZE_DIR/$name" &

Dużo sprawdzania konfiguracji, pomocy. Sens jest w tym by pobrać stronę (znów curl) i przy pomocy sed wyciąć wszystkie arkusze stylów, podstawić swoje i zapisać do pliku, a następnie otworzyć w przeglądarce. Jednocześnie lepiej czytać i archiwizacja. Praktycznie i w większości przypadków działało lepiej niż dedykowane temu wtyczki. Przydałoby się usuwać style wpisane w znaczniku i atrybutach style, a nawet przedpotopowych znacznikach modyfikujących styl, bo i takie strony niekiedy się spotyka. Z drugiej strony, dotąd nie było to potrzebne.

Skoro już o curl mowa, warto wspomnieć jeszcze co najmniej o dwóch narzędziach związanych z WWW. Na przykład wget pozwala ściągać całe strony internetowe, rekursywnie ściągając podstrony, modyfikując przy tym linki, aby można było stronę swobodnie oglądać w trybie offline. Warto przejrzeć podręcznik tego programu, ja nadmienię tylko najistotniejsze dla mnie przełączniki: -r — ściągaj rekursywnie podstrony; -c wznów wcześniej zaczęte pobieranie. Dla rekursywnego pobierania, -l n określa maksymalny poziom zagłębienia rekursji n; -L spowoduje ściąganie tylko linków o względnych adresach; opcje -I i -X z kolei pozwalają wyznaczyć poszczególne podkatalogi, które powinny lub niepowinny być ściągne. Czasem może się przydać przełącznik -U, pozwalający zmienić User Agent. Spotkałem się już ze stroną, która odrzucała żądania od wgeta. Wystarczyło dodać -U "Mozilla Firefox".

Innym przydatnym narzędziem jest youtube-dl, pozwalający ściągać filmy zagnieżdżone na stronach (nie tylko YouTube, również facebook, dailymotion i wiele innych). Najczęstsze opcje to -x — ściągnij tylko ścieżkę audio; -F — wypisz wszystkie dostępne formaty i -f wybiera konkretny format — to szczególnie przydatne jeśli mamy ograniczony transfer. -u i -p pozwalają wprowadzić nazwę użytkownika i hasło, na przykład na facebooku, gdzie konieczne jest zalogowanie. odpowiednio listy katalogów, w obrębie których chcemy ściągać i pomijać.

Automatyczna obróbka grafiki (imagemagick)

Jest kilka bardzo przydatnych narzędzi do obróbki plików graficznych. Najbardziej rozbudowany jest imagemagick, dedykowany grafice rastrowej. Ma ogromne możliwości, ja wymienię najprostsze przykłady. Przede wszystkim:

for x in *.jpg
do
    convert "$x" -resize 500 "scaled/$x"
done

Przeskaluj wszystkie pliki jpg do szerokości 500px. Gdyby użyć -scale 50%, pomniejszylibyśmy wszystkie obrazki o połowę. Przy okazji możemy poprawić jasność, kontrast i nasycenie (parametry -brightness, -saturation, -modulate).

W pakiecie jest narzędzie import pozwalające robić zrzuty ekranu. Domyślnie, po jego wywołaniu pojawia się kursor, należy wówczas kliknąć na okno, któremu chcemy zrobić zrzut ekranu. Przełącznikiem -screen możemy zrobić zrzut całego ekranu. Pozwala również robić większą ilość zrzutów w odstępach czasu. Możemy też zdefiniować okno po id. Niestety niezależnie od tych opcji, i tak musimy kliknąć żeby zacząć proces.

Już kilkakrotnie pisałem programy tworzące różną grafikę. Zwykle chcemy mieć duży wybór formatów wyjściowych. Czego użyć do tego? Kilku bibliotek pokroju libpng? Niby imagemagick też występuje jako biblioteka, jednak ma ten mankament, że choć ma interfejs dla wielu języków, każdy jest diametralnie różny. Ostatecznie zdecydowałem, że najprostszym rozwiązaniem jest napisać program, który zwraca na wyjściu obrazy w formacie ppm (banalny w implementacji) i złączyć ją skryptem z konwerterem. Aby zilustrować prostotę takiego rozwiązania, szybki skrypt:

#!/bin/bash

w=100
h=100

do_math() {
    echo "scale=2;$@" | bc | sed 's/\..*$//'
}

{
    #nagłówek
    echo -en "P6\\n$w $h\\n255\\n"

    for ((x=0; x<$w; x++)); do
        for ((y=0; y<$h; y++)); do
            echo -en "x,y: $x $y\\r" >&2
            r=$(do_math "$x / ($w./255)")
            g=$(do_math "$y / ($h./255)")
            b=$((RANDOM%256))

            for c in $r $g $b; do
                printf \\x$(printf "%x" $c)
            done
        done
    done

} | convert -format PPM - -scale 200% x.jpg

Skrypt ilustruje też słabą wydajność powłoki do takich zadań. Na i5 skrypt wykonywał się 1,5 minuty i to zdecydowanie strona powłoki jest wąskim gardłem (wystarczy odjąć polecenie convert żeby się przekonać). W C spokojnie zapisywałem gradient tego rodzaju wprost do bufora ramki z wydajnością zapewniającą natychmiastowe pojawienie się całego obrazu.

Wywoływanie fotografii w formacie RAW

Miłośnicy fotografii mogą docenić, że choć otwartoźródłowy program rawtherapee może nie dorównuje produktowi pewnego monopolisty, ale za to sprzyja tworzeniu skryptów. Jest trochę toporny, bo zamiast pozwolić ustalić poszczególne parametry wywoływania, pozwala tylko zdefiniować plik profilu. Na szczęście ten ostatni jest w prostym formacie tekstowym i można go generować automatycznie, choć czasami trudno dojść jak osiągnąć dany efekt, bo nie ma dokumentacji tego formatu.

~/Obrazy> rawtherapee-cli -Y -s -c *.PEF &> /dev/null

Przełączniki -Y -s odpowiednio oznaczają zastępowanie istniejących plików i użycie profilu z pliku o nazwie z dodanym rozszerzeniem .pp3 (można użyć -p plik.pp3 aby zastosować jeden profil dla wszystkich). Parametr -c występuje zawsze i rozpoczyna listę plików do przetworzenia.

Wdrożenie zmian na stronie (SSH, SCP, md5sum, tar)

Edytuję stronę lokalnie, chcę zamieścić zmiany na zdalnym serwerze. Oczywiście, chcę wysyłać tylko te pliki, które się zmieniły:

#!/bin/bash

# Dane serwera, do uzupelnienia
user="…"
host="…"
doc_path="…"
port="…"

# Nazwa pliku z hashami wersji plików na serwerze
deploy_lst=".deployed.lst"

declare -a add_lst
declare -a all
declare -a all_hash

IFS=":"

# Iteruję po wszystkich plikach, które mogą być istotne na stronie.
# Szukam hasha wcześniejszej wersji (deployed_hash) i porównuję z
# hashem bieżącej wersji (preasent hash), używam polecenia md5sum
# W tablicy all zapisuję wszystkie pliki do późniejszego sprawdzenia,
# które pliki na serwerze trzeba usunąć.
# W tablicy all_hash zapisuję hashe nowych wersji do zapisania po
# udanym zastosowaniu zmian na serwerze.
# Niestety takie skomplikowane globy nie są tak mądre jak by się
# mogło zdawać. Muszą być ewaluowane do serii mniejszych globów,
# przez co, jeśli w katalogu bieżącym nie ma żadnego pliku png,
# to w jeden z iteracji file przyjmie wartość ./*.png. Taki plik
# nie istnieje, dlatego muszę sprawdzić na końcu czy preasent_hash
# ma wartość. Wcześniej o to nie dbam, bo w deployed_ls na pewno
# go nie ma.
for file in {.,**}/*.{png,mp3,html}
do
    deployed_hash=$(awk "\$2 == \"$file\" {print \$1}" $deploy_lst)
    preasent_hash=$(md5sum $file | awk '{print $1}')
    if [ "$deployed_hash" != "$preasent_hash" ]; then
        add_lst[${#add_lst[*]}]="$file"
    fi

    all[${#all[*]}]="$file"

    if [ ! -z "$preasent_hash" ]; then
        all_hash[${#all_hash[*]}]="$preasent_hash $file"
    fi
done

# Iteruję po wszystkich plikach w bazie i sprawdzam, które już nie
# istnieją lokalnie, zapisuję je w del_lst.
for file in $(awk '{printf $2 ":"}' $deploy_lst)
do
    hit=0

    for x in "${all[@]}"
    do
        if [ "$file" == "$x" ]; then
            hit=1
        fi
    done

    if [ "$hit" == 0 ]; then
        del_lst[${#del_lst[@]}]="$file"
    fi
done

# Od tej pory nie toleruję żadnych błędów.
set -e

# Tworzę archiwum z plików do wysłania na serwer.
# Tar zwróci błąd jeśli nie ma plików do dodania.
tar cvf update.tar "${add_lst[@]}"

# Wysyłam archiwum na serwer
scp -P $port update.tar $user@$host:$doc_path

# rozpakowuję archiwum i usuwam je potem.
echo "cd $doc_path && tar xvf update.tar && rm update.tar" \
         | ssh -p $port $user@$host 

# zapisuję zmiany w pliku wersji plików zdalnych.
for hash in "${all_hash[@]}"
do
    echo $hash
done > $deploy_lst

rm update.tar

Nie jest to może najlepsza implementacja, myślę że na granicy rozsądku (bardziej skomplikowaną rzecz już należałoby zrobić w języku ogólnego przeznaczenia). Jednak działa i myślę, że jako przykład w sam raz, bo pokazuje co najmniej kilka ważnych narzędzi.

Używając tego skryptu trzeba uważać żeby mieć aktualną wersję bazy plików na zdalnym serwerze. W przeciwnym wypadku mogą zostać na serwerze nieużywane pliki (w sumie nie bardzo wielka szkoda, ale jednak). Do bardziej złożonych sytuacji, może być sens zastosować rsync (też może działać przez ssh), ale w takim przypadku to chyba byłaby przesada. Dla mnie na pewno, bo nigdy go nie używałem. Przeczytanie instrukcji obsługi trwałoby więcej niż napisanie takiego skryptu, bez sensu.

Również, zamiast bawić się w manualne sprawdzanie hashy, można by użyć gita. Z drugiej strony, jeśli używam gita do kontroli wersji dokumentów i drugiego repozytorium do kontroli generatora (jak w moim przypadku) mogłoby to pokomplikować logikę. Tutaj mam najprostsze podejście jakie umiem sobie wyobrazić.

Obróbka audio/video, napisy

Są dwa bardzo potężne narzędzia ffmpeg i mencoder. Jest również sox, dedykowany wyłącznie dźwiękowi. Nie wnikam w pomniejsze program, takie jak (de)kodery poszczególnych formatów. Podobnie jak w przypadku imagemagick, pewnie nie pokryję nawet promila ich możliwości.

Oczywiście, można użyć tych narzędzi do prostych zadań, takich jak zmiana formatu, kompresja, wgrywanie napisów itp. Jednak ja poszedłem o krok dalej i napisałem sobie skrypty do montowania filmów w konsoli. Brzmi dziwnie? Jak dla mnie, jest to rozwiązanie wcale wygodne. Po pierwsze, taki skrypt ma bardzo małe wymagania sprzętowe (tylko renderowanie wyniku końcowego trwa, ale od tego uciec nie sposób). Po drugie, wolę skrypt, który zakłada rutynę, którą stosuję w 99% przypadków, aniżeli mieć jakiś klikany interfejs, z którego 99% opcji nigdy nie użyję; 1% zawszę mogę dopisać kiedyś, albo w osobnym skrypcie.

I tak, rutyna jest bardzo prosta, wybierz plik źródłowy, wybierz ujęcie, wybierz kadr, filtry graficzne i utnij gdzie trzeba. I tak w kółko. W procesie powstaje mi plik tekstowy z listą ujęć, w którym mogę nanosić poprawki.

Wybranie granic ujęcia jest proste, dla każdego pliku zapisuję, w którym momencie wykonałem ostatnie cięcie i od tego miejsca uruchamiam mplayer (opcja -ss). Za pierwszym razem zamykam w miejscu początku (mplayer cały czas wypisuje w konsoli bieżącą pozycję, więc mogę ją odczytać). mplayer. uruchamia się ponownie, aby wybrać miejsce końca ujęcia. Żeby zwiększyć precyzję, wokół obu punktów robię zrzut klatek z kilku sekund przed i po, żeby móc precyzyjnie określić miejsce cięcia. Podobnej procedury używam do tworzenia napisów.

Mplayer pozwala wyświetlać film z nałożonymi filtrami w locie i to nie będzie działać tylko na bardzo słabej maszynie (na przykład Raspberry pi), więc bez problemu mogę oglądać poszczególne ujęcia i oglądać po kolei. Jeśli nie mogę znieść tej półsekundowej przerwy (a mogę) między ujęciami mogę wyrenderować film w mniejszej jakości, żeby było szybciej. Na moje skromne potrzeby to jest wystarczające i używam zaledwie podstawowej składni ffmpeg: Ile razy chcę przełącznik -i z plikiem wejściowym, a dla niego -ss (początek) -t czas trwania, -vf definicja filtrów video (trzeba poczytać w instrukcji), -acodec/-vcodec do ustawienia kodeków i plik wyjściowy. Nawet mplayer ma ciekawe opcje ułatwiające wybieranie kadrów. Muszę tylko pamiętać, żeby pracować na nieskompresowanych plikach, bo inaczej będzie się dziwnie zachowywać.

Jest tylko jedna poważna wada, różne implementacje kodeków, różne wersje potrafią się zachowywać inaczej i skrypt może nie działać po migracji (już raz tak miałem), ale cóż. I tak jestem zadowolony i sobie radzę. Pełen kod źródłowy możecie znaleźć tutaj: https://github.com/BartekLew/vidkit. Szybki opis poszczególnych skryptów: vidtake – stwórz nowe ujęcie; vidvid — wyrenderuj film, vidsub — rób napisy; vidshow — wyświetl ujęcie; vidcrop — dostosuj kadr. Reszta ma znaczenie wewnętrzne: vidcode wygeneruj kod dodający zmienne z parametrami danego ujęcia; vidls — dostosuj dokładne miejsce cięcia na podstawie zrzutów. I nie musicie mówić, kod taki sobie (to było jakiś czas temu), a logikę powinienem przepisać w jakimś lepszym języku. Jak kiedyś znów będę potrzebować tego programu pewnie to zrobię. :)

Podsumowanie

Powłoka to potężne narzędzie, ale przede wszystkim sposób myślenia, w którym małe części, stworzone do łatwego komponowania, łączyć w większe rozwiązania. Dzięki temu, że środowisko open-source stworzyło wiele ciekawych narzędzi. Często może się okazać, że stworzenie prostego narzędzia, które opisuje tylko co zrobić i złączyć je z gotowym programem za pomocą skryptu i po krzyku. Żadnego uczenia się skomplikowanych bibliotek — wszystko to programy i przełączniki, które można najpierw poćwiczyć pisząc komendy ręcznie. Jeśli przyłożymy się do dostosowywania powłoki do swoich potrzeb, szybko stanie się bardzo wygodnym narzędziem, a mnogość różnych poleceń i przełączników nie będzie wielkim problemem, bo większość z nich będzie zaszyta w skryptach, które już implementują nasze zwyczaje, przez co z łatwością zapamiętamy jak je używać.

W tym wszystkim pozostaje tylko jeden haczyk, od początku trzeba wiedzieć co się chce zrobić. Może nie jest to najłatwiejsze, ale czy nie jest to przydatny nawyk? Uważam, że bardzo mi pomaga i czyni lepszym programistą. Po latach, nie wyobrażam sobie pracy bez konsoli.