Bash recharge automatiquement (injecte) les mises à jour dans un script en cours d'exécution lors de son enregistrement: pourquoi? Une utilisation pratique?

10

J'écrivais un script bash et j'ai mis à jour le code (enregistré le fichier de script sur le disque) pendant que le script attendait une entrée dans une whileboucle. Après être revenu sur le terminal et continuer avec l'appel précédent du script, bash a donné une erreur sur la syntaxe du fichier:

/home/aularon/bin/script: line 58: unexpected EOF while looking for matching `"'
/home/aularon/bin/script: line 67: syntax error: unexpected end of file

J'ai donc essayé de faire ce qui suit:

1er: créez un script, self-update.shappelons-le:

#!/bin/bash
fname=$(mktemp)
cat $0 | sed 's/BEFORE\./AFTER!./' > $fname
cp $fname $0
rm -f $fname
echo 'String: BEFORE.';

Le script lit son code, change le mot «AVANT» en «APRÈS», puis se réécrit avec le nouveau code.

2ème Run it:

chmod +x self-update.sh
./self-update.sh

3ème merveille ...

aularon@aularon-laptop:~$ ./self-update.sh 
String: AFTER!.

Maintenant, je n'aurais pas deviné que sur la même invocation, il sortirait APRÈS! , sur la deuxième manche, bien sûr, mais pas sur la première.

Ma question est donc: est-ce intentionnel (par conception)? ou c'est à cause de la façon dont bash exécute le script? Ligne par ligne ou commande par commande. Y a-t-il une bonne utilisation d'un tel comportement? Un exemple?


Edit: J'ai essayé de reformater le fichier pour mettre toutes les commandes sur une seule ligne, cela ne fonctionne pas maintenant:

#!/bin/bash
fname=$(mktemp);cat $0 | sed 's/BEFORE\./AFTER!./' > $fname;cp $fname $0;rm -f $fname;echo 'String: BEFORE.';

Production:

aularon@aularon-laptop:~$ ./self-update.sh #First invocation
String: BEFORE.
aularon@aularon-laptop:~$ ./self-update.sh #Second invocation
String: AFTER!.

Lors du déplacement de la echochaîne vers la ligne suivante, en la séparant de l' cpappel rewriting ( ):

#!/bin/bash
fname=$(mktemp);cat $0 | sed 's/BEFORE\./AFTER!./' > $fname;cp $fname $0;rm -f $fname;
echo 'String: BEFORE.';

Et maintenant ça marche à nouveau:

aularon@aularon-laptop:~$ ./self-update.sh 
String: AFTER!.
aularon
la source
1
Connexe: stackoverflow.com/questions/4754592/…
Martin von Wittich
1
J'ai regardé la source, mais je ne trouve pas vraiment d'explication à cela. Je suis à peu près sûr que j'ai vu quelques installateurs auto-extractibles qui ont été implémentés en tant que scripts shell il y a quelques années. Ces scripts contiennent quelques lignes de commandes shell exécutables, puis un énorme bloc de données qui est lu par ces commandes. Je suppose donc que cela est conçu de cette façon afin que bash n'ait pas à lire le script entier en mémoire, ce qui ne fonctionnerait pas avec d'énormes scripts auto-extractibles.
Martin von Wittich
Voici un exemple de script SFX: ftp.games.skynet.be/pub/wolfenstein/…
Martin von Wittich
Agréable! Je me souviens avoir vu ces scripts auto-installés, le pilote AMD Catalyst (propriétaire) est toujours livré comme ça, il s'auto-extrait puis s'installe. Ceux-ci dépendent d'un tel comportement de lecture des fichiers en morceaux. Merci pour l'exemple!
aularon

Réponses:

12

C'est par conception. Bash lit les scripts en morceaux. Il va donc lire une partie du script, exécuter toutes les lignes qu'il peut, puis lire le morceau suivant.

Vous rencontrez donc quelque chose comme ceci:

  • Bash lit les 256 premiers octets (octets 0-255) du script.
  • Dans les 256 premiers octets se trouve une commande qui prend un certain temps à s'exécuter et bash démarre cette commande, en attendant qu'elle se termine.
  • Pendant l'exécution de la commande, le script est mis à jour et la partie modifiée est après les 256 octets déjà lus.
  • Lorsque la commande bash était en cours d'exécution, elle continue de lire le fichier, en reprenant d'où il était, en obtenant les octets 256-511.
  • Cette partie du script a changé, mais bash ne le sait pas.

Lorsque cela devient encore plus problématique, c'est que si vous modifiez quelque chose avant l'octet 256. Disons que vous supprimez quelques lignes. Ensuite, les données dans le script qui était à l'octet 256, sont maintenant ailleurs, disons à l'octet 156 (100 octets plus tôt). Pour cette raison, lorsque bash continuera à lire, il obtiendra ce qui était à l'origine 356.

C'est juste un exemple. Bash ne lit pas nécessairement 256 octets à la fois. Je ne sais pas exactement combien il lit à la fois, mais cela n'a pas d'importance, le comportement est toujours le même.

Patrick
la source
Non, il lit par morceaux, mais revient à l'endroit où il devait être sûr de lire la commande suivante telle qu'elle est après le retour de la commande précédente.
Stéphane Chazelas
@StephaneChazelas D'où tirez-vous cela? Je viens de faire une séquence et ce n'est même pas statle fichier pour voir s'il a changé. Pas d' lseekappels.
Patrick
Voir pastie.org/8662761 pour les parties pertinentes de la sortie strace. Voyez comment le echo foochangé en a echo barpendant sleep. Il se comporte comme ça depuis les versions 2, donc je ne pense pas que ce soit un problème de version.
Stéphane Chazelas
Apparemment, il lit le fichier ligne par ligne, j'ai essayé avec le fichier et c'est ce que j'ai découvert. Je vais modifier ma question pour mettre en évidence ce comportement.
aularon
@Patrick si vous pouvez mettre à jour votre réponse pour qu'elle reflète la lecture ligne par ligne, afin que je puisse accepter votre réponse. (Vérifiez ma modification de la question à ce sujet).
aularon