Обработка скрипта Bash с CRLF (возврат каретки) в Linux, как в MSYS2?

Обработка скрипта Bash с CRLF (возврат каретки) в Linux, как в MSYS2?

Допустим, у меня есть следующий простой скрипт tmp.sh:

echo "testing"
stat .
echo "testing again"

Как бы тривиально это ни было, в качестве окончания строк он имеет \r\n(то есть CRLF, то есть возврат каретки+перевод строки). Поскольку веб-страница не сохраняет окончания строк, вот шестнадцатеричный дамп:

$ 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

Теперь он имеет окончания строк CRLF, поскольку скрипт был запущен и разработан в Windows, под MSYS2. Поэтому, когда я запускаю его в Windows 10 в MSYS2, я получаю ожидаемое:

$ 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

Однако если я скопирую этот скрипт на машину с Ubuntu 18.04 и запущу его там, я получу нечто другое:

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

В других скриптах с такими же окончаниями строк я также получал эту ошибку в Ubuntu bash:

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

...вероятно, из пустой строки.

Итак, очевидно, что-то в Ubuntu подавляется возвратом каретки. Я виделBASH и поведение возврата каретки:

это не имеет никакого отношения к Bash: \r и \n интерпретируются терминалом, а не Bash

... однако, я полагаю, что это касается только того, что набирается дословно в командной строке; здесь \rи \nуже набраны в самом скрипте, так что, должно быть, Bash интерпретирует \rздесь.

Вот версия Bash в Ubuntu:

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

... а вот версия Bash в MSYS2:

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

(они не кажутся такими уж разными...)

В любом случае, мой вопрос - есть ли способ убедить Bash в Ubuntu/Linux игнорировать \r, а не пытаться интерпретировать его как (так сказать) "печатаемый символ" (в данном случае, имеется в виду символ, который может быть частью допустимой команды, которую bash интерпретирует как таковую)? ПРАВКА:безнеобходимость конвертировать сам скрипт (чтобы он остался прежним, с окончаниями строк CRLF, если он проверяется таким образом, скажем, в git)

EDIT2: Я бы предпочел сделать это именно так, потому что другие люди, с которыми я работаю, могут снова открыть скрипт в текстовом редакторе Windows, потенциально снова ввести его \r\nв скрипт и зафиксировать его; и тогда мы можем получить бесконечный поток коммитов, которые могут оказаться ничем иным, как преобразованиями, \r\nзагрязняющими \nрепозиторий.

EDIT2: @Kusalananda в комментариях упомянул dos2unix( sudo apt install dos2unix); обратите внимание, что только что написал это:

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

... преобразует файл на месте; чтобы вывести его на stdout, необходимо настроить перенаправление stdin:

$ 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

... и тогда, в принципе, это можно было бы запустить на Ubuntu, что, кажется, работает в данном случае:

$ 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

Однако, помимо того, что команда немного запутана, ее следует запомнить, это также меняет семантику bash, поскольку stdin больше не является терминалом; это могло бы сработать в этом тривиальном примере, но см., например,https://stackoverflow.com/questions/23257247/pipe-a-script-into-bashнапример, более серьезных проблем.

решение1

Насколько мне известно, нет способа заставить Bash принимать окончания строк в стиле Windows.

В ситуациях, связанных с Windows, общепринятой практикой является использование возможности Git автоматически преобразовывать окончания строк при коммите, используя autocrlfфлаг конфигурации. См. напримерДокументация GitHub по окончаниям строк, что не является специфичным для GitHub. Таким образом, файлы фиксируются с окончаниями строк в стиле Unix в репозитории и преобразуются соответствующим образом для каждой клиентской платформы.

(Противоположная проблема не является проблемой: MSYS2 отлично работает с окончаниями строк в стиле Unix в Windows.)

решение2

Вам следует использоватьbinfmt_miscдля этого [1].

Сначала определите магию, которая обрабатывает файлы, начинающиеся с #! /bin/bash<CR><LF>, затем создайте исполняемый интерпретатор для него. Интерпретатором может быть другой скрипт:

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

Попробуй это:

$ 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

У интерпретатора образцов есть две проблемы:1.поскольку он передает скрипт через неотслеживаемый файл (канал), bash будет читать его байт за байтом, что очень неэффективно, и2.любые сообщения об ошибках будут ссылаться на /dev/fd/63имя исходного скрипта или на что-то похожее вместо него.

[1] Конечно, вместо использования binfmt_misc вы можете просто создать /bin/bash^Mсимволическую ссылку на интерпретатор, которая будет работать и в других системах, таких как OpenBSD:

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

Но в Linux исполняемые файлы shebanged не имеют никаких преимуществ перед binfmt_misc, а размещение мусора в системных каталогах — неправильная стратегия, и любой системный администратор останется в недоумении ;-)

решение3

Хорошо, я нашел своего рода обходной путь:

«Соединенные» символические ссылки

Современные системы Unix позволяют представить произвольные данные в виде файла, независимо от того, как они хранятся:ПРЕДОХРАНИТЕЛЬ. С FUSE каждая операция с файлом (создание, открытие, чтение, запись, список каталогов и т. д.) вызывает некоторый код в программе, и этот код может делать все, что вы захотите. СмотритеСоздайте виртуальный файл, который на самом деле является командой. Вы можете попробоватьскриптфсилифьюзефлтили, если вы амбициозны, создайте свой собственный.

... иСоздайте виртуальный файл, который на самом деле является командой

Возможно, вы ищетеименованный канал.

Итак, подход следующий: создаем именованный канал, направляем dos2unixв него вывод, а затем bashвызываем именованный канал.

Здесь у меня есть оригинал tmp.shс окончанием строки CRLF в /tmp; для начала давайте создадим именованный канал:

tmp$ mkfifo ftmp.sh

Теперь, если вы выполните эту команду:

tmp$ dos2unix <tmp.sh >ftmp.sh

... вы заметите, что он блокируется; если это так, скажите:

~$ 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

... вы заметите, что преобразование было выполнено, и после того, как catкоманда отработала, dos2unix <tmp.sh >ftmp.shкоманда, которая была заблокирована ранее, завершилась.

Итак, мы можем настроить dos2unixзапись в именованный канал в «бесконечном» цикле while:

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

... и даже если это «тугой» цикл, это не должно быть проблемой, так как большую часть времени команда внутри цикла while является блокирующей.

Тогда я могу сделать:

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

... и очевидно, что скрипт работает нормально.

Преимущество этого подхода в том, что я могу tmp.shоткрыть оригинал в текстовом редакторе, написать новый код (с окончаниями CRLF), а затем сохранить его tmp.sh; а при запуске bash /tmp/ftmp.shпод Linux будет запущена последняя сохраненная версия.

Проблема в том, что такие команды, read -p "Enter user: " userкоторые полагаются на фактический stdin терминала, не дадут результата; или, скорее, не дадут результата, но если вы попробуете, скажите это так:/tmp/tmp.sh

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

... то будет выведено следующее:

$ 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
...

... и так далее - то есть stdin с клавиатуры в терминале интерпретируется правильно, но по какой-то причине скрипт начинает циклиться и выполняется с самого начала снова и снова (чего не происходит, если у нас нет команды read -p ...в оригинале tmp.sh). Возможно, есть какие-то перенаправления (например, добавление чего-либо 0>1&или чего-либо еще к whileкоманде цикла ; на самом деле, у меня был .shскрипт, wgetкоторый также начинал циклиться таким образом, и простое добавление явного exitв конец скрипта .sh, похоже, работало, чтобы остановить цикл скрипта), которые могли бы справиться с этим, - но пока что скрипт, который мне нужно использовать, не имеет read -pподобных команд, так что этот подход может мне подойти.

решение4

Вы можете вставить хэш (#) просто в конце каждой строки в ваших скриптах bash. Таким образом, оболочки в Unix будут считать CR просто комментарием и не будут обращать на него внимания.

«Говоря шестнадцатеричным языком», любая строка должна заканчиваться на

0x23 0x0D 0x0A

Пример:

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

Связанный контент