Bash-Skripte mit CRLF (Wagenrücklauf) in Linux wie in MSYS2 handhaben?

Bash-Skripte mit CRLF (Wagenrücklauf) in Linux wie in MSYS2 handhaben?

Nehmen wir an, ich habe das folgende triviale Skript tmp.sh:

echo "testing"
stat .
echo "testing again"

So trivial es auch ist, es hat \r\n(also CRLF, also Wagenrücklauf+Zeilenvorschub) als Zeilenende. Da die Webseite die Zeilenenden nicht beibehält, hier ein Hexdump:

$ hexdump -C tmp.sh 
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0d 0a  |echo "testing"..|
00000010  73 74 61 74 20 2e 0d 0a  65 63 68 6f 20 22 74 65  |stat ...echo "te|
00000020  73 74 69 6e 67 20 61 67  61 69 6e 22 0d 0a        |sting again"..|
0000002e

Jetzt hat es CRLF-Zeilenenden, weil das Skript unter Windows unter MSYS2 gestartet und entwickelt wurde. Wenn ich es also unter Windows 10 in MSYS2 ausführe, erhalte ich das Erwartete:

$ bash tmp.sh
testing
  File: .
  Size: 0               Blocks: 40         IO Block: 65536  directory
Device: 8e8b98b6h/2391513270d   Inode: 281474976761067  Links: 1
Access: (0755/drwxr-xr-x)  Uid: (197609/      USER)   Gid: (197121/    None)
Access: 2020-04-03 10:42:53.210292000 +0200
Modify: 2020-04-03 10:42:53.210292000 +0200
Change: 2020-04-03 10:42:53.210292000 +0200
 Birth: 2019-02-07 13:22:11.496069300 +0100
testing again

Wenn ich dieses Skript jedoch auf eine Ubuntu 18.04-Maschine kopiere und es dort ausführe, erhalte ich etwas anderes:

$ bash tmp.sh
testing
stat: cannot stat '.'$'\r': No such file or directory
testing again

In anderen Skripten mit denselben Zeilenenden habe ich in Ubuntu Bash auch diesen Fehler erhalten:

line 6: $'\r': command not found

... wahrscheinlich aus einer leeren Zeile.

Offensichtlich blockiert Ubuntu die Wagenrückläufe. Ich habe gesehen,BASH und Wagenrücklaufverhalten:

es hat nichts mit Bash zu tun: \r und \n werden vom Terminal interpretiert, nicht von Bash

... ich vermute jedoch, dass dies nur für Dinge gilt, die wörtlich in die Befehlszeile eingegeben werden; hier sind \rund \nbereits im Skript selbst eingegeben, daher muss Bash das \rhier interpretieren.

Hier ist die Version von Bash in Ubuntu:

$ bash --version
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)

... und hier die Version von Bash in MSYS2:

$ bash --version
GNU bash, version 4.4.23(2)-release (x86_64-pc-msys)

(sie scheinen nicht so weit voneinander entfernt zu sein ...)

Wie dem auch sei, meine Frage lautet: Gibt es eine Möglichkeit, Bash unter Ubuntu/Linux dazu zu bringen, das zu ignorieren \r, anstatt zu versuchen, es als (sozusagen) „druckbares Zeichen“ zu interpretieren (in diesem Fall also als ein Zeichen, das Teil eines gültigen Befehls sein könnte und das Bash als solches interpretiert)? BEARBEITEN:ohnedas Skript selbst konvertieren zu müssen (damit es gleich bleibt, mit CRLF-Zeilenenden, wenn es beispielsweise in Git auf diese Weise geprüft wird)

EDIT2: Mir wäre es so lieber, weil andere Leute, mit denen ich arbeite, das Skript möglicherweise erneut im Windows-Texteditor öffnen, es möglicherweise \r\nnoch einmal in das Skript einführen und es committen könnten; und dann hätten wir am Ende möglicherweise einen endlosen Strom von Commits, die möglicherweise nichts anderes sind als Konvertierungen \r\nzur \nVerschmutzung des Repository.

EDIT2: @Kusalananda erwähnte in den Kommentaren dos2unix( sudo apt install dos2unix); beachten Sie, dass allein das Schreiben dieses:

$ dos2unix tmp.sh 
dos2unix: converting file tmp.sh to Unix format...

... konvertiert die Datei direkt; um sie auf stdout auszugeben, muss man eine stdin-Umleitung einrichten:

$ dos2unix <tmp.sh | hexdump -C
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0a 73  |echo "testing".s|
00000010  74 61 74 20 2e 0a 65 63  68 6f 20 22 74 65 73 74  |tat ..echo "test|
00000020  69 6e 67 20 61 67 61 69  6e 22 0a                 |ing again".|
0000002b

... und dann könnte man dies prinzipiell unter Ubuntu ausführen, was in diesem Fall zu funktionieren scheint:

$ dos2unix <tmp.sh | bash
testing
  File: .
  Size: 20480       Blocks: 40         IO Block: 4096   directory
Device: 816h/2070d  Inode: 1572865     Links: 27
Access: (1777/drwxrwxrwt)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-04-03 11:11:00.309160050 +0200
Modify: 2020-04-03 11:10:58.349139481 +0200
Change: 2020-04-03 11:10:58.349139481 +0200
 Birth: -
testing again

Allerdings ändert sich dadurch - abgesehen von dem etwas komplizierten Befehl, den man sich merken muss - auch die Bash-Semantik, da stdin kein Terminal mehr ist; dies mag bei diesem trivialen Beispiel funktioniert haben, aber siehe z. B.https://stackoverflow.com/questions/23257247/pipe-a-script-into-bashzum Beispiel bei größeren Problemen.

Antwort1

Soweit mir bekannt ist, gibt es keine Möglichkeit, Bash anzuweisen, Zeilenenden im Windows-Stil zu akzeptieren.

In Situationen mit Windows ist es gängige Praxis, sich auf die Fähigkeit von Git zu verlassen, Zeilenenden beim Commit automatisch zu konvertieren, indem das autocrlfKonfigurationsflag verwendet wird. Siehe zum BeispielGitHubs Dokumentation zu Zeilenenden, was nicht spezifisch für GitHub ist. Auf diese Weise werden Dateien mit Zeilenenden im Unix-Stil in das Repository übernommen und für jede Client-Plattform entsprechend konvertiert.

(Das gegenteilige Problem tritt nicht auf: MSYS2 funktioniert unter Windows problemlos mit Zeilenenden im Unix-Stil.)

Antwort2

Du solltest benutzenbinfmt_miscdafür [1].

Definieren Sie zunächst eine Magie, die Dateien verarbeitet, die mit beginnen #! /bin/bash<CR><LF>, und erstellen Sie dann einen ausführbaren Interpreter dafür. Der Interpreter kann ein anderes Skript sein:

INTERP=/path/to/bash-crlf

echo ",bash-crlf,M,,#! /bin/bash\x0d\x0a,,$INTERP," > /proc/sys/fs/binfmt_misc/register
cat > "$INTERP" <<'EOT'; chmod 755 "$INTERP"
#! /bin/bash
script=$1; shift; exec bash <(sed 's/\r$//' "$script") "$@"
EOT

Probier es aus:

$ printf '%s\r\n' '#! /bin/bash' pwd >/tmp/foo; chmod 755 /tmp/foo
$ cat -v /tmp/foo
#! /bin/bash^M
pwd^M
$ /tmp/foo
/tmp

Der Beispielinterpreter weist zwei Probleme auf:1.da es das Skript über eine nicht suchbare Datei (eine Pipe) weiterleitet, wird Bash es Byte für Byte lesen, was sehr ineffizient ist, und2.Alle Fehlermeldungen beziehen sich auf /dev/fd/63oder etwas Ähnliches anstelle des Namens des Originalskripts.

[1] Anstatt binfmt_misc zu verwenden, kann man natürlich auch einfach einen /bin/bash^Msymbolischen Link zum Interpreter erstellen, was auch auf anderen Systemen wie OpenBSD funktionieren würde:

ln -s /path/to/bash-crlf $'/bin/bash\r'

Aber unter Linux haben Shebang-Executables keinen Vorteil gegenüber binfmt_misc und Müll in Systemverzeichnissen zu lagern ist nicht die richtige Strategie und wird jeden Systemadministrator kopfschüttelnd zurücklassen ;-)

Antwort3

Ok, ich habe eine Art Workaround gefunden, und zwar über:

„Verknüpfte“ symbolische Links

Moderne Unix-Systeme verfügen über eine Möglichkeit, beliebige Daten als Datei erscheinen zu lassen, unabhängig davon, wie sie gespeichert sind:SICHERUNG. Mit FUSE ruft jede Operation an einer Datei (erstellen, öffnen, lesen, schreiben, Verzeichnis auflisten usw.) Code in einem Programm auf, und dieser Code kann alles tun, was Sie wollen. SieheErstellen Sie eine virtuelle Datei, die eigentlich ein Befehl istSie könnten versuchenSkriptfsoderSicherung, oder wenn Sie ehrgeizig sind, drehen Sie Ihr eigenes.

... UndErstellen Sie eine virtuelle Datei, die eigentlich ein Befehl ist

Vielleicht suchen Sie nach einemBenannte Pipe.

Das ist also der Ansatz: Erstellen Sie eine benannte Pipe, lassen Sie dos2unixdie Ausgabe dorthin senden und bashrufen Sie dann die benannte Pipe auf.

Hier habe ich das Original tmp.shmit CRLF-Zeilenenden in /tmp; zuerst erstellen wir die benannte Pipe:

tmp$ mkfifo ftmp.sh

Wenn Sie jetzt diesen Befehl ausführen:

tmp$ dos2unix <tmp.sh >ftmp.sh

... Sie werden feststellen, dass es blockiert. Wenn das der Fall ist, sagen Sie:

~$ cat /tmp/ftmp.sh | hexdump -C
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0a 73  |echo "testing".s|
00000010  74 61 74 20 2e 0a 65 63  68 6f 20 22 74 65 73 74  |tat ..echo "test|
00000020  69 6e 67 20 61 67 61 69  6e 22 0a                 |ing again".|
0000002b

... werden Sie feststellen, dass die Konvertierung abgeschlossen ist – und nachdem der catBefehl seinen Lauf genommen hat, wurde der dos2unix <tmp.sh >ftmp.shBefehl, der zuvor blockiert hat, beendet.

dos2unixSo können wir das Schreiben in die benannte Pipe in einer „endlosen“ While-Schleife einrichten :

tmp$ while [ 1 ] ; do dos2unix <tmp.sh >ftmp.sh ; done

... und selbst wenn es eine „enge“ Schleife ist, sollte dies kein Problem darstellen, da der Befehl innerhalb der While-Schleife meistens blockiert.

Dann kann ich Folgendes tun:

~$ bash /tmp/ftmp.sh
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 801h/2049d  Inode: 5276132     Links: 7
...
testing again
$

... und offensichtlich läuft das Skript einwandfrei.

Das Gute an diesem Ansatz ist, dass ich das Original tmp.shin einem Texteditor öffnen und neuen Code schreiben kann (mit CRLF-Endung), dann speichern kann tmp.shund dass beim Ausführen bash /tmp/ftmp.shunter Linux die zuletzt gespeicherte Version ausgeführt wird.

Das Problem dabei ist, dass solche Befehle, read -p "Enter user: " userdie auf der tatsächlichen Standardeingabe des Terminals basieren, fehlschlagen; oder besser gesagt, nicht fehlschlagen, aber wenn Sie es versuchen, sagen Sie dies als/tmp/tmp.sh

echo "testing"
stat .
echo "testing again"
read -p "Enter user: " user
echo "user is: $user"

... dann wird folgendes ausgegeben:

$ bash /tmp/ftmp.sh
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
...
 Birth: -
testing again
Enter user: tyutyu
user is: tyutyu
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
...
 Birth: -
testing again
Enter user: asd
user is: asd
testing
...

... und so weiter – das heißt, stdin von der Tastatur im Terminal wird richtig interpretiert, aber aus irgendeinem Grund beginnt das Skript zu schleifen und wird immer wieder von Anfang an ausgeführt (was nicht passiert, wenn wir read -p ...im Original keinen Befehl haben tmp.sh). Vielleicht gibt es Umleitungsmaterial (z. B. Hinzufügen von etwas 0>1&oder was auch immer zum whileSchleifenbefehl ; tatsächlich hatte ich ein .shSkript , wgetdas auch so zu schleifen begann, und das einfache Hinzufügen eines expliziten exitam Ende des .shSkripts schien zu funktionieren, um die Skriptschleife zu stoppen), das dies ebenfalls handhaben könnte – aber bisher hat das Skript, das ich verwenden muss, keine read -pderartigen Befehle, also könnte dieser Ansatz für mich funktionieren.

Antwort4

Sie können in Ihren Bash-Skripten einfach am Ende jeder Zeile ein Hash (#) einfügen. Auf diese Weise betrachten die Shells unter Unix den CR nur als Kommentar und kümmern sich nicht darum.

"Hex-sprechend" sollte jede Zeile mit enden

0x23 0x0D 0x0A

Beispiel:

echo "testing" #
stat . #
echo "testing again" #

verwandte Informationen