findstr: Die Zeile darüber ausgeben

Manchmal hat man Probleme ähnlich wie dieses hier: Man möchte nicht die gesuchte Zeile in einer Datei ausgeben, sondern die Zeile darüber. Und das Ganze soll mit einem Batch-Skript ausgeführt werden. Für dieses Problem habe ich mir einer Testdatei erstellt, die in etwas so aussieht:

123
abc
234
qwe
345
qwe
456
abc

Alle Zeilen, die über dem gesuchten String abc stehen, sollen ausgegeben werden, also

123
456

Das habe ich anschließend in einem Batch-Skript realisiert. Ich hoffe, die Kommentare hierfür sind ausreichend:

@echo off
SetLocal EnableDelayedExpansion

rem set initial values for the variables
set index=0
set "previous_line="
set "current_line="

rem Iterate over file, line by line
For /F "tokens=*" %%L in (array_data2.txt) do (
set "previous_line=!current_line!"
set "current_line=%%L"

rem check if current line has searched
string echo %%L| findstr /C:"abc" > nul

rem if it has searched string, print previous
rem and current line
if not errorlevel 1 (
echo !index!: !previous_line!
set /a "curr_index=index+1"
echo !curr_index!: !current_line!
echo.
)

rem add 1 to the index
set /a "index=index+1"
)

Quellen:

UCS-2 Kodierung und findstr

Dateien können verschiedene Kodierungen haben. Das Kommandozeilen-Werkzeug findstr kann in so einem Fall einen gesuchten String in einer Datei nicht finden. Ich hatte eine Datei, die hatte eine UCS-2 Kodierung und beim Aufruf von

findstr "abc" utc2.txt

kam nichts dabei heraus, obwohl die Textdatei diesen String enthielt. Um den gesuchten String trotzdem zu finden, kann man in so einem Fall auf das SysInternal Tools strings.exe zurückgreifen:

strings -nobanner utc2.txt | findstr "abc"


This computer doesn’t have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory

Ich wollte meine ersten Schritte mit minikube auf einer Linux Maschine machen. Diese Maschine war eine virtuelle Maschine, die unter Azure lief. Beim Aufruf von

minikube start

bekam ich jedoch die Meldung:

This computer doesn't have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory

Nach einer Weile kam ich auf eine Lösung. Meine virtuelle Maschine hatte die Größe D2_v2. Laut Dokumentation muss es jedoch eine Maschine der Größe v3 sein:

Zur schnellen Information: Alle virtuellen v3-Computer unterstützen geschachtelte Virtualisierung. Eine vollständige Liste der VM-Größen, die Schachtelung unterstützen, finden Sie im Artikel zur Azure-Compute-Einheit.

https://docs.microsoft.com/de-de/azure/virtual-machines/windows/nested-virtualization

Nachdem ich also meine Maschine auf die Größe D2_v3 geändert hatte, ist das minikube-Kommando problemlos durch gelaufen.

CreateProcess failed: The system cannot find the file specified.

Beim Bau der Bibliothek skia unter Windows bekam ich diese Fehlermeldung:

[419/2871] copy ../../third_party/externals/icu/common/icudtl.dat icudtl.dat
FAILED: icudtl.dat
python .../skia/build/skia/gn/cp.py ../../third_party/externals/icu/common/icudtl.dat icudtl.dat
CreateProcess failed: The system cannot find the file specified.
ninja: fatal: ReadFile: The handle is invalid.

Wie es sich herausgestellt hat, lag das Problem darin, dass ninja versucht hat, Python aufzurufen, Python aber nicht gefunden hat. Genauer gesagt, python.exe.

Um die python.exe aus den depot_tools aufrufen zu können, muss man vorher erstmal das Skript update_depot_tools.bat aufrufen (wenn man das vorher nicht schon getan hat). Danach sollte sich eine python.exe in dem Ordner <some\path>\depot_tools\win_tools-2_7_6_bin\python\bin befinden. Der Pfad kann sich in der Zukunft vermutlich ändern. Jedenfalls war das bei mir der Fall. Diesen Pfad muss man noch in der Umgebungsvariable PATH hinzufügen, z.b. in einem Batch-Skript mit

set "PATH=%PATH%;<some\path>\depot_tools\win_tools-2_7_6_bin\python\bin"

Danach sollte der Fehler verschwunden sein.

Anmerkungen zu if errorlevel 0

Ab und an sieht man im Internet Batch-Code, bei denen es um die Abfrage geht, ob der Errorlevel 0 oder ungleich 0 ist. Dabei sieht man Code-Zeilen, die in etwa so aussehen:

if errorlevel 0 (
...
) else (
...
)

Diese Abfrage ist aber in vielen Fällen falsch. Im Grunde bedeutet sie: Ist der Errorlevel 0 oder größer. D.h. diese Abfrage kommt fast gar nicht in den else-Teil, weil der Errorlevel meist 0 oder größer ist. Außer man hat ein Programm, welches negative Errorlevel zurückgibt.

Wenn man nun aber wissen möchte, ob der Errorlevel 1 oder größer ist, dann muss die Abfrage anders gestaltet werden:

if errorlevel 1 (
...
) else (
...
)

mkdirs() gibt false zurück

mkdirs() ist in Java eine Funktion, die einen Ordner samt aller Väterordner erstellt, wenn diese noch nicht existieren. Man kann dies mit dem Linuxkommando

mkdir -p /path/to/some/dir

vergleichen. Wie es sich herausgestellt hat, gibt diese Funktion ein false zurück, selbst wenn sie einen Ordner erfolgreich erstellt hat. Hier ein Java-Beispielcode:

import java.io.*;
public class MyFirstClass {
public static void main(String[] args) {

String folderName = "/home/jenkins/a/b";
try {

File folder = new File(folderName);
if(!folder.mkdirs()) {
System.out.println("Folders created!");
} else {
System.out.println("Failed to create folders!");
}

} catch (Exception e) {

System.err.println("Things went wrong: " + e.getMessage());
e.printStackTrace();

}
}
}

In diesem Programm wird ein Ordner namens /home/jenkins/a/b erstellt. Zu diesem Code habe ich noch ein Shell-Skript geschrieben, dass mir bei jedem Aufruf den Ordner /home/jenkins/a löscht, den Code kompiliert und das Programm startet (build.sh):

 !/bin/sh
if [ -d "/home/jenkins/a" ]; then
rm -rvf /home/jenkins/a
fi
rm -vf *.class
javac MyFirstClass.java
if [ -f "MyFirstClass.class" ]; then
java MyFirstClass
fi

Bei jedem Aufruf dieses Skripts wurde am Ende der Ordner /home/jenkins/a/b erstellt, jedoch kam bei jedem Durchlauf die Meldung Failed to create folders!.

Hintergrund ist der: Die Funktion mkdirs() gibt nur dann ein true zurück, wenn alle Ordner in der Ordnerstruktur neu erstellt werden. Wenn Teile davon schon vorher existiert haben, dann gibt die Funktion ein false zurück. Dazu die Doku:

Returns:
true if and only if the directory was created, along with all necessary parent directories; false otherwise

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/File.html#mkdirs()

Ähnliches findet sich auch in einem Bugreport für OpenJDK von vor ziemlich langer Zeit:

File.mkdir() will return false if the target exists. File.mkdirs() will return false if any directory in the path was not created (e.g., it already existed) which will usually be the case. But a programmer has the facilities to see if a directory exists (File.exists()) which yes, probably will be of more interest in most cases.

https://bugs.openjdk.java.net/browse/JDK-1241581

“File not found” beim dir Kommando

Mittels des Kommandos

dir /b "<Pfad>" 

kann man sich die Dateinamen in dem Ordner <Pfad> auflisten lassen. Also nur die Dateinamen, ohne den gesamten Pfadnamen und ohne sonstige Informationen. Nun war die Aufgabe nicht alle Dateinamen aufzulisten, sondern nur jene die eine bestimmte Endung hatten. Versucht habe ich dann erst einmal so:

dir /b "<Pfad>" *.cmd

Ich bekam eine Ausgabe, die mich etwas stutzig gemacht hat:

Woher kam die Meldung File not Found? Wie es sich herausgestellt hat, führt diese Art dir aufzurufen zu folgendem Verhalten: Zuerst werden alle Dateien in dem Ordner <Pfad> aufgelistet und dann alle *.cmd Dateien, die sich in dem Ordner befinden in dem ich mich gerade in der Konsole befinde. Und in diesem Beispiel war das eben der C:\ Ordner. Und weil ich in C:\ keine *.cmd Dateien hatte, hat dir mir die Fehlermeldung File not Found ausgegeben.

Um trotzdem alle Dateinamen mit bestimmter Endung in einem Ordner heraus zu finden, muss man das Kommando nur leicht abändern:

dir /b <Pfad> | findstr /R /C:"\.cmd$"

Mit findstr das letzte Zeichen prüfen

Mit dem Kommandowerkzeug findstr kann man unter Windows auf das letzte Zeichen eines Strings überprüfen. Eine Art dies zu bewerkstelligen kann man an folgendem Code sehen. Hier wird geprüft, ob die Variable %Project% mit einem Schrägstrich / endet oder nicht. Dabei wird ein Regex verwendet. Das Dollarzeichen in dem Regex steht hier für das Zeilenende, daher auch der “/$”:

@ECHO OFF
SETLOCAL EnableDelayedExpansion
SET "Project=/Folder1"
ECHO %Project%| findstr /R /C:"/$" 1>nul
echo %ERRORLEVEL%

SET "Project=/Folder1/"
ECHO %Project%| findstr /R /C:"/$" 1>nul
echo %ERRORLEVEL%

Bei diesem Code sollte man beachten, dass es kein Leerzeichen zwischen der Variablen %Project% und dem Pipelinezeichen | gibt. Gibt es ein Leerzeichen wird der gesamte Befehl missinterpretiert als würde das letzte Zeichen des Strings ein Leerzeichen sein und findstr gibt dann in beiden Fällen eine 1 zurück.

Der Jenkinsslave und die Javaversion

Wie sich mal wieder herausgestellt hat, ist die Javaversion auf einer Windows-Slave-Maschine von großer Bedeutung. Auf einer Windows-Slave-Maschine wollte ich eine Batch-Datei ausführen, die wiederum ein Powershell-Skript aufgerufen hat. Während des Durchlaufs bekam ich jedoch diese Fehlermeldung in Jenkins:

Import-Module : Could not load file or assembly 'file:///C:\Program Files\WindowsPowerShell\Modules\Msonline\1.1.183.17\Microsoft.Online.Administration.Automation.PSModule.dll' or one of its dependencies. An attempt was made to load a program with an incorrect format.

Das seltsame daran war jedoch, dass dieses Skript in der Eingabeaufforderung von Windows auf derselben Maschine ohne Probleme lief. Mit Jenkins bekam ich jedoch diese Fehlermeldung. Wie sich herausgestellt hat, war auf der Slave-Maschine eine x86-Version von Java installiert. Diese habe ich dann deinstalliert und die entsprechende x64-Version installiert. Danach musste man nur noch den Slave-Agenten neu starten und die Fehlermeldung war beim nächsten Bau verschwunden.

Quellen: