web-dev-qa-db-fra.com

Comment utiliser sed pour ne remplacer que la première occurrence dans un fichier?

Je souhaite mettre à jour un grand nombre de fichiers source C++ avec une directive d'inclusion supplémentaire avant tout fichier #includes existant. Pour ce type de tâche, j'utilise normalement un petit script bash avec sed pour ré-écrire le fichier. 

Comment faire pour que sed ne remplace que la première occurrence d'une chaîne dans un fichier plutôt que de remplacer toutes les occurrences?

Si j'utilise

sed s/#include/#include "newfile.h"\n#include/

il remplace tous les #includes. 

Des suggestions alternatives pour atteindre le même objectif sont également les bienvenues.

173
David Dibben
 # 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---

ou, si vous préférez: Note de l'éditeur: fonctionne avec GNUsed uniquement.

sed '0,/RE/s//to_that/' file 

La source

108
Ben Hoffstein

Écrivez un script sed qui remplacera la première occurrence de "Apple" par "Banana"

Exemple d'entrée: Sortie:

     Apple       Banana
     Orange      Orange
     Apple       Apple

C'est le script simple: Note de l'éditeur: fonctionne avecGNUsed uniquement.

sed '0,/Apple/{s/Apple/Banana/}' filename
231
tim
sed '0,/pattern/s/pattern/replacement/' filename

cela a fonctionné pour moi.

exemple

sed '0,/<Menu>/s/<Menu>/<Menu><Menu>Sub menu<\/Menu>/' try.txt > abc.txt

Note de l'éditeur: les deux fonctionnent avecGNUsed uniquement.

50
Sushil

Vous pouvez utiliser awk pour faire quelque chose de similaire.

awk '/#include/ && !done { print "#include \"newfile.h\""; done=1;}; 1;' file.c

Explication:

/#include/ && !done

Exécute l'instruction action entre {} lorsque la ligne correspond à "#include" et que nous ne l'avons pas encore traitée.

{print "#include \"newfile.h\""; done=1;}

Ceci affiche #include "newfile.h", nous devons échapper aux guillemets. Ensuite, nous définissons la variable done à 1, nous n’ajoutons donc pas d’autres inclus.

1;

Cela signifie "imprimer la ligne" - une action vide permet d’imprimer $ 0 par défaut, ce qui permet d’imprimer la ligne entière. Un one-line et plus facile à comprendre que sed IMO :-)

22
richq

Une collection assez complète de réponses sur linuxtopia sed FAQ . Il souligne également que certaines réponses fournies par les utilisateurs ne fonctionneront pas avec une version de sed non-GNU, par exemple: 

sed '0,/RE/s//to_that/' file

dans une version non-GNU devra être 

sed -e '1s/RE/to_that/;t' -e '1,/RE/s//to_that/'

Cependant, cette version ne fonctionnera pas avec gnu sed. 

Voici une version qui fonctionne avec les deux:

-e '/RE/{s//to_that/;:a' -e '$!N;$!ba' -e '}'

ex:

sed -e '/Apple/{s//Banana/;:a' -e '$!N;$!ba' -e '}' filename
16
MikhailVS
#!/bin/sed -f
1,/^#include/ {
    /^#include/i\
#include "newfile.h"
}

Fonctionnement de ce script: Pour les lignes comprises entre 1 et le premier #include (après la ligne 1), si la ligne commence par #include, ajoutez ensuite la ligne spécifiée.

Toutefois, si le premier #include est à la ligne 1, la ligne 1 et le prochain #include suivant auront la ligne précédée. Si vous utilisez GNU sed, il a une extension où 0,/^#include/ (au lieu de 1,) fera le bon choix.

12
Chris Jester-Young

Il suffit d'ajouter le nombre d'occurrences à la fin:

sed s/#include/#include "newfile.h"\n#include/1
11
unexist

Une solution possible:

    /#include/!{p;d;}
    i\
    #include "newfile.h"
    :
    n
    b

Explication:

  • lire les lignes jusqu'à trouver le #include, imprimer ces lignes puis commencer un nouveau cycle
  • insère la nouvelle ligne d'inclusion
  • entrez une boucle qui ne lit que des lignes (par défaut, sed imprimera également ces lignes), nous ne reviendrons pas à la première partie du script à partir d'ici
8
mitchnull

Je sais que ceci est un ancien post mais j'avais une solution que j'utilisais auparavant:

grep -E -m 1 -n 'old' file | sed 's/:.*$//' - | sed 's/$/s\/old\/new\//' - | sed -f - file

En gros, utilisez grep pour trouver la première occurrence et vous y arrêter. Indiquez également le numéro de ligne, par exemple 5: ligne. Transférez-le dans sed et supprimez le: et tout ce qui suit afin de vous laisser un numéro de ligne. Pipe that dans sed qui ajoute s /.*/ replace à la fin, ce qui donne le script d’une ligne qui est acheminé vers le dernier fichier sed à exécuter en tant que script sur fichier.

ainsi, si regex = #include et replace = blah et que la première occurrence de grep find est sur la ligne 5, les données acheminées vers le dernier sed seraient alors 5s /.*/ blah /.

3
Michael Edwards

Si quelqu'un est venu ici pour remplacer un personnage pour la première occurrence dans toutes les lignes (comme moi), utilisez ceci:

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

En changeant 1 en 2 par exemple, vous pouvez remplacer tous les seconds a uniquement.

2
FatihSarigol

Comme autre suggestion, vous voudrez peut-être consulter la commande 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
2
timo

Utiliser FreeBSD ed et éviter l’erreur "aucune correspondance" de ed au cas où il n’y aurait pas d’instruction include dans un fichier: 

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
2
nazq

J'ai finalement réussi à utiliser cela dans un script Bash utilisé pour insérer un horodatage unique dans chaque élément d'un flux RSS:

        sed "1,/====RSSpermalink====/s/====RSSpermalink====/${nowms}/" \
            production-feed2.xml.tmp2 > production-feed2.xml.tmp.$counter

Cela change seulement la première occurrence. 

${nowms} est le temps en millisecondes défini par un script Perl, $counter est un compteur utilisé pour le contrôle de boucle dans le script, \ permet de poursuivre la commande sur la ligne suivante.

Le fichier est lu et stdout est redirigé vers un fichier de travail.

Si je comprends bien, 1,/====RSSpermalink====/ indique à sed quand arrêter en définissant une limite de plage, puis s/====RSSpermalink====/${nowms}/ est la commande sed habituelle permettant de remplacer la première chaîne par la seconde. 

Dans mon cas, je mets la commande entre guillemets parce que je l’utilise dans un script Bash avec des variables.

2
Michael Cook

Cela pourrait fonctionner pour vous (GNU sed):

sed -si '/#include/{s//& "newfile.h\n&/;:a;$!{n;ba}}' file1 file2 file....

ou si la mémoire n'est pas un problème:

sed -si ':a;$!{N;ba};s/#include/& "newfile.h\n&/' file1 file2 file...
2
potong

je le ferais avec un script awk:

BEGIN {i=0}
(i==0) && /#include/ {print "#include \"newfile.h\""; i=1}
{print $0}    
END {}

puis lancez-le avec awk:

awk -f awkscript headerfile.h > headerfilenew.h

peut-être négligé, je suis nouveau à cela.

2
wakingrufus

Rien de nouveau mais peut-être une réponse un peu plus concrète: sed -rn '0,/foo(bar).*/ s%%\1%p'

Exemple: xwininfo -name unity-launcher produit un résultat tel que:

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

L'extraction d'ID de fenêtre avec xwininfo -name unity-launcher|sed -rn '0,/^xwininfo: Window id: (0x[0-9a-fA-F]+).*/ s%%\1%p' produit:

0x2200003
0
Stephen Niedzielski

POSIXly (également valable dans sed), Only one regex utilisé, nécessite de la mémoire pour une seule ligne (comme d’habitude):

sed '/\(#include\).*/!b;//{h;s//\1 "newfile.h"/;G};:1;n;b1'

A expliqué:

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.
0
Isaac

Avec l'option -z de GNU sed, vous pouvez traiter le fichier entier comme s'il ne s'agissait que d'une ligne. De cette façon, un s/…/…/ ne remplacerait que la première correspondance dans tout le fichier. N'oubliez pas: s/…/…/ ne remplace que la première correspondance de chaque ligne, mais avec l'option -z, l'option sed traite le fichier entier comme une seule ligne.

sed -z 's/#include/#include "newfile.h"\n#include'

Dans le cas général, vous devez réécrire votre expression sed car l’espace-modèle contient maintenant tout le fichier au lieu d’une seule ligne. Quelques exemples:

  • s/text.*// peut être réécrit en s/text[^\n]*//. [^\n] correspond à tout sauf le caractère de nouvelle ligne. [^\n]* fera correspondre tous les symboles après text jusqu'à atteindre une nouvelle ligne.
  • s/^text// peut être réécrit en tant que s/(^|\n)text//.
  • s/text$// peut être réécrit en tant que s/text(\n|$)//.
0
Socowi

La commande suivante supprime la première occurrence d'une chaîne, dans un fichier. Il supprime aussi la ligne vide. Il est présenté sur un fichier XML, mais cela fonctionnerait avec n'importe quel fichier. 

Utile si vous travaillez avec des fichiers XML et que vous souhaitez supprimer une balise. Dans cet exemple, il supprime la première occurrence de la balise "isTag".

Commander: 

sed -e 0,/'<isTag>false<\/isTag>'/{s/'<isTag>false<\/isTag>'//}  -e 's/ *$//' -e  '/^$/d'  source.txt > output.txt

Fichier source (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>

Fichier de résultat (output.txt)

<xml>
    <testdata>
        <canUseUpdate>true</canUseUpdate>
        <moduleLocations>
            <module>esa_jee6</module>
            <isTag>false</isTag>
        </moduleLocations>
        <node>
            <isTag>false</isTag>
        </node>
    </testdata>
</xml>

ps: cela ne fonctionnait pas sous Solaris SunOS 5.10 (assez ancien), mais sous Linux 2.6, sed version 4.1.5

0