shellcheck odradza korzystanie z basename: dlaczego?

25

Próbuję sprawdzić shellcheck .

Mam coś takiego

basename "${OPENSSL}" 

i otrzymuję następującą sugestię

Use parameter expansion instead, such as ${var##*/}.

Z praktycznego punktu widzenia nie widzę różnicy

$ export OPENSSL=/opt/local/bin/openssl
$ basename ${OPENSSL}
openssl
$ echo ${OPENSSL##*/}
openssl

Ponieważ basenamejest to specyfikacja POSIX , nie mam powodu, dla którego powinna to być najlepsza praktyka. Jakaś wskazówka?

Matteo
źródło
8
Rozwidla nowy proces, gdy nie jest to konieczne.
jordanm
@jordanm dość uczciwie ... Nie myślałem o wydajności.
Matteo,
3
@jordanm Z drugiej strony działa z powłokami innymi niż bash
Matteo
1
@JosephR. tak myślałem, ale właśnie dowiedziałem się, że to nie działa csh. Chyba cshnie jest to wtedy POSIX.
terdon
3
@terdon - csh jest bardzo daleki od POSIX.
jordanm

Odpowiedzi:

25

Nie chodzi o wydajność - chodzi o poprawność. basenameużywa znaku nowej linii do określenia nazw plików, które drukuje. W zwykłym przypadku, gdy przekazujesz tylko jedną nazwę pliku, dodaje on końcowy znak nowej linii do wyniku. Ponieważ nazwy plików mogą zawierać same znaki nowej linii, utrudnia to prawidłową obsługę tych nazw plików.

To dodatkowo komplikuje fakt, że ludzie zwykle wykorzystać basenametak: "$(basename "$file")". To sprawia, że ​​wszystko jest jeszcze trudniejsze, ponieważ $(command)usuwa wszystkie końcowe znaki nowej linii command. Rozważ mało prawdopodobny przypadek, który $filekończy się nową linią. Następnie basenamedoda nową "$(basename "$file")"linię , ale usunie obie linie, pozostawiając niepoprawną nazwę pliku.

Innym problemem basenamejest to, że jeśli $filezaczyna się od -(myślnik aka minus), będzie to interpretowane jako opcja. Ten jest łatwy do naprawienia:$(basename -- "$file")

Solidny sposób użycia basenamejest następujący:

# A file with three trailing newlines.
file=$'/tmp/evil\n\n\n'

# Add an 'x' so we can tell where $file's newlines end and basename's begin.
file_x="$(basename -- "$file"; printf x)"

# Strip off two trailing characters: the 'x' added by us and the newline added by basename. 
base="${file_x%??}"

Alternatywą jest użycie ${file##*/}, które jest łatwiejsze, ale ma własne błędy. W szczególności jest to złe w przypadkach, gdy $filejest /lub foo/.

Matt
źródło
2
Bardzo dobre punkty, +1. Cieszę się, że OP zaakceptował to zamiast mojego.
terdon
2
Kontrapunkt: co jeśli $filejest foo/? Co jeśli to jest /?
Gilles „SO- przestań być zły”
2
@Gilles: Masz rację. Zaczynam myśleć, że to basenamepodejście jest jednak lepsze, jakkolwiek jest hackerskie. Najlepsze alternatywy, jakie mogę wymyślić, to ${${${file}%%/#}##*/}i [[ $file =~ "([^/]*)/*$" ]] && printf "%s" $match[1]oba są specyficzne dla Zsh i żadna z nich nie obsługuje się /poprawnie.
Matt
@terdon Dzięki, że nie wziąłeś tego osobiście :-). Pliki z nowymi wierszami nie są powszechne, ale Matt ma rację. Oczywiście stosowanie substytucji zmiennych jest również bardziej wydajne.
Matteo,
16

Odpowiednie linie w shellcheck„s kodu źródłowego są:

checkNeedlessCommands (T_SimpleCommand id _ (w:_)) | w `isCommand` "dirname" =
    style id "Use parameter expansion instead, such as ${var%/*}."
checkNeedlessCommands (T_SimpleCommand id _ (w:_)) | w `isCommand` "basename" =
    style id "Use parameter expansion instead, such as ${var##*/}."
checkNeedlessCommands _ = return ()

Nie podano wyraźnego wyjaśnienia, ale na podstawie nazwy funkcji ( checkNeedlessCommands) wygląda na to, że @jordanm ma rację i sugeruje unikanie tworzenia nowego procesu.

terdon
źródło
7
Niech źródło będzie z tobą :)
Joseph R.
3

dirname, basename, readlinkEtc (dzięki @Marco - jest to poprawione) może stworzyć problemy przenośności gdy bezpieczeństwo staje się ważnym (wymagające zabezpieczenia toru). Wiele systemów (takich jak Fedora Linux) umieszcza go w, /binpodczas gdy inne (jak Mac OSX) umieszczają go w /usr/bin. Potem jest Bash na Windowsie, np. Cygwin, msys i inne. Zawsze lepiej pozostać czystym Bash, jeśli to możliwe. (na komentarz @Marco)

BTW, dzięki za wskaźnik do shellcheck, nie widziałem tego wcześniej.

AsymLabs
źródło
1
1) Co rozumiesz przez „bezpieczeństwo”? Czy możesz rozwinąć? 2) PO w ogóle nie wspomina dirname. 3) Podstawowe narzędzia podstawowe powinny znajdować się w ścieżce PATH, gdziekolwiek są przechowywane. Nie widziałem jeszcze systemu, którego basenamenie było w ŚCIEŻCE. 4) Zakładanie, że bash jest dostępny, to problem z przenośnością. Zawsze lepiej trzymać się z daleka od basha i używać powłoki POSIX, gdy wymagana jest przenośność.
Marco,
@Marco Dziękujemy za zwrócenie uwagi na te problemy. Tak, masz rację, że narzędzia są na ścieżce. Ale jeśli ktoś chce zapewnić dodatkowe bezpieczeństwo skryptu Bash, dobrą praktyką jest zapewnienie absolutnej ścieżki. Wywołanie „/ bin / basename” będzie działać w systemach RedHat, ale spowoduje błąd na komputerze Mac. Jest to lepiej wyjaśnione w książce kucharskiej Bash, w której około jedna czwarta z 600 stron jest poświęcona temu tematowi. Podjęliśmy trudną próbę rozwiązania problemów bezpieczeństwa w naszym darmowym oprogramowaniu secure-lib .
AsymLabs,
@Marco Point 4 jest poprawnym komentarzem, ale pytanie (OP) zaczyna się od i jest napisane wokół shellcheck, który ma na celu sprawdzenie obu skryptów sh / bash. Dlatego musimy założyć, że domyślnie nie jest to ściśle Posix.
AsymLabs,
@Marco Bezpieczeństwo zmiennej środowiskowej ścieżki może być łatwo zagrożone. Na przykład byłem bardzo zaskoczony, gdy kilka lat temu odkryłem, że Ruby RVM, zainstalowany lokalnie, domyślnie umieścił swoją ścieżkę przed ścieżką systemową.
AsymLabs,
OP w szczególności wymienia POSIX, a pytanie jest oznaczone, posixa nie bash. Nie mogę znaleźć żadnego wskaźnika, że ​​OP wymaga rozwiązania bash. Twoje stwierdzenie „Zawsze lepiej pozostać czystym Bash” jest po prostu błędne, przepraszam.
Marco