Modifier plusieurs fichiers

194

La commande suivante modifie correctement le contenu de 2 fichiers.

sed -i 's/abc/xyz/g' xaa1 xab1 

Mais ce que je dois faire est de changer dynamiquement plusieurs de ces fichiers et je ne connais pas les noms de fichiers. Je veux écrire une commande qui lira tous les fichiers du répertoire courant en commençant par xa*et seddevrait changer le contenu du fichier.

shantanuo
la source
63
Tu veux dire sed -i 's/abc/xyz/g' xa*?
Paul R
3
Les réponses ici ne suffisent pas. Voir unix.stackexchange.com/questions/112023/…
Isaac

Réponses:

136

Mieux encore:

for i in xa*; do
    sed -i 's/asd/dfg/g' $i
done

car personne ne sait combien de fichiers il y a, et il est facile de dépasser les limites de la ligne de commande.

Voici ce qui se passe lorsqu'il y a trop de fichiers:

# grep -c aaa *
-bash: /bin/grep: Argument list too long
# for i in *; do grep -c aaa $i; done
0
... (output skipped)
#
Lenik
la source
19
S'il y a autant de fichiers, vous dépasserez la limite de ligne de commande dans la forcommande. Pour vous protéger de cela, vous devez utiliserfind ... | xargs ...
glenn jackman
1
Je ne connais pas l'implémentation, mais le modèle "xa *" doit être développé à un moment donné. Le shell effectue-t-il l'expansion différemment forpour echoou pour ou grep?
glenn jackman
4
voir la réponse mise à jour. si vous avez besoin de plus d'informations, posez une question officielle afin que les gens puissent vous aider.
lenik
5
Dans la commande sed, vous devez utiliser "$i"au lieu de $ipour éviter le fractionnement de mots sur les noms de fichiers avec des espaces. Sinon c'est très sympa.
Wildcard
4
En ce qui concerne la liste, je pense que la différence est que cela forfait partie de la syntaxe du langage, pas même simplement une fonction intégrée. Car sed -i 's/old/new' *, l'expansion de *doit ALL être passée en tant qu'arglist à sed, et je suis à peu près sûr que cela doit se produire avant même que le sedprocessus puisse être lancé. En utilisant la forboucle, l'arglist complet (l'expansion de *) n'est jamais passé en tant que commande, seulement stocké dans la mémoire du shell et itéré. Je n'ai aucune référence à ce sujet cependant, il semble juste que ce soit la différence. (J'adorerais entendre quelqu'un de plus compétent ...)
Wildcard
167

Je suis surpris que personne n'ait mentionné l'argument -exec to find, qui est destiné à ce type de cas d'utilisation, bien qu'il lancera un processus pour chaque nom de fichier correspondant:

find . -type f -name 'xa*' -exec sed -i 's/asd/dsg/g' {} \;

Alternativement, on pourrait utiliser xargs, qui invoquera moins de processus:

find . -type f -name 'xa*' | xargs sed -i 's/asd/dsg/g'

Ou plus simplement, utilisez la + variante exec au lieu de ;in find pour permettre à find de fournir plus d'un fichier par appel de sous-processus:

find . -type f -name 'xa*' -exec sed -i 's/asd/dsg/g' {} +
ealfonso
la source
8
J'ai dû modifier la commande dans cette réponse comme ceci: find ./ -type f -name 'xa*' -exec sed -i '' 's/asd/dsg/g' {} \;c'est l'emplacement de la commande find ./et une paire de guillemets simples après -ipour OSX.
Shelbydz
La commande find fonctionne comme elle est fournie par ealfonso, ./est égale à .et après -in'a que le paramètre backupsuffix.
uhausbrand
L' -execoption de recherche avec {} +est suffisante pour résoudre le problème comme indiqué et devrait convenir à la plupart des exigences. Mais xargsc'est un meilleur choix en général car il permet également un traitement parallèle avec l' -poption. Lorsque votre extension glob est suffisamment grande pour dépasser la longueur de votre ligne de commande, vous êtes susceptible de bénéficier également d'une accélération sur une exécution séquentielle.
Amit Naidu le
78

Vous pouvez utiliser grep et sed ensemble. Cela vous permet de rechercher des sous-répertoires de manière récursive.

Linux: grep -r -l <old> * | xargs sed -i 's/<old>/<new>/g'
OS X: grep -r -l <old> * | xargs sed -i '' 's/<old>/<new>/g'

For grep:
    -r recursively searches subdirectories 
    -l prints file names that contain matches
For sed:
    -i extension (Note: An argument needs to be provided on OS X)
Raj
la source
3
Le bonus de cette méthode pour moi était que je pouvais glisser dans un grep -vpour éviter les dossiers gitgrep -rl <old> . | grep -v \.git | xargs sed -i 's/<old>/<new>/g'
Martin Lyne
meilleure solution pour un mac!
Marquis Blount
30

Ces commandes ne fonctionneront pas par défaut sed avec Mac OS X.

De man 1 sed:

-i extension
             Edit files in-place, saving backups with the specified
             extension.  If a zero-length extension is given, no backup 
             will be saved.  It is not recommended to give a zero-length
             extension when in-place editing files, as you risk corruption
             or partial content in situations where disk space is exhausted, etc.

A essayé

sed -i '.bak' 's/old/new/g' logfile*

et

for i in logfile*; do sed -i '.bak' 's/old/new/g' $i; done

Les deux fonctionnent bien.

funroll
la source
2
@sumek Voici un exemple de session de terminal sur OS X qui montre sed remplaçant toutes les occurrences: GitHub Gist
funroll
Je l'ai utilisé pour remplacer deux lignes différentes dans tous les fichiers de configuration de mon site Web par une seule ligne ci-dessous. sed -i.bak "s / supercache_proxy_config / proxy_includes \ / supercache_config / g; s / basic_proxy_config / proxy_include \ / basic_proxy_config / g" sites-available / * N'oubliez pas de supprimer les fichiers * .bak lorsque vous avez terminé pour le fichier souci d'hygiène du système.
Josiah
19

@PaulR a posté ceci comme un commentaire, mais les gens devraient le voir comme une réponse (et cette réponse convient le mieux à mes besoins):

sed -i 's/abc/xyz/g' xa*

Cela fonctionnera pour une quantité modérée de fichiers, probablement de l'ordre de dizaines, mais probablement pas de l'ordre de millions .

palswim
la source
Supposons que vous ayez des barres obliques dans vos remplacements. Un autre exemple avec les chemins de fichiers sed -i 's|auth-user-pass nordvpn.txt|auth-user-pass /etc/openvpn/nordvpn.txt|g' *.ovpn.
Léo Léopold Hertz 준영
10

Une autre façon plus polyvalente consiste à utiliser find:

sed -i 's/asd/dsg/g' $(find . -type f -name 'xa*')
Dkinzer
la source
1
la sortie de cette commande find est développée, donc cela ne résout pas le problème. À la place, vous devriez utiliser -exec
ealfonso
@erjoalgo cela fonctionne car la commande sed peut gérer plusieurs fichiers d'entrée. L'extension de la commande find est exactement nécessaire pour la faire fonctionner.
dkinzer
cela fonctionne tant que le nombre de fichiers ne dépasse pas les limites de la ligne de commande.
ealfonso
Cette limite dépend uniquement des ressources de mémoire disponibles pour la machine et est exactement la même que la limite pour exec.
dkinzer
4
Ce n'est tout simplement pas vrai. Dans votre commande ci-dessus, le $ (find. ...) est développé en une seule commande, ce qui peut être très long s'il y a beaucoup de fichiers correspondants. S'il est trop long (par exemple dans mon système, la limite est d'environ 2097152 caractères), vous pourriez obtenir une erreur: "Liste d'arguments trop longue" et la commande échouera. Veuillez rechercher cette erreur sur Google pour obtenir des informations à ce sujet
ealfonso
2

J'utilise findpour une tâche similaire. C'est assez simple: vous devez le passer comme argument pour sedcomme ceci:

sed -i 's/EXPRESSION/REPLACEMENT/g' `find -name "FILE.REGEX"`

De cette façon, vous n'avez pas besoin d'écrire des boucles complexes, et il est simple de voir quels fichiers vous allez modifier, exécutez simplement findavant d'exécuter sed.

Bluesboy
la source
1
C'est exactement la même chose que la réponse de @ dkinzer .
M. Tao
0

tu peux faire

' xxxx ' text u recherche et le remplacera par ' yyyy '

grep -Rn '**xxxx**' /path | awk -F: '{print $1}' | xargs sed -i 's/**xxxx**/**yyyy**/'
Mohamed Galal
la source
0

Si vous êtes capable d'exécuter un script, voici ce que j'ai fait pour une situation similaire:

En utilisant un dictionnaire / hashMap (tableau associatif) et des variables pour la sedcommande, nous pouvons parcourir le tableau pour remplacer plusieurs chaînes. L'inclusion d'un caractère générique dans le name_patternpermettra de remplacer sur place dans les fichiers par un modèle (cela pourrait être quelque chose comme name_pattern='File*.txt') dans un répertoire spécifique ( source_dir). Tous les changements sont écrits dans le logfiledans ledestin_dir

#!/bin/bash
source_dir=source_path
destin_dir=destin_path
logfile='sedOutput.txt'
name_pattern='File.txt'

echo "--Begin $(date)--" | tee -a $destin_dir/$logfile
echo "Source_DIR=$source_dir destin_DIR=$destin_dir "

declare -A pairs=( 
    ['WHAT1']='FOR1'
    ['OTHER_string_to replace']='string replaced'
)

for i in "${!pairs[@]}"; do
    j=${pairs[$i]}
    echo "[$i]=$j"
    replace_what=$i
    replace_for=$j
    echo " "
    echo "Replace: $replace_what for: $replace_for"
    find $source_dir -name $name_pattern | xargs sed -i "s/$replace_what/$replace_for/g" 
    find $source_dir -name $name_pattern | xargs -I{} grep -n "$replace_for" {} /dev/null | tee -a $destin_dir/$logfile
done

echo " "
echo "----End $(date)---" | tee -a $destin_dir/$logfile

Tout d'abord, le tableau de paires est déclaré, chaque paire est une chaîne de remplacement, puis WHAT1sera remplacée FOR1et OTHER_string_to replacesera remplacée string replaceddans le fichier File.txt. Dans la boucle le tableau est lu, le premier membre de la paire est récupéré en tant que replace_what=$iet le second en tant que replace_for=$j. La findcommande recherche dans le répertoire le nom de fichier (qui peut contenir un caractère générique) et la sed -icommande remplace dans le (s) même (s) fichier (s) ce qui a été précédemment défini. Enfin, j'ai ajouté un grepfichier redirigé vers le fichier journal pour enregistrer les modifications apportées au (x) fichier (s).

Cela a fonctionné pour moi GNU Bash 4.3 sed 4.2.2et basé sur la réponse de VasyaNovikov pour Loop over tuples in bash .

Lejuanjowski
la source