Archiv der Kategorie: Shell

Aktuellste/älteste Dateien in einem Verzeichniszweig ermitteln

Will man in einem einzelnen Verzeichnis die aktuellsten (oder ältesten) Dateien finden, geht das mit den Sortierungs-Optionen von ls:

ls -tr | tail

zeigt die neusten Dateien an (aktuellste am Ende). Umgekehrt für die ältesten:

ls -t | tail

Will (oder muss 😉 ) man aber eine ganze Verzeichnisstruktur durchsuchen, geht das mit ls nicht. Hier muss man find und stat zu Hilfe nehmen:

find . -exec stat --format '%y %n' {} \; | sort | tail

So erhält man die neusten Dateien, z.B.:

2010-11-08 10:55:49.000000000 +0100 ./javascript/selectm.js
2010-11-08 10:55:59.000000000 +0100 ./Formulareinm.php
2010-11-08 10:57:16.000000000 +0100 ./Eintragenm.php
2012-04-02 10:52:40.000000000 +0200 ./nop.html
2012-04-02 11:02:00.000000000 +0200 ./.htaccess
2012-07-10 17:06:16.000000000 +0200 ./phpinclude/autoabfrage.inc
2012-07-10 17:13:34.000000000 +0200 ./phpinclude/ergebnis.inc
2014-03-04 16:17:58.000000000 +0100 ./phpinclude
2018-10-23 10:22:02.000000000 +0200 ./phpinclude/send_infoMail.inc
2019-04-26 11:51:36.000000000 +0200 .

Will man die ältesten, so muss man die Reihenfolge mit den Sort-Parameter -r umkehren:

find . -exec stat --format '%y %n' {} \; | sort -r | tail

Shell: Zeitstempel in die Ausgabe einfügen

Loggt man innerhalb eines Shellskripts in eine Logdatei, so ist es häufig sinnvoll, den Einträgen einen Zeitstempel voran zu stellen. Das macht das kleine Tool ts (von Timestamp) ganz einfach:

echo "Eine ganz wichtige Meldung" | ts '[%Y-%m-%d %H:%M:%S]' >> meine-Logdatei

Das Tool ts ist Bestandteil von moreutils, das Paket muss also ggf. erst installiert werden. Will/kann man das nicht, kann man sich auch mit awk, perl und anderen Tools helfen. Wie das geht, ist in einem Post auf StackExchange erklärt (wo ich auch den Tipp mit ts her habe).

Dateien mit find suchen und verschieben

Will man Dateien, die sich per find finden lassen, in ein anderes Verzeichnis verschieben, gelingt das nur mit ein paar kleinen Tricks.

Beispielsweise sollen in einem Doku-Wiki-Verzeichnis alle zu foo.txt gehörenden Dateien in das Unterverzeichnis bar verschoben werden (wobei die entspr. bar-Verzeichnisse bereits existieren). Man denk natürlich zunächst an so was:

find . -name "foo*" -exec mv "{} `dirname {}`/bar/`basename {}`" \;

Leider funktioniert das so nicht, weil dirname {} nur . (einen Punkt) zurück liefert.

Nun kann man dirname ja durch ${VAR%/*} ersetzen, aber das funktioniert mit find nur in einer Sub-Shell:

find . -name "foo*" -ok sh -c 'mv -v $0 ${0%/*}/bar/${0##*/}' {} \;

Das funktioniert und dank -ok statt -exec fragt find vor jeder Befehlsausführung, ob das tatsächlich gemacht werden soll. Dabei fällt dann auf, dass find auch gerne die gerade eben verschobene Datei nochmal verschieben möchte. Um das zu verhindern, muss der Zielpfad ausgeklammert werden. Das gelingt so:

find . -name "foo*" -not -path '*/bar/*' -ok sh -c 'mv -v $0 ${0%/*}/bar/${0##*/}' {} \;

In einem Tar-Archiv die größten Dateien ermitteln

Wie in „Tar-Archive vergleichen“ beschrieben, werden Backups manchmal plötzlich größer. Wenn man nun einfach wissen möchte, welches denn die größten Dateien in dem Archiv sind, geht das ohne viel Aufwand mit

tar tvf 20141117.tgz | sed 's/ \{1,\}/\t/g' | cut -f 3,6 | sort -n | tail

Tar gibt die Größe in Bytes aus. Leider kenne ich keine Option, mit welcher sich bei tar die Anzeige der Größe beeinflussen lässt.

Prüfen, ob es im Verzeichnis neue Dateien gibt

Neulich hatte ich die Aufgabe zu prüfen, ob es in einer Reihe von Unterverzeichnissen aktuelle Dateien gibt (d.h.: ob die Backups tatsächlich was übertragen hatten und wo es nicht funktioniert hatte). Die zu prüfenden Verzeichnisse waren allesamt Unterverzeichnisse von netdir, so dass man gut mit einer For-Schleife arbeiten kann:

cd netdir
for dir in `ls -1`
do
   echo $dir
   cd $dir > /dev/null || exit 99
   res=`find . -mtime -2 -type f`
   if [ -z "$res" ]
   then
      echo "$dir - nichts aktuelles gefunden!"
   fi
   cd ..
done

Wenn es also in dem zu prüfenden Verzeichnis keine Datei gibt, die jünger als zwei Tage ist, wird „nichts aktuelles gefunden!“ gemeldet.

Tar-Archive vergleichen

Ich sichere bestimmte Verzeichnisse von Rechnern täglich in Tar-Archiven (siehe auch „Verzeichnisse auf einen anderen Rechner übertragen“). Nun kommt es vor, dass diese Archive plötzlich oder auch mit der Zeit größer werden. Dann will man natürlich wissen, wieso. Um im ersten Schritt zu vermeiden, dass ich die Archive auspacken und dann vergleichen muss, lese ich den Inhalt zwei Tar-Dateien aus, bereite sie mittels sed auf und vergleiche sie mit diff:

tar tvf 20140822.tgz | sed 's/^\([a-zA-Z\-]\{1,\}\) \([^[:blank:]]\{1,\}\) \{1,\}\([0-9]\{1,\}\) .* \(.*\)$/\4 \3/' > 20140822.content 
tar tvf 20140901.tgz | sed 's/^\([a-zA-Z\-]\{1,\}\) \([^[:blank:]]\{1,\}\) \{1,\}\([0-9]\{1,\}\) .* \(.*\)$/\4 \3/' > 20140901.content
diff -y -W 200 --suppress-common-lines 20140822.content 20140901.content > 20140822vs20140901

Sofern sich nicht all zu viele Dateien geändert haben oder dazu gekommen sind, kann das Ergebnis schon erste Hinweise geben. Ansonsten muss man mit den üblichen Werkzeugen (z.B. grep) weiter filtern und analysieren.

Unix-Kommandozeile

Alle Scripte im aktuellen Verzeichnis greppen

Heute hatte ich den Fall, dass ich in einem Verzeichnis alle Perl- und Shell-Scripte nach dem Wort „gtar“ durchsuchen musste. Leider befanden sich in dem Verzeichnis nicht nur Scripte, sondern auch, zum Teil recht große, Binär-Dateien, die ich ausklammern wollte. Entstanden ist der folgende Einzeiler:

for FILE in `ls -1`; do if `file $FILE | grep -q Befehlstext >/dev/null`; then grep -l gtar $FILE; fi; done

Das funktioniert unter HP-UX, bei Linux ist file gesprächiger und muss daher auf den MIME-Type begrenzt werden:

for FILE in `ls -1`; do if `file --mime-type $FILE | egrep -e '(x-perl|x-shellscript)' >/dev/null`; then grep -l gtar $FILE; fi; done

 

Eine Datei umbenennen und dabei das Datum anfügen

Des öfteren kommt man die die Verlegenheit, dass man eine Datei so umbenennen möchte, dass das Modifikationsdatum der Datei an den Namen angehängt wird. Zum Beispiel soll aus „foo.bar“ die Datei „foo.bar_2013-02-12“ werden.

Jetzt kann man das natürlich so von Hand tippen, nach dem man sich mit ls -l über das entsprechende Datum informiert hat. Man kann das aber auch die Shell machen lassen:

mv foo.bar foo.bar_`stat -c '%y' foo.bar | cut -f 1 -d " "`

Dabei muss man aber viel tippen und vor allem den Dateinamen gleich drei mal. Das geht auch anders:

THEFILE=foo.bar && mv $THEFILE ${THEFILE}_`stat -c '%y' $THEFILE | cut -f 1 -d " "`

Weiterhin viel Tipparbeit, daher bauen wir daraus am besten einen Alias. Allerdings kann man einem Alias keine Parameter übergeben, daher machen wir eine Funktion daraus und tragen das folgende in unsere bash.rc ein:

mvdate(){ mv $1 ${1}_`stat -c '%y' $1 | cut -f 1 -d " "`; }

Nun macht mvdate foo.bar aus der Datei „foo.bar“ die „foo.bar_2013-02-12“.

 

diff aus 2x STDOUT

Man kann bei diff normalerweise nur einen der beiden Datei-Parameter durch ein „-“ ersetzen, um diese Daten vom Standard-Input zu lesen. Was aber, wenn man die Ausgaben von zwei Programmen vergleichen und man diese nicht in Dateien zwischenspeichern will?

Bei commandlinefu.com fand ich die Lösung:

diff <( cmd1 ) <( cmd2 )

Man kann also prima die Ausgabe von zwei Greps auf ps vergleichen, um die User zu finden, bei denen ein Programm nicht gestartet ist:

diff <( ps -ef | grep  'bin/anel' | sort | cut -c 1-8 ) \
  <( ps -ef | grep 'script/start_aws' | sort | cut -c 1-8 )

Skip der ersten Zeile wenn leer

Heute hatte ich das Problem, eine leere Ausgabe des Programms „zip“ zu unterdrücken, damit Cron mir keine Mail schickt, wenn es nichts zu zippen gibt.

Findet zip nichts, was es einpacken kann, bekommt man die folgende Meldung:

/usr/bin/zip -r /tmp/test.zip . -t `date -d "-1 days" +%Y-%m-%d` -i "*/bearbeitet/*"

zip error: Nothing to do! (/tmp/test.zip)

Die Zeile mit „Nothing to do!“ lässt sich ja noch einfach mit einem grep unterdrücken, aber wie werde ich die Leerzeile davor los? Ein

| tail +2

bewirkt zwar, dass die Ausgabe erst ab der zweiten Zeile erfolgt – leider auch dann, wenn zip etwas zu tun bekommt. Also muss perl ran:

more testdaten | perl -n -e 'print $_ unless ( $. == 1 && /^$/ )'

Dies unterdrückt die erste Zeile nur, wenn sie leer ist, wobei weitere Leerzeilen nicht ausgefiltert werden.

Der ganze Aufruf sieht dann so aus:

/usr/bin/zip -r /tmp/test.zip . -t `date -d "-1 days" +%Y-%m-%d` -i "*/bearbeitet/*" 2>&1 | \
  perl -n -e 'print $_ unless /zip error: Nothing to do/ || ( $. == 1 && /^$/ )'