neonzero
Goto Top

Batch - setlocal EnableDelayedExpansion - ersetzen von Text, der ein Ausrufezeichen enthält

a) Batch-Variable: !-Zeichen durch ^^! ersetzen (wurde verworfen)
b) zwischen den setlocal-Modi DisableDelayedExpansion und EnableDelayedExpansion wechseln
Tipp: Wird der Inhalt in den Quelltextfenstern chaotisch angezeigt, kann es hilfreich sein dort auf den Link "Quelltext" (oben rechts) zu klicken. Es öffnet sich dann ein separates Fenster, welches in der maximierten Darstellung, je nach Bildschirmauflösung, den unerwünschten Zeilenumbruch unterbindet.  
Der Beitrag, inkl. Quelltext, ist Gemeinfrei (er darf ohne jegliche Einschränkung genutzt werden).

Hallo.

Ich habe ein Problem damit, einen String durch einen anderen zu ersetzen, wenn der Text ein Ausrufezeichen enthält. Hier ein Beispielcode, der in einer Textdatei sämtliches Aufkommen von „foo“ in „bar“ ersetzt:

@echo off & setlocal EnableDelayedExpansion

call :FncReplaseString "foo" "bar" "d:\tmp\test.txt"  
goto :EOF

:FncReplaseString  {{comment_single_line_remark:0}}
::Parameters:   <FromString> <ToString> <FileName>
::        or:   <VarNameToReturnReplasedText> <FromString> <ToString> <TextToReplase>
  if not "%~4" == "" ( ::String im übergebenen Text austauschen  
    set _FncReplaseString_Text=%~4
    set %~1=!_FncReplaseString_Text:%~2=%~3!
    rem Alternativ dazu: call set %~1=%%_FncReplaseString_Text:%~2=%~3%%
  ) else if exist "%~3" ( ::String in einer Text-Datei austauschen  
    set _FncReplaseString_TmpFile=%TMP%\%~n0_out%RANDOM%.tmp
    for /F "tokens=1* delims=:" %%a in ('findstr /n $ %3') do (  
      if not "%%b" == "" (  
        set _FncReplaseString_TextLine=%%b
        set _FncReplaseString_TextLine=!_FncReplaseString_TextLine:%~1=%~2!
        echo.!_FncReplaseString_TextLine!
      ) else (echo.)
    ) >>!_FncReplaseString_TmpFile!
    move "!_FncReplaseString_TmpFile!" "%~3"  
  )
 exit /b 0

Die Ersetzung funktioniert, jedoch werden dabei auch sämtliche Ausrufezeichen, die die Datei möglicherweise enthält, eliminiert.

Mir ist klar, dass ein Bug in setlocal EnableDelayedExpansion die Ursache des Problems ist. Ich suche nur eine Lösung dafür, da der Bug schon sein Jahren existiert und kaum noch damit zu rechnen ist, das MS ihn je fixen wird. Eine Möglichkeit, unter diesen Umständen ein Ausrufezeichen zuzuweisen und auszugeben wäre:

@echo off & setlocal EnableDelayedExpansion
set _x=Hallo^^!
echo _x=!_x!
------------------
Ausgabe: _x=Hallo!

Mein erster Ansatz sah deshalb vor, sämtliche Ausrufezeichen in _FncReplaseString_TextLine durch „^^!“ zu ersetzen, bevor die Zeile ausgegeben wird. Und genau dafür suche ich nach einer Lösung. Mir wollte das bisher nicht gelingen. Kann mir jemand von euch dabei auf die Sprünge helfen? Oder hat hier einer eine vollkommen andere Idee, wie sich das realisieren läßt?

Bye, nz


Nachtrag: Hier der etwas aufbereitete Quellcode, der das Problem behebt. -- NeonZero 20.08.2008 22:58

@echo off

call :FncReplaseString "foo" "bar" "d:\tmp\test.txt"  
goto :EOF

:FncReplaseString  {{comment_single_line_remark:0}}
::Parameters:   <FromString> <ToString> <FileName>
::        or:   <VarNameToReturnReplasedText> <FromString> <ToString> <TextToReplase>
::Note:         "="-Characters are not allowed in FromString 
::Example:      call :FncReplaseString _Var "foo" "bar" "the foo"     (_Var="the bar") 
::              call :FncReplaseString "foo" "bar" "d:\text.txt"      (replase string in text-File) 
  setlocal DisableDelayedExpansion &::damit bei der Wertzuweisung keine Ausrufezeichen verloren gehen
  if "%~4" == "" goto :LEB_FncReplaseString_File  
  ::String im uebergebenen Text austauschen und der Variablen (%1) zuweisen
    set "_FncReplaseString_Text=%~4"  
    setlocal EnableDelayedExpansion  &::ermoeglicht die folgende Syntax
    set _FncReplaseString_Text=!_FncReplaseString_Text:%~2=%~3!
      rem Alternativ dazu: call set %~1=%%_FncReplaseString_Text:%~2=%~3%%
    ::die beiden oben und hier geoeffneten setlocal-Instanzen schließen & Variable uebernehmen:
    endlocal & set "_FncReplaseString_Text=%_FncReplaseString_Text%"  
    endlocal & set "%~1=%_FncReplaseString_Text%"  
   exit /b 0
   
  :LEB_FncReplaseString_File
  set _FncReplaseString_TmpFile=%TMP%\%~n0_out%RANDOM%.tmp
  if exist "%~3" ( ::String in einer Text-Datei austauschen  
    for /F "tokens=1* delims=:" %%a in ('type %3 ^| findstr /n $') do (  
    ::type konvertiert UC nach ANSI; findstr verhindert, dass Leerzeilen verloren gehen
      if not "%%b" == "" (  
        set _FncReplaseString_TextLine=%%b
        setlocal EnableDelayedExpansion  &::ermoeglicht die folgende Syntax
        set _FncReplaseString_TextLine=!_FncReplaseString_TextLine:%~1=%~2!
        echo.!_FncReplaseString_TextLine!
        endlocal &rem schließt die hier geoeffnete setlocal-Instanz
      ) else (echo.)
    ) >>%_FncReplaseString_TmpFile%
    move %_FncReplaseString_TmpFile% "%~3"  
  )
  endlocal &rem schließt die oben geoeffnete setlocal-Instanz
 exit /b 0
Hinweis: Einige Textdateien, wie z.B exportierte .reg-Dateien, liegen im UNICODE-Format vor. Da die Konsole nicht mit UNICODE zurechtkommt, konvertiert die Funktion Textdateien mithilfe von type automatisch in das für die Konsole verständliche ANSI-Format. Andernfalls würde der Text verstümmelt werden.

Für den Registryeditor (regedit.exe) stellt die Konvertierung kein Problem dar; er kann auch ANSI-formatierte .reg-Dateien importieren. Zu beachten bleibt aber, dass UNICODE über den Zeichensatz von ANSI hinaus weitere Zeichen darstellen kann. Auch wenn das eher selten vorkommt, könnten bei der Konvertierung von UNICODE zu ANSI Sonderzeichen verloren gehen. Testen lässt sich das, indem man die Quelldatei (hier test.reg ) zuvor von Hand in das ANSI-Format konvertiert und wieder zurückwandelt, um sie dann mit dem Original zu vergleichen:
c:\>type test.reg > test_ansi.reg
c:\>notepad test_ansi.reg
  Über „Datei / Speichern unter“ im Feld „Codierung: Unicode“ auswählen und die Datei speichern.
c:\>comp test.reg test_ansi.reg
  Die Dateien sollten identisch sein (andernfalls sind durch die Konvertierung Daten verloren gegangen).


Version 1.0.5: Es wurde eine Optimierung vorgenommen (setlocal DisableDelayedExpansion aus der for-Schleife entfernt und an den Anfang der Funktion gesetzt). -- NeonZero 09.10.2008

Version 1.0.4: UNICODE-formatierte Dateien werden nun gemäß dem obigen Hinweis zuvor nach ANSI konvertiert. -- NeonZero 05.09.2008

Version 1.0.3: Der erste Teil der Funktion (String im uebergebenen Text austauschen und der Variablen (%1) zuweisen) wurde nun ebenfalls entsprechend der "!"-Problematik angepaßt. Dabei findet auch der unten stehende Hinweis von bastla und Biber betreffs endlocal & set "Var=%Var%" Anwendung. -- NeonZero 26.08.2008

Version 1.0.2: Der Quelltext wurde nach einem Hinweis von bastla nochmals angepaßt (endlocal hinzugefügt), um den unten beschriebenen Fehler zu beseitigen. -- NeonZero 22.08.2008

Version 1.0.1: Ein etwas aufbereiteter Quellcode, der das eingangs beschriebene Problem von Version 1.0.0 behebt. -- NeonZero 20.08.2008

Content-Key: 94915

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

Printed on: April 24, 2024 at 04:04 o'clock

Member: bastla
bastla Aug 20, 2008, updated at Oct 18, 2012 at 16:36:07 (UTC)
Goto Top
Hallo NeonZero und willkommen im Forum!

Dass (temporäres) VBS für derlei (insbes für die Version mit 3 Parametern) wesentlich handlicher ist, willst Du ja sicherlich nicht von mir hören ... face-wink

... daher also doch ein Versuch in native Batch:
@echo off & setlocal
set from=all
set to=oh
set _x=Hallo!
echo\|set /P=%_x%_to_
setlocal enabledelayedexpansion
echo !_x:%from%=%to%!
setlocal disabledelayedexpansion
Zeile 5 ist übrigens ein Biber-Special von hier - passt, wie ich finde, aber auch irgendwie in diesen Thread face-wink ...

Grüße
bastla
Member: NeonZero
NeonZero Aug 20, 2008 at 20:58:06 (UTC)
Goto Top
Danke, bastla, für die schnelle Antwort.

Die Lösung: setlocal DisableDelayedExpansion muss gesetzt sein _bevor_ die Wertzuweisung per set _FncReplaseString_TextLine=%%b erfolgt. Andernfalls befindet sich in _FncReplaseString_TextLine bereits kein Ausrufezeichen mehr. Meine falsche Annahme war, dass die Eliminierung des Zeichens erst bei der Ausgabe erfolgt. Bei meiner Suche nach einer Lösung hatte ich so die ganze Zeit versucht, jedes Aufkommen eines „!“-Zeichens durch „^^!“ zu ersetzen, in einem String, der schon längst kein Ausrufezeichen mehr enthielt. face-confused

Im einleitenden Beitrag habe ich nun den kompletten etwas aufbereiteten Quelltext als Nachtrag angehängt.

Bye, nz
Member: NeonZero
NeonZero Aug 22, 2008 at 16:32:50 (UTC)
Goto Top
Zu früh gefreut: Die zweite Version der Funktion kann lediglich Textdateien bearbeiten, die maximal 16 Zeilen enthält. Dann kommt es zur folgenden Fehlerausschrift:

                           Maximale Rekursionstiefe für SETLOCAL erreicht.

Ich habe schon alles mir Mögliche probiert, aber es scheint nichts um einen ständigen Wechsel zwischen setlocal DisableDelayedExpansion und setlocal EnableDelayedExpansion vorbeizuführen.

Bleibe man komplett bei setlocal DisableDelayedExpansion, wäre
     set _FncReplaseString_TextLine=!_FncReplaseString_TextLine:%~1=%~2!
nicht mehr erlaubt. Stattdessen kann man
     call set _FncReplaseString_TextLine=%%_FncReplaseString_TextLine:%~1=%~2%%
verwenden. Nur dass eben dieser Konstrukt nicht mit den üblichen Sonderzeichen der DOS-Konsole zurechtkommt. „Hallo!“ bliebe zwar erhalten (inkl. „!“-Zeichen), aber eine Zeile die z.B. „x>y“ enthält, würde dann auf „x“ verkürzt. Damit käme man vom Regen in die Traufe.

Das mit dem Ausrufezeichen bei DelayedExpansion ist schon sehr ärgerlich. Auch Programmierer dürfen Fehler machen. Aber warum wird dieser gottverda^W Mis^W, äh, meine, warum wird das in all den Jahren nicht gefixt?

Womöglich hat ja einer von euch einen zündenden Gedanken für einen anderen Ansatz (?).

Bye, nz

PS: Bastla hat es oben richtig erkannt: Ich suche eine Lösung für Batch, nicht für vbs.
Member: bastla
bastla Aug 22, 2008 at 16:38:49 (UTC)
Goto Top
Hallo NeonZero!

Vielleicht lässt sich ja an passender Stelle ein "endlocal" einstreuen ...

Grüße
bastla
Member: NeonZero
NeonZero Aug 22, 2008 at 17:11:30 (UTC)
Goto Top
Hallo bastla. Schön von Dir 'zu hören'.

endlocal hat die Eigenschaft, dass alle lokal gesetzten Variablen wieder zurückgesetzt werden; eigene Variablen sind danach leer (tatsächlich existieren sie nicht mehr). Deshalb kam die Verwendung für mich nicht in Frage.

Davon war ich so überzeugt, dass ich es in diesem Fall nicht einmal ausprobiert hatte. Aber jetzt kommt’s: Diese Folgen gibt es hier nicht. Ich habe (noch) keine Ahnung, warum das so ist. Davon unabhängig: Du hast es! Big thanks! Den als Nachtrag angehängten Quelltext (siehe einleitenden Beitrag) habe ich entsprechend angepasst. Ich denke, das war’s.

Bye, nz
Member: bastla
bastla Aug 22, 2008 at 17:42:17 (UTC)
Goto Top
Hallo NeonZero!

endlocal hat die Eigenschaft, dass alle lokal gesetzten Variablen wieder zurückgesetzt werden; eigene Variablen sind danach leer (tatsächlich existieren sie nicht mehr).
Das ist ja der eigentliche Sinn der Verwendung von "setlocal" - allerdings gibt es einen netten Workaround (natürlich von Biber) für die "Mitnahme" zumindest einzelner Variablenwerte:
@echo off
set "Test=Original"  
setlocal
echo %Test%
set "Test=Kopie"  
echo %Test%
endlocal
echo %Test%
bringt erwartungsgemäß die Ausgabe:
Original
Kopie
Original
Anders sieht das Ergebnis mit dieser Variante aus:
@echo off
set "Test=Original"  
setlocal
echo %Test%
set "Test=Kopie"  
echo %Test%
endlocal & set "Test=%Test%"  
echo %Test%
nämlich:
Original
Kopie
Kopie
Vielleicht kannst Du das ja einmal brauchen ...

Grüße
bastla
Member: NeonZero
NeonZero Aug 22, 2008 at 19:36:08 (UTC)
Goto Top
Ein nützlicher Hinweis. Danke. Ich habe diese Variante gleich für den ersten Teil der Funktion adoptiert (betrifft String im uebergebenen Text austauschen und der Variablen zuweisen). Das musste ja auch noch entsprechend der „!“-Problematik angepasst werden.

Die Möglichkeiten von setlocal hatte ich bislang nur zum Teil erfasst. Mir war nicht klar, dass man mehrere setlocal-Instanzen verschachteln kann (eine neue Instanz legt sich um die darunter liegende ältere Instanz – ähnlich den Schichten einer Zwiebel). Genau das war es, was mir die Fehlermeldung "Maximale Rekursionstiefe für SETLOCAL erreicht" versucht hat zu sagen.

endlocal wirkt demzufolge auch nicht global, sondern beendet immer nur die äußere setlocal-Instanz. Daher hatte der Aufruf keinen Einfluss auf meine „darunter liegende“ Umgebung, deren Variablen in einer weiter innen liegenden setlocal-Instanz angelegt wurden. Diese Variablen existierten in den äußeren Schichten nur als Kopie, deren Änderung keinen Einfluss auf das Original hat. Beendet man die äußeren Schichten, kommt irgendwann wieder das Original mit dem ursprünglichen Inhalt zum Vorschein. Neu war für mich die Erkenntnis, dass es mehrere dieser Schichten geben kann.

Aber: setlocal EnableDelayedExpansion genauso wie setlocal DisableDelayedExpansion ändert nicht einfach so den Modus der aktuellen setlocal-Instanz. Es wird dabei immer eine neue Instanz angelegt, in der der veränderte Modus wirkt. Tatsächlich hat das keinen Einfluss auf die ursprüngliche Instanz. Der oft zu sehende einleitende Befehl „@echo off & setlocal & setlocal EnableDelayedExpansion“ ist demzufolge um den ersten setlocal-Befehl überflüssig, denn es werden sonst zwei setlocal-Instanzen geöffnet.

Fazit: Zusammengenommen ergibt sich der Grundsatz, dass auf jede setlocal-Instanz, die in einer Funktion angelegt wird (und sei es nur zum Wechsel des Modus), ein endlocal folgen muss.

Das war dann auch die Lösung für das Problem.

Bye, nz
Member: Biber
Biber Sep 02, 2008, updated at Oct 18, 2012 at 16:36:11 (UTC)
Goto Top
Moin NeonZero,

auch wenn Du es sicherlich nicht so geplant hattest - herausgekommen ist nun definitiv ein HowTo, eine Anleitung.

Und dementsprechend stufe ich Deinen Beitrag auch hoch zum Tutorial.

Vielen Dank für Deine Ausarbeitung und vor allem für das Andere-Teilhaben-Lassen daran.

Abschließende Fussnote zur Verwendung von Setlocal/Endlocal: in dem GetAllDateTimeInfos.bat im Batch-Workshop III habe ich auch das Feature benötigt, dass Variablen "global" erhalten/gesetzt bleiben oder umgekehrt "globale" Variablen wieder gelöscht werden (/SET bzw. /UNSET-Option).
Auch das geht mit einem "Endlocal" an der richtigen Stelle - im Batch die Zeilen 16-20.

Grüße
Biber