pedant
Goto Top

Batch - ein paar Basics die man kennen sollte

Hallo Batchfreunde,

in den letzten Tagen habe ich ein paar Dinge über Batch gelernt, die eigentlich zu den Basics gehören, mir aber bisher unbekannt waren, obwohl ich relativ viel mit Batch mache und das schon seit MS-Dos 5.0.
Für den Fall, dass diese Dinge dem einen oder anderen unter Euch auch noch unbekannt sein sollten, habe ich es hier zusammengefasst.

Alles Folgende ist von mir nur reproduktiv, aus diversen Quellen zusammengestellt und mit eigenen Worten kommentiert.
Anmerkungen und Korrekturen aus den Kommentaren, habe ich in diesen Beitrag einpflegt, damit er möglichst korrekt und vollständig ist.


Inhalt
1. echo %random%
2. xcopy leer 2> datei.txt 1>&2 ("xcopy leer" ist hierbei nur ein Befehl, der zu einem Fehler führt)
3. echo ^1> datei.txt
3. echo.Text und echo(Text
4. exit /b %wert%
Wer mit diesen Befehlen vertraut ist, kann sich das Weiterlesen sparen.


1. Zufalls-Zahl

Es steht eine Umgebungsvariable zur Verfügung, die einen Zufallswert liefert.
Diese heißt
%random%
Wertebereich 0-32767
set /a schulnote=(%RANDOM%*6/32768)+1
echo %schulnote%
*6 sorgt in dieser Formel dafür, dass der Wertebereich für %schulnote% 1-6 ist.
Quelle: https://ss64.com/nt/syntax-random.html

Wichtig:
Mit set /a können nur Integeroperationen ausgeführt werden.
Es wird daher an jeder Stelle einer Formel ausschließlich mit Ganzzahlen gerechnet. Eventuelle Nachkommastellen werden sofort verworfen, was gleichbedeutent wäre mit genereller Abrundung auf ganze Zahlen und nicht nur im Endergebnis, sondern schon bei jedem Teilschritt.

Betrachten wir die funktionierende Formel mit dem Maximalwert von Random (32767) in Teilschritten:
Formel: (32767*6/32768)+1
1.) 32767 * 6 = 196602
2.) 196602 / 32768 = 5,9998169 => 5
3.) 5 + 1 = 6

Sähe die Formel wie folgt aus, was zunächst gleichwertig erscheint, würde sie nicht wie gewünscht funktionieren.
Formel: (32767/32768)*6+1
1.) 32767/32768 = 0,9999695 => 0
2.) 0 * 6 = 0
3.) 0 + 1 = 1
Diese Formel würde immer 1 als Ergebnis haben, egal welchen Wert wir von Random erhielten.


2. Ausgaben umleiten

Wenn ich eine Ausgabe in eine Datei umleitet, dann lasse ich das Leerzeichen vor dem Umleitungszeichen normalerweise weg.
echo Text > datei.txt (schreibt "Text " (mit dem Leerzeichen) in die Datei)
echo Text> datei.txt (schreibt "Text" (ohne das Leerzeichen) in die Datei)
Beim Rumprobieren mit %random% stieß ich auf eine Merkwürdigkeit, deren Untersuchung mich zu der Erkenntnis brachte, dass Ausgabe-Umleitungen komplexer sein können, als mir bisher bekannt war.
Versucht man eine Ausgabe, die mit einer freistehenden Ziffer endet, in eine Datei umzuleiten, klappt das nicht so wie gewohnt.
Der Grund ist, dass die Funktion des Umleitungszeichens durch eine führende Ziffer geändert wird.
Zusätzlich wurde ich gewahr, dass Ausgaben unterschiedlicher Art sein können und zwar "Meldungen" und "Fehlermeldungen".
Standardmäßig werden beide Arten in der Console ausgegeben, wobei sich beide Arten getrennt umleiten lassen.
" 1> " - leitet nur stdout (Standard Out) um, also nur Meldungen
" 2> " - leitet nur stderr (Standard Error) um, also nur Fehlermeldungen
" > " - ist ein Synonym für " 1> "
0 steht für stdin (Standard In) und 3-9 sind undefiniert.

Was bedeutet das und wie kann man das nutzen?

Ein paar Beispiele für Eingaben und deren Ausgaben:

xcopy leer
Datei leer nicht gefunden (das ist eine Fehlermeldung)
0 Datei(en) kopiert (das ist nur eine Meldung/Information)

xcopy leer > datei.txt
ist gleichbedeutend mit
xcopy leer 1> datei.txt (es werden nur die Meldungen umgeleitet)
Datei leer nicht gefunden (landet in der Console)
0 Datei(en) kopiert (landet in der Datei)

xcopy leer 2> datei.txt (es werden nur die Fehlermeldungen umgeleitet)
Datei leer nicht gefunden (landet in der Datei)
0 Datei(en) kopiert (landet in der Console)

Um beide Arten umzuleiten kann man sie in getrennte Dateien umleiten
xcopy leer 1> meldung.txt 2> fehler.txt (es werden alle Ausgaben umgeleitet)
Datei leer nicht gefunden (landet in der Datei "fehler.txt")
0 Datei(en) kopiert (landet in der Datei "meldung.txt")

Um beide Arten in dieselbe Datei umzuleiten muss man stderr zu stdout oder stdout zu stderr umleiten und die "Summe" in eine Datei
xcopy leer 1> datei.txt 2>&1 (es wird 2 nach 1 umgeleitet und 1+2 in die Datei)
xcopy leer 2> datei.txt 1>&2 (es wird 1 nach 2 umgeleitet und 1+2 in die Datei)
Datei leer nicht gefunden (die Fehlermeldung landet in der Datei)
0 Datei(en) kopiert (die Meldung landet in der Datei)

Statt in eine Datei umzuleiten kann man auch ins Nirwana umleiten (das Null-Device) = nul
Man kann auch explizit zur Console = con umleiten, was zunächst sinnlos erscheint, da die Console ja normalerweise ohnehin das Ausgabeziel ist.
Nehmen wir mal eine Batch als Beispiel:
@echo off
echo Hallo
xcopy leer
REM Fehler : Datei leer nicht gefunden
REM Meldung: 0 Datei(en) kopiert
echo Welt> con
Wir rufen die Batch auf und leiten deren Ausgaben nach nul um
batch.cmd > nul
Hallo (landet wie erwartet im Nichts, nul)
Datei leer nicht gefunden (landet als Fehlermeldung in der Console, da nur "1" umgeleitet ist und nicht "2")
0 Datei(en) kopiert (landet als Meldung im Nichts, nul)
Welt (wird in der Console ausgegeben)
Auch wenn wir die Batch so aufrufen ist die explizite Umleitung zu con davon unbetroffen
batch.cmd > nul 2>&1
Welt (wird in der Console ausgegeben, der Rest landet bei nul)

Eine Anmerkung:
Alle Beispiele hab ich mit einfachem Umleitungszeichen aufgezeigt. Sie funktionieren aber auch mit doppeltem Umleitungszeichen.
" 1> datei.txt" leitet in "datei.txt" um und erzeugt die Datei dabei neu.
" 1>> datei.txt" leitet in "datei.txt" um und hängt die Ausgabe an den vorhandenen Dateiinhalt an.


Ganz allgemein lassen sich Umleitungen unterschiedlich positionieren.
>"test.txt" echo Hallo - Umleitung am Anfang
echo> "test.txt" Hallo - Umleitung mittendrin
echo Hallo> "test.txt" - Umleitung am Ende
Alle drei dargestellten Varianten erzeugen dieselbe Ausgabe.

@rubberman empfiehlt grundsätzlich die Umleitung an den Anfang zu setzen.
Das helfe nicht nur das Deuten von Ziffern als Streamnummer zu vermeiden, sondern trage auch wesentlich zur Lesbarkeit des Codes bei.
Sein Beispiel:
echo Hallo!>"test.txt" 
echo Das ist eine Testzeile>>"test.txt" 
echo und noch eine.>>"test.txt"
im Vergleich zu
 >"test.txt" echo Hallo!
>>"test.txt" echo Das ist eine Testzeile
>>"test.txt" echo und noch eine.
In diesem Beispiel ist der zweite Code besser lesbar.

@rubberman merkte noch an, dass es auch noch < gäbe (Umleitung zum stdin).

Umleitung aus einer Datei:
@echo off
echo Hallo Welt> datei.txt
set /p meineVar= < datei.txt
del datei.txt
echo %meineVar%
Hallo Welt wird in datei.txt geleitet.
set /p meineVar= belegt die Variable meineVar mit was auch immer man anschließend in der Eingabeaufforderung eintippt und mit [Enter] abschließt.
< datei.txt leitet den Inhalt von datei.txt in die Standardeingabe (stdin) um.
set /p meineVar= < datei.txt bewirkt daher, dass Hallo Welt aus der datei.txt in die Variable meineVar geschrieben wird.
echo %meineVar% gibt Hallo Welt aus.


3. Ausgaben von Ziffern umleiten

echo Text #> datei.txt (# steht hier für eine einzelne Ziffer und "Text" ist optional und nicht relevant)
Text
Nur bei #=1 landet "Text " in der Datei. Die Ziffer wird nirgends ausgegeben.
Bei allen anderen Ziffern landet "Text " in der Console und die Ziffer nirgends.
Um die besondere Funktion von #> zu umgehen kann man ein Leerzeichen einfügen # >, doch dieses Leerzeichen landet dann auch im Umleitungsziel.
Möchte man das Leerzeichen vermeiden, so gibt es diese Alternativen:
a) (echo 1)> datei.txt (den Befehl in Klammer setzen)
b) echo ^1> datei.txt (die Ziffer mit Control-Zeichen maskieren)
c) echo.1> datei.txt (die Ziffer steht nicht alleine da, sondern mit einem Punkt davor, sie wird daher anders behandelt. Siehe Kommentar)
d) echo> datei.txt 1 (die Umleitung schon vor die Ausgabe setzen)
e) >datei.txt echo 1 (die Umleitung schon ganz am Anfang setzen)
fa) (echo |set /p=1)> datei.txt & echo.>> datei.txt (Wusste nicht so genau warum das funktioniert, aber das hat @Friemler in seinem Kommentar gut erläutert)
fb) (echo |set /p=1)> datei.txt (schreibt eine "1" in die Datei ohne anschließenden Zeilenumbruch)
Am eingängigsten finde ich b), da es bei anderen Dingen genauso funktioniert und mir entsprechend vertraut ist.
echo ^<br^>> datei.txt (<br> landet in der Datei)

Kommentar zu c)
echo. gibt eine Leerzeile aus.
echo.Text Folgt dem Leerzeilenausgabebefehl doch noch Text so wird er ausgegeben, statt der Leerzeile.

Von der Verwendung von echo. ist allerdings abzuraten, da dabei echo nicht sofort als interner Befehl erkannt und ausgeführt wird, sondern zuvor nach einer Datei namens echo gesucht wird und das zuerst im aktuellen Verzeichnis und danach im gesamten Pfad. Erst dann wird der interne Befehl echo ausgeführt.
Aus Performancegründen ist echo. daher sehr nachteilig.
Es hat aber auch noch Sicherheitsaspekte.
echo.cmd sollte cmd ausgeben und tut es auch, es sei denn, dass sich irgendwo im Pfad tatsächlich eine Datei namens echo.cmd befindet.
In dem Fall würde keine Ausgabe erfolgen, sondern diese echo.cmd gestartet werden.
Im Falle von echo.exe wird eine Datei namens echo.exe ausgeführt, wenn sie im aktuellen Verzeichnis vorliegt. Liegt sie dort nicht, sondern irgendwo im Pfad wird sie (inkonsequenterweise) nicht gestartet, sondern exe ausgegeben.

Wie auch immer, der Punkt hinter echo ist zu vermeiden.
Es gibt dazu mehrere Alternativen und zu deren Risiken und Nebenwirkungen kann hier nachgelsen werden:
http://www.dostips.com/forum/viewtopic.php?t=774 und http://www.dostips.com/forum/viewtopic.php?t=1900
Die zu bevorzugenden Alternative ist die öffnende Klammer, also echo( - ohne Leerzeichen zwischen echo und (.
Diese hat die wenigsten Nebenwirkungen. Der Kommandozeileninterpreter wertet sie nicht als öffnende Klammer in Klammerausdrücken eines Codes.

echo Test versus echo(Text
Interessant ist das bei der Ausgabe von Zeilen, die nur aus Leerzeichen und/oder Tabs bestehen.
Ebenso interessant ist das bei der Ausgabe von Variablen die eventuell leer (nicht definiert) sind.
if "a"=="b" set ergebnis=merkwürdig
echo %ergebnis%
echo(%ergebnis%
set ergebnis=egal
echo %ergebnis%
echo(%ergebnis%
Hier die resultierenden Ausgaben:
ECHO ist ausgeschaltet (OFF). (da %ergebnis% leer/undefiniert ist, wurde echo ohne Parameter aufgerufen und gibt daher seinen Zustand aus)
Leerzeile (da %ergebnis% leer/undefiniert ist, wurde echo( aufgerufen und gibt daher eine Leerzeile aus)
egal (echo egal liefert "egal")
egal (echo(egal liefert auch "egal")

Nebenbei erwähnt:
echo on (schaltet das Echo an)
echo(on (gibt "on" aus)


4. Zum Schluss das Ende

Eine Batchdatei endet, wenn sie ihre letzte Zeile abgearbeitet hat oder vorzeitig beendet oder verlassen wird.

Eine schlechte Möglichkeit zum vorzeitigen Beenden ist der Befehl exit
Schlecht insofern, dass er nicht nur die Batch beendet, sondern auch die Eingabeaufforderung schließt, aus der man heraus die Batch gestartet hat.
Ruft man eine Batchdatei per call aus einer anderen Batchdatei heraus auf, würde das exit in der aufgerufenen Batch auch die aufrufende Batch beenden, sodass das call nutzlos wäre, weil die gesamte Ausführung beim ersten exit endet.
Schreibt man statt exit allerdings exit /b so wird nur die Batch beendet, in der das exit /b ausgeführt wird.

a.cmd
@echo off
call b.cmd
echo Welt!
exit /b

b.cmd
@echo off
echo Hallo
exit /b

Will man Hallo Welt! als Ausgabe haben, so darf b.cmd kein exit (ohne /b) enthalten.
Anmerkung: In den Beispielen a.cmd und b.cmd ist das exit /b überflüssig, da es jeweils in der letzten Zeile steht und die Batches dort ohnhin enden.

Eine weitere, gute Möglichkeit zum vorzeitigen Beenden ist der Befehl goto :eof (eof=End Of File). Wichtig ist hier der Doppelpunkt vor eof, denn ohne würde eine Sprungmarke namens eof gesucht werden und nicht das Dateiende.

Ein Beispiel für ein vorzeitiges Beenden ist das hier:
@echo off
if %random% gtr 20000 exit /b
echo %random% ist ^<= 20000
if %random% lss 10000 goto :eof
echo %random% ist ^>= 10000
Anmerkungen:
%random% liefert bei jedem Aufruf ein neues Zufallsergebnis.
Die Umleitungszeichen > und < ("nach" und "von") sind hier mit ^ maskiert, damit sie per echo ausgegeben und nicht als Umleitung verstanden werden.

exit /b und goto :eof ist gleichwertig und in Batches gut verwendbar, da beides nur die jeweils aktuelle Batch beendet und nicht die gesamte Kette und auch nicht die Eingabeaufforderung, falls der Aufruf denn in einer solchen erfolgte.

Ergänzend und optional kann man bei exit /b noch einen ExitCode mit angeben, der anschließend als %errorlevel% zur Verfügung steht.
exit /b 2 setzt den Errorlevel beispielsweise auf 2, wobei der angegebene Code auch eine Variable sein kann: exit /b %meinWert%

Gruß /b 0
Frank

PS: Danke für die konstruktiven Kommentare

Content-ID: 336508

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

Ausgedruckt am: 24.11.2024 um 18:11 Uhr

rubberman
rubberman 01.05.2017 um 14:02:11 Uhr
Goto Top
Hallo Frank

1. Zufalls-Zahl
Es ist richtig und gut die Berechnung mit 32768 statt mit Modulo durchzuführen, um das Ungleichgewicht von kleineren zu größeren Zahlen zu eliminieren. Zur Erklärung fehlt aber, dass Batch mittels SET /A nur Integeroperationen durchführen kann, was dazu führt, dass Nachkommastellen "abgeschnitten" werden (es findet auch keine Rundung statt). Das ist der Grund für das +1 in der Berechnung.

2. Ausgaben umleiten
Es gäbe da noch < (Umleitung zum stdin). Das >> hast du im nächsten Abschnitt kurz angerissen, würde aber eher in Punkt 2 passen.

3. Ausgaben von Ziffern umleiten
Ich empfehle grundsätzlich die Umleitung an den Anfang zu setzen. Das hilft nicht nur um das Deuten von Ziffern als Streamnummer zu vermeiden, sondern trägt auch wesentlich zur Lesbarkeit den Codes bei. Bsp.
echo Hallo!>"test.txt"  
echo Das ist eine Testzeile.>>"test.txt"  
echo Und noch eine.>>"test.txt"  
vs.
 >"test.txt" echo Hallo!  
>>"test.txt" echo Das ist eine Testzeile.  
>>"test.txt" echo Und noch eine.  

Kommentar zu c)
ECHO. ist der Worst Case. Es ist unsicher und langsam, da bei jedem Aufruf nach einer Datei mit Name echo gesucht wird. Nutze statt dessen ECHO( ohne Leerzeichen zwischen ECHO und der öffnenden Klammer. Diese hat die wenigsten Nebenwirkungen. Keine Angst, der Kommandozeileninterpreter zählt sie nicht als öffnende Klammer in Klammerausdrücken des Codes. Referenzen
http://www.dostips.com/forum/viewtopic.php?t=774
http://www.dostips.com/forum/viewtopic.php?t=1900
Es sei noch erwähnt, dass dies nicht nur für die Erzeugung von Leerzeilen interessant ist. Sollte eine Zeile nur aus Leerzeilen oder Tabs bestehen, ist das ebenso erforderlich.

4. Zum Schluss das Ende
Grundsätzlich wird weder EXIT noch EXIT /B benötigt. Die Abarbeitung des Scripts endet nach der letzten Zeile des Scripts. Sollte sich die Scriptausführung nicht selbständig beenden, so liegt das am falschen Aufruf des Scripts.
EXIT /B (oder GOTO :EOF) ist nur für das Beenden von Codeteilen wichtig, bei dem nachfolgender Code nicht weiter verarbeiten werden soll. Bspw. für den Hauptcode, wenn noch Subroutinen folgen oder für das Beenden von mittels CALL aufgerufenen Subroutinen. Die Möglichkeit der Rückgabe eines Wertes bleibt natürlich ein valider Grund.

Grüße
rubberman
Friemler
Friemler 02.05.2017 aktualisiert um 15:01:39 Uhr
Goto Top
Hallo Frank,

Zitat von @Pedant:

fa) (echo |set /p=1)> datei.txt & echo.>> datei.txt (Weiß nicht so genau warum das funktioniert)
fb) (echo |set /p=1)> datei.txt (schreibt eine "1" in die Datei ohne anschließenden Zeilenumbruch)

Der SET-Befehl mit der Option /p dient eigentlich dazu, Benutzereingaben zu erfassen und einer Umgebungsvariablen zuzuweisen:
set /p "Name=Geben sie den Benutzernamen ein: "

Der Text "Geben sie den Benutzernamen ein:" (ohne Anführungszeichen) wird als Prompt/Eingabeaufforderung ausgegeben und dann auf eine Benutzereingabe gewartet, die mit ENTER abgeschlossen werden muss. Die Eingabe steht danach in der Umgebungsvariablen Name zur Verfügung (wenn man %Name% schreibt).

Außerdem kann die Standardeingabe von SET /p umgeleitet werden (auf ein anderes Device, eine Datei oder eine Pipe).

Ob gewollt oder wegen eines schlecht programmierten Parsers kann SET /p aber auch zur reinen Ausgabe von Text ohne abschließenden Zeilenumbruch missbraucht werden. Der Befehl
<NUL set /p "=Geben Sie den Benutzernamen ein: "
gibt nur den Text "Geben Sie den Benutzernamen ein:" aus (ohne Anführungszeichen und ohne abschließenden Zeilenumbruch). Da die Standardeingabe auf das NUL Device umgeleitet ist, ließt SET /p seine Eingabe von dort. Das NUL Device liefert natürlich immer nur END OF STREAM zurück, wodurch die Eingabe abgeschlossen wird. Da vor dem Gleichheitszeichen kein Variablennamen angegeben ist, wird die (sowieso leere) Eingabe nirgends gespeichert.

Somit lässt sich Dein Beispiel fa) folgendermaßen erklären:

  • Der ECHO-Befehl in
    (echo |set /p=1)
    gibt den ECHO-Status und einen Zeilenumbruch auf der Standardausgabe aus, die über eine Pipe in die Standardeingabe des SET /p Befehls geschrieben werden.
  • Dieser Befehl gibt zunächst die 1 rechts vom Gleichheitszeichen auf seiner Standardausgabe aus und ließt dann von seiner Standardeingabe (der Pipe) die Ausgabe des ECHO Befehls. Da diese mit einem Zeilenumbruch endet, gilt sie als abgeschlossen und der SET Befehl wird beendet. Die über die Pipe gelesene Ausgabe des ECHO-Status wird in Ermangelung einer Variablen links vom Gleichheitszeichen verschluckt.
  • Die Ausgabe des SET Befehls (die 1) wird per Ausgabeumleitung in die Datei datei.txt geschrieben.
  • Anschließend wird durch den Befehlsverkettungsoperator & der Befehl
    echo.>> datei.txt
    ausgeführt, der einen Zeilenumbruch an die Datei datei.txt anhängt.

Bei Deinem Beispiel fb) fehlt der letzte Schritt.

Grüße
Friemler
Pedant
Pedant 02.05.2017 um 22:11:46 Uhr
Goto Top
Hallo rubberman, Hallo Friemler,

herzlichen Dank für Euren zusätzlichen Input.

Ich nehme das mal als Hausaufgabe und werde das alles in den Ausgangsbeitrag einpflegen oder zumindest entsprechende Verweise hinzufügen, damit dort gleich alles richtig ist.

Dank Eurer ausführlichen Erklärungen hab ich alles soweit verstanden, bis auf folgende Anmerkung:
Zitat von @Friemler
Bei Deinem Beispiel fb) fehlt der letzte Schritt.
Das Fehlen des letzten Schrittes ist doch genau der Unterschied zwischen fa) und fb) und als Alternative (mit unterschiedlicher Auswirkung) anzusehen oder hab ich Deinen Kommentar falsch verstanden?

Gruß Frank
Friemler
Friemler 02.05.2017 um 22:43:06 Uhr
Goto Top
Hallo Frank,

Zitat von @Pedant:

oder hab ich Deinen Kommentar falsch verstanden?

nein, nein. Ich hatte fb) im Zitat stehen, da wollte ich halt auch noch etwas dazu sagen.

N8
Friemler
Pedant
Pedant 05.05.2017 um 21:29:50 Uhr
Goto Top
Hallo,

ich habe Eure Kommentare jetzt in meinen Ausgangsbeitrag einfließen lassen.
Für Beschwerden wäre ich offen, hoffe aber, dass jetzt alles soweit in Ordung ist.

Gruß und Dank
Frank