# Bash: lange Listen auf langsamen Rechner & grep

## slick

Ich habe ein Script welches nach folgendem Muster arbeitet:

```
cat list.txt | while read file ; do

   if ! cat files.log | grep "^${file}$" &> /dev/null ; then

      $Kommando && echo "${file}" >> files.log

   fi

done
```

Das läuft soweit ganz gut. Es werden jedoch mehrere zehntausend Dateien auf einem sehr langsamen Rechner bearbeitet. Das Problem ist nun, das nach einem Stop des Script und erneutem Start mal eben gefühlte 30 min. vergehen bevor es wieder an der Position fortfährt wo es abgebrochen wurde. Logisch, den es muss ja die potenzierte Anzahl der Zeilen bis zu dieser Position mit grep durchsuchen. Da kommt was zusammen.

Hat jemand eine Idee wie ich dieses Konstrukt schneller machen kann, insbesondere unter der Annahme das ich dieses Script während des Durchlaufs anhalten und neustarten muss.

----------

## Finswimmer

Du könntest die list.txt zunächst als Sicherung kopieren, dann in der Schleife die bearbeitete Zeile löschen.

Ich denke, das sollte mit sed gehen. Wenn du das Skript abbrichst, hast du eine Datei, die um X Zeilen kleiner ist.

Alternativ könntest du dir die Zeilennummer in einer separaten Datei speichern und beim erneuten Aufruf diese Anzahl an Zeilen überspringen.

Viele Grüße

Tobi

P.S: Schön, dass du mal wieder da bist  :Wink: 

EDIT:

```
if ! cat files.log | grep "^${file}$" &> /dev/null ; then 
```

ist für mich gefühlt schlechter als

```
if [ $(cat files.log | grep "^${file}$" -c) -eq 0 ] ; then
```

zumindest müsste so auf der Bash nicht die Ausgabe des Grep-Befehls verarbeitet werden, sondern nur ein Integer-Vergleich durchgeführt werden.

Ob und inwieweit das was bringt, kann ich nicht beurteilen.

----------

## slick

Also ein -m1 bringt schonmal deutlich was. Dann hört er beim ersten Treffer aus, was ja auch reicht.

```
 if ! cat files.log | grep -m 1 "^${file}$" &> /dev/null ; then 
```

EDIT:

Deine Version ist messbar langsamer, auch mit -m1.

Btw. ich war nie wirklich weg  :Wink: 

----------

## py-ro

Das cat weglassen sollte auch nochmal helfen, wieder ein Prozess weniger:

```
if ! grep -m 1 "^${file}$" files.log &> /dev/null ; then
```

Die Version oben ist übrigens langsamer, weil [ zusätzlichen code erzeugt, gibt auch nur nen returncode zurück. Außerdem wird jedesmal mit $() eine Sub-Shell erzeugt.

----------

## mv

 *slick wrote:*   

> Also ein -m1 bringt schonmal deutlich was. Dann hört er beim ersten Treffer aus, was ja auch reicht.

 

Wird durch -q impliziert, was extra für solche Dinge gedacht ist. Und besonders hier ist der "useless use of cat" störend, da die Pipe unnötig ist und im Trefferfall abgebrochen wird. Außerdem kann man sich das "^ \$" schenken, wenn man -x benutzt. Ob die Option -F vielleicht sogar gewünscht ist, kann ich dem Kontext nicht entnehmen; schneller wäre es damit wohl allemal:

```
if ! grep -q -x -F -- "${file}" files.log

then ...

fi
```

Edit: Für mich sieht das Problem so aus, dass man statt einer Fileliste lieber ein Hash-Array nehmen sollte - dann braucht man "praktisch" nur konstante Zeit. Hash-Arrays gibt es zumindest in zsh (oder natürlich awk oder perl), aber wenn es denn unbedingt bash sein muss, auch in neueren Bash-Versionen. Aber da hier Zeit ein Thema zu sein scheint, ist bash so ziemlich die schlechteste Wahl: Ich würde eher zu zsh (oder ggf. auch perl oder notfalls awk) raten.

----------

## toralf

Jungs, Detailverbesserungen bringen hier gar nix - wenn der Lösungsansatz selbst nicht für viele Dateien taugt.

Besser (wenn vielleicht auch noch nicht gut genug) ist :

```
[tfoerste ~]$ cat list.txt

file1

file2

file3

[tfoerste ~]$ cat files.log

file3

file4

[tfoerste ~]$ diff list.txt files.log | grep '^<' | cut -f2 -d ' ' | xargs -n1 echo do something with

do something with file1

do something with file2

```

----------

## py-ro

Naja, um eine bessere Lösung zu generieren bräuchte man mehr Informationen über Ausgangslage und Ziel.

----------

## slick

Also das ursprüngliche Ziel ist das rekursive abarbeiten in einem Verzeichnisbaum. Vereinfacht sah das so aus: 

```
find ./ -type f | while read file ; do 

   [..]

done
```

Das ganze auf einem NFS-Mount auf einem kleinem schwachen headless System. Dabei kommen bis zu 100.000 Datein raus. Um mir das durchsuchen jedesmal zu sparen habe ich die Ausgabe von find in die list.txt gepipt und später nur statt dem find die liste verwendet. Die (gefundenen) Dateien ansich werden nicht angefaßt, sie werden nur "lesend" bearbeitet. In der Regel bleibt sowohl das Dateisystem wie auch die Liste im read-only Zugriff. u.U. stoppe ich das Script aber auch, sortiere die Liste nach irgendwelchen Kriterien um (um bestimmte Files zu priorisieren) und starte das Script neu. (Daher fallen Sachen wie 'Zeilennummer merken' eigentlich aus). Bearbeitet werden soll jedes  File in der Liste nur 1x.

Ich hatte auch schon die Idee mit Symlinks o.ä. die bearbeiteten zu "markieren" o.ä. Das ginge jedoch nur auf Ramdisk und die wäre bei einem evt. Reboot weg. Zumal ein Test hier dann vermutlich noch länger dauert. 

Ínteressanter Punkt ist in diesem Szenario noch folgender: Die Abarbeitung selbst dauert je File ein paar Sekunden. Daher ists egal ob da noch ein paar Millisekunden drauf kämen für die Pflege eines Index etc. bei jedem Filedurchlauf.  Entscheidend ist das schnelle Anspringen der letzten Position beim nächsten Scriptstart (nach einem Stopp vor Ende der Abarbeitung)

----------

## py-ro

Also ist das eigentlich Problem, was zu lösen ist, zu behalten welche Dateien bereits bearbeitet wurden

Ich gehe mal davon aus, ein Dateiname pro Zeile.

Quick & Dirty und erlaubt keine Priorisierung...

```

for file in $(cat input log | sort | uniq -u )

do

  something && echo "${file}" >> log

done

```

Ungetestet und ohne Gewähr. Evtl. muss der IFS noch passend gesetzt werden.

Bye

Py

----------

## mv

Wie gesagt: Das natürliche Hilfsmittel für die Aufgabe ist ein Hashtabelle (oder ein sortierter Baum, aber das erledigt sowie die Programmiersprache/Bibliothek).

Hier ein Beispiel für zsh:

```
#! /usr/bin/env zsh

declare -A files

while read i

do files[$i]=

done <list.txt

while read i

do unset "files[$i]"

done <files.log

for i in ${(k)files}

do process_file $i

   printf '%s\n' $i >>files.log

done
```

In Bash ginge es wohl ähnlich, allerdings wäre mehr Quoting nötig (und die Syntax für "unset" und ${(k)files} müsste ich erst nachschauen).

----------

## slick

 *mv wrote:*   

> Hier ein Beispiel für zsh

 

Da gibst nur bash.

----------

## mv

 *slick wrote:*   

> Da gibst nur bash.

 

Ich schrieb doch: Geht ähnlich, ist nur langsamer und mehr Quoting nötig:

```
#! /usr/bin/env bash

declare -A files

while read i

do files["$i"]=

done <list.txt

while read i

do unset files["$i"]

done <files.log

for i in "${!files[@]}"

do process_file "$i"

   printf '%s\n' "$i" >>files.log

done
```

----------

