Comment puis-je atteindre la portabilité avec sed -i (montage sur place)?

42

J'écris des scripts shell pour mon serveur, qui est un hébergement partagé sous FreeBSD. Je veux aussi pouvoir les tester localement, sur mon PC sous Linux. Par conséquent, j'essaie de les écrire de manière portable, mais sedje ne vois aucun moyen de le faire.

Une partie de mon site Web utilise des fichiers HTML statiques générés, et cette ligne sed insère le DOCTYPE correct après chaque régénération:

sed -i '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

Cela fonctionne avec GNU sedsous Linux, mais FreeBSD seds'attend à ce que le premier argument après l' -ioption soit l'extension de la copie de sauvegarde. Voici à quoi cela ressemblerait:

sed -i '' '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

Cependant, GNU seds'attend à ce que l'expression suive immédiatement après -i. (Cela nécessite également des correctifs avec la gestion des nouvelles lignes, mais cela a déjà été résolu ici )

Bien sûr, je peux inclure ce changement dans la copie du script que je suis sur le serveur, mais cela gâcherait mon utilisation de VCS pour la gestion des versions. Existe-t-il un moyen d'y parvenir avec sed de manière totalement portable?

rouge
la source
Les deux extraits sed que vous avez fournis sont identiques, êtes-vous sûr qu'il n'y a pas de faute de frappe? De plus, je suis capable d'exécuter GNU sed en fournissant l'extension de sauvegarde juste après-i
iruvar le
Duh, merci d'avoir remarqué cela. J'ai corrigé ma question. La deuxième ligne entraîne une erreur dans mon fichier sed, il s'attend à ce que '1s / ^ / <! DOCTYPE html> \ n /' soit un fichier et se plaint de ne pas pouvoir le trouver.
Rouge
1
Référence croisée: indicateur sed in-place qui fonctionne à la fois sur Mac (BSD) et Linux sur dépassement de capacité.
MvG

Réponses:

41

GNU sed accepte une extension optionnelle après -i. L'extension doit être dans le même argument sans espace intermédiaire. Cette syntaxe fonctionne également sur BSD sed.

sed -i.bak -e '…' SOMEFILE

Notez que sous BSD, -imodifie également le comportement lorsqu'il y a plusieurs fichiers d'entrée: ils sont traités indépendamment ( $correspond par exemple à la dernière ligne de chaque fichier). De plus, cela ne fonctionnera pas sur BusyBox.

Si vous ne souhaitez pas utiliser de fichiers de sauvegarde, vous pouvez vérifier quelle version de sed est disponible.

case $(sed --help 2>&1) in
  *GNU*) set sed -i;;
  *) set sed -i '';;
esac
"$@" -e '…' "$file"

Ou bien, pour éviter de brouiller les paramètres de position, définissez une fonction.

case $(sed --help 2>&1) in
  *GNU*) sed_i () { sed -i "$@"; };;
  *) sed_i () { sed -i '' "$@"; };;
esac
sed_i -e '…' "$file"

Si vous ne voulez pas vous déranger, utilisez Perl.

perl -i -pe '…' "$file"

Si vous voulez écrire un script portable, n'utilisez pas -i- ce n'est pas dans POSIX. Faites manuellement ce que sed fait sous le capot - ce n’est plus qu’une ligne de code.

sed -e '…' "$file" >"$file.new"
mv -- "$file.new" "$file"
Gilles, arrête de faire le mal
la source
2
GNU sed -iimplique également -s. Et le moyen le plus simple de rechercher une unité GNU consiste à utiliser sedla sed vcommande qui est un noop valide pour GNU mais qui échoue partout ailleurs.
mikeserv
Inspiré par les conseils ci - dessus, voici une seule ligne (si laide) version portable pour ceux qui veulent vraiment un, mais il ne fraient un sous - shell: sed -i$(sed v < /dev/null 2> /dev/null || echo -n " ''") -e '...' "$file"Si ce n'est pas GNU sed, il insère un espace suivi de deux citations de singe après -iafin qu'il fonctionne sur BSD. GNU sed obtient seulement -i.
Ivan X
2
@ IvanX Je me méfie d'utiliser la présence de la vcommande pour tester GNU sed. Et si FreeBSD décidait de l'implémenter?
Gilles 'SO- arrête d'être méchant'
@Gilles Fair point, mais la page de manuel de GNU sed décrit v comme étant exactement dans ce but (détecter que c'est GNU sed et pas autre chose), donc on espérerait que * BSD honorerait cela. Je ne peux pas penser à un autre test, qui ne prend aucune action sur GNU sed, tout en provoquant une erreur sur BSD sed (ou vice versa), autre que l'utilisation de -i, mais cela nécessiterait la création préalable d'un fichier factice. Votre test pour sed ci-dessus est correct mais difficile à gérer pour inline. Éviter complètement - comme vous le suggérez - semble être le pari le plus sûr, mais je peux utiliser, sed vétant donné que c'est son objectif d'exister.
Ivan X
1
@lapo Une alternative consiste à définir une fonction, voir ma modification.
Gilles 'SO- arrête d'être méchant'
10

Si vous ne trouvez pas d'astuce pour rendre le sedjeu agréable, vous pouvez essayer:

  1. Ne pas utiliser -i:

    sed '1s/^/<!DOCTYPE html> \n/' "${file_name.html}" > "${file_name.html}.tmp" &&
      mv "${file_name.html}.tmp" "${file_name.html}"
  2. Utilisez Perl

    perl -i -pe 'print "<!DOCTYPE html> \n" if $.==1;' "${file_name.html}"
terdon
la source
8

ed

Vous pouvez toujours utiliser edpour ajouter une ligne à un fichier existant.

$ printf '0a\n<!DOCTYPE html>\n.\nw\n' | ed my.html

Détails

Les bits autour de <!DOCTYPE html>sont des commandes pour lui eddemander d’ajouter cette ligne au fichier my.html.

sed

Je crois que cette commande sedpeut aussi être utilisée:

$ sed -i '1i<!DOCTYPE html>\n` testfile.csv
slm
la source
J'ai finalement eu recours à Perl, mais utiliser ed est une bonne alternative qui n’est pas populaire parmi les utilisateurs du type Unix, comme il se doit.
Rouge le
@Red - content de vous entendre résoudre votre problème. Ouais, je n'avais jamais vu cela auparavant, googler l'a rendu possible et cela semblait en fait la manière la plus portable et la plus adaptée de le faire.
slm
7

Vous pouvez également faire manuellement ce que perl -ifait sous le capot:

{ rm -f file && { echo '<!DOCTYPE html>'; cat; } > file;} < file

Par exemple perl -i, il n'y a pas de sauvegarde, et comme la plupart des solutions données ici, prenez garde que cela peut affecter les autorisations, la propriété du fichier et peut transformer un lien symbolique en un fichier normal.

Avec:

sed '1i\
<!DOCTYPE html>' file 1<> file

sedécraserait le fichier sur lui-même, n'affecterait donc pas la propriété, les permissions ou les liens symboliques. Cela fonctionne avec GNU sedcar sedil a généralement lu un tampon plein de données file(4k dans mon cas) avant de l’écraser avec la icommande. Cela ne fonctionnerait pas si le fichier comptait plus de 4k, à part le fait que sedsa sortie tamponne également.

Fonctionne essentiellement sedsur des blocs de 4k pour la lecture et l’écriture. Si la ligne à insérer est inférieure à 4k, sedne remplacera jamais un bloc qu'il n'a pas encore lu.

Je ne compterais pas dessus cependant.

Stéphane Chazelas
la source
Devrait être echo '<!DOCTYPE html>'ou échappé sans "" guillemets.
AD
@AD Bon point. J'ai tendance à oublier ce bogue ^ Wfeature de interactive bash / zsh car je le désactive généralement pour moi-même.
Stéphane Chazelas
Ceci est l' une des très réponses peu ici qui ne dispose pas d' un trou de sécurité dans l' ensemble ouvert de redirection vers un « fichier temporaire » avec un statique, le nom prévisible sans vérifier si elle existe déjà. Je crois que cela devrait être la réponse acceptée. Très belle démonstration de l'utilisation des commandes de groupe, également.
Wildcard
3

FreeBSD sed , qui est également utilisé sur Mac OS X, a besoin de l’ -eoption après le -icommutateur pour définir et reconnaître la commande suivante (regex) correctement et sans ambiguïté.

En d'autres termes, sed -i -e ...devrait fonctionner à la fois avec FreeBSD et GNU sed.

Plus généralement, l'omission de l'extension de sauvegarde après FreeBSD sed -inécessite une sedoption ou un commutateur explicite -ipour éviter toute confusion avec FreeBSD sedlors de l'analyse de ses arguments de ligne de commande.

(Notez cependant que sedles modifications de fichiers sur place entraînent des modifications d'inode de fichier, voir Modification de fichiers "sur place" ).

(En règle générale, les versions récentes de FreeBSD sedpermettent -rd’augmenter la compatibilité avec GNU sed).

echo a > testfile.txt
ls -li testfile.txt
#gsed -i -e 's/a/A/' testfile.txt
#bsdsed -i 's/a/A/' testfile.txt  # does not work
bsdsed -i -e 's/a/A/' testfile.txt
ls -li testfile.txt
cat testfile.txt
carlo
la source
5
Non, ce bsdsed -i -e 's/a/A/'n'est pas l' édition en place, c'est l'édition avec l'enregistrement de l'original avec un suffixe "-e" ( testfile.txt-e).
Stéphane Chazelas
notez que GNU sed supporte également -E (en plus de -r) pour la compatibilité avec FreeBSD. -E est susceptible d'être spécifié dans la prochaine version de POSIX, nous devrions donc tous en oublier -r le non-sens et prétendre que cela n'a jamais existé.
Stéphane Chazelas
2

Pour émuler sed -ipour un seul fichier de manière portable tout en évitant autant que possible les conditions de concurrence:

sed 'script' <<FILE >file
$(cat file)
FILE

Soit dit en passant, cela permet également de résoudre le problème éventuel: sed -ien fonction des autorisations accordées sur les répertoires et les fichiers, sed -iun utilisateur peut remplacer un fichier qu’il n’a pas l’autorisation de modifier.

Vous pouvez également faire des sauvegardes comme:

sed 'script' <<FILE >file
$(tee file.old <file)
FILE
Mikeserv
la source
2

Vous pouvez utiliser Vim en mode Ex:

ex -sc '1i|<!DOCTYPE html>' -cx file
  1. 1 sélectionner la première ligne

  2. i insérer du texte et une nouvelle ligne

  3. x sauver et fermer

Ou même, comme dans les commentaires, tout simplement vieux standard ex :

printf '%s\n' 1i '<!DOCTYPE html>' . x | ex file 
Steven Penny
la source
Il n'y a rien de spécifique à Vim ici. Ceci est entièrement conforme aux spécifications POSIX pourex , sauf que les implémentations ne sont pas obligées de prendre en charge plusieurs -cdrapeaux. Pour une portabilité définitive, je voudrais utiliserprintf '%s\n' 1i '<!DOCTYPE html>' . x | ex file
Wildcard