print_r für Perl

Ich nutze unter PHP recht oft die Funktion print_r für’s Debugging. Diese Funktion gibt eine beliebige Variable strukturiert aus. Sinnvoll ist das natürlich am meisten bei Arrays und Objekten.

Für Perl gibt es kein print_r, dafür aber Data::Dumper (CPAN).

Ein kleines Beispiel:

use Data::Dumper;
my @arr = qw(foo bar foobar);
my %hash = (
             foo => 'bar',
             bar => @arr
           );
print Dumper(@arr, \%hash);

Das ergibt:

$VAR1 = [
          'foo',
          'bar',
          'foobar'
        ];
$VAR2 = {
          'bar' => $VAR1,
          'foo' => 'bar'
        };

Steht Data::Dumper mal nicht zur Verfügung geht es zur Not auch ohne.

Ein einfaches Array kann man sich so ansehen:

print "@array";

Mit dem Array aus den Beispiel von oben ergibt das:

foo bar foobar

Bei Hashs funktioniert das aber nicht. Hier geht es aber so:

my %hash =  (
              A => 1,
              B => 2,
              C => 3,
            );

{
  local $, = ' ';
  print %hash, $/;
}

Die Ausgabe sieht so aus:

A 1 C 3 B 2

Zugegeben, Data::Dumper ist da optisch besser, aber in der Not frisst der Teufel Fliegen…

Eine Reihe von IP-Adressen per Ping prüfen

Mitunter hat man das Problem, eine nicht kleine Anzahl an Rechnern auf Erreichbarkeit zu überprüfen. Die Adressen und Hostnamen liegen als Excel-Liste vor, aber diese alle von Hand zu pingen … dazu bin ich echt zu faul. Also habe ich mir ein kleines Script gebaut, welches diese Aufgabe elegant erledigt:

#!/bin/sh
#
# Sendet einen Ping an alle in der übergebenen Datei aufgeführten Adressen.
# In der Liste muss pro Zeile zuerst die IP-Adresse und danach durch Leerzeichen
# getrennt der Hostname stehen (optional).
# Die Ausgabe von Ping kann unterdrückt werden, indem STDERR ins Null-Dev
# geschickt wird:
# ping_list liste.txt 2>/dev/null
#

#set -x

Newline='
'

IFS=$Newline
for LINE in `cat $1`
do
  #echo "'$LINE'"
  ADR=${LINE%% *}
  HOST=${LINE##* }
  #echo "Adr: $ADR"
  #echo "Host: $HOST"
  if [ "$OSTYPE" = "linux" ]
  then
    ping -q -W 3 -c 1 $ADR 1>&2
  elif [ "$OSTYPE" = "hpux11.00" ]
  then
    ping $ADR -n 1 -m 3 1>&2
  else
    echo "Unknown OS"
    exit 99
  fi
  if [ $? -eq 0 ]
  then
    echo "$ADR ($HOST) is up"
  else
    echo "$ADR ($HOST) FAILED"
  fi
done
IFS=

Es arbeitet zzt. unter Linux (SuSE 11.1) und HP-UX 11i und kann auch hier heruntergeladen werden:
ping_list

munin auf centOS 5.2 installieren

Ich war kürzlich in der Verlegenheit, ein System mit centOS 5.2 überwachen zu müssen. Ich habe mir als Tool dazu munin auserkoren, das sich an Hand der Anleitung „Überwachung eines CentOS 5.2 Servers mit munin und monit“ einfach installieren und in Betrieb nehmen lässt (auf monit habe ich verzichtet).

Allerdings bekam ich danach alle fünf Minuten eine E-Mail mit der Meldung:

Pango-WARNING **: Invalid UTF-8 string passed to pango_layout_set_text()

Zunächst habe ich alle relevanten Dateien in /etc/munin mittels iconv von ISO nach UTF8 konvertiert – was nichts genützt hat, da die Dateien bereits in UTF8 sind bzw. keine Umlaute etc. enthalten.

Eine weitere Recheche im Netz der Netze brachte mich auf ein Post im Gentoo-Forum, in dem ein Patch auf die Datei munin-graph erwähnt wird. Dieser Patch ist leider für die Munin-Version 1.3.4, auf meinem System ist aber die Stable-Version 1.2.5 installiert. Ich habe mir aber einmal den Patch angesehen und festgestellt, dass er recht einfach ist. Er baut  an sich nur eine Erkennung ein, ob die verwendete RRD eine Version größer oder gleich 1.3. Nur dann werden die an RRD übergebenen Parameter nach UTF8 gewandelt. Netterweise war der Patch im Context-Format erstellt worden, so dass es mir ein Leichtes war, die entsprechenden Code-Stellen auch in der Datei /usr/share/munin/munin-graph der Version 1.2.5 zu finden. Ich habe die beiden Code-Fragmente an den entsprechenden Stellen eingebaut – und es geht!

Hier nun der entsprechend angepasste Patch für die Munin-Version 1.2.5:

*** munin-graph.old     2009-03-25 01:28:10.000000000 +0100
--- munin-graph 2009-10-01 15:42:36.000000000 +0200
***************
*** 29,34 ****
--- 29,35 ----
  use Digest::MD5;
  use Getopt::Long;
  use Time::HiRes;
+ if ($RRDs::VERSION >= 1.3) { use Encode; }

  my $graph_time= Time::HiRes::time;
  my $DEBUG = 0;
***************
*** 832,837 ****
--- 833,848 ----
                    push @complete, "--end",(int($lastupdate/$resolutions{$time}))*$resolutions{$time};
            }
            print "

rrdtool "graph" "", join (""
	"",@complete), ""
" if $DEBUG;
+
+           # Since version 1.3 rrdtool uses libpango which needs its input
+           # as utf8 string. So we assume that every input is in latin1
+           # and decode it to perl's internal representation and then to utf8.
+           if ( $RRDs::VERSION >= 1.3 ) {
+               @complete = map {
+                   $_ = encode("utf8", (decode("latin1", $_)));
+               } @complete;
+           }
+
            RRDs::graph (@complete);
            if (my $ERROR = RRDs::error) {
                logger ("Unable to graph $filename: $ERROR");

Den Patch gibt es natürlich auch zum Download: munin-1.2.5_utf8.patch

Dateien eines Jahres archivieren

Heute stand ich (mal wieder) vor der Aufgabe, in einem Verzeichnis alle Dateien vom Vorjahr zu archivieren.

Im Artikel „Dateien an Hand des Datums und der Uhrzeit finden“ habe ich gezeigt, wie man mit find Dateien eines bestimmten Zeitraums finden kann. Da ich aber für die Archivierung keine Rekursion benötige, kann ich das Ganze etwas einfacher mit einem Einzeiler machen:

stat -c '%n Date:%y' * | perl -n -e 'print "$1
" if /^(.+)sDate:2008/' \n | zip archiv_2008.zip -@

Ich verwende hier stat, um das Ausgabeformat bestimmen zu können. Da die Dateinamen Leerzeichen enthalten können, kann ich keine Leerzeichen als Trenner zwischen Name und Datum verwenden, daher die Kennzeichnung mittels „Date:“. Perl filtert mir erstens alle Dateien von 2008 heraus und gibt auch nur den Dateinamen aus. Zip liest diese von der Standardeingabe, wenn die Dateiliste als -@ angegeben ist.

Soll Zip die Dateien auch gleich entsorgen, so kann man das über die Option -m erreichen.

Artikel von Beta-Blogger nach WordPress umziehen

So, ich habe es geschafft, alle Artikel des alten Blog-Systems Beta-Blogger zu exportieren und diese in WordPress wieder zu importieren. Da ich keine direkte Übertragungsmöglichkeit gefunden habe, habe ich den Weg über CSV gewählt. Es gibt ein halbwegs brauchbares CSV-Import-Plugin und die Artikel aus dem alten Blog ließen sich mit dem folgenden PHP-Script als CSV exportieren:

<?php
require 'bb_core.php';

/* see http://de.php.net/manual/de/function.htmlentities.php#90111 */
function htmlButTags($str) {
  // Take all the html entities
  $caracteres = get_html_translation_table(HTML_ENTITIES);
  // Find out the "tags" entities
  $remover = get_html_translation_table(HTML_SPECIALCHARS);
  // Spit out the tags entities from the original table
  $caracteres = array_diff($caracteres, $remover);
  // Translate the string....
  $str = strtr($str, $caracteres);
  // And that's it!
  // oo now amps
  $str = preg_replace("/&(?![A-Za-z]{0,4}w{2,3};|#[0-9]{2,3};)/","&amp;" , $str);
  return $str;
}

$entries = $blog->get_items('entry', NULL, 1000) or print $lang->no_entries;

print("wp_title|wp_post_date|wp_category|wp_content|field1|field2|field3
");
foreach ($entries as $entry) {
  // Tags: Leerzeichen in "" ersetzen und " entfernen
  $tags =  preg_replace('/"(w+) (w+)"/', '$1=$2', $entry->tags);
  // Tags durch Kommata trennen
  $tags = str_replace(" ", ", ", $tags);
  // oben ersetzte Leerzeichen zurück wandeln
  $tags = str_replace("=", " ", $tags);
  $title = htmlButTags($entry->title);
  $date = strftime("%Y-%m-%d %T", $entry->date);
  $body = preg_replace('/
/', '&#x000A', htmlButTags($entry->body));
  $body = preg_replace('/src=["']media2/', 'src="/wp/wp-content/uploads/2009/09', $body);
  // Pipes müssen escaped werden
  $body = preg_replace('/|/', '|', $body);
  $body2 = preg_replace('/
/', '
', htmlspecialchars($entry->body));
  $teaser = htmlButTags($entry->topic);
  $category = htmlspecialchars($entry->category);

  print("$title|$date|$category|$body|$tags|$teaser|
");

}

?>

Damit die Leerzeichen, die beim Export mit ‚&#x000A‘ escaped wurden, beim Import entsprechend wieder hergestellt werden, musste ich das CSV-Import-Plugin noch geringfügig anpassen und eine Zeile einfügen:

$post_content = str_replace('&#x000A', "
", $post_content);

(Das komplette, angepasste Plugin gibt es hier.)

Leider kann das Import-Plugin keine Tags importieren. Ich habe diese daher im ersten Benutzerfeld abgelegt, so dass sie dann beim Editieren des Artikels per Cut & Paste in das Tag-Feld übertragen werden können. Das ist sicherlich nicht der Hit und bedeutet, dass man jeden Artikel anfassen muss. Aber bei 34 Artikeln ist das noch machbar.

Nur einen bestimmten Zeitabschnitt einer Logdateien ausgeben

In einer Logdatei lässt sich ja prima mit grep nach einer Zeichenkette suchen. Was aber, wenn man nur einen bestimmten Zeitraum der Logdatei durchsuchen möchte?

Nun, da hilft uns mal wieder das Universalwerkzeug Perl mit seinem sog. Bereichsoperator (..). Wird dieser nämlich im skalaren Kontext verwendet, liefert er uns solange falsch, solange der linke Ausdruck falsch ist. Sobald der linke Ausdruck (einmal) wahr wird, liefert der Bereichsoperator solange wahr zurück, bis der rechte Ausdruck wahr wird. Er ist also so etwas wie ein bistabiler Schalter mit einer Einschalt- und einer Ausschaltbedingung (der Elektroniker nennt so etwas ein Flip-Flop). Genug der Theorie, hier die praktische Anwendung für unseren Fall:

perl -n -e 'print $_ if /^Aug 18/ .. /^Aug 31/' /var/log/messages

Dies liefert uns alle Einträge vom 18. August bis zum 31. August (oder bis zum Ende der Datei, wenn heute erst der 28. August ist).

Mit einem nachgeschalteten grep kann man nun prima in diesem Abschnitt suchen. Hier ein Beispiel, welches alle anfragende Rechner für den o.g. Zeitraum aus der Logdatei des DNS-Servers ermittelt:

perl -n -e 'print $_ if /^Aug 18/ .. /^Aug 31/' named.queries | sed -n 's/^.* client ([^#]*)#.*$/1/p' | sort | uniq

Umstellung auf WordPress

Nachdem die Seite nun endlich auf eine eigene Domain umgezogen ist, werde ich das Blog nun auch nach WordPress überführen.

Das kann aber noch was dauern, weil ich nicht weiß, wie ich die Artikel am Besten aus dem alten Blog raus und ins neue rein bekommen. Zur Not von Hand — es sind ja nur ca. 30 Artikel.

Script-Fehler beim IE6 finden

Meldet der IE6 einen Javascript-Fehler, der sich im Firefox nicht nachvollziehen lässt, so wird es schwierig. Der IE6 meldet zwar, er hätte in der Datei xy.php in Zeile x bei Zeichen y einen Fehler gefunden, aber diese Angaben sind irreführend. Oder doch nicht? Wie man dem Fehler auf die Spur kommen kann zeige ich hier.

Beim Aufrufen einer Seite wird zum Beispiel die folgende Fehlermeldung angezeigt:

Screenshot der 1. Fehlermeldung

Im Prinzip ist die Angabe von Zeile und Spalte gar nicht so verkehrt, nur die Datei-Angabe stimmt meist überhaupt nicht, da eine HTML-Seite zum einen oft aus verschiedenen Teilen per PHP zusammengesetzt wird und zum anderen der Javascript-Code häufig in eigene Dateien ausgelagert ist.

Um nun herauszubekommen, in welcher Datei der Teufel im Detail steckt, muss man den Debug-Modus aktivieren und die Anzeige der Script-Fehler ausschalten. Dies geschieht in Extras » Internetoptionen » Erweitert. Dort werden, wie unten gezeigt, die Häkchen bei »Skriptdebugging deaktivieren« und bei »Skriptfehler anzeigen« entfernt. Danach muss der IE neu gestartet werden, damit die Änderungen wirksam werden.

Screenshot der Internetopionen des IE6

Wird nun die betreffende Seite erneut aufgerufen, sieht die Fehlermeldung etwas anders aus:

Screenshot der 2. Fehlermeldung

Hier ist nun auch erstaunlicher Weise von Zeile 14 die Rede! Klick man nun auf »Ja«, wird der IE-Debugger gestartet und die Datei geladen und angezeigt, in der der Internet-Explorer den Fehler gefunden hat. Der Cursor wird allerdings an die Position gesetzt, die in der ersten Fehlermeldung angezeigt wurde: Zeile 15, Spalte 22.

Ausschnitt der Debug-Anzeige

Wenn wir uns aber an die Angabe aus der zweiten Fehlermeldung (Zeile 14) erinnern und unser Augenmerk auf die Zeile über dem Cursor richten, sehen wir dort in Spalte 22 einen zweiten Punkt, der dort nicht hingehört. Dies ist der Fehler!

Das Verfahren ist etwas umständlich, aber immerhin zielführend. Und wenn man sich erst einmal daran gewöhnt bzw. damit abgefunden hat, ist es recht nützlich.