Comment puis-je obtenir le premier match de l'extension générique?

39

Des shells comme Bash et Zsh développent les caractères génériques en arguments, autant d'arguments que de motifs:

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

Mais que se passe-t-il si je veux seulement que le premier match soit retourné, pas tous les matchs?

$ echo *.txt
1.txt

Les solutions spécifiques au shell ne me dérangent pas, mais j'aimerais une solution qui fonctionne avec des espaces dans les noms de fichiers.

Flimm
la source
ls * .txt | tête -1?
Archemar
1
@Archemar: ne fonctionne pas avec les nouvelles lignes dans les noms de fichiers.
Flimm

Réponses:

26

Une méthode efficace dans bash consiste à développer un tableau et à ne générer que le premier élément:

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

(Vous pouvez même simplement echo $files, un index manquant est traité comme [0].)

Ceci gère en toute sécurité les espaces / tab / newline et autres métacaractères lors du développement des noms de fichiers. Notez que les paramètres régionaux en vigueur peuvent modifier le "premier".

Vous pouvez également le faire de manière interactive avec une fonction d'achèvement de 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

Cela lie la _echofonction pour compléter les arguments de la echocommande (écrasant l'achèvement normal). Un "*" supplémentaire est ajouté dans le code ci-dessus, vous pouvez simplement appuyer sur tab sur un nom de fichier partiel et j'espère que la bonne chose se passera.

Le code est légèrement compliqué, plutôt que de définir ou de supposer nullglob( shopt -s nullglob) nous vérifions que nous compgen -Gpouvons étendre le glob à quelques correspondances, puis nous développons en toute sécurité dans un tableau, et définissons enfin COMPREPLY pour que la citation soit robuste.

Vous pouvez faire ceci en partie (développer un glob par programme) avec les bash compgen -G, mais ce n'est pas robuste car il sort en sortie de stdout.

Comme d'habitude, l'achèvement est plutôt complexe, cela rompt l'achèvement d'autres choses, y compris les variables d'environnement (voir la _bash_def_completion()fonction ici pour plus de détails sur l'émulation du comportement par défaut).

Vous pouvez également simplement utiliser en compgendehors d'une fonction d'achèvement:

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

Un point à noter est que "~" n’est pas un glob, il est traité par bash dans une phase d’expansion distincte, comme le sont $ variables et d’autres expansions. compgen -Gne fait que balayer le nom de fichier, mais compgen -Wvous donne toute l’extension par défaut de bash, bien qu’elle soit peut-être trop étendue (y compris ``et $()). Contrairement à -G, le -W est cité en toute sécurité (je ne peux pas expliquer la disparité). Comme le but de -West de développer les jetons, cela signifie qu'il étendra "un" en "même" même si aucun fichier de ce type n'existe, ce n'est donc peut-être pas idéal.

Ceci est plus facile à comprendre, mais peut avoir des effets secondaires indésirables:

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

Ensuite:

touch $'curious \n filename'

echo curious*tab

Notez l'utilisation de printf %qpour citer en toute sécurité les valeurs.

Une dernière option consiste à utiliser une sortie délimitée par 0 avec les utilitaires GNU (voir la FAQ de 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 )

Cette option vous donne un peu plus de contrôle sur l’ordre de tri (l’ordre lors de l’extension d’un glob est sujet à votre locale / LC_COLLATEet peut se plier ou non à la casse), mais c’est plutôt un gros marteau pour un problème aussi petit ;-)

mr.spuratic
la source
21

Dans zsh, utilisez le [1] qualificatif glob . Notez que même si ce cas spécial renvoie au maximum une correspondance, il reste une liste et les objets globaux ne sont pas développés dans des contextes qui attendent un mot unique, tel que assignations (affectations de tableau exceptées).

echo *.txt([1])

En ksh ou bash, vous pouvez farcir la liste complète des correspondances dans un tableau et utiliser le premier élément.

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

Dans n'importe quel shell, vous pouvez définir les paramètres de position et utiliser le premier.

set -- *.txt
echo "$1"

Cela écrase les paramètres de position. Si vous ne le souhaitez pas, vous pouvez utiliser un sous-shell.

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

Vous pouvez également utiliser une fonction qui possède son propre ensemble de paramètres de position.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
Gilles, arrête de faire le mal
la source
1
Et pour obtenir les premiers matches de $ n $, vous pouvez utiliser*.txt([1,n])
Emre
6

Essayer:

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

Remarque: le développement de nom de fichier est trié en fonction de la séquence de classement en vigueur dans les paramètres régionaux en cours.

cuonglm
la source
3

Une solution simple:

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

Ou utilisez printfsi vous préférez.

G-Man dit 'Réintégrez Monica'
la source
1

Je suis juste tombé sur cette vieille question en me demandant la même chose. J'ai fini avec ceci:

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

Vous pouvez bien sûr remplacer headpar tailet -n1avec tout autre numéro.


Ce qui précède ne fonctionnera pas si vous travaillez avec des fichiers dont le nom contient des nouvelles lignes. Pour travailler avec de nouvelles lignes, vous pouvez utiliser l’un de ces types:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Ne fonctionne pas sur 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)" (Fonctionne avec n'importe quel caractère spécial)
utilisateur149485
la source
3
Non, cela échouera si le nom du fichier contient des nouvelles lignes.
Isaac
7
Dans quel genre de monde fou vivons-nous dans lequel les noms de fichiers contiennent des nouvelles lignes?
Billynoah
-1

Un cas d’utilisation que je rencontre souvent est la définition du répertoire supérieur / inférieur après une expansion globale (par exemple, un répertoire contenant des kits de développement logiciel (SDK) ou des outils de compilation). Dans cette situation, je souhaite généralement enregistrer ce nom de répertoire dans une variable à utiliser à quelques endroits dans un script shell.

Cette commande le fait généralement pour moi:

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

Avertissement: L'expansion de Glob ne va pas trier vos dossiers par semver; tu as été prévenu. C'est très bien si vous n'avez Dockerfilequ'une seule version d'un répertoire, mais cette version des répertoires peut varier d'une image à l'autre

Andrew Odri
la source
Bienvenue chez U & L! Cela gère la plupart des noms de répertoire, mais pas les noms de répertoire avec une nouvelle ligne. Essayez de créer un répertoire comme celui-ci mkdir "$(echo one; echo two)"et voyez ce que je veux dire.
Flimm
Quel est l'avantage par rapport aux autres alternatives, notamment la version qui l'utilise tail?
RalfFriedl
Standard dirnamene prend qu'un seul chemin, vous ne pouvez donc pas compter sur plusieurs noms de chemins sans savoir que votre implémentation le supporte.
Kusalananda
@ Flimm Bon point; Je pense que la plupart des développeurs auraient de plus gros problèmes à résoudre si leur structure de dossiers contenait des retours à la ligne. Je n'ai jamais eu à gérer cela, et je ne m'attendais pas à utiliser des conteneurs et des logiciels à moitié corrects que j'utilise.
Andrew Odri le
@RalfFriedl Bonne question; ceci filtre essentiellement tout ce qui n'est pas un répertoire valide (et ne listera pas / ne traversera pas. et ..)
Andrew Odri