Wykorzystanie list danych w wielokrotnych podstawieniach

Olamagato

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ć.

     1 Idea
     2 Implementacja
     3 Inne zastosowanie rozszerzeń Batcha

Idea

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 przeparsowania pozostałych – uwzględniając, że wszędzie, gdzie pojawi się znaczek @, pojawi się też 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 więc powstał projekt składni tego skryptu: Co można zrobić w batchu? (rekurencyjna obsługa 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 do 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 rozwijać znajdujące się wśród nich ścieżki do plików list. Dla każdego rozwinięcia znowu trzeba 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ń.

Implementacja

Teraz szczegóły. Pierwszy i każdy kolejny argument przypisujemy zmiennej %checking% tak, aby od razu usunąć z niego ewentualny cudzysłów. To zadanie wykonuje następujące polecenie:

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 następujące polecenie:

for /f "tokens=1,2* delims=@" %%A in ("%checking%") do set argHead=%%A&set argList=%%B&set argTail=%%C

Mówi ono, że za pomocą znaku @ ciąg znaków reprezentowany przez fragment "%checking%" zostanie podzielony na części, do pierwszego elementu zostanie przypisana pierwsza zmienna %%A, do drugiego elementu (czyli już między znakami @) zostanie przypisana druga zmienna %%B, a wszystkie pozostałe elementy – podzielone przez znak @, czyli * zostaną połączone i przypisane do 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. Podczas pisania okazało się, że funkcja podziału ciągu znaków za pomocą polecenia for działa świetnie z jednym wyjątkiem. Kiedy znak podziału (tj. @) 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 następujące 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% pozostaje puste. Działa to również dla całego argumentu otoczonego cudzysłowem i zawierającego spacje.

Trzeba jeszcze wykryć, czy w ogóle jakiekolwiek znaki podziału występują – bo dopiero 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 następującym 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). To robi polecenie 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ę do 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 samo 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 znakiem @ a cudzysłowem lub 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) ignorują 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.

Inne zastosowanie rozszerzeń Batcha

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 też 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 komentarzy

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.

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 :/