Jak mogę wzmocnić skrypty bash przed spowodowaniem szkody, gdy zostaną zmienione w przyszłości?

46

Tak więc usunąłem mój folder domowy (a ściślej wszystkie pliki, do których miałem dostęp do zapisu). Stało się to, że miałem

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

w skrypcie bash i, po nieużywaniu $build, usuwając deklarację i wszystkie jej zastosowania - ale rm. Bash z radością się rozwija rm -rf /*. Tak.

Czułam się głupio, zainstalowałam kopię zapasową, przerobiłam utraconą pracę. Próbuję przejść obok wstydu.

Zastanawiam się: jakie są techniki pisania skryptów bash, aby takie błędy nie mogły się zdarzyć, a przynajmniej były mniej prawdopodobne? Na przykład, gdybym napisał

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

w skrypcie Rubiego tłumacz narzekałby, że buildnie został zadeklarowany, więc język mnie chroni.

To, co rozważałem w bashu, oprócz korygowania rm(które, jak wspomina wiele odpowiedzi w powiązanych pytaniach, nie jest bezproblemowe):

  1. rm -rf "./${build}/"*
    To zabiłoby moją obecną pracę (repozytorium Git), ale nic więcej.
  2. Wariant / parametryzacja rmtego wymaga interakcji, gdy działa poza bieżącym katalogiem. (Nie można znaleźć żadnego.) Podobny efekt.

Czy to jest, czy istnieją inne sposoby pisania skryptów bash, które są „solidne” w tym sensie?

Raphael
źródło
3
Nie ma powodu, rm -rf "${build}/*bez względu na to, dokąd idą znaki cudzysłowu. rm -rf "${build} zrobi to samo z powodu f.
Monty Harder
1
Sprawdzanie statyczne za pomocą shellcheck.net to bardzo solidne miejsce na rozpoczęcie. Dostępna jest integracja z edytorem, dzięki czemu możesz otrzymać ostrzeżenie z narzędzi, gdy tylko usuniesz definicję czegoś, co jest nadal używane.
Charles Duffy
@CharlesDuffy dodać do mojego wstydu, napisałem ten scenariusz w IDE IDEA stylu z zainstalowanym BashSupport, który ma ostrzegać w tej sprawie. Tak, słuszna uwaga, ale naprawdę potrzebowałem twardego anulowania.
Raphael
2
Gotcha Pamiętaj o zastrzeżeniach w BashFAQ # 112 . set -unie jest tak marszczony, jak set -ejest, ale nadal ma swoje wady.
Charles Duffy
2
odpowiedź żartu: po prostu umieść #! /usr/bin/env rubyna górze każdego skryptu powłoki i zapomnij o bash;)
Pod

Odpowiedzi:

75
set -u

lub

set -o nounset

Spowodowałoby to, że bieżąca powłoka traktowałaby ekspansje zmiennych nieustalonych jako błąd:

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

set -ui set -o nounsetopcjami powłoki POSIX .

Pusta wartość to nie wywoła błędu chociaż.

W tym celu użyj

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

Rozwinięcie ${variable:?word}spowoduje rozwinięcie do wartości, variablechyba że jest puste lub nieuzbrojone. Jeśli jest pusty lub rozbrojony, wordzostanie wyświetlony przy standardowym błędzie, a powłoka potraktuje rozszerzenie jako błąd (polecenie nie zostanie wykonane, a jeśli uruchomione w powłoce nieinteraktywnej, zostanie zakończone). Pozostawienie :wyjścia spowodowałoby błąd tylko dla wartości nieustawionej, tak jak poniżej set -u.

${variable:?word}jest rozszerzeniem parametru POSIX .

Żadne z nich nie spowodowałoby zakończenia interaktywnej powłoki, chyba że set -e(lub set -o errexit) również działało. ${variable:?word}powoduje zamknięcie skryptów, jeśli zmienna jest pusta lub nieuzbrojona. set -uspowodowałoby zamknięcie skryptu, gdyby był używany razem z set -e.


Co do twojego drugiego pytania. Nie ma możliwości ograniczenia, rmaby nie działać poza bieżącym katalogiem.

Implementacja GNU rmma --one-file-systemopcję, która powstrzymuje go przed rekurencyjnym usuwaniem zamontowanych systemów plików, ale jest to tak blisko, jak uważam, że możemy uzyskać bez zawijania rmwywołania w funkcji, która faktycznie sprawdza argumenty.


Na marginesie: ${build}jest dokładnie równoważny, $buildchyba że rozwinięcie następuje jako część ciągu, w którym znak następujący bezpośrednio jest poprawnym znakiem w nazwie zmiennej, na przykład w "${build}x".

Kusalananda
źródło
Bardzo dobrze, dziękuję! 1) Czy to ${build:?msg}czy ${build?msg}? 2) W kontekście czegoś takiego jak skrypty budowania, myślę, że użycie innego narzędzia niż rm do bezpieczniejszego usuwania byłoby w porządku: wiemy , że nie chcemy pracować poza bieżącym katalogiem, dlatego wyrażamy to wyraźnie, używając ograniczonego Komenda. Generalnie nie ma potrzeby zwiększania bezpieczeństwa rm.
Raphael
1
@Raphael Przepraszamy, powinien być z:. Poprawię teraz tę literówkę i wspomnę o jej znaczeniu później, kiedy wrócę do komputera.
Kusalananda
2
@Raphael Ok, dodałem krótkie zdanie o tym, co dzieje się bez :(spowodowałoby to błąd tylko dla zmiennych nieustawionych ). Nie odważę się pisać narzędzia, które radziłoby sobie z usuwaniem plików wyłącznie pod określoną ścieżką, we wszystkich okolicznościach. Analiza ścieżek i dbanie / nie dbanie o dowiązania symboliczne itp. Jest obecnie dla mnie trochę zbyt skomplikowane.
Kusalananda
1
Dzięki, wyjaśnienie pomaga! Odnośnie do „lokalnego rm”: głównie zastanawiałem się, może szukam „pewnie, to <nazwa>” - ale na pewno nie próbuję pomóc wampirom w napisaniu tego! OO Wszystko dobrze, pomogłeś wystarczająco! :)
Raphael
2
@MateuszKonieczny Nie sądzę, przepraszam. Uważam, że takie środki bezpieczeństwa powinny być stosowane w razie potrzeby . Podobnie jak w przypadku wszystkiego, co zapewnia bezpieczeństwo środowisku, w końcu sprawi, że ludzie będą coraz bardziej nieostrożni i uzależnieni od środków bezpieczeństwa. Lepiej wiedzieć, co robi każdy środek bezpieczeństwa, a następnie stosować je selektywnie w razie potrzeby.
Kusalananda
12

Mam zamiar zasugerować normalne sprawdzanie poprawności za pomocą test/[ ]

Byłbyś bezpieczny, gdybyś napisał swój skrypt jako taki:

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

Te [ -n "${build}" ]kontrole to "${build}"jest długość łańcucha niezerowe .

Jest ||to logiczny operator OR w bash. Powoduje uruchomienie innej komendy, jeśli pierwsza nie powiodła się.

W ten sposób ${build}były puste / niezdefiniowane / itp. skrypt zostałby zakończony (z kodem powrotu 1, co jest błędem ogólnym).

To również ochroniłoby cię na wypadek, gdybyś usunął wszystkie zastosowania, ${build}ponieważ [ -n "" ]zawsze będzie to fałsz.


Zaletą używania test/ [ ]jest to, że istnieje wiele innych bardziej znaczących kontroli, których może również używać.

Na przykład:

[ -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.
Centyman
źródło
Jasne, ale trochę niewygodne. Czy czegoś mi brakuje, czy też robi to prawie to samo, co ${variable:?word}proponuje @Kusalananda?
Raphael
@ Rafał działa podobnie w przypadku „czy ciąg ma wartość”, ale test(tj. [) Ma wiele innych istotnych sprawdzeń, takich jak -d(argument jest katalogiem), -f(argument jest plikiem), -O(plik jest własnością bieżącego użytkownika) i tak dalej.
Centimane
1
@Raphael również, sprawdzanie poprawności powinno być normalną częścią każdego kodu / skryptów. Pamiętaj też, że jeśli usunąłeś wszystkie instancje kompilacji, to czy nie usunąłbyś ich ${build:?word}wraz? Ten ${variable:?word}format nie chroni cię, jeśli usuniesz wszystkie wystąpienia zmiennej.
Centimane
1
1) Myślę, że istnieje różnica między upewnieniem się, że założenia są wstrzymane (pliki istnieją itp.), A sprawdzeniem, czy zmienne są ustawione (zadanie imho kompilatora / interpretera). Jeśli muszę to zrobić ręcznie, lepiej jest zastosować ultra-sprytną składnię - co nie jest w pełni rozwinięte if. 2) „czy nie usunąłbyś również $ {build:? Word} wraz z nim” - scenariusz jest taki, że przegapiłem użycie zmiennej. Używanie ${v:?w}uchroniłoby mnie przed uszkodzeniem. Gdybym usunął wszystkie zwyczaje, nawet zwykły dostęp nie byłby oczywiście szkodliwy.
Raphael,
To powiedziawszy, twoja odpowiedź jest uczciwą i pomocną odpowiedzią na ogólne tytułowe pytanie: upewnienie się, że założenia ważne, dla skryptów, które pozostają w pobliżu. Konkretną kwestią w pytaniu jest, imho, lepiej odpowiedzieć Kusalananda. Dzięki!
Raphael,
4

W twoim konkretnym przypadku w przeszłości przerabiałem „usuwanie”, aby zamiast tego przenosić pliki / katalogi (zakładając, że / tmp znajduje się na tej samej partycji co katalog):

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

Za kulisami przenosi referencje do plików / $trashdirkatalogów najwyższego poziomu ze źródeł do struktur katalogów docelowych na tej samej partycji i nie spędza czasu na chodzeniu po strukturze katalogów i zwalnianiu bloków dyskowych dla poszczególnych plików. Powoduje to znacznie szybsze czyszczenie, gdy system jest w użyciu, w zamian za nieco wolniejszy restart (/ tmp jest czyszczony przy ponownym uruchomieniu).

Alternatywnie, wpis crona do okresowego czyszczenia /tmp/.trash-$USER powstrzyma / tmp przed zapełnieniem, dla procesów (np. Kompilacji), które zajmują dużo miejsca na dysku. Jeśli twój katalog znajduje się na innej partycji jako / tmp, możesz utworzyć podobny katalog / tmp na swojej partycji i zamiast tego mieć cron clean.

Najważniejsze jest jednak, że jeśli w jakikolwiek sposób spieprzysz zmienne, możesz odzyskać zawartość przed czyszczeniem.

użytkownik117529
źródło
2
„Powoduje to znacznie szybsze czyszczenie, gdy system jest w użyciu” - prawda? Wydawało mi się, że oba dotyczą tylko zmiany dotkniętych i-węzłów. Z pewnością nie jest szybszy, jeśli /tmpznajduje się na innej partycji (myślę, że zawsze jest dla mnie, przynajmniej dla skryptów działających w przestrzeni użytkownika); następnie folder kosza musi zostać zmieniony (i nie skorzysta z obsługi systemu operacyjnego /tmp).
Raphael,
1
Możesz użyć mktemp -dtego katalogu tymczasowego bez stąpania po palcach innego procesu (i poprawnie honorować $TMPDIR).
Toby Speight,
Dodałem dokładne i pomocne notatki od was obu, dzięki. Myślę, że pierwotnie opublikowałem to zbyt szybko, nie biorąc pod uwagę podniesionych przez ciebie kwestii.
user117529,
2

Użyj podstawienia parametrów bash, aby przypisać wartości domyślne, gdy zmienne są niezainicjowane, np .:

rm -rf $ {zmienna: - "/ nonexistent"}

rackandboneman
źródło
1

Zawsze staram się zaczynać skrypty Bash od linii #!/bin/bash -ue.

-eoznacza „niepowodzenie przy pierwszym niepowodzeniu e rrr ”;

-uoznacza „nie na pierwszym użyciu u ndeclared zmienną”.

Znajdź więcej szczegółów w świetnym artykule Użyj nieoficjalnego trybu ścisłego bash (chyba, że ​​debugujesz Looove) . Również autor zaleca używanie, set -o pipefail; IFS=$'\n\t'ale dla moich celów jest to przesada.

niya3
źródło
1

Ogólna rada, aby sprawdzić, czy twoja zmienna jest ustawiona, jest przydatnym narzędziem do zapobiegania tego rodzaju problemom. Ale w tym przypadku było prostsze rozwiązanie.

Najprawdopodobniej nie ma potrzeby globalizowania zawartości $buildkatalogu, aby je usunąć, ale nie samego $buildkatalogu. Więc jeśli pominąłeś to, co obce, *wówczas nieustawiona wartość zamieniłaby się w rm -rf /, że domyślnie większość implementacji rm w ostatniej dekadzie odmówiłaby wykonania (chyba że wyłączysz tę ochronę za pomocą opcji GNU rm --no-preserve-root).

Omijając końcowego znaku /, jak również skutkowałoby rm ''która spowodowałaby komunikat o błędzie:

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

Działa to nawet jeśli twoje polecenie rm nie implementuje ochrony /.

eschwartz
źródło
-2

Myślisz w kategoriach języka programowania, ale bash jest językiem skryptowym :-) Więc zastosuj zupełnie inny paradygmat instrukcji dla zupełnie innego paradygmatu języka .

W tym przypadku:

rmdir ${build}

Ponieważ rmdirodmówi usunięcia niepustego katalogu, musisz najpierw usunąć członków. Wiesz, kim są ci członkowie, prawda? Prawdopodobnie są to parametry twojego skryptu lub pochodzą z parametru, więc:

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

Teraz, jeśli umieścisz tam inne pliki lub katalogi, takie jak plik tymczasowy lub coś, czego nie rmdirpowinieneś mieć , spowoduje to błąd. Postępuj właściwie, a następnie:

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

Ten sposób myślenia mi dobrze służył, ponieważ ... tak, byłem tam, zrobiłem to.

Bogaty
źródło
6
Dzięki za twój wysiłek, ale to zupełnie nie do zniesienia. Założenie „wiesz, kim są ci członkowie” jest błędne; pomyśl o wynikach kompilatora. make cleanużyje niektórych symboli wieloznacznych (chyba że bardzo sumienny kompilator utworzy listę wszystkich tworzonych plików). Wydaje mi się też, że rm -rf ${build}/${parameter}problem przesunął się nieco w dół.
Raphael
Hm Zobacz, jak to brzmi. „Dziękuję, ale chodzi o coś, czego nie ujawniłem w pierwotnym pytaniu, więc nie tylko odrzucę twoją odpowiedź, ale też głosuję ją, mimo że ma ona bardziej ogólne zastosowanie”. Łał.
Bogaty
„Pozwolę sobie przyjąć dodatkowe założenia poza tym, co napisałeś w pytaniu, a następnie napisz specjalistyczną odpowiedź”. * * wzruszając ramionami (FYI, że to nie twoja sprawa: ja nie downvote Zapewne powinienem mieć, ponieważ odpowiedź nie była przydatna (dla mnie przynajmniej)..)
Raphael
Hm Nie poczyniłem żadnych założeń i napisałem ogólną odpowiedź. W maketym pytaniu nie ma nic . Napisanie odpowiedzi, która ma zastosowanie tylko do, makebyłoby bardzo konkretnym i zaskakującym założeniem. Moja odpowiedź działa na przypadki inne niż maketworzenie oprogramowania, inne niż konkretny problem z nowicjuszem, który miałeś przez usunięcie swoich rzeczy.
Bogaty
1
Kusalananda nauczył mnie łowić ryby. Przyszedłeś i powiedziałeś: „Skoro mieszkasz na równinach, dlaczego zamiast tego nie jesz wołowiny?”.
Raphael