Wstęp
O autorze, o książce
Kiedy miałem siedem lat, zostałem dumnym posiadaczem swojego pierwszego, własnego komputera Atari 800XL wraz z magnetofonem oraz dwoma dżojstikami; do zestawu był również dołączony zestaw kaset z przeróżnymi grami, takimi jak River Ride, Bruce Lee czy Archon. Wczytanie gry trwało zazwyczaj od pięciu do dziesięciu minut, a więc całą wieczność dla niecierpliwego dziecka. Na szczęście jedna z gier nie wymagała kasety i była dostępna od razu po włączeniu komputera - nazywała się Atari BASIC i była grą w programowanie. Co więcej, była do niej dołączona instrukcja w formie zdobytej cudem kserokopii książki, która dla osoby rozpoznającej litery, ale niekoniecznie umiejącej złożyć je w wyrazy, stanowiła zagadkę, tak samo jak BASIC. Niemniej jednak umiejętność czytania nie jest potrzebna, by przepisywać kolejne znaki z przykładowych listingów, co też z upodobaniem czyniłem - najpierw z kserówek, a potem z kolejnych numerów czasopisma "Bajtek", po które co miesiąc udawałem się wraz z którymś z rodziców do pobliskiego kiosku.
Na samym przepisywaniu listingów się nie kończyło - jeszcze większą radość przynosiło mi nanoszenie rozmaitych zmian we wprowadzonych programach i obserwowanie ich skutków. W efekcie okazało się, że parametry rozkazu REM były bez znaczenia (z czym wiązała się inna świetna wiadomość - nie trzeba było ich przepisywać!), parametry komendy PRINT były wypisywane na ekran, po instrukcji GOSUB musiało znaleźć się RETURN, a GRAPHICS na spółkę z PLOT i DRAWTO pozwalały rysować kolorowe linie. Z każdym przepisanym listingiem poznawałem coraz więcej wzorców, a z każdą wprowadzoną zmianą coraz lepiej rozumiałem to, co właściwie działo się w programie.
Bardzo szybko zacząłem również pisać własne, proste programiki, które z biegiem czasu stawały się coraz dłuższe i bardziej złożone. Wkrótce potem w domowym salonie stanął komputer kompatybilny z IBM PC, wyposażony w procesor 80286, 1 MB RAM, stację dyskietek 5,25" i kartę graficzną Hercules oferującą jedynie monochromatyczne tryby graficzne i tekstowe. Wraz z nim nadszedł czas nauki nowego wariantu znanego mi już języka - stworzonego przez Microsoft GW-BASIC. Wkrótce potem komputer został doposażony w dysk twardy o pojemności 50 MB, co umożliwiło zainstalowanie pełnego systemu MS-DOS 5 wraz z interpreterem języka QBasic, a później także Windows 3.1 i potężnym IDE języka Visual Basic 1.0. Czas leciał, leciwy 80286 został wymieniony na wielokrotnie szybszy 80486, a z języka Visual Basic przeniosłem się na Turbo Pascal. W kolejnych latach pojawiły się procesory z rodziny Pentium, pierwsze akceleratory 3D do kart graficznych (wtedy jeszcze jako osobne urządzenia), a także ogólnodostępny Internet za sprawą numeru 0-20 21 22. Wraz z nim uzyskałem dostęp do sieci IRC, grup dyskusyjnych i hobbystycznie prowadzonych stron internetowych poświęconych różnym językom programowania. Tymczasem trafiłem do liceum, komputer przeszedł kolejną metamorfozę i został wyposażony w procesor Intel Celeron taktowany z częstotliwością 333 MHz oraz kartę graficzną z wbudowaną akceleracją 3D, a ja za namową znajomego porzuciłem Turbo Pascal na rzecz C++ i OpenGL; moje zainteresowanie wzbudził też temat inżynierii wstecznej. Potem nadeszły studia na Politechnice Wrocławskiej, kolejne języki programowania w niemałej liczbie i coraz bardziej skomplikowane projekty. Na drugim roku rozpocząłem pierwszą pracę jako programista i reverse engineer dla jednej z polskich firm antywirusowych, coraz bardziej interesując się również zagadnieniami związanymi z bezpieczeństwem komputerowym. Wkrótce przeszedłem do hiszpańskiej firmy Hispasec, skończyłem studia inżynierskie, by ostatecznie trafić do firmy Google, w której pracuję do dzisiaj.
Spoglądając w przeszłość, mogę powiedzieć, że moja fascynacja wszystkim, co wiąże się z komputerami, a w szczególności programowaniem, nigdy nie minęła, mimo że zaczęła się ponad ćwierć wieku temu - jednym z jej efektów było napisanie książki, którą właśnie, Czytelniku, trzymasz w ręku. Podczas jej tworzenia starałem się uchwycić preferowane przeze mnie podejście do programowania, które jest zabarwione ciekawością, chęcią zrozumienia tego, jak każdy z używanych mechanizmów działa od środka, a także zamiłowaniem do niskopoziomowych zakątków informatyki. Co za tym idzie, książkę pisałem z myślą o pasjonatach, dla których programowanie jest jednocześnie wyzwaniem i świetną zabawą, bez względu na to, czy programują jedynie dla przyjemności, czy też pracują w zawodzie.
Dobierając zawartość książki, starałem się przede wszystkim wybrać tematy, o które często pytają początkujący i średnio zaawansowani programiści - stąd m.in. obecność rozdziałów o wątkach, gniazdach sieciowych czy plikach binarnych. Do spisu treści trafiły również tematy moim zdaniem istotne dla każdego programisty, takie jak niskopoziomowe kodowanie danych, synchronizacja procesów wielowątkowych czy choćby korzystanie z wiersza poleceń. Chciałem jednak również wskazać, że programowanie nie kończy się na aplikacjach narzędziowych lub "usługowych", stąd obecność ostatniego rozdziału książki zatytułowanego "Programowanie dla zabawy". Jednocześnie zdecydowałem się pominąć pewne, skądinąd bardzo istotne, zagadnienia, takie jak algorytmy, wzorce projektowe czy szczegółowe opisy konkretnych języków programowania - są one opisane w wielu innych, bardziej wyspecjalizowanych publikacjach.
Przykłady w książce zostały napisane w różnych językach programowania - są to przede wszystkim Python, C, C++ oraz Java. Ta nietypowa różnorodność brała się z chęci podkreślenia, że w większości środowisk występują te same podstawowe mechanizmy, których używa się w bardzo podobny sposób niezależnie od wybranego języka programowania, a różnice często sprowadzają się wyłącznie do nazw bibliotek, funkcji, klas itp. Jednocześnie z uwagi na specyfikę poszczególnych języków oraz ich umowną klasyfikację jako nisko- bądź wysokopoziomowe istnieją zadania łatwiejsze i trudniejsze do wykonania w każdym z nich - na przykład korzystanie z zaawansowanych funkcji oferowanych przez system operacyjny jest łatwiejsze w C i C++, ale już przetwarzanie danych tekstowych jest zdecydowanie prostsze w językach pokroju Python. Ostatecznie nie ma języka idealnego - warto więc, by programista znał ich kilka i zgodnie z zasadą use the right tool for the right job oraz własnymi preferencjami wybierał język adekwatny do danego zadania. Tak jak nie istnieje idealny język programowania, tak nie ma również idealnego systemu operacyjnego, stąd tematy poruszane w książce omawiam w kontekście dwóch popularnych rodzin systemów: Microsoft Windows oraz GNU/Linux (w szczególności Ubuntu).
Kontynuując temat przykładowych programów, starałem się również, by listingi miały wewnętrznie spójny styl, ale jednak zróżnicowany pomiędzy sobą - jednym z celów książki było zaprezentowanie fragmentów istniejącego ekosystemu programistycznego, w którym różni programiści stosują odmienne style tworzenia kodu. Warto więc jak najwcześniej nabrać pewnej elastyczności w tej kwestii - choć każdy projekt powinien konsekwentnie kierować się zasadami konkretnego stylu, jego wybór jest często kwestią osobistych preferencji programisty.
Nie w każdym przykładowym kodzie zawarłem pełne sprawdzanie błędów - wynikało to z chęci zwiększenia czytelności listingów i redukcji ich długości, która w przeciwnym razie mogłaby być przytłaczająca dla bardziej początkujących Czytelników. W przypadku rozwijania kodu produkcyjnego (tj. o wysokiej jakości) pełne sprawdzanie błędów jest w zasadzie obowiązkowe, choć oczywiście nie każdy tworzony kod musi przystawać do takich standardów. W trakcie pracy nad pomocniczymi narzędziami jednorazowego użytku czy rozwiązaniami zadań podczas konkursów trwających kilka lub kilkadziesiąt godzin obranie drogi "na skróty" może nierzadko zaoszczędzić sporo czasu i okazać się ostatecznie strategią najkorzystniejszą.
W książce oprócz przykładowych listingów kodu występuje również znaczna liczba ramek oznaczonych jako [VERBOSE] oraz [BEYOND]. Celem tych pierwszych jest dokładniejsze przedyskutowanie zagadnień, które pojawiają się w tekście, i są skierowane przede wszystkim do bardziej początkujących Czytelników. Zawartość ramek [BEYOND] natomiast znacznie rozwija tematykę i często wykracza poza średni poziom skomplikowania danego rozdziału - jestem przekonany, że nawet zaawansowani programiści mogą w nich znaleźć coś nowego i ciekawego.
Chciałbym również zachęcić do odwiedzenia oficjalnego serwisu "Zrozumieć Programowanie", w którym można znaleźć przykładowe kody źródłowe, erratę, jak i niewielkie forum stworzone z myślą o dyskusji na tematy poruszane w książce, w tym również o ćwiczeniach i flagach. Znajduje się on pod adresem:
http://gynvael.coldwind.pl/book/
Podziękowania
W powstaniu i nadaniu ostatecznego kształtu tej książce swój udział miało wiele osób, którym chciałbym w tym miejscu podziękować. Mojej żonie Arashi Coldwind za wsparcie okazywane podczas całego czasu trwania tego, bądź co bądź, niemałego projektu, jakim jest napisanie książki. Autorowi przedmowy, a jednocześnie redaktorowi merytorycznemu i mojemu dobremu przyjacielowi Mateuszowi Jurczykowi, który na edycję i korektę tej książki poświęcił ogromną ilość własnego czasu. Tomaszowi Łopuszańskiemu, który wraz ze mną przesiadywał do późnych godzin nocnych, przeglądając i szlifując każdy kolejny nadesłany rozdział. Łukaszowi Łopuszańskiemu, który przekonał mnie do napisania tej książki i doprowadził do jej wydania. Sebastianowi Rosikowi za genialny projekt okładki. Recenzentom merytorycznym, którzy wychwycili znaczą liczbę niedociągnięć obecnych w pierwotnej wersji, a byli to: Paweł "KrzaQ" Zakrzewski, Robert "Jagger" Święcki, Mariusz Zaborski, Unavowed, Michał Leszczyński, Michał Melewski, Karol Kuczmarski, Adam "pi3" Zabrocki, Sergiusz "q3k" Bazański, Tomasz 'KeiDii' Bukowski. Testerom, którzy wskazali dodatkowe błędy i nieścisłości, w składzie: Łukasz "Stiltskin" Głowacki, Nism0, Łukasz "Lord Darkstorm", Trzeciakiewicz, Alan Cesarski. A także Ange Albertiniemu, Mikołajowi Koprasowi, Mattowi Moore'owi, Ferminowi Sernie oraz Michałowi Zalewskiemu.
Mam nadzieję, Drogi Czytelniku, że książka ta będzie dla Ciebie ciekawą i inspirującą lekturą.
Gynvael Coldwind
Zurych, wrzesień 2015 r.
Zgłaszanie błędów i errata
W idealnym wszechświecie napisałbym książkę bez błędów, w której nie byłoby miejsca na literówki czy brakujące przecinki. Niestety, książka ta pochodzi z naszego wszechświata, więc wbrew wszelkim staraniom błędy na pewno się pojawią.
Z tego względu chciałbym zachęcić Czytelników do zaglądania od czasu do czasu na stronę z erratą, którą będę aktualizował przy okazji każdego znalezionego lub zgłoszonego błędu merytorycznego. Erratę można znaleźć pod adresem:
http://gynvael.coldwind.pl/book/errata
Chciałbym również zachęcić Czytelników do zgłaszania wszelkiego rodzaju błędów, zarówno merytorycznych, jak i językowych - wszystkie zauważone niedociągnięcia będą eliminowane w kolejnych wydaniach książki. Informacje na temat wykrytych błędów proszę zgłaszać pod poniższym adresem:
http://gynvael.coldwind.pl/book/bugbounty
Dodatkowo, naśladując niejako Donalda Knutha[1], postaram się wysłać pamiątkową pocztówkę każdemu Czytelnikowi, który jako pierwszy zgłosi dany błąd merytoryczny. Szczegóły tego swoistego bug bounty można również znaleźć na wspomnianej stronie internetowej.
Rozdział 1Konsola i interpreter poleceń
Pomimo upływu lat konsola nadal jest jednym z podstawowych narzędzi wykorzystywanych przez zaawansowanych programistów i administratorów systemów - w praktyce konsola i interpreter poleceń to często niezwykle wygodne i przydatne narzędzia, a w niektórych wypadkach stanowią wręcz jedyną możliwość skorzystania z pewnych programów. Zarówno konsola, jak i interpreter to de facto standard w przypadku większości unixowych systemów operacyjnych (rys. 1).
Rysunek 1. ConEmu-Maximus[3], DOSBox[4] z uruchomioną aplikacją w trybie tekstowym oraz GNOME Terminal[5]
Na samym początku warto wyjaśnić pewną nieścisłość związaną z używanymi terminami, a mianowicie: czym jest konsola, a czym interpreter poleceń?
Konsola (nazywana również emulatorem terminalu lub potocznie - terminalem) jest pewnego rodzaju środowiskiem wykonania, z którego mogą (ale nie muszą) korzystać aplikacje - należy od razu zaznaczyć, że w danym momencie z jednej konsoli może korzystać wiele aplikacji. Elementem centralnym jest bufor tekstowy o określonej (ale niekoniecznie stałej) wielkości, który jest w całości lub częściowo wyświetlany w oknie konsoli i który może być pośrednio lub bezpośrednio modyfikowany przez wszystkie aplikacje z niej korzystające. Dodatkowo w danym momencie jedna aplikacja jest tzw. aplikacją pierwszoplanową (foreground) - wejście z klawiatury, a czasem również myszki, jest przekazywane właśnie do niej.
Domyślnie dane wypisywane przez aplikację na standardowe wyjście (stdout) oraz standardowe wyjście błędów (stderr) są również odbierane przez konsolę, która wyświetla tekst, biorąc pod uwagę wszystkie znaki specjalne i sekwencje kontrolne oraz pozycję kursora. Analogicznie wejście z klawiatury jest przesyłane przez konsolę na standardowe wejście (stdin) aplikacji pierwszoplanowej[6].
Z punktu widzenia użytkownika konsola jest po prostu tekstowym interfejsem wykorzystywanym przez niektóre programy. Jedną z podstawowych aplikacji korzystających z konsoli jest właśnie interpreter poleceń (command processor lub command-line interpreter), nazywany czasem powłoką systemową (shell)[7], który zwyczajowo udostępnia użytkownikowi zestaw poleceń i funkcji do:
Nawigacji w systemie (polecenia typu cd, dir lub ls, pwd itp.).
Konfigurowania środowiska uruchomieniowego, w tym zmiennych środowiskowych (set, export, path, ulimit itp.).
Uruchamiania innych programów z podanymi argumentami i ewentualnymi przekierowaniami strumieni
stdin/
stdout/
stderr.
Tworzenia skryptów automatyzujących powyższe czynności.
Przykładowe interpretery poleceń to m.in.:
cmd.exe (Windows);
PowerShell (Windows);
Bash (GNU/Linux itp.);
Z shell (GNU/Linux itp.).
W dalszych podrozdziałach opiszę główne koncepty (przekierowania, argumenty, zmienne środowiskowe, katalog roboczy) wykorzystywane przy pracy z interpreterami poleceń, przy czym skupię się na dwóch z nich: cmd.exe (Windows 7) oraz Bash (Ubuntu 14.04.2 LTS). Ponieważ będzie to opis skrócony, zachęcam czytelników do bliższego przyjrzenia się wybranemu przez siebie interpreterowi we własnym zakresie. Pominę również opis większości poleceń (zarówno wbudowanych, jak i dostępnych w systemie operacyjnym) do operacji na plikach i katalogach - zostały one opisane w wielu innych, łatwo dostępnych publikacjach, np.:
Bash:- http://www.tldp.org/LDP/GNU-Linux-Tools-Summary/html/c2690.htm
- http://www.tldp.org/LDP/GNU-Linux-Tools-Summary/html/x3289.htmcmd.exe:- http://en.wikibooks.org/wiki/Guide_to_Windows_Commands/File_and_Directory_Management
- http://en.wikibooks.org/wiki/Guide_to_Windows_Commands/File_Commands
1.1. Wykorzystanie interpretera
Poniżej zaprezentowalem kilka przykładów użycia interpretera wraz z krótkim, wysokopoziomowym opisem ich działania. Większość użytych mechanizmów jest wyjaśniona w dalszej części tego rozdziału.
(Ubuntu) reset && make test
(Windows) cls && make test
Wyczyszczenie okna konsoli oraz rekompilacja projektu i jego uruchomienie (przy wykorzystaniu skryptu Makefile). W przypadku dystrybucji Ubuntu osobiście preferuję stosowanie komendy (programu) reset zamiast clear, ponieważ w emulatorze konsoli, z którego korzystam (GNOME Terminal), reset czyści cały bufor tekstowy konsoli, natomiast clear jedynie przewija ekran niżej tak, by wydawało się, że został on wyczyszczony - jest to istotne, gdy kompilacja lub uruchomienie projektu powoduje wypisanie dużej ilości tekstu, a dla nas wygodne byłoby szybkie przewinięcie ekranu na początek danych. Alternatywnie można by skorzystać np. z polecenia make test 2>&1 | less.
(Ubuntu) python gen_input.py | LD_PRELOAD='pwd'/debug.so ./app
Uruchomienie skryptu gen_input.py, który generuje na standardowe wyjście pewne dane wejściowe dla aplikacji app - są one przekazywane na jej standardowe wejście. Dodatkowo niektóre importowane przez aplikację funkcje zostaną podmienione na funkcje o tej samej nazwie, znajdujące się w dynamicznej bibliotece debug.so, która umieszczona jest w obecnym katalogu roboczym.
(Ubuntu) for i in {1..1000}; do ./a.out $i; cp out.data data/$i.data; done
(Windows) for /l %i in (1,1,1000) do @(a.exe %i && copy out.data data\%i.data)
Uruchomienie aplikacji a.out / a.exe tysiąc razy z kolejnymi liczbami (od 1 do 1000) w pierwszym parametrze. Za każdym razem wygenerowany przez aplikację plik out.data jest kopiowany do katalogu data i nadawana jest mu nowa nazwa w formacie <liczba z argumentu>.data.
(Ubuntu) watch pep8 test.py
Co dwie sekundy (domyślne opóźnienie programu watch) okno konsoli jest czyszczone, a następnie uruchomiony zostaje walidator stylu PEP 8 języka Python. Jest to wygodne rozwiązanie, jeśli w tym czasie w osobnym oknie poprawiamy skrypt test.py i chcemy od razu widzieć, czy zmiany odniosły zamierzony skutek (tj. czy ostrzeżenie o nieprawidłowym stylu zniknęło).
Kilka innych przykładów pojawia się również w innych miejscach w tym rozdziale.
1.2. Przekierowania
Jedną z najważniejszych cech interpreterów jest możliwość przekierowania standardowego wyjścia (oznaczanego najczęściej deskryptorem 1), wyjścia błędów (2) i wejścia (0) procesów oraz łączenia ich między procesami. Najłatwiej jest to wytłumaczyć na przykładach (które są poprawne zarówno dla Bash, jak i cmd.exe; prawdopodobnie zadziałają również w przypadku większości innych współczesnych interpreterów):
program > xyz
Wszystkie dane wypisywane na standardowe wyjście przez uruchomiony program trafią do pliku xyz (jeśli plik ten istnieje, zostanie nadpisany). Jest to idealne rozwiązanie, gdy danych jest dużo, a my chcemy je na spokojnie przejrzeć lub użyć jako danych wyjściowych w późniejszym terminie.
Alternatywnie można by napisać program 1>xyz, choć nie jest to konieczne, ponieważ standardowe wyjście (1) jest domyślnym argumentem dla przekierowania w tę stronę.
program 2> errors
Podobnie jak wyżej, z tą różnicą, że do pliku errors trafią dane wypisywane na standardowe wyjście błędów.
program >> xyz
Analogicznie jak w powyższych przypadkach, przy czym dane (ze standardowego wyjścia) zostaną dopisane na koniec pliku xyz.
program < input
Program otrzyma dane z pliku input na standardowe wejście (tj. czytanie ze standardowego wejścia będzie równoznaczne z czytaniem z pliku input). To przekierowanie idealnie nadaje się do powtarzanych wielokrotnie testów aplikacji odczytujących dane ze standardowego wejścia - dzięki temu nie trzeba ich za każdym razem wprowadzać ręcznie.
program 2>&1
Przekierowanie standardowego wyjścia błędów na standardowe wyjścia; przydatne rozwiązanie, jeśli zachodzi potrzeba przefiltrowania standardowego wyjście błędów (patrz dalej).
program >xyz 2>&1
Przekierowanie obu standardowych wyjść do pliku xyz. Mogłoby się wydawać, że warianty program 2>&1 >xyz lub program >xyz 2>xyz również zadziałają, jednak tak niestety nie jest (patrz również ramka "Jak rozwiązywane są przekierowania [VERBOSE]" oraz ramka "Dwa przekierowania do jednego pliku [BEYOND]").
program | inny_program
Standardowe wyjście pierwszego programu zostanie przekierowane na standardowe wejście drugiego programu. Drugi program najczęściej albo filtruje otrzymane dane (np. wyszukując tylko podanych wzorców), albo poddaje je dalszej obróbce.
Jak rozwiązywane są przekierowania [VERBOSE]
Wspomniałem już, że przy przekierowaniu obu standardowych wyjść (stdout, stderr) do jednego pliku należy użyć polecenia program >xyz 2>&1 oraz że podanie przekierowań w odwrotnej kolejności nie zadziała zgodnie z oczekiwaniami - wynika to ze sposobu ich realizacji.
W dużym uproszczeniu: o stosowanym zapisie w stylu lewa_strona>prawa_strona, lewa_strona<prawa_strona itd. można myśleć jako o standardowej operacji przypisania lewa_strona = prawa_strona, przy założeniu, że obie strony są zmiennymi (deskryptorami), które w tym przypadku reprezentują faktyczne odwołania do potoków, plików, konsoli itp.
Warto pamiętać, że punktem wyjścia są zawsze deskryptory stdin/stdout/stderr odziedziczone po procesie interpretera (rodzica), a więc:
deskryptor 0 (w przykładzie poniżej oznaczany jako fd0), czyli
stdin procesu potomnego, wskazuje na to samo urządzenie standardowego wejścia, które jest używane przez interpreter;
deskryptor 1 (fd1), czyli
stdout procesu potomnego, wskazuje na
stdout procesu rodzica;
deskryptor 2 (fd2), czyli
stderr procesu potomnego, wskazuje na
stderr procesu rodzica.
Korzystając z powyższego uproszczenia i mając na uwadze punkt wyjścia, rozważmy, co się stanie w przypadku wykonania polecenia program 2>&1 >xyz:
1. 2>&1 (a więc fd2=fd1) - deskryptor 2 ma od teraz wskazywać na ten sam potok/plik itp., do którego odnosi się deskryptor 1. W tym momencie deskryptor 1 wskazuje na odziedziczone po interpreterze stdout, zatem deskryptor 2 od tego momentu będzie również odnosić się do standardowego wyjścia interpretera.
2. >xyz (a więc fd1=file("xyz", WRITE)) - deskryptor 1 od teraz ma wskazywać na nowo otwarty (do zapisu) plik xyz. Deskryptor 2 nie jest w żaden sposób uaktualniany ani zmieniany, zatem cały czas odnosi się do standardowego wyjścia interpretera.
Podsumowując, w momencie faktycznego uruchomienia programu (przekierowania odbywają się bezpośrednio przed jego uruchomieniem), stdout nowego procesu (deskryptor 1) jest przekierowane do pliku xyz, a stderr (deskryptor 2) wskazuje na stdout procesu rodzica (interpretera). Nic natomiast nie zmieniło się w przypadku stdin (deskryptor 0).
Rozważmy jeszcze jeden przypadek. Załóżmy, że chcemy zamienić stdout i stderr miejscami, tak aby stdout uruchamianego procesu wskazywało na stderr interpretera, oraz analogicznie, aby stderr uruchamianego procesu wskazywało na stdout interpretera. W tym celu należy stworzyć dodatkowy tymczasowy deskryptor (podobnie jak przy zamianie miejscami wartości zmiennych) i z niego skorzystać.
Polecenie, którego użyjemy, wygląda następująco: program 3>&1 1>&2 2>&3. Rozważmy, co się dzieje w kolejnych krokach przekierowań:
1. 3>&1 (a więc fd3=fd1) - ma zostać utworzony nowy deskryptor - 3 (fd3), który ma wskazywać na to samo, co deskryptor 1, czyli na odziedziczone po interpreterze stdout.
2. 1>&2 (a więc fd1=fd2) - od teraz deskryptor 1 ma wskazywać na to samo, co deskryptor 2, czyli na odziedziczone po interpreterze stderr.
3. 2>&3 (a więc fd2=fd3) - od teraz deskryptor 2 ma wskazywać na to samo, co deskryptor 3, czyli na odziedziczony po interpreterze stdout.
Ostatecznie w momencie uruchomienia programu stdout nowego procesu będzie wskazywać na stderr procesu interpretera, a stderr nowego procesu na stdout interpretera.
Aby przetestować opisany mechanizm w praktyce, warto stworzyć pomocniczy program, np. w języku Python (2.7):
import sys
print>>sys.stderr, "STDERR"
print "STDOUT"
Przetestowanie podmiany będzie wymagało od nas upewnienia się, że stdout i stderr interpretera wskazują na różne urządzenia. Możemy np. wywołać interpreter z przekierowanymi stdout/stderr do oddzielnych plików oraz nakazać mu uruchomić pomocniczy program ze wspomnianymi przekierowaniami:
Ubuntu:
$ bash -c -- "python p1.py 3>&1 1>&2 2>&3" >log_stdout 2>log_stderr
$ cat log_stdout
STDERR
$ cat log_stderr
STDOUT
Windows:
> cmd /c "python p.py 3>&1 1>&2 2>&3" >log_stdout 2>log_stderr
> type log_stdout
STDERR
> type log_stderr
STDOUT
Więcej informacji na temat przekierowań można znaleźć w oficjalnym podręczniku Basha [1] oraz na MSDN [2].
Dwa przekierowania do jednego pliku [BEYOND]
Wspomniałem również o drugim intuicyjnym, lecz błędnym sposobie przekierowania obu standardowych wyjść do jednego pliku: program >xyz 2>xyz. Dlaczego wariant ten jest niepoprawny?
Na systemach z rodziny Windows odpowiedź otrzymujemy natychmiast:
> program >xyz 2>xyz
The process cannot access the file because it is being used by another process.
Interpreter zgłosił, że plik xyz jest już otwarty przez inny proces, nie można go więc użyć do przekierowania. "Innym procesem" w tym przypadku jest sam interpreter, który przy realizacji wyrażenia >xyz sam otworzył wskazany plik, ustawiając flagę FILE_SHARE_READ, która zezwala na ponowne otwarcie tego pliku jedynie do odczytu. Przy realizacji 2>xyz interpreter próbuje ponownie otworzyć plik xyz do zapisu, co skutkuje błędem ze względu na wspomnianą flagę.
W przypadku interpretera Bash polecenie wykona się, lecz w pliku xyz znajdziemy jedynie strzępy informacji. Wynika to z faktu, że obie próby otwarcia pliku xyz powiodły się. Są one jednak przypisane do oddzielnych deskryptorów, co łatwo sprawdzić, korzystając z programu strace:
> strace -f bash -c -- "program >xyz 2>xyz" 2>&1 | grep -A 3 'open("xyz'
[pid 7716] open("xyz", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
[pid 7716] dup2(3, 1) = 1
[pid 7716] close(3) = 0
[pid 7716] open("xyz", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
[pid 7716] dup2(3, 2) = 2
[pid 7716] close(3) = 0
[pid 7716] execve("/program", ["program"], [/* 22 vars */]) = 0
Ponieważ każdy deskryptor ma swój własny, niezależny kursor zapisu (patrz również część IV książki), który najpierw jest ustawiony na początek pliku, to przy każdej operacji zapisu mogą zostać nadpisane dane, które zostały zapisane, korzystając z drugiego deskryptora. Można to przetestować, używając pomocniczego skryptu (Python 2.7):
import sys
sys.stdout.write("aaaaaa")
sys.stdout.flush()
sys.stderr.write("AAA")
Zaprezentowany program (w kontekście omawianego przekierowania) zapisuje do pliku ciąg "aaaaaa", korzystając z deskryptora stdout - jego kursor zapisu jest zatem przesunięty o 6 bajtów do przodu, dzięki czemu następny zapis korzystający z tego deskryptora wyemituje dane za już istniejącymi. Dwie linie dalej zapisanie ciągu "AAA" używa innego deskryptora, wskazującego na ten sam plik - stderr. Kursor zapisu tego deskryptora cały czas wskazuje na początek pliku, a więc tam właśnie trafią dane. W efekcie powinniśmy otrzymać ciąg "AAAaaa", co możemy sprawdzić, uruchamiając skrypt z przekierowaniami:
$ python test.py >xyz 2>xyz
$ cat xyz
AAAaaa
Podsumowując, opisana metoda przekierowania nie pozwala na skuteczne zapisanie wszystkich danych wypisywanych na stdout i stderr.
1.3. Przykładowe polecenia wykorzystujące przekierowania
Poniżej zaprezentowałem kilka typowych przykładów wykorzystania przekierowań wraz z ich wyjaśnieniem:
(Ubuntu) program | less
(Windows) program | more
Wyświetl dane ze standardowego wyjścia programu po jednej stronie ekranowej naraz.
(Ubuntu) ps aux | grep "python"
(Windows) tasklist | find "python"
Przefiltruj standardowe wyjście poleceń ps aux i tasklist w poszukiwaniu słowa "python". W praktyce wypisane zostaną przede wszystkim informacje o procesach Pythona.
(Ubuntu) strace -e trace=file program 2>&1 | grep passwd
Przefiltruj logi generowane przez strace w poszukiwaniu słowa "passwd". W praktyce wypisane zostaną przede wszystkim bezpośrednie interakcje procesu z plikiem passwd.
(Ubuntu) cat /etc/passwd | wc -l
Wypisz, ile linii ma plik /etc/passwd. Pod Windowsem analogiczny efekt można uzyskać jedynie niebezpośrednio (np. korzystając z polecenia find /c /v "ciąg", aby wypisać liczbę linii niezawierających danego ciągu, który należy dobrać tak, by nigdy nie wystąpił w pliku), ale w praktyce lepiej jest zainstalować np. pakiet GnuWin32 [3] i skorzystać z wc -l.
(Ubuntu) program | grep -i "warning" | sort > warning.log
(Windows) program | find /i "warning" | sort > warning.log
Uruchom program, przefiltruj jego wejście pod kątem linii zawierających słowo "warning" (wielkość liter nie ma znaczenia), a następnie posortuj linie alfabetycznie i zapisz je do pliku warning.log.
1.4. Bieżący katalog roboczy
W skład środowiska, w którym działa aplikacja, wchodzi tzw. katalog roboczy (nazywany czasem w skrócie cwd lub rzadziej - wd), czyli po prostu jeden z katalogów w systemie plików, w którym dana aplikacja została uruchomiona lub który dana aplikacja wybrała po uruchomieniu. Katalog roboczy jest wykorzystywany przez system operacyjny jako domyślny katalog w przypadku operacji na systemie plików korzystających ze ścieżek względnych (tj. relatywnych względem katalogu roboczego). Należy dodać, że operując na ścieżkach (dotyczy to zarówno interpretera, większości innych aplikacji, jak i większości poleceń w językach programowania), można użyć dodatkowo dwóch pseudokatalogów:
. (kropka) - obecny katalog;
.. (dwie kropki) - nadrzędny katalog.
Ponadto niektóre interpretery poleceń udostępniają pseudokatalog ~ (tylda) oznaczający katalog domowy użytkownika (zazwyczaj /home/<nazwa użytkownika> w przypadku systemów unixowych).
Interpreter poleceń posiada zazwyczaj szereg poleceń do zmiany oraz wyświetlenia katalogu roboczego (tab. 1), np.:
Tabela 1. Przykładowe polecenia operujące na katalogu roboczym
Bash
cmd.exe
Opis
pwd
cd
Wyświetlenie obecnego katalogu roboczego.
cd xyz
cd xyz
Przejście do katalogu xyz znajdującego się w obecnym katalogu roboczym.
cd ..
cd ..
lub
cd..
Przejście do nadrzędnego katalogu.
ls
lub
ls -la
dir /w
lub
dir
Wylistowanie zawartości katalogu.
cd /
cd \
Przejście do głównego katalogu dysku lub systemu plików.
C:
D:
itp.
Zmiana obecnego dysku na podany [4].
Uruchomiona aplikacja domyślnie dziedziczy katalog roboczy po swoim rodzicu (np. po interpreterze poleceń), dzięki czemu wszelkie operacje na systemie plików zazwyczaj przeprowadza się, nawigując po katalogach (tj. zmieniając katalog roboczy interpretera) i posługując się ścieżkami względnymi.
1.5. Zmienne środowiskowe
Zmienne środowiskowe (environment variables) są niewielkimi, tekstowymi wartościami, w których najczęściej przechowuje się pewne ustawienia, z których korzystają wywoływane aplikacje lub niektóre funkcje biblioteczne. Przykładowe zmienne środowiskowe możemy zobaczyć poniżej:
$ env
SHELL=/bin/bash
VTE_VERSION=3409
TERM=xterm
PATH=/usr/bin:/bin:/usr/sbin:/usr/local/sbin
PWD=/tmp
LANG=en_US.UTF-8
HOME=/home/gynvael
LANGUAGE=en_US:en
[...]
> set
ALLUSERSPROFILE=C:\ProgramData
APPDATA=C:\Users\gynvael\AppData\Roaming
BXSHARE=d:\bin\bochs-2.6
COMMANDER_DRIVE=C:
COMMANDER_EXE=C:\totalcmd\TOTALCMD64.EXE
COMMANDER_INI=C:\Users\gynvael\wincmd.ini
COMMANDER_PATH=C:\totalcmd
CommonProgramFiles=C:\Program Files\Common Files
[...]
Z reguły w systemie istnieje możliwość zdefiniowania domyślnego zestawu zmiennych środowiskowych[8], natomiast w praktyce każdy proces posiada ich kopię, którą otrzymuje od procesu-rodzica i którą może swobodnie modyfikować - w takim wypadku procesy potomne domyślnie dziedziczą właśnie tę zmodyfikowaną kopię. Proces-rodzic może również zdefiniować dla uruchamianego programu całkowicie odrębny zestaw zmiennych środowiskowych, niezależny od otrzymanych bądź domyślnych zmiennych. Należy jednak zaznaczyć, że zmienne są kopiowane w momencie uruchomienia procesu potomnego - oznacza to, że proces-rodzic nie będzie mógł modyfikować zmiennych środowiskowych procesu potomnego po jego uruchomieniu, ponieważ ten operował będzie na własnej kopii. Analogicznie proces potomny nie może zmienić wartości danej zmiennej środowiskowej w bloku procesu-rodzica. Z tego względu zmienne środowiskowe nie mogą służyć za metodę komunikacji między dwoma procesami[9], a jedynie do przekazania pewnych parametrów w momencie uruchomienia procesu.
Podobnie jak w przypadku cwd, interpretery poleceń posiadają zestaw rozkazów do modyfikacji zmiennych środowiskowych interpretera, które zostaną odziedziczone przez uruchamiane aplikacje. Dodatkowo interpretery często korzystają ze zmiennych środowiskowych jako ze zmiennych wewnętrznych w skryptach. W takim wypadku niektóre interpretery (np. Bash) umożliwiają określenie, które zmienne powinny być przekazywane procesom potomnym, a które powinny być dostępne jedynie z poziomu interpretera lub jego skryptów.
Tabela 2 zawiera przykładowe polecenia operujące na zmiennych środowiskowych.
Tabela 2. Przykładowe polecenia operujące na zmiennych środowiskowych
Bash
cmd.exe
Opis
export a=xyz
lub
export a
a=xyz
set a=xyz
Ustawienie zmiennej środowiskowej a na wartość xyz.
W przypadku Bash samo export a powoduje ustawienie "bitu dziedziczenia" dla dotychczas lokalnej zmiennej a.
a=xyz
Ustawienie zmiennej a, która nie zostanie przekazana procesom potomnym, na wartość xyz.
a=xyz ./program
Uruchomienie programu program oraz przekazanie mu zmiennej środowiskowej a. Po zakończeniu programu zmienna ta nie jest widoczna w środowisku.
unset a
set a=
Usunięcie zmiennej środowiskowej a.
export
lub
env
lub
printenv
lub
declare -xp
set
Wypisanie wszystkich zmiennych środowiskowych (które otrzyma proces potomny).
declare -p
Wypisanie wszystkich zmiennych środowiskowych i lokalnych.
Jeśli chodzi o same zmienne środowiskowe i ich znaczenie, to kilka przykładów znajdziemy poniżej:
PATH - określa, w jakich katalogach mają być m.in. poszukiwane pliki wykonywalne do uruchomienia (w przypadku podania jedynie nazwy programu z pominięciem ścieżki). Zmienna ta jest wykorzystywana przez wiele aplikacji oraz funkcji API systemu. W systemach z rodziny Windows separatorem, w przypadku podania większej liczby katalogów, jest średnik, natomiast w systemach unixowych standardowo jest to dwukropek. Zmienna ta jest o tyle istotna z punktu widzenia programowania, że warto zawrzeć w niej wszelkie katalogi z kompilatorami i interpreterami, z jakich się korzysta[10].
PROMPT (cmd.exe), PS1 i PS2 (Bash) - definiują tzw. znak zachęty (
command prompt) w interpreterze poleceń.
USERPROFILE (Windows), HOME (GNU/Linux) - określa ścieżkę katalogu domowego użytkownika.
COMSPEC (Windows) - zmienna wykorzystywana przez funkcję system ze standardowej biblioteki C pod systemem Windows (
msvcrt.dll) do określenia, który interpreter poleceń powinien zostać wywołany.
LD_PRELOAD (GNU/Linux) - określa dynamiczne biblioteki, które powinny zostać załadowane przed wszystkimi innymi bibliotekami, wymaganymi przez uruchamiany program (oznacza to, że funkcje wyeksportowane w tych bibliotekach zostaną użyte w miejsce funkcji o tej samej nazwie znajdujących się w załadowanych później bibliotekach).
LD_LIBRARY_PATH (GNU/Linux) - określa ścieżki, w których mogą znajdować się wymagane przez program biblioteki dynamiczne.
Zachęcam czytelników do wylistowania zmiennych środowiskowych w swoim systemie i próby określenia znaczenia każdej z nich (ćwiczenie [CON:what's-this-what's-that]).
Do zmiennych środowiskowych można odwoływać się również z linii poleceń w interpreterze (oraz skryptach interpretera). Obrazują to następujące przykłady:
Bash:
$ test="Some example text."
$ echo "{$test}"
Some example text.
$ echo "SomeChar${test}AnotherChar"
SomeCharSome example text.AnotherChar
$
cmd.exe:
> set test=Some example text.
> echo %test%
Some example text.
> echo SomeChar%test%AnotherChar
SomeCharSome example text.AnotherChar
Delayed expansion w cmd.exe [BEYOND]
W przypadku interpretera cmd.exe istnieje jeszcze jeden mechanizm odwoływania się do zmiennych środowiskowych - tzw. "opóźnione rozwinięcie" (delayed expansion), oznaczane znakami wykrzykników. Korzysta się z niego w zasadzie tylko w przypadku bardziej skomplikowanych skryptów, używających bloków kodu (oznaczanych nawiasami okrągłymi). O "opóźnionym rozwinięciu" możemy myśleć jako o standardowych odwołaniach do zmiennych znanych z różnych języków programowania - zaskoczeniem wydaje się, że to właśnie "normalne" (tj. oznaczone znakami procentów) rozwinięcie wymaga pewnego wyjaśnienia.
W przypadku skryptów .bat (zwanych czasem skryptami batch lub skryptami wsadowymi) standardowe rozwinięcie działa podobnie do preprocesora z języków C/C++, czyli podmiana zmiennej na jej wartość następuje na samym początku przetwarzania danego bloku (lub linii) skryptu. Oznacza to, że jeśli w danym bloku nastąpi zmiana wartości zmiennej, to zmiana ta nie zostanie odzwierciedlona przy kolejnym użyciu zmiennej w tym samym bloku (w przeciwieństwie do zmiennych rozwijanych z opóźnieniem, których wartość zostaje podstawiona dopiero w momencie faktycznego użycia).
Stwierdzenie to najlepiej ilustruje poniższy przykład, wykorzystujący obydwa typy odwołania do zmiennej test, której wartość jest modyfikowana wewnątrz bloku kodu:
@echo off
setlocal enabledelayedexpansion
set test=Some example text.
(
echo %test%, !test!
set test=Switching text to this.
echo %test%, !test!
)
Wynik działania skryptu jest następujący:
Some example text., Some example text.
Some example text., Switching text to this.
Wykomentowując instrukcję @echo off, można zaobserwować, jak wyglądają kolejne uruchamiane linie i bloki skryptu (interpreter je wypisze bezpośrednio przed wykonaniem) po fazie rozwinięcia zmiennych oznaczonych procentami:
> rem @echo off
> setlocal enabledelayedexpansion
> set test=Some example text.
> (
echo Some example text., !test!
set test=Switching text to this.
echo Some example text., !test!
)
Some example text., Some example text.
Some example text., Switching text to this.
W powyższym listingu możemy zaobserwować, że bezpośrednio przed wykonaniem ostatniego bloku kodu rozwinięcie %test% zostało już wykonane.
Co ciekawe, istnienie dwóch różnych mechanizmów rozwijania zmiennych pozwala na tworzenie całkiem ciekawych skryptów, które mogą np. emulować obiektowość [5][6].
1.6. Skrypt startowy
Co oczywiste, modyfikowanie zmiennych środowiskowych (np. PATH) przy każdym uruchomieniu interpretera jest zdecydowanie uciążliwe. Zamiast tego można albo zmodyfikować zmienne domyślne w systemie, albo skorzystać ze skryptu startowego, który jest uruchamiany przy starcie interpretera. W przypadku cmd.exe pod systemami z rodziny Windows skrypt startowy można stworzyć w dowolnym miejscu i podać jego ścieżkę w rejestrze systemowym:
\Software\Microsoft\Command Processor\AutoRun (REG_SZ)
Wartość można dodać zarówno dla wszystkich użytkowników (HKEY_LOCAL_MACHINE), jak i tylko dla siebie (HKEY_CURRENT_USER). Osobiście zazwyczaj umieszczam skrypt w C:\Users\gynvael\cmdstart.bat i ma on treść zależną głównie od zainstalowanych interpreterów i kompilatorów, np.:
@echo off
color a
rem Additional command-line binaries
set PATH=%PATH%;d:\commands;d:\bin\virtualbox;d:\bin\reskit2003;
rem MinGW GCC
set PATH=%PATH%;d:\bin\gcc\bin
rem Python
set PATH=%PATH%;d:\bin\Python27_64;d:\bin\pypy-2.2.1-win32
rem PHP
set PATH=%PATH%;d:\bin\php
rem Bochs
set BXSHARE=d:\bin\bochs-2.6
set PATH=%PATH%;d:\bin\bochs-2.6
rem Sysinternals
set PATH=%PATH%;d:\bin\SysinternalsSuite
rem Go
set GOROOT=d:\bin\go
set PATH=%PATH%;d:\bin\go\bin
prompt gynvael:haven-windows$g
W przypadku Bash i systemów unixowych skrypt startowy nosi nazwę .bashrc i znajduje się w katalogu domowym użytkownika (w praktyce Bash uruchamia plik .bash_profile, który jednak zwyczajowo wywołuje .bashrc). Na przykład moje zmiany w nim zazwyczaj ograniczają się do modyfikacji znaku zachęty oraz dopisania jednego lub kilku aliasów:
[...]
PATH=$PATH:~/commands:/usr/sbin:/usr/local/sbin
PS1='\[\033[0;31m\]'date +%T' \[\033[1;34m\]\u\[\033[m\]:\[\033[1;30m\]\h\[\033[0;32m\]> \[\033[1;32m\]
alias c='g++ -Wall -Wextra'
Warto zapoznać się bliżej z tworzeniem bardziej złożonych skryptów - w praktyce nieraz mogą one okazać się bardzo przydatne (omówienie konkretnych przypadków wykracza jednak poza zakres książki).
1.7. Konsola okiem programisty
Z punktu widzenia programowania najbardziej podstawową operacją, którą powinien znać każdy programista, jest interakcja ze standardowym wyjściem oraz wejściem. Dla przypomnienia tabela 3 zawiera przykłady operacji na standardowym wyjściu i wejściu w kilku językach.
Tabela 3. Wypisanie wiadomości na stdout i stderr oraz pobranie linii tekstu z stdin
Język
Interakcja z
Przykładowy fragment kodu
Python
stdout
print "Example text"
lub
sys.stdout.write("Example text\n")
stderr
sys.stderr.write("Example text\n")
lub
print>>sys.stderr, "Example text" # 2.7
lub
print("Example text", file=sys.stderr) # 3
stdin
line = raw_input() # 2.7
lub
line = sys.stdin.readline()
C / C++
stdout
puts("Example text")
lub
fputs("Example text", stdout);
stderr
fputs("Example text", stderr);
lub
fwrite("Example text", 1, 12, stderr);
stdin
fgets(buffer, buffer_size, stdin);
lub
gets_s(buffer, buffer_size);[11]
lub
scanf("%15[^\n]", buffer); // dla char buffer[16]
C++
stdout
std::cout << "Example text\n";
stderr
std::cerr << "Example text\n";
stdin
std::getline(std::cin, string_object);
lub
std::cin.getline(buffer, buffer_size);
Java
stdout
System.out.println("Example text\n");
stderr
System.err.println("Example text\n");
stdin
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
line = in.readLine();
Standardowe wyjście i wejście w aplikacjach okienkowych [BEYOND]
Warto wspomnieć, że pod systemami z rodziny Windows również aplikacje okienkowe (tj. niekorzystające z konsoli) mogą użyć standardowego wyjścia lub wejścia. Oczywiście z powodu braku zaalokowanej dla nich konsoli interakcja z stdin/stdout ogranicza się jedynie do przekierowań, np.:
#include <stdio.h>
#include <windows.h>
int main(void) {
char buffer[128] = "You have entered: ";
scanf("%109s", buffer + 18);
MessageBoxA(NULL, buffer, "Example", MB_OK);
return 0;
}
Kompilacja i uruchomienie:
> gcc test.c -mwindows
> echo Example text | a.exe
Zdarza się, iż podczas tworzenia aplikacji docelowo okienkowych korzysta się z wypisywania pomocniczych informacji (tzw. debug messages) na standardowe wyjście. Przed publikacją finalnego produktu następuje jedynie przestawienie subsystemu na okienkowy (tak by konsola nie była alokowana dla tej aplikacji), ale korzystając z przekierowań, można nadal dostać się do wypisywanych informacji.
Drugie w kolejce są zazwyczaj argumenty programu - co prawda aplikacje okienkowe również ich używają, niemniej jednak są one zdecydowanie najczęściej spotykane w przypadku programów konsolowych. Dostęp do argumentów odbywa się przeważnie na jeden z dwóch sposobów: gdy główna funkcja programu otrzymuje tablicę argumentów w jednym z parametrów lub gdy dostęp do tej tablicy zachodzi przy użyciu standardowej biblioteki albo funkcji systemowych. Tabela 4 wskazuje, gdzie można znaleźć tablicę argumentów w kilku przykładowych językach.
Tabela 4. Dostęp do argumentów programu
Język
Metoda
Przykładowy kod
Python
std. biblioteka
import sys
print sys.argv
C / C++
main
#include <stdio.h>
int main(int argc, char **argv) {
for (int i = 0; i < argc; i++) {
printf("%i: %s\n", i, argv[i]);
}
return 0;
}
C / C++
(WinAPI)
API systemowe
#include <windows.h>
#include <stdio.h>
int main(void) {
puts(GetCommandLine());
// Patrz również: CommandLineToArgvW
return 0;
}
Java
main
class args {
public static void main(String[] args) {
for(int i = 0; i < args.length; i++) {
System.out.println(i + ": " + args[i]);
}
}
}
Sytuacja ze zmiennymi środowiskowymi wygląda podobnie jak z argumentami, choć dostęp do nich zazwyczaj odbywa się poprzez funkcję ze standardowej biblioteki w danym języku - pewnym wyjątkiem są języki C i C++, w których funkcja main w niektórych implementacjach może zostać zadeklarowana z trzema argumentami i otrzymać zmienne środowiskowe w trzecim z nich. Tabela 5 zawiera przykłady odwołań do zmiennych środowiskowych w kilku językach:
Tabela 5. Dostęp do zmiennych środowiskowych
Język
Metoda
Przykładowy kod
Python
std. biblioteka
print os.environ['PATH']
lub
print os.getenv('PATH')
C / C++
main
#include <stdio.h>
int main(int argc, char **argv, char **envp) {
for (int i = 0; envp[i] != NULL; i++) {
printf("%i: %s\n", i, envp[i]);
}
return 0;
}
C / C++
std. biblioteka
#include <stdio.h>
#include <stdlib.h>
int main(void) {
puts(getenv("PATH"));
return 0;
}
Java
std.biblioteka
class env {
public static void main(String[] args) {
System.out.println(System.getenv("PATH"));
// Patrz również: os.getenv()
}
}
Warto wspomnieć jeszcze o kilku innych metodach dostępu do zmiennych środowiskowych zależnych od systemu operacyjnego:
W niektórych systemach opartych na jądrze Linux początkowe zmienne środowiskowe danego procesu znajdują się również w pseudopliku
/proc/<pid>/environ. W języku C w przypadku pewnych systemów unixowych istnieje zmienna globalna char **environ lub char **__environ, która posiada tablicę zmiennych środowiskowych analogiczną do envp.
WinAPI posiada dodatkowe funkcje do operowania na zmiennych środowiskowych - np. GetEnvironmentVariable oraz CreateEnvironmentBlock.
Dla wszystkich wymienionych funkcji pobierających zmienne środowiskowe, rozpoczynających się od get* lub Get*, istnieją również odpowiedniki modyfikujące blok zmiennych środowiskowych danego procesu, których nazwy zazwyczaj brzmią identycznie, ale zaczynają się od czasowników: set, put lub unset. Oprócz tego, jak wspomniałem wcześniej, można zdefiniować zupełnie nowy blok zmiennych środowiskowych dla procesu potomnego podczas jego tworzenia - umożliwiają to poszczególne funkcje uruchamiające programy, np. execve czy CreateProcess.
Ponadto systemy operacyjne, jak i same konsole, często udostępniają możliwość dodatkowego konfigurowania terminalu, sposobu wyświetlania danych czy odbioru innych zdarzeń od użytkownika (np. pozycji myszki). Tabela 6 zawiera krótką wskazówkę, gdzie szukać informacji o API konsolowym.
Tabela 6. Przykładowe funkcje i biblioteki służące do bezpośredniej interakcji z konsolą
System lub Język
Funkcje lub biblioteka
Python
(GNU/Linux)
Moduły termios oraz tty.
C/C++
(GNU/Linux)
Pseudoplik /dev/tty, a także nagłówki termios.h oraz sys/ioctl.h.
C/C++ i
(WinAPI)
Funkcje z "Console" w nazwie, np. SetConsoleMode, FillConsoleOutputAttribute, WriteConsoleOutput itp.
Java
Klasa java.io.Console.
Kończąc ten rozdział, chciałbym jeszcze raz gorąco zachęcić czytelników do zapoznania się z możliwościami konsoli i wybranym interpreterem poleceń.
Ćwiczenia
[CON:start-script]
Stwórz lub zmodyfikuj skrypt startowy interpretera poleceń, z którego korzystasz.
[CON:what's-this-what's-that]
Uruchom interpreter i wyświetl zmienne środowiskowe, a następnie postaraj się określić znaczenie każdej z nich, tj. przez jaki program lub funkcję jest dana zmienna wykorzystywana.
Bibliografia
http://coldwind.pl/s/c1r1
[1] Redirections, Bash Reference Manual, http://www.gnu.org/software/bash/manual/bash.html#Redirections
[2] Using command redirection operators, Microsoft Developer Network, https://technet.microsoft.com/en-us/library/bb490982.aspx
[3] GnuWin32, http://gnuwin32.sourceforge.net/
[4] Chen R., Why does each drive have its own current directory?, 2010, http://blogs.msdn.com/b/oldnewthing/archive/2010/10/11/10073890.aspx
[5] Coldwind G., Skrypty .BAT i programowanie obiektowe, 2009, http://gynvael.coldwind.pl/?id=123
[6] Coldwind G., OpenGL w skryptach .BAT, 2009, http://gynvael.coldwind.pl/?id=127