¿Por qué la salida de mi herramienta se sobrescribe y cómo lo soluciono?

Resuelto Ed Morton asked hace 7 años • 3 respuestas

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?

Ed Morton avatar Aug 19 '17 21:08 Ed Morton
Aceptado

El problema es que su archivo de entrada usa finales de línea de DOS de CRLFen lugar de finales de línea de UNIX de just LF, y está ejecutando una herramienta UNIX en él, por lo que CRsigue siendo parte de los datos que opera la herramienta UNIX. CRcomúnmente se denota por \ry puede verse como control-M ( ^M) cuando se ejecuta cat -vEen el archivo, mientras que LFes \ny 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 \ncomo el final de línea) en el archivo, se \nconsume por el acto de leer la línea, pero eso deja los 2 campos como:

<what> <isgoingon\r>

Tenga en cuenta el \ral final del segundo campo. \rsignifica 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á isgoingony devolverá el cursor al inicio de la línea antes de imprimir un espacio seguido de what, razón por la cual whatparece 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 dos2unixes también conocido como fromdosen 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 RSapropiadamente:

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=3gawk para ver incluso los \rs, 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 CRLFcomo finales de línea, pero pueden tener LFmensajes 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\ns en \ns, 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 LFsa pestañas y convierte todas las líneas que terminan en s CRLFa LFs:

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 CRcuando 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$
Ed Morton avatar Aug 19 '2017 14:08 Ed Morton

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 \Rformulario 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/gpara capturar cualquier noción de "final de línea" y estandarizarla en un \ncará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 \Rcombinadas 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 \respacio entre las dos palabras se deja solo)

Si no lo tienes \Rpuedes utilizar el equivalente de (?>\r\n|\v)PCRE.

Con herramientas POSIX directas, su mejor opción probablemente sea awkla 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):

trelimina todo \rincluso si se usa en otro contexto (dado que el uso de \res raro y el procesamiento XML requiere que \rse elimine, por lo que tres 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 sedfunciona, pero no POSIX seddesde entonces \ry \x0Dno 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".

dawg avatar Aug 19 '2017 16:08 dawg

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 dos2unixcolocará la dos2unixherramienta en su lugar (en caso de que no esté instalada).

Hay un dos2unixpaquete 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\ny 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 tren el que simplemente se reemplaza \rpor nada!

tr -d '\r' < infile > outfile
Edwin Buck avatar Aug 19 '2017 14:08 Edwin Buck