dirname i basename vs interpretacja parametrów

20

Czy istnieje obiektywny powód, aby preferować jedną formę od drugiej? Wydajność, niezawodność, przenośność?

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"

Produkuje:

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

(v1 używa rozszerzenia parametrów powłoki, v2 używa zewnętrznych plików binarnych.)

Dzika karta
źródło

Odpowiedzi:

21

Obaj mają swoje dziwactwa, niestety.

Oba są wymagane przez POSIX, więc różnica między nimi nie dotyczy przenośności¹.

Prostym sposobem korzystania z narzędzi jest

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

Zwróć uwagę na podwójne cudzysłowy wokół podstawień zmiennych, jak zawsze, a także --po poleceniu, w przypadku gdy nazwa pliku zaczyna się od myślnika (w przeciwnym razie polecenia interpretowałyby nazwę pliku jako opcję). Nadal nie udaje się to w przypadku jednego brzegu, co jest rzadkie, ale może być wymuszone przez złośliwego użytkownika²: podstawienie polecenia usuwa końcowe znaki nowej linii. Więc jeśli plik jest zwany foo/bar␤następnie basezostanie ustawiony barzamiast bar␤. Obejściem tego problemu jest dodanie znaku nie będącego znakiem nowej linii i usunięcie go po podstawieniu polecenia:

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

Dzięki podstawianiu parametrów nie napotykasz przypadków krawędzi związanych z ekspansją dziwnych znaków, ale istnieje wiele trudności ze znakiem ukośnika. Jedną rzeczą, która wcale nie jest przypadkiem na krawędzi, jest to, że obliczenie części katalogu wymaga innego kodu dla przypadku, w którym nie ma /.

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

Przypadek na krawędzi występuje, gdy występuje ukośnik (w tym wielkość katalogu głównego, który jest wszystkimi ukośnikami). Te basenamei dirnamepolecenia zdejmować końcowe ukośniki przed robią swoje. Nie ma sposobu na usunięcie końcowych ukośników za jednym razem, jeśli trzymasz się konstrukcji POSIX, ale możesz to zrobić w dwóch krokach. Musisz zająć się przypadkiem, gdy wejście składa się wyłącznie z ukośników.

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

Jeśli zdarzy ci się wiedzieć, że nie jesteś w przypadku krawędzi (np. findWynik inny niż punkt początkowy zawsze zawiera część katalogu i nie ma końca/ ), wówczas manipulowanie łańcuchem ekspansji parametrów jest proste. Jeśli musisz poradzić sobie ze wszystkimi przypadkami na krawędziach, narzędzia są łatwiejsze w użyciu (ale wolniej).

Czasami możesz chcieć traktować foo/jak, foo/.a nie jak foo. Jeśli działasz na podstawie wpisu w katalogu, foo/powinno to być równoważne foo/., a nie foo; robi to różnicę, gdy foojest dowiązaniem symbolicznym do katalogu: foooznacza dowiązanie symboliczne, foo/oznacza katalog docelowy. W takim przypadku basename ścieżki z końcowym ukośnikiem jest korzystnie ., a ścieżka może być własnym katalogiem.

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

Szybką i niezawodną metodą jest użycie zsh z jego modyfikatorami historii (to pierwsze usuwa końcowe ukośniki, takie jak narzędzia):

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

¹ O ile nie korzystasz z powłok wcześniejszych niż POSIX, takich jak Solaris 10 i starsze /bin/sh(którym brakowało funkcji manipulowania łańcuchem ekspansji parametrów na maszynach wciąż będących w produkcji - ale w instalacji zawsze jest wywoływana powłoka POSIX sh, ale to /usr/xpg4/bin/shnie jest /bin/sh).
² Na przykład: prześlij plik wywołany foo␤do usługi przesyłania plików, która nie chroni przed tym, a następnie usuń go i spowoduj foousunięcie

Gilles „SO- przestań być zły”
źródło
Łał. Brzmi więc to tak, jakby (w dowolnej powłoce POSIX) najsolidniejszy był drugi, o którym wspominałeś? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Czytałem uważnie i nie zauważyłem, że wspominacie o jakichkolwiek wadach.
Wildcard
1
@Wildcard Wadą jest to, że traktuje foo/jak foo, a nie jak foo/., co nie jest zgodne z narzędziami zgodnymi z POSIX.
Gilles „SO- przestań być zły”
Mam to, dzieki. Wydaje mi się, że nadal wolę tę metodę, ponieważ wiedziałbym, czy staram się radzić sobie z katalogami, a /gdybym tego potrzebował, mogłem po prostu poprawić (lub „przywrócić”) trailing .
Wildcard
„np. findwynik, który zawsze zawiera część katalogu i nie ma /końca”. Niezupełnie prawda, find ./wyświetli się ./jako pierwszy wynik.
Tavian Barnes
@Gilles Znak nowej linii po prostu oszalał. Dzięki za odpowiedź
Sam Thomas
10

Oba są w systemie POSIX, więc przenośność „nie powinna” mieć znaczenia. Należy założyć, że podstawienia powłoki działają szybciej.

Jednak zależy to od tego, co rozumiesz przez przenośny. Niektóre (niekoniecznie) stare systemy nie implementowały tych funkcji w swoich /bin/sh(Solaris 10 i starsze przychodzą na myśl), podczas gdy z drugiej strony, dawno temu ostrzegano programistów, że dirnamenie jest tak przenośny jak basename.

Na przykład:

Rozważając przenośność, musiałbym wziąć pod uwagę wszystkie systemy, w których utrzymuję programy. Nie wszystkie są POSIX, więc są kompromisy. Twoje kompromisy mogą się różnić.

Thomas Dickey
źródło
7

Jest również:

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

0

Dziwne rzeczy takie się zdarzają, ponieważ jest dużo interpretacji i analizy, a reszta musi się zdarzyć, gdy rozmawiają dwa procesy. Podstawienia poleceń usuwają końcowe znaki nowej linii. I NULs (choć to oczywiście nie ma tu znaczenia) . basenamea dirnametakże usunie końcowe znaki nowej linii, bo jak inaczej z nimi rozmawiasz? Wiem, że końcowe znaki w nazwie pliku i tak są swego rodzaju przekleństwem, ale nigdy nie wiadomo. I nie ma sensu iść w jakikolwiek wadliwy sposób, kiedy można zrobić inaczej.

Nadal ... ${pathname##*/} != basenamei podobnie ${pathname%/*} != dirname. Polecenia te zostały określone w celu przeprowadzenia przeważnie dobrze określonej sekwencji kroków w celu uzyskania określonych rezultatów.

Specyfikacja jest poniżej, ale najpierw jest wersja terser:

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

Jest to w pełni zgodny z POSIX basenamew prosty sposóbsh . Nie jest to trudne. Połączyłem kilka gałęzi, których używam poniżej, ponieważ mogłem bez wpływu na wyniki.

Oto specyfikacja:

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.

... może komentarze odwracają uwagę ...

mikeserv
źródło
1
Wow, dobry punkt na temat kończenia nowego wiersza w nazwach plików. Co za puszka robaków. Nie sądzę jednak, że naprawdę rozumiem twój scenariusz. Nigdy wcześniej nie widziałem [!/], czy to tak [^/]? Ale twój komentarz, który wydaje się nie pasować ...
Wildcard
1
@Wildcard - cóż .. to nie jest mój komentarz. To jest standard . Specyfikacja POSIX- basenamea to zestaw instrukcji, jak to zrobić z powłoką. Ale [!charclass]czy przenośnym sposobem na zrobienie tego z globs [^class]jest regex - i powłoki nie są wyspecyfikowane dla regex. O dopasowanie komentarz ... casefiltry, więc jeśli mogę dopasować ciąg, który zawiera ukośnik / i to !/wtedy, jeśli następny przypadek wzór poniżej meczów wszelkie końcowe /ukośniki w ogóle mogą być one tylko wszystkie ukośniki. A jeden poniżej, który nie może mieć żadnego końcowego /
mikeserv
2

Możesz uzyskać impuls z procesu basenamei dirname(nie rozumiem, dlaczego nie są to wbudowane - jeśli nie są to kandydaci, nie wiem, co to jest), ale implementacja musi obsługiwać takie rzeczy jak:

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

^ Od basename (3)

i inne skrzynki krawędziowe.

Korzystałem z:

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"
}

(Moja najnowsza implementacja GNU basenamei dirnamedodaje specjalne fantazyjne przełączniki wiersza poleceń do takich rzeczy, jak obsługa wielu argumentów lub usuwanie sufiksów, ale to bardzo łatwe do dodania w powłoce.)

Nie jest to również trudne do przekształcenia ich we bashwbudowane (dzięki wykorzystaniu podstawowej implementacji systemu), ale powyższa funkcja nie musi być kompilowana, a także zapewniają pewne ulepszenie.

PSkocik
źródło
Lista przypadków skrajnych jest w rzeczywistości bardzo pomocna. To są bardzo dobre punkty. Lista faktycznie wydaje się dość kompletna; czy naprawdę są jakieś inne przypadki krawędziowe?
Wildcard
Moja poprzednia implementacja nie obsługiwała takich rzeczy jak x//poprawnie, ale naprawiłem dla ciebie przed odpowiedzią. Mam nadzieję, że to tyle.
PSkocik
Możesz uruchomić skrypt, aby porównać działanie funkcji i plików wykonywalnych w tych przykładach. Dostaję 100% dopasowania.
PSkocik
1
Wydaje się, że funkcja dirname nie usuwa powtarzających się ukośników. Na przykład: dirname a///b//c//d////edaje a///b//c//d///.
codeforester