dirname e basename vs expansão de parâmetro

20

Existe alguma razão objetiva para preferir um formulário ao outro? 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 expansão de parâmetro do shell, v2 usa binários externos.)

Curinga
fonte

Respostas:

21

Ambos têm suas peculiaridades, infelizmente.

Ambos são exigidos pelo POSIX, portanto, a diferença entre eles não é uma preocupação 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 finais. Portanto, se um nome de arquivo for chamado foo/bar␤, baseele será definido como em barvez de bar␤. Uma solução alternativa é adicionar um caractere não-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âmetro, você não encontra casos extremos relacionados à expansão de caracteres estranhos, mas há várias dificuldades com o caractere de barra. Uma coisa que não é um caso de extrema importância é que a computação da parte do diretório requer código diferente para o caso em que não existe /.

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

O caso de borda é quando há uma barra final (incluindo o caso do diretório raiz, que é tudo barra). Os comandos basenamee dirnameretiram as barras finais antes que eles façam seu trabalho. Não há como remover as barras finais de uma só vez, se você seguir as construções do POSIX, mas poderá fazê-lo em duas etapas. Você precisa cuidar do caso quando a entrada consistir 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 você souber que não está em um caso de borda (por exemplo, um findresultado que não seja o ponto inicial sempre contém uma parte do diretório e não tem rastro /), a manipulação da cadeia de expansão de parâmetros é simples. Se você precisar lidar com todos os casos extremos, os utilitários serão mais fáceis de usar (mas mais lentos).

Às vezes, você pode querer tratar foo/como foo/., em vez de como foo. Se você está atuando em uma entrada de diretório, foo/é suposto ser equivalente a foo/., não foo; isso faz diferença quando fooexiste um link simbólico para um diretório: foosignifica o link simbólico, foo/significa o diretório de destino. Nesse caso, o nome da base de um caminho com uma barra final é vantajoso .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 (este primeiro remove barras finais, como os utilitários):

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

¹ A menos que você esteja usando shells pré-POSIX como Solaris 10 e mais antigos /bin/sh(que não possuíam recursos de manipulação de cadeia de expansão de parâmetros em máquinas ainda em produção - mas sempre há um shell POSIX chamado shna instalação, só que /usr/xpg4/bin/shnão /bin/sh).
² Por exemplo: envie um arquivo chamado foo␤para um serviço de upload de arquivo que não protege contra isso, exclua-o e faça foocom que seja excluído

Gilles 'SO- parar de ser mau'
fonte
Uau. Então parece que (em qualquer shell POSIX) a maneira mais robusta é a segunda que você menciona? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Eu estava lendo atentamente e não notei que você mencionava quaisquer desvantagens.
Curinga
1
@Wildcard Uma desvantagem é que ele trata foo/como foo, não como foo/., o que não é consistente com os utilitários compatíveis com POSIX.
Gilles 'SO- stop be evil' '
Entendi, obrigado. Acho que ainda prefiro esse método, porque saberia se estou tentando lidar com diretórios e poderia simplesmente seguir (ou "voltar atrás") uma trilha /se precisar.
Curinga
"por exemplo, um findresultado, que sempre contém uma parte do diretório e não tem /" "Não é verdade, find ./será exibido ./como o primeiro resultado.
Tavian Barnes 19/03
@Gilles O exemplo de personagem da nova linha me surpreendeu. Obrigado pela resposta
Sam Thomas
10

Ambos estão no POSIX, portanto, a portabilidade "não deve" ser uma preocupação. Presume-se que as substituições de shell sejam executadas mais rapidamente.

No entanto - depende do que você quer dizer com portátil. Alguns sistemas antigos (não necessários) não implementavam esses recursos em seus dispositivos /bin/sh(Solaris 10 e anteriores), enquanto, por outro lado, há algum tempo, os desenvolvedores foram avisados ​​de que dirnamenão eram tão portáteis quanto basename.

Para referência:

Ao considerar a portabilidade, eu precisaria levar em consideração todos os sistemas em que mantenho programas. Nem todos são POSIX, portanto, existem vantagens e desvantagens. Suas trocas podem ser diferentes.

Thomas Dickey
fonte
7

Há também:

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

0

Coisas estranhas como essa acontecem porque há muita interpretação e análise e o restante precisa acontecer quando dois processos falam. As substituições de comando removerão as novas linhas à direita. E NULs (embora isso obviamente não seja relevante aqui) . basenamee dirnametambém tirará novas linhas de qualquer maneira, porque de que outra forma você fala com elas? Eu sei, rastrear novas linhas em um nome de arquivo é uma espécie de anátema, mas você nunca sabe. E não faz sentido seguir o caminho possivelmente defeituoso quando você poderia fazer o contrário.

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

A especificação está abaixo, mas primeiro aqui está uma versão 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

Isso é totalmente compatível com POSIX de basenamemaneira simples sh. Não é difícil de fazer. Mesclei algumas ramificações que uso abaixo porque 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 sejam perturbadores ...

mikeserv
fonte
1
Uau, bom argumento sobre o rastreamento de novas linhas nos nomes de arquivos. Que lata de vermes. Eu acho que realmente não entendo o seu roteiro. Eu nunca vi [!/]antes, é assim [^/]? Mas o seu comentário ao lado que não parece corresponder-lo ....
Wildcard
1
@Wildcard - bem .. não é o meu comentário. Esse é o padrão . A especificação POSIX para basenameé um conjunto de instruções sobre como fazê-lo com seu shell. Mas [!charclass]a maneira portátil de fazer isso com os globs [^class]é para o regex - e as conchas não são especificadas para o regex. Sobre a correspondência do casefiltro de comentários ... , por isso, se eu corresponder a uma string que contém uma barra final / e um !/se o próximo padrão de caso abaixo corresponder a qualquer barra final, /elas podem ser apenas todas as barras. E um abaixo que não pode ter nenhum /
mikeserv
2

Você pode obter um impulso do processo basenameedirname (não entendo por que eles não são internos - se 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 extremos.

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 mais recente implementação do GNU basename e dirnameadiciona algumas opções especiais de linha de comando para coisas como lidar com vários argumentos ou remoção de sufixos, mas isso é super fácil de adicionar no shell.)

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

PSkocik
fonte
A lista de casos extremos é realmente muito útil. Todos esses são pontos muito bons. A lista realmente parece bastante completa; existem realmente outros casos extremos?
Curinga
Minha implementação anterior não lidava com as coisas x//corretamente, mas eu as corrigi antes de responder. Espero que seja isso.
PSKocik
Você pode executar um script para comparar o que as funções e os executáveis ​​fazem nesses exemplos. Estou conseguindo uma partida de 100%.
PSKocik
1
Sua função dirname não parece remover repetidas ocorrências de barras. Por exemplo: dirname a///b//c//d////erendimentos a///b//c//d///.
codeforester