¿Cómo puedo usar un archivo en un comando y redirigir la salida al mismo archivo sin truncarlo?


98

Básicamente, quiero tomar como entrada el texto de un archivo, eliminar una línea de ese archivo y enviar la salida al mismo archivo. Algo en esta línea si eso lo aclara.

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

sin embargo, cuando hago esto, termino con un archivo en blanco. ¿Alguna idea?


Respuestas:


84

No puede hacer eso porque bash procesa las redirecciones primero y luego ejecuta el comando. Entonces, cuando grep mira file_name, ya está vacío. Sin embargo, puede utilizar un archivo temporal.

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

así, considere usar mktemppara crear el tmpfile pero tenga en cuenta que no es POSIX.


47
La razón por la que no puede hacer eso: bash procesa las redirecciones primero y luego ejecuta el comando. Entonces, cuando grep mira file_name, ya está vacío.
Glenn Jackman

1
@glennjackman: por "redireccionamiento de procesos quieres decir que en el caso de> abre el archivo y lo borra y en el caso de >> solo lo abre"?
Razvan

2
sí, pero es de destacar que en esta situación, la >redirección abrirá el archivo y lo truncará antes de que se inicie el shell grep.
Glenn Jackman

1
Vea mi respuesta si no desea utilizar un archivo temporal, pero no vote a favor de este comentario.
Zack Morris

En lugar de esto, se debe aceptar la respuesta que usa el spongecomando .
vlz

96

Utilice esponja para este tipo de tareas. Es parte de moreutils.

Prueba este comando:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name

4
Gracias por la respuesta. Como una adición posiblemente útil, si está usando homebrew en Mac, puede usar brew install moreutils.
Anthony Panozzo

2
O sudo apt-get install moreutilsen sistemas basados ​​en Debian.
Jonás

3
¡Maldición! Gracias por presentarme moreutils =) ¡algunos buenos programas allí!
netigger

muchas gracias, moreutils por el rescate! esponja como un jefe!
aqquadro

3
Una advertencia, "esponja" es destructiva, por lo que si tiene un error en su comando, puede borrar su archivo de entrada (como hice la primera vez que probé la esponja). Asegúrese de que su comando funcione y / o que el archivo de entrada esté bajo control de versión si está intentando iterar para que funcione el comando.
user107172

18

Utilice sed en su lugar:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name

1
iirc -ies solo una extensión de GNU, solo notando.
c00kiemon5ter

3
En * BSD (y por lo tanto también OSX) puede decir -i ''que la extensión no es estrictamente obligatoria, pero la -iopción requiere algún argumento.
tripleee

14

prueba este simple

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

Su archivo no estará en blanco esta vez :) y su salida también se imprimirá en su terminal.


1
¡Me gusta esta solución! Y si no desea que se imprima en la terminal, aún puede redirigir la salida a /dev/nulllugares similares.
Frozn

4
Esto también borra el contenido del archivo aquí. ¿Eso se debe a una diferencia GNU / BSD? Estoy en macOS ...
ssc

7

No puede usar el operador de redirección ( >o >>) al mismo archivo, porque tiene una prioridad más alta y creará / truncará el archivo incluso antes de que se invoque el comando. Para evitar esto, usted debe utilizar las herramientas adecuadas, tales como tee, sponge, sed -io cualquier otra herramienta que puede escribir resultados en el archivo (por ejemplo sort file -o file).

Básicamente, redirigir la entrada al mismo archivo original no tiene sentido y debe usar los editores locales adecuados para eso, por ejemplo, Ex editor (parte de Vim):

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

dónde:

  • '+cmd'/ -c- ejecutar cualquier comando Ex / Vim
  • g/pattern/d- eliminar líneas que coincidan con un patrón usando global ( help :g)
  • -s- modo silencioso ( man ex)
  • -c wq- ejecutar :writey :quitcomandos

Usted puede utilizar sedpara lograr el mismo (como ya se ha mostrado en otras respuestas), sin embargo en el lugar ( -i) es la extensión de FreeBSD no estándar (pueden funcionar de forma diferente entre Unix / Linux) y básicamente se trata de un s TREAM ed itor, no un editor de archivos . Ver: ¿El modo Ex tiene algún uso práctico?


6

Una alternativa de revestimiento: establezca el contenido del archivo como variable:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name

4

Dado que esta pregunta es el resultado principal en los motores de búsqueda, aquí hay una sola línea basada en https://serverfault.com/a/547331 que usa una subcapa en lugar de sponge(que a menudo no es parte de una instalación básica como OS X) :

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

El caso general es:

echo "$(cat file_name)" > file_name

Editar, la solución anterior tiene algunas advertencias:

  • printf '%s' <string>debe usarse en lugar de echo <string>para que los archivos que contienen -nno causen un comportamiento no deseado.
  • La sustitución de comandos elimina las líneas nuevas al final ( esto es un error / característica de shells como bash ) por lo que deberíamos agregar un carácter de sufijo como xa la salida y eliminarlo en el exterior mediante la expansión de parámetros de una variable temporal como ${v%x}.
  • El uso de una variable temporal $vpisa fuerte el valor de cualquier variable existente $ven el entorno de shell actual, por lo que debemos anidar toda la expresión entre paréntesis para preservar el valor anterior.
  • Otro error / característica de shells como bash es que la sustitución de comandos elimina los caracteres no imprimibles como los nullde la salida. Verifiqué esto llamando dd if=/dev/zero bs=1 count=1 >> file_namey viéndolo en hexadecimal con cat file_name | xxd -p. Pero echo $(cat file_name) | xxd -pestá despojado. Por lo tanto, esta respuesta no debe usarse en archivos binarios o cualquier cosa que use caracteres no imprimibles, como señaló Lynch .

La solución general (aunque un poco más lenta, más memoria y aún eliminando caracteres no imprimibles) es:

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

Prueba de https://askubuntu.com/a/752451 :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Debería imprimir:

hello
world

Mientras que llamar cat file_uniquely_named.txt > file_uniquely_named.txtal shell actual:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Imprime una cadena vacía.

No lo he probado en archivos grandes (probablemente más de 2 o 4 GB).

He tomado prestada esta respuesta de Hart Simha y Kos .


2
Por supuesto, no funcionará con archivos grandes. Esto no puede ser una buena solución o funcionar todo el tiempo. Lo que está sucediendo es que bash ejecuta primero el comando y luego carga el stdout caty lo coloca como primer argumento de echo. Por supuesto, las variables no imprimibles no se generarán correctamente y dañarán los datos. No intente redirigir un archivo a sí mismo, simplemente no puede ser bueno.
Lynch

1

También hay ed(como alternativa a sed -i):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name

1

Puede hacerlo mediante la sustitución de procesos .

Sin embargo, es un truco, ya que bash abre todas las tuberías de forma asincrónica y tenemos que sleepsolucionarlo usando YMMV.

En tu ejemplo:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) crea un archivo temporal que recibe la salida de grep
  • sleep 1 se retrasa por un segundo para darle tiempo a grep para analizar el archivo de entrada
  • finalmente cat > file_nameescribe la salida

1

Puede usar slurp con POSIX Awk:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
  q = q ? q RS $0 : $0
}
END {
  print q > ARGV[1]
}

Ejemplo


1
Quizás debería señalarse que "slurp" significa "leer todo el archivo en la memoria". Si tiene un archivo de entrada grande, tal vez desee evitarlo.
tripleee

1

Esto es muy posible, solo tiene que asegurarse de que para cuando escriba la salida, la esté escribiendo en un archivo diferente. Esto se puede hacer eliminando el archivo después de abrirle un descriptor de archivo, pero antes de escribir en él:

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

O línea por línea, para entenderlo mejor:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

Todavía es algo arriesgado, porque si COMMAND no se ejecuta correctamente, perderá el contenido del archivo. Eso se puede mitigar restaurando el archivo si COMMAND devuelve un código de salida distinto de cero:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

También podemos definir una función de shell para que sea más fácil de usar:

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

Ejemplo:

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

Además, tenga en cuenta que esto mantendrá una copia completa del archivo original (hasta que se cierre el tercer descriptor de archivo). Si está utilizando Linux y el archivo en el que está procesando es demasiado grande para caber dos veces en el disco, puede consultar este script que canalizará el archivo al comando especificado bloque por bloque mientras desasigna el ya procesado bloques. Como siempre, lea las advertencias en la página de uso.


0

Prueba esto

echo -e "AAA\nBBB\nCCC" > testfile

cat testfile
AAA
BBB
CCC

echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC

Una breve explicación o incluso comentarios pueden resultar útiles.
Rich

Creo que funciona porque la extrapolación de cadenas se ejecuta antes del operador de redireccionamiento, pero no lo sé exactamente
Виктор Пупкин

0

Lo siguiente logrará lo mismo que spongehace, sin requerir moreutils:

    shuf --output=file --random-source=/dev/zero 

La --random-source=/dev/zeroparte engaña shufpara hacer lo suyo sin hacer ningún tipo de mezcla, por lo que almacenará en búfer su entrada sin alterarla.

Sin embargo, es cierto que lo mejor es utilizar un archivo temporal por motivos de rendimiento. Entonces, aquí hay una función que he escrito que lo hará por usted de manera generalizada:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.