Z pogranicza

Wykorzystanie list danych w wielokrotnych podstawieniach

  • 2011-02-08 07:33
  • 2 komentarze
  • 1864 odsłony
  • Oceń ten tekst jako pierwszy
Bardzo mi brakowało obsługi plików tekstowych jako list tekstowych w skryptach DOS/Windows. Wcześniej spodobała mi się możliwość użycia zwykłego pliku jako zwykłego zbioru kolejnych wierszy tekstowych, którą oferowały w liście poleceń niektóre archiwizery. Jednak takie podejście do podawania argumentów było odosobnione i ograniczone tylko do tych programów, których autorzy byli na tyle przewidujący, żeby taką możliwość udostępnić.

Wpadłem więc na pomysł, który telepał mi się w głowie latami, że najlepiej byłoby gdyby sam system obsługiwał takie perspektywiczne podejście do podawania argumentów wywołania dowolnego programu. Tego się jednak nigdy nie doczekałem. Za to doczekałem się rozszerzeń Batcha, które wydały się tyleż ciekawe, co niedziałające. Niestety polskie amatorskie tłumaczenie systemu Windows (szczególnie opis tych rozszerzeń) bardzo utrudniało zrozumienie jakie możliwości się za nimi kryją - szczególnie kluczowe opisy dla for, set i call. Trzeba było sięgnąć po oryginalne teksty, które pozbawione są nonsensów wynikłych z wadliwego tłumaczenia.
Tak czy inaczej rozszerzenia pozwoliły poważniej pomyśleć nad prawdziwą i uniwersalną obsługą list tekstowych. Co prawda nie można było zmusić systemu operacyjnego Windows aby nagle zaczął interpretować nazwy plików list jako agregaty danych do podstawiania, ale można było stworzyć program, który wykona to za niego i wielokrotnie uruchomi oryginalny wiersz polecenia dla wszystkich danych zawartych w takim pliku. Tak więc powstał projekt składni, w której wystarczyło podać nazwę tego hipotetycznego procesora wiersza poleceń i właściwe polecenie z wstawkami składającymi się z nazw plików list zawierających konkretne dane. Problemem dodatkowym, który dostaliśmy w prezencie wraz z Windows 95 i VFAT okazały się nazwy plików, które przecież zawsze mogły zawierać spacje. Ten problem zostawiłem sobie jednak na później.

Idea sprowadziła się więc do potraktowania pierwszego argumentu jako polecenia i przeparsowanie pozostałych uwzględniając, że wszędzie gdzie pojawi się znaczek @ pojawi się nazwa pliku z listą wierszy do podstawienia. Można to było oczywiście zrobić w "normalnym" języku programowania, ale wraz z pojawieniem się rozszerzeń Batcha nie było potrzeby wyważania otwartych drzwi. Tak powstał więc projekt składni tego skryptu: http://forum.4programmers.net/[...]tchu_rekurencyjna_obsluga_list

Przede wszystkim interesujące wydało się rozszerzenie polecenia FOR o parsowanie tekstu i dzielenie poszczególnych elementów na tokeny. Drugą interesującą odmianą tego polecenia było parsowanie pliku wierszami i przypisywanie ich zawartości iterowanej w pętli zmiennej. Te dwie rzeczy właściwie wystarczyłyby żeby stworzyć skrypt, który będzie sam parsował argumenty, identyfikował nazwy list oraz robił coś z ich zawartością.

Sama idea jest całkiem prosta, choć w wykonaniu trochę się komplikuje. Trzeba przeszukiwać kolejne argumenty wywołania i znajdujące się wśród nich ścieżki do plików list rozwijać oraz dla każdego rozwinięcia znowu uruchamiać tę samą procedurę. Należy to powtarzać tak długo aż każde wywołanie będzie zawierało wyłącznie rozwinięcia.Takie polecenie będzie mogło zostać ostatecznie wykonane jako finalny produkt wszystkich kombinacji podstawień.

Teraz szczegóły. Pierwszy i każdy kolejny argument przypisujemy zmiennej %checking% tak aby od razu usunąć z niego ewentualny cudzysłów. To zadanie robi set checking=%~1
Przeszukując kolejne argumenty wywołania trzeba tylko znaleźć wśród nich znaki specjalne oznaczające początek i koniec nazwy listy (tutaj @) - tak aby podzielić każdy argument na trzy części: początek właściwą nazwę pliku listy oraz całą resztę, wśród której wciąż mogą kryć się kolejne nazwy list. To zadanie wykonuje polecenie:
for /f "tokens=1,2* delims=@" %%A in ("%checking%") do set argHead=%%A&set argList=%%B&set argTail=%%C

Mówi ono że ciąg znaków reprezentowany przez "%checking%" podzieli na części za pomocą znaku @, przypisze pierwszą zmienną A do pierwszego elementu, drugą zmienną B do drugiego elementu (czyli już między znakami @) oraz wszystkie pozostałe podzielone przez znak @ elementy czyli * - połączy i przypisze do ekstra dodatkowej zmiennej o kolejnej literze, czyli w tym wypadku %%C. W poleceniu tej dziwnej odmiany for przypisywane są te zmienne bardziej treściwym zmiennym argHead, argList i argTail. Głównie po to aby można było nimi manipulować i nie pogubić się przy tym. Okazało się w międzyczasie, że funkcja podziału ciągu znaków przez polecenie for działa świetnie z jednym wyjątkiem. Kiedy znak podziału (@) znajdzie się na początku łańcucha, to pierwszy argument czyli argHead otrzyma wartość za nim czyli tą, którą w każdym innym wypadku otrzyma argList. Ten jeden wyjątek jest sprawdzany i właściwie korygowany przez polecenie:
if [@] equ [%checking:~0,1%] (set argTail=%argList%%argTail%&set argList=%argHead%&set argHead=)

Przypadek gdy %checking% składa się tylko z jednego znaku @ też dobrze działa bo cała reszta ciągu za nim przypisana zostaje do %argList%, a %argTail% pozostanie puste. Działa to również dla całego argumentu otoczonego cudzysłowem i zawierającym spacje.

Trzeba jeszcze wykryć czy w ogóle jakiekolwiek znaki podziału występują bo na tej podstawie można w ogóle mówić o znalezieniu nazwy listy. Sprawa jest prosta - jeżeli znaku takiego nigdzie nie będzie, to całość sprawdzanego ciągu (%checking%) znajdzie się w pierwszej zmiennej czyli %%A, czyli %argHead%. I to jest właśnie sprawdzane w poleceniu:
if ["%argHead%"] neq ["%checking%"] goto :listFound

Kod, który znajdzie się za ta instrukcją będzie wiedział, że żadnej listy nie znaleziono. Przypadek znalezienia listy będzie opisany w następnym akapicie.
Gdy znak podziału nie zostanie znaleziony (czyli ścieżka do pliku listy), to zmienne argHead, argList i argTail będą już zbędne i można odczyścić z nich środowisko. Jednak aby się o tym upewnić trzeba sprawdzone elementy odrzucić, czyli dołączyć cały testowany właśnie argument do łańcucha zmiennej %parsed% (która dla pierwszego argumentu jest pusta). A to robi set parsed=%parsed% %checking%. Następnie trzeba  "zjeść" sprawdzony już argument %checking%, czyli faktycznie %1, co robi polecenie shift /1, nie ruszając przy tym argumentu %0, który będzie potrzebny później do kolejnych wywołań.
Po obrocie pętli sprawdzane jest czy kolejny argument do sprawdzenia (czyli poprzedni %2) jest pusty. Jeżeli jest, to wiemy, że nie ma już więcej argumentów i cały ciąg argumentów pozbawionych znaków @ znajduje się w zmiennej %parsed%. Dzięki czemu można je spokojnie wywołać jako docelowe wywołanie co robi polecenie call %parsed% i kończy swoje działanie w tej instancji. Jeżeli argumenty skryptu list.bat w ogóle nie zawierały znaku @, to na tym kończy się całość działania tego skryptu. Czyli na wywołaniu argumentów list.bat jako właściwego polecenia.

W przypadku znalezienia listy w sprawdzanym argumencie %checking%, wywoływana jest druga postać polecenia for, czyli:
for /f "usebackq tokens=*" %%L in ("%argList%") do call %0 %parsed% %argHead%%%L%argTail% %2 %3 %4 %5 %6 %7 %8 %9

Jej działanie sprowadza się to tego, że interpretuje argList jako ścieżkę do pliku tekstowego, którego wiersze są kolejno podstawiane do zmiennej %%L.
Wykorzystany został tu mały trik, który polega na tym, że ciąg tokens został zdegenerowany do '*', co powoduje, że do zmiennej iterowanej czyli L nie jest nic przypisywane (nie zawiera żadnych cyfr), ale zaraz potem do następnej niewykorzystanej zmiennej, czyli znowu L przypisywana jest cała pozostała znaleziona zawartość wiersza. Efekt jest taki, że zmiennej tej przypisywany jest cały pojedynczy wiersz z pliku tekstowego. Zmienna argList zawiera spacje, więc aby mogła być użyta jako nazwa pliku musi zostać otoczona jawnymi cudzysłowami, a z kolei aby cudzysłowowy były dozwolone w nazwie zbioru (czyli ścieżce pliku) trzeba użyć opcji usebackq.
Polecenie call z kolejnymi podstawionymi wierszami pochodzącym z %argList% wywołuje znowu samego siebie czyli polecenie LIST.BAT (nawet jeżeli ma inną nazwę) w taki sposób, że na liście argumentów są kolejno:
1. Elementy argumentów pozbawione już znaków @ czyli zawarte w %parsed%
2. Bieżący parsowany argument z rozwinięciami pochodzącymi z (wykrytego jako lista) pliku, czyli %argHead%%%L%argTail%
3. Pozostałe argumenty wywołania, które być może wymagają jeszcze dalszego parsowania, czyli argumenty od %2 do %9

Kolejna instancja składająca się z ciągu wywołań z rozwinięciami %%L znowu będzie parsować cały wiersz argumentów poleceń, ale tym razem nie natknie się na znaki @ i zawartą między nimi listę ponieważ w tym wywołaniu będzie to już zawartość wiersza pochodzącego ze znalezionego pliku listy1. Tak więc elementy te zostaną pominięte i parsowanie zacznie się faktycznie od %argTail% argumentu parsowanego w poprzedniej instancji skryptu. Jeżeli %argTail% okaże się pozbawiony znaków @, to parsowany będzie %2 z poprzedniej instancji. W ten sposób każdy znaleziony argument między znakami @ lub między @, a cudzysłowem (albo spacją) zostanie rozwinięty i efektem tego rozwinięcia będzie kolejna instancja wywołania skryptu LIST.BAT z pozostałymi jeszcze do rozwinięcia ewentualnymi argumentami.

Można więc uznać, że pojawi się tyle instancji ile na liście argumentów użyte zostanie list2 i tyle wywołań w każdej instancji ile wierszy liczy każda użyta lista. Całkowita liczba wykonań, powinna zamknąć się w pomnożonych przez siebie ilościach wierszy wszystkich użytych list.

Działające uproszczenia: Zmienna %parsed% będzie na swoim początku zawierała niepotrzebną spację, którą można odfiltrować odpowiednim przypisaniem z odcięciem pierwszego znaku np. set parsed=%parsed:~1%. Ale ponieważ wywołania programów lub skryptów z argumentami (np. call) ignoruje nadmiarowe spacje, to polecenie takie jest zbędne. Być może zbędne jest również eliminowanie zmiennych środowiska użytych w skrypcie ponieważ tylko raz udało mi się te zmienne po wykonaniu polecenia list zobaczyć zdefiniowane.

A oto inne zastosowanie rozszerzeń Batcha użyte do nieśmiertelnego idioto-odpornego "Are you sure?". :)
:quest
set /p resp=Are you sure to continue? (y/n)
if [%resp:N=n%]==[n] ( goto :eof ) else if not [%resp:Y=y%]==[y] (echo Please type: y or n&goto :quest) else goto :continue
:continue

W tym przypadku odpowiedź inna niż oczekiwane litery n, N, y, Y nie pozwoli na kontynuowanie, a jednocześnie można łatwo zmienić goto :eof na cokolwiek innego lub w ogóle pominąć część else goto :continue i etykietę jeżeli wykonać ma się kod bezpośrednio za tym. Zwarte i uniwersalne, chociaż niezbyt czytelne.


1 Oczywiście można sobie wyobrazić, że w pliku tym też zostaną użyte znaki @, co daje teoretycznie nieskończone możliwości podstawiania wartości z kolejnych list. Ta cecha przerosła moje oczekiwania co do uniwersalności podstawiania danych z list.
2 Bez uwzględnienia rozwinięć kolejnych list użytych w wierszach samych plików list.

2 komentarze

Olamagato 2011-05-25 22:48

Przedstawiony skrypt ma wciąż pewne wady.
1. Eliminuje jawnie użyte znaki cudzysłowa, co praktycznie uniemożliwia działanie z użyciem list polecenia del z długimi nazwami plików zawierającymi spacje (a to ogromna wada).
2. Nie pozwala na użycie w listach danych znaków zabronionych dla plików MS-DOS (np. &()%!) ponieważ interferują one z parsowaniem rozszerzonej składni poleceń zawartych w pliku skryptu oraz znaków @ w innym kontekście niż jako znaczników informujących o początku nazwy pliku listy. Szczególnie znaki &, nawiasy i @ są dość często używane w długich nazwach plików.

msm 2011-02-09 22:14

Nieźle :)
Tym razem dokładnie przeczytałem i pomysł mi się podoba (chociaż jakieś 10 lat spóźniony ;) ) - jeśli będę coś w najbliższym czasie pisał w batchu to na pewno się przyda.
Ale do Twojego poziomu znajomości tego języka mi bardzo daleko :/