Po co sprawdzać istnienie pliku przed jego uzyskaniem?

13

Czy przy próbie źródła pliku nie chcesz, aby błąd wskazywał, że plik nie istnieje, więc wiesz, co naprawić?

Na przykład nvm zaleca dodanie tego do twojego profilu / rc:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

Z powyższym, jeśli nvm.shnie istnieje, pojawi się „cichy błąd”. Ale jeśli spróbujesz . "$NVM_DIR/nvm.sh", wynik będzie FILE_PATH: No such file or directory.

JBallin
źródło
3
Nie powinieneś To jest rasowe. Spróbuj użyć źródła, a następnie obsłużyć błąd, jeśli występuje
Mikel
2
@Mikel racja! to dlaczego widzę to wszędzie?
JBallin,
3
@ FaheemMitha, cóż, jeśli to, co robisz, jest w ogóle wrażliwe na bezpieczeństwo (tj. Twój program działa w czyimś imieniu), to naprawdę musisz dbać o warunki wyścigu ( TOCTOU ). Prawdopodobnie nie jest tak w tym przypadku, ponieważ masz większe problemy, jeśli ktoś może zmodyfikować pliki HOME.
ilkkachu
8
@FaheemMitha Nigdy nie jest lepiej najpierw sprawdzić istnienie. Sytuacja może się zmieniać między testem a użyciem, dając zarówno fałszywie dodatnie, jak i fałszywe negatywy: i tak nadal musisz poradzić sobie z niepowodzeniem podczas używania. Jak już wspomniałeś o wydajności, testowanie przed użyciem jest endemicznie dwa razy wolniejsze niż brak działania i umożliwienie systemowi zrobienia tego, co i tak zrobi. Nie może być.
user207421,
1
@ SimonRichter, w tym przypadku nvm nie będzie działać. Użytkownik musiałby dowiedzieć się, że brakuje pliku na własną rękę.
JBallin,

Odpowiedzi:

25

W powłokach POSIX .jest specjalnym wbudowanym narzędziem, więc jego awaria powoduje zamknięcie powłoki (w niektórych takich powłokach bashrobi się to tylko w trybie POSIX).

To, co kwalifikuje się jako błąd, zależy od powłoki. Nie wszystkie z nich wychodzą po błędzie składni podczas analizowania pliku, ale większość kończy działanie, gdy nie można znaleźć lub otworzyć pliku źródłowego. Nie znam żadnego, który by się zakończył, gdyby ostatnie polecenie w pliku źródłowym zwróciło z niezerowym statusem wyjścia (chyba że errexitopcja jest oczywiście włączona).

Tutaj robiąc:

[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

Jest to przypadek, w którym chcesz pobrać plik, jeśli on tam jest, i nie rób tego, jeśli go nie ma (lub jest pusty tutaj -s).

Oznacza to, że nie należy go uważać za błąd (błąd krytyczny w powłokach POSIX), jeśli pliku nie ma, plik ten jest uważany za plik opcjonalny.

Nadal byłby (krytyczny) błąd, gdyby plik nie był czytelny lub był katalogiem lub (w niektórych powłokach), jeśli wystąpił błąd składniowy podczas analizowania go, co byłoby prawdziwymi warunkami błędu, które należy zgłosić.

Niektórzy twierdzą, że istnieje warunek wyścigu. Ale jedyne co to oznacza, to że powłoka wyjdzie z błędem, jeśli plik zostanie usunięty pomiędzy [i ., ale argumentuję, że uzasadnione jest uznanie za błąd, że ten ustalony plik ścieżki nagle zniknie, gdy skrypt jest bieganie.

Z drugiej strony,

command . "$NVM_DIR/nvm.sh" 2> /dev/null

gdzie command¹ usuwa specjalny atrybut .polecenia (aby nie opuścił powłoki po błędzie) nie działałby, ponieważ:

  • ukrywałoby .to błędy, ale także błędy poleceń uruchamianych w pliku źródłowym
  • ukryłoby to również rzeczywiste warunki błędu, takie jak plik mający nieprawidłowe uprawnienia.

Inne typowe składnie (zobacz na przykład grep -r /etc/default /etc/init*w systemach Debian skrypty inicjujące, na które nie zostały systemdjeszcze przekonwertowane (gdzie EnvironmentFile=-/etc/default/servicezamiast tego określa się opcjonalny plik środowiska)):

  • [ -e "$file" ] && . "$file"

    Sprawdź plik, który tam jest, wciąż go źródłuj, jeśli jest pusty. Nadal błąd krytyczny, jeśli nie można go otworzyć (nawet jeśli już tam jest lub był). Możesz zobaczyć więcej wariantów, takich jak [ -f "$file" ](istnieje i jest zwykłym plikiem), [ -r "$file" ](jest czytelny) lub ich kombinacje.

  • [ ! -e "$file" ] || . "$file"

    Nieco lepsza wersja. Wyjaśnia, że ​​nieistniejący plik jest przypadkiem OK. Oznacza to również, że $?będzie odzwierciedlał status wyjścia ostatniego polecenia uruchomionego w $file(w poprzednim przypadku, jeśli tak 1, nie wiesz, czy to dlatego, $fileże nie istniało, czy to polecenie nie powiodło się).

  • command . "$file"

    Spodziewaj się, że plik tam będzie, ale nie zamykaj go, jeśli nie można go zinterpretować.

  • [ ! -e "$file" ] || command . "$file"

    Kombinacja powyższych: jest OK, jeśli pliku nie ma, a dla powłok POSIX nieudane otwarcie (lub parsowanie) pliku jest zgłaszane, ale nie jest krytyczne (co może być bardziej pożądane ~/.profile).


¹ Uwaga: zshJednak nie można tego używać command, chyba że w shemulacji; zwróć uwagę, że w powłoce Korna sourcejest tak naprawdę pseudonimem command ., niespecjalnym wariantem.

Stéphane Chazelas
źródło
Ciekawy! Nie wiedziałem tego o POSIX sh. Ale pytanie dotyczyło .bash_profile. Chyba lepiej, niż przepraszam, ale czy bash jest kiedykolwiek w trybie POSIX, kiedy .bash_profilejest pozyskiwany?
Mikel
(Zdaję sobie sprawę, że można interpretować to pytanie jako szersze zastosowanie do wszystkich powłok POSIX opartych na czytaniu linku źródłowego nvm w pytaniu.)
Mikel
@Mikel, moja odpowiedź wciąż dotyczy, bashgdy nie jest w trybie POSIX. Chcesz, [ -e /file ] && . /filejeśli nie uważasz, że to błąd, gdy plik nie istnieje. Następnie spróbuj użyć źródła, jeśli nie można tego zrobić tutaj.
Stéphane Chazelas,
1
@Mikel, to przynosi efekt przeciwny do zamierzonego. 1), który nie zapobiega wyjściu po błędzie z powłokami POSIX (lub bash w trybie POSIX) 2), który powiela komunikat o błędzie (twój na stdout), .już zgłosi błąd (na stderr). A jeśli chodzi o to, aby nie uważać go za błąd, gdy plik nie istnieje, jest to niepoprawne (i nie jest to możliwe, ze statusu wyjścia stwierdzić, czy .nie powiodło się, ponieważ plik nie istniał lub był nieczytelny lub był brak możliwości przetworzenia lub ostatnie polecenie nie powiodło się), o których tutaj mówię w tej odpowiedzi.
Stéphane Chazelas,
1
W odniesieniu do warunków wyścigu - o wiele mniejszym problemem jest to, że IMHO raz się nie udaje (podczas gdy coś dziwnego dzieje się w systemie) niż konsekwentnie się nie udaje (nawet jeśli w konfiguracji użytkownika jest coś nie tak) -akta). Zatem warunek wyścigu w tej kontroli jest wciąż lepszy niż brak kontroli.
ruakh
5

Opiekun nvmodpowiedzi:

łatwo jest odinstalować NVM po prostu usuwając plik; zmuszanie dodatkowej pracy (aby wyśledzić, gdzie linie są źródłowym źródłem nvm) nie wydaje się szczególnie cenne.

Moja interpretacja (w połączeniu z doskonałym wyjaśnieniem Stéphane'a i komentarzem Kusalanandy):

To jest prostsze i bezpieczniejsze.

Chroni przed powłokami POSIX wychodzącymi podczas uruchamiania z powodu brakującego pliku (z różnych powodów). Osoby używające powłok innych niż POSIX (np. Bash) mogą usunąć warunek, jeśli wolą.

JBallin
źródło
1
Sprzeciwiam się twojej interpretacji, że „jest skierowana do początkujących”. Jest defensywny. Nie chcesz, aby powłoka logowania użytkownika nieoczekiwanie kończyła się podczas uruchamiania tylko dlatego, że ten plik zaginął (z jakiegokolwiek powodu). Jeśli ten wiersz kodu znajduje się w jednym z plików inicjujących powłokę /etc, oznacza to, że niektórzy użytkownicy mają ten plik, a niektórzy go nie mają. IMHO, odpowiedź nvmopiekuna dotyczy tylko jednego aspektu.
Kusalananda
1

Jak zauważyli JBallin i Stéphane Chazelas , w powłokach POSIX pozyskanie pliku, który nie istnieje, spowodowałby niepowodzenie logowania.

Ale dodanie testu, aby sprawdzić, czy plik istnieje, a następnie próba jego uzyskania może spowodować coś, co nazywa się warunkiem wyścigu. Jeśli coś zmieni się nvm.shpomiędzy [ -s nvm.sh ]i . nvm.sh, spowoduje dokładnie błąd, którego próbują zapobiec, aczkolwiek znacznie rzadziej.

Ogólnie rzecz biorąc, sposobem na zapobieganie warunkom wyścigowym jest po prostu wypróbowanie tego, co chcesz zrobić, a następnie usunięcie błędu, jeśli się nie powiedzie, np.

. "$NVM_DIR/nvm.sh" || echo "Sourcing $NVM_DIR/nvm.sh failed" >&2

Okazuje się, że to nie działa w powłokach POSIX, ponieważ, jak wyżej, .niepowodzenie spowoduje natychmiastowe zamknięcie powłoki, zanim będzie można uruchomić obsługę błędów.

Moja odpowiedź dowodzi, że powłoki POSIX nie mają związku z tym pytaniem, ponieważ .bash_profilenigdy nie powinny działać w trybie POSIX. W każdym razie możemy po prostu zrobić powyższy kod.

Aby być najbezpieczniejszym, możemy upewnić się, że tryb POSIX nie działa lub że tryb POSIX zostanie wyłączony przy użyciu techniki opisanej w /unix//a/383581/3169 .

Odpowiedź Stéphane'a zawiera kilka użytecznych sugestii dotyczących sposobu obsługi wszystkich powłok POSIX, co moim zdaniem było intencją autora nvm, ale było nieco inne niż pytanie zadane tutaj, dlatego mamy wiele możliwych podejść, w zależności od tego, jaki jest twój cel .

Mikel
źródło