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.
# 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
É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
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.
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 :-)
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
#!/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.
Il suffit d'ajouter le nombre d'occurrences à la fin:
sed s/#include/#include "newfile.h"\n#include/1
Une solution possible:
/#include/!{p;d;}
i\
#include "newfile.h"
:
n
b
Explication:
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 /.
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.
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
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
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.
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...
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.
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
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.
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|$)//
.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