Shellcheck советует не использовать базовое имя: почему?

25

Я пытаюсь из shellcheck .

У меня что-то подобное

basename "${OPENSSL}" 

и я получаю следующее предложение

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

С практической точки зрения я не вижу разницы

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

Так basenameкак в спецификации POSIX , я не причина, почему это должно быть лучшей практикой. Любой намек?

Matteo
источник
8
Он создает новый процесс, когда в этом нет необходимости.
Иордания
@ jordanm достаточно справедливо ... Я не думал об эффективности.
Маттео
3
@jordanm С другой стороны, он работает с другими оболочками, кроме bash
Matteo
1
@JosephR. это то, что я думал, но только что узнал, что это не работает csh. Я думаю, cshэто не POSIX тогда.
Terdon
3
@terdon - csh очень далеко от POSIX.
Иордания

Ответы:

25

Дело не в эффективности, а в правильности. basenameиспользует новые строки для разграничения имен файлов, которые он печатает. В обычном случае, когда вы передаете только одно имя файла, он добавляет завершающий символ новой строки в свой вывод. Поскольку имена файлов могут содержать символы новой строки, это затрудняет правильную обработку этих имен файлов.

Это осложняется еще и тем , что люди , как правило , используют basenameтак: "$(basename "$file")". Это делает вещи еще более сложными, потому что $(command)убирает все завершающие символы новой строки command. Рассмотрим маловероятный случай, который $fileзаканчивается новой строкой. Затем basenameдобавит дополнительную новую строку, но "$(basename "$file")"удалит обе строки, оставив вас с неправильным именем файла.

Другая проблема в basenameтом, что если $fileначинается с -(тире или минус), это будет интерпретироваться как опция. Это легко исправить:$(basename -- "$file")

Надежный способ использования basenameзаключается в следующем:

# 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%??}"

Альтернативой является использование ${file##*/}, которое проще, но имеет свои собственные ошибки. В частности, это неправильно в тех случаях, когда $fileесть /или foo/.

Matt
источник
2
Очень хорошие очки +1. Рад, что ОП принял это вместо моего.
Terdon
2
Контрапункт: что если $fileесть foo/? Что если это /?
Жиль "ТАК - перестань быть злым"
2
@ Жиль: Ты прав. Я начинаю думать, что basenameподход все- таки лучше, такой же хакерский. Лучшими альтернативами, которые я могу придумать, являются ${${${file}%%/#}##*/}и [[ $file =~ "([^/]*)/*$" ]] && printf "%s" $match[1], оба из которых являются специфичными для zsh, и ни один из которых не обрабатывает /правильно.
Мэтт
@terdon Спасибо, что вы не приняли это на свой счет :-). Файлы с символами новой строки встречаются редко, но у Мэтта есть смысл. Конечно, использование подстановки переменных также более эффективно.
Маттео
16

Соответствующие строки в shellcheck«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 ()

Объяснение не дается явно, но, основываясь на имени функции ( checkNeedlessCommands), похоже, что @jordanm совершенно прав и предлагает вам избегать разветвления нового процесса.

Тердон
источник
7
Пусть источник будет с вами :)
Джозеф Р.
3

dirname, basename, И readlinkт.д. (спасибо @Marco - это исправляется) может создать проблемы с переносимостью , когда безопасность становится важной (требование безопасности пути). Многие системы (например, Fedora Linux) размещают его там, /binа другие (например, Mac OSX) размещают его там /usr/bin. Тогда есть Bash на Windows, например, Cygwin, MSYS и другие. Всегда лучше оставаться чистым Башом, когда это возможно. (за комментарий @Marco)

Кстати, спасибо за указатель на shellcheck, я раньше такого не видел.

AsymLabs
источник
1
1) Что вы подразумеваете под «безопасностью»? Можете ли вы уточнить? 2) ОП вообще не упоминается dirname. 3) Базовые основные утилиты должны быть в PATH, где бы они ни хранились. Я еще не видел систему, где basenameне было в PATH. 4) Если предположить, что bash доступен, это проблема переносимости. Всегда лучше держаться подальше от bash и использовать оболочку POSIX, когда требуется переносимость.
Марко
@Marco Спасибо за указание на эти проблемы. Да, вы правы, что утилиты находятся на пути. Но если кто-то хочет обеспечить дополнительную безопасность скрипту Bash, хорошей практикой является предоставление абсолютного пути. Поэтому вызов / bin / basename 'будет работать для систем RedHat, но на Mac он выдаст ошибку. Это лучше объяснено в Поваренной книге Bash, где около четверти из 600 страниц посвящено этой теме. Мы сделали грубую попытку решить проблемы безопасности в нашем бесплатном исходном коде secure-lib .
AsymLabs
@Marco Point 4 является действительным комментарием, но вопрос (OP) начинается с и написан вокруг shellcheck, который предназначен для проверки обоих сценариев sh / bash. Поэтому мы должны предположить, что это не строго Posix по умолчанию.
AsymLabs
@Marco Безопасность переменной среды пути может быть легко скомпрометирована. Например, я был очень удивлен, обнаружив несколько лет назад, что Ruby RVM, установленный локально, по умолчанию размещает свой путь перед системным путем.
AsymLabs
В ОП конкретно упоминается POSIX, а вопрос помечен posixи нет bash. Я не могу найти индикатор того, что ОП требует решения для bash. Ваше утверждение «всегда лучше оставаться чистым Башом» просто неправильно, извините.
Марко