Comment lire l'intégralité du script shell avant de l'exécuter?

35

Généralement, si vous éditez un scrpit, toutes les utilisations du script en cours sont sujettes aux erreurs.

Autant que je sache, bash (les autres shells aussi?) Lisent le script de manière incrémentielle. Par conséquent, si vous modifiez le fichier de script en externe, il commence à lire le mauvais contenu. Y a-t-il un moyen de le prévenir?

Exemple:

sleep 20

echo test

Si vous exécutez ce script, bash lira la première ligne (disons 10 octets) et s'endormira. À la reprise, le script peut contenir différents contenus commençant au 10ème octet. Je suis peut-être au milieu d'une ligne dans le nouveau script. Ainsi, le script en cours sera brisé.

VasyaNovikov
la source
Qu'entendez-vous par "modifier le script en externe"?
Maulinglawns
1
Peut-être y a-t-il un moyen d'envelopper tout le contenu dans une fonction ou quelque chose, de sorte que le shell lise tout le script en premier? Mais qu'en est-il de la dernière ligne où vous appelez la fonction, sera-t-elle lue jusqu'à EOF? Peut-être qu'omettre le dernier \nferait l'affaire? Peut-être qu'un sous-shell ()fera l'affaire? Je ne suis pas très expérimenté, aidez-moi!
VasyaNovikov
@maulinglawns Si le script a un contenu du type sleep 20 ;\n echo test ;\n sleep 20et que je commence à l'éditer, il risque de mal se comporter. Par exemple, bash pourrait lire les 10 premiers octets du script, comprendre la sleepcommande et se mettre en veille. Après la reprise, le fichier contiendrait un contenu différent commençant à 10 octets.
VasyaNovikov
1
Donc, ce que vous dites, c'est que vous éditez un script en cours d'exécution? Arrêtez d'abord le script, faites vos modifications, puis relancez-le.
maulinglawns
@maulinglawns oui, c'est fondamentalement ça. Le problème, c’est qu’il n’est pas pratique pour moi d’arrêter les scripts, et il est difficile de toujours se rappeler de le faire. Peut-être y at-il un moyen de forcer bash à lire tout le script en premier?
VasyaNovikov

Réponses:

43

Oui, les shells, et bashen particulier, veillent à lire le fichier ligne par ligne, de sorte qu'il fonctionne de la même manière que lorsque vous l'utilisez de manière interactive.

Vous remarquerez que lorsque le fichier ne peut pas être recherché (comme un tube), bashmême lit un octet à la fois pour être sûr de ne pas lire après le \ncaractère. Lorsque le fichier peut être recherché, il optimise en lisant des blocs entiers à la fois, mais en recherchant après \n.

Cela signifie que vous pouvez faire des choses comme:

bash << \EOF
read var
var's content
echo "$var"
EOF

Ou écrivez des scripts qui se mettent à jour. Ce que vous ne pourriez pas faire si cela ne vous donnait pas cette garantie.

Maintenant, il est rare que vous souhaitiez faire de telles choses et, comme vous l'avez découvert, cette fonctionnalité a tendance à gêner plus souvent qu'elle n'est utile.

Pour éviter cela, vous pouvez essayer de vous assurer que vous ne modifiez pas le fichier sur place (par exemple, modifiez une copie et déplacez la copie sur place (comme sed -iou perl -piet certains éditeurs le font par exemple)).

Ou vous pouvez écrire votre script comme:

{
  sleep 20
  echo test
}; exit

(Notez qu'il est important que le exitsoit sur la même ligne que }, bien que vous puissiez aussi le mettre entre les accolades juste avant le dernier).

ou:

main() {
  sleep 20
  echo test
}
main "$@"; exit

Le shell devra lire le script jusqu’à exitavant de commencer à faire quoi que ce soit. Cela garantit que le shell ne lira plus le script.

Cela signifie que tout le script sera stocké dans la mémoire.

Cela peut également affecter l'analyse du script.

Par exemple, dans bash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Produirait que U + 00E9 codé en UTF-8. Cependant, si vous le changez en:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

La \ue9volonté sera développée dans le jeu de caractères qui était en vigueur au moment de l'analyse de la commande et qui, dans ce cas, est antérieure à l' exportexécution de la commande.

Notez également que si la commande sourceaka .est utilisée, avec certains shells, vous aurez le même genre de problème pour les fichiers sourcés.

Ce n'est pas le cas de bashbien dont la sourcecommande lit le fichier complètement avant de l'interpréter. Si vous écrivez bashspécifiquement, vous pouvez en faire usage en ajoutant au début du script:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Je ne m'appuierais pas là-dessus, même si, comme vous pouvez l'imaginer, les versions futures bashpourraient changer ce comportement qui peut actuellement être considéré comme une limitation (bash et AT & T ksh sont les seuls shells de type POSIX qui se comportent comme ça autant que l'on peut en juger) et l' already_sourcedastuce est un peu fragile car elle suppose que la variable n'est pas dans l'environnement, sans parler du fait qu'elle affecte le contenu de la variable BASH_SOURCE)

Stéphane Chazelas
la source
@VasyaNovikov, il semble y avoir quelque chose qui ne va pas avec SE pour le moment (ou du moins pour moi). Il n’ya que quelques réponses lorsque j’ai ajouté le mien, et votre commentaire semble n’être apparu que maintenant, même s’il dit qu’il a été posté il ya 16 minutes (ou peut-être que c’est juste moi qui perd mes billes). Quoi qu'il en soit, notez la "sortie" supplémentaire qui est nécessaire ici pour éviter les problèmes lorsque la taille du fichier augmente (comme indiqué dans le commentaire que j'ai ajouté à votre réponse).
Stéphane Chazelas
Stéphane, je pense avoir trouvé une autre solution. C'est à utiliser }; exec true. De cette façon, il n'y a aucune exigence sur les nouvelles lignes à la fin du fichier, ce qui convient à certains éditeurs (comme emacs). Tous les tests auxquels je pourrais penser fonctionnent correctement}; exec true
VasyaNovikov
@ VasyaNovikov, je ne sais pas ce que vous voulez dire. Comment est-ce meilleur que }; exit? Vous perdez également le statut de sortie.
Stéphane Chazelas
Comme mentionné à une question différente: il est courant d'analyser d'abord le fichier entier, puis d'exécuter l'instruction composée au cas où la commande dot ( . script) est utilisée.
Schily
@schily, oui, je le mentionne dans cette réponse en tant que limitation de ksh and bash d'AT & T. Les autres shells de type POSIX n'ont pas cette limitation.
Stéphane Chazelas
12

Vous devez simplement supprimer le fichier (c'est-à-dire le copier, le supprimer, renommer la copie en son nom d'origine). En fait, de nombreux éditeurs peuvent être configurés pour le faire à votre place. Lorsque vous modifiez un fichier et que vous enregistrez un tampon modifié dans celui-ci, au lieu de le remplacer, il renomme l'ancien fichier, en crée un nouveau et place le nouveau contenu dans le nouveau fichier. Par conséquent, tout script en cours devrait continuer sans problèmes.

En utilisant un système de contrôle de version simple tel que RCS, qui est facilement disponible pour vim et emacs, vous bénéficiez du double avantage de disposer d'un historique de vos modifications. Le système de paiement devrait par défaut supprimer le fichier actuel et le recréer avec les modes appropriés. (Méfiez-vous des liens durs avec de tels fichiers bien sûr).

meuh
la source
"supprimer" ne fait pas partie du processus. Si vous voulez le rendre correctement atomique, vous renommerez le fichier de destination. Si vous avez une étape de suppression, votre processus risque de disparaître après la suppression, mais avant le changement de nom, sans laisser aucun fichier en place ( ou un lecteur essaie d’accéder au fichier dans cette fenêtre et ne trouve aucune version ancienne ou nouvelle disponible).
Charles Duffy
11

La solution la plus simple:

{
  ... your code ...

  exit
}

De cette façon, bash lira l’ensemble du {}bloc avant de l’exécuter et la exitdirective s’assurera que rien ne sera lu en dehors du bloc de code.

Si vous ne voulez pas "exécuter" le script, mais plutôt pour le "trouver", vous avez besoin d'une solution différente. Cela devrait fonctionner alors:

{
  ... your code ...

  return 2>/dev/null || exit
}

Ou si vous souhaitez contrôler directement le code de sortie:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Voilà! Ce script peut être édité, source et exécuté en toute sécurité. Vous devez toujours vous assurer de ne pas le modifier en quelques millisecondes lors de sa lecture initiale.

VasyaNovikov
la source
1
Ce que j’ai trouvé, c’est qu’il ne voit ni EOF ni arrêter de lire le fichier, mais il est empêtré dans son traitement de "flux mis en mémoire tampon" et finit par chercher après la fin du fichier. C’est pourquoi il est correct si la taille de le fichier n'augmente pas beaucoup, mais son aspect est mauvais lorsque vous le créez plus de deux fois plus gros qu'auparavant. Je signalerai un bug aux responsables de Bash sous peu.
Stéphane Chazelas
1
bug signalé maintenant , voir aussi le patch .
Stéphane Chazelas
Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
terdon
5

Preuve de concept. Voici un script qui se modifie lui-même:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

nous voyons la version modifiée imprimer

En effet, bash chargements conserve un descripteur de fichier à ouvrir au script, de sorte que les modifications apportées au fichier sont immédiatement visibles.

Si vous ne souhaitez pas mettre à jour la copie en mémoire, dissociez le fichier d'origine et remplacez-le.

Une façon de faire est d'utiliser sed -i.

sed -i '' filename

preuve de concept

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

Si vous utilisez un éditeur pour modifier le script, il suffit peut-être d'activer la fonctionnalité "Conserver une copie de sauvegarde" pour que l'éditeur écrive la version modifiée dans un nouveau fichier au lieu de remplacer le fichier existant.

Jasen
la source
2
Non, bashn'ouvre pas le fichier avec mmap(). Veillez simplement à lire une ligne à la fois selon vos besoins, comme lorsque vous recevez les commandes d'un terminal lorsqu'il est interactif.
Stéphane Chazelas
2

Envelopper votre script dans un bloc {}est probablement la meilleure option mais nécessite de changer vos scripts.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

serait la deuxième meilleure option (en supposant que tmpfs ) l’inconvénient est qu’elle casse $ 0 si vos scripts l’utilisent.

utiliser quelque chose comme F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashest moins idéal car il doit garder tout le fichier en mémoire et casser 0 $.

il faut éviter de toucher au fichier d'origine afin que, la dernière fois modifiée, les verrous de lecture et les liens physiques ne soient pas perturbés. De cette façon, vous pouvez laisser un éditeur ouvert pendant l’exécution du fichier et rsync ne contrôlera pas inutilement le fichier pour les sauvegardes et les liens physiques fonctionneront comme prévu.

Le remplacement du fichier en mode édition fonctionnerait, mais il est moins robuste car il n’est pas opposable à d’autres scripts / utilisateurs /. Et encore, cela romprait les liens durs.

utilisateur1133275
la source
tout ce qui fait une copie fonctionnerait. tac test.sh | tac | bash
Jasen