Archiv der Kategorie: Shell

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 && /^$/ )'

STDERROR in eine Variable umleiten

In einem Script verkette ich mehrere Befehle, was aber den Nachteil hat, dass sich für die Fehlerbehandlung nur der Exit-Wert des äußeren Befehls auswerten lässt. Hier die Zeile, um die es geht:

sudo -u backup ssh root@${RECHNER} "~/sbin/do_SSHbackup" > $BACKUPDIR/${RECHNER%%.*}/${AKTDATE}.tgz || OK=NOK

Hier wird die Variable OK nur auf NOK gesetzt, wenn z.B. sudo fehlschlägt oder das Zielverzeichnis nicht existiert. Wirft jedoch das per SSH aufgerufene Script do_SSHbackup einen Fehler, so bekommt die Variable OK nichts davon mit. Auf dem Standardfehlerkanal landet jedoch eine Fehlermeldung.

Wenn ich nun die Ausgabe der Fehlermeldungen in einer Variablen sammeln und diese auswerten könnte, wäre ein Abfangen des Fehlers möglich. Aber wie, ohne dabei den Standardausgabekanal zu beeinflussen? Nach ein wenig „Googleei“ fand ich die Lösung bei stackoverflow.com.

OUTPUT=$( { sudo -u backup ssh root@${RECHNER} "~/sbin/do_SSHbackup" > $BACKUPDIR/${RECHNER%%.*}/${AKTDATE}.tgz; } 2>&1 ) || OK=NOK

Somit bekomme ich alle Fehlermeldungen in die Variable OUTPUT, welche ich im Nachgang auswerten kann:

if [ "$OK" != "OK" -o -n "$OUTPUT" ] ; then
  echo "$OUTPUT">&2
  ...
fi