dirname та basename vs розширення параметра


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, тому різниця між ними не стосується портативності¹.

Простий спосіб використання утиліт - це

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

Зверніть увагу на подвійні лапки навколо змінних підстановок, як завжди, а також --після команди, якщо ім'я файлу починається з тире (інакше команди інтерпретуватимуть ім'я файлу як опцію). Це все-таки не вдається в одному крайовому випадку, що є рідкісним, але може бути вимушеним зловмисним користувачем²: заміна команди видаляє зворотні нові рядки. Отже, якщо ім'я файлу викликається, 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.
Жиль "ТАК - перестань бути злим"

Зрозумів дякую. Я думаю, що я все ще віддаю перевагу цьому методу, тому що я б знав, якщо я намагаюся мати справу з каталогами, і я міг би просто застосувати (або "взяти назад") трейлінг, /якщо мені це потрібно.
Уайлдкард

"наприклад, 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]це портативний спосіб зробити це з глобусами [^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.