43 Stimmen

Warum schlägt die verzögerte Erweiterung fehl, wenn sie sich innerhalb eines gepufferten Codeblocks befindet?

Hier ist eine einfache Stapeldatei, die zeigt, wie die verzögerte Expansion fehlschlägt, wenn sie sich innerhalb eines Blocks befindet, der gepiped wird. (Der Fehler tritt gegen Ende des Skripts auf). Kann jemand erklären, warum das so ist?

Ich habe einen Workaround, aber er erfordert die Erstellung einer temporären Datei. Ich bin zum ersten Mal auf dieses Problem gestoßen, als ich an Find files and sort by size in a Windows batch file gearbeitet habe

@echo off
setlocal enableDelayedExpansion

set test1=x
set test2=y
set test3=z

echo(

echo NORMAL EXPANSION TEST
echo Unsorted works
(
  echo %test3%
  echo %test1%
  echo %test2%
)
echo(
echo Sorted works
(
  echo %test3%
  echo %test1%
  echo %test2%
) | sort

echo(
echo ---------
echo(

echo DELAYED EXPANSION TEST
echo Unsorted works
(
  echo !test3!
  echo !test1!
  echo !test2!
)
echo(
echo Sorted fails
(
  echo !test3!
  echo !test1!
  echo !test2!
) | sort
echo(
echo Sort workaround
(
  echo !test3!
  echo !test1!
  echo !test2!
)>temp.txt
sort temp.txt
del temp.txt

Hier sind die Ergebnisse

NORMAL EXPANSION TEST
Unsorted works
z
x
y

Sorted works
x
y
z

---------

DELAYED EXPANSION TEST
Unsorted works
z
x
y

Sorted fails
!test1!
!test2!
!test3!

Sort workaround
x
y
z

54voto

jeb Punkte 74279

Wie Aacini zeigt, scheint es, dass viele Dinge innerhalb eines Pipes fehlschlagen.

echo hello | set /p var=
echo here | call :function

Aber in Wirklichkeit ist es nur ein Problem zu verstehen, wie der Pipe funktioniert.

Jede Seite eines Pipes startet ihren eigenen cmd.exe in einem eigenen asynchronen Thread.
Das ist der Grund, warum so viele Dinge kaputt zu sein scheinen.

Aber mit diesem Wissen können Sie dies vermeiden und neue Effekte erzeugen

echo one | ( set /p varX= & set varX )
set var1=var2
set var2=Inhalt von zwei
echo one | ( echo %%%var1%%% )
echo three | echo MYCMDLINE %%cmdcmdline%%
echo four  | (cmd /v:on /c  echo 4: !var2!)

Aktualisierung 2019-08-15:
Wie bei Warum gibt `findstr` mit der Variablenerweiterung in seinem Suchstring unerwartete Ergebnisse zurück, wenn es in einem Pipe verwendet wird? festgestellt wurde, wird cmd.exe nur verwendet, wenn der Befehl intern in cmd.exe ist, wenn der Befehl eine Stapeldatei ist oder wenn der Befehl in einem Klammernblock eingeschlossen ist. Externe Befehle, die nicht in Klammern eingeschlossen sind, werden in einem neuen Prozess ohne die Hilfe von cmd.exe gestartet.

BEARBEITEN: Detaillierte Analyse

Wie dbenham zeigt, sind beide Seiten der Pipes für die Erweiterungsphasen äquivalent.
Die Hauptregel scheint zu sein:

Die normalen Stapel-Parserphasen sind abgeschlossen
.. Prozentuale Erweiterung
.. Sonderzeichenphase/Blockanfangserkennung
.. verzögerte Erweiterung (aber nur, wenn verzögerte Erweiterung aktiviert ist UND es sich nicht um einen Befehlsblock handelt)

Starten Sie cmd.exe mit C:\Windows\system32\cmd.exe /S /D /c""
Diese Erweiterungen folgen den Regeln des Cmd-Zeilen-Parser, nicht des Batch-Zeilen-Parser.

.. Prozentuale Erweiterung
.. verzögerte Erweiterung (aber nur, wenn verzögerte Erweiterung aktiviert ist)

Der wird geändert, wenn er sich in einem Klammerblock befindet.

(
echo one %%cmdcmdline%%
echo two
) | more

Aufgerufen als C:\Windows\system32\cmd.exe /S /D /c" ( echo one %cmdcmdline% & echo two )", werden alle Zeilenumbrüche in den & Operator geändert.

Warum ist die verzögerte Erweiterung von Klammern betroffen?
Ich vermute, dass sie sich nicht in der Stapel-Parserphase ausdehnen kann, da ein Block aus vielen Befehlen bestehen kann und die verzögerte Erweiterung wirksam wird, wenn eine Zeile ausgeführt wird.

(
set var=one
echo !var!
set var=two
) | more

Offensichtlich kann !var! nicht im Stapelkontext ausgewertet werden, da die Zeilen nur im Cmd-Zeilenkontext ausgeführt werden.

Aber warum kann es in diesem Fall im Stapelkontext ausgewertet werden?

echo !var! | more

Nach meiner Meinung ist dies ein "Fehler" oder eine inkonsistente Verhaltensweise, aber es ist nicht der erste

BEARBEITEN: Hinzufügen des LF-Tricks

Wie dbenham zeigt, gibt es scheinbar einige Einschränkungen durch das Cmd-Verhalten, das alle Zeilenumbrüche in & ändert.

(
  echo 7: Teil1
  rem Dies tötet den gesamten Block, da das schließende ) auskommentiert ist!
  echo Teil2
) | more

Dies führt zu
C:\Windows\system32\cmd.exe /S /D /c" ( echo 7: Teil1 & rem Dies ...& echo Teil2 ) "
Das rem wird den kompletten Zeilenrest auskommentieren, daher fehlt sogar die abschließende Klammer dann.

Aber Sie können dies mit dem Einbetten Ihrer ownen Zeilenumbrüche lösen!

set LF=^

REM Die zwei leeren Zeilen oben werden benötigt
(
  echo 8: Teil1
  rem Dies funktioniert, da es die Befehle aufteilt &LF% echo Teil2  
) | more

Dies führt zu C:\Windows\system32\cmd.exe /S /D /c" ( echo 8: Teil1 %cmdcmdline% & rem Dies funktioniert, da es die Befehle aufteilt %LF% echo Teil2 )"

Und da das %LF% beim Parsen der Klammern erweitert wird, sieht der resultierende Code wie folgt aus

( echo 8: Teil1 & rem Dies funktioniert, da es die Befehle aufteilt
  echo Teil2  )

Dieses %LF%-Verhalten funktioniert immer innerhalb von Klammern, auch in einer Stapeldatei.
Aber nicht auf "normalen" Zeilen, dort wird ein einzelner das Parsen für diese Zeile stoppen.

BEARBEITEN: Asynchron ist nicht die volle Wahrheit

Ich sagte, dass beide Threads asynchron sind, normalerweise ist dies wahr.
Aber in Wirklichkeit kann sich der linke Thread blockieren, wenn die gepipeten Daten nicht vom rechten Thread konsumiert werden.
Es scheint eine Grenze von ~1000 Zeichen im "Pipe"-Puffer zu geben, dann wird der Thread blockiert, bis die Daten konsumiert sind.

@echo off
(
    (
    for /L %%a in ( 1,1,60 ) DO (
            echo Ein langer Text kann diesen Thread blockieren
            echo Thread1 ##### %%a > con
        )
    )
    echo Thread1 ##### Ende > con
) | (
    for /L %%n in ( 1,1,6 ) DO @(
        ping -n 2 localhost > nul
        echo Thread2 ..... %%n
        set /p x=
    )
)

9voto

Aacini Punkte 61728

Lustige Sache! Ich kenne die Antwort nicht, aber ich weiß, dass die Pipeline-Operationen in Windows Batch konsistente Fehler haben, die im originalen MS-DOS Batch (falls solche Funktionen im alten MS-DOS Batch ausgeführt werden konnten) nicht vorhanden sein sollten. Deshalb vermute ich, dass der Fehler eingeführt wurde, als die neuen Windows Batch-Funktionen entwickelt wurden.

Hier sind einige Beispiele:

echo Zuweisungswert | set /p var=

In der vorherigen Zeile wird der Wert nicht der Variablen zugewiesen, also müssen wir es auf diese Weise beheben:

echo Zuweisungswert > temp.txt & set /p var=< temp.txt

Ein weiteres Beispiel:

(
echo Wert eins
echo Wert zwei
echo Wert drei
) | call :BatchSubroutine

Funktioniert nicht. Behebe es so:

(
echo Wert eins
echo Wert zwei
echo Wert drei
) > temp.txt
call :BatchSubroutine < temp.txt

Allerdings funktioniert diese Methode in bestimmten Fällen; mit DEBUG.COM zum Beispiel:

echo set tab=9> def_tab.bat
(
echo e108
echo 9
echo w
echo q
) | debug def_tab.bat
call def_tab
echo EINS%tab%ZWEI

Das vorherige Programm zeigt:

EINS     ZWEI

In welchen Fällen funktioniert es und in welchen nicht? Nur Gott (und Microsoft) mögen es wissen, aber es scheint mit neuen Windows Batch-Funktionen zusammenzuhängen: SET /P Befehl, verzögerte Expansion, Codeblock in Klammern, usw.

EDIT: Asynchrone Batch-Dateien

HINWEIS: Ich habe diesen Abschnitt geändert, um einen Fehler von mir zu korrigieren. Siehe meinen letzten Kommentar an jeb für Details.

Wie jeb sagte, führt die Ausführung beider Seiten einer Pipeline zu zwei asynchronen Prozessen, die es ermöglichen, asynchrone Threads auszuführen, selbst wenn der START Befehl nicht verwendet wird.

Mainfile.bat:

@echo off
echo Main start. Geben Sie Zeilen ein, geben Sie "end" ein, um zu beenden
Erste | Zweite
echo Main end

First.bat:

@echo off
echo First start

:Schleife
    set /P first=
    echo Erste Eingabe: %first%
if /I not "%first%" == "end" goto loop
echo EOF

echo First end

Second.bat:

@echo off
echo Second start

:Schleife
    set /P second=Eingabe Zeile: 
    echo Zweite Eingabe: %second%
    echo/
if not "%second%" == "EOF" goto loop

echo Second end

Wir können diese Funktion verwenden, um ein Programm zu entwickeln, das äquivalent zu der Expect-Anwendung (die auf ähnliche Weise wie das pexpect Python-Modul arbeitet) ist und jedes interaktive Programm auf diese Weise steuern kann:

Eingabe | irgendwelchesprogramm | Ausgabe

Die Output.bat-Datei wird den "Expect"-Teil durch Analysieren der Ausgabe des Programms erreichen, und Input.bat wird den "Sendline"-Teil durch Bereitstellung der Eingabe für das Programm erreichen. Die Rückwärtskommunikation von Ausgabe zu Eingabe-Modulen wird über eine Datei mit den gewünschten Informationen und ein einfaches Semaphore-System gesteuert, das über die Anwesenheit/Abwesenheit von ein oder zwei Flag-Dateien funktioniert.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X