dirname и basename против расширения параметров


20

Есть ли объективная причина предпочесть одну форму другой? Производительность, надежность, портативность?

filename=/some/long/path/to/a_file

parentdir_v1="${filename%/*}"
parentdir_v2="$(dirname "$filename")"

basename_v1="${filename##*/}"
basename_v2="$(basename "$filename")"

echo "$parentdir_v1"
echo "$parentdir_v2"
echo "$basename_v1"
echo "$basename_v2"

Производит:

/some/long/path/to
/some/long/path/to
a_file
a_file

(v1 использует расширение параметров оболочки, v2 использует внешние двоичные файлы.)

Ответы:


21

У обоих есть свои причуды, к сожалению.

POSIX требует и того, и другого, поэтому разница между ними не является проблемой переносимостиtability.

Простой способ использовать утилиты

base=$(basename -- "$filename")
dir=$(dirname -- "$filename")

Обратите внимание на двойные кавычки вокруг подстановок переменных, как всегда, а также команду --after, если имя файла начинается с тире (в противном случае команды интерпретируют имя файла как опцию). Это по-прежнему не удается в одном крайнем случае, что редко, но может быть вызвано злонамеренным пользователем2: подстановка команд удаляет завершающие символы новой строки. Так что если имя файла вызывается, foo/bar␤тогда baseбудет установлено barвместо bar␤. Обходной путь - добавить не символ новой строки и удалить его после подстановки команды:

base=$(basename -- "$filename"; echo .); base=${base%.}
dir=$(dirname -- "$filename"; echo .); dir=${dir%.}

При подстановке параметров вы не сталкиваетесь с крайними случаями, связанными с раскрытием странных символов, но с косой чертой есть ряд трудностей. Одна вещь, которая вообще не является крайним случаем, состоит в том, что для вычисления части каталога требуется другой код для случая, когда его нет /.

base="${filename##*/}"
case "$filename" in
  */*) dirname="${filename%/*}";;
  *) dirname=".";;
esac

Крайний случай - когда есть завершающий слеш (включая случай корневого каталога, который все слэши). Эта basenameи dirnameкоманда сдирать заднюю косые черты , прежде чем они делают свою работу. Если вы придерживаетесь конструкций POSIX, нет способа зачистить завершающие косые черты за один раз, но вы можете сделать это в два этапа. Вы должны позаботиться о случае, когда ввод состоит только из косых черт.

case "$filename" in
  */*[!/]*)
    trail=${filename##*[!/]}; filename=${filename%%"$trail"}
    base=${filename##*/}
    dir=${filename%/*};;
  *[!/]*)
    trail=${filename##*[!/]}
    base=${filename%%"$trail"}
    dir=".";;
  *) base="/"; dir="/";;
esac

Если вам случается знать, что вы не находитесь в крайнем случае (например, findрезультат, отличный от начальной точки, всегда содержит часть каталога и не имеет конечного значения /), тогда манипулирование строкой расширения параметра является простым. Если вам нужно справиться со всеми крайними случаями, утилиты проще в использовании (но медленнее).

Иногда вы можете хотеть относиться к foo/как, foo/.а не как foo. Если вы действуете в записи каталога, то foo/предполагается, что она эквивалентна foo/., а не foo; это имеет значение, когда fooесть символическая ссылка на каталог: fooозначает символическую ссылку, foo/означает целевой каталог. В этом случае базовое имя пути с косой чертой имеет преимущество ., и путь может быть его собственным dirname.

case "$filename" in
  */) base="."; dir="$filename";;
  */*) base="${filename##*/}"; dir="${filename%"$base"}";;
  *) base="$filename"; dir=".";;
esac

Быстрый и надежный метод - использовать zsh с его модификаторами истории (это сначала удаляет завершающие косые черты, как утилиты):

dir=$filename:h base=$filename:t

¹ Если вы не используете оболочки до POSIX, такие как Solaris 10 и более ранние /bin/sh(в которых не было функций манипуляции со строками расширения параметров на машинах, которые все еще находятся в производстве - но всегда есть оболочка POSIX, вызываемая shпри установке, только она /usr/xpg4/bin/sh, а не /bin/sh).
² Например: отправьте файл, вызванный foo␤в службу загрузки файлов, которая не защищает от этого, затем удалите его и fooвместо этого удалите


Вау. Похоже, что (в любой оболочке POSIX) самый надежный способ - это второй, который вы упомянули? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Я внимательно читал и не заметил, что вы упомянули какие-либо недостатки.
Wildcard

1
@Wildcard Недостатком является то, что он обрабатывает foo/как foo, а не как foo/., что не согласуется с POSIX-совместимыми утилитами.
Жиль "ТАК - перестань быть злым"

Понял, спасибо. Я думаю, что я все еще предпочитаю этот метод, потому что я знал бы, пытаюсь ли я иметь дело с каталогами, и я мог бы просто прикрепить (или «вернуться назад») трейлинг, /если мне это нужно.
Wildcard

msgstr "например, findрезультат, который всегда содержит часть каталога и не имеет запаздывания /" Не совсем верно, find ./будет выводиться ./как первый результат.
Тавиан Барнс

@Gilles Пример символа новой строки просто взорвал мой разум. Спасибо за ответ
Сэм Томас

10

Оба находятся в POSIX, поэтому переносимость «не должна» беспокоить. Предполагается, что замены оболочки выполняются быстрее.

Однако - это зависит от того, что вы подразумеваете под портативным. Некоторые (не обязательно) старые системы не реализовали эти функции в своих /bin/sh(Solaris 10 и более ранних версиях), в то время как с другой стороны, разработчики предупреждали, что они dirnameне так переносимы, как basename.

Для справки:

При рассмотрении переносимости я должен был бы учитывать все системы, в которых я поддерживаю программы. Не все POSIX, поэтому есть компромиссы. Ваши компромиссы могут отличаться.


7

А также есть:

mkdir '
';    dir=$(basename ./'
');   echo "${#dir}"

0

Такие странные вещи случаются, потому что существует много интерпретаций и синтаксического анализа, а остальное должно произойти, когда говорят два процесса. Подстановки команд удаляют завершающие символы новой строки. И NUL (хотя это, очевидно, здесь не актуально) . basenameи dirnameв любом случае также уберет завершающие символы новой строки, потому что, как еще вы разговариваете с ними? Я знаю, что переводы строки в имени файла в любом случае являются своего рода анафемой, но вы никогда не знаете. И не имеет смысла идти по неправильному пути, когда вы могли бы поступить иначе.

Тем не менее ... ${pathname##*/} != basenameи аналогично ${pathname%/*} != dirname. Эти команды предназначены для выполнения в основном четко определенной последовательности шагов для достижения указанных результатов.

Спецификация ниже, но сначала вот более краткая версия:

basename()
    case   $1   in
    (*[!/]*/)     basename         "${1%"${1##*[!/]}"}"   ${2+"$2"}  ;;
    (*/[!/]*)     basename         "${1##*/}"             ${2+"$2"}  ;;
  (${2:+?*}"$2")  printf  %s%b\\n  "${1%"$2"}"       "${1:+\n\c}."   ;;
    (*)           printf  %s%c\\n  "${1##///*}"      "${1#${1#///}}" ;;
    esac

Это полностью POSIX-совместимый basenameв простом sh. Это не сложно сделать. Я слил пару веток, которые я использую ниже, потому что я мог, не влияя на результаты.

Вот спецификация:

basename()
    case   $1 in
    ("")            #  1. If  string  is  a null string, it is 
                    #     unspecified whether the resulting string
                    #     is '.' or a null string. In either case,
                    #     skip steps 2 through 6.
                  echo .
     ;;             #     I feel like I should flip a coin or something.
    (//)            #  2. If string is "//", it is implementation-
                    #     defined whether steps 3 to 6 are skipped or
                    #     or processed.
                    #     Great. What should I do then?
                  echo //
     ;;             #     I guess it's *my* implementation after all.
    (*[!/]*/)       #  3. If string consists entirely of <slash> 
                    #     characters, string shall be set to a sin‐
                    #     gle <slash> character. In this case, skip
                    #     steps 4 to 6.
                    #  4. If there are any trailing <slash> characters
                    #     in string, they shall be removed.
                  basename "${1%"${1##*[!/]}"}" ${2+"$2"}  
      ;;            #     Fair enough, I guess.
     (*/)         echo /
      ;;            #     For step three.
     (*/*)          #  5. If there are any <slash> characters remaining
                    #     in string, the prefix of string up to and 
                    #     including the last <slash> character in
                    #     string shall be removed.
                  basename "${1##*/}" ${2+"$2"}
      ;;            #      == ${pathname##*/}
     ("$2"|\
      "${1%"$2"}")  #  6. If  the  suffix operand is present, is not
                    #     identical to the characters remaining
                    #     in string, and is identical to a suffix of
                    #     the characters remaining  in  string, the
                    #     the  suffix suffix shall be removed from
                    #     string.  Otherwise, string is not modi‐
                    #     fied by this step. It shall not be
                    #     considered an error if suffix is not 
                    #     found in string.
                  printf  %s\\n "$1"
     ;;             #     So far so good for parameter substitution.
     (*)          printf  %s\\n "${1%"$2"}"
     esac           #     I probably won't do dirname.

... возможно, комментарии отвлекают ....


1
Ничего себе, хорошая идея о том, чтобы завершать переводы строк в именах файлов. Какая банка червей. Я не думаю, что я действительно понимаю ваш сценарий, хотя. Я никогда раньше не видел [!/], это как [^/]? Но ваш комментарий вместе с этим, кажется, не соответствует ...
Wildcard

1
@Wildcard - ну .. это не мой комментарий. Это стандарт . Спецификация POSIX для basenameпредставляет собой набор инструкций о том, как сделать это с вашей оболочкой. Но [!charclass]переносимый способ сделать это с помощью globs [^class]- для регулярных выражений - а оболочки не предназначены для регулярных выражений. О соответствии комментария ... caseфильтры, так что если я соответствую строке , которая содержит слэш / и в !/то , если следующий рисунок случая ниже спичек любых хвостовых /Slashes на все они могут быть только все косыми. И тот, который ниже, не может иметь никакого трейлинга /
mikeserv

2

Вы можете получить импульс от работы basenameи dirname(я не понимаю, почему они не являются встроенными - если это не кандидаты, я не знаю, что это), но реализация должна обрабатывать такие вещи, как:

path         dirname    basename
"/usr/lib"    "/usr"    "lib"
"/usr/"       "/"       "usr"
"usr"         "."       "usr"
"/"           "/"       "/"
"."           "."       "."
".."          "."       ".."

^ Из базового имени (3)

и другие крайние случаи.

Я использовал:

basename(){ 
  test -n "$1" || return 0
  local x="$1"; while :; do case "$x" in */) x="${x%?}";; *) break;; esac; done
  [ -n "$x" ] || { echo /; return; }
  printf '%s\n' "${x##*/}"; 
}

dirname(){ 
  test -n "$1" || return 0
  local x="$1"; while :; do case "$x" in */) x="${x%?}";; *) break;; esac; done
  [ -n "$x" ] || { echo /; return; }
  set -- "$x"; x="${1%/*}"
  case "$x" in "$1") x=.;; "") x=/;; esac
  printf '%s\n' "$x"
}

(Моя последняя реализация GNU basenameи dirnameдобавляет некоторые необычные переключатели командной строки для таких вещей, как обработка нескольких аргументов или удаление суффиксов, но это очень легко добавить в оболочку.)

Это не так сложно превратить их во bashвстроенные (используя основную реализацию системы), но вышеприведенную функцию не нужно компилировать, и они также обеспечивают некоторую поддержку.


Список крайних случаев на самом деле очень полезен. Это все очень хорошие моменты. Список на самом деле кажется довольно полным; действительно ли есть другие крайние случаи?
Wildcard

Моя прежняя реализация не работала x//правильно, но я исправил для вас, прежде чем ответить. Я надеюсь, что это так.
PSkocik

Вы можете запустить скрипт, чтобы сравнить, что функции и исполняемые файлы делают в этих примерах. Я получаю 100% совпадение.
PSkocik

1
Ваша функция dirname, похоже, не удаляет повторяющиеся слэши. Например: dirname a///b//c//d////eдоходность a///b//c//d///.
codeforester
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.