101 votes

Substitution dans le fichier texte **sans** expressions régulières

Je dois substituer un peu de texte à l'intérieur d'un fichier texte par un remplacement. Habituellement, je ferais quelque chose comme

sed -i 's/text/replacement/g' chemin/vers/le/fichier

Le problème est que à la fois texte et remplacement sont des chaînes complexes contenant des tirets, des barres obliques, des barres obliques inversées, des guillemets, etc. Si j'échappe à tous les caractères nécessaires à l'intérieur de texte, la chose devient rapidement illisible. D'un autre côté, je n'ai pas besoin de la puissance des expressions régulières : je dois juste substituer le texte littéralement.

Y a-t-il un moyen de faire une substitution de texte sans utiliser d'expressions régulières avec une commande bash ?

Il serait plutôt trivial d'écrire un script qui fait cela, mais je me dis qu'il devrait déjà exister quelque chose.

3voto

Samer Ata Points 131

Découvrez mon script Perl. il fait exactement ce dont vous avez besoin sans utiliser explicitement des expressions régulières :

https://github.com/Samer-Al-iraqi/Linux-str_replace

str_replace Rechercher Remplacer Fichier # remplacer dans le fichier en place

STDIN | str_replace Rechercher Remplacer # vers STDOUT

très pratique, non? J'ai dû apprendre Perl pour le faire. parce que j'en avais vraiment vraiment besoin.

3voto

Ceci est une amélioration de la réponse de Hashbrown (et la réponse de wef à une question très similaire).

Nous pouvons éliminer le problème du sens spécial de divers caractères spéciaux et chaînes (^, ., [, *, $, \(, \), \{, \}, \+, \?, &, \1, …, peu importe, et le délimiteur /) en supprimant les caractères spéciaux. En particulier, nous pouvons tout convertir en hexadécimal ; ensuite nous n'avons plus que les caractères 0-9 et a-f à traiter.  Cet exemple démontre le principe :

$ echo -n '3.14' | xxd
0000000: 332e 3134                                3.14

$ echo -n 'pi'   | xxd
0000000: 7069                                     pi

$ echo '3.14 is a transcendental number.  3614 is an integer.' | xxd
0000000: 332e 3134 2069 7320 6120 7472 616e 7363  3.14 is a transc
0000010: 656e 6465 6e74 616c 206e 756d 6265 722e  endental number.
0000020: 2020 3336 3134 2069 7320 616e 2069 6e74    3614 is an int
0000030: 6567 6572 2e0a                           eger..

$ echo "3.14 is a transcendental number.  3614 is an integer." | xxd -p \
                                                        | sed 's/332e3134/7069/g' | xxd -p -r
pi is a transcendental number.  3614 is an integer.

alors que bien sûr, sed 's/3.14/pi/g' changerait aussi 3614.

Ci-dessus est une légère simplification ; elle ne tient pas compte des limites.  Considérez cet exemple (quelque peu artificiel) :

$ echo -n 'E' | xxd
0000000: 45                                       E

$ echo -n 'g' | xxd
0000000: 67                                       g

$ echo '$Q Eak!' | xxd
0000000: 2451 2045 616b 210a                      $Q Eak!.

$ echo '$Q Eak!' | xxd -p | sed 's/45/67/g' | xxd -p -r
&q gak!

Parce que $ (24) et Q (51) se combinent pour former 2**_45_**1, la commande s/45/67/g le déchire de l'intérieur.  Elle change 2451 en 2671, qui est &q (26+71).  Nous pouvons empêcher cela en séparant les octets de données dans le texte de recherche, le texte de remplacement et le fichier avec des espaces.  Voici une solution stylisée :

encode() {
        xxd -p    -- "$@" | sed 's/../& /g' | tr -d '\n'
}
decode() {
        xxd -p -r -- "$@"
}
left=$( printf '%s' "$search"      | encode)
right=$(printf '%s' "$replacement" | encode)
encode path/to/the/file | sed "s/$left/$right/g" | decode

J'ai défini une fonction encode car j'ai utilisé cette fonctionnalité trois fois, puis j'ai défini decode pour la symétrie.  Si vous ne voulez pas définir une fonction decode, changez simplement la dernière ligne en

encode path/to/the/file | sed "s/$left/$right/g" | xxd -p –r

Remarquez que la fonction encode triple la taille des données (texte) dans le fichier, puis les envoie à travers sed en une seule ligne — sans même avoir un retour à la ligne à la fin.  GNU sed semble être capable de gérer cela ; d'autres versions pourraient ne pas y parvenir.  En outre, cela ne modifie pas le fichier sur place ; vous devrez écrire la sortie dans un fichier temporaire et ensuite le copier sur le fichier original (ou l'une des autres astuces pour faire cela).

En bonus, cette solution gère les recherches et remplacements sur plusieurs lignes (en d'autres termes, des chaînes de recherche et de remplacement qui contiennent des sauts de ligne).

1voto

Fábio Points 1089

J'ai rassemblé quelques autres réponses et j'en suis arrivé à ceci :

function unregex {
   # Ceci est une fonction car gérer les guillemets est pénible.
   # http://stackoverflow.com/a/2705678/120999
   sed -e 's/[]\/()$*.^|[]/\\&/g' <<< "$1"
}
function fsed {
   local find=$(unregex "$1")
   local replace=$(unregex "$2")
   shift 2
   # sed -i est seulement supporté dans GNU sed.
   #sed -i "s/$find/$replace/g" "$@"
   perl -p -i -e "s/$find/$replace/g" "$@"
}

1voto

simlev Points 3517

Vous pouvez utiliser str_replace de php :

php -R 'echo str_replace("\|!£$%&/()=?^\"'\''","replace",$argn),PHP_EOL;'output.txt

Remarque : Vous auriez quand même besoin d'échapper les apostrophes simples ' et les guillemets ".

1voto

Hashbrown Points 2148

Vous pouvez le faire en sh sans aucun script (bien que mettre ce "one-liner" dans un script serait mieux) ou sans programme externe non standard (j'ai vraiment aimé la réponse de @Nowaker grâce à sa sécurité contre les injections, mais cette ancienne boîte CentOS sur laquelle j'avais besoin de cela n'avait pas ruby!). tant que <code>perl</code> n'est pas non standard pour vous

Sans essayer d' échapper la chaîne (et de tenir compte des problèmes liés à le faire correctement syntaxiquement, en connaissant tous les caractères spéciaux, etc.), nous pouvons simplement encoder toutes les chaînes pour que rien n'ait la possibilité d'être spécial.

cat chemin/vers/le/fichier | xxd -p | tr -d '\n' \
| perl -pe "s/$(printf '%s' 'text' | xxd -p | tr -d '\n')(?=(?:.{2})*\$)/$(printf '%s' 'replacement' | xxd -p | tr -d '\n')/g" \
| xxd -p -r

C'était juste pour correspondre à l'exemple de l'auteur de la question, d'autres utilisateurs peuvent bien sûr remplacer 'text' par "$text" s'ils utilisent une variable, ou cat chemin/vers/le/fichier par printf '%s' "$input" s'ils n'utilisent pas de fichier.

Vous pouvez même remplacer le /g par / pour le remplacer une seule fois, ou modifier autrement l'expression régulière en dehors du $() pour "échapper" uniquement des parties du motif (par exemple, ajouter un ^ après s/ pour qu'il corresponde uniquement au début du fichier).
Si dans ce qui précède vous avez besoin de ^/$ pour correspondre à nouveau à des <em>extrémités de lignes</em>, vous devrez les désencoder:

cat chemin/vers/le/fichier | xxd -p | tr -d '\n' | sed 's/0a/\n/g'\
| perl -pe "s/^$(printf '%s' 'text' | xxd -p | tr -d '\n')(?=(?:.{2})*\$)/$(printf '%s' 'replacement' | xxd -p | tr -d '\n')/g" \
| sed 's/\n/0a/g' | xxd -p -r

Cela remplacera toutes les lignes du fichier commençant par 'text' pour qu'elles commencent à la place par 'replacement'


Test:

À l'intérieur de ^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}, remplacer littéralement ^/.[a]|$0\\{7} par littéralement $0\\

printf '%s' '^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}!!^/.[a]|$0\\{7}' \
| xxd -p | tr -d '\n' \
| perl -pe "s/$(printf '%s' '^/.[a]|$0\\{7}' | xxd -p | tr -d '\n')(?=(?:.{2})*\$)/$(printf '%s' '$0\\' | xxd -p | tr -d '\n')/g" \
| xxd -p -r

Sortie:

$0\\!!$0\\!!$0\\

SistemesEz.com

SystemesEZ est une communauté de sysadmins où vous pouvez résoudre vos problèmes et vos doutes. Vous pouvez consulter les questions des autres sysadmins, poser vos propres questions ou résoudre celles des autres.

Powered by:

X