Sed - Remplace les k premières instances d'un mot dans le fichier

24

Je veux remplacer uniquement les premières kinstances d'un mot.

Comment puis-je faire ceci?

Par exemple. Le fichier Say foo.txtcontient 100 occurrences d'occurrences du mot «linux».

Je dois remplacer seulement les 50 premières occurrences.

narendra-choudhary
la source
1
Vous pouvez vous y référer: unix.stackexchange.com/questions/21178/…
cuonglm
Avez-vous besoin de sed spécifiquement ou d'autres outils sont-ils acceptables? Avez-vous besoin de travailler sur la ligne de commande ou un éditeur de texte est-il acceptable?
evilsoup
Tout ce qui fonctionne sur la ligne de commande est acceptable.
narendra-choudhary

Réponses:

31

La première section ci-dessous décrit l'utilisation sedde la modification des premières occurrences k sur une ligne. La deuxième section étend cette approche pour modifier uniquement les premières occurrences k d'un fichier, quelle que soit la ligne sur laquelle elles apparaissent.

Solution orientée ligne

Avec sed standard, il existe une commande pour remplacer la k-ème occurrence d'un mot sur une ligne. Si kest 3, par exemple:

sed 's/old/new/3'

Ou, on peut remplacer toutes les occurrences par:

sed 's/old/new/g'

Ni l'un ni l'autre n'est ce que vous voulez.

GNU sedpropose une extension qui changera la k-ème occurrence et tout cela après. Si k est 3, par exemple:

sed 's/old/new/g3'

Ceux-ci peuvent être combinés pour faire ce que vous voulez. Pour modifier les 3 premières occurrences:

$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old

\nest utile ici car nous pouvons être sûrs qu'il ne se produit jamais sur une ligne.

Explication:

Nous utilisons trois sedcommandes de substitution:

  • s/\<old\>/\n/g4

    C'est l'extension GNU pour remplacer la quatrième et toutes les occurrences suivantes de oldwith \n.

    La fonction d'expression régulière étendue \<est utilisée pour faire correspondre le début d'un mot et \>pour correspondre à la fin d'un mot. Cela garantit que seuls les mots complets correspondent. L'expression regex étendue nécessite l' -Eoption de sed.

  • s/\<old\>/new/g

    Seules les trois premières occurrences de oldrestent et cela les remplace toutes par new.

  • s/\n/old/g

    La quatrième et toutes les occurrences restantes de oldont été remplacées par \ndans la première étape. Cela les ramène à leur état d'origine.

Solution non GNU

Si GNU sed n'est pas disponible et que vous souhaitez modifier les 3 premières occurrences de olden new, utilisez trois scommandes:

$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old

Cela fonctionne bien quand kest un petit nombre mais évolue mal à grand k.

Étant donné que certains seds non GNU ne prennent pas en charge la combinaison de commandes avec des points-virgules, chaque commande ici est introduite avec sa propre -eoption. Il peut également être nécessaire de vérifier que votre sedprend en charge les symboles de limite de mot, \<et \>.

Solution orientée fichier

Nous pouvons dire à sed de lire l'intégralité du fichier puis d'effectuer les substitutions. Par exemple, pour remplacer les trois premières occurrences de l' oldutilisation d'un sed de style BSD:

sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'

Les commandes sed H;1h;$!d;xlisent l'intégralité du fichier.

Étant donné que ce qui précède n'utilise aucune extension GNU, il devrait fonctionner sur BSD (OSX) sed. Notez, pensait, que cette approche nécessite un sedqui peut gérer les longues lignes. GNU seddevrait aller bien. Ceux qui utilisent une version non GNU de seddevraient tester sa capacité à gérer les longues lignes.

Avec un sed GNU, nous pouvons continuer à utiliser l' gastuce décrite ci-dessus, mais avec \nremplacé par \x00, pour remplacer les trois premières occurrences:

sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'

Cette approche évolue bien et kdevient grande. Cela suppose, cependant, que ce \x00n'est pas dans votre chaîne d'origine. Puisqu'il est impossible de mettre le caractère \x00dans une chaîne bash, c'est généralement une hypothèse sûre.

John1024
la source
5
Cela ne fonctionne que pour les lignes et modifiera les 4 premières occurrences de chaque ligne
1
@mikeserv Excellente idée! Réponse mise à jour.
John1024
(1) Vous mentionnez GNU et non-GNU sed, et suggérez tr '\n' '|' < input_file | sed …. Mais, bien sûr, cela convertit l'intégralité de l'entrée en une seule ligne, et certains seds non GNU ne peuvent pas gérer des lignes arbitrairement longues. (2) Vous dites: «… ci-dessus, la chaîne entre guillemets '|'doit être remplacée par n'importe quel caractère, ou chaîne de caractères,…» Mais vous ne pouvez pas utiliser trpour remplacer un caractère par une chaîne (de longueur> 1). (3) Dans votre dernier exemple, vous dites -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Cela semble être une faute de frappe pour -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man dit `` Réintègre Monica '' le
@ G-Man Merci beaucoup! J'ai mis à jour la réponse.
John1024
c'est si moche
Louis Maddox
8

Utiliser Awk

Les commandes awk peuvent être utilisées pour remplacer les N premières occurrences du mot par le remplacement.
Les commandes ne remplaceront que si le mot est une correspondance complète.

Dans les exemples ci-dessous, je remplace les premières 27occurrences de oldparnew

Utilisation de sous

awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file

Cette commande parcourt chaque champ jusqu'à ce qu'elle corresponde old, vérifie que le compteur est inférieur à 27, incrémente et remplace la première correspondance de la ligne. Passe ensuite au champ / ligne suivant et répète.

Remplacement manuel du champ

awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Semblable à la commande précédente, mais comme elle possède déjà un marqueur sur le champ auquel elle appartient ($i), elle modifie simplement la valeur du champ de oldà new.

Effectuer une vérification avant

awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Vérifier que la ligne contient des anciens et que le compteur est inférieur à 27 SHOULDfournit une petite augmentation de vitesse car elle ne traitera pas les lignes lorsqu'elles sont fausses.

RÉSULTATS

Par exemple

old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old

à

new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old
Jeff Schaller
la source
Le premier (en utilisant sub) fait la mauvaise chose si la chaîne «old» précède le mot * old; par exemple, "Donner de l'or au vieil homme." → "Donner un peu de gnew au vieil homme."
G-Man dit 'Reinstate Monica'
@ G-Man Oui, j'ai oublié le $ibit, il a été modifié, merci :)
7

Supposons que vous souhaitiez remplacer uniquement les trois premières instances d'une chaîne ...

seq 11 100 311 | 
sed -e 's/1/\
&/g'              \ #s/match string/\nmatch string/globally 
-e :t             \ #define label t
-e '/\n/{ x'      \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{'   \ #if not 3 characters in hold space do
-e     's/$/./'   \ #add a new char to hold space
-e      x         \ #exchange hold/pattern spaces again
-e     's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e     'b t'      \ #branch back to label t
-e '};x'          \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g'      #end match function; remove all newline characters

note: ce qui précède ne fonctionnera probablement pas avec des commentaires intégrés
... ou dans mon cas d'exemple, d'un '1' ...

SORTIE:

22
211
211
311

Là, j'utilise deux techniques notables. En premier lieu, chaque occurrence de 1sur une ligne est remplacée par \n1. De cette façon, comme je fais les remplacements récursifs ensuite, je peux être sûr de ne pas remplacer l'occurrence deux fois si ma chaîne de remplacement contient ma chaîne de remplacement. Par exemple, si je remplace heparhey cela fonctionnera.

Je fais ça comme:

s/1/\
&/g

Deuxièmement, je compte les remplacements en ajoutant un caractère à l' hancien espace pour chaque occurrence. Une fois que j'aurai atteint trois, il ne se passera plus. Si vous appliquez cela à vos données et changez le \{3\}nombre total de remplacements que vous désirez et les /\n1/adresses en tout ce que vous voulez remplacer, vous ne devez remplacer que le nombre que vous souhaitez.

Je n'ai fait que toutes les -echoses pour la lisibilité. POSIX Il pourrait être écrit comme ceci:

nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"

Et avec GNU sed:

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'

Souvenez-vous également qu'il sedest orienté ligne - il ne lit pas dans tout le fichier et essaie ensuite de le boucler comme c'est souvent le cas dans d'autres éditeurs. sedest simple et efficace. Cela dit, il est souvent pratique de faire quelque chose comme ceci:

Voici une petite fonction shell qui la regroupe en une commande simplement exécutée:

firstn() { sed "s/$2/\
&/g;:t 
    /\n/{x
        /.\{$(($1))"',\}/!{
            s/$/./; x; s/\n'"$2/$3"'/
            b t
        };x
};s/\n//g'; }

Donc avec ça je peux faire:

seq 11 100 311 | firstn 7 1 5

...et obtenir...

55
555
255
311

...ou...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'

...obtenir...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25

... ou, pour correspondre à votre exemple (sur un ordre de grandeur plus petit) :

yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux
mikeserv
la source
4

Une courte alternative en Perl:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file

Modifiez la valeur de `$ n $ à votre guise.

Comment ça marche:

  • Pour chaque ligne, il continue d'essayer de se substituer newà old( s/old/new/) et chaque fois qu'il le peut, il incrémente la variable $i( ++$i).
  • Il continue de travailler sur la ligne ( 1 while ...) tant qu'il a effectué moins de $nsubstitutions au total et qu'il peut effectuer au moins une substitution sur cette ligne.
Joseph R.
la source
4

Utilisez une boucle shell et ex!

{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt

Oui, c'est un peu maladroit.

;)

Remarque: cela peut échouer s'il y a moins de 50 instances de olddans le fichier. (Je ne l'ai pas testé.) Si c'est le cas, le fichier ne sera pas modifié.


Mieux encore, utilisez Vim.

vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x

Explication:

q                                # Start recording macro
 q                               # Into register q
  gg                             # Go to start of file
    /old<CR>                     # Go to first instance of 'old'
            :s/old/new/<CR>      # Change it to 'new'
                           q     # Stop recording
                            49@q # Replay macro 49 times

:x  # Save and exit
Caractère générique
la source
: s // new <CR> devrait également fonctionner, car une expression rationnelle vide réutilise la dernière recherche utilisée
comme
3

Une solution simple, mais pas très rapide, consiste à parcourir les commandes décrites dans /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -fichier

for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/"  file.txt  ; done

Cette commande sed particulière ne fonctionne probablement que pour GNU sed et si newword ne fait pas partie de oldword . Pour les sed non GNU, voyez ici comment remplacer uniquement le premier pattern d'un fichier.

jofel
la source
+1 pour identifier que le remplacement de "old" par "bold" peut causer des problèmes.
G-Man dit `` Réintègre Monica '' le
2

Avec GNU, awkvous pouvez définir le séparateur d'enregistrement RSsur le mot à remplacer délimité par des limites de mot. Ensuite, il s'agit de définir le séparateur d'enregistrement sur la sortie sur le mot de remplacement pour les premiers kenregistrements tout en conservant le séparateur d'enregistrement d'origine pour le reste

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, NR <= limit? replacement: RT}' file

OU

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
iruvar
la source