|
rklm
Projektleitung
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
Ich such nach einer eleganteren Lösung. Ziel ist es umgangssprachlich, alle Dateien zu finden und zu verarbeiten, die unter einem Verzeichnis liegen, das einen bestimmten Namen (z.B. "wanted") hat und vor höchstens zehn Tagen modifiziert wurde. Der naive Ansatz | find -path '*/wanted/*' -mtime -10 -type f
|
funktioniert offensichtlich nicht, weil -mtime die Dateien filtert und nicht das Verzeichnis. Versuchen wir also einen geschachtelten Aufruf von find, der im ersten Schritt die Verzeichnisse findet und im zweiten die Dateien liefert: | $ find -type d -name 'wanted' -mtime -10 -exec find {} -type f +
find: missing argument to `-exec'
|
Aha, find kann also nicht mehrere Argumente an einer beliebigen Stelle der Kommandozeile einsetzen. Dann also eins. Das geht: | $ find -type d -name 'wanted' -mtime -10 -exec find {} -type f \;
./a/wanted/file1
./a/wanted/file2
./a/wanted/file3
./a/wanted/file4
./a/wanted/file5
./b/wanted/file1
./b/wanted/file2
...
|
Wir sind nicht am Ziel, denn wir wollen die Dateien ja auch verarbeiten - sagen wir der Einfachheit halber mit wc. Also: | $ find -type d -name 'wanted' -mtime -10 -exec find {} -type f -exec wc {} + \;
find: Only one instance of {} is supported with -exec ... +
$ find -type d -name 'wanted' -mtime -10 -exec find {} -type f -exec wc {} \; \;
find: paths must precede expression: `;'
$ find -type d -name 'wanted' -mtime -10 -exec find {} -type f -exec wc {} \; +
find: paths must precede expression: `+'
|
Wieder nix. Probieren wir also xargs: | $ find -type d -name 'wanted' -mtime -10 -exec find {} -type f -print0 \; | xargs -0r wc
0 0 0 ./a/wanted/file1
0 0 0 ./a/wanted/file2
0 0 0 ./a/wanted/file3
0 0 0 ./a/wanted/file4
0 0 0 ./a/wanted/file5
0 0 0 ./b/wanted/file1
0 0 0 ./b/wanted/file2
...
0 0 0 total
|
Aha! Das macht jetzt, was es soll und wir können auch weitere Kriterien auf die Dateien loslassen (z.B. mit einer bestimmten Erweiterung wie ".txt" filtern): | find -type d -name 'wanted' -mtime -10 -exec find {} -type f -name '*.txt' -print0 \; | xargs -0r wc
|
Ich finde das ziemlich hässlich, aber es ist die eleganteste Lösung, die mir eingefallen ist. Auch eine Ruby-Lösung ist noch länger - und die beinhaltet noch nicht einmal die Verarbeitung: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | #!/usr/bin/ruby
require 'find'
dirs = []
start = Time.now - 10 * 60 * 60
Find.find(*ARGV) do |f|
if File.directory?(f) && File.basename(f) == 'wanted' && File.stat(f).mtime > start
dirs << f
Find.prune
end
end
Find.find(*dirs) do |f|
puts f if f.end_with?('.txt')
end
|
Hat jemand eine noch elegantere Lösung?
|
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4735
|
Ich glaube „elegant“ ist hier „nicht kurz“. Das ist ja in sich kein Eleganzkriterium, sondern kann auch einfach nur „schwer verständlich“ bedeuten. Die find-Lösung macht was subtil anderes als das Ruby-Programm und beim Ruby-Programm frage ich mich ob das nicht eventuell noch subtil anders sein sollte. find durchsucht auch alle wanted/-Verzeichnisse rekursiv. Die Ruby-Lösung nur wenn das Datumskriterium nicht zutrifft. Soll das so, oder sollte das nur vom Namen abhängen, aber nicht von der Zeit? Ich finde ja Python-Lösungen fast grundsätzlich elegant, aber da bin ich nicht ganz unvoreingenommen. 😉 Hier mal so in etwa was das Ruby-Programm macht:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | #!/usr/bin/env python3
import subprocess
import sys
from datetime import datetime as DateTime, timedelta as TimeDelta
from pathlib import Path
def get_mtime(path):
return DateTime.fromtimestamp(path.stat().st_mtime)
def find_directory_paths(base_path, name, start_timestamp):
for path in base_path.iterdir():
if path.is_dir():
if path.name == name and get_mtime(path) >= start_timestamp:
yield path
else:
yield from find_directory_paths(path, name, start_timestamp)
def main():
for base_path in map(Path, sys.argv[1:]):
for directory_path in find_directory_paths(
base_path, "wanted", DateTime.now() - TimeDelta(days=10)
):
for path in directory_path.rglob("*.txt"):
if path.is_file():
subprocess.run(["wc", str(path)], check=True)
if __name__ == "__main__":
main()
|
Wenn die Abweichung vom find mit dem prune nicht wäre, hätte man da auch einfach rglob() verwenden können, statt selbst eine rekursive Funktion zu schreiben.
|
|
seahawk1986
Anmeldungsdatum: 27. Oktober 2006
Beiträge: 11278
|
Spricht etwas dagegen den Pfadbestandteil gleich im rglob Aufruf zu nutzen und die m_time für den jeweiligen Ordner zu cachen?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | #!/usr/bin/env python3
import functools
import itertools
import shlex
import subprocess
import sys
import time
from argparse import ArgumentParser
from pathlib import Path
parser = ArgumentParser(
prog='process_recent_folders',
description='Find all folders that match the given name and maximum age and run a command on each of the files they contain matching a glob search string',
)
parser.add_argument("BASE_FOLDER", nargs="*", type=Path, help="folders to process", default=[Path()])
parser.add_argument("-a", "--age", type=int, default=10, help="maximum age in days (default: 10)")
parser.add_argument("-d", "--dir-name", type=str, help="folder name to look for", required=True)
parser.add_argument("-f", "--file-glob", type=str, help="filter file names with this glob string", default="*")
parser.add_argument("CMD", help="command to run with each file as an argument")
@functools.cache
def within_time(path: Path, max_age: float) -> bool:
return max_age <= path.stat().st_mtime
def main():
args = parser.parse_args()
max_age = time.time() - (args.age * 24 * 3600)
for p in itertools.chain.from_iterable(Path(d).rglob(f'{args.dir_name}/{args.file_glob}') for d in args.BASE_FOLDER):
if p.is_file() and within_time(p.parent, max_age):
subprocess.run([*shlex.split(args.CMD), str(p)], check=True)
if __name__ == '__main__':
main()
|
$ python3 process_files.py -h
usage: process_recent_folders [-h] [-a AGE] -d DIR_NAME [-f FILE_GLOB] [BASE_FOLDER ...] CMD
Find all folders that match the given name and maximum age and run a command on each of the files they contain matching a glob search string
positional arguments:
BASE_FOLDER folders to process
CMD command to run with each file as an argument
options:
-h, --help show this help message and exit
-a AGE, --age AGE maximum age in days (default: 10)
-d DIR_NAME, --dir-name DIR_NAME
folder name to look for
-f FILE_GLOB, --file-glob FILE_GLOB
filter file names with this glob string Dann kann man z.B. sowas machen, um ausgehend vom aktuellen Ordner alle Unterordner mit dem Namen wanted zu suchen, die jünger als 10 Tage sind und auf die darin enthaltenen Dateien mit der Endung .txt den Befehl wc anzuwenden:
python3 process_files.py -a 10 -d 'wanted' -f '*.txt' wc Damit man dem Befehl noch zusätzliche Argumente mitgeben kann, habe ich noch shlex.split auf das letzte Argument angewendet, das das Skript übergeben bekommt - damit funktioniert z.B. auch sowas:
python3 process_files.py -a 10 -d 'wanted' -f '*.txt' 'wc -l'
|
|
shiro
Supporter
Anmeldungsdatum: 20. Juli 2020
Beiträge: 1303
|
Hat jemand eine noch elegantere Lösung?
Warum nutzt du nicht "-wholename" ?
$ # Erstelle Testumgebung
$ mkdir test
$ cd test
$ mkdir -p {a,b}/{wanted,notwanted}
$ touch {a,b}/wanted/file{1..3}
$ touch {a,b}/notwanted/file{4..6}
$ # Nun der Test mit find
$ find -mtime -10 -type f -wholename "*/wanted/*"
./a/wanted/file5
./a/wanted/file1
./a/wanted/file3
./a/wanted/file4
./a/wanted/file2
./b/wanted/file1
./b/wanted/file2
$ find -mtime -10 -type f -wholename "*/wanted/*" -exec wc {} \;
0 0 0 ./a/wanted/file5
0 0 0 ./a/wanted/file1
0 0 0 ./a/wanted/file3
0 0 0 ./a/wanted/file4
0 0 0 ./a/wanted/file2
0 0 0 ./b/wanted/file1
0 0 0 ./b/wanted/file2
|
|
seahawk1986
Anmeldungsdatum: 27. Oktober 2006
Beiträge: 11278
|
Das Ziel ist soweit ich die Anforderungen verstehe die mtime des Ordners wanted zu nutzen, nicht die der jeweiligen Dateien.
|
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4735
|
@seahawk1986: Da ist jetzt das Find.prune aus dem Ruby-Programm nicht umgesetzt. Ist halt die Frage ob das wichtig ist oder nicht. Anmerkung: Das Muster sollte man nicht als Zeichenkette zusammenbasteln, sonst hat man da einen festen Pfadtrenner. Also eher Path(d).rglob(str(Path(args.dir_name, args.file_glob))) an der Stelle.
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
Marc_BlackJack_Rintsch schrieb: Ich glaube „elegant“ ist hier „nicht kurz“. Das ist ja in sich kein Eleganzkriterium, sondern kann auch einfach nur „schwer verständlich“ bedeuten.
Stimmt, über Eleganz kann man streiten.
Die find-Lösung macht was subtil anderes als das Ruby-Programm und beim Ruby-Programm frage ich mich ob das nicht eventuell noch subtil anders sein sollte.
Ich sollte auch -prune bei find benutzen - guter Punkt! Der Unterschied tritt aber nur zu Tage, wenn es ein "wanted"-Verzeichnis unterhalb eines "wanted"-Verzeichnisses gibt und beide das Zeitkriterium erfüllen. In dem Fall wird nämlich der tiefere Teilbaum zwei Mal durchlaufen.
find durchsucht auch alle wanted/-Verzeichnisse rekursiv. Die Ruby-Lösung nur wenn das Datumskriterium nicht zutrifft.
Ich finde die Formulierung irreführend. Die Ruby-Version bricht die Rekursion ab, wenn sie eines der gesuchten Verzeichnisse (also die, die Namen- und Zeit-Kriterium erfüllen) gefunden hat - sonst nicht. Das ergibt auch Sinn, weil dann der Baum nicht doppelt durchlaufen wird. Solange es keine geschachtelten "wanted" gibt, führt das aber nur zu mehr IO und CPU-Nutzung.
Soll das so, oder sollte das nur vom Namen abhängen, aber nicht von der Zeit?
Es hängt ja von beiden ab. Kann es sein, dass Du da irgendwo gedanklich falsch abgebogen bist? seahawk1986 schrieb: Spricht etwas dagegen den Pfadbestandteil gleich im rglob Aufruf zu nutzen und die m_time für den jeweiligen Ordner zu cachen?
Tue mich etwas schwer den Python-Code zu lesen, aber kann es sein, dass da nur Dateien gefunden werden, die direkt in einem "wanted"-Verzeichnis liegen? Das wäre zu restriktiv. Ich sehe auch nicht das mtime-Caching, aber das ist ein Nebenthema. Insgesamt bin ich nicht auf der Suche nach einem Programm / Skript, sondern eher nach einer ad-hoc-Lösung. Vielleicht hätte ich das Ruby-Programm nicht posten sollen. Das war möglicherweise irreführend. Ich wollte nur zeigen, dass ich das selbst in Ruby nicht besser hinbekommen habe. shiro schrieb: Hat jemand eine noch elegantere Lösung?
Warum nutzt du nicht "-wholename" ?
Ich habe ja -path genutzt, aber das tut ja nicht. -wholename ist das gleiche, aber mehr Tipparbeit und weniger portabel: -wholename pattern
See -path. This alternative is less portable than -path.Du wiederholst die Lösung aus meinem Eingangsposting, die die Anforderungen nicht erfüllt.
|
|
user_unknown
Anmeldungsdatum: 10. August 2005
Beiträge: 17630
|
xargs!
Ruby!
Python! ☺ Prelude:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | hp430g:~/proj/mini/forum/tmp/f 🐧 > mkdir test
cd test
mkdir -p {a,b}/{,not}wanted
touch {a,b}/{,not}wanted/file{1..3}
hp430g:~/proj/mini/forum/tmp/f/test 🐧 > tree
.
├── a
│ ├── notwanted
│ │ ├── file1
│ │ ├── file2
│ │ └── file3
│ └── wanted
│ ├── file1
│ ├── file2
│ └── file3
└── b
├── notwanted
│ ├── file1
│ ├── file2
│ └── file3
└── wanted
├── file1
├── file2
└── file3
6 directories, 12 files
|
Bashash fifind: | find $(find -type d -name 'wanted' -mtime -10) -type f -exec wc {} \;
|
Elegantes Design ist erreicht, wenn man nichts mehr weglassen kann. ☺
|
|
Marc_BlackJack_Rintsch
Ehemalige
Anmeldungsdatum: 16. Juni 2006
Beiträge: 4735
|
rklm schrieb: Soll das so, oder sollte das nur vom Namen abhängen, aber nicht von der Zeit?
Es hängt ja von beiden ab. Kann es sein, dass Du da irgendwo gedanklich falsch abgebogen bist?
Nö wieso? Ich weiss dass das von beidem abhängt, meine Frage war ob das so sein sollte. Ich fand das komisch und hätte eher gedacht, dass die Entscheidung Rekursion oder nicht eher nur vom Namen abhängen sollte, und getrennt von der Entscheidung verarbeiten oder nicht sein sollte. Aber das hängt wie gesagt vom Einsatzzweck ab.
seahawk1986 schrieb: Spricht etwas dagegen den Pfadbestandteil gleich im rglob Aufruf zu nutzen und die m_time für den jeweiligen Ordner zu cachen?
Tue mich etwas schwer den Python-Code zu lesen, aber kann es sein, dass da nur Dateien gefunden werden, die direkt in einem "wanted"-Verzeichnis liegen? Das wäre zu restriktiv. Ich sehe auch nicht das mtime-Caching, aber das ist ein Nebenthema.
Stimmt, da werden nur direkte Kinder des Verzeichnisses gefunden. Das lässt sich aber leicht beheben, mit einem zusätzlichen **,
also im konkreten Fall dann "wanted/**/*.txt" als Muster. Dann ist aber der Test auf den Zeitstempel vom Elternverzeichnis ein bisschen mehr Code, weil man nicht nur zum direkten Elternteil gehen muss. Das Caching ist der @functools.cache-Dekorator. Der merkt sich eine Abbildung von Argumenten zu Ergebnis und wenn es die Argumente schon mal gab, wird das gecachte Ergebnis geliefert, sonst die Funktion aufgerufen und das Ergebnis geliefert und in den Cache gesteckt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 | #!/usr/bin/env python3
import functools
import itertools
import shlex
import subprocess
import time
from argparse import ArgumentParser
from pathlib import Path
parser = ArgumentParser(
description=(
"Find all folders that match the given name and maximum age and run a"
" command on each of the files they contain matching a glob search"
" string."
),
)
parser.add_argument(
"base_folders",
metavar="BASE_FOLDER",
nargs="*",
type=Path,
help="folders to process or current working directory",
default=[Path()],
)
parser.add_argument(
"-a",
"--age",
type=int,
default=10,
help="maximum age in days (default: %(default)s)",
)
parser.add_argument(
"dir_name", metavar="DIR_NAME", type=str, help="folder name to look for"
)
parser.add_argument(
"-f",
"--file-glob",
metavar="PATTERN",
type=str,
help="filter file names with this glob pattern (default: %(default)s)",
default="*",
)
parser.add_argument(
"command",
metavar="CMD",
help="command to run with each file as an argument",
)
@functools.cache
def within_time(path, max_age):
return max_age <= path.stat().st_mtime
def main():
args = parser.parse_args()
max_age = time.time() - (args.age * 24 * 3600)
glob_pattern = str(Path(args.dir_name, "**", args.file_glob))
for path in itertools.chain.from_iterable(
Path(p).rglob(glob_pattern) for p in args.base_folders
):
dir_path = path
while dir_path.name != args.dir_name:
dir_path = dir_path.parent
if path.is_file() and within_time(dir_path, max_age):
subprocess.run([*shlex.split(args.CMD), str(path)], check=True)
if __name__ == "__main__":
main()
|
Kein „pruning“, aber auch keine mehrfache Verarbeitung von Dateien.
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
user_unknown schrieb:
| find $(find -type d -name 'wanted' -mtime -10) -type f -exec wc {} \;
|
Elegantes Design ist erreicht, wenn man nichts mehr weglassen kann. ☺
☺ Eine schöne Lösung, die allerdings die Einschränkung hat, dass sie nicht funktioniert, wenn in Pfadnamen Leerzeichen auftreten können. Das kann man mit einer Änderung retten: | find $(find -type d -name 'wanted' -mtime -10 -exec ls -d --quoting-style=shell {} +) -type f -exec wc {} +
|
Wenn man die Umgebungsvariable QUOTING_STYLE entsprechend setzt, wird es kürzer, weil man sich das Argument bei ls sparen kann. Noch eine kleine Änderung: Da ich meist mehrere Dateien uniform verarbeite, nehme ich -exec ... + anstatt -exec ... \;.
|
|
user_unknown
Anmeldungsdatum: 10. August 2005
Beiträge: 17630
|
rklm schrieb:
☺ Eine schöne Lösung, die allerdings die Einschränkung hat, dass sie nicht funktioniert, wenn in Pfadnamen Leerzeichen auftreten können. Das kann man mit einer Änderung retten: | find $(find -type d -name 'wanted' -mtime -10 -exec ls -d --quoting-style=shell {} +) -type f -exec wc {} +
|
Oder, indem man (Dateien u.) Verzeichnisse mit solch unpraktischen Namen vorher umbenennt - das kann man 1x machen, und hat dann für die Zukunft (wenn man es konsequent für alle Dateien umsetzt) Ruhe. ☺
Wenn man die Umgebungsvariable QUOTING_STYLE entsprechend setzt, wird es kürzer, weil man sich das Argument bei ls sparen kann.
Interessant - den kannte ich noch nicht. ☺ Bis ich das nochmal brauche habe ich es hoffentlich nicht ganz vergessen. ☺
Noch eine kleine Änderung: Da ich meist mehrere Dateien uniform verarbeite, nehme ich -exec ... + anstatt -exec ... \;.
Tja - da muss man vorher analysieren oder testen, ob es klappen wird - das dauert meist länger, als dass es die Zeitersparnis zur Laufzeit wieder reinholt, außer man verewigt es in einem Script, das immer wieder läuft und auch viele, viele Dateien verarztet. Aber auch, weil man das Plus nicht maskieren muss erwägenswert.
|
|
frostschutz
Anmeldungsdatum: 18. November 2010
Beiträge: 7795
|
rklm schrieb:
die allerdings die Einschränkung hat, dass sie nicht funktioniert, wenn in Pfadnamen Leerzeichen auftreten können
Die Argumentliste darf auch insgesamt nicht zu lang werden... und dann ist man doch wieder bei xargs. Da schreibt man einen Kommentar dazu was das macht und gut ist. Schön oder nicht...
|
|
rklm
Projektleitung
(Themenstarter)
Anmeldungsdatum: 16. Oktober 2011
Beiträge: 13242
|
user_unknown schrieb: rklm schrieb:
Oder, indem man (Dateien u.) Verzeichnisse mit solch unpraktischen Namen vorher umbenennt - das kann man 1x machen, und hat dann für die Zukunft (wenn man es konsequent für alle Dateien umsetzt) Ruhe. ☺
Das geht nicht in allen Fällen, z.B. wenn Namen verglichen werden oder das Vorhandensein einer Datei mit einem bestimmten Namen einen Zustand wie z.B. "verarbeitet" anzeigt.
Noch eine kleine Änderung: Da ich meist mehrere Dateien uniform verarbeite, nehme ich -exec ... + anstatt -exec ... \;.
Tja - da muss man vorher analysieren
Da muss man nicht groß analysieren.
Aber auch, weil man das Plus nicht maskieren muss erwägenswert.
Und ein + gibt sich generell leichter ein als ein \, der mit einer deutschen Tastatur zwei Tastendrücke benötigt. Dank Euch allen für die Anregungen!
|
|
user_unknown
Anmeldungsdatum: 10. August 2005
Beiträge: 17630
|
rklm schrieb: user_unknown schrieb: rklm schrieb:
Oder, indem man (Dateien u.) Verzeichnisse mit solch unpraktischen Namen vorher umbenennt - das kann man 1x machen, und hat dann für die Zukunft (wenn man es konsequent für alle Dateien umsetzt) Ruhe. ☺
Das geht nicht in allen Fällen, z.B. wenn Namen verglichen werden oder das Vorhandensein einer Datei mit einem bestimmten Namen einen Zustand wie z.B. "verarbeitet" anzeigt.
Das verstehe ich nun nicht. Das Vorhandensein einer Datei mit einem bestimmten Namen zeigt einen Zustand wie z.B. "verarbeitet" an? Und dann hilft "ls --quoting-style ..."?
Noch eine kleine Änderung: Da ich meist mehrere Dateien uniform verarbeite, nehme ich -exec ... + anstatt -exec ... \;.
Tja - da muss man vorher analysieren
Da muss man nicht groß analysieren.
Deswegen habe ich auch nicht "**groß** analysieren" geschrieben sondern schlicht "analysieren". Bzw. man muss es für das jeweilige nur wissen, aber wenn man es nicht weiß, ausprobieren oder der Doku noch mal nachschauen oder auf gut Glück hoffen. ☺ Das dauert nicht groß, sondern nur klein, aber unterbricht den Arbeitsfluss ein wenig, und es ist dann oft schneller einfach ";" zu verwenden.
|