dirname y basename vs parámetro de expansión

20

¿Hay alguna razón objetiva para preferir una forma a la otra? ¿Rendimiento, fiabilidad, portabilidad?

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 usa la expansión de parámetros de shell, v2 usa binarios externos).

Comodín
fuente

Respuestas:

21

Ambos tienen sus peculiaridades, desafortunadamente.

POSIX requiere ambos, por lo que la diferencia entre ellos no es un problema de portabilidad¹.

La manera simple de usar las utilidades es

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

Tenga en cuenta las comillas dobles alrededor de las sustituciones de variables, como siempre, y también --después del comando, en caso de que el nombre del archivo comience con un guión (de lo contrario, los comandos interpretarían el nombre del archivo como una opción). Esto todavía falla en un caso extremo, lo cual es raro pero puede ser forzado por un usuario malintencionado²: la sustitución de comandos elimina las nuevas líneas finales. Así que si un nombre de archivo se denomina foo/bar␤a continuación basese establecerá en barlugar de bar␤. Una solución alternativa es agregar un carácter que no sea de nueva línea y eliminarlo después de la sustitución del comando:

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

Con la sustitución de parámetros, no se topa con casos extremos relacionados con la expansión de caracteres extraños, pero hay una serie de dificultades con el carácter de barra diagonal. Una cosa que no es un caso límite en absoluto es que calcular la parte del directorio requiere un código diferente para el caso donde no hay /.

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

El caso límite es cuando hay una barra inclinada final (incluido el caso del directorio raíz, que es todo una barra inclinada). Los comandos basenamey dirnameeliminan las barras diagonales antes de hacer su trabajo. No hay forma de quitar las barras diagonales de una vez si se adhiere a las construcciones POSIX, pero puede hacerlo en dos pasos. Debe ocuparse del caso cuando la entrada consiste en nada más que barras.

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

Si sabe que no está en un caso límite (p. Ej., Un findresultado que no sea el punto de partida siempre contiene una parte del directorio y no tiene seguimiento /), entonces la manipulación de la cadena de expansión de parámetros es sencilla. Si necesita hacer frente a todos los casos límite, las utilidades son más fáciles de usar (pero más lentas).

A veces, es posible que desee tratar foo/como en foo/.lugar de como foo. Si está actuando en una entrada de directorio, foo/se supone que es equivalente a foo/., no foo; esto hace la diferencia cuando foohay un enlace simbólico a un directorio: foosignifica el enlace simbólico, foo/significa el directorio de destino. En ese caso, el nombre base de una ruta con una barra diagonal final es ventajosa ., y la ruta puede ser su propio nombre de directorio.

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

El método rápido y confiable es usar zsh con sus modificadores de historial (este primero elimina las barras diagonales finales, como las utilidades):

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

¹ A menos que esté utilizando shells pre-POSIX como Solaris 10 y anteriores /bin/sh(que carecían de características de manipulación de cadenas de expansión de parámetros en máquinas que aún se encontraban en producción, pero siempre hay un shell POSIX llamado shen la instalación, solo que /usr/xpg4/bin/shno /bin/sh).
² Por ejemplo: envíe un archivo llamado foo␤a un servicio de carga de archivos que no protege contra esto, luego elimínelo y haga fooque se elimine en su lugar

Gilles 'SO- deja de ser malvado'
fuente
Guau. Entonces, ¿suena como (en cualquier shell POSIX) la forma más sólida es la segunda que mencionas? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Estaba leyendo detenidamente y no noté que mencionaras ningún inconveniente.
Comodín el
1
@Wildcard Una desventaja es que trata foo/como foo, no como foo/., lo que no es consistente con las utilidades compatibles con POSIX.
Gilles 'SO- deja de ser malvado'
Gracias. Creo que todavía prefiero ese método porque sabría si estoy tratando de lidiar con directorios y podría agregar (o "agregar") un seguimiento /si lo necesito.
Comodín el
"por ejemplo, un findresultado, que siempre contiene una parte del directorio y no tiene seguimiento /" No es del todo cierto, se find ./mostrará ./como primer resultado.
Tavian Barnes
@Gilles El ejemplo de personaje de nueva línea me dejó sin aliento. Gracias por la respuesta
Sam Thomas
10

Ambos están en POSIX, por lo que la portabilidad "debería" no ser motivo de preocupación. Se debe suponer que las sustituciones de shell se ejecutan más rápido.

Sin embargo, depende de lo que entiendas por portátil. Algunos sistemas antiguos (no necesariamente) no implementaron esas características en su /bin/sh(Solaris 10 y versiones anteriores vienen a la mente), mientras que, por otro lado, hace un tiempo, se advirtió a los desarrolladores que dirnameno era tan portátil como basename.

Para referencia:

Al considerar la portabilidad, tendría que tener en cuenta todos los sistemas donde mantengo programas. No todos son POSIX, por lo que hay compensaciones. Sus compensaciones pueden diferir.

Thomas Dickey
fuente
7

También hay:

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

0

Suceden cosas extrañas como esa porque hay mucha interpretación y análisis y el resto que debe suceder cuando dos procesos hablan. Las sustituciones de comandos eliminarán las nuevas líneas finales. Y NUL (aunque eso obviamente no es relevante aquí) . basenamey dirnametambién eliminará las nuevas líneas finales en cualquier caso porque, ¿de qué otra manera les hablas? Lo sé, las nuevas líneas finales en un nombre de archivo son una especie de anatema de todos modos, pero nunca se sabe. Y no tiene sentido seguir el camino posiblemente defectuoso cuando podría hacerlo de otra manera.

Aún así ... ${pathname##*/} != basenamey de la misma manera ${pathname%/*} != dirname. Esos comandos se especifican para llevar a cabo una secuencia de pasos mayormente bien definida para llegar a los resultados especificados.

La especificación está debajo, pero primero aquí hay una versión 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

Eso es totalmente compatible con POSIX basenameen simple sh. No es difícil de hacer. Fusioné un par de ramas que uso a continuación porque pude sin afectar los resultados.

Aquí está la especificación:

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.

... tal vez los comentarios distraen ...

mikeserv
fuente
1
Wow, buen punto sobre el rastreo de nuevas líneas en nombres de archivo Qué lata de gusanos. Sin embargo, no creo que realmente entienda tu guión. Nunca he visto [!/]antes, ¿es así [^/]? Pero su comentario al lado de eso no parece coincidir ...
Comodín
1
@Wildcard - bueno ... no es mi comentario. Ese es el estándar . La especificación POSIX para basenamees un conjunto de instrucciones sobre cómo hacerlo con su shell. Pero [!charclass]es la forma portátil de hacer eso con globs [^class]es para expresiones regulares, y los proyectiles no están especificados para expresiones regulares. Sobre el juego del comentario ... casefiltros, por lo que si coinciden con una cadena que contiene una barra diagonal / y un !/entonces si el siguiente patrón caso por debajo de partidos cualquier arrastran /golpes rápidos en todos ellos sólo pueden ser todas las barras. Y uno más abajo que no puede tener ningún seguimiento /
mikeserv
2

Puede obtener un impulso en el proceso basenamey dirname(no entiendo por qué estos no son incorporados, si no son candidatos, no sé qué es), pero la implementación debe manejar cosas como:

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

^ De basename (3)

y otros casos de borde.

He estado usando:

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

(Mi última implementación de GNU basenamey dirnameagrega algunos interruptores especiales de línea de comandos sofisticados para cosas como el manejo de múltiples argumentos o la eliminación de sufijos, pero eso es muy fácil de agregar en el shell).

No es tan difícil convertirlos en bashcomponentes incorporados (haciendo uso de la implementación del sistema subyacente), pero la función anterior no necesita compilarse, y también brindan cierto impulso.

PSkocik
fuente
La lista de casos extremos es realmente muy útil. Esos son todos muy buenos puntos. La lista en realidad parece bastante completa; ¿Hay realmente otros casos límite?
Comodín el
Mi implementación anterior no manejaba las cosas x//correctamente, pero lo solucioné antes de responder. Espero que sea eso.
PSkocik
Puede ejecutar un script para comparar lo que hacen las funciones y los ejecutables en estos ejemplos. Estoy obteniendo un 100% de coincidencia.
PSkocik
1
Su función de nombre de directorio no parece eliminar las repeticiones de barras. Por ejemplo: dirname a///b//c//d////erendimientos a///b//c//d///.
codeforester