dirname und basename vs parameter expansion

20

Gibt es einen objektiven Grund, eine Form der anderen vorzuziehen? Leistung, Zuverlässigkeit, Portabilität?

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"

Produziert:

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

(v1 verwendet die Shell-Parameter-Erweiterung, v2 verwendet externe Binärdateien.)

Platzhalter
quelle

Antworten:

21

Beide haben leider ihre Macken.

Beide werden von POSIX benötigt, daher ist der Unterschied zwischen ihnen kein Problem der Portabilität¹.

Der einfache Weg, um die Dienstprogramme zu verwenden, ist

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

Beachten Sie die doppelten Anführungszeichen bei Variablensubstitutionen wie immer und auch --nach dem Befehl, falls der Dateiname mit einem Bindestrich beginnt (andernfalls würden die Befehle den Dateinamen als Option interpretieren). Dies schlägt in einem Randfall immer noch fehl, was selten vorkommt, aber möglicherweise von einem böswilligen Benutzer erzwungen wird²: Durch das Ersetzen von Befehlen werden nachgestellte Zeilenumbrüche entfernt. Also , wenn ein Dateiname genannt wird , foo/bar␤dann basewird gesetzt werden barstatt bar␤. Eine Problemumgehung besteht darin, ein Nicht-Zeilenumbruchzeichen hinzuzufügen und es nach der Befehlsersetzung zu entfernen:

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

Mit der Parametersubstitution stoßen Sie nicht auf Randfälle, die mit der Erweiterung von seltsamen Zeichen zusammenhängen, aber es gibt eine Reihe von Schwierigkeiten mit dem Schrägstrich. Eine Sache, die überhaupt kein Randfall ist, besteht darin, dass für die Berechnung des Verzeichnisteils ein anderer Code erforderlich ist als für den Fall, dass es keinen gibt /.

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

Der Kantenfall liegt vor, wenn ein abschließender Schrägstrich angezeigt wird (einschließlich des Stammverzeichnisses, bei dem es sich ausschließlich um Schrägstriche handelt). Die Befehle basenameund dirnameentfernen abschließende Schrägstriche, bevor sie ihre Arbeit erledigen. Es gibt keine Möglichkeit, die abschließenden Schrägstriche auf einmal zu entfernen, wenn Sie sich an POSIX-Konstrukte halten, aber Sie können dies in zwei Schritten tun. Sie müssen sich um den Fall kümmern, dass die Eingabe nur aus Schrägstrichen besteht.

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

Wenn Sie zufällig wissen, dass Sie sich nicht in einer Randbedingung befinden (z. B. enthält ein findanderes Ergebnis als der Startpunkt immer einen Verzeichnisteil und weist keine nachfolgenden Elemente auf /), ist die Manipulation der Parametererweiterungszeichenfolge unkompliziert. Wenn Sie alle Randfälle bewältigen müssen, sind die Dienstprogramme einfacher zu verwenden (aber langsamer).

Manchmal können Sie behandeln wollen foo/wie foo/.anstatt wie foo. Wenn Sie auf einem Verzeichniseintrag sind handeln dann foo/sollte äquivalent sein foo/., nicht foo; Dies macht einen Unterschied, wenn fooes sich um eine symbolische Verknüpfung zu einem Verzeichnis handelt: foobedeutet die symbolische Verknüpfung, foo/bedeutet das Zielverzeichnis. In diesem Fall ist der Basisname eines Pfads mit einem abschließenden Schrägstrich vorteilhaft .und der Pfad kann ein eigener Verzeichnisname sein.

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

Die schnelle und zuverlässige Methode besteht darin, zsh mit seinen Verlaufsmodifikatoren zu verwenden (in diesem Beispiel werden abschließende Schrägstriche wie in den Dienstprogrammen entfernt):

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

¹ Wenn Sie pre-POSIX - Shells wie Solaris verwenden 10 und älter ist /bin/sh(die Parameter Expansion String - Manipulation fehlte noch in der Produktion von Maschinen bietet - aber es gibt immer eine POSIX genannt shell shin der Installation, nur es ist /usr/xpg4/bin/shnicht /bin/sh).
² Zum Beispiel: Übermitteln Sie eine aufgerufene Datei foo␤an einen Datei-Upload-Dienst, der nicht davor schützt, und löschen Sie sie dann und veranlassen Sie foostattdessen, dass sie gelöscht wird

Gilles 'SO - hör auf böse zu sein'
quelle
Beeindruckend. Es hört sich also so an, als wäre (in jeder POSIX-Shell) die zweite, die Sie erwähnen, die robusteste? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Ich habe sorgfältig gelesen und habe nicht bemerkt, dass Sie irgendwelche Nachteile erwähnt haben.
Wildcard
1
@Wildcard Ein Nachteil ist, dass es foo/wie behandelt foo, nicht wie foo/., was mit POSIX-kompatiblen Dienstprogrammen nicht konsistent ist.
Gilles 'SO- hör auf böse zu sein'
Danke, verstanden. Ich denke, ich bevorzuge diese Methode immer noch, weil ich wissen würde , ob ich mich mit Verzeichnissen beschäftige und ich könnte einfach einen Trailing-Vorgang fortsetzen (oder "zurücksetzen"), /wenn ich ihn benötige.
Wildcard
"zB ein findErgebnis, das immer einen Verzeichnisteil enthält und kein Trailing hat /" Nicht ganz wahr, find ./wird ./als erstes Ergebnis ausgegeben .
Tavian Barnes
@ Gilles Das Beispiel für Newline-Charaktere hat mich einfach umgehauen. Danke für die Antwort
Sam Thomas
10

Beide sind in POSIX enthalten, daher sollte die Portabilität keine Rolle spielen. Es sollte angenommen werden, dass die Shell-Substitutionen schneller ablaufen.

Dies hängt jedoch davon ab, was Sie unter tragbar verstehen. Einige (nicht notwendigerweise) alte Systeme haben diese Funktionen in ihren /bin/sh(Solaris 10 und älter ) nicht implementiert , während die Entwickler vor einiger Zeit darauf hingewiesen wurden, dass sie dirnamenicht so portabel sind wie basename.

Als Referenz:

Bei der Prüfung der Portabilität müsste ich alle Systeme berücksichtigen , auf denen ich Programme verwalte. Nicht alle sind POSIX, also gibt es Kompromisse. Ihre Kompromisse können sich unterscheiden.

Thomas Dickey
quelle
7

Es gibt auch:

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

0

Solche seltsamen Dinge passieren, weil es viel Interpretieren und Parsen gibt und der Rest, der passieren muss, wenn zwei Prozesse sprechen. Befehlsersetzungen entfernen nachfolgende Zeilenumbrüche. Und NULs (obwohl das hier offensichtlich nicht relevant ist) .basenameund dirnamewerden auf jeden fall auch nachgestellte newlines entfernen, weil wie redest du sonst mit ihnen? Ich weiß, dass das Nachziehen von Zeilenumbrüchen in einem Dateinamen sowieso ein Anathema ist, aber man weiß es nie. Und es macht keinen Sinn, den möglicherweise fehlerhaften Weg zu gehen, wenn Sie es anders machen könnten.

Trotzdem ... ${pathname##*/} != basenameund ebenso ${pathname%/*} != dirname. Diese Befehle sind zum Ausführen eines Großteils angegeben genau definierte Abfolge von Schritten um zu den angegebenen Ergebnissen zu gelangen.

Die Spezifikation ist unten, aber zuerst ist hier eine tersere Version:

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

Das ist eine voll POSIX-konforme basenamein einfachersh . Es ist nicht schwer zu tun. Ich habe ein paar Zweige zusammengelegt, die ich unten benutze, weil ich konnte, ohne die Ergebnisse zu beeinflussen.

Hier ist die Spezifikation:

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.

... vielleicht lenken die Kommentare ab ...

mikeserv
quelle
1
Wow, ein guter Punkt, um Zeilenumbrüche in Dateinamen zu verfolgen. Was für eine Dose Würmer. Ich glaube jedoch nicht, dass ich Ihr Drehbuch wirklich verstehe. Ich habe noch nie [!/]zuvor gesehen , ist das so [^/]? Aber Ihr Kommentar daneben scheint nicht zu passen ....
Wildcard
1
@Wildcard - na ja .. es ist nicht mein Kommentar. Das ist der Standard . Die POSIX-Spezifikation für enthält basenameeine Reihe von Anweisungen, wie Sie dies mit Ihrer Shell tun können. Aber [!charclass]ist die tragbare Möglichkeit, dies mit Globs zu tun, [^class]für Regex - und Shells sind nicht für Regex spezifiziert? Über die Kommentar passende ... caseFilter, so dass , wenn ich eine Zeichenfolge übereinstimmen , die einen Schrägstrich enthält / und einen !/dann , wenn der nächste Fall Muster unten alle nachgestellten Matches /an alle Schrägstriche können sie nur sein alle Schrägstriche. Und eine darunter, die keine Trailing /
MikeServ haben kann
2

Sie können einen Schub von In-Process erhalten basenameund dirname(ich verstehe nicht, warum dies keine Buildins sind - wenn dies keine Kandidaten sind, weiß ich nicht, was das ist), aber die Implementierung muss Dinge wie Folgendes handhaben:

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

^ Aus dem Basisnamen (3)

und andere Randfälle.

Ich habe verwendet:

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

(Meine neueste Implementierung von GNU basenameund dirnamefügt einige spezielle Phantasie Befehlszeilenoptionen für Sachen wie mehrere Argumente oder Suffix Handhabung Strippen, aber das ist super einfach in der Schale hinzuzufügen.)

Es ist auch nicht so schwierig, diese in bashBuiltins umzuwandeln (indem die zugrunde liegende Systemimplementierung verwendet wird), aber die obige Funktion muss nicht kompiliert werden, und sie bietet auch einen gewissen Schub.

PSkocik
quelle
Die Liste der Randfälle ist eigentlich sehr hilfreich. Das sind alles sehr gute Punkte. Die Liste scheint tatsächlich ziemlich vollständig zu sein; Gibt es wirklich andere Randfälle?
Wildcard
Meine frühere Implementierung hat die Dinge nicht x//richtig gehandhabt, aber ich habe sie für Sie behoben, bevor ich antwortete. Ich hoffe das ist es.
PSkocik
Sie können ein Skript ausführen, um zu vergleichen, was die Funktionen und ausführbaren Dateien in diesen Beispielen bewirken. Ich bekomme eine 100% Übereinstimmung.
PSkocik
1
Ihre Dirname-Funktion scheint wiederholte Schrägstriche nicht zu entfernen. Zum Beispiel: dirname a///b//c//d////eErträge a///b//c//d///.
Codeforester