friemler
Goto Top

FOR F mit einer variabelen Anzahl Tokens

Die FOR /F-Schleife bietet ja bekanntlich die Möglichkeit, Eingabedaten in sog. Tokens zu zerlegen. Dazu muss die Anzahl der Tokens aber schon beim erstellen des Batchscripts bekannt sein. Was aber, wenn das nicht der Fall ist? Oder man ein universell verwendbares Script schreiben möchte, das flexibel bzgl. der Anzahl der Tokens ist?

Ich möchte im folgenden ein Verfahren (zwei Unterprogramme für Batchscripts) vorstellen, mit der sich eine variable Anzahl Tokens (1 bis 31) für die FOR /F-Schleife realisieren lässt. Anschließend folgen noch zwei Anwendungsbeispiele.




back-to-topAllgemeines


back-to-topWas sind Tokens?
Die FOR /F-Schleife ist zur Verarbeitung einfach strukturierter Daten gedacht. Die einzelnen Datenfelder müssen durch Trennzeichen (Delimiter) voneinander separiert sein. Das kann z.B. der Backslash in Dateipfaden oder das Semikolon in CSV-Dateien von Excel sein. Die Datenfelder zwischen diesen Trennzeichen werden als Tokens bezeichnet. Die Standard-Trennzeichen von FOR /F sind das Leer- und das Tabulatorzeichen. Es können aber auch (mehrere) eigene Trennzeichen definiert werden.


back-to-topNutzen des Verfahrens
Man kann ein Script so entwickeln, dass es für gleichartige Eingabedaten mit einer unterschiedlichen Anzahl Tokens ohne Anpassung verwendbar ist.


back-to-topDie Syntax der FOR /F-Schleife

Für ausführliche Informationen verweise ich auf mein Tutorial zur FOR-Schleife.


back-to-topDas Verfahren

Die Idee zu dem Verfahren kam mir, als mir auffiel, dass normale Umgebungsvariablen, deren Inhalt aus Bezeichnern von Laufvariablen der FOR /F-Schleife besteht, innerhalb einer Schleife sozusagen doppelt ausgewertet werden. Vor Beginn der Schleife werden sie erweitert (der Code enthält nun die Bezeichner der Laufvariablen) und während der Abarbeitung der Schleife werden dann die Laufvariablen erweitert.

Also müsste man
  1. eine Probe der Eingabedaten untersuchen, um herauszufinden, wie viele Tokens die Eingabedaten enthalten und
  2. eine Umgebungsvariable mit so vielen Bezeichnern für Laufvariablen füllen wie Tokens vorhanden sind.

Aufgabe 1 wird vom Unterprogramm CountTokens erledigt, Aufgabe 2 vom Unterprogramm GenVars.

Um den praktischen Nutzen zu erhöhen, legt GenVars noch eine weitere Variable an, die den Bezeichner der letzten auftretenden Laufvariablen enthält, die bei der Abarbeitung der FOR /F-Schleife dem letzten Token entspricht.

Durch einen Parameter von GenVars kann außerdem gesteuert werden, ob die Bezeichner der Laufvariablen in Anführungszeichen eingeschlossen werden sollen. Das ist dann von Bedeutung, wenn einzelne Tokens auch Leerzeichen enthalten könnten und so wie im Anwendungsbeispiel 2 nach der Erfassung einzeln weiterverarbeitet werden sollen.


back-to-topEinschränkungen
Natürlich gelten auch hier wieder die Einschränkungen des Batchscript-Interpreters in Bezug auf die Verarbeitung von Zeichenketten, die bestimmte Sonderzeichen enthalten (%, ", ^, usw.). Man muss sich eben sicher sein, dass diese Sonderzeichen nicht auftreten können.

Die Eingabedaten können aufgrund von Beschränkungen der FOR-Schleife minimal 1 Token und maximal 31 Tokens enthalten.


back-to-topDie Unterprogramme


back-to-topCountTokens
:: Parameter:
:: %1 - Eine Zeichenkette, die darauf untersucht werden soll
::      wie viele Tokens sie enthält.
:: %2 - Ein einzelnes Zeichen, dass als Trennzeichen zwischen
::      den Tokens gelten soll.

:: Rückgabe:
:: Die Variable nTokens enthält die Anzahl der gefundenen Tokens

:CountTokens
setlocal

set "str=%~1"  
set nTokens=1

:CountTokensLoop
call set "newstr=%%str:%~2=%%"  

if "%newstr%" neq "%str%" (  
  set /a nTokens+=1

  call set "str=%%str:*%~2=%%"  
  goto :CountTokensLoop
)

endlocal & set nTokens=%nTokens%
exit /b
In Zeile 17 wird die Zeichenkette von allen Zeichen befreit, die dem angegebenen Trennzeichen entsprechen. Wenn diese neue Zeichenkette und die ursprüngliche danach gleich sind, wird die Verarbeitung abgebrochen.

Ansonsten wird in Zeile 20 der Zähler für die Tokens erhöht und in Zeile 22 die übergebene Zeichenkette um das erste Trennzeichen und alle Zeichen davor verkürzt. Danach wird wieder an den Schleifenanfang gesprungen.


back-to-topGenVars
:: Parameter:
:: %1 - Ein einzelnes Zeichen, dass als Trennzeichen zwischen die Bezeichner
::      der Laufvariablen eingefügt werden soll.
:: %2 - Die Anzahl der Tokens, die von CountTokens ermittelt wurde.
:: %3 - Wenn dieser Parameter den String QUOTE enthält (Groß-/Kleinschreibung
::      egal), werden die Bezeichner der Laufvariablen in Anführungszeichen
::      eingeschlossen. Weglassen kann man den Parameter NICHT!
:: %4   und folgende sind die gewünschten Bezeichner für die Laufvariablen der
::      FOR-Schleife OHNE Prozentzeichen davor.

:: Rückgabe:
:: - Die Variable Tokens enthält die Bezeichner aller Laufvariablen für die
::   FOR-Schleife
:: - Die Variable LastToken enthält den Bezeichner der letzten Laufvariablen
::   für die FOR-Schleife

:GenVars
setlocal

set "Delim=%~1"  
set "nToks=%~2"  
set "Quote=%~3"  

if /i "%Quote%" equ "quote" (  
  set Tokens="%%%4"  
) else (
  set Tokens=%%%4
)

set /a Cnt=1
shift & shift & shift & shift

:GenVarsLoop
if %Cnt% lss %nToks% (
  if /i "%Quote%" equ "quote" (  
    set "Tokens=%Tokens%%Delim%"%%%1""  
  ) else (
    set "Tokens=%Tokens%%Delim%%%%1"  
  )

  shift

  set /a Cnt+=1
  if "%1" neq "" goto :GenVarsLoop  
)

if /i "%Quote%" equ "quote" (  
  set "LastToken=%Tokens:~-4%"  
) else (
  set "LastToken=%Tokens:~-2%"  
)

endlocal & set "Tokens=%Tokens%" & set "LastToken=%LastToken%"  
exit /b
Der Bezeichner für die erste Laufvariable wird der Variablen Tokens in Zeile 25 bzw. Zeile 27 zugewiesen, abhängig vom Parameter %3. Die weiteren Bezeichner werden in Zeile 36 bzw. Zeile 38 hinzugefügt. Vor die Bezeichner wird immer nur ein Prozentzeichen gesetzt, das genügt in diesem Fall. Durch die SHIFT-Befehle in Zeile 31 bzw. Zeile 41 wird der nächste gewünschte Bezeichner in die Parametervariable %1 verschoben. Der Zähler CNT läuft nur mit, damit auch der Fall "Anzahl der Tokens ist 1" korrekt behandelt werden kann.


back-to-topAnwendungsbeispiel 1

Man hat folgende Verzeichnisstruktur:
Kunden
Kunden\Kunde_Maier
Kunden\Kunde_Maier\Maier_Rechnungen_2009
Kunden\Kunde_Maier\Maier_Rechnungen_2010
Kunden\Kunde_Maier\Maier_Rechnungen_2011
Kunden\Kunde_Mueller
Kunden\Kunde_Mueller\Mueller_Rechnungen_2010
Kunden\Kunde_Mueller\Mueller_Rechnungen_2011
Kunden\Kunde_Schulze
Kunden\Kunde_Schulze\Schulze_Rechnungen_2008
Kunden\Kunde_Schulze\Schulze_Rechnungen_2009
Kunden\Kunde_Schulze\Schulze_Rechnungen_2010
Aufgabe ist, die Dateien, die sich in den Verzeichnissen X_Rechnungen_Y befinden, in passwortgeschützte ZIP-Archive zu packen, die den Namen des Verzeichnisses erhalten, aus dem die enthaltenen Dateien stammen, z.B. Maier_Rechnungen_2009.zip. Diese Verzeichnisstruktur kann auf mehreren Rechnern existieren, immer unter einem anderen Basispfad mit einer verschiedenen Anzahl übergeordneter Verzeichnisse. Das ist zwar kein Real-World-Beispiel und zeugt von einer schlechten Organisation, aber das ist hier nicht das Thema.

Normalerweise müsste das Batchfile zur Erstellung der ZIP-Archive auf jeden Rechner angepasst werden. Durch dynamisch erzeugte Laufvariablen/Tokens kann das Verzeichnis Kunden auf der 1. bis (31-FolderDepth). Ebene einer Verzeichnisstruktur liegen. Die Variable FolderDepth gibt die Anzahl der Verzeichnisse unterhalb von Kunden an. Die Variable SrcDir muss immer einen vollständigen Pfad inkl. Laufwerk enthalten.
@echo off

setlocal

set "SrcDir=E:\Test1\Test2\Kunden"  
set "DestDir=E:\Rechnungen_gezippt"  
set FolderDepth=2

set "Delimiter=\"  


call :CountTokens "%SrcDir%" "%Delimiter%"  
set /a nTokens+=FolderDepth

call :GenVars "%Delimiter%" %nTokens% noquote ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ]  

for /f "eol= delims=%Delimiter% tokens=1-%nTokens%" %%? in ('dir /b /s /a:d /o:ne "%SrcDir%" 2^>NUL') do (  
  if "%LastToken%" neq "" (  
    7z.exe a -pGEHEIM -mx0 "%DestDir%\%LastToken%.zip" "%Tokens%\*.*"  
  )
)

exit /b
Die Variable LastToken enthält den Bezeichner der letzten automatisch erzeugten Laufvariablen, die hier den Namen des Verzeichnisses mit den zu verarbeitenden Dateien repräsentiert. Nur wenn diese Variable einen Inhalt hat, wird gerade die richtige Verzeichnisebene betrachtet, deshalb Zeile 18.


back-to-topAnwendungsbeispiel 2

Der Inhalt der Zellen einer Excel-Tabelle, die als CSV-Datei vorliegt, soll für jede Zelle einzeln weiterverarbeitet werden, unabhängig davon, wie viele Spalten die Tabelle hat. Die Anzahl der Spalten darf aber höchstens 31 sein.
@echo off

setlocal

set "SrcFile=E:\Test.csv"  
set /p "Line=" < "%SrcFile%"  

set "Delimiter=;"  

call :CountTokens "%Line%" "%Delimiter%"  
call :GenVars " " %nTokens% quote ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ]  

for /f "usebackq eol= delims=%Delimiter% tokens=1-%nTokens%" %%? in ("%SrcFile%") do (  
  set "Line=%Tokens%"  
  call :ProcessLine
  echo.
)

endlocal
exit /b


:ProcessLine
for %%i in (%Line%) do <NUL set /p "=%%~i" & echo.  
exit /b
Damit der Inhalt der Zellen auch Leerzeichen enthalten kann, wird hier der Parameter QUOTE benutzt. Die Bezeichner der Laufvariablen werden dadurch in Anführungszeichen gesetzt.

Der Inhalt einer Tabellenzeile steht für jeden Durchlauf der FOR-Schleife im Hauptprogramm in der Variablen Line zu Verfügung. Die Zelleninhalte sind durch Leerzeichen separiert und in Anführungszeichen eingefasst und können somit von der FOR-Schleife in Zeile 24 ausgegeben werden.


Gruß
Friemler

Content-ID: 167111

Url: https://administrator.de/contentid/167111

Ausgedruckt am: 26.11.2024 um 23:11 Uhr

mathe172
mathe172 13.06.2011 um 21:29:10 Uhr
Goto Top
Hallo,

zuerst mal: Gutes tut, wie immer face-smile

Nur noch kurz eine Frage:
1. Warum ist in Beispiel 2 die Zeile 14 in der Schleife?
2.
Würde sich diese Idee nicht irgendwie so verwirklichen lassen?
@echo off & setlocal enabledelayedexpansion
set "Delim=:"  
set "Rare="  
::Oder anderes seltenens Zeichen
set "ToSplit=Das:ist:ein:Test"  

set "ToSplit=!ToSplit: =%Rare%!"  
set "ToSplit=!ToSplit:%Delim%= !"  

for %%A in (%ToSplit%) do (
set "Token=%%A"  
set "Token=!Token:%Rare%= !"  
call :DoSomething !Token!
)
goto :eof

:DoSomething
echo.%~1
goto :eof

MfG,
Mathe172
Friemler
Friemler 14.06.2011 um 13:57:53 Uhr
Goto Top
Hallo mathe,

Zu Frage 1:
Die Variable Tokens enthält doch die Bezeichner einer variablen Anzahl von Laufvariablen der FOR-Schleife. In Zeile 14 werden diese durch die Werte der zugehörigen Tokens ersetzt.

Zu Frage 2:
Sicher, in Bezug auf Beispiel 2 ließe sich Dein Code auch verwenden. Was ich daran aber nicht so gut finde ist die Ersetzung von Leerzeichen durch ein selten benutztes Zeichen. Evtl. ist genau dieses Zeichen doch mal im zu zerlegenden Text enthalten, kann man vorher ja nie wissen. Du hast hier zwar den Smily aus dem DOS-Zeichensatz verwendet, aber dieses Zeichen (und alle mit einem Code kleiner 32) kann ich im von mir favorisierten Texteditor TextPad nicht eingeben, müsste zur Änderung dieses einen Zeichens den Code extra in MS-Edit laden und ändern.

Das ganze war auch mehr zur Vorstellung eines Konzepts gedacht, sozusagen Grundlagenforschung für FOR in Batchscript. Und wie das mit Grundlagenforschung so ist: Wie man es sinnvoll einsetzt muss jetzt wieder ein anderer erforschen. face-wink

Gruß
Friemler
mathe172
mathe172 14.06.2011 um 16:45:21 Uhr
Goto Top
Hallo,

Die Idee zu dem Verfahren kam mir, als mir auffiel, dass normale Umgebungsvariablen, deren Inhalt aus Bezeichnern von Laufvariablen der FOR /F-Schleife besteht, innerhalb einer Schleife sozusagen doppelt ausgewertet werden. Vor Beginn der Schleife werden sie erweitert (der Code > enthält nun die Bezeichner der Laufvariablen) und während der Abarbeitung der Schleife werden dann die Laufvariablen erweitert.
Uups, sollte besser lesen face-wink

Zu 2.: Du hast wie immer recht, es ist ein gefährlich(und aufwendig). (Das muss ja niemand wissen face-wink)

MfG,
Mathe172
jeb-the-batcher
jeb-the-batcher 20.06.2011 um 14:23:47 Uhr
Goto Top
Hallo Friemler,

zwei Gedanken kamen mir da so.
Wieso nimmst du nicht einfach immer die Maximalanzahl an tokens an? Wenn es weniger sind ist es ja auch nicht tragisch.

Warum teilst du die einzelnen Zeilen nicht durch Linefeeds, dann fällt doch auch die Begrenzung auf maximal 31 Tokens/Spalten weg.

Also z.B. um eine CSV-Datei zu lesen

@echo off
setlocal DisableDelayedExpansion
set LF=^


set rowCount=0
FOR /F ^"eol^=^  

delims^=^" %%R in (table.csv) do (  
  set /a rowCount+=1
  set "row=%%R"  
  call :splitRow 
  echo(
)
goto :eof

:splitRow
setlocal EnableDelayedExpansion
for %%L in ("!LF!") do set "row=#!row:;=%%~L#!"  
FOR /F ^"eol^=^  

delims^=^" %%C in ("!row!") do (  
  set column=%%C
  echo Row #!rowCount! Column = !column:~1!
)
endlocal
goto :eof

Beispiel Ausgabe:
Row #1 Column = hallo
Row #1 Column = zwei
Row #1 Column = drei

Row #2 Column = Line2
Row #2 Column = vier
Row #2 Column = fünf

Normalerweise kann man mit FOR /F einen String nur in einzelne Token zerlegen, mit Linefeeds kann man aber pro delim eine eigene Zeile erzeugen und daher auch beliebig viele "Spalten" bearbeiten.
Die # hänge ich vor jede Spalte damit ich auch die leeren Spalten erwische.

jeb
Friemler
Friemler 20.06.2011 um 15:38:23 Uhr
Goto Top
Hallo jeb,

Zitat von @jeb-the-batcher:
Wieso nimmst du nicht einfach immer die Maximalanzahl an tokens an? Wenn es weniger sind ist es ja auch nicht tragisch.
Beim Beispiel 1 ist es wichtig, an das letzte vorhandene Token heranzukommen.

Zitat von @jeb-the-batcher:
Warum teilst du die einzelnen Zeilen nicht durch Linefeeds
Weil das Deine Spezialtricks sind. face-wink

bastla hat mir per PN schon geschrieben, dass für Beispiel 1 diese Sache auch nicht notwendig ist. Und Beispiel 2 lässt sich im konkreten Fall sicher noch auf ein paar mehr Wegen lösen als denen, die von Mathe und Dir beschrieben wurden. Wie gesagt, es sollte nur ein Konzept vorgestellt werden. Evtl. ist es ja wirklich nicht praxisrelevant, aber evtl. auch gut zu wissen, dass es geht.

Gruß
Friemler