Como posso obter o primeiro jogo da expansão com curinga?

30

Shells como Bash e Zsh expandem curinga em argumentos, como muitos argumentos que correspondem ao padrão:

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

Mas e se eu quiser que o primeiro jogo seja retornado, não todos os jogos?

$ echo *.txt
1.txt

Eu não me importo com soluções específicas de shell, mas gostaria de uma solução que trabalhe com espaços em branco em nomes de arquivos.

    
por Flimm 18.09.2014 / 11:29

5 respostas

22

Uma maneira robusta no bash é expandir em uma matriz e produzir apenas o primeiro elemento:

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

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

Isso lida com segurança com espaço / tabulação / nova linha e outros metacaracteres ao expandir os nomes dos 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 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 liga a função _echo para concluir os argumentos para o comando echo (substituindo a conclusão normal). Um extra "*" é acrescentado no código acima, você pode simplesmente clicar em um nome parcial do arquivo e esperamos que aconteça a coisa certa.

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

Você pode parcialmente fazer isso (expandir programaticamente um glob) com o compgen -G do bash, mas ele não é robusto, pois gera resultados não referenciados para o stdout.

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

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

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

Um ponto a ser observado é que "~" não é um glob, é manipulado por bash em um estágio separado de expansão, assim como variáveis $ e outras expansões. compgen -G apenas faz a globalização de nome de arquivo, mas compgen -W fornece toda a expansão padrão do bash, embora possivelmente muitas expansões (incluindo '' e $() ). Ao contrário de -G , o -W é citado com segurança (não consigo explicar a disparidade). Como o propósito de -W é que ele expande os tokens, isso significa que ele expandirá "a" para "a" mesmo que não exista esse arquivo, por isso 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* guia

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

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

pattern="*.txt"
while IFS= read -r -d $'
pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!
' filename; do printf '%q' "$filename"; break; done < <(find . -maxdepth 1 -name "$pattern" -printf "%f
_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
" | sort -z )

Esta opção lhe dá um pouco mais de controle sobre a ordem de classificação (a ordem de expansão de glob estará sujeita a sua localidade / LC_COLLATE e pode ou não dobrar maiúsculas), mas é um martelo bastante grande para tal um pequeno problema; -)

    
por 18.09.2014 / 18:48
16

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

echo *.txt([1])

Em ksh ou bash, você pode preencher toda a lista 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 elimina os parâmetros posicionais. Se você não quiser, você 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=\"\\""
}
set_to_first f *.txt
echo "$f"
    
por 19.09.2014 / 00:09
5

Tente:

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

Uma observação de que a expansão de nome de arquivo é classificada de acordo com a sequência de intercalação em vigor na localidade atual.

    
por 18.09.2014 / 11:38
3

Uma solução simples:

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

Ou use printf , se preferir.

    
por 18.09.2014 / 21:26
0

Eu só deparei com essa velha pergunta enquanto pensava a mesma coisa. Eu acabei com isso:

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

Você pode, naturalmente, substituir head por tail e -n1 por qualquer outro número.

    
por 30.08.2018 / 00:28