Bash-Funktion, um das neueste Dateimatching-Muster zu finden


141

In Bash möchte ich eine Funktion erstellen, die den Dateinamen der neuesten Datei zurückgibt, die einem bestimmten Muster entspricht. Zum Beispiel habe ich ein Verzeichnis von Dateien wie:

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

Ich möchte die neueste Datei, die mit 'b2' beginnt. Wie mache ich das in Bash? Ich muss das in meinem ~/.bash_profileSkript haben.


4
siehe superuser.com/questions/294161/... für weitere Antwort Hinweise. Die Sortierung ist der Schlüsselschritt, um Ihre neueste Datei zu erhalten
Wolfgang Fahl

Antworten:


228

Der lsBefehl verfügt über einen Parameter -tzum Sortieren nach Zeit. Sie können dann die erste (neueste) mit greifen head -1.

ls -t b2* | head -1

Aber Vorsicht: Warum sollten Sie die Ausgabe von ls nicht analysieren?

Meine persönliche Meinung: Das Parsen lsist nur dann gefährlich, wenn die Dateinamen lustige Zeichen wie Leerzeichen oder Zeilenumbrüche enthalten können. Wenn Sie garantieren können, dass die Dateinamen keine lustigen Zeichen enthalten, ist das Parsen lsziemlich sicher.

Wenn Sie ein Skript entwickeln, das von vielen Menschen auf vielen Systemen in vielen verschiedenen Situationen ausgeführt werden soll, empfehle ich dringend, es nicht zu analysieren ls.

So geht's "richtig": Wie finde ich die neueste (neueste, früheste, älteste) Datei in einem Verzeichnis?

unset -v latest
for file in "$dir"/*; do
  [[ $file -nt $latest ]] && latest=$file
done

8
Hinweis für andere: Wenn Sie dies für ein Verzeichnis tun, fügen Sie ls die Option -d hinzu, z. B. 'ls -td <Muster> | Kopf -1 '
ken.ganong

5
Der Parsing-LS- Link fordert dazu auf, dies nicht zu tun, und empfiehlt die Methoden in BashFAQ 99 . Ich bin auf der Suche nach einem 1-Liner und nicht nach einem kugelsicheren Element, das in ein Skript aufgenommen werden soll. Daher werde ich weiterhin unsicher wie @lesmana analysieren.
Eponymous

1
@Eponymous: Wenn Sie nach einem Einzeiler suchen, ohne den zerbrechlichen zu verwenden ls, printf "%s\n" b2* | head -1wird dies für Sie erledigt .
David Ongaro

2
@ DavidOngaro Die Frage besagt nicht, dass die Dateinamen Versionsnummern sind. Hier geht es um Änderungszeiten. Auch mit der Dateinamenannahme b2.10_5_2tötet diese Lösung.
Namensgeber

1
Ihr Einzeiler gibt mir die richtige Antwort, aber der "richtige" Weg gibt mir tatsächlich die älteste Datei. Irgendeine Idee warum?
NewNameStat

15

Die Kombination von findund lsfunktioniert gut für

  • Dateinamen ohne Zeilenumbrüche
  • nicht sehr große Anzahl von Dateien
  • nicht sehr lange Dateinamen

Die Lösung:

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

Lassen Sie es uns zusammenfassen:

Mit können findwir alle interessanten Dateien wie folgt abgleichen:

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

dann können -print0wir mit allen Dateinamen sicher lswie folgt übergeben:

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

Hier können zusätzliche findSuchparameter und Muster hinzugefügt werden

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

ls -tsortiert Dateien nach Änderungszeit (neueste zuerst) und druckt sie einzeln aus. Sie können -cnach Erstellungszeit sortieren. Hinweis : Dies wird mit Dateinamen unterbrochen, die Zeilenumbrüche enthalten.

Endlich bekommen head -1wir die erste Datei in der sortierten Liste.

Hinweis: xargs Verwenden Sie Systembeschränkungen für die Größe der Argumentliste. Wenn diese Größe überschreitet, xargswird lsmehrmals aufgerufen . Dies wird die Sortierung und wahrscheinlich auch die endgültige Ausgabe unterbrechen. Lauf

xargs  --show-limits

um die Grenzen Ihres Systems zu überprüfen.

Hinweis 2: Verwenden find . -maxdepth 1 -name "my-pattern" -print0Sie diese Option, wenn Sie keine Dateien in Unterordnern durchsuchen möchten.

Anmerkung 3: Wie von @starfry hervorgehoben, verhindert das -rArgument für xargsden Aufruf von ls -1 -t, wenn keine Dateien von der übereinstimmen find. Vielen Dank für den Vorschlag.


2
Dies ist besser als die ls-basierten Lösungen, da es für Verzeichnisse mit extrem vielen Dateien funktioniert, in denen ls erstickt.
Marcin Zukowski

find . -name "my-pattern" ... -print0gibt mirfind: paths must precede expression: `...'
Jaakko

Oh! ...steht für "mehr Parameter". Lass es einfach weg, wenn du es nicht brauchst.
Boris Brodski

2
Ich habe festgestellt, dass dies eine Datei zurückgeben kann, die nicht mit dem Muster übereinstimmt, wenn keine Dateien vorhanden sind, die mit dem Muster übereinstimmen. Dies geschieht, weil find nichts an xargs übergibt, wodurch ls ohne Dateilisten aufgerufen wird und alle Dateien funktionieren. Die Lösung besteht darin -r, der xargs-Befehlszeile hinzuzufügen , die xargs anweist, seine Befehlszeile nicht auszuführen, wenn an seiner Standardeingabe nichts empfangen wird.
starfry

@starfry danke! Schöner Fang. Ich fügte -rder Antwort hinzu.
Boris Brodski

7

Dies ist eine mögliche Implementierung der erforderlichen Bash-Funktion:

# 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
}

Es verwendet nur integrierte Bash-Dateien und sollte Dateien verarbeiten, deren Namen Zeilenumbrüche oder andere ungewöhnliche Zeichen enthalten.


1
Sie könnten Sie verwenden nullglob_shopt=$(shopt -p nullglob)und später $nullglobwieder zurücksetzen, nullglobwie es vorher war.
gniourf_gniourf

Der Vorschlag von @gniourf_gniourf, $ (shopt -p nullglob) zu verwenden, ist gut. Ich versuche im Allgemeinen, die Verwendung von Befehlssubstitution ( $()oder Backticks) zu vermeiden, da dies insbesondere unter Cygwin langsam ist, selbst wenn der Befehl nur integrierte Funktionen verwendet. Außerdem kann der Subshell-Kontext, in dem die Befehle ausgeführt werden, manchmal dazu führen, dass sie sich unerwartet verhalten. Ich versuche auch zu vermeiden, Befehle in Variablen (wie nullglob_shopt) zu speichern, da sehr schlimme Dinge passieren können, wenn Sie den Wert der Variablen falsch verstehen.
pjh

Ich schätze die Aufmerksamkeit für Details, die zu einem obskuren Versagen führen können, wenn sie übersehen werden. Vielen Dank!
Ron Burk

Ich finde es toll, dass Sie sich für einen einzigartigeren Weg entschieden haben, um das Problem zu lösen! Es ist sicher, dass es unter Unix / Linux mehr als einen Weg gibt, das zu "häuten cat!". Auch wenn dies mehr Arbeit erfordert, hat es den Vorteil, Menschen Konzepte zu zeigen. Habe eine +1!
Pryftan

3

Ungewöhnliche Dateinamen (z. B. eine Datei mit dem gültigen \nZeichen können bei dieser Art der Analyse zu Chaos führen. Hier ist eine Möglichkeit, dies in Perl zu tun:

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

Das ist eine Schwartzsche Transformation, die dort verwendet wird.


1
Möge der Schwartz mit dir sein!
Nathan Monteleone

Diese Antwort mag funktionieren, aber ich würde ihr angesichts der schlechten Dokumentation nicht vertrauen.
Wolfgang Fahl

1

Sie können statmit einem Dateikugel und einem Dekorieren-Sortieren-Nichtdekorieren mit der auf der Vorderseite hinzugefügten Dateizeit verwenden:

$ stat -f "%m%t%N" b2* | sort -rn | head -1 | cut -f2-

Nee. "stat: kann Dateisysteminformationen für '% m% t% N' nicht lesen: Keine solche Datei oder kein solches Verzeichnis"
Ken Ingram

Ich denke, dies könnte für die Mac / FreeBSD-Version von sein stat, wenn ich mich richtig an die Optionen erinnere. Um ähnliche Ausgaben auf anderen Plattformen zu erhalten, können Siestat -c $'%Y\t%n' b2* | sort -rn | head -n1 | cut -f2-
Jeffrey Cash

1

Dunkle magische Funktionsbeschwörung für diejenigen, die die find ... xargs ... head ...obige Lösung wünschen , aber in einfach zu verwendender Funktionsform, damit Sie nicht denken müssen:

#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

Drucke:

file2.txt

Welches ist:

Der Dateiname mit dem ältesten geänderten Zeitstempel der Datei unter dem angegebenen Verzeichnis, der dem angegebenen Muster entspricht.


1

Verwenden Sie den Befehl find.

Angenommen, Sie verwenden Bash 4.2+, verwenden Sie diesen -printf '%T+ %p\n'Wert für den Zeitstempel der Datei.

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

Beispiel:

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

Ein nützlicheres Skript finden Sie im neuesten Suchskript hier: https://github.com/l3x/helpers


Um mit Dateinamen zu arbeiten, die Leerzeichen enthalten, ändern Sie cut -d '' -f2,3,4,5,6,7,8,9 ...
valodzka

0

Es gibt einen viel effizienteren Weg, dies zu erreichen. Betrachten Sie den folgenden Befehl:

find . -cmin 1 -name "b2*"

Dieser Befehl findet die neueste Datei, die vor genau einer Minute mit der Platzhaltersuche auf "b2 *" erstellt wurde. Wenn Sie Dateien aus den letzten zwei Tagen möchten, sollten Sie den folgenden Befehl verwenden:

find . -mtime 2 -name "b2*"

Das "." repräsentiert das aktuelle Verzeichnis. Hoffe das hilft.


9
Dies findet nicht das "neueste Dateimatching-Muster" ... es findet nur alle Dateimatching-Muster, die vor einer Minute erstellt oder vor zwei Tagen geändert wurden.
GnP

Diese Antwort basierte auf der gestellten Frage. Sie können den Befehl auch optimieren, um die neueste Datei anzuzeigen, die vor ungefähr einem Tag eingegangen ist. Es hängt davon ab, was Sie versuchen zu tun.
Naufal

"Tweaking" ist nicht die Antwort. Es ist so, als würde man dies als Antwort posten: "Optimieren Sie einfach den Suchbefehl und finden Sie die Antwort, je nachdem, was Sie tun möchten."
Kennet Celeste

Ich bin mir nicht sicher über den unnötigen Kommentar. Wenn Sie der Meinung sind, dass meine Antwort nicht begründet ist, geben Sie bitte den richtigen Grund an, warum meine Antwort mit BEISPIELEN keinen Sinn ergibt. Wenn dies nicht möglich ist, verzichten Sie bitte auf weitere Kommentare.
Naufal

1
Für Ihre Lösung müssen Sie wissen, wann die neueste Datei erstellt wurde. Das war nicht in der Frage, also nein, Ihre Antwort basiert nicht auf der gestellten Frage.
Bloke Down The Pub
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.