¿Los scripts de Shell son sensibles a la codificación y los finales de línea?
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-nw
archivo 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\n
con\n
(en caso de que\r
cree problemas) pero no cambia nada. - Parece que no encuentra las carpetas (con o sin
dirname
instrucciones), o tal vez no conoce elcd
comando. - Parece que no entiende el
install
argumentonpm
. - La parte que realmente me extraña es que todavía ejecuta la aplicación (si lo hice
npm install
manualmente)...
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 diff
en 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 ~/.gitconfig
desde 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
.gitconfig
archivo.
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 ( ^M
en notación de intercalación o \r
en 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
(osrc^M
), imprime: No such file or directory
- Pasa
install^M
en lugar deinstall
ser un argumento alnpm
que se debenpm
reclamar.
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, grep
no 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:
Esto se puede hacer usando el
dos2unix
programa:dos2unix filename
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
Si tiene una versión de la
sed
utilidad que admite la opción-i
o--in-place
, por ejemplo, GNUsed
, 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
De manera similar, el
tr
filtro 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 igncr
opció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 file
utilidad 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 cat
utilidad tiene una -v, --show-nonprinting
opción que muestra caracteres que no se pueden imprimir.
La dos2unix
utilidad 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.
En los productos JetBrains (PyCharm, PHPStorm, IDEA, etc.), deberá click activar CRLF
o LF
alternar entre los dos tipos de separadores de línea ( y\r\n
) \n
.
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:
git config --global core.autocrlf input
- elimina tu repositorio local
- 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ó:
dos2unix scriptname.sh
sed -i -e 's/\r$//' scriptname.sh
sed -i -e 's/^M$//' scriptname.sh
Si está utilizando el read
comando para leer desde un archivo (o canalización) que está (o podría estar) en formato DOS/Windows, puede aprovechar el hecho de que read
recortará 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 IFS
variable), 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 -r
opció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 read
comando, por lo que solo afecta a ese comando y no es necesario restablecerlo después.