My Profile Photo

kubuszok.pl


Na codzień po prostu deweloper bez żadnego X z przodu.

Lubię uczyć się nowych rzeczy, zwłaszcza bardziej abstrakcyjnych jak matematyka czy algorytmika.

Obecnie pracuję w Scali, gdyż dla mnie programowanie funkcyjne jest bliższe matematyki, bardziej estetyczne i czyste. Mam również doświadczenie komercyne w pracy z językami imperatywnymi jak Java i C++, jak również kilka projektów w innych językach.


Narzędzia programisty: terminal

Najbardziej zaawansowane opcje zazwyczaj nie są dostępne w ładnym interfejsie do wyklikania. Kiedy w grę wchodzi pierdyliard opcji prościej jest je dodać jako kolejny argument do programu. I często jest to też szybsze w użyciu, niż znalezienie opcji zakopanej gdzieś w okienkach. To tylko 2 z powodów, dla których na dłuższą metę musisz zaprzyjaźnić się z oknem terminala.

Odrobina historii

Dawno, dawno temu, kiedy bogowie chodzili po ziemi i rozwijali Uniksa, wszystko było tekstem. Znaczy, formalnie wszystko było plikiem: katalogi, normalne pliki, procesy, dyski itd., ale oglądaliśmy głównie tekst.

W najprostszym przypadku widzieliśmy znak zachęty, coś w tym stylu:

$ 

gdzie po $ mogliśmy wpisać polecenie do wykonania przez system. Później na systemach uniksowych rozwinął się X-Window System, który pozwalał używać graficznego interfejsu użytkownika, zaś na DOSa powstała nakładka pod nazwą Windows 1.0. Ponieważ DOS był od samego początku dość… ubogi, w porównaniu do Uniksa, na Windowsie interfejs graficzny znacznie bardziej zdominował interakcję z komputerem.

Po jakimś czasie Windows przestał być nakładką na DOSa i nie był zdolny do uruchomienia się bez trybu graficznego, zaś sam DOS uruchamiał się niejako wewnątrz Windowsa (choć, z tego co pamiętam osobny DOS był używany przy starcie komputera do uruchomienia systemu Windows…).

Na uniksach tymczasem tryby graficzny oraz tekstowy istniały obok siebie. Obecnie w Linuksie mogę wybierać między jedną z kilku powłok - jedna z nich jest (na desktopach) używana do uruchamiania trybu graficznego, pozostałe zaś są wyłącznie tekstowe. Mogę się między nimi swobodnie przełączać. Dla wygody jednak mogę też używać komend w oknie wewnątrz tzw. emulatora terminala.

Spróbujmy pobawić się terminalem. Powłoką z jaką zaczniemy naszą przygodę będzie bash (Bourne Again Shell).

Jak zacząć używać terminala na Windowsie?

Jeżeli pracujesz na Windowsie i chcesz używać terminala, to przyjmij ode mnie wyrazy współczucia. Domyślnie masz do wyboru DOSa (nie rób tego), oraz PowerShella (do automatyzacji zadań nawet się nadaje, ale w sumie nie czułem się w nim nigdy tak wygodnie jak na powłokach uniksowych).

Jeśli jednak musisz siedzieć na Windowsie, i nie chcesz bawić się w stawianie maszyny wirtualnej do deweloperki, pewnym rozwiązaniem jest instalacja Cygwina. Ponieważ wbudowane okno basha jest dość słabe, dodatkowo zainstaluj sobie ConEmu. Ktoś nawet udostępnił wersję ze wstępnymi tweakami pod nazwą Cmder.

Po zainstalowaniu obu, uruchamiamy ConEmu i w opcjach dodajemy sobie nowego taska, jako polecenie ustawiamy "[katalog z cygwinem]\bin\bash.exe" --login -i. Po uruchomieniu taska będziemy mieli normalną konsolę.

Jak zacząć używać terminala na Linuksie?

Jeśli jesteś w trybie tekstowym - le voila! W trybie graficznym wybierz po prostu Terminal z menu.

Jak zacząć używać terminala na MacOS?

Zapytaj pomocy technicznej Apple.

Mam terminal, co dalej?

Na początek rozejrzyjmy się nieco.

$ pwd
/home/dev

Polecenie pwd wyświetliło katalog w którym obecnie znajduje się terminal (coś jak przy przeglądaniu plików) - print working directory. Po starcie jest to katalog domowy bieżącego użytkownika, czyli katalog z taką nazwą, jak nazwa użytkownika wewnątrz katalogu /home. Jak widać na moim komputerze moją nazwą użytkownika jest dev.

$ ls
Desktop Downloads fontconfig Movies

Polecenie ls listuje zawartość katalogu roboczego - jak widać w soim katalogu domowym mam pliki Desktop, Downloads, fontconfig, Movies.

Gapienie się na jeden katalog byłoby jednak dość mało praktyczne, więc możemy przenieść się gdzie indziej przy pomocy polecenia cd (change directory).

$ cd Downloads

Przeniesie nas do podkatalogu Downloads. Aby przejść wyżej musimy wpisać cd ... .. oznacza katalog wyżej. Pojedyncza kropka . oznacza ten sam katalog. Jeśli chcemy połączyć te czynności łączymy ścieżki przez /. I tak:

  • a/b oznacza przejdź do podkatalogu a, a następnie do podkatalogu b,
  • ../b oznacz wyjdź a obecnego katalogu, a następnie wejdź w podkatalog b
  • ./a jest (zazwyczaj) tym samym co a.

Samo / bez niczego oznacza główny katalog systemu. Na uniksach nie ma podziału na dyski C:, D:, itd. Wszystko jest (tak jakby) w jednym katalogu.

O tym oznaczeniach warto pamiętać także przy tworzeniu stron WWW, bo tak samo zachowują się strony URLe przy przechodzeniu z jednej podstrony do drugiej.

Czy poza wbudowanymi polecaniami można uruchamiać normalne programy? Można, a jakże. Załóżmy, że w naszym katalogu znajduje się kalkulator calc:

$ ls
calc

W takim wypadku uruchamiamy go poleceniem:

./calc

Tu rodzi się pytanie: dlaczego nie samo calc? Twórcy powłoki zauważyli pewien problem. Co, jeśli jakiś złośliwiec stworzył wirusa, nazwał go cd i wrzucił do katalogu do którego często zaglądasz? Wówczas chcąc przejść do innego katalogu przypadkiem uruchomisz wirusa! Aby uniknąć takich pomyłek tylko programy wbudowane mogą być uruchomione bez podawania konkretnej ścieżki (czyt. ../katalog/program też zadziała).

A właśnie. Wbudowane. Tak naprawdę bash - bo tak się nazywa powłoka na której pracujemy - nie ma za bardzo wbudowanych poleceń (w przeciwieństwie do np. DOSa). Tak naprawdę posiada on po prostu listę katalogów, które ma po kolei przeszukać i jeśli w którymś z nich znajdzie program o nazwie takiej jak polecenie, które próbujemy wywołać, wówczas go uruchomi. Jak możemy sprawdzić tą listę?

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

: rozdziela od siebie poszczególne katalogi. Ale czym tak naprawdę jest PATH?

Jest to tzw. zmienna środowiskowa. Para nazwa-wartość, do której dostęp ma terminal i uruchamiane przez niego programy. Po wpisaniu polecenia env zobaczymy, że jest ich zdefiniowanych znacznie więcej.

(Gdybyśmy chcieli nadpisać/zdefiniować nową zmienną wystarczy wpisać zmienna=wartość, lub też export zmienna=wartość, jeśli chcemy aby programy uruchomione w terminalu również mogły ją zobaczyć)

Co z kolei robi $ przed nazwą zmiennej? W dużym skrócie podstawia tę wartość. Jak łatwo zgadnąć polecenie echo po prostu bierze wszystkie argumenty jakie mu przekażemy i przekazuje je na standardowe wyjście.

$ echo test 1 2 3
test 1 2 3

To rodzi kolejne pytanie: co to jest standardowe wyjście?

Wszystko jest plikiem

Kiedy uruchamiamy program tekstowy zazwyczaj będzie on brał dane (tekst) z tego, co wpiszemy na klawiaturze i wypisywał to bezpośrednio na ekran. Spróbujmy z poleceniem cat:

  • wpiszmy dowolny tekst i naciśnijmy enter,
  • zobaczymy, że cat wzięło to, co napisaliśmy wcześniej i wydrukowało na ekranie,
  • możemy tak powtórzyć kilka razy,
  • kiedy znudzi się nam zabawa wciśnijmy enter, a następnie ctrl+d.

No dobra, ale to mało ciekawe. Możemy coś tutaj zmienić?

  • wpiszmy w terminal cat > test.txt,
  • ponownie wpiszmy parę razy dowolny tekst i naciśnijmy enter,
  • na koniec po ostatnim enterze wciśnijmy ctrl+d.

Tym razem tekst nie duplikował się. Jednak jeśli zajrzymy do katalogu, jaki był otwarty w terminalu zobaczymy, że znajduje się w nim plik test.txt, a dokładnie taką treścią, jaką wpisywaliśmy w terminalu.

Przeprowadźmy jeszcze jeden eksperyment.

  • wpiszmy w terminal cat < text.txt.

Tym razem wyświetliło nam zawartość pliku text.txt. Co dokładnie się stało?

W Uniksie, a przynajmniej jego filozofii, panuje zasada wszystko jest plikiem. Z punktu widzenia systemu, a także wszystkich programów jakie w nim uruchamiamy, przed uruchomieniem programu mamy pewien plik z zawartością jaka zostanie skierowana przekazana programowi. Jednocześnie wszystko co program wyprodukuje również jest zapisywane do pliku. Kiedy jednak te pliki nie są podane wprost, klawiatura udaje, że jest takim plikiem wejściowym, zaś ekran terminala przejmuje rolę pliku wyjściowego. Te ciągi znaków (liter) noszą nazwę strumieni: pierwszy plik/klawiatura stanowią strumień wejściowy, zaś drugi plik/ekran to strumień wyjściowy. W przypadku wartości domyślnych (klawiatura/ekran) możemy również użyć nazw standardowe wejście i standardowe wyjście. Istnieje jednak jeszcze jeden strumień.

Spróbujmy wywołać następujące polecenie:

ls folder-ktorego-nie-ma > test.txt

Cała zawartość folder-ktorego-nie-ma zostałaby wówczas przekierowana do pliku test.txt… ale przecież takiego katalogu nie ma. Co się więc stanie?

Po prostu, ls wyświetli nam komunikat o błędzie:

$ ls folder-ktorego-nie-ma > test.txt
ls: cannot access 'folder-ktorego-nie-ma': No such file or directory

Gdyby ten komunikat znalazł się w strumieniu wyjściowym nie zobaczylibyśmy co poszło nie tak. Twórcy Unika przewidzieli takie sytuacje, dlatego też istnieje strumień błędów, zaś terminal jest standardowym wyjściem błędów.

Gdybyśmy chcieli przekierować błędy do osobnego pliku moglibyśmy to zrobić w ten sposób:

ls > output.txt 2> errors.txt

Z kolei w szczególnym przypadku gdyby oba strumienie miały być przekierowane do tego samego pliku:

ls > output.txt 2>&1

Wróćmy jeszcze na chwilę do polecenia cat. Wykorzystaliśmy je do przekazywania wpisywanych z klawiatury danych na wyjście. Jest to tak naprawdę tylko jedno z jego zastosowań: kiedy nie przekażemy poleceniu cat żadnych argumentów, bierze dane wyłącznie ze strumienia wejścia. Jednak możemy przekazać mu takie parametry:

cat plik1.txt plik2.txt - plik3.txt

Zakładając, że pliki istnieją polecenie wydrukuje na wyjście zawartość plik1.txt, następnie plik2.txt, później wpisaną przez nas treść (zwróćmy uwagę na -), a na końcu plik3.txt. Prawdziwym celem istnienia polecenia cat jest konkatenacja (concatenation) plików - czyli sklejanie kilku ciągów znaków w jeden.

Uzbrojeni w powyższą wiedzę powinniśmy zrozumieć, co zrobiłoby polecenie:

cat plik1 - < plik2 > plik3 2> plik4

Została nam jeszcze jedna rzecz do omówienia: co jeśli chcielibyśmy, aby wyjście jednego programu, było wyjściem drugiego? Moglibyśmy w teorii użyć jakiegoś pliku tymczasowego:

ls > test
cat < test

Ale jeżeli plik tymczasowy zajmowałby np. 50TB i nie mieścił się na naszym dysku byłoby to słabe. Zwłaszcza, że nie jest nam do niczego potrzebny po jego przetworzeniu. Na szczęście jest do tego specjalny operator |:

ls | cat

Przekierowujemy wyjście ls na wejście cat. Strumień błędów wychodzi standardowo na terminal informując nas o wszelkich potencjalnych problemach.

Osobom korzystającym z interfejsów graficznych ta idea może się wydawać nieintuicyjna. W programach z GUI zazwyczaj całą funkcjonalność jest zintegrowana w jednym miejscu i udostępnia ona od razu wszystkie opcje przewidziane przez autorów programu. Problem w tym, że gdy tylko próbują dodać do nich nową funkcjonalność, muszą ja sensownie zintegrować ze wszystkimi już istniejącymi funkcjonalnościami - większość użytkowników naprawdę potrzebuje zaledwie części z nich, ale ponieważ różni użytkownicy potrzebują różnych kombinacji, autorzy muszą albo wspierać wszystkie, albo zostawić część użytkowników nieusatysfakcjonowanych.

Unix pierwotnie opierał się na filozofii zrób jedną rzecz, ale rób ją dobrze - przełożyło się to projektowanie programów, które skupiały się na konkretnych funkcjonalnościach, ale które można było łatwo ze sobą łączyć, aby uzyskać upragnioną funkcjonalność. Powiedziałbym, że Unix i jego pochodne nie odniosłyby takiego sukcesu, gdyby nie m.in. łatwość składania potrzebnej funkcjonalności z prostych dostępnych już w momencie instalacji bloczków.

Podsumowanie

Nie jest to oczywiście wyczerpujący opis tego co można zrobić z terminalem. Jest to tylko bardzo powierzchowny wstęp do jednej tylko powłoki, basha. Powinien on jednak wystarczyć, abyśmy mogli swobodnie nawigować po katalogach i uruchamiać programy tekstowe (a przy programowaniu prawie wszystkie programy jakie będziemy uruchamiać taki będą).