Como posso obter a primeira correspondência da expansão de curinga?

39

Shell como Bash e Zsh expandem o curinga em argumentos, tantos argumentos quanto o padrão:

$ echo *.txt
1.txt 2.txt 3.txt

Mas e se eu quiser apenas retornar a primeira correspondência, e não todas?

$ echo *.txt
1.txt

Não me importo de soluções específicas de shell, mas gostaria de uma solução que funcione com espaço em branco nos nomes de arquivos.

Flimm
fonte
ls * .txt | cabeça -1?
Archemar 18/09/14
1
@Archemar: não funciona com novas linhas nos nomes de arquivos.
Flimm

Respostas:

26

Uma maneira robusta no bash é expandir para uma matriz e gerar apenas o primeiro elemento:

pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!

(Você pode apenas echo $files, um índice ausente é tratado como [0].)

Isso lida com segurança com espaço / tab / nova linha e outros metacaracteres ao expandir os nomes de arquivos. Observe que as configurações de localidade em vigor podem alterar o que é "primeiro".

Você também pode fazer isso interativamente com uma função de conclusão do bash :

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}   # string to expand

    if compgen -G "$cur*" > /dev/null; then
        local files=( ${cur:+$cur*} )   # don't expand empty input as *
        [ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
    fi
}
complete -o bashdefault -F _echo echo

Isso vincula a _echofunção a concluir argumentos para o echocomando (substituindo a conclusão normal). Um "*" extra é anexado no código acima, você pode simplesmente pressionar a tecla Tab em um nome de arquivo parcial e esperamos que a coisa certa aconteça.

O código é um pouco complicado, em vez de definir ou assumir nullglob( shopt -s nullglob) que verificamos compgen -Gpode expandir o glob para algumas correspondências, depois expandimos com segurança para uma matriz e finalmente configuramos COMPREPLY para que a citação seja robusta.

Em parte, você pode fazer isso (expandir programaticamente um globo) com o bash compgen -G, mas não é robusto, pois é gerado sem aspas para o stdout.

Como de costume, a conclusão é bastante complicada, isso interrompe a conclusão de outras coisas, incluindo variáveis ​​de ambiente (consulte a _bash_def_completion()função aqui para obter detalhes sobre como emular o comportamento padrão).

Você também pode usar apenas compgenfora de uma função de conclusão:

files=( $(compgen -W "$pattern") )

Um ponto a ser observado é que "~" não é um globo, ele é tratado pelo bash em um estágio separado de expansão, assim como as variáveis ​​$ e outras expansões. compgen -Gapenas faz o globbing do nome do arquivo, mas compgen -Wfornece toda a expansão padrão do bash, embora possivelmente muitas expansões (incluindo ``e $()). Ao contrário -G, o -W é citado com segurança (não sei explicar a disparidade). Como o objetivo -Wé expandir os tokens, isso significa que ele expandirá "a" para "a" mesmo que esse arquivo não exista, portanto, talvez não seja o ideal.

Isso é mais fácil de entender, mas pode ter efeitos colaterais indesejados:

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}

Então:

touch $'curious \n filename'

echo curious*tab

Observe o uso de printf %qpara citar com segurança os valores.

Uma opção final é usar saída delimitada com 0 com utilitários GNU (consulte as perguntas frequentes do bash ):

pattern="*.txt"
while IFS= read -r -d $'\0' filename; do 
    printf '%q' "$filename"; 
    break; 
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )

Essa opção oferece um pouco mais de controle sobre a ordem de classificação (a ordem ao expandir uma glob estará sujeita à sua localidade / LC_COLLATEe pode ou não ser dobrada), mas, de outra forma, é um martelo bastante grande para um problema tão pequeno ;-)

mr.spuratic
fonte
21

No zsh, use o [1] qualificador glob . Observe que, embora esse caso especial retorne no máximo uma correspondência, ainda é uma lista e os globs não são expandidos em contextos que esperam uma única palavra como atribuições (atribuições de matriz de lado).

echo *.txt([1])

No ksh ou no bash, você pode preencher a lista inteira de correspondências em uma matriz e usar o primeiro elemento.

tmp=(*.txt)
echo "${tmp[0]}"

Em qualquer shell, você pode definir os parâmetros posicionais e usar o primeiro.

set -- *.txt
echo "$1"

Isso derruba os parâmetros posicionais. Se você não quiser, pode usar um subshell.

echo "$(set -- *.txt; echo "$1")"

Você também pode usar uma função, que possui seu próprio conjunto de parâmetros posicionais.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
Gilles 'SO- parar de ser mau'
fonte
1
E para obter as primeiras partidas de $ n $, você pode usar*.txt([1,n])
Emre
6

Experimentar:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

Observe que a expansão do nome do arquivo é classificada de acordo com a sequência de intercalação em vigor no código do idioma atual.

cuonglm
fonte
3

Uma solução simples:

sh -c 'echo "$1"' sh *.txt

Ou use printfse você preferir.

G-Man Diz 'Reinstate Monica'
fonte
1

Eu apenas tropecei nessa pergunta antiga enquanto me perguntava a mesma coisa. Eu acabei com isso:

echo $(ls *.txt | head -n1)

Obviamente, você pode substituir headpor taile -n1por qualquer outro número.


O exemplo acima não funcionará se você estiver trabalhando com arquivos com novas linhas em seus nomes. Para trabalhar com novas linhas, você pode usar qualquer um destes:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Não funciona no BSD)
  • ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'
  • ls -b *.txt | head -n1 | perl -pe 's/\\n/\n/g'
  • echo -e "$(ls -b *.txt | head -n1)" (Funciona com qualquer caractere especial)
user149485
fonte
3
Não, isso falhará se o nome do arquivo tiver novas linhas.
Isaac
7
Em que tipo de mundo louco vivemos, onde os nomes de arquivos contêm novas linhas?
precisa saber é o seguinte
-1

Um caso de uso que encontro frequentemente é definir o diretório superior / inferior após a expansão global (por exemplo, um diretório cheio de SDKs com versão ou ferramentas de construção). Nessa situação, geralmente desejarei salvar esse nome de diretório em uma variável para uso em alguns lugares dentro de um script de shell.

Este comando geralmente faz isso por mim:

export SDK_DIR=$(dirname /path/to/versioned/sdks/*/. | tail -n1)

Isenção de responsabilidade: a expansão Glob não vai classificar suas pastas nem sempre; voce foi avisado. Isso é ótimo se você tiver uma Dockerfileúnica versão de um diretório, mas essa versão dos diretórios possa variar de imagem para imagem.

Andrew Odri
fonte
Bem-vindo ao U&L! Isso lida com a maioria dos nomes de diretório, mas não lida com uma nova linha. Tente criar um diretório como este mkdir "$(echo one; echo two)"e veja o que quero dizer.
Flimm
Qual é a vantagem em comparação com as outras alternativas, especialmente a versão que usa tail?
RalfFriedl 06/06
O padrão dirnameusa apenas um único nome de caminho, portanto você não pode confiar nele trabalhando em vários nomes de caminho, a menos que saiba que sua implementação específica é compatível.
Kusalananda
@Flimm Bom ponto; Eu acho que a maioria dos desenvolvedores teria problemas maiores se sua estrutura de pastas contivesse novas linhas ... Eu nunca tive que lidar com isso e não espero com nenhum container e software meio decente que estou usando
Andrew Odri 06/06
@RalfFriedl Good question; isso basicamente filtra qualquer coisa que não seja um diretório válido (e não listará / atravessará. e ..)
Andrew Odri 06/06