Fonction Bash pour trouver le modèle de correspondance de fichier le plus récent

141

Dans Bash, je voudrais créer une fonction qui renvoie le nom de fichier du fichier le plus récent qui correspond à un certain modèle. Par exemple, j'ai un répertoire de fichiers comme:

Directory/
   a1.1_5_1
   a1.2_1_4
   b2.1_0
   b2.2_3_4
   b2.3_2_0

Je veux le fichier le plus récent commençant par «b2». Comment faire cela dans bash? J'ai besoin de cela dans mon ~/.bash_profilescript.

jlconlin
la source
4
voir superuser.com/questions/294161/… pour plus d'indices de réponse. Le tri est l'étape clé pour obtenir votre dernier fichier
Wolfgang Fahl

Réponses:

229

La lscommande a un paramètre -tà trier par heure. Vous pouvez ensuite récupérer le premier (le plus récent) avec head -1.

ls -t b2* | head -1

Mais attention: pourquoi vous ne devriez pas analyser la sortie de ls

Mon opinion personnelle: l'analyse lsn'est dangereuse que lorsque les noms de fichiers peuvent contenir des caractères amusants comme des espaces ou des retours à la ligne. Si vous pouvez garantir que les noms de fichiers ne contiennent pas de caractères amusants, l'analyse syntaxique lsest tout à fait sûre.

Si vous développez un script destiné à être exécuté par de nombreuses personnes sur de nombreux systèmes dans de nombreuses situations différentes, je vous recommande vivement de ne pas analyser ls.

Voici comment faire "correctement": Comment puis-je trouver le dernier fichier (le plus récent, le plus ancien, le plus ancien) dans un répertoire?

unset -v latest
for file in "$dir"/*; do
  [[ $file -nt $latest ]] && latest=$file
done
lesmana
la source
8
Note aux autres: si vous faites cela pour un répertoire, vous ajouteriez l'option -d à ls, comme ceci 'ls -td <modèle> | head -1 '
ken.ganong
5
Le lien d' analyse LS dit de ne pas le faire et recommande les méthodes de BashFAQ 99 . Je recherche un 1-liner plutôt que quelque chose de pare-balles à inclure dans un script, donc je vais continuer à analyser les ls de manière non sécurisée comme @lesmana.
Éponyme
1
@Eponymous: Si vous cherchez un one liner sans utiliser le fragile ls, le printf "%s\n" b2* | head -1fera pour vous.
David Ongaro
2
@DavidOngaro La question ne dit pas que les noms de fichiers sont des numéros de version. Il s'agit des heures de modification. Même avec l'hypothèse du nom de fichier, b2.10_5_2cette solution tue.
éponyme
1
Votre seule ligne me donne la bonne réponse, mais la «bonne» façon me donne en fait le fichier le plus ancien . Une idée pourquoi?
NewNameStat
15

La combinaison de findet lsfonctionne bien pour

  • noms de fichiers sans nouvelles lignes
  • pas très grand nombre de fichiers
  • noms de fichiers pas très longs

La solution:

find . -name "my-pattern" -print0 |
    xargs -r -0 ls -1 -t |
    head -1

Décomposons-le:

Avec findnous pouvons faire correspondre tous les fichiers intéressants comme celui-ci:

find . -name "my-pattern" ...

puis en utilisant, -print0nous pouvons passer tous les noms de fichiers en toute sécurité lscomme ceci:

find . -name "my-pattern" -print0 | xargs -r -0 ls -1 -t

des findparamètres et des modèles de recherche supplémentaires peuvent être ajoutés ici

find . -name "my-pattern" ... -print0 | xargs -r -0 ls -1 -t

ls -ttriera les fichiers par heure de modification (le plus récent en premier) et l'imprimera un par ligne. Vous pouvez utiliser -cpour trier par heure de création. Remarque : cela rompra avec les noms de fichiers contenant des nouvelles lignes.

Enfin head -1nous obtient le premier fichier dans la liste triée.

Remarque: xargs utilisez les limites du système à la taille de la liste d'arguments. Si cette taille dépasse, xargsappellera lsplusieurs fois. Cela interrompra le tri et probablement aussi la sortie finale. Courir

xargs  --show-limits

pour vérifier les limites de votre système.

Remarque 2: à utiliser find . -maxdepth 1 -name "my-pattern" -print0si vous ne souhaitez pas rechercher de fichiers dans les sous-dossiers.

Note 3: Comme indiqué par @starfry - l' -rargument pour xargsempêche l'appel de ls -1 -t, si aucun fichier ne correspond au find. Merci pour la suggestion.

Boris Brodski
la source
2
C'est mieux que les solutions basées sur ls, car cela fonctionne pour les répertoires contenant un très grand nombre de fichiers, où ls s'étouffe.
Marcin Zukowski
find . -name "my-pattern" ... -print0me donnefind: paths must precede expression: `...'
Jaakko
Oh! ...signifie «plus de paramètres». Omettez-le simplement si vous n'en avez pas besoin.
Boris Brodski
2
J'ai trouvé que cela peut renvoyer un fichier qui ne correspond pas au modèle s'il n'y a pas de fichiers qui correspondent au modèle. Cela se produit parce que find ne passe rien à xargs qui appelle ensuite ls sans liste de fichiers, ce qui le fait fonctionner sur tous les fichiers. La solution est d'ajouter -rà la ligne de commande xargs qui dit à xargs de ne pas exécuter sa ligne de commande s'il ne reçoit rien sur son entrée standard.
starfry
@starfry merci! Belle prise. J'ai ajouté -rà la réponse.
Boris Brodski
7

Voici une implémentation possible de la fonction Bash requise:

# Print the newest file, if any, matching the given pattern
# Example usage:
#   newest_matching_file 'b2*'
# WARNING: Files whose names begin with a dot will not be checked
function newest_matching_file
{
    # Use ${1-} instead of $1 in case 'nounset' is set
    local -r glob_pattern=${1-}

    if (( $# != 1 )) ; then
        echo 'usage: newest_matching_file GLOB_PATTERN' >&2
        return 1
    fi

    # To avoid printing garbage if no files match the pattern, set
    # 'nullglob' if necessary
    local -i need_to_unset_nullglob=0
    if [[ ":$BASHOPTS:" != *:nullglob:* ]] ; then
        shopt -s nullglob
        need_to_unset_nullglob=1
    fi

    newest_file=
    for file in $glob_pattern ; do
        [[ -z $newest_file || $file -nt $newest_file ]] \
            && newest_file=$file
    done

    # To avoid unexpected behaviour elsewhere, unset nullglob if it was
    # set by this function
    (( need_to_unset_nullglob )) && shopt -u nullglob

    # Use printf instead of echo in case the file name begins with '-'
    [[ -n $newest_file ]] && printf '%s\n' "$newest_file"

    return 0
}

Il utilise uniquement des fonctions intégrées Bash et doit gérer les fichiers dont les noms contiennent des retours à la ligne ou d'autres caractères inhabituels.

pjh
la source
1
Vous pouvez utiliser nullglob_shopt=$(shopt -p nullglob)et ensuite $nullglobremettre nullglobce qu'il était avant.
gniourf_gniourf
La suggestion de @gniourf_gniourf d'utiliser $ (shopt -p nullglob) est bonne. J'essaie généralement d'éviter d'utiliser la substitution de commande ( $()ou les backticks) car elle est lente, en particulier sous Cygwin, même lorsque la commande n'utilise que des builtins. En outre, le contexte de sous-shell dans lequel les commandes sont exécutées peut parfois les amener à se comporter de manière inattendue. J'essaie également d'éviter de stocker des commandes dans des variables (comme nullglob_shopt) car de très mauvaises choses peuvent arriver si vous obtenez la valeur de la variable incorrecte.
pjh
J'apprécie l'attention portée aux détails qui peuvent conduire à un échec obscur lorsqu'ils sont négligés. Merci!
Ron Burk
J'adore que vous ayez choisi un moyen plus unique de résoudre le problème! Il est certain que sous Unix / Linux, il y a plus d'une façon de «skin the cat!». Même si cela demande plus de travail, cela a l'avantage de montrer les concepts des gens. Avoir un +1!
Pryftan
3

Les noms de fichiers inhabituels (comme un fichier contenant le \ncaractère valide peut faire des ravages avec ce type d'analyse. Voici une façon de le faire en Perl:

perl -le '@sorted = map {$_->[0]} 
                    sort {$a->[1] <=> $b->[1]} 
                    map {[$_, -M $_]} 
                    @ARGV;
          print $sorted[0]
' b2*

C'est une transformation schwartzienne utilisée ici.

Glenn Jackman
la source
1
Que le schwartz soit avec vous!
Nathan Monteleone
cette réponse peut fonctionner mais je ne lui ferais pas confiance étant donné la mauvaise documentation.
Wolfgang Fahl
1

Vous pouvez utiliser statavec un fichier glob et un decorate-sort-undecorate avec le temps de fichier ajouté sur le devant:

$ stat -f "%m%t%N" b2* | sort -rn | head -1 | cut -f2-
dawg
la source
Nan. "stat: impossible de lire les informations du système de fichiers pour '% m% t% N': aucun fichier ou répertoire de ce type"
Ken Ingram
Je pense que cela pourrait être pour la version Mac / FreeBSD de stat, si je me souviens bien de ses options. Pour obtenir une sortie similaire sur d'autres plates-formes, vous pouvez utiliserstat -c $'%Y\t%n' b2* | sort -rn | head -n1 | cut -f2-
Jeffrey Cash
1

Incantation de fonction de magie noire pour ceux qui veulent la find ... xargs ... head ...solution ci-dessus, mais sous forme de fonction facile à utiliser pour que vous n'ayez pas à penser:

#define the function
find_newest_file_matching_pattern_under_directory(){
    echo $(find $1 -name $2 -print0 | xargs -0 ls -1 -t | head -1)
}

#setup:
#mkdir /tmp/files_to_move
#cd /tmp/files_to_move
#touch file1.txt
#touch file2.txt

#invoke the function:
newest_file=$( find_newest_file_matching_pattern_under_directory /tmp/files_to_move/ bc* )
echo $newest_file

Impressions:

file2.txt

Lequel est:

Le nom de fichier avec le plus ancien horodatage modifié du fichier sous le répertoire donné correspondant au modèle donné.

Eric Leschinski
la source
1

Utilisez la commande find.

En supposant que vous utilisez Bash 4.2+, utilisez -printf '%T+ %p\n'pour la valeur d'horodatage du fichier.

find $DIR -type f -printf '%T+ %p\n' | sort -r | head -n 1 | cut -d' ' -f2

Exemple:

find ~/Downloads -type f -printf '%T+ %p\n' | sort -r | head -n 1 | cut -d' ' -f2

Pour un script plus utile, consultez le script de recherche le plus récent ici: https://github.com/l3x/helpers

l3x
la source
pour travailler avec des noms de fichiers contenant des espaces, changez cut -d '' -f2,3,4,5,6,7,8,9 ...
valodzka
0

Il existe un moyen beaucoup plus efficace d'y parvenir. Considérez la commande suivante:

find . -cmin 1 -name "b2*"

Cette commande trouve le dernier fichier produit il y a exactement une minute avec la recherche générique sur "b2 *". Si vous voulez des fichiers des deux derniers jours, vous ferez mieux d'utiliser la commande ci-dessous:

find . -mtime 2 -name "b2*"

Le "." représente le répertoire courant. J'espère que cela t'aides.

Naufal
la source
9
Cela ne trouve pas réellement le "modèle de correspondance de fichier le plus récent" ... il trouve simplement tous les fichiers correspondant au modèle créé il y a une minute ou modifié il y a deux jours.
GnP
Cette réponse était basée sur la question posée. En outre, vous pouvez modifier la commande pour consulter le dernier fichier arrivé il y a environ un jour. Cela dépend de ce que vous essayez de faire.
Naufal le
"peaufiner" n'est pas la réponse. c'est comme poster ceci comme réponse: "Il suffit de modifier la commande find et de trouver la réponse en fonction de ce que vous voulez faire".
Kennet Celeste
Pas sûr du commentaire inutile. Si vous pensez que ma réponse n'est pas étayée, veuillez expliquer pourquoi ma réponse n'a pas de sens avec les EXEMPLES. Si vous ne parvenez pas à le faire, veuillez vous abstenir de tout commentaire.
Naufal
1
Votre solution nécessite que vous sachiez quand le dernier fichier a été créé. Ce n'était pas dans la question donc non, votre réponse n'est pas basée sur la question posée.
Bloke Down The Pub