dirname et basename vs expansion de paramètres

20

Y a-t-il une raison objective de préférer une forme à l'autre? Performance, fiabilité, portabilité?

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"

Produit:

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

(v1 utilise l'expansion des paramètres du shell, v2 utilise des binaires externes.)

Caractère générique
la source

Réponses:

21

Les deux ont leurs bizarreries, malheureusement.

Les deux sont requis par POSIX, donc la différence entre eux n'est pas un problème de portabilité¹.

La manière simple d'utiliser les utilitaires est

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

Notez les guillemets doubles autour des substitutions de variables, comme toujours, et également --après la commande, au cas où le nom de fichier commence par un tiret (sinon les commandes interpréteraient le nom de fichier comme une option). Cela échoue toujours dans un cas de bord, ce qui est rare mais peut être forcé par un utilisateur malveillant²: la substitution de commande supprime les nouvelles lignes de fin. Donc, si un nom de fichier est appelé, foo/bar␤il basesera défini sur au barlieu de bar␤. Une solution de contournement consiste à ajouter un caractère non-retour à la ligne et à le supprimer après la substitution de commande:

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

Avec la substitution de paramètres, vous ne rencontrez pas de cas marginaux liés à l'expansion de caractères étranges, mais il y a un certain nombre de difficultés avec le caractère barre oblique. Une chose qui n'est pas du tout un cas de bord est que le calcul de la partie répertoire nécessite un code différent pour le cas où il n'y en a pas /.

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

Le cas de bord est quand il y a une barre oblique de fin (y compris le cas du répertoire racine, qui est toutes les barres obliques). Les commandes basenameet dirnamesuppriment les barres obliques avant de faire leur travail. Il n'y a aucun moyen de supprimer les barres obliques en une seule fois si vous vous en tenez aux constructions POSIX, mais vous pouvez le faire en deux étapes. Vous devez prendre soin de l'affaire lorsque l'entrée ne se compose que de barres obliques.

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

Si vous savez que vous n'êtes pas dans un cas de bord (par exemple, un findrésultat autre que le point de départ contient toujours une partie de répertoire et n'a pas de fin /), la manipulation de la chaîne d'extension des paramètres est simple. Si vous devez gérer tous les cas de bord, les utilitaires sont plus faciles à utiliser (mais plus lents).

Parfois, vous voudrez peut-être traiter foo/comme foo/.plutôt que comme foo. Si vous agissez sur une entrée de répertoire, foo/est censé être équivalent à foo/., non foo; cela fait une différence quand fooest un lien symbolique vers un répertoire: foosignifie le lien symbolique, foo/signifie le répertoire cible. Dans ce cas, le nom de base d'un chemin d'accès avec une barre oblique de fin est avantageusement ., et le chemin d'accès peut être son propre nom de répertoire.

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

La méthode rapide et fiable consiste à utiliser zsh avec ses modificateurs d'historique (ce premier supprime les barres obliques de fin, comme les utilitaires):

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

¹ À moins que vous n'utilisiez des shells pré-POSIX comme Solaris 10 et les versions antérieures /bin/sh(qui manquaient de fonctionnalités de manipulation de chaîne d'extension des paramètres sur les machines encore en production - mais il y a toujours un shell POSIX appelé shdans l'installation, seulement c'est /usr/xpg4/bin/sh, non /bin/sh).
² Par exemple: soumettre un fichier appelé foo␤à un service de téléchargement de fichiers qui ne protège pas contre cela, puis supprimez-le et faites- foole supprimer à la place

Gilles 'SO- arrête d'être méchant'
la source
Sensationnel. Donc, cela ressemble à (dans n'importe quel shell POSIX) la manière la plus robuste est la deuxième que vous mentionnez? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Je lisais attentivement et je n'ai pas remarqué que vous mentionniez des inconvénients.
Wildcard
1
@Wildcard Un inconvénient est qu'il traite foo/comme foo, pas comme foo/., ce qui n'est pas compatible avec les utilitaires compatibles POSIX.
Gilles 'SO- arrête d'être méchant'
Je l'ai Merci. Je pense que je préfère toujours cette méthode parce que je saurais si j'essaie de gérer les répertoires et je pourrais simplement clouer (ou "revenir") /si je en ai besoin.
Wildcard
"Par exemple, un findrésultat, qui contient toujours une partie de répertoire et qui n'a pas de fin /" Pas tout à fait vrai, find ./sera affiché ./comme premier résultat.
Tavian Barnes
@Gilles L'exemple de caractère de nouvelle ligne vient de me couper le souffle. Merci pour la réponse
Sam Thomas
10

Les deux sont en POSIX, donc la portabilité "ne devrait" pas être un problème. Les substitutions de shell doivent être supposées s'exécuter plus rapidement.

Cependant - cela dépend de ce que vous entendez par portable. Certains anciens systèmes (pas nécessairement) n'ont pas implémenté ces fonctionnalités dans leur /bin/sh(Solaris 10 et plus ancien me viennent à l'esprit), tandis que d'un autre côté, les développeurs ont été avertis que ce dirnamen'était pas aussi portable que basename.

Pour référence:

En considérant la portabilité, je devrais prendre en compte tous les systèmes où je maintiens des programmes. Tous ne sont pas POSIX, il y a donc des compromis. Vos compromis peuvent différer.

Thomas Dickey
la source
7

Il y a aussi:

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

0

Des choses étranges comme ça se produisent parce qu'il y a beaucoup d'interprétation et d'analyse et le reste qui doit se produire lorsque deux processus parlent. Les substitutions de commandes suppriment les nouvelles lignes de fin. Et NULs (bien que ce ne soit évidemment pas pertinent ici) . basenameet dirnamesupprimera également les nouvelles lignes en fin de cas, car comment leur parlez-vous autrement? Je sais, les sauts de ligne dans un nom de fichier sont en quelque sorte un anathème, mais on ne sait jamais. Et cela n'a pas de sens d'emprunter la voie éventuellement défectueuse alors que vous pourriez faire autrement.

Toujours ... ${pathname##*/} != basenameet de même ${pathname%/*} != dirname. Ces commandes sont spécifiées pour effectuer une séquence d'étapes principalement bien définie pour arriver à leurs résultats spécifiés.

La spécification est ci-dessous, mais voici d'abord une version 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

C'est entièrement compatible POSIX basename simplesh . Ce n'est pas difficile à faire. J'ai fusionné quelques branches que j'utilise ci-dessous car je pouvais sans affecter les résultats.

Voici la spécification:

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.

... peut-être que les commentaires distraient ...

mikeserv
la source
1
Wow, bon point sur la fin des nouvelles lignes dans les noms de fichiers. Quelle boîte de vers. Je ne pense pas vraiment comprendre votre script. Je n'en ai jamais vu [!/]auparavant, c'est comme ça [^/]? Mais votre commentaire à côté de cela ne semble pas correspondre ...
Wildcard
1
@Wildcard - eh bien .. ce n'est pas mon commentaire. Voilà la norme . La spécification POSIX pour basenameest un ensemble d'instructions sur la façon de le faire avec votre shell. Mais [!charclass]la façon portable de le faire avec des globes [^class]est pour regex - et les shells ne sont pas spécifiés pour regex. À propos des casefiltres correspondant au commentaire ... , donc si je fais correspondre une chaîne qui contient une barre oblique de fin / et un !/si le motif de cas suivant ci-dessous correspond à des /barres obliques de fin, il ne peut s'agir que de toutes les barres obliques. Et celui ci-dessous qui ne peut pas avoir de fuite /
mikeserv
2

Vous pouvez obtenir un coup de pouce du processus en cours basenameet dirname(je ne comprends pas pourquoi ceux-ci ne sont pas intégrés - si ce ne sont pas des candidats, je ne sais pas ce que c'est) mais l'implémentation doit gérer des choses comme:

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

^ Du nom de base (3)

et d'autres cas de bord.

J'utilise:

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

(Ma dernière implémentation de GNU basenameet dirnameajoute quelques commutateurs de ligne de commande fantaisie spéciaux pour des choses telles que la gestion de plusieurs arguments ou la suppression des suffixes, mais c'est super facile à ajouter dans le shell.)

Ce n'est pas si difficile de les intégrer dans les bashbuilds non plus (en utilisant l'implémentation du système sous-jacent), mais la fonction ci-dessus n'a pas besoin d'être compilée, et elle fournit également un coup de pouce.

PSkocik
la source
La liste des cas marginaux est en fait très utile. Ce sont tous de très bons points. La liste semble en fait assez complète; y a-t-il vraiment d'autres cas marginaux?
Wildcard
Mon ancienne implémentation ne gérait pas x//correctement les choses , mais j'ai corrigé pour vous avant de répondre. J'espère que c'est ça.
PSkocik
Vous pouvez exécuter un script pour comparer ce que font les fonctions et les exécutables sur ces exemples. Je reçois un match à 100%.
PSkocik
1
Votre fonction dirname ne semble pas supprimer les occurrences répétées de barres obliques. Par exemple: les dirname a///b//c//d////erendements a///b//c//d///.
codeforester