Digamos que eu tenha o seguinte script trivial tmp.sh
:
echo "testing"
stat .
echo "testing again"
Por mais trivial que seja, tem \r\n
(ou seja, CRLF, que é retorno de carro + avanço de linha) como finais de linha. Como a página da web não preservará os finais de linha, aqui está um 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
Agora possui finais de linha CRLF, pois o script foi iniciado e desenvolvido em Windows, em MSYS2. Então, quando executo no Windows 10 no MSYS2, obtenho o esperado:
$ 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
No entanto, se eu copiar este script para uma máquina Ubuntu 18.04 e executá-lo lá, recebo outra coisa:
$ bash tmp.sh
testing
stat: cannot stat '.'$'\r': No such file or directory
testing again
Em outros scripts com os mesmos finais de linha, também recebi este erro no Ubuntu bash:
line 6: $'\r': command not found
... provavelmente de uma linha vazia.
Então, claramente, algo no Ubuntu engasga com o retorno da carruagem. Eu tenho vistoBASH e comportamento de retorno de carro:
não tem nada a ver com Bash: \r e \n são interpretados pelo terminal, não pelo Bash
... no entanto, acho que isso é apenas para coisas digitadas literalmente na linha de comando; aqui os \r
e \n
já estão digitados no próprio script, então deve ser que o Bash interprete os \r
aqui.
Aqui está a versão do Bash no Ubuntu:
$ bash --version
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
... e aqui a versão do Bash no MSYS2:
$ bash --version
GNU bash, version 4.4.23(2)-release (x86_64-pc-msys)
(eles não parecem tão distantes...)
De qualquer forma, minha pergunta é: existe uma maneira de persuadir o Bash no Ubuntu/Linux a ignorar o \r
, em vez de tentar interpretá-lo como um (por assim dizer) "caractere imprimível" (neste caso, significando um caractere que poderia ser um parte de um comando válido, que o bash interpreta como tal)? EDITAR:semter que converter o script em si (para que permaneça o mesmo, com finais de linha CRLF, se for verificado dessa forma, digamos, no git)
EDIT2: Eu preferiria assim, porque outras pessoas com quem trabalho podem reabrir o script no editor de texto do Windows, potencialmente reintroduzi-lo \r\n
novamente no script e confirmá-lo; e então poderemos acabar com um fluxo interminável de commits que podem não ser nada além de conversões para \r\n
poluir \n
o repositório.
EDIT2: @Kusalananda nos comentários mencionados dos2unix
( sudo apt install dos2unix
); observe que apenas escrevendo isto:
$ dos2unix tmp.sh
dos2unix: converting file tmp.sh to Unix format...
... converterá o arquivo no local; para que ele seja enviado para stdout, é necessário configurar o redirecionamento 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
... e então, em princípio, alguém poderia rodar isso no Ubuntu, o que parece funcionar neste caso:
$ 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
No entanto, - além do comando um pouco confuso para lembrar - isso também altera a semântica do bash, já que stdin não é mais um terminal; isso pode ter funcionado com este exemplo trivial, mas veja, por exemplohttps://stackoverflow.com/questions/23257247/pipe-a-script-into-bashpor exemplo, de problemas maiores.
Responder1
Até onde eu sei, não há como dizer ao Bash para aceitar finais de linha no estilo do Windows.
Em situações que envolvem o Windows, a prática comum é confiar na capacidade do Git de converter automaticamente os finais de linha durante o commit, usando o autocrlf
sinalizador de configuração. Veja por exemploDocumentação do GitHub sobre finais de linha, que não é específico do GitHub. Dessa forma, os arquivos são confirmados com finais de linha no estilo Unix no repositório e convertidos conforme apropriado para cada plataforma cliente.
(O problema oposto não é um problema: MSYS2 funciona bem com finais de linha no estilo Unix, no Windows.)
Responder2
Você deveria usarbinfmt_miscpara isso [1].
Primeiro, defina uma mágica que lide com arquivos que começam com #! /bin/bash<CR><LF>
e, em seguida, crie um interpretador executável para ela. O intérprete pode ser outro script:
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
Teste-o:
$ 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
O intérprete de amostra tem dois problemas:1.uma vez que ele passa o script por meio de um arquivo não procurável (um pipe), o bash irá lê-lo byte por byte, de forma muito ineficiente, e2.quaisquer mensagens de erro farão referência /dev/fd/63
ou algo semelhante em vez do nome do script original.
[1] Claro, em vez de usar binfmt_misc você pode simplesmente criar um /bin/bash^M
link simbólico para o interpretador, que também funcionaria em outros sistemas como OpenBSD:
ln -s /path/to/bash-crlf $'/bin/bash\r'
Mas no Linux, os executáveis shebanged não têm vantagem sobre o binfmt_misc, e colocar lixo dentro dos diretórios do sistema não é a estratégia certa e deixará qualquer administrador de sistema balançando a cabeça ;-)
Responder3
Ok, encontrei uma solução alternativa, via:
Os sistemas unix modernos têm uma maneira de fazer com que dados arbitrários apareçam como um arquivo, independentemente de como estão armazenados:FUSÍVEL. Com o FUSE, cada operação em um arquivo (criar, abrir, ler, escrever, listar diretório, etc.) invoca algum código em um programa, e esse código pode fazer o que você quiser. VerCrie um arquivo virtual que seja na verdade um comando. Você poderia experimentarscriptfsoufusível, ou se você estiver se sentindo ambicioso, crie o seu próprio.
... eCrie um arquivo virtual que seja na verdade um comando
Você pode estar procurando por umtubo nomeado.
Então, esta é a abordagem: criar um pipe nomeado, gerar dos2unix
saída para ele e, em seguida, chamar bash
o pipe nomeado.
Aqui eu tenho o original tmp.sh
com terminações de linha CRLF em /tmp
; primeiro, vamos criar o pipe nomeado:
tmp$ mkfifo ftmp.sh
Agora, se você executar este comando:
tmp$ dos2unix <tmp.sh >ftmp.sh
... você notará que ele bloqueia; então se você fizer isso, diga:
~$ 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
... você notará que a conversão foi feita - e depois que o cat
comando seguiu seu curso, o dos2unix <tmp.sh >ftmp.sh
comando, que foi bloqueado anteriormente, foi encerrado.
Portanto, podemos configurar a dos2unix
gravação no pipe nomeado em um loop while "infinito":
tmp$ while [ 1 ] ; do dos2unix <tmp.sh >ftmp.sh ; done
... e mesmo que seja um loop "apertado", não deve ser um problema, pois na maioria das vezes o comando dentro do loop while está bloqueando.
Então eu posso fazer:
~$ bash /tmp/ftmp.sh
testing
File: .
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: 801h/2049d Inode: 5276132 Links: 7
...
testing again
$
... e claramente, o script funciona bem.
O que há de bom nessa abordagem é que posso tmp.sh
abrir o original em um editor de texto; e escreva um novo código - com terminações CRLF - e salve tmp.sh
; e rodar bash /tmp/ftmp.sh
no Linux executará a versão salva mais recente.
O problema com isso é que comandos como read -p "Enter user: " user
esse dependem do stdin do terminal real falharão; ou melhor, não falhar, mas se você tentar, diga isso como/tmp/tmp.sh
echo "testing"
stat .
echo "testing again"
read -p "Enter user: " user
echo "user is: $user"
... então isso será gerado:
$ 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
...
... e assim por diante - ou seja, stdin do teclado no terminal é interpretado corretamente, mas por algum motivo o script começa a fazer um loop e é executado desde o início repetidamente (o que não acontece se não tivermos um read -p ...
comando em o original tmp.sh
). Talvez haja algum material de redirecionamento (por exemplo, adicionar algo ; na verdade, eu tinha um 0>1&
ou qualquer outra coisa ao while
comando de loop.sh
script wget
que também começou a fazer um loop assim, e simplesmente adicionar um explícito exit
no final do .sh
script pareceu funcionar para interromper o loop do script) isso também poderia resolver isso - mas até agora, o script que preciso usar não possui read -p
comandos semelhantes, portanto, essa abordagem pode funcionar para mim.
Responder4
Você pode inserir um hash (#) logo no final de cada linha em seus scripts bash. Desta forma, os shells do Unix considerarão o CR apenas como um comentário e não se importarão com isso.
"Hex falando", qualquer linha deve terminar com
0x23 0x0D 0x0A
Exemplo:
echo "testing" #
stat . #
echo "testing again" #