¿Cómo usar sed para reemplazar solo la primera aparición en un archivo?
-
02-07-2019 - |
Pregunta
Me gustaría actualizar una gran cantidad de archivos fuente de C++ con una directiva de inclusión adicional antes de cualquier #include existente.Para este tipo de tarea, normalmente uso un pequeño script bash con sed para reescribir el archivo.
Como lo consigo sed
¿Reemplazar solo la primera aparición de una cadena en un archivo en lugar de reemplazar cada aparición?
si uso
sed s/#include/#include "newfile.h"\n#include/
reemplaza todos los #incluye.
También se aceptan sugerencias alternativas para lograr el mismo objetivo.
Solución
# sed script to change "foo" to "bar" only on the first occurrence
1{x;s/^/first/;x;}
1,/foo/{x;/first/s///;x;s/foo/bar/;}
#---end of script---
o, si lo prefiere: Nota del editor: funciona solo con GNU sed
.
sed '0,/RE/s//to_that/' file
Otros consejos
Escriba un script sed que solo reemplace la primera aparición de " Apple " por " Banana "
Ejemplo de entrada: Salida:
Apple Banana
Orange Orange
Apple Apple
Este es el guión simple: Nota del editor: funciona solo con GNU sed
.
sed '0,/Apple/{s/Apple/Banana/}' filename
sed '0,/pattern/s/pattern/replacement/' filename
esto funcionó para mí.
ejemplo
sed '0,/<Menu>/s/<Menu>/<Menu><Menu>Sub menu<\/Menu>/' try.txt > abc.txt
Nota del editor: ambos funcionan solo con GNU sed
.
Una descripción general de las muchas respuestas existentes útiles , complementadas con explicaciones :
Los ejemplos aquí usan un caso de uso simplificado: reemplace la palabra 'foo' con 'bar' solo en la primera línea coincidente.
Debido al uso de cadenas con comillas C ANSI ($'...'
) para proporcionar las líneas de entrada de muestra, se asume bash
, ksh
o zsh
como el shell.
GNU sed
solamente:
La respuesta de Ben Hoffstein nos muestra que GNU proporciona una extensión a especificación POSIX para 0,/re/
que permite el siguiente formulario de 2 direcciones: re
(1,/re/
representa una expresión regular arbitraria aquí).
//
permite que la expresión regular coincida con en la primera línea también . En otras palabras: dicha dirección creará un rango desde la primera línea hasta e incluyendo la línea que coincida con s/.../.../
, ya sea s
en la primera línea o en cualquier línea posterior.
- Compare esto con el formulario compatible con POSIX
foo
, que crea un rango que coincide desde la primera línea hasta e incluye la línea que coincide cont
en posterior líneas; en otras palabras: este no detectará la primera aparición de una-e
coincidencia si ocurre en la primera línea y también evita el uso de la taquigrafía1 s/foo/bar/
para reutilizar la expresión regular utilizada más recientemente (ver punto siguiente). [1]
Si combina una dirección 1,//
con una llamada 2
(sustitución) que usa la expresión regular same , su comando efectivamente solo realizará la sustitución en el primero línea que coincide con s//
.
sed '1,/foo/ s/foo/bar/' <<<$'1foo\n2foo'
proporciona un práctico acceso directo para reutilizar la expresión regular aplicada más recientemente : un par delimitador vacío , $'1bar\n2bar'
.
$ sed '0,/foo/ s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo
Un 1
solo con funciones POSIX como BSD (macOS) /foo/
(también funcionará con GNU s/foo/bar/
):
Dado que sed '1,/foo/ s//bar/' <<<$'1foo\n2foo\n3foo'
no se puede usar y el formulario sed: first RE may not be empty
no detectará sed: -e expression #1, char 0: no previous regular expression
si ocurre en la primera línea (ver arriba), se requiere manejo especial para la primera línea .
La respuesta de MikhailVS menciona la técnica, puesta en un ejemplo concreto aquí:
$ sed -e '1 s/foo/bar/; t' -e '1,// s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo
Nota:
-
El acceso directo vacío regex <=> se emplea dos veces aquí: una para el punto final del rango y otra en la llamada <=>; en ambos casos, regex <=> se reutiliza implícitamente, lo que nos permite no tener que duplicarlo, lo que hace que el código sea más corto y más fácil de mantener.
-
POSIX <=> necesita nuevas líneas reales después de ciertas funciones, como después del nombre de una etiqueta o incluso su omisión, como es el caso con <=> aquí; dividir estratégicamente el script en múltiples opciones de <=> es una alternativa al uso de una nueva línea real: finalice cada <=> fragmento del script donde normalmente debería ir una nueva línea.
<=> reemplaza <=> solo en la primera línea, si se encuentra allí. Si es así, <=> se bifurca al final del script (omite los comandos restantes en la línea). (La función <=> se ramifica a una etiqueta solo si la llamada <=> más reciente realizó una sustitución real; en ausencia de una etiqueta, como es el caso aquí, el final de la secuencia de comandos se ramifica).
Cuando eso sucede, la dirección de rango <=>, que normalmente encuentra la primera aparición a partir de la línea 2 , no coincidirá, y el rango no se procesará, porque la dirección se evalúa cuando la línea actual ya está <=>.
Por el contrario, si no hay una coincidencia en la primera línea, se ingresará <=> y terminarád el verdadero primer partido.
El efecto neto es el mismo que con GNU <=> 's <=>: solo se reemplaza la primera aparición, ya sea que ocurra en la primera línea o en cualquier otra.
Enfoques que no son de rango
la respuesta de potong demuestra loop técnicas que evitar la necesidad de un rango ; como usa la sintaxis GNU <=>, aquí están los equivalentes que cumplen con POSIX :
Técnica de bucle 1: en la primera coincidencia, realice la sustitución, luego ingrese un bucle que simplemente imprima las líneas restantes tal como están :
$ sed -e '/foo/ {s//bar/; ' -e ':a' -e '$!{n;ba' -e '};}' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo
Técnica de bucle 2, solo para archivos pequeños : lea toda la entrada en la memoria, luego realice una única sustitución .
$ sed -e ':a' -e '$!{N;ba' -e '}; s/foo/bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo
[1] 1.61803 proporciona ejemplos de lo que sucede con <=>, con y sin un <=> siguiente:
- <=> produce <=>; es decir, ambas líneas se actualizaron, porque el número de línea <=> coincide con la primera línea, y la expresión regular <=>, el final del rango, solo se busca a partir del siguiente línea. Por lo tanto, se seleccionan ambas líneas en este caso, y la sustitución <=> se realiza en ambas.
- <=> falla : con <=> (BSD / macOS) y <=> (GNU), porque, en el momento en que se procesa la primera línea (debido al número de línea <=> comenzando el rango), aún no se ha aplicado expresión regular, por lo que <=> no se refiere a nada.
Con la excepción de la sintaxis especial <=> de GNU <=>, cualquier rango que comienza con un número de línea impide efectivamente el uso de <=>.
Podría usar awk para hacer algo similar ...
awk '/#include/ && !done { print "#include \"newfile.h\""; done=1;}; 1;' file.c
Explicación:
/#include/ && !done
Ejecuta la instrucción de acción entre {} cuando la línea coincide con " #include " y aún no lo hemos procesado.
{print "#include \"newfile.h\""; done=1;}
Esto imprime #include " newfile.h " ;, necesitamos escapar de las comillas. Luego establecemos la variable done en 1, por lo que no agregamos más inclusiones.
1;
Esto significa " imprime la línea " - una acción vacía por defecto imprime $ 0, que imprime toda la línea. Una línea y más fácil de entender que sed IMO :-)
Una colección bastante completa de respuestas en linuxtopia sed FAQ . También destaca que algunas respuestas que las personas proporcionaron no funcionarán con una versión de sed que no sea GNU, por ejemplo,
sed '0,/RE/s//to_that/' file
en una versión que no sea GNU tendrá que ser
sed -e '1s/RE/to_that/;t' -e '1,/RE/s//to_that/'
Sin embargo, esta versión no funcionará con gnu sed.
Aquí hay una versión que funciona con ambos:
-e '/RE/{s//to_that/;:a' -e '$!N;$!ba' -e '}'
ex:
sed -e '/Apple/{s//Banana/;:a' -e '$!N;$!ba' -e '}' filename
Simplemente agregue el número de ocurrencias al final:
sed s/#include/#include "newfile.h"\n#include/1
#!/bin/sed -f
1,/^#include/ {
/^#include/i\
#include "newfile.h"
}
Cómo funciona este script: para las líneas entre 1 y la primera #include
(después de la línea 1), si la línea comienza con sed
, añada la línea especificada.
Sin embargo, si el primer 0,/^#include/
está en la línea 1, entonces la línea 1 y el siguiente 1,
tendrán la línea antepuesta. Si está utilizando GNU <=>, tiene una extensión donde <=> (en lugar de <=>) hará lo correcto.
Una posible solución:
/#include/!{p;d;}
i\
#include "newfile.h"
:
n
b
Explicación:
- lee las líneas hasta encontrar el #include, imprime estas líneas y luego comienza un nuevo ciclo
- inserte la nueva línea de inclusión
- ingrese un bucle que solo lea líneas (de forma predeterminada, sed también imprimirá estas líneas), no volveremos a la primera parte del script desde aquí
Sé que esta es una publicación antigua pero tenía una solución que solía usar:
grep -E -m 1 -n 'old' file | sed 's/:.*$//' - | sed 's/$/s\/old\/new\//' - | sed -f - file
Básicamente usa grep para encontrar la primera ocurrencia y detente allí. También imprima el número de línea, es decir, 5: línea. Canalice eso en sed y elimine el: y cualquier cosa posterior para que quede un número de línea. Canalice eso en sed que agrega s /.*/ replace al final, lo que da un script de 1 línea que se canaliza en el último sed para ejecutarse como script en el archivo.
así que si regex = #include y replace = blah y la primera aparición grep encuentra en la línea 5, entonces los datos canalizados al último sed serían 5s /.*/ blah /.
Si alguien vino aquí para reemplazar un personaje por primera vez en todas las líneas (como yo), use esto:
sed '/old/s/old/new/1' file
-bash-4.2$ cat file
123a456a789a
12a34a56
a12
-bash-4.2$ sed '/a/s/a/b/1' file
123b456a789a
12b34a56
b12
Al cambiar 1 a 2, por ejemplo, puede reemplazar todas las segundas solo en su lugar.
haría esto con un script awk:
BEGIN {i=0}
(i==0) && /#include/ {print "#include \"newfile.h\""; i=1}
{print $0}
END {}
luego ejecútelo con awk:
awk -f awkscript headerfile.h > headerfilenew.h
puede ser descuidado, soy nuevo en esto.
Como sugerencia alternativa, puede consultar el comando ed
.
man 1 ed
teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'
# for in-place file editing use "ed -s file" and replace ",p" with "w"
# cf. http://wiki.bash-hackers.org/howto/edit-ed
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
H
/# *include/i
#include "newfile.h"
.
,p
q
EOF
Finalmente conseguí que esto funcionara en una secuencia de comandos Bash utilizada para insertar una marca de tiempo única en cada elemento en una fuente RSS:
sed "1,/====RSSpermalink====/s/====RSSpermalink====/${nowms}/" \
production-feed2.xml.tmp2 > production-feed2.xml.tmp.$counter
Solo cambia la primera aparición.
${nowms}
es el tiempo en milisegundos establecido por un script Perl, $counter
es un contador utilizado para el control de bucle dentro del script, \
permite que el comando continúe en la siguiente línea.
El archivo se lee y stdout se redirige a un archivo de trabajo.
Según tengo entendido, 1,/====RSSpermalink====/
le dice a sed cuándo detenerse estableciendo una limitación de rango, y luego s/====RSSpermalink====/${nowms}/
es el comando familiar sed para reemplazar la primera cadena por la segunda.
En mi caso, pongo el comando entre comillas dobles porque lo estoy usando en un script Bash con variables.
Usando FreeBSD ed
y evitar ed
El error "no coincide" en caso de que no haya include
declaración en un archivo a procesar:
teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'
# using FreeBSD ed
# to avoid ed's "no match" error, see
# *emphasized text*http://codesnippets.joyent.com/posts/show/11917
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
H
,g/# *include/u\
u\
i\
#include "newfile.h"\
.
,p
q
EOF
Esto podría funcionar para usted (GNU sed):
sed -si '/#include/{s//& "newfile.h\n&/;:a;$!{n;ba}}' file1 file2 file....
o si la memoria no es un problema:
sed -si ':a;$!{N;ba};s/#include/& "newfile.h\n&/' file1 file2 file...
Con la opción -z
de GNU sed, puede procesar todo el archivo como si fuera solo una línea. De esa forma, un s/…/…/
solo reemplazaría la primera coincidencia en todo el archivo. Recuerde: sed
solo reemplaza la primera coincidencia en cada línea, pero con la opción s/text.*//
s/text[^\n]*//
trata el archivo completo como una sola línea.
sed -z 's/#include/#include "newfile.h"\n#include'
En el caso general, debe reescribir su expresión sed ya que el espacio del patrón ahora contiene todo el archivo en lugar de solo una línea. Algunos ejemplos:
-
[^\n]
puede reescribirse como[^\n]*
.text
coincide con todo excepto el carácter de nueva línea.s/^text//
coincidirá con todos los símbolos después des/(^|\n)text//
hasta que se llegue a una nueva línea. -
s/text$//
puede reescribirse comos/text(\n|$)//
. - <=> puede reescribirse como <=>.
El siguiente comando elimina la primera aparición de una cadena, dentro de un archivo. También elimina la línea vacía. Se presenta en un archivo xml, pero funcionaría con cualquier archivo.
Útil si trabaja con archivos xml y desea eliminar una etiqueta. En este ejemplo, elimina la primera aparición de & Quot; isTag & Quot; etiqueta.
Comando:
sed -e 0,/'<isTag>false<\/isTag>'/{s/'<isTag>false<\/isTag>'//} -e 's/ *$//' -e '/^$/d' source.txt > output.txt
Archivo fuente (source.txt)
<xml>
<testdata>
<canUseUpdate>true</canUseUpdate>
<isTag>false</isTag>
<moduleLocations>
<module>esa_jee6</module>
<isTag>false</isTag>
</moduleLocations>
<node>
<isTag>false</isTag>
</node>
</testdata>
</xml>
Archivo de resultados (output.txt)
<xml>
<testdata>
<canUseUpdate>true</canUseUpdate>
<moduleLocations>
<module>esa_jee6</module>
<isTag>false</isTag>
</moduleLocations>
<node>
<isTag>false</isTag>
</node>
</testdata>
</xml>
ps: no funcionó para mí en Solaris SunOS 5.10 (bastante antiguo), pero funciona en Linux 2.6, sed versión 4.1.5
Nada nuevo, pero quizás una respuesta un poco más concreta: sed -rn '0,/foo(bar).*/ s%%\1%p'
Ejemplo: xwininfo -name unity-launcher
produce resultados como:
xwininfo: Window id: 0x2200003 "unity-launcher"
Absolute upper-left X: -2980
Absolute upper-left Y: -198
Relative upper-left X: 0
Relative upper-left Y: 0
Width: 2880
Height: 98
Depth: 24
Visual: 0x21
Visual Class: TrueColor
Border width: 0
Class: InputOutput
Colormap: 0x20 (installed)
Bit Gravity State: ForgetGravity
Window Gravity State: NorthWestGravity
Backing Store State: NotUseful
Save Under State: no
Map State: IsViewable
Override Redirect State: no
Corners: +-2980+-198 -2980+-198 -2980-1900 +-2980-1900
-geometry 2880x98+-2980+-198
La extracción de la ID de la ventana con xwininfo -name unity-launcher|sed -rn '0,/^xwininfo: Window id: (0x[0-9a-fA-F]+).*/ s%%\1%p'
produce:
0x2200003
POSIXly (también válido en sed), solo se usa una expresión regular, necesita memoria solo para una línea (como es habitual):
sed '/\(#include\).*/!b;//{h;s//\1 "newfile.h"/;G};:1;n;b1'
Explicado:
sed '
/\(#include\).*/!b # Only one regex used. On lines not matching
# the text `#include` **yet**,
# branch to end, cause the default print. Re-start.
//{ # On first line matching previous regex.
h # hold the line.
s//\1 "newfile.h"/ # append ` "newfile.h"` to the `#include` matched.
G # append a newline.
} # end of replacement.
:1 # Once **one** replacement got done (the first match)
n # Loop continually reading a line each time
b1 # and printing it by default.
' # end of sed script.