Допустим, у меня есть следующий простой скрипт 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" #