¿Por qué la salida de mi herramienta se sobrescribe y cómo lo soluciono?
La intención de esta pregunta es ser canónica y cubrir todo tipo de preguntas cuya respuesta se reduce a "tienes finales de línea de DOS ingresados en una herramienta Unix". Cualquiera que tenga una pregunta relacionada debería encontrar una explicación clara de por qué se la señaló aquí, así como herramientas que pueden resolver su problema, además de los pros, los contras y las advertencias de las posibles soluciones. Algunas de las preguntas existentes sobre este tema han aceptado respuestas que solo dicen "ejecute esta herramienta" con poca explicación o simplemente son peligrosas y nunca deben usarse.
Ahora a una pregunta típica que resultaría en una referencia aquí:
Tengo un archivo que contiene 1 línea:
what isgoingon
y cuando lo imprimo usando este script awk para invertir el orden de los campos:
awk '{print $2, $1}' file
en lugar de ver el resultado, espero:
isgoingon what
Obtengo el campo que debería estar al final de la línea que aparece al principio de la línea y sobrescribe algo de texto:
whatngon
o obtengo la salida dividida en 2 líneas:
isgoingon
what
¿Cuál podría ser el problema y cómo lo soluciono?
El problema es que su archivo de entrada usa finales de línea de DOS de CRLF
en lugar de finales de línea de UNIX de just LF
, y está ejecutando una herramienta UNIX en él, por lo que CR
sigue siendo parte de los datos que opera la herramienta UNIX. CR
comúnmente se denota por \r
y puede verse como control-M ( ^M
) cuando se ejecuta cat -vE
en el archivo, mientras que LF
es \n
y aparece como $
con cat -vE
.
Entonces tu archivo de entrada no era realmente solo:
what isgoingon
en realidad fue:
what isgoingon\r\n
como puedes ver con cat -vE
:
$ cat -vE file
what isgoingon^M$
y od -c
:
$ od -c file
0000000 w h a t i s g o i n g o n \r \n
0000020
entonces, cuando ejecuta una herramienta UNIX como awk (que se trata \n
como el final de línea) en el archivo, se \n
consume por el acto de leer la línea, pero eso deja los 2 campos como:
<what> <isgoingon\r>
Tenga en cuenta el \r
al final del segundo campo. \r
significa retorno de carro , que es literalmente una instrucción para devolver el cursor al inicio de la línea. Entonces cuando lo hagas:
print $2, $1
awk lo imprimirá en la terminal, que imprimirá isgoingon
y devolverá el cursor al inicio de la línea antes de imprimir un espacio seguido de what
, razón por la cual what
parece sobrescribir el inicio de isgoingon
.
Solución
Para solucionar el problema, haga cualquiera de estos:
dos2unix file
sed 's/\r$//' file
awk '{sub(/\r$/,"")}1' file
perl -pe 's/\r$//' file
Aparentemente dos2unix
es también conocido como fromdos
en algunas variantes de UNIX (por ejemplo, Ubuntu).
Tenga cuidado si decide utilizar tr -d '\r'
lo que se sugiere a menudo, ya que eso eliminará todos \r
los correos electrónicos de su archivo, no solo los que se encuentran al final de cada línea. (Más detalles a continuación).
Notas
Manejo de finales de línea de DOS con awk
GNU awk le permitirá analizar archivos que tienen finales de línea de DOS simplemente configurándolos RS
apropiadamente:
gawk -v RS='\r\n' '...' file
pero otros awks no lo permitirán, ya que POSIX solo requiere que los awks admitan un solo carácter RS y la mayoría de los demás awks se truncarán silenciosamente RS='\r\n'
a RS='\r'
. Es posible que deba agregar -v BINMODE=3
gawk para ver incluso los \r
s, ya que las primitivas C subyacentes los eliminarán en algunas plataformas, por ejemplo, cygwin.
Datos CSV que contienen nuevas líneas
Una cosa a tener en cuenta es que los CSV creados por herramientas de Windows como Excel se utilizarán CRLF
como finales de línea, pero pueden tener LF
mensajes incrustados dentro de un campo específico del CSV, por ejemplo:
"field1","field2.1
field2.2","field3"
realmente es:
"field1","field2.1\nfield2.2","field3"\r\n
entonces, si simplemente convierte \r\n
s en \n
s, ya no podrá distinguir los avances de línea dentro de los campos de los avances de línea como finales de línea, por lo que si desea hacer eso, le recomiendo convertir primero todos los avances de línea dentro de los campos a otra cosa, por ejemplo, esto convertiría todos los avances de línea dentro de los campos a otra cosa. -campo LFs
a pestañas y convierte todas las líneas que terminan en s CRLF
a LF
s:
gawk -v RS='\r\n' '{gsub(/\n/,"\t")}1' file
Hacer algo similar sin GNU awk se deja como ejercicio, pero con otros awks implica combinar líneas que no terminan en CR
cuando se leen.
FS predeterminado de Awk
También tenga en cuenta que, aunque CR es parte de la [[:space:]]
clase de caracteres POSIX, no es uno de los espacios en blanco incluidos como campos de separación cuando se " "
usa el FS predeterminado, cuyos espacios en blanco son solo tabulación, espacios en blanco y nueva línea. Esto puede generar resultados confusos si su entrada puede tener espacios en blanco antes de CRLF:
$ printf 'x y \n'
x y
$ printf 'x y \n' | awk '{print $NF}'
y
$
$ printf 'x y \r\n'
x y
$ printf 'x y \r\n' | awk '{print $NF}'
$
Esto se debe a que el espacio en blanco del separador de campo final se ignora al principio/final de una línea que tiene finales de línea LF, pero \r
es el campo final en una línea con finales de línea CRLF si el carácter anterior era un espacio en blanco:
$ printf 'x y \r\n' | awk '{print $NF}' | cat -Ev
^M$
Puede utilizar la \R
secuencia de barra invertida en PCRE para archivos con finales de línea desconocidos. Hay aún más finales de línea a considerar con Unicode u otras plataformas. El \R
formulario es una clase de caracteres recomendada por el consorcio Unicode para representar todas las formas de una nueva línea genérica.
Entonces, si tiene un 'extra', puede encontrarlo y eliminarlo con la expresión regular s/\R$/\n/
que normalizará cualquier combinación de finales de línea en \n
. Alternativamente, puede utilizar s/\R/\n/g
para capturar cualquier noción de "final de línea" y estandarizarla en un \n
carácter.
Dado:
$ printf "what\risgoingon\r\n" > file
$ od -c file
0000000 w h a t \r i s g o i n g o n \r \n
0000020
Perl y Ruby y la mayoría de las versiones de PCRE se implementan \R
combinadas con la afirmación de fin de cadena $
(fin de línea en modo multilínea):
$ perl -pe 's/\R$/\n/' file | od -c
0000000 w h a t \r i s g o i n g o n \n
0000017
$ ruby -pe '$_.sub!(/\R$/,"\n")' file | od -c
0000000 w h a t \r i s g o i n g o n \n
0000017
(Tenga en cuenta que el \r
espacio entre las dos palabras se deja solo)
Si no lo tienes \R
puedes utilizar el equivalente de (?>\r\n|\v)
PCRE.
Con herramientas POSIX directas, su mejor opción probablemente sea awk
la siguiente:
$ awk '{sub(/\r$/,"")} 1' file | od -c
0000000 w h a t \r i s g o i n g o n \n
0000017
Cosas que funcionan (pero conoce tus limitaciones):
tr
elimina todo \r
incluso si se usa en otro contexto (dado que el uso de \r
es raro y el procesamiento XML requiere que \r
se elimine, por lo que tr
es una gran solución):
$ tr -d "\r" < file | od -c
0000000 w h a t i s g o i n g o n \n
0000016
GNU sed
funciona, pero no POSIX sed
desde entonces \r
y \x0D
no es compatible con POSIX.
Sólo GNU sed:
$ sed 's/\x0D//' file | od -c # also sed 's/\r//'
0000000 w h a t \r i s g o i n g o n \n
0000017
La Guía de expresiones regulares de Unicode es probablemente la mejor apuesta sobre cuál es el tratamiento definitivo de lo que es una "nueva línea".
Ejecute dos2unix . Si bien puedes manipular los finales de línea con código que hayas escrito tú mismo, existen utilidades en el mundo Linux/Unix que ya lo hacen por ti.
Si está en un sistema Fedora, dnf install dos2unix
colocará la dos2unix
herramienta en su lugar (en caso de que no esté instalada).
Hay un dos2unix
paquete deb similar disponible para sistemas basados en Debian.
Desde el punto de vista de la programación, la conversión es sencilla. Busque la secuencia en todos los caracteres de un archivo \r\n
y sustitúyala por \n
.
Esto significa que hay docenas de formas de convertir de DOS a Unix utilizando casi todas las herramientas imaginables. ¡Una forma sencilla es utilizar el comando tr
en el que simplemente se reemplaza \r
por nada!
tr -d '\r' < infile > outfile