dirname e basename vs parameter expansion

15

Existe alguma razão objetiva para preferir uma forma à outra? Desempenho, confiabilidade, portabilidade?

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"

Produz:

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

(v1 usa a expansão de parâmetros do shell, v2 usa binários externos.)

    
por Wildcard 06.01.2016 / 01:29

4 respostas

13

Ambos têm suas peculiaridades, infelizmente.

Ambos são requeridos pelo POSIX, então a diferença entre eles não é um problema de portabilidade.

A maneira simples de usar os utilitários é

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

Observe as aspas duplas em torno das substituições de variáveis, como sempre, e também o -- após o comando, caso o nome do arquivo comece com um traço (caso contrário, os comandos interpretariam o nome do arquivo como uma opção). Isso ainda falha em um caso extremo, o que é raro, mas pode ser forçado por um usuário mal-intencionado²: a substituição de comandos remove as novas linhas iniciais. Portanto, se um nome de arquivo for chamado de foo/bar␤ , então base será definido como bar em vez de bar␤ . Uma solução alternativa é adicionar um caractere não pertencente à nova linha e removê-lo após a substituição do comando:

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

Com a substituição de parâmetros, você não se depara com casos de borda relacionados à expansão de caracteres estranhos, mas há várias dificuldades com o caractere de barra. Uma coisa que não é um argumento de ponta é que calcular a parte do diretório requer um código diferente para o caso em que não há / .

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

O caso de borda é quando há uma barra à direita (incluindo o caso do diretório raiz, que é todas as barras). Os comandos basename e dirname eliminam barras finais antes de executarem o trabalho. Não há como dividir as barras finais de uma só vez se você mantiver as construções POSIX, mas você pode fazê-lo em duas etapas. Você precisa cuidar do caso quando a entrada consiste em nada além de barras.

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

Se acontecer de você saber que não está em um caso de borda (por exemplo, um find result, que sempre contém uma parte do diretório e não possui / ), a manipulação da cadeia de expansão do parâmetro é simples. Se você precisa lidar com todos os casos de borda, os utilitários são mais fáceis de usar (mas mais lentos).

Às vezes, convém tratar foo/ como foo/. em vez de foo . Se você estiver agindo em uma entrada de diretório, foo/ deve ser equivalente a foo/. , não foo ; isso faz diferença quando foo é um link simbólico para um diretório: foo significa o link simbólico, foo/ significa o diretório de destino. Nesse caso, o nome de base de um caminho com uma barra final é, com vantagem, . e o caminho pode ser seu próprio nome de diretório.

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

O método rápido e confiável é usar o zsh com seus modificadores de histórico (essas primeiras faixas barras à direita, como os utilitários):

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

¹ A menos que você esteja usando shells pré-POSIX como Solaris 10 e /bin/sh (que não tinham recursos de manipulação de strings de expansão de parâmetros em máquinas ainda em produção - mas há sempre um shell POSIX chamado sh no instalação, apenas é /usr/xpg4/bin/sh , não /bin/sh ).
² Por exemplo: envie um arquivo chamado foo␤ para um serviço de upload de arquivo que não proteja contra isso, depois apague-o e faça com que foo seja excluído

    
por 07.01.2016 / 02:50
10

Ambos estão no POSIX, portanto a portabilidade "deve" não ser preocupante. Supõe-se que as substituições do shell sejam executadas mais rapidamente.

No entanto - depende do que você entende por portátil. Alguns sistemas antigos (não necessariamente) não implementaram esses recursos em /bin/sh (o Solaris 10 e mais antigos vêm à mente), enquanto, por outro lado, há algum tempo, os desenvolvedores foram alertados de que dirname não era tão portátil quanto basename .

Para referência:

Ao considerar a portabilidade, eu teria que levar em consideração todos os sistemas em que eu mantenho os programas. Nem todos são POSIX, então há compensações. Suas compensações podem ser diferentes.

    
por 06.01.2016 / 01:36
7

Existe também:

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

Coisas estranhas como essas acontecem porque há muita interpretação e análise e o resto que precisa acontecer quando dois processos falam. Substituições de comandos irão remover novas linhas. E NULs (embora isso obviamente não seja relevante aqui) . basename e dirname também vão retirar novas linhas em qualquer caso, porque de que outra forma você fala com elas? Eu sei, rastrear novas linhas em um nome de arquivo é um tipo de anátema, mas você nunca sabe. E não faz sentido seguir o caminho possivelmente falho quando você poderia fazer o contrário.

Ainda ... ${pathname##*/} != basename e da mesma forma ${pathname%/*} != dirname . Esses comandos são especificados para executar uma sequência de etapas principalmente bem definida para chegar aos resultados especificados.

A especificação está abaixo, mas primeiro, aqui está uma versão mais resumida:

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

Isso é um basename compatível com POSIX no sh simples. Não é difícil de fazer. Eu mesclei alguns ramos que uso abaixo, porque eu poderia, sem afetar os resultados.

Aqui está a especificação:

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.

... talvez os comentários distraiam ...

    
por 07.01.2016 / 01:27
2

Você pode obter um impulso do processo basename e dirname (não entendo por que esses não são internos - se eles não são candidatos, não sei o que é), mas a implementação precisa lidar com coisas como:

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

^ De nome-base (3)

e outros casos de borda.

Eu tenho usado:

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

(Minha implementação mais recente do GNU basename e dirname adiciona algumas opções especiais de linha de comando para coisas como manipular vários argumentos ou remoção de sufixos, mas isso é super fácil de adicionar no shell.)

Não é tão difícil transformá-los em bash builtins (usando a implementação do sistema subjacente), mas a função acima não precisa ser compilada, e eles fornecem algum impulso também.

    
por 06.01.2016 / 02:31