Lidando com script Bash com CRLF (retorno de carro) no Linux como no MSYS2?

Lidando com script Bash com CRLF (retorno de carro) no Linux como no MSYS2?

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 \re \njá estão digitados no próprio script, então deve ser que o Bash interprete os \raqui.

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\nnovamente 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\npoluir \no 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 autocrlfsinalizador 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/63ou algo semelhante em vez do nome do script original.

[1] Claro, em vez de usar binfmt_misc você pode simplesmente criar um /bin/bash^Mlink 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:

Links simbólicos "juntados"

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 dos2unixsaída para ele e, em seguida, chamar basho pipe nomeado.

Aqui eu tenho o original tmp.shcom 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 catcomando seguiu seu curso, o dos2unix <tmp.sh >ftmp.shcomando, que foi bloqueado anteriormente, foi encerrado.

Portanto, podemos configurar a dos2unixgravaçã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.shabrir 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.shno Linux executará a versão salva mais recente.

O problema com isso é que comandos como read -p "Enter user: " useresse 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 0>1&ou qualquer outra coisa ao whilecomando de loop ; na verdade, eu tinha um .shscript wgetque também começou a fazer um loop assim, e simplesmente adicionar um explícito exitno final do .shscript 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 -pcomandos 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" #

informação relacionada