¿Los scripts de Shell son sensibles a la codificación y los finales de línea?

Resuelto thomasb asked hace 8 años • 16 respuestas

Estoy creando una aplicación NW.js en macOS y quiero ejecutar la aplicación en modo de desarrollo haciendo doble clic en un icono. En el primer paso, intento que mi script de shell funcione.

Usando VS Code en Windows (quería ganar tiempo), creé un run-nwarchivo en la raíz de mi proyecto que contiene esto:

#!/bin/bash

cd "src"
npm install

cd ..
./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &

pero me sale este resultado:

$ sh ./run-nw

: command not found  
: No such file or directory  
: command not found  
: No such file or directory  

Usage: npm <command>

where <command> is one of:  (snip commands list)

(snip npm help)

[email protected] /usr/local/lib/node_modules/npm  
: command not found  
: No such file or directory  
: command not found

Algunas cosas no las entiendo.

  • Parece que toma líneas vacías como comandos. En mi editor (Código VS) he intentado reemplazarlo \r\ncon \n (en caso de que \rcree problemas) pero no cambia nada.
  • Parece que no encuentra las carpetas (con o sin dirnameinstrucciones), o tal vez no conoce el cdcomando.
  • Parece que no entiende el installargumento npm.
  • La parte que realmente me extraña es que todavía ejecuta la aplicación (si lo hice npm installmanualmente)...

Al no poder hacerlo funcionar correctamente y sospechando que había algo extraño con el archivo en sí, creé uno nuevo directamente en la Mac, usando vim esta vez. Ingresé exactamente las mismas instrucciones y... ahora funciona sin ningún problema.
A diffen los dos archivos revela exactamente cero diferencia.

¿Cuál puede ser la diferencia? ¿Qué puede hacer que el primer script no funcione? ¿Cómo puedo saberlo?

Actualizar

Siguiendo las recomendaciones de la respuesta aceptada, después de que aparecieron finales de línea incorrectos, verifiqué varias cosas. Resulta que desde que copié mi ~/.gitconfigdesde mi máquina con Windows, tenía autocrlf=true, por lo que cada vez que modificaba el archivo bash en Windows, restablecía los finales de línea en \r\n.
Entonces, además de ejecutar dos2unix(que tendrás que instalar usando Homebrew en una Mac), si estás usando Git, revisa tu .gitconfigarchivo.

thomasb avatar Sep 16 '16 16:09 thomasb
Aceptado

Sí. Los scripts Bash son sensibles a los finales de línea, tanto en el propio script como en los datos que procesa. Deben tener finales de línea estilo Unix, es decir, cada línea termina con un carácter de avance de línea (decimal 10, hexadecimal 0A en ASCII).

Finales de línea de DOS/Windows en el script

Con finales de línea estilo Windows o DOS, cada línea termina con un retorno de carro seguido de un carácter de avance de línea. Puedes ver este carácter invisible en el resultado de cat -v yourfile:

$ cat -v yourfile
#!/bin/bash^M
^M
cd "src"^M
npm install^M
^M
cd ..^M
./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &^M

En este caso, el retorno de carro ( ^Men notación de intercalación o \ren notación de escape C) no se trata como un espacio en blanco. Bash interpreta la primera línea después del shebang (que consta de un único carácter de retorno de carro) como el nombre de un comando/programa a ejecutar.

  • Como no hay ningún comando nombrado ^M, imprime: command not found
  • Como no hay ningún directorio llamado "src"^M(o src^M), imprime: No such file or directory
  • Pasa install^Men lugar de installser un argumento al npmque se debe npmreclamar.

Finales de línea de DOS/Windows en datos de entrada

Como arriba, si tiene un archivo de entrada con retornos de carro:

hello^M
world^M

entonces se verá completamente normal en los editores y al escribirlo en la pantalla, pero las herramientas pueden producir resultados extraños. Por ejemplo, grepno podrá encontrar líneas que obviamente estén ahí:

$ grep 'hello$' file.txt || grep -x "hello" file.txt
(no match because the line actually ends in ^M)

En cambio, el texto agregado sobrescribirá la línea porque los retornos de carro mueven el cursor al inicio de la línea:

$ sed -e 's/$/!/' file.txt
!ello
!orld

La comparación de cadenas parecerá fallar, aunque las cadenas parezcan ser las mismas al escribir en la pantalla:

$ a="hello"; read b < file.txt
$ if [[ "$a" = "$b" ]]
  then echo "Variables are equal."
  else echo "Sorry, $a is not equal to $b"
  fi

Sorry, hello is not equal to hello

Soluciones

La solución es convertir el archivo para que utilice finales de línea estilo Unix. Hay varias maneras de lograr esto:

  1. Esto se puede hacer usando el dos2unixprograma:

    dos2unix filename
    
  2. Abra el archivo en un editor de texto compatible (Sublime, Notepad++, no Notepad) y configúrelo para guardar archivos con finales de línea de Unix, por ejemplo, con Vim, ejecute el siguiente comando antes de (re)guardarlo:

    :set fileformat=unix
    
  3. Si tiene una versión de la sedutilidad que admite la opción -io --in-place, por ejemplo, GNU sed, puede ejecutar el siguiente comando para eliminar los retornos de carro finales:

    sed -i 's/\r$//' filename
    

    Con otras versiones de sed, puede utilizar la redirección de salida para escribir en un archivo nuevo. Asegúrese de utilizar un nombre de archivo diferente para el destino de la redirección (se puede cambiar el nombre más adelante).

    sed 's/\r$//' filename > filename.unix
    
  4. De manera similar, el trfiltro de traducción se puede utilizar para eliminar caracteres no deseados de su entrada:

    tr -d '\r' <filename >filename.unix
    

Golpe de Cygwin

Con el puerto Bash para Cygwin, hay una igncropción personalizada que se puede configurar para ignorar el retorno de carro en los finales de línea (presumiblemente porque muchos de sus usuarios usan programas nativos de Windows para editar sus archivos de texto). Esto se puede habilitar para el shell actual ejecutando set -o igncr.

Establecer esta opción se aplica sólo al proceso de shell actual , por lo que puede resultar útil cuando se obtienen archivos con retornos de carro extraños. Si encuentra regularmente scripts de shell con finales de línea de DOS y desea que esta opción se establezca de forma permanente, puede configurar una variable de entorno llamada SHELLOPTS(todas letras mayúsculas) para incluir igncr. Bash utiliza esta variable de entorno para configurar las opciones de Shell cuando se inicia (antes de leer cualquier archivo de inicio).

Utilidades útiles

La fileutilidad es útil para ver rápidamente qué finales de línea se utilizan en un archivo de texto. Esto es lo que imprime para cada tipo de archivo:

  • Finales de línea de Unix:Bourne-Again shell script, ASCII text executable
  • Finales de línea de Mac:Bourne-Again shell script, ASCII text executable, with CR line terminators
  • Finales de línea de DOS:Bourne-Again shell script, ASCII text executable, with CRLF line terminators

La versión GNU de la catutilidad tiene una -v, --show-nonprintingopción que muestra caracteres que no se pueden imprimir.

La dos2unixutilidad está escrita específicamente para convertir archivos de texto entre finales de línea de Unix, Mac y DOS.

Enlaces útiles

Wikipedia tiene un excelente artículo que cubre las diferentes formas de marcar el final de una línea de texto, la historia de dichas codificaciones y cómo se tratan las nuevas líneas en diferentes sistemas operativos, lenguajes de programación y protocolos de Internet (por ejemplo, FTP).

Archivos con terminaciones de línea clásicas de Mac OS

Con Classic Mac OS (anterior a OS X), cada línea terminaba con un retorno de carro (decimal 13, hexadecimal 0D en ASCII). Si se guardó un archivo de script con tales finales de línea, Bash solo vería una línea larga como esta:

#!/bin/bash^M^Mcd "src"^Mnpm install^M^Mcd ..^M./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &^M

Dado que esta única línea larga comienza con un octothorpe ( #), Bash trata la línea (y todo el archivo) como un solo comentario.

Nota: En 2001, Apple lanzó Mac OS X, que se basó en el sistema operativo NeXTSTEP derivado de BSD. Como resultado, OS X también utiliza finales de línea de estilo Unix únicamente LF y, desde entonces, los archivos de texto terminados con CR se han vuelto extremadamente raros. Sin embargo, creo que vale la pena mostrar cómo Bash intentaría interpretar dichos archivos.

Anthony Geoghegan avatar Sep 16 '2016 09:09 Anthony Geoghegan

En los productos JetBrains (PyCharm, PHPStorm, IDEA, etc.), deberá click activar CRLFo LFalternar entre los dos tipos de separadores de línea ( y\r\n ) \n.

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Pedro Lobito avatar Feb 26 '2019 03:02 Pedro Lobito

Estaba intentando iniciar mi contenedor acoplable desde Windows y obtuve esto:

Bash script and /bin/bash^M: bad interpreter: No such file or directory

Estaba usando git bash y el problema estaba relacionado con la configuración de git, luego seguí los pasos a continuación y funcionó. Configurará Git para que no convierta los finales de línea al finalizar la compra:

  1. git config --global core.autocrlf input
  2. elimina tu repositorio local
  3. clonarlo de nuevo.

Muchas gracias a Jason Harmon en este enlace: https://forums.docker.com/t/error- while-running-docker-code-in-powershell/34059/6

Antes de eso, probé esto y no funcionó:

  1. dos2unix scriptname.sh
  2. sed -i -e 's/\r$//' scriptname.sh
  3. sed -i -e 's/^M$//' scriptname.sh
dougparnoff avatar Aug 19 '2020 22:08 dougparnoff

Si está utilizando el readcomando para leer desde un archivo (o canalización) que está (o podría estar) en formato DOS/Windows, puede aprovechar el hecho de que readrecortará los espacios en blanco desde el principio y el final de las líneas. Si le dice que los retornos de carro son espacios en blanco (agregándolos a la IFSvariable), los recortará desde los finales de las líneas.

En bash (o zsh o ksh), eso significa que reemplazarías este modismo estándar:

IFS= read -r somevar    # This will not trim CR

con este:

IFS=$'\r' read -r somevar    # This *will* trim CR

(Nota: la -ropción no está relacionada con esto; normalmente es una buena idea evitar alterar las barras invertidas).

Si no estás usando el IFS=prefijo (por ejemplo, porque quieres dividir los datos en campos), entonces reemplazarías esto:

read -r field1 field2 ...    # This will not trim CR

con este:

IFS=$' \t\n\r' read -r field1 field2 ...    # This *will* trim CR

Si está utilizando un shell que no admite el $'...'modo de comillas (por ejemplo, guión, el /bin/sh predeterminado en algunas distribuciones de Linux), o su script incluso podría ejecutarse con dicho shell, entonces necesita obtener un poco de mas complejo:

cr="$(printf '\r')"
IFS="$cr" read -r somevar    # Read trimming *only* CR
IFS="$IFS$cr" read -r field1 field2 ...    # Read trimming CR and whitespace, and splitting fields

Tenga en cuenta que normalmente, cuando cambie IFS, debe volver a la normalidad lo antes posible para evitar efectos secundarios extraños; pero en todos estos casos, es un prefijo del readcomando, por lo que solo afecta a ese comando y no es necesario restablecerlo después.

Gordon Davisson avatar May 19 '2021 22:05 Gordon Davisson