Come leggere l'intero script della shell prima di eseguirlo?


35

Di solito, se modifichi uno scrpit, tutti gli usi in esecuzione dello script sono soggetti a errori.

Per quanto ho capito, bash (anche altre shell?) Legge lo script in modo incrementale, quindi se hai modificato il file di script esternamente, inizia a leggere le cose sbagliate. C'è un modo per impedirlo?

Esempio:

sleep 20

echo test

Se esegui questo script, bash leggerà la prima riga (diciamo 10 byte) e andrà a dormire. Quando riprende, ci possono essere diversi contenuti nello script a partire dal decimo byte. Potrei essere nel mezzo di una riga nella nuova sceneggiatura. Pertanto lo script in esecuzione verrà interrotto.


Cosa intendi con "modifica dello script esternamente"?
Maulinglawns

1
Forse c'è un modo per racchiudere tutto il contenuto in una funzione o qualcosa del genere, quindi la shell leggerà prima l'intero script? Ma per quanto riguarda l'ultima riga in cui invochi la funzione, verrà letta fino a EOF? Forse omettere l'ultimo \nfarebbe il trucco? Forse una subshell ()farà? Non ne ho molta esperienza, per favore aiutatemi!
VasyaNovikov,

@maulinglawns se lo script ha contenuti simili sleep 20 ;\n echo test ;\n sleep 20e comincio a modificarlo, potrebbe non funzionare correttamente. Ad esempio, bash potrebbe leggere i primi 10 byte dello script, comprendere il sleepcomando e andare in modalità di sospensione. Dopo che riprende, ci saranno diversi contenuti nel file a partire da 10 byte.
VasyaNovikov,

1
Quindi, quello che stai dicendo è che stai modificando uno script che sta eseguendo? Interrompi prima lo script, esegui le modifiche e quindi riavvialo.
Maulinglawns

@maulinglawns sì, è praticamente tutto. Il problema è che non è conveniente per me interrompere gli script ed è difficile ricordare sempre di farlo. Forse c'è un modo per forzare bash a leggere prima l'intera sceneggiatura?
VasyaNovikov,

Risposte:


43

Sì, le shell, e bashin particolare, stanno attenti a leggere il file una riga alla volta, quindi funziona allo stesso modo di quando lo usi in modo interattivo.

Noterai che quando il file non è ricercabile (come una pipe), bashlegge anche un byte alla volta per essere sicuro di non leggere oltre il \ncarattere. Quando il file è ricercabile, si ottimizza leggendo i blocchi completi alla volta, ma cerca nuovamente dopo \n.

Ciò significa che puoi fare cose come:

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

Oppure scrivi script che si aggiornano da soli. Cosa che non saresti in grado di fare se non ti desse quella garanzia.

Ora, è raro che tu voglia fare cose del genere e, come hai scoperto, quella caratteristica tende a interferire più spesso di quanto sia utile.

Per evitarlo, puoi provare ad assicurarti di non modificare il file sul posto (ad esempio, modificare una copia e spostare la copia in posizione (come sed -io perl -pie alcuni editor lo fanno ad esempio)).

Oppure potresti scrivere la tua sceneggiatura come:

{
  sleep 20
  echo test
}; exit

(nota che è importante exitessere sulla stessa linea di }; anche se potresti anche metterlo tra le parentesi appena prima di quello di chiusura).

o:

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

La shell dovrà leggere lo script fino a quando exitprima di iniziare a fare qualsiasi cosa. Ciò garantisce che la shell non leggerà più dallo script.

Ciò significa che l'intero script verrà comunque archiviato in memoria.

Ciò può anche influire sull'analisi dello script.

Ad esempio, in bash:

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

Emetterebbe U + 00E9 codificato in UTF-8. Tuttavia, se lo cambi in:

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

Il \ue9sarà ampliato nel charset che era in vigore al momento che il comando è stato analizzato che in questo caso è prima il exportviene eseguito il comando.

Si noti inoltre che se si utilizza il comando sourceaka ., con alcune shell, si avrà lo stesso tipo di problema per i file di origine.

Questo non è il caso di chi, il bashcui sourcecomando legge il file completamente prima di interpretarlo. Se si scrive in modo bashspecifico, è possibile utilizzarlo aggiungendo all'inizio dello script:

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

(Non farei affidamento su questo, anche se, come puoi immaginare, le versioni future bashpotrebbero cambiare quel comportamento che può essere attualmente visto come una limitazione (bash e AT&T ksh sono le uniche shell tipo POSIX che si comportano in questo modo per quanto ne so) e il already_sourcedtrucco è un po 'fragile in quanto presuppone che la variabile non sia nell'ambiente, per non parlare del fatto che influenza il contenuto della variabile BASH_SOURCE)


@VasyaNovikov, al momento sembra esserci qualcosa di sbagliato in SE (o almeno per me). C'erano solo un paio di risposte quando ho aggiunto il mio, e il tuo commento sembra essere arrivato solo ora anche se dice che è stato pubblicato 16 minuti fa (o forse sono solo io a perdere i miei biglie). Ad ogni modo, nota l'ulteriore "uscita" che è necessario qui per evitare problemi quando la dimensione del file aumenta (come indicato nel commento che ho aggiunto alla tua risposta).
Stéphane Chazelas,

Stéphane, penso di aver trovato un'altra soluzione. È da usare }; exec true. In questo modo, non ci sono requisiti per i newline alla fine del file, il che è amichevole per alcuni editor (come emacs). Tutti i test a cui ho potuto pensare di funzionare correttamente}; exec true
VasyaNovikov,

@VasyaNovikov, non sono sicuro di cosa intendi. Com'è meglio di }; exit? Stai anche perdendo lo stato di uscita.
Stéphane Chazelas,

Come menzionato in una domanda diversa: è comune analizzare prima l'intero file e quindi eseguire l'istruzione composta nel caso in cui . scriptvenga utilizzato il comando punto ( ).
schily,

@schily, sì, lo dico in questa risposta come una limitazione di AT&T ksh e bash. Altre shell di tipo POSIX non hanno questa limitazione.
Stéphane Chazelas,

12

Devi semplicemente eliminare il file (cioè copiarlo, eliminarlo, rinominare la copia con il nome originale). In effetti molti editor possono essere configurati per fare questo per te. Quando si modifica un file e si salva un buffer modificato su di esso, invece di sovrascriverlo, il file verrà rinominato, il nuovo verrà creato e il nuovo contenuto verrà inserito nel nuovo file. Quindi qualsiasi script in esecuzione dovrebbe continuare senza problemi.

Usando un semplice sistema di controllo della versione come RCS che è prontamente disponibile per vim ed emacs, ottieni il doppio vantaggio di avere una cronologia delle tue modifiche e il sistema di checkout dovrebbe rimuovere il file corrente e ricrearlo con le modalità corrette per impostazione predefinita. (Fai attenzione a non collegare questi file ovviamente).


"delete" in realtà non fa parte del processo. Se vuoi renderlo correttamente atomico, fai una ridenominazione sul file di destinazione: se hai un passaggio di eliminazione, c'è il rischio che il processo muoia dopo l'eliminazione ma prima della ridenominazione, senza lasciare alcun file in atto ( o un lettore tenta di accedere al file in quella finestra e non trova versioni né vecchie né nuove disponibili).
Charles Duffy,

11

La soluzione più semplice:

{
  ... your code ...

  exit
}

In questo modo, bash leggerà l'intero {}blocco prima di eseguirlo e la exitdirettiva farà in modo che nulla venga letto al di fuori del blocco di codice.

Se non si desidera "eseguire" lo script, ma piuttosto "sottrarlo", è necessaria una soluzione diversa. Questo dovrebbe funzionare quindi:

{
  ... your code ...

  return 2>/dev/null || exit
}

O se vuoi il controllo diretto sul codice di uscita:

{
  ... your code ...

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

Ecco! Questo script è sicuro da modificare, creare ed eseguire. Devi ancora essere sicuro di non modificarlo in quei millisecondi quando viene inizialmente letto.


1
Quello che ho scoperto è che non vede EOF e smette di leggere il file, ma si aggroviglia nel suo processo di "flusso bufferizzato" e finisce per cercare oltre la fine del file, motivo per cui sembra a posto se la dimensione di il file aumenta di non molto, ma sembra male quando lo si fa più del doppio di prima. A breve segnalerò un bug ai manutentori di bash.
Stéphane Chazelas,


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
terdon

5

Verifica teorica. Ecco uno script che si modifica da solo:

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

vediamo stampare la versione modificata

Questo perché i carichi di bash mantengono un handle di file da aprire allo script, quindi le modifiche al file verranno visualizzate immediatamente.

Se non si desidera aggiornare la copia in memoria, scollegare il file originale e sostituirlo.

Un modo per farlo è usare sed -i.

sed -i '' filename

verifica teorica

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

Se si utilizza un editor per modificare lo script, abilitare la funzione "conserva una copia di backup" potrebbe essere tutto ciò che è necessario per fare in modo che l'editor scriva la versione modificata in un nuovo file anziché sovrascrivere quello esistente.


2
No, bashnon apre il file con mmap(). È solo attento a leggere una riga alla volta secondo necessità, proprio come quando riceve i comandi da un dispositivo terminale quando è interattivo.
Stéphane Chazelas,

2

Avvolgere lo script in un blocco {}è probabilmente l'opzione migliore ma richiede la modifica degli script.

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

sarebbe la seconda migliore opzione (supponendo tmpfs ) lo svantaggio è che si rompe $ 0 se i tuoi script lo usano.

usare qualcosa di simile F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashè meno ideale perché deve mantenere l'intero file in memoria e spezzare $ 0.

toccare il file originale dovrebbe essere evitato in modo che, l'ultima volta modificata, i blocchi di lettura e gli hard link non siano disturbati. in questo modo è possibile lasciare un editor aperto durante l'esecuzione del file e rsync non farà inutilmente il checksum del file per i backup e i collegamenti fissi come previsto.

la sostituzione del file in modifica avrebbe funzionato ma è meno robusta perché non è applicabile ad altri script / utenti / o si potrebbe dimenticare. E di nuovo spezzerebbe i collegamenti duri.


tutto ciò che fa una copia funzionerebbe. tac test.sh | tac | bash
Jasen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.