Come posso indurire gli script bash per non causare danni se cambiati in futuro?


46

Quindi, ho eliminato la mia cartella home (o, più precisamente, tutti i file a cui avevo accesso in scrittura). Quello che è successo è che ho avuto

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

in uno script bash e, dopo non essere più necessario $build, rimuovendo la dichiarazione e tutti i suoi usi - ma il rm. Bash si espande felicemente a rm -rf /*. Sì.

Mi sono sentito stupido, ho installato il backup, ho rifatto il lavoro perso. Cercando di superare la vergogna.

Ora, mi chiedo: quali sono le tecniche per scrivere script bash in modo che tali errori non possano accadere, o almeno siano meno probabili? Ad esempio, avevo scritto

FileUtils.rm_rf("#{build}/*")

in una sceneggiatura di Ruby, l'interprete si sarebbe lamentato di buildnon essere stato dichiarato, quindi la lingua mi protegge.

Ciò che ho considerato in Bash, oltre al corraling rm(che, come menzionano molte risposte in domande correlate, non è problematico):

  1. rm -rf "./${build}/"*
    Ciò avrebbe ucciso il mio lavoro attuale (un repository Git) ma nient'altro.
  2. Una variante / parametrizzazione rmche richiede interazione quando agisce al di fuori della directory corrente. (Impossibile trovare.) Effetto simile.

È così, o ci sono altri modi per scrivere script bash che sono "robusti" in questo senso?


3
Non c'è ragione per rm -rf "${build}/*non importa dove vanno le virgolette. rm -rf "${build} farà la stessa cosa a causa del f.
Monty Harder,

1
Il controllo statico con shellcheck.net è un punto di partenza molto solido. È disponibile l'integrazione dell'editor, quindi potresti ricevere un avviso dai tuoi strumenti non appena rimuovi la definizione di qualcosa che è ancora utilizzato.
Charles Duffy,

@CharlesDuffy da aggiungere alla mia vergogna, ho scritto lo script in un IDE IDEA-stile con installato BashSupport, che non avvertono in questo caso. Quindi sì, punto valido, ma avevo davvero bisogno di un duro annullamento.
Raffaello,

2
Gotcha. Assicurati di notare le avvertenze in BashFAQ # 112 . set -unon è così malvisto come set -eè, ma ha ancora i suoi gotchas.
Charles Duffy,

2
risposta scherzo: basta mettere #! /usr/bin/env rubyin cima a ogni script shell e dimenticare bash;)
Pod

Risposte:


75
set -u

o

set -o nounset

Ciò renderebbe l'attuale shell trattare le espansioni di variabili non impostate come un errore:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -ue set -o nounsetsono opzioni di shell POSIX .

Un vuoto valore sarebbe non attivare un errore però.

Per quello, usa

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

L'espansione di ${variable:?word}si espanderebbe al valore di a variablemeno che non sia vuota o non impostata. Se è vuoto o non impostato, wordverrà visualizzato in caso di errore standard e la shell tratterà l'espansione come un errore (il comando non verrebbe eseguito e, se eseguito in una shell non interattiva, terminerebbe). Se si lascia il campo :, l'errore si innescherebbe solo per un valore non impostato, proprio come sotto set -u.

${variable:?word}è un'espansione dei parametri POSIX .

Nessuno di questi avrebbe causato la chiusura di una shell interattiva a meno che set -e(o set -o errexit) non fosse attivo. ${variable:?word}provoca la chiusura degli script se la variabile è vuota o non impostata. set -ufarebbe uscire uno script se usato insieme a set -e.


Per quanto riguarda la tua seconda domanda. Non è possibile limitare il rmnon funzionamento al di fuori della directory corrente.

L'implementazione GNU di rmha --one-file-systemun'opzione che gli impedisce di eliminare in modo ricorsivo i filesystem montati, ma è il più vicino che credo che possiamo ottenere senza racchiudere la rmchiamata in una funzione che controlla effettivamente gli argomenti.


Come nota a margine: ${build}è esattamente equivalente a $buildmeno che l'espansione non avvenga come parte di una stringa in cui il carattere immediatamente successivo è un carattere valido in un nome di variabile, come in "${build}x".


Molto bene, grazie! 1) È ${build:?msg}o ${build?msg}? 2) Nel contesto di qualcosa come script di build, penso che usare uno strumento diverso da rm per una cancellazione più sicura andrebbe bene: sappiamo che non vogliamo lavorare al di fuori della directory corrente, quindi lo rendiamo esplicito usando un comando. Non è necessario rendere l'RM più sicuro in generale.
Raffaello,

1
@Raphael Mi dispiace, dovrebbe essere con:. Ora correggerò quel refuso e ne parlerò più tardi quando tornerò al mio computer.
Kusalananda

2
@Raphael Ok, ho aggiunto una breve frase su ciò che accade senza il :(causerebbe l'errore solo per le variabili non impostate ). Non oso tentare di scrivere uno strumento in grado di far fronte all'eliminazione di file esclusivamente in un percorso specifico, in ogni circostanza. Analizzare percorsi e preoccuparsi / non preoccuparsi di collegamenti simbolici ecc. È un po 'troppo complicato per me al momento.
Kusalananda

1
Grazie, la spiegazione aiuta! Per quanto riguarda il "rm locale": stavo riflettendo principalmente, forse pescando un "sicuro, questo è <toolname>" - ma certamente non cercavo di aiutarti a farti diventare un vampiro per scriverlo! OO Tutto bene, hai aiutato abbastanza! :)
Raffaello

2
@MateuszKonieczny Non credo che lo farò, scusa. Credo che misure di sicurezza come queste dovrebbero essere utilizzate quando necessario . Come per tutto ciò che rende un ambiente sicuro, alla fine renderà le persone sempre più incuranti e dipendenti dalle misure di sicurezza. È meglio sapere cosa fa ogni singola misura di sicurezza e quindi applicarle selettivamente secondo necessità.
Kusalananda

12

Suggerirò i normali controlli di validazione usando test/[ ]

Saresti stato al sicuro se avessi scritto la tua sceneggiatura come tale:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

I [ -n "${build}" ]controlli che "${build}"sono una stringa di lunghezza diversa da zero .

Il ||è l'operatore logico OR in bash. Fa eseguire un altro comando se il primo fallisce.

In questo modo, era ${build}stato vuoto / indefinito / ecc. lo script sarebbe uscito (con un codice di ritorno 1, che è un errore generico).

Anche questo ti avrebbe protetto nel caso in cui hai rimosso tutti gli usi ${build}perché [ -n "" ]sarà sempre falso.


Il vantaggio di usare test/ [ ]è che ci sono molti altri controlli più significativi che può anche usare.

Per esempio:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.

Giusto, ma un po 'ingombrante. Mi sto perdendo qualcosa o fa più o meno la stessa ${variable:?word}cosa che propone @Kusalananda?
Raphael,

@Raphael funziona in modo simile nel caso di "la stringa ha un valore" ma test(ie [) ha molti altri controlli che sono rilevanti, come -d(l'argomento è una directory), -f(l'argomento è un file), -O(il file è di proprietà dell'utente corrente) e così via.
Centimane,

1
@Raphael, inoltre, la validazione dovrebbe essere una parte normale di qualsiasi codice / script. Inoltre, se avessi rimosso tutte le istanze di build, non le avresti rimosse ${build:?word}insieme? Il ${variable:?word}formato non ti protegge se rimuovi tutte le istanze della variabile.
Centimane,

1
1) Penso che ci sia una differenza tra assicurarsi che le ipotesi siano valide (esistono file, ecc.) E controllare se le variabili sono impostate (come lavoro di un compilatore / interprete). Se devo fare quest'ultimo a mano, è meglio che ci sia una sintassi ultra-accattivante, che non è un vero e proprio colpo if. 2) "non avresti rimosso anche $ {build:? Word} insieme ad esso" - lo scenario è che ho perso un uso della variabile. L'uso ${v:?w}mi avrebbe protetto dai danni. Se avessi rimosso tutti gli usi, anche il semplice accesso non sarebbe stato dannoso, ovviamente.
Raffaello,

Detto questo, la tua risposta è una risposta equa e attenta alla domanda generale generale: assicurarsi che le ipotesi siano valide per gli script che restano in giro. Khoalananda risponde meglio alla questione specifica del corpo delle domande. Grazie!
Raffaello,

4

Nel tuo caso specifico, in passato ho rielaborato la 'cancellazione' per spostare invece file / directory (supponendo che / tmp si trovi sulla stessa partizione della tua directory):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

Dietro le quinte, questo sposta i riferimenti di file / dir di livello superiore dall'origine alle $trashdirstrutture di directory di destinazione tutte sulla stessa partizione e non passa il tempo a camminare sulla struttura di directory e a liberare i blocchi del disco per file proprio allora. Questo produce una pulizia molto più veloce mentre il sistema è in uso attivo, in cambio di un riavvio leggermente più lento (/ tmp viene pulito al riavvio).

In alternativa, una voce cron per pulire periodicamente /tmp/.trash-$USER manterrà / tmp dal riempimento, per i processi (ad esempio build) che consumano molto spazio su disco. Se la tua directory si trova su una partizione diversa come / tmp, puoi creare una directory simile / tmp-like sulla tua partizione e avere cron clean invece.

Ancora più importante, tuttavia, se si rovinano le variabili in qualche modo, è possibile ripristinare i contenuti prima che avvenga la pulizia.


2
"Questo produce una pulizia molto più veloce mentre il sistema è in uso attivo" - vero? Pensavo che entrambi riguardassero solo la modifica degli inode interessati. Certamente non è più veloce se si /tmptrova su un'altra partizione (penso che lo sia sempre per me, almeno per gli script eseguiti nello spazio utente); quindi, la cartella cestino deve essere modificata (e non trarrà vantaggio dalla gestione del sistema operativo di /tmp).
Raffaello,

1
È possibile utilizzare mktemp -dper ottenere quella directory temporanea senza calpestare le dita dei piedi di un altro processo (e onorare correttamente $TMPDIR).
Toby Speight,

Aggiunti i tuoi appunti accurati e utili da entrambi, grazie. Penso di averlo pubblicato troppo rapidamente in origine senza considerare i punti che hai sollevato.
user117529,

2

Utilizzare la sostituzione del parametro bash per assegnare i valori predefiniti quando la variabile non è inizializzata, ad esempio:

rm -rf $ {variabile: - "/ inesistente"}



1

Il consiglio generale per verificare se la variabile è impostata, è uno strumento utile per prevenire questo tipo di problema. Ma in questo caso c'era una soluzione più semplice.

Molto probabilmente non è necessario globare i contenuti della $builddirectory per eliminarli, ma non la $builddirectory stessa. Quindi, se salti l'estraneo, *un valore non impostato si trasformerebbe in quello rm -rf /che per impostazione predefinita la maggior parte delle implementazioni di rm nell'ultimo decennio rifiuterà di eseguire (a meno che tu non disabiliti questa protezione con l' --no-preserve-rootopzione GNU rm ).

Saltare la finale /così comporterebbe rm ''che risulterebbe nel messaggio di errore:

rm: can't remove '': No such file or directory

Funziona anche se il tuo comando rm non implementa la protezione per /.


-2

Stai pensando in termini di linguaggio di programmazione, ma bash è un linguaggio di scripting :-) Quindi, usa un paradigma di istruzioni completamente diverso per un paradigma di linguaggio completamente diverso .

In questo caso:

rmdir ${build}

Poiché rmdirrifiuterà di rimuovere una directory non vuota, è necessario rimuovere prima i membri. Sai cosa sono quei membri, vero? Probabilmente sono parametri del tuo script o derivati ​​da un parametro, quindi:

rm -rf ${build}/${parameter}
rmdir ${build}

Ora, se inserisci altri file o directory come un file temporaneo o qualcosa che non avresti dovuto, rmdirgenererà un errore. Gestiscilo correttamente, quindi:

rmdir ${build} || build_dir_not_empty "${build}"

Questo modo di pensare mi è servito bene perché ... sì, ci sono stato, l'ho fatto.


6
Grazie per il tuo impegno, ma questo è completamente fuori dal comune. L'ipotesi "sai cosa sono quei membri" non è corretta; pensa all'output del compilatore. make cleanutilizzerà alcuni caratteri jolly (a meno che un compilatore molto diligente non crei un elenco di tutti i file che crea). Inoltre, mi sembra che aver rm -rf ${build}/${parameter}spostato il problema solo leggermente verso il basso.
Raffaello,

Hm. Guarda come si legge. "Grazie, ma questo è per qualcosa che non ho rivelato nella domanda originale, quindi non solo respingerò la tua risposta, ma la declasserò, nonostante sia più applicabile al caso generale." Wow.
Rich

"Consentitemi di fare ipotesi aggiuntive oltre a quelle che hai scritto nella domanda e quindi di scrivere una risposta specializzata." * spallucce * (FYI, non che si tratta di affari tuoi: ho fatto non downvote Probabilmente avrei dovuto, perché la risposta non era utile (per me, almeno)..)
Raphael

Hm. Non ho formulato ipotesi e ho scritto una risposta generale . Non c'è assolutamente nulla makenella domanda. Scrivere una risposta applicabile solo makesarebbe un'ipotesi molto specifica e sorprendente. La mia risposta funziona per casi diversi da make, diversi dallo sviluppo di software, diversi dal tuo specifico problema per i principianti che stavi riscontrando eliminando le tue cose.
Rich

1
Kusalananda mi ha insegnato a pescare. Ti sei avvicinato e hai detto: "Dato che vivi in ​​pianura, perché non mangi carne invece?"
Raffaello,
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.