dirname e basename vs espansione dei parametri


20

C'è qualche motivo oggettivo per preferire una forma all'altra? Prestazioni, affidabilità, 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"

produce:

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

(v1 utilizza l'espansione dei parametri della shell, v2 utilizza i binari esterni.)

Risposte:


21

Entrambi hanno le loro stranezze, sfortunatamente.

Entrambi sono richiesti da POSIX, quindi la differenza tra loro non è un problema di portabilità¹.

Il modo semplice di utilizzare le utility è

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

Nota le doppie virgolette attorno alle sostituzioni variabili, come sempre, e anche --dopo il comando, nel caso in cui il nome del file inizi con un trattino (altrimenti i comandi interpreterebbero il nome del file come opzione). Ciò non riesce ancora in un caso limite, che è raro ma potrebbe essere forzato da un utente maligno²: la sostituzione dei comandi rimuove le nuove righe finali. Quindi, se un nome di file si chiama foo/bar␤allora basesarà impostato baral posto di bar␤. Una soluzione alternativa consiste nell'aggiungere un carattere non newline e rimuoverlo dopo la sostituzione del comando:

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

Con la sostituzione dei parametri, non ci si imbatte in casi limite legati all'espansione di caratteri strani, ma ci sono una serie di difficoltà con il carattere barra. Una cosa che non è affatto un caso limite è che il calcolo della parte della directory richiede un codice diverso per il caso in cui non esiste /.

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

Il caso limite è quando c'è una barra finale (incluso il caso della directory radice, che è tutta barra). I comandi basenamee dirnamerimuovono le barre finali prima che facciano il loro lavoro. Non è possibile rimuovere le barre finali in una volta sola se ci si attacca ai costrutti POSIX, ma è possibile farlo in due passaggi. Devi occuparti del caso quando l'input è costituito solo da barre.

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

Se ti capita di sapere che non sei in un caso limite (ad es. Un findrisultato diverso dal punto iniziale contiene sempre una parte di directory e non ha tracce /), la manipolazione della stringa di espansione dei parametri è semplice. Se devi affrontare tutti i casi limite, le utility sono più facili da usare (ma più lente).

A volte, potresti voler trattare foo/come foo/.piuttosto che come foo. Se stai agendo su una voce della directory, allora foo/dovrebbe essere equivalente a foo/., no foo; ciò fa la differenza quando si footratta di un collegamento simbolico a una directory: fooindica il collegamento simbolico, foo/indica la directory di destinazione. In tal caso, il nome di base di un percorso con una barra finale è vantaggiosamente .e il percorso può essere il suo nome dir.

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

Il metodo rapido e affidabile è usare zsh con i suoi modificatori di cronologia (questa prima striscia di barre, come le utility):

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

¹ A meno che non si utilizzino shell pre-POSIX come Solaris 10 e precedenti /bin/sh(che mancavano delle funzionalità di manipolazione delle stringhe di espansione dei parametri su macchine ancora in produzione - ma c'è sempre una shell POSIX chiamata shnell'installazione, solo che /usr/xpg4/bin/shnon lo è /bin/sh).
² Ad esempio: inviare un file chiamato foo␤a un servizio di caricamento file che non protegge da questo, quindi eliminarlo e causare fooinvece l' eliminazione


Wow. Quindi sembra che (in qualsiasi shell POSIX) il modo più efficace sia il secondo di cui parli? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Stavo leggendo attentamente e non ho notato che hai menzionato degli svantaggi.
Carattere jolly

1
@Wildcard Uno svantaggio è che tratta foo/come foo, non come foo/., che non è coerente con le utility compatibili con POSIX.
Gilles 'SO- smetti di essere malvagio' il

Capito grazie. Penso di preferire ancora quel metodo perché saprei se sto cercando di gestire le directory e potrei semplicemente attaccare (o "rimandare indietro") un trailing /se ne ho bisogno.
Carattere jolly

"Ad es. un findrisultato, che contiene sempre una parte della directory e non ha tracce /" Non del tutto vero, find ./verrà generato ./come primo risultato.
Tavian Barnes

@Gilles L'esempio del personaggio newline mi ha lasciato senza parole. Grazie per la risposta
Sam Thomas,

10

Entrambi sono in POSIX, quindi la portabilità "non dovrebbe" essere fonte di preoccupazione. Si presume che le sostituzioni della shell funzionino più velocemente.

Tuttavia, dipende da cosa intendi per portatile. Alcuni vecchi sistemi (non necessariamente necessari) non implementavano quelle funzionalità nei loro /bin/sh(vengono in mente Solaris 10 e precedenti), mentre d'altra parte, un po 'di tempo fa, gli sviluppatori venivano avvertiti che dirnamenon era portatile come basename.

Per riferimento:

Nel considerare la portabilità, dovrei prendere in considerazione tutti i sistemi in cui mantengo i programmi. Non tutti sono POSIX, quindi ci sono dei compromessi. I tuoi compromessi potrebbero essere diversi.


7

C'è anche:

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

0

Strane cose del genere accadono perché c'è molta interpretazione e analisi e il resto che deve accadere quando due processi parlano. Le sostituzioni di comandi rimuoveranno le nuove righe finali. E NUL (anche se ovviamente non è rilevante qui) . basenamee dirnamespogliamo anche le nuove righe finali in ogni caso perché in quale altro modo parli con loro? Lo so, trascinare le nuove righe in un nome file è comunque un tipo di anatema, ma non lo sai mai. E non ha senso andare nel modo forse imperfetto quando si potrebbe fare diversamente.

Comunque ... ${pathname##*/} != basenamee allo stesso modo ${pathname%/*} != dirname. Tali comandi sono specificati per eseguire una sequenza di passaggi per lo più ben definita per arrivare ai risultati specificati.

Le specifiche sono sotto, ma prima ecco una versione 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

Questo è completamente conforme POSIX basenamein modo semplice sh. Non è difficile da fare. Ho unito un paio di rami che uso qui sotto perché potrei senza influenzare i risultati.

Ecco le specifiche:

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.

... forse i commenti sono fonte di distrazione ....


1
Wow, buon punto per trascinare le nuove righe nei nomi dei file. Che lattina di vermi. Non credo di capire davvero la tua sceneggiatura, comunque. Non ho mai visto [!/]prima, è così [^/]? Ma il tuo commento a fianco non sembra corrispondere ad esso ...
Wildcard

1
@Wildcard - beh .. non è il mio commento. Questo è lo standard . Le specifiche POSIX per basenamesono un insieme di istruzioni su come farlo con la shell. Ma [!charclass]il modo portatile per farlo con globs [^class]è per regex - e le shell non sono pensate per regex. Circa la corrispondenza il commento ... casefiltri, quindi se corrispondono una stringa che contiene una barra finale / e un !/poi, se il prossimo caso modello di seguito corrisponde a qualsiasi trailing /slash a tutto ciò che può essere solo tutte le barre. E uno di seguito che non può avere alcun trascinamento /
mikeserv

2

Puoi ottenere un impulso da in-process basenamee dirname(non capisco perché questi non sono integrati - se questi non sono candidati, non so cosa sia) ma l'implementazione deve gestire cose come:

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

^ Da basename (3)

e altri casi limite.

Ho usato:

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

(La mia ultima implementazione di GNU basenamee dirnameaggiunge alcune opzioni speciali della riga di comando per cose come la gestione di più argomenti o lo stripping del suffisso, ma è super facile da aggiungere nella shell.)

Non è nemmeno difficile trasformarli in bashbuiltin (facendo uso dell'implementazione del sistema sottostante), ma non è necessario compilare la funzione sopra e forniscono anche un po 'di spinta.


L'elenco dei casi limite è in realtà molto utile. Questi sono tutti punti molto buoni. L'elenco sembra in realtà abbastanza completo; ci sono davvero altri casi limite?
Wildcard il

La mia precedente implementazione non ha gestito cose del genere x// correttamente le , ma ho risolto per te prima di rispondere. Spero che sia così.
PSkocik,

È possibile eseguire uno script per confrontare cosa fanno le funzioni e gli eseguibili in questi esempi. Ricevo una corrispondenza del 100%.
PSkocik,

1
La tua funzione dirname non sembra eliminare ricorrenze ripetute di barre. Per esempio:dirname a///b//c//d////e rese a///b//c//d///.
codeforester,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.