Jogger Minia

Strona w permanentnej budowie.

Popularne błędy popełniane podczas pisania skryptów powłoki

Informacje o wpisie.

Opublikowano 21 listopada 2009 o 21:52:04 w kategoriach: Techblog, Zerojedynka.

19 komentarzy; Góra strony.

Trackback; Poprzedni wpis; Następny wpis.

Od pewnego czasu nosiłem się z zamiarem napisania tekstu, w którym przedstawiałbym często popełniane błędy spotykane w skryptach powłoki. Niestety, przytłoczony innymi obowiązkami, nie znalazłem w sobie dość motywacji aby to uczynić. Szczęśliwie niedawno trafił mi się bodziec dość silny, by przerwać bezproduktywną passę — wpis na blogu redelka skrypt opublikowany przez redelka. Przez chwilę miałem nawet zamiar cytować jego kod linijka po linijce, komentując jakie błędy popełnił. Ostatecznie jednak zrezygnowałem z tego zamiaru, ponieważ — po pierwsze — kod redelka nie jest aż tak kiepskiej jakości, oraz — po drugie — nie popełnił w swoim wpisie wszystkich błędów, o których chciałbym wspomnieć.

Nim przejdę do właściwiej części wpisu, chciałbym wspomnieć jeszcze o jednej rzeczy. Otóż przedstawione poniżej błędy nie są błędami w dosłownym tego słowa znaczeniu. Użyte konstrukcje są poprawne o tyle, że spełniają swoje zadanie. Są jednak błędne o tyle, że łamią regułę KISS — cele, które one osiągają, można osiągnąć w prostszy sposób, w efekcie uzyskując przejrzystszy i bardziej estetyczny kod.

W związku z tym przedstawione poniżej sposoby nie powinny być traktowane jako reguły których należy bezwzględnie przestrzegać, ale jako pewne zalecenia których warto się trzymać. Szczerze mówiąc, nie interesuje mnie czy ktokolwiek z nich korzysta, przynajmniej tak długo jak swoje dzieła pozostawia dla siebie (a każdy bardziej zaawansowany użytkownik ma kilka skryptów które spełniają jego indywidualne zachcianki). Gdy jednak ktoś decyduje się na opublikowanie swojego kodu, powinien zadbać by był on wysokiej jakości. Początkujący użytkownicy mają tendencję do uczenia się na przykładach prostych skryptów znalezionych w Internecie, a co za tym idzie powtarzania błędów w nich zawartych.

Jeżeli zaś ktoś jest zainteresowany prawdziwymi błędami (czyli gdy nie działa coś co teoretycznie działać powinno), najlepiej zrobi przeglądając Bash Pitfalls.

Pierwsze koty za płoty

Zdecydowanie najpopularniejszym błędem jest tzw. „mnożenie kotów” (czasem mówi się o nich jako o „martwych kotach”). Błąd ten polega na użyciu polecenia cat (służącego do wypisania zawartości pliku na STDOUT) i przepotokowania jego wyniku do innego polecenia, operującego na tekście (np. grep). W efekcie uzyskuje się tekst z pliku przetworzony przy pomocy innego narzędzia, np:

$ cat file.txt | grep -i foo
foobar

Wszystko wydaje się logiczne i poprawne. I logiczne z pewnością jest. Rzecz tylko w tym, że zdecydowana większość (w tej chwili nie potrafię podać żadnego wyjątku, ale dopuszczam możliwość ich występowania) programów operujących na tekście przyjmuje plik na którym ma pracować jako argument:

$ grep -i foo file.txt
foobar

Specjalnym przykładem tego błędu jest wc -l, służący do liczenia z ilu linii składa się plik. Czasami nie wystarczy wiedza czy plik ma jakąś zawartość (co można sprawdzić przy pomocy testu -s, sprawdzającego czy argument jest istniejącym i niepustym plikiem). Oba poniższe przykłady sprawdzający czy plik ma więcej niż 100 linii:

if [ $(cat file |wc -l) -gt 100 ]; then ... fi
if [ $(wc -l file | awk '{print $1}') -gt 100 ]; then ... fi

Dlaczego nie można po prostu zrobić wc -l file, zgodnie z powyższym zaleceniem? Ponieważ wc najpierw poda liczbę linii a potem nazwę pliku. To wszystko będzie jednym ciągiem znaków, nienadającym się do prostych porównań arytmetycznych. Gdyby mieć do wyboru tylko dwie powyższe konstrukcje, można by polemizować czy kot z pierwszej faktycznie jest martwy. Obie konstrukcje wymagają użycia potoku, a niektórzy przyjmują stanowisko że nie ma większego znaczenia co do czego jest potokowane tak długo jak potok w ogóle jest w użyciu. Na szczęście jest jeszcze trzecia, najlepsza alternatywa — przekierowanie wejścia:

if [ $(wc -l < file) -gt 100 ]; then ... fi

Warto jeszcze podkreślić, że mnożenie kotów nie odnosi się tylko i wyłącznie do polecenia cat. Błąd ten, w najogólniejszym rozumieniu, odnosi się do wszystkich sytuacji w których wykorzystany jest zbędny potok. Dla przykładu bardzo niewiele osób zadało sobie trud by przeczytać man ps, i tworzy coś takiego:

$ ps aux |grep -i someprogram

Niektórym tak bardzo przeszkadzał grep w wyjściu, że posunęli się do poniższych konstrukcji:

$ ps aux |grep -i [s]omeprogram
$ ps aux |grep -i someprogram |grep -v grep

Tymczasem do sprawdzenia czy jakiś konkretny program działa, wystarczy poniższe polecenie:

$ ps -C someprogram

Przykłady takie można mnożyć. Nie jest moim celem wypisanie ich wszystkich. W dalszej części tekstu wspomnę jeszcze o awk-u i sed-zie.

Liczenie linii w wyjściu

Drugi często spotykany błąd to przedziwna maniera liczenia liczby linii z wyjścia jakiegoś polecenia i podejmowania dalszych działań na tej podstawie. Np. ktoś może chcieć sprawdzić czy jakiś plik zawiera określony ciąg znaków i — jeśli tak — podjąć jakąś akcję:

if [ $(grep -i foo file.txt | wc -l) -ne 0 ]; then ... fi

Wydaje się logiczne — jeżeli liczba linii wyjścia w wyniku polecenia grep ... jest różna od zera (innymi słowy: jeśli w file.txt znajduje się szukany ciąg znaków) zrób coś. Rzecz w tym, że wbudowane polecenie if nie służy tylko i wyłącznie do porównań (czy to arytmetycznych czy nie), a do podejmowania określonej czynności na podstawie statusu wyjścia polecenia podanego jako argumenty. Nawias kwadratowy to tak naprawdę jedynie uproszczony, czytelniejszy zapis wbudowanego polecenia test (powyższy zapis warunku mógłby wyglądać tak: if test $(grep -i foo file.txt | wc -l) -ne 0 ; then). Oznacza to, że po if można podać dowolne inne polecenie — powłoka je wykona, i na podstawie statusu wyjścia (zero oznacza przejście do bloku po then; inna liczba jego pominięcie) wykona odpowiedni blok kodu.

Nim przejdę dalej — a już powoli pointuję ten błąd — jeszcze kilka słów o statusach (inaczej kodach) wyjścia. Osoby z nimi obeznane mogą od razu przejść do następnego akapitu (jednak raczej niewielu na tyle zaawansowanych czytelników czyta ten tekst). Status wyjścia jest liczbą z zakresu 0-255. 0 jest zwracane zawsze gdy polecenie powiedzie się i zakończy sukcesem. Inny status wyjścia oznacza błąd. Chociaż wykształciły się pewne konwencje odnośnie znaczenia poszczególnych liczb (np. programy GNU rezerwują wysokie liczby dla poważnych błędów), trudno mówić o jakichkolwiek prawidłowościach i możliwości wnioskowania co dokładnie poszło źle na podstawie samej liczby. Znaczenie kodów wyjścia powinna zawierać dokumentacja programu. Kod wyjścia ostatniego polecenia zawiera zmienna $?. Chcąc pisać dobre skrypty, warto zadbać o to by zwracały one odpowiedni kod wyjścia w przypadku niepowodzenia (liczbę należy podać jako argument polecenia exit, lub — w przypadku funkcji — return). Można również śmiało założyć, że każde narzędzie którego przyjdzie użyć w skrypcie przestrzega tej konwencji i zwraca 0 w przypadku sukcesu oraz inną liczbę w przypadku błędu.

Łącząc wszystkie powyższe informacje, wniosek nasuwa się sam — zamiast zapisu przedstawionego wcześniej, zastosować powinno się

if grep -i foo file.txt ; then ... fi

Należy pamiętać o tym, że polecenie wykonane po if współdzieli z całym skryptem swoje standardowe wyjście, co oznacza że wynik polecenia którego powodzenie jest sprawdzane „zaśmieca” wyjście całego skryptu. Dlatego warto przekierowywać jego wyjście (oraz standardowe wyjście błędu) do /dev/null lub odpowiedniego pliku logu. Alternatywnie można program uruchamiać z flagą która włączy tryb cichy — grep -q lub grep --quiet. Chociaż dodanie stosownej flagi zdecydowanie upraszcza kod, ma jedną poważną wadę — ponieważ nie wszystkie programy oferują tryb cichy, ostatecznie prowadzi to do niekonsekwencji; niektóre programy są uciszane za pomocą odpowiedniego przełącznika zaś inne poprzez przekierowanie STDOUT oraz STDERR. Osobiście uważam że najważniejsza podczas pisania jest konsekwencja i spójność kodu, dlatego preferuję przekierowywanie wyjścia nawet programom które oferują odpowiednie przełączniki. Warto jednak mieć świadomość istnienia wbudowanych mechanizmów i stosować je gdy nie niesie to ze sobą żadnych subiektywnie postrzeganych jako negatywne konsekwencji.

Ogon przez głowę

Czasem przychodzi potrzeba wydrukowania tylko jednej, specyficznej linii z całego pliku. Wspomniany redelek potrzebował robić niemal wyłącznie to. Jaka jest najgorsza możliwa metoda? Właśnie ta zastosowana przez redelka:

$ head -n15 file.txt |tail -n1
foobar

Wydaje się sensowne. Skoro interesująca jest linia numer X, to należy wydrukować cały plik od początku do linii X włącznie, zaś następnie ograniczyć wydruk jedynie do ostatniej z nich. Rzecz w tym, że to samo można osiągnąć przy pomocy sed-a lub awk-a:

$ sed -n 15p file.txt
foobar
$ awk 'NR == 15 {print}' file.txt
foobar

Podstawową zaletą jest zdecydowanie poprawiona czytelność skryptu. Dodatkowo znacznie łatwiej jest wydrukować wszystkie linie z zakresu. Wszystkie trzy poniższe dadzą ten sam efekt:

$ head -n93 file.txt |tail -n6
$ awk 'NR >= 88 && NR <= 93 {print}' file.txt
$ sed -n 88,93p

Przewaga awk-a i sed-a jest wyraźna — wystarczy wprost określić zakres linii które mają zostać wypisane. Tymczasem metoda wykorzystująca headtail jest trudna do zrozumienia — najpierw należy określić górny zakres, zaś następnie — stosując prostą arytmetykę — dolny. Dodatkowo nie do końca wiadomo czy linia graniczna zostanie wliczona czy nie (93 minus 6 daje 87, prawda? Tymczasem, jak widać, 87. linia nie zostanie uwzględniona w wyniku).

Dodatkowo dość często konkretna linia i tak jest jedynie wejściem do programu przetwarzającego, którym jest sed lub awk. Tak właśnie wygląda sytuacja u redelka. Oba poniższe polecenia dają ten sam efekt:

$ cat $plik |head -n3 |tail -n1 |awk '{print $7}'
$ awk 'NR == 3 {print $7}' $plik

Tutaj warto podkreślić, że zarówno sed jak i awk potrafią zastąpić także grep-a, przynajmniej w podstawowym zakresie wyszukiwania ciągów znaków. Oznacza to że zamiast przekazywać im wynik tego ostatniego, można użyć tylko ich:

$ grep -i foo file.txt | {sed,awk} ...
$ sed -n -e '/foo/ ... p' file.txt
$ awk '/foo/ {...}' file.txt

*shyzmy

Opis tego błędu jest najbardziej teoretyczny ze wszystkich. Polega on na wykorzystaniu w skrypcie mechanizmów istniejących tylko w jednej powłoce. Nie jest to samo w sobie nic złego — wręcz przeciwnie, powinno dobierać się narzędzia do potrzeb i jak najefektywniej je wykorzystywać. Traktowanie takiego zachowania w kategoriach błędu nabiera jednak sensu w kontekście źle określonego tzw. shebanga — wtedy i tylko wtedy gdy wygląda on tak:

#!/bin/sh

Już od niezliczonej liczby lat /bin/sh nie jest zwykłym programem wykonywalnym powłoki Bourne'a a dowiązaniem symbolicznym do wybranej przez administratora systemu powłoki. Wiele dystrybucji używa w tej roli bash-a, jednak są wyjątki — jednym z nich jest bodaj najpopularniejsze Ubuntu. Zastosowanie mechanizmów właściwych tylko jednej z powłok wraz z shebangiem wskazującym na /bin/sh powoduje, że skrypt przestaje funkcjonować w środowiskach różniących się od środowiska twórcy skryptu programem na który wskazuje /bin/sh. Z tego powodu nigdy, ale przenigdy nie używaj /bin/sh jeżeli nie masz pewności że Twój skrypt nie wykorzystuje żadnych mechanizmów właściwych tylko i wyłącznie powłoce domyślnej w Twoim systemie.

Ze względu na trwającą przez pewien czas dominację bash-a, błąd ten często nazywa się „stosowaniem bashyzmów”. W Debianie, w pakiecie devscripts, znajduje się nawet specjalny skrypt o nazwie checkbashisms służący do wykrywania konstrukcji specyficznych dla powłoki bash. Jest to jednak szczególny przypadek ogólnego błędu polegającego na zastosowaniu w skrypcie wykonywanym przy pomocy /bin/sh mechanizmów właściwych tylko jednej z istniejących powłok. Tak samo jak bashyzmy, w skrypcie mogą znajdować się zshyzmy czy cshyzmy — wszystko zależy od tego jakiej powłoki używa autor skryptu. Dlatego jeżeli upubliczniasz swój kod, pamiętaj aby albo zachować zgodność z Posiksem, albo w shebangu wstawić ścieżkę do odpowiedniej powłoki.

Odwoływanie się do zmiennych wprost

Błąd najczęściej zauważany podczas wykonywania jakiejś operacji na wszystkich plikach w danym katalogu. Odwoływanie się do zmiennych wprost (bez otaczania ich cudzysłowami) jest w porządku tak długo, jak zmienne nie zawierają znaków interpretowalnych przez powłokę (należą do nich spacje, gwiazdki, nawiasy i inne). Gdy jednak zmienna zawiera którykolwiek z nich, rzeczy zaczynają się komplikować:

VARIABLE='some variable'
cp file $VARIABLE

Powyższe polecenie miało stworzyć kopię pliku file o nazwie some variable. Zamiast tego jednak użytkownik dowiedział się, że „variable nie jest katalogiem”. Skąd to się wzięło? Otóż powłoka odczytała powyższy zapis następująco:

cp file some variable

Jeżeli cp podać więcej niż 2 argumenty (oczywiście pomijając przełączniki), uznaje on ostatni za katalog do którego ma skopiować wszystkie pozostałe. Jak więc osiągnąć spodziewany efekt? Wystarczy zmienną otoczyć cudzysłowami:

cp file "$VARIABLE"

Przykład z cp jest mało destrukcyjny, ale dobrze obrazuje na czym polega problem. Jeżeli ktoś potrzebuje czegoś bardziej oddziałującego na wyobraźnię, niech sobie wyobrazi analogiczny przykład z katalogiem zawierającym bardzo ważne pliki, z których jeden ma w nazwie gwiazdkę.

Sprawdzanie czy wyjście programu zawiera specyficzny tekst w języku naturalnym

Ten błąd zdecydowanie nie jest popularny. Szczerze mówiąc, dopiero podczas pisania tego tekstu natknąłem się na niego bodaj po raz pierwszy. Mimo wahania, z dwóch powodów zdecydowałem się go opisać. Po pierwsze — ku przestrodze. Po drugie — przytrafił on się jednemu z deweloperów Debiana (bug 508905). Oznacza to ni mniej, ni więcej tylko to, że nawet ludzie bardzo doświadczeni w pisaniu skryptów czasem popełniają błędy. Nie oznacza to jednak przyzwolenia na ich popełnianie.

Poniższy kod obrazuje na czym polega ten błąd:

someprogram | grep -i 'removed successfully'

Sprawdza on, czy wyjście programu someprogram zawiera ciąg znaków „removed successfully”. Powyższy zapis najczęściej będzie fragmentem if — jeżeli wyjście zawiera ten ciąg, podejmowana jest jakaś akcja; jeśli nie — inna lub żadna. Problem w tym, że wyjście someprogram będzie odmienne w zależności od wartości zmiennej $LC_MESSAGES (ewentualnie $LANG), która określa w jakim języku mają być wyświetlane komunikaty. W języku polskim powyższy tekst będzie brzmiał „usunięto poprawnie”, przez co warunek zostanie uznany za niespełniony — podczas gdy może on być spełniony, tylko nie zostało to sprawdzone w odpowiedni sposób. Dlatego jeśli już koniecznie musisz sprawdzać komunikaty wyjściowe programu (a nigdy nie możesz być pewny w jakim języku będą one wyświetlane), najpierw upewnij się że będą one w jakimś konkretnym języku. Tutaj jednak kolejna pułapka — nie upewniaj się że komunikaty będą w Twoim ojczystym języku. Raczej mało prawdopodobne żeby Chińczyk miał wygenerowane holenderskie locale. Dlatego zawsze ustalaj locale które mają wszyscy użytkownicy Twojego systemu — C:

LANG=C someprogram |grep -i 'removed successfully'

Podobna zasada obowiązuje przy manipulacjach datą przy pomocy przełączników formatu date. Część z nich, jak np. %A, jest zależna od locali. Dlatego zamiast nich lepiej używać formatek niezależnych, jak np. %u.

Nieusuwanie plików tymczasowych

Wreszcie stosunkowo popularne jest nieusuwanie plików tymczasowych. Czasami chce się wynik jakiejś operacji mieć w pliku a nie w zmiennej (z różnych względów — choćby po to aby na wieloliniowej zmiennej pracować linia po linii). To jest dobre, ale tak długo jak pliki tymczasowe są usuwane przed zakończeniem pracy programu. Użytkownik niczego nie lubi tak bardzo jak śmieci powstałych z działania jakiegoś programu wśród swoich Bardzo Ważnych i Starannie Uporządkowanych Plików. Często nie wie skąd dany plik pochodzi i czy jego usunięcie nie będzie miało negatywnych konsekwencji.

Są dwa rozwiązania tego problemu. Pierwszym jest najzwyklejsze w świecie sprzątanie po sobie:

TEMPORARY_FILE='./someprogram-tmp'
...
rm -f "$TEMPORARY_FILE"

Rozwiązanie to ma jednak kilka wad. Czasem niemożliwe jest utworzenie pliku o takiej właśnie nazwie lub właśnie w tym katalogu. Powyższy zapis ma sztywno ustaloną nazwę pliku tymczasowego — jakkolwiek nieprawdopodobne, możliwe jest że dokładnie taką samą nazwę będzie miał plik użytkownika z Bardzo Ważnymi Danymi. Rozwiązaniem będzie użycie zmiennej $RANDOM; a dokładniej używanie jej tak długo, aż nie będzie pewności że plik o preferowanej nazwie nie istnieje. Dodatkowo — co również jest specyfiką tylko powyższego zapisu — plik tymczasowy tworzony jest w bieżącym katalogu, a następnie z bieżącego katalogu usuwany. Jeżeli w toku programu bieżący katalog ulegnie zmianie (co nie powinno się nigdy zdarzyć, ale może), plik nie zostanie usunięty — lub, co gorsze, usunięty zostanie inny plik o identycznej nazwie. Dlatego zdecydowanie lepiej jest używać ścieżek absolutnych.

Ewentualnie, jeżeli ktoś uważa że sprzątanie jest poniżej jego godności, może utworzyć plik tymczasowy w katalogu /tmp/. Dzięki temu na pewno nie znajdzie się on przypadkiem wśród innych plików użytkownika. Dodatkowo zawartość tego katalogu jest czyszczona przy każdym uruchamianiu komputera, co zapewnia że nie będzie on w nieskończoność zajmował cennej przestrzeni dyskowej.

Warto tutaj wspomnieć o wchodzącym w skład GNU coreutils programie mktemp służącym do tworzenia pliku tymczasowego. Domyślnie tworzy go w katalogu /tmp/, dzięki czemu można później zapomnieć posprzątać po sobie. Upewnia się że plik będzie miał unikalną nazwę, w związku z czym można sobie odpuścić sprawdzanie tego elementu. Zwraca ścieżkę do utworzonego pliku, co pozwala od razu przypisać ją do zmiennej.

if ! TEMPORARY_FILE=$(mktemp) ; then 
        echo 'nie udało się stworzyć pliku tymczasowego'
        exit 1
fi

Taki zapis, prócz wszystkich już przedstawionych zalet, ma jeszcze jedną — sprawdza czy udało się utworzyć plik tymczasowy. Sprawdzanie powodzenia takich trywialnych rzeczy (jak utworzenie pliku, zapis do niego, możliwość odczytu) jest rzadko praktykowane wśród programistów powłoki, chyba że mieli oni wcześniej dużo do czynienia z językami niższych poziomów. O tym, że jest to dobra praktyka, traktuje opublikowany wczoraj artykuł.

Komentarze

Niezłe! Jak będę miał czas przepisać od nowa rsget-moda to na pewno wezmę to pod uwagę.

BTW, twój tekst o psychice, chronieniu dzieci itd. już dawno przestał być zabawny, a jest wkurzający przy wchodzeniu. Może czas się go pozbyć?

D4: po kliknięciu na „nie” w ciachu jest zapisywana informacja aby więcej komunikatu nie wyświetlać. Jeżeli regularnie czyścisz ciacha (to moje ma ustawione absurdalnie długi czas trwania — 1000 lat od daty utworzenia — więc na pewno samo nie wygasło) to polecam albo wyłączyć obsługę ciastek dla witryny albo obsługę JS. Wtedy również się nie wyświetla.

Komunikat jest częścią obrazu bloga który sobie wymyśliłem przed dokonywaniem modyfikacji. W zamyśle blog — przede wszystkim — nie miał być techniczny. Rozumiem że przy wpisach takich jak ten może to irytować, ale czytelnicy wpisów takich jak ten nie są targetem bloga jako całości.

Był czas (już trochę temu), gdy przy ustawionym polskim środowisku sypał się emacsowy dired i musiałem pisać dlań swoje ls (LANG=C ls). O format dat szło.

A nieco obok Twojego tekstu dodam, że pisałem swego czasu dość sporo skryptów ale doszedłem ostatecznie do wniosku, że jeśli coś ma więcej niż kilka linijek, zaczyna odpalać parokrotnie seda czy awka, miewa parę ifów czy pętle, to należy to machnąć w perlu a nie w shellu.

(Komentarz zmodyfikowany 22.11.2009 o 11:33)

Pomyślałem jeszcze o mnożeniu grepa:

grep -v fraza1 plik.txt | grep -v fraza2 # źle
grep -v -e fraza1 -e fraza2   plik.txt            # dobrze

Ponadto czasami wc jest niepotrzebny, gdyż dany program ma opcję służącą do liczenia, np.:

grep fraza plik.txt | wc -l     # źle
grep -c fraza plik.txt          # dobrze

Ogólnie: popularnym błędem jest nieczytanie pomocy i manuali. ;)

regułę KISS zastąpiłbym regułą BUZI (Bez Udziwnień Zapisuj Idioto) :P
a tak na poważnie to merytorycznie tekst mi się bardzo podoba i aż się zarumieniłem patrząc że niektóre wymienione jako błędne skrypty wyglądają jakoś tak "znajomo" :( (tzn. u mnie takich pełno)

TEMPORARY_FILE=$(mktemp) || (echo 'nie udało się stworzyć pliku tymczasowego'; exit 1)

Erm. How is this supposed to work? Jeśli się nie uda, to uruchom podpowłokę i z tej podpowłoki wyjdź z kodem 1? To w ogóle niemal głupota używać || do sterowania wykonaniem poleceń. Powinieneś dorzucić to jako kolejny popularny błąd.

if ! TEMPORARY_FILE=$(mktemp); then echo "blablabla"; exit 1; fi

@Stanisław 'dozzie' Klekot: używanie && oraz || do sterowania nie jest bynajmniej błędęm. Prędzej za błąd bym uznał nadużywanie if-ów (oczywiście && i || też można nadużywać, ale to nie jest ten przypadek).

Nie została też uruchomiona podpowłoka — () to nie $().

Dozzie: odnośnie () masz rację. Nie wiedziałem że wykonuje to kod w podpowłoce, a swojego przykładu nie sprawdziłem w praktyce. Usunąłem go w związku z tym z wpisu.

Nie mogę się jednak z Tobą zgodzić że używanie || (czy &&) jest głupie. Jakkolwiek osobiście również nie stosuję tych konstrukcji (są wg mnie dużo mniej czytelne), znam kilka osób które cenią je sobie za zwięzłość. Ostatecznie to konstrukcje takie same jak każde inne.

Ale używanie || i && jako instrukcji sterujących jest z reguły głupotą. Jeśli to ma być if, to niech to będzie normalny if, a nie jakaś podróbka na jedną komendę. Wtedy widać że to jest warunkowe wykonanie. Poza tym nie trzeba wprowadzać jakichś dzikich hacków na shell w postaci {} i (), żeby tylko zestaw instrukcji się wykonał. No i if-else-fi nie wprowadza problemu niezerowego kodu błędu w ostatniej instrukcji bloku dla spełnionego testu (test && nabla || foo; jeśli nabla zawiedzie, to foo się wykona). && i || mają sens właściwie wyłącznie w warunkach do łączenia ze sobą kodów błędu kilku różnych komend.

Fajny tekst, myślałem na początku, że będą same oczywistości a okazuje się, że i ja popełniam pewne tego typu błędy. Dzięki :)

mina86: dzięki za link. Myślę że wraz z wspomnianym we wstępie do tekstu bash pitfalls, artykuł ten stanowi podstawę z którą należy się zapoznać chcąc pisać skrypty powłoki.

Piszesz:

zdecydowana większość (w tej chwili nie potrafię podać żadnego wyjątku, ale dopuszczam możliwość ich występowania) programów operujących na tekście przyjmuje plik na którym ma pracować jako argument

Najbardziej znany wyjątek to tr. Warto może też wspomnieć, że większość programów, które pozwalają podać nazwy plików w argumentach, interpretuje '-' jako stdin - dzięki temu można np. użyć zabawnej konstrukcji: paste - - &lt;plik.

I jeszcze przy okazji: pc -C apache pokaże wszystkie procesy wykonujące program apache, ale już np. apache2 czy apachectl nie - a ps -e|grep apache tak, co może być efektem zamierzonym.

Tak, właśnie tr miałem na myśli gdy pisałem tamto zdanie, ale wtedy nie mogłem sobie przypomnieć tego programu (nie wiem czy kiedykolwiek miałem potrzebę z niego korzystać). Później w innym miejscu mi o nim przypomniano.

A jeśli chodzi o ps — zgadzam się. Ale uważam również że osoby które potrzebują tego typu informacji są wystarczająco zaawansowanymi użytkownikami aby wiedzieć, że łączenie grep-a z ps-em nie jest bluźnierstwem i jest jak najbardziej dozwolone w pewnych sytuacjach. Jednocześnie sądzę że w zdecydowanej większości przypadków ps -C w zupełności wystarcza i nie ma potrzeby odwoływania się do pomocy grep-a.

ps -C aptana
PID TTY TIME CMD
ps -C Aptana
PID TTY TIME CMD
ps aux | grep aptana
pag-r 5710 14.3 8.9 2966260 356248 ? Sl 17:43 0:37 /usr/bin/java -Xms40m -Xmx512m -Djava.awt.headless=true -XX:MaxPermSize=256m -jar /home/pag-r/Aptana Studio 2.0/...
Widać polecenie ps -C nie zawsze działa jak powinno :)
Ale sam artykuł dobry co do cat|grep faktycznie tak robiłem :)

pag-r: działa jak powinno, tylko Ty nie wiesz że oczekujesz czegoś czego opcja -C Ci nie zapewni.

Porównaj wyniki:

ps o comm -C java
ps o args -C java

Opcja -C wyszukuje procesy po nazwie, a nazwą jest tylko ścieżka wykonywalna.
Opcja -C nie sprawdza czy argument znajduje się gdziekolwiek w nazwie procesu i przekazanych mu argumentach — a właśnie tak jest w przypadku o którym mówisz.

Gdybyś uruchomił jakiś program przez bash /usr/bin/some_program, to informacje o nim znajdziesz przez ps -C bash (a najlepiej ps o pid,tty,time,args -C bash, żeby móc zidentyfikować ten konkretny proces), a nie ps -C some_program.

Ponieważ ps nie oferuje żadnej opcji do przeszukiwania tablicy procesów po przekazanych im argumentach, w tym i podobnych przypadkach oczywiście grepowanie jest jedynym sposobem by zidentyfikować dany proces.

no chyba ze tak, nie czytalem mana, w kazdym razie kilka przydatnych informacji znalazlem w arcie:)

usuwanie plików tymczasowych przez wpisanie 'rm "$temporary_file"' jest zdecydowanie kiepskim pomysłem.

Zdecydowanie lepszym jest skorzystanie z trap.

Np. tak:

tempfile="$( mktemp )"
trap 'rm "$temp
file"' EXIT

Poniższy formularz służy do wysyłania komentarzy. O ich strukturę i prezentację dba Markdown.

Komentarze stanowią wyłączną własność autorów, choćby zaznaczono inaczej. Również autorom przysługuje wyłączne prawo do ich modyfikacji.

Autor zastrzega sobie prawo do moderacji komentarzy.

Śledzenie wątku: