Forkbombs – oder wie man sich selbst in den Fuß schießt

Mai 25, 2014 um 1:09 nachmittags | Veröffentlicht in Bash, Programmieren, Ubuntuusers | 1 Kommentar

Als eine Forkbomb bezeichnet man ein außer Kontrolle geratenes Programm oder Skript. Das fragliche Programm “forkt” sich ununterbrochen, erzeugt also immer neue Prozesse. Dadurch erhalten wichtige Prozesse weniger CPU-Zeit und wichtige Daten werden nach und nach aus dem RAM in den Swap übertragen. Mit anderen Worten: das System wird in die Knie gezwungen. Als Form einer DDoS-Attacke kann eine Forkbomb zum Angriff auf ein System verwendet werden. Manchmal passiert es einem aber auch, dass man sich unabsichtlich eine Konstruktion baut, welche wie eine Forkbomb agiert.

Vor etwa 18 Monaten schrieb ich hier einen Artikel über “Alternativen zu youtube-dl“, in welchem ich terminalbasierte Möglichkeiten untersucht habe Videos von YouTube für die offline-Aufbewahrung herunterzuladen. Damals wollte ich mich nicht näher mit dem Problem beschäftigen und habe mir ein einfaches, kleines Wrapper-Skript für cclive geschrieben. Vor kurzem sprach ich das Problem gegenüber einem Kollegen an und wir entschlossen uns dem Problem auf den Grund zu gehen. Der Erfolg ließ nicht lange auf sich warten, nach etwa einer halben Stunde war das Mysterium gelöst. Ich hatte, aus Bequemlichkeit, ein Skript geschrieben welches youtube-dl mit einer Option aufrief, die ich auch sonst immer an das Programm anhing. Da ich zu dieser Zeit gerade meine aliases aufgeräumt hatte, schrieb ich dafür ein kleines Wrapper-Skript mit folgendem Inhalt:

#!/bin/bash

youtube-dl -t

Das Skript benannte ich dann, wieder aus Bequemlichkeit, youtube-dl. Das Skript legte ich ab unter $HOME/bin, welches in meiner PATH-Variable eingetragen war. Voila, Si Forkbömb ist fertisch.</fakeFrenchAccent>

Für alle, die den Ablauf noch nicht nachvollziehen können: Wenn das Programm mit youtube-dl aufgerufen wurde, wurde in Wahrheit das Skript unter $HOME/bin aufgerufen. Dieses sollte dann das eigentliche Programm aufrufen, rief jedoch einfach wieder das Skript unter $HOME/bin auf …

Das ist natürlich ein peinlicher Fehler, der eigentlich einfach zu finden gewesen sein sollte. Er fällt allerdings unter die Kategorie “How did this ever work?“, denn ich hatte das Skript zum Zeitpunkt als die Probleme auftraten schon eine Zeit lang in Gebrauch.

Gegenmittel

Man sieht wie leicht es ist sich selbst in eine Forkbomb zu bauen. Was ist aber nun, wenn man sich mit einer Forkbomb, ob absichtlich oder nicht, konfrontiert sieht? Die englische Wikipedia bietet dazu ein paar Vorschläge. Der meiner Ansicht nach eleganteste ist ein zsh-Einzeiler:

while (sleep 100 &) do; done

Dieser Code erzeugt immer neue Instanzen des Programmes sleep. Das ‘&‘ sorgt dafür, dass der Aufruf in einen neuen Prozess ausgelagert wird. Dadurch können beliebig viele Prozesse gestartet werden, welche sich alle nach jeweils 100 Sekunden wieder beenden. Dadurch hat man eine “kontrollierte” Forkbomb geschaffen, welche dem Schadprogramm langsam das Wasser abgräbt, bis es sich nicht mehr forken kann. Nach knapp 2 Minuten sollte man wieder die Kontrolle über das System erhalten.

Um schnell auf eine Forkbomb reagieren zu können sollte der obige Code als Skript vorliegen um schnell aufrufbar zu sein. Dazu schreibt man den Code in eine Textdatei:

#!/bin/zsh
while (sleep 100 &) do; done

und benennt die Datei möglichst eindeutig wie kurz. Ich habe sie einfach ff genannt (ForkFighter!). Diese Datei legt man nun in einem Verzeichnis ab, welches man in der PATH-Variable eingetragen hat.

=-=-=-=-=
Powered by Blogilo

Horner – Konvertieren zwischen Zahlensystemen

April 8, 2013 um 7:25 nachmittags | Veröffentlicht in Bash, Free Software/Open Source, Programmieren, Ubuntuusers | 10 Kommentare

“Mama, weißt du wie ich von einem Zahlensystem in ein anderes umrechne?”

“Nein, Schätzchen. Weiß Linux es nicht?”

“Nein, ich kann kein Programm finden, dass das kann.”

“Es gibt kein Programm um zwischen Zahlensystemen zu Konvertieren? Oh, nein …”

*Türe wird aufgestoßen, Horner-man stürmt herein*

“Fürchtet euch nicht, gesetzestreue Linuxer! Eure Klagen wurden erhöhrt!”

“Ooh, Horner-man. Unser Held!”

Autsch. Das tut weh. Ich habe es selbst geschrieben und es schmerzt trotzdem sehr. Abgegriffenes Szenario, unglaubwürdige Darsteller, ein Hauch von Sexismus. Autsch.

Kommen wir zum Thema. Seit Langem wollte ich ein Programm haben, mit dem ich einen Wert in verschiedenen Zahlensystemen darstellen kann. Dazu musste dieses Programm jedoch die Umrechnung zwischen diesen Systemen beherrschen. Mein vorrangiges Ziel war die Umrechnung vom Dezimalsystem ins Binärsystem und umgekehrt. Es gibt sicherlich Programme da draußen, die dafür geschrieben sind oder es zumindest nebenbei beherrschen. Speziell die Konvertierung zwischen Zahlensystemen zur Basis 2, 8, 10 und 16 findet sich häufig in digitalen Taschenrechnern. Speedcrunch ist ein schönes Beispiel dafür. Allerdings wollte ich nicht einfach nur ein Programm, welches mir ein Ergebnis anzeigt, sondern auch die Möglichkeit dieses Programm in einem Script zu verwenden. Es musste also konsolenbasiert arbeiten und Ein- und Ausgabe sollten über die Standardkanäle erfolgen.

Da ich kein entsprechendes Programm gefunden habe, habe ich mir vorgenommen selbst eines zu schreiben. Da ich außerdem schon länger das Horner-Schema testweise implementieren wollte,  entschied ich mich dazu, die Umrechnung damit vorzunehmen. Dazu muss erwähnt werden: ich habe dieses Programm für meine persönlichen Bedürfnisse entwickelt, weswegen es nur mit ganzen Zahlen umgehen kann. Die Berechnung mit Gleitkommazahlen ist um einiges komplizierter und es braucht ein bisschen Hirnschmalz um ein Programm zu entwickeln, das nicht sofort Probleme mit der Rechengenauigkeit bekommt. Vielleicht erweitere ich das Programm irgendwann in diese Richtung. Vorerst kann es nur Integer verwerten. Dafür kann es mit Zahlen verschiedenen Basen arbeiten. Die Untergrenze ist Basis 2 (Binär), die Obergrenze ist 36. Rein theoretisch könnte es natürlich noch mehr Basen verarbeiten, bei 36 gehen mir jedoch die sinnvollen Zahlenrepräsentationen aus (0-9, a-z).

Das Programm, oder mehr die Befehlssammlung, kann positive und negative Zahlen von 0 bis 4294967295 verwerten. Dabei ist jedoch 4294967295 das Minimum an Obergrenze. Auf einem 64-bit System ist die Obergrenze 18446744073709551615 (abhängig vom Compiler; hier: GCC). Installation und Verwendung werden noch beschrieben. Zuerst jedoch eine kleine Exkursion:

Was ist das Horner-Schema?

Das Horner-Schema wurde von William George Horner entwickelt und dient der Polynomberechnung. Da die Umrechnung in ein anderes Zahlensystem als Polynom dargestellt werden kann ist das Horner Schema (oder Horner’s Method im Englischen) eine sehr einfache und schnelle Art der Berechnung. Im Großen und Ganzen funktioniert es so:

Umwandlung in das Dezimalsystem; B ist die Basis, a ist die Ausgangszahl, a1 die erste Stelle derselben, …:
(((a1*B + a2)*B + a3)*B + a4) ...

Bsp:
Wir wollen den Binärwert (Basis = 2) 101010 in das Dezimalsystem umwandeln:
((((1*2+0)*2+1)*2+0)*2+1)*2+0 = 42

Die Umwandlung eines Dezimalwertes in ein anderes System funktioniert analog dazu. Zuerst wird per Modulo (Restwertdivision) der Restwert errechnet. Dieser stellt bereits eine Stelle des Ergebnisses dar. Da dieser Rechenvorgang genau umgekehrt zur Umwandlung ins Dezimalsystem verläuft, ist auch das Ergebnis umgekehrt:
42   % 2 = 0
42-0 / 2 = 21
21   % 2 = 1
21-1 / 2 = 10
10   % 2 = 0
10-0 / 2 = 5
 5   % 2 = 1
 5-1 / 2 = 2
 2   % 2 = 0
 2-0 / 2 = 1
 1   % 2 = 1
 1-1 / 2 = 0

Sobald die Zahl, mit der man rechnet 0 geworden ist, endet die Rechnung. Die fettgedruckten Zahlen ergeben, von unten nach oben gelesen, das Ergebnis. Die Zeichenkette ist also 101010. Das ist dieselbe Zeichenkette, die wir zuvor ins Dezimalsystem umgewandelt haben. Es funktioniert!

Die Programme

Ich habe für jeden Rechenvorgang ein C++-Programm geschrieben. 2dec wandelt eine Zahl in das Dezimalsystem um, dec2 wandelt eine Dezimalzahl in eine beliebige Basis (von 2 bis 36) um. Die beiden Programme können jedes für sich aufgerufen werden mit:
dec2 <Basis> <Wert>
2dec <Basis> <Wert>

Die <Basis> ist immer die Basis aus der, bzw. in die ich umrechnen möchte. Die jeweils andere Basis ist ja das Dezimalsystem.

Möchte ich nun den Wert 42 in das Binärsystem umrechnen, so rufe ich das Programm dec2 auf:
dec2 2 42

Möchte ich die Berechnung umdrehen, so verwende ich das Programm 2dec:
2dec 2 101010

Die beiden Programme lassen sich kombinieren, wenn ich von einer Basis in eine andere konvertieren möchte und keine von beiden die Basis 10 ist. Möchte ich wissen, welchen Binärwert der Hexadezimale Ausdruck affe hat, dann mache ich das so:
dec2 2 $(2dec 16 affe)

Ergebnis: 1010111111111110

Das Script

Diese Kombination übernimmt das Script horner. Dieses übernimmt per -i die Basis des Inputs und per -o die Basis, in die der Wert umgewandelt werden soll (Output):
horner -i 16 -o 2 affe
entspricht der obigen Kombination der beiden Programme. Das Script geht davon aus, dass die beiden Programme in einem Verzeichnis abgelegt sind, das in der $PATH-Variablen des Nutzers eingetragen ist.

Das Makefile

Am Besten ist es, wenn man den Code selbst kompiliert. Für all diejenigen, die sich dabei unwohl fühlen oder es einfach noch nicht gemacht haben: keine Sorge, das Makefile übernimmt diese Arbeit. Dazu muss das Paket automake im System installiert sein.

Installation

Zuerst benötigt man den Code, den es hier zum herunterladen gibt.

Dieses Archiv lässt sich per
tar -xvzf Horner.tar.gz
ins aktuelle Verzeichnis entpacken. Die entpackten Dateien liegen dann im selben Verzeichnis wie das Archiv.

Sobald man die Dateien entpackt hat muss man nur in einem Terminal
make
aufrufen. Dies kompiliert den Code und erzeugt die beiden Programme 2dec und dec2.

Mittels
make install
kann man die Programme dann automatisch installieren. Dabei werden sie im Verzeichnis /bin im Home-Verzeichnis des Nutzers abgelegt. Dieses Verzeichnis sollte immer im $PATH liegen und der Nutzer hat dort immer Schreibrechte. Möchtest du die Installation anpassen (sprich: die Dateien woanders unterbringen), dann musst du nur die Dateien 2dec, dec2 und horner im gewünschten Verzeichnis ablegen.

Abschlussbemerkung

Die Programme, das Script und das Makefile stehen unter der GPLv3. Sie sind in keinster Art und Weise getestet und implementieren so gut wie keine Fehlerbehandlung. Über Bugmeldungen freue ich mich, kann aber nicht versprechen, dass ich sie schnell behebe. Wie erwähnt: die Programme sind aus Neugier entstanden und erfüllen meine Anforderungen. Deswegen habe ich vorerst auch nicht vor, sie auf GitHub oder sonstwo hochzuladen. Wenn du das gerne tun möchtest, tu dir keinen Zwang an. Informiere mich in diesem Fall aber bitte darüber.

Über den Inhalt der Programme und des Scripts verliere ich hier keine großen Worte. Wenn du Interesse hast zu erfahren was hier genau passiert, dann schreib mir das in den Kommentaren. Ich antworte dir dann entweder dort oder schreibe einen neuen Artikel zum Thema.

=-=-=-=-=
Powered by Blogilo

Playr – Ein iPod Shuffle für die Bash

Januar 21, 2013 um 10:05 vormittags | Veröffentlicht in Bash, Multimedia, Programmieren, Ubuntuusers | 9 Kommentare

Schon länger habe ich nach einer Möglichkeit gesucht ein paar Musikdateien auszuwählen und in zufälliger Reihenfolge abspielen zu lassen. Das kann sowohl ein einzelnes Album sein, als auch eine Zusammenstellung mehrere Tracks aus verschiedenen Verzeichnissen. Außerdem sollte dieses Programm Konsolen-basiert sein, da ich es für schnelle Wiedergabe brauche und nicht zuerst in einer GUI alles zusammenklicken will. Da ich ein solches Programm leider noch nicht gefunden habe, habe ich mir selbst ein Bash-Skript zusammengestellt, welches als Scheduler dient und play (Paket sox aus den Quellen) zum Abspielen verwendet. play kann sowohl OGG, FLAC und WAV abspielen. Nach Installation von libsox-fmt-mp3 auch MP3.

Playr

Playr steht für die Zusammensetzung aus play (dem Programm fürs Abspielen der einzelnen Tracks) und random (engl.: zufällig). Dass es aussieht wie ein Web2.0-Name ist wiederum Zufall (dt.: coincidence).

Die grobe Funktionsweise: Die übergebenen Musiktracks werden in eine versteckte Textdatei geschrieben. Dann wird mit Hilfe von $RANDOM eine Zufallszahl zur Berechnung des nächsten Tracks verwendet. Dieser Track wird sodann abgespielt und aus der Textdatei entfernt. Das verhindert, dass dieser erneut abgespielt wird (was etwas ist, dass ich bei portablen Audio-Playern sehr nervig finde). Solange noch Tracks in der Liste sind wird fortgefahren.

Soll die Wiedergabe abgebrochen werden, muss der Nutzer nur [strg]+[c] drücken. Leider habe ich bis jetzt keine Möglichkeit gefunden zusätzlich dazu auch einfach zum nächsten Track zu wechseln. Man kann natürlich einfach die trap entfernen, dann bricht [strg]+[c] nur das aktuelle play ab. Dann kann jedoch das Skript selbst nicht mehr komfortabel beendet werden.

Das Skript

Wir beginnen mit der trap. Dieses Konstrukt sorgt dafür, dass unser Skript [strg]+[c] verarbeiten kann. Dafür schreiben wir folgende Zeile:
trap quit SIGINT SIGTERM

Diese Zeile führt dazu, dass beim Erhalt der Signale SIGINT (Signal 2) oder SIGTERM (Signal 15) quit ausgeführt wird. Nun ist quit kein Bash-Befehl sondern eine Funktion, die wir uns selbst schreiben, welche das Skript geordnet beendet:
function quit {
    rm -f $TRACK_FILE $TEMP_FILE
    exit 1
}

Diese Funktion schreiben wir vor die Trap. In dieser löschen wir einfach unsere Textdateien, damit sie beim nächsten Programmaufruf nicht stören.

Als nächstes definieren wir zwei Variablen für unsere versteckten Textdateien. Zum einen eine Datei um die Tracklist zu speichern, zum Anderen eine Datei in der die veränderte Liste gespeichert wird:
TRACK_FILE=/tmp/.tracklist.playr
TEMP_FILE=/tmp/.temp.playr

Wir speichern die Dateien in  /tmp , damit sie keine Verzeichnisse zumüllen. Außerdem sollten wir in  /tmp  immer Schreibzugriff haben. Sollte  /tmp  nicht verfügbar sein, kann man das Verzeichnis ja auf  $HOME  ändern.

Nun wirds Zeit sicherheitshalber Überbleibsel einer vorherigen Ausführung zu entfernen:
rm -f $TRACK_FILE $TEMP_FILE

Nur für den Fall, dass etwas total schief gelaufen ist.

Um die Tracklist zu erzeugen schreiben wir nun einfach alle Parameter, die beim Skriptaufruf übergeben wurde in das TRACK_FILE:
for file in " $@ "; do
    echo " $file "
done > $TRACK_FILE

Das funktioniert gewisserweise wie eine foreach-Schleife. Eine beliebige Anzahl Parameter wird durchlaufen ( $@ ). Der jeweils aktuelle Parameter wird in  $file  gespeichert und in der Schleife verarbeitet. Hier wird er einfach an die Standardausgabe geschickt. Die komplette Ausgabe der Schleife wird wieder auf  $TRACK_FILE  umgebogen, wodurch die Ausgabe nicht auf der Kommandozeile erscheint, sondern in der angegebenen Datei.

Die Größe der Tracklist holen wir uns mit wc:
size=$( cat "$TRACK_FILE" | wc -l)

Natürlich könnte man die Datei auch gleich als Parameter für wc angeben:
wc -l " $TRACK_FILE "

Dann jedoch erhält man eine Ausgabe nach folgendem Muster:
6 .tracklist.playr

Man muss also erst noch den eigentlichen Wert extrahieren. Da mache ich lieber den “Umweg” über cat. Ist kürzer.

Nun kommt die eigentliche Hauptschleife des Programmes:
while [[ $size -gt 0 ]]; do
    ## Code
    ((size–))
done

Diese Schleife läuft solange durch, bis  $size  den Wert 0 hat. Da  $size  auf jeden Fall größer als 0 sein muss und in jedem Durchgang um eins verringert wird, ist das irgendwann der Fall. Mit der Abbruchbedingung verhindern wir auch, dass die Schleife ausgeführt wird, wenn die Tracklist leer ist.

Es ist Zeit. Zeit um die Urväter, die Auditoren, die Götter anzurufen. Zeit eine Zufallszahl zu erzeugen. Einer Zufallszahl innerhalb eines Intervalls errechnet man am Besten mithilfe von Modulo. Modulo ist der Name der in der Unterstufe als Restwertdivision bekannten Berechnung.

Kleine Beispiele:  10 / 3 = 3 . Rest:  1 13 / 5 = 2 . Rest:  3 . Uns interessiert immer der Restwert. Dieser ist auf jeden Fall immer  um  1  kleiner als der Divisor (für diejenigen, die schon lange aus der Schule draußen sind :) ).

Bash scheint keine built-in Funktion für Modulo zu haben, dafür gibt es ein paar andere Möglichkeiten eine solche Berechnung durchzuführen. Am ansprechendsten habe ich  let  gefunden:
let "rand = $RANDOM % $size + 1"

In der Variable  $rand  wird das Ergebnis der Berechnung festgehalten.  $size  ist natürlich der Begrenzer.  $RANDOM  ist eine Umgebungsvariable, welche bei jedem Aufruf einen neuen zufälligen Integer (Ganzzahl) ausgibt. Da diese Berechnung bei einer Tracklist-Größe von z.B.  6  den Wertebereich von  0-5  abdeckt, wir aber kein  0 tes Lied, dafür aber ein  6 tes, zählen wir zum Ergebnis einfach  1  dazu.

Den damit errechneten Track erhalten so:
track= " $( head  $TRACK_FILE  -n$rand | tail -n1)

Hier passiert ein bisschen was:
head  -n$rand

filtert die ersten  $rand  Zeilen aus der Liste. Ist  $rand also  2 , dann erhalten wir durch  head   die ersten  2  Zeilen. Dadurch ist der gesuchte Track immer an letzter Stelle in der Liste. Diese letzte Stelle können wir uns nun per  tail  holen:
tail  -n1

Hier holt uns  tail  die  1 ste Zeile von hinten. Damit haben wird unseren Track. Dieser wird nun in die Variable  $track  gespeichert.

Jetzt wird es Zeit den Track abzuspielen:
play " $track "

Natürlich kann man auch das Ausrechnen des Tracks und den Aufruf von  play  in einer Zeile unterbringen. Ich bin jedoch eher für lesbaren Code als für “Zeileneffizienz”.

Zum Abschluss muss noch eine neue Tracklist angelegt werden, ohne den gerade abgespielten Track. Dazu holen wir uns zuerst alle Tracks vor dem Aktuellen:
head " $TRACK_FILE " -n$[ $rand-1 ] > " $TEMP_FILE "

und die Tracks danach:
tail " $TRACK_FILE " -n$[ $size-$rand ] >> " $TEMP_FILE "

Mit
$rand-1

holen wir uns alle Tracks vor dem Aktuellen und sparen diesen aus. Hingegen
$size-$rand

liefert uns alle Tracks nach dem Aktuellen. Damit erhalten wir wieder dieselbe Liste wie zuvor, nur ohne dem letzten Track. Man beachte die Pfeile, welche das Ergebnis jeweils nach $TEMP_FILE schicken. In der ersten Zeile befindet sich nur ein Pfeil. Das bedeutet, dass die Datei überschrieben und neu befüllt wird. Es wird also nur das gespeichert, was in dieser Zeile herauskommt. In der zweiten Zeile hingegen sind zwei Pfeile. Das bedeutet, das das Ergebnis an die Datei angehängt wird. Der Inhalt der Datei wird also nicht über-, sondern der neue Inhalt dahinter in die Datei geschrieben.

Schließlich müssen wir die neue Tracklist noch über die alte schreiben:
mv " $TEMP_FILE " " $TRACK_FILE "

Nach der Schleife selbst rufen wir noch einmal quit auf, damit das Skript sich auch ohne Abbruch durch den Nutzer sauber beendet.

Das vollständige Skript

#!/bin/bash

function quit {
     rm  -f $TRACK_FILE $TEMP_FILE
     exit  1
}

trap  quit SIGINT SIGTERM

TRACK_FILE=/tmp/.tracklist.playr
TEMP_FILE=/tmp/.temp.playr

rm  -f $TRACK_FILE $TEMP_FILE

for  file  in  $@ do
     echo   $file
done > $TRACK_FILE

size=$( cat  “$TRACK_FILE” |  wc  -l)

while [[  $size -gt 0 ]]; do
     let  “rand =  $RANDOM  %  $size  + 1″
    
track= " $( head  $TRACK_FILE  -n$rand  tail  -n1)
     play  $track
     head   $TRACK_FILE  -n$[ $rand-1 ] >  $TEMP_FILE
     tail   $TRACK_FILE  -n$[ $size-$rand ] >>  $TEMP_FILE
     mv   $TEMP_FILE ” “ $TRACK_FILE
    ((size–))
done

quit

Zum Schluss

Ich bin vergleichsweise ein Anfänger in Shell-Script. Ich habe einige Erfahrung in C++ und es kann daher sein, dass ich versuche Konzepte daraus in Bash umzusetzen, obwohl es wesentlich einfachere Lösungen gibt. Wenn du eine bessere Lösung weißt, schreibe sie doch bitte in die Kommentare.

Lizenz

Das Skript steht unter der GPLv3.

Alternativen zu youtube-dl

Dezember 13, 2012 um 9:28 nachmittags | Veröffentlicht in Bash, Free Software/Open Source, Multimedia, Programmieren, Ubuntuusers | 15 Kommentare

Ich mag youtube-dl. Es ist einfach zu benutzen, ist plattformunabhängig (Python) und braucht nicht viele Ressourcen (Terminal-Anwendung).

Leider mag youtube-dl mich zur Zeit nicht. Sobald ich versuche ein Video herunterzuladen schaufelt die Anwendung meinen Arbeitsspeicher voll, sodass alles andere in den Swap ausweichen muss. Das führt dazu, dass das System innerhalb von Sekunden nicht mehr reagiert. Am längsten überlebt noch die Maus, welche sich etwa eine halbe Minute lang stark ruckelnd bewegen kann. Danach ist auch sie tot und das Einzige das hilft ist ein Hard-Reset. Keine wünschenswerte Situation.

Ich habe bereits
youtube-dl -U
als sudo ausgeführt um ein update des Scripts zu erzwingen. Das hat leider nichts geholfen. Der Streaming-Dienst ist auch egal. Ob YouTube, Vimeo oder sonst was, das Problem ist immer gleich.

Logischer Schritt: ich schaue mich nach Alternativen um. Wenn man per
apt-cache search
nach youtube sucht, werden einem bereits eine Liste an Programmen angeboten, welche in der Lage sind Videos von Streaming-Services herunterzuladen. Darunter befinden sich

  • clive
  • cclive
  • fatrat
  • slimrat
  • nicovideo-dl
  • get-flash-videos
  • metacafe-dl
  • nomnom

clive

Speziell clive hat meine Aufmerksamkeit erregt. Es ist ein Terminal-Tool, welches sich für Streaming-Services eignet, die ihre Videos in einem Flash-Player abspielen. Es ist leichtgewichtig, hat eine Vielzahl an praktischen Optionen und verhält sich allgemein sehr wie ich es von youtube-dl gewohnt bin.

Ein einfaches
clive <video_url>
lädt das Video an der angegebenen URL herunter. An sich kann man damit leben. Allerdings hat clive eben mehrere interessante Funktionen, mit denen man sich gerne spielt. Dazu gehört einmal der Schalter -F. Damit erhält man eine Liste von verfügbaren Formaten und Qualitätsstufen in denen das Video vorhanden ist. Diese Liste kommt im Format
hd|mobile|sd
Die durch “|” getrennten Optionen kann man dann in einem zweiten Aufruf dem Schalter -f angeben, welcher dann dafür sorgt, dass clive das Video in der gewünschten Qualität herunterlädt.

Da es etwas unbequem ist für jedes Video zwei Programm-Aufrufe zu starten, habe ich ein einfaches Skript geschrieben, welches diese Aufgabe abnimmt:

#!/bin/bash

VIDEO_URL="$1"
TEMP_FILE=".video-dl_temp_file"

clive -F "$VIDEO_URL" > "$TEMP_FILE"

CHOICES=$(cat "$TEMP_FILE" | tail -n1)

echo -n "Choose format (${CHOICES%% *}|best): "
read FORMAT

clive "$VIDEO_URL" --format="$FORMAT"
--filename-format="%t_$FORMAT.%s"

rm -f "$TEMP_FILE"

Was passiert hier?

VIDEO_URL=”$1
Wir speichern die angegebene URL in einer Variable.

TEMP_FILE=”.video-dl_temp_file
Der Einfachheit halber brauchen wir eine temporäre Textdatei, in welcher wir Informationen speichern können. Der Punkt am Anfang des Dateinamens bedeutet, dass die Datei versteckt ist. Sie wird also im Dateibrowser, oder per ls nicht angezeigt. Die Datei wird am Ende des Skripts wieder gelöscht.

clive -F “$VIDEO_URL” > “$TEMP_FILE
Hier holen wir uns die möglichen Formate, in denen das Video vorliegt und schreiben diese Ausgabe in die temporäre Datei. Da es sich hierbei um zwei Zeilen handelt und nur die zweite Zeile relevant ist, brauche ich:

CHOICES=$(cat “$TEMP_FILE” | tail -n1)
Diese Zeile gibt mir den Inhalt der temporären Datei aus (cat), schneidet die letzte Zeile ab (tail) und speichert die gewünschte Zeile in der Variablen CHOICES.

Als Nächstes fragen wir den Nutzer, welches Format bevorzug wird:
echo -n “Choose format (${CHOICES%% *}|best): 
wobei
${CHOICES%% *}
dafür sorgt, dass hinter der Liste nichts mehr steht. clive gibt hier nochmal die Adresse des Videos aus. Dieser Ausdruck schneidet sie weg.

Dann lesen wir die Wahl des Nutzers ein:
read FORMAT

Nun ist es an der Zeit endlich das Video herunterzuladen:
clive “$VIDEO_URL” –format=”$FORMAT
–filename-format=”%t_$FORMAT.%s

–filename-format gibt die Formatierung des Dateinamens an. %t und %s sind dabei Platzhalter, welche für den Titel und das Suffix der Datei stehen. Zusätzlich zu den Platzhaltern kann man noch beliebig weitere Vorgaben für den Dateinamen machen. Ich füge zwischen Dateinamen und Suffix noch das Format der Datei ein.

Nachdem die Video-Datei sicher auf der Platte liegt lösche ich noch die temporäre Datei:
rm -f “$TEMP_FILE

Fazit

Bis jetzt habe ich clive mit YouTube und Vimeo getestet. Beides funktionierte sehr gut und machte keine Probleme. Das Skript ist natürlich ausbaufähig. Zur Zeit kann es immer nur mit einem Video gleichzeitig umgehen und möglicherweise findet man noch einen besseren Weg als die Informationen in einer temporären Datei zu speichern. Für meine aktuellen Zwecke reichts.

clive steht unter der GPLv3.

=-=-=-=-=
Powered by Blogilo

Bash – Bedingte Ausführung mit if und die while-Schleife

Juli 4, 2012 um 8:12 nachmittags | Veröffentlicht in Bash, GNU/Linux, Programmieren, Ubuntuusers | 8 Kommentare

Wie jede vernünftige Programmiersprache hat auch die Bash eine Möglichkeit Code nur dann abzuarbeiten wenn bestimmte Bedingungen eintreffen. Für alle, die mit Programmierung nicht viel zu tun haben: if wird in den meisten Programmiersprachen dazu verwendet das Programm bestimmte Dinge machen zu lassen, abhängig vom Zustand. Grundsätzlich gibt es für die Auswertung eines Audrucks in wahr und falsch das Kommando test. Allerdings ist if normalerweise ein klein wenig übersichtlicher zu verwenden. Ein Beispiel in C:

if( is_closed(WienerWald) ) {
    gehe_zu(McDonalds);
}
else {
    gehe_zu(WienerWald);
}

Dieses Codestück versucht zu ermitteln ob ein bestimmtes Restaurant geschlossen ist. Wenn es geschlossen ist, wird ein anderes Restaurant besucht. Wenn es nicht geschlossen ist, dann wird das fragliche Restaurant besucht. Die Funktion is_closed() liefert uns entweder wahr oder falsch zurück. Dies sind die einzigen Werte die das if selbst auswerten kann. Auf dieser Grundlage wird dann entschieden welcher Code ausgeführt wird. Wenn der Ausdruck wahr zurückliefert, dann wird der Code gleich nach dem if ausgeführt. Wenn der Ausdruck falsch zurückliefert, dann wird der Code nach dem else ausgeführt. Ist der Ausdruck falsch und es gibt jedoch kein else, wird das ganze if einfach übersprungen. So funktioniert das if in allen (mir bekannten) Programmiersprachen. Aus welchen Datenbeständen das Programm diese Information abfragt ist hier uninteressant. Es soll nur die bedingte Ausführung gezeigt werden.

In Bash ist die Syntax ähnlich:

if [ is_closed $WienerWald ]
then
    gehe_zu $McDonalds
else
    gehe_zu $WienerWald
fi

Wichtig dabei zu beachten ist, dass um die eckigen Klammern Leerzeichen stehen müssen, sonst wird ein Fehler ausgegeben und das if versagt. Ich weiß leider selbst nicht, warum die Bash hier so pickig ist. Bei den Klammern handelt es sich um eine Kurzform des Kommandos test, welches für die eigentliche Auswertung des Ausdrucks zuständig ist.

Vergleich zwischen Zahlen

Zwischen den Klammern kann grundsätzlich alles stehen was irgendwie zu wahr oder falsch abgeleitet werden kann. So können auch Zahlen oder Zeichenketten verglichen werden:

if [ $i -lt 10 ]
then
    echo "0$i"
else
    echo "$i"
fi

Dieses if fragt ab ob die Variable $i kleiner (lt = lesser than) ist als der Wert 10. Ist dies der Fall wird der Zahl eine führende Null vorangestellt, ist dies nicht der Fall, dann nicht. Das kann nützlich sein, wenn man bei formatierten Aufzählungen die Formatierung beibehalten will:

08 - ...             8 - ...
09 - ...    statt    9 - ...
10 - ...             10 - ...

Außer lt gibt es noch folgende Befehle zum Vergleichen von zwei Zahlenwerten:

  • eq = equal            = Gleichheit
  • ne = not equal        = Ungleichheit
  • le = lesser or equal  = kleiner oder gleich
  • ge = greater or equal = größer oder gleich
  • gt = greater than     = größer als

Vergleich zwischen Zeichenketten

Auch Zeichenketten, sogenannte strings, können verglichen werden. Hier gibt es mehrere Möglichkeiten:

if [ $str = "Auto" ]

vergleicht ob die Zeichenkette, welche in der Variable $str gespeichert ist, der Zeichenkette “Auto” entspricht. Wenn dem so ist, dann ist der Ausdruck wahr.

Jetzt wirds ein bisschen kompliziert:

if [ $str != "Auto" ]

vergleicht ebenfalls die Variable $str und die Zeichenkette “Auto“. Dieser Ausdruck ist dann wahr, wenn die beiden Zeichenketten nicht übereinstimmen. Das Rufzeichen agiert hier als Anzeige dafür, dass der gewünschte Vergleich nicht erfolgreich sein soll, damit die Bedingung erfüllt ist.

Als dritte Möglichkeit eine Zeichenkette in einem if zu verwenden kommt die Abfrage ob in der Zeichenkette überhaupt etwas steht:

if [ $str ]

fragt ab ob irgendein Zeichen (egal welches oder wieviele) in der Variable gespeichert sind. Ist mindestens ein Zeichen vorhanden, dann ist der Ausdruck wahr. Ist die Zeichenkette leer (= “”), dann ist der Ausdruck falsch. Zum selben Ergebnis kommt dieser Ausdruck:

if [ -n $str ]

Der Befehl -n prüft ob die Länge der Zeichenkette in der angegebenen Variable nicht Null (non zero) ist. Hingegen

if [ -z $str ]

prüft genau das Gegenteil: -z fragt ob die Länge Null (zero) ist.

UND, ODER und NICHT

Wie jede brauchbare Programmiersprache bietet die Bash bei if auch die Möglichkeit mehrere Ausdrücke hintereinander auszuwerten, ohne jedesmal ein neues if zu benötigen. Der Code

if [ $i -lt 10 ]
then
    if [ $j -lt 10 ]
    then
        ...
    fi
fi

lässt sich kürzer so schreiben:

if [ $i -lt 10 -a $j -lt 10 ]
then
    ...
fi

Das -a (UND) sorgt dafür, dass der Gesamtausdruck nur dann wahr ist, wenn beide Seiten (also links vom -a und rechts davon) jeweils wahr sind. In den meisten Programmiersprachen ist es so, dass zuerst die linke und dann die rechte Seite ausgewertet werden. Sollte die linke Seite bereits falsch sein, wird normalerweise die rechte Seite nicht mehr ausgewertet. Ist im allgemeinen auch nicht notwendig, da der gesamte Ausdruck dann sowieso falsch ist, wenn eine Seite falsch ist. Wahrer als falsch kann es nicht mehr werden. Die Bash hingegen wertet beide Seiten aus. Egal ob der Gesamtausdruck bereits falsch ist oder nicht.

Ähnlich funktioniert das ODER (-o). Hierbei soll geschaut werden ob eine der beiden Seiten wahr ist. Wenn zumindest eine wahr ist, ist der Gesamtausdruck wahr. Auch hier sollte normalerweise der rechte Ausdruck nicht mehr angesehen werden, wenn der Linke bereits wahr ergeben hat. Die Bash machts trotzdem:

if [ $i -lt 10 -o $j -lt 10 ]

Zu guter Letzt im Reich der sogenannten logischen Operatoren gibt es noch das NICHT. Dieses wird als Rufzeichen dargestellt (wie auch schon bei den Vergleichen der Zeichenketten). Seine Aufgabe ist es ein wahr in ein falsch umzukehren und vice versa:

if [ ! $i -lt 10]

Dieser Ausdruck ist dann wahr, wenn $i gerade nicht kleiner ist als 10. Selbstverständlich kann dieses Verhalten auch mit einem anderen Befehl erreicht werden. Im konkreten Fall wäre ein

if [ $i -ge 10 ]

gleichwertig. Das Rufzeichen hat trotzdem seine Existenzberechtigung, weil es manche Ausdrücke wesentlich übersichtlicher werden lässt, da es weniger zu schreiben gibt.

Arbeiten mit Dateien und Ordnern

Es ist mit if in der Bash auch möglich direkt mit Dateien zu interagieren. Ein einfaches

if [ -e datei ]

prüft ob eine Datei mit Namen “datei” existiert. Dabei wird sich immer auch das aktuelle Verzeichnis bezogen. Man kann natürlich auch einen kompletten Pfad angeben, dann ist es egal in welchen Ordner man sich befindet:

if [ -e /Pfad/zur/datei ]

Bisher kann “datei” jedoch sowohl Ordner als auch reguläre Datei sein. Um zwischen diesen Möglichkeiten zu unterscheiden gibt es eigene Befehle:

if [ -f datei ]

prüft ob eine reguläre Datei mit Namen “datei” im aktuellen Verzeichnis liegt. Ist das der Fall, ist das Ergebnis wahr. Es kann natürlich auch ein Ordner mit dem Namen “datei” im aktuellen Verzeichnis liegen. Dann liefert dieser Ausdruck falsch, da ein Ordner mit diesem Namen zwar existiert, aber ein Ordner keine reguläre Datei ist. Hingegen mit

if [ -d ordner ]

wird konkret geprüft ob ein Ordner mit Namen “ordner” an dieser Stelle existiert. Hier gilt dasselbe wie bei Dateien: existiert ein Ordner mit angegebenem Namen, ist der Ausdruck wahr. Existiert kein Ordner mit diesem Namen (egal ob eine Datei mit diesem Namen existiert oder nicht) ist der Ausdruck falsch.

if [ -x executable ]

prüft ob die angegebene Datei ausführbar ist. Das kann ein Programm, ein ausführbares Script oder ein Ordner sein.

Soviel zu den wichtigsten Vergleichen bei der Verwendung von if. Eine vollständige Auflistung gibt es auf der GNU-Infopage zu Bash.

while

Zum Abschluss noch ein Hinweis: das if lässt sich mit drei kleinen Änderungen in eine Schleife verwandeln. Eine Schleife ist ein Gebilde dessen Code mehrmals ausgeführt wird bis eine bestimmte Bedingung eintritt:

while [ $i -lt 10 ]
do
    echo $i
    ((i++))
done

Diese Schleife gibt die Zahlen von n (ein Wert der vorher festgelegt wurde) bis 9 aus. Die '((' und '))' lassen uns ungehindert arithmetische Ausdrücke ausführen. i++ bedeutet, dass der Wert, der in $i gespeichert ist um 1 erhöht wird.

  • Das Wort if muss also durch ein while ersetzt werden
  • then muss durch do ersetzt werden
  • fi muss durch done ersetzt werden

Als Ausdruck in den Klammern ist alles möglich das auch im if möglich ist.

Wichtig: eine while-Schleife verfügt nicht über ein else.

=-=-=-=-=
Powered by Blogilo

Bash – Filename Expansion, for-Schleife

Juli 3, 2012 um 7:52 nachmittags | Veröffentlicht in Bash, GNU/Linux, Programmieren, Ubuntuusers | 14 Kommentare

Die Bash ist ja bekanntlich das Schweizer Taschenmesser beim Umgang mit Unixoiden Systemen. Vor 23 Jahren geboren, heute in der Version 4.2 verfügbar, bleibt sie jedoch immer noch für viele ein Mysterium. Primär für Anfänger ist der Umgang mit ihr umständlich und die große Funktionsvielfalt schier erdrückend. Bis man sich hier durchgekämpft und die steile Lernkurve hinter sich gebracht hat, hat sich bereits eine Haarlänge a’la Rapunzel gebildet. Oder ein Bart nach Stallman. Natürlich zähle ich mich selbst bei weitem nicht zu den Gurus unter den Programmierern. Lerne ich jedoch etwas Praktisches, so versuche ich es leicht verständlich weiter zu geben. Heute geht es um Filename Expansion.

Die GNU-Infopage zu Bash-4.2 lässt sich in Kapitel 3.5.3 etwas umständlich über die genaue Wirkungsweise der Filename Expansion aus. Leider ohne ein Beispiel zu geben. Dieses Versäumnis soll nun nachgeholt werden.

Vorbereitung

Nehmen wir an, wir haben eine Variable $file mit folgendem Inhalt:
/home/user/datei.png

Für bisher-nicht-Bash-Nutzer, so weise ich einer Variable in Bash eine Zeichenkette (ein Pfad zu einer Datei ist eine solche) zu:
file="/home/user/datei.png"
Die Variable $file wird, falls sie noch nicht existiert an dieser Stelle angelegt.

Mit dem Befehl echo kann ich überprüfen, welchen Inhalt die Variable hat:
echo $file
Ausgabe: /home/user/datei.png

Man beachte das Dollar-Zeichen wenn die Variable verwendet wird. Nur mit dem Dollar-Zeichen davor kann auf den Inhalt der Variablen zugegriffen werden.

Der Dateiname

Angenommen ich möchte nun aus unserem Dateipfad den Namen der Datei herauslösen. Hierfür muss ich wohl alles wegschneiden, was vor dem Dateinamen steht:
/home/user/datei.png

Das Konstrukt um dies zu erreichen sieht in Bash so aus:
${file##*/}

Man beachte, dass nach wie vor das Dollar-Zeichen zu Anfang stehen muss. Der Name der Variable ist nun in geschwungenen Klammern eingeschlossen. Die Zeichen dahinter teilen sich in zwei Gruppen auf. Das '##' steht für den Befehl alles von links her wegzuschneiden. Alle weiteren Zeichen, welche darauf folgen, geben an was genau weggeschnitten werden soll.

*/
steht für: alles bis zum Zeichen '/'.

Folgender Befehl:
echo ${file##*/}
ergibt also folgende Ausgabe:
datei.png

Nun kann man argumentieren, dass in der Zeichenkette
/home/user/datei.png
ja viele '/' vorkommen. Das stimmt. Das '##' schneidet eben alles bis zum letzten '/' weg.

Ähnlich funktioniert das Konstrukt
${file#*/}
Man beachte das einfache '#'. Dieser Befehl schneidet alles bis zum ersten '/' weg.

Folgender Befehl:
echo ${file#*/}
ergibt also folgende Ausgabe:
home/user/datei.png
Man beachte das fehlende '/' am Anfang.

Das Suffix

Dieses Spielchen funktioniert natürlich auch von der rechten Seite her. Statt dem '##', bzw. dem '#', verwenden wir hier ein '%%', bzw. ein '%'.

Nehmen wir an, wir haben den kompletten Dateinamen (inklusive Endung) in einer weiteren Variablen gespeichert:
file_name=${file##*/}

Ein
echo $file_name
bringt uns nun folgende Ausgabe:
datei.png

Wir wollen nun nur den eigentlichen Dateinamen herausholen, ohne Endung. Dazu müssen wir die Endung wegschneiden:
datei.png

Das funktioniert so:
${file_name%.*}

Das '%' funktioniert hier als Anweisung die Zeichenkette von rechts her zu beschneiden. Weggeschnitten werden soll '.*', also alles was rechts vom ersten '.' steht.
(Natürlich macht hier die Unterscheidung zwischen erstem und letztem Auftreten eines Zeichens keinen Unterschied, da das Zeichen hier nur einmal vorkommt.)

Anwenden

Manch einer fragt sich sicherlich schon: und wofür brauche ich das? Nun, die Bash ist zwar sehr komplex, wenn man sie jedoch mal ein wenig verstanden hat auch sehr flexibel und effizient. Ich erledige viele Aufgaben, für welche ich mit grafischen Programmen viele Klicks und mehrere Minuten brauche (z.B. Bildbearbeitung mit Gimp), in wenigen Sekunden.

Nehmen wir an, wir wollen von unserem Bild ein Thumbnail für eine Website erzeugen. Das Thumbnail soll etwa ein Fünftel der Größe des Originals haben (also 20%). Hierzu verwenden wir das Programm convert, welches zum Paket imagemagick gehört. Der Befehl sieht dann so aus:
file="datei.png"
convert "$file" -resize 20% "${file%.*}_thumb.png"

Damit wird eine neue Datei erzeugt, welche 20% der Größe der Datei datei.png hat und datei_thumb.png heißt. Hier haben wir die Filename Expansion also verwendet um den Dateinamen anzupassen, den wir der neuen Datei gegeben haben.

Auf mehrere Dateien anwenden

Das ist selbstverständlich noch nicht der Weisheit letzter Schluss. Für eine einzelne Datei zeigt sich die Flexibilität von Filename Expansion noch nicht so wirklich. Erst wenn ich mehrere Dateien, mit stark unterschiedlichen Namen bearbeiten will sieht man das Potential.

Nehmen wir also an, wir haben einen Ordner voller Bilder:
Geburtstag1.png
Valerie_Donau.jpg
Visitenkarte.png
Zweiwohnschloss.gif
...

Von allen diesen Bildern möchten wir, wie im obigen Beispiel, ein Thumbnail erzeugen lassen. Wir brauchen nun alle Techniken, die wir oben gelernt haben, um dieses Vorhaben elegant umzusetzen. Zu aller erst brauchen wir eine Schleife. Eine Schleife ist ein Konstrukt, welches den Code, der in der Schleife steht, sooft ausführt, bis eine bestimmte Bedingung erreicht ist. Unsere Schleife sieht so aus:

for file in ./*; do ; done

Diese Schleife macht noch nichts. Hier will ich nur den grundsätzlichen Aufbau erklären. for ist das Schlüsselwort mit dem die Schleife beginnt. Danach wählen wir einen Namen für eine Variable, die wir in der Schleife verwenden wollen (hier: file). in ist wieder ein Schlüsselwort, welches wir bei der Schleife brauchen. Danach steht was in die vorher gewählte Variable eingetragen werden soll. ./* steht für “alle Dateien in diesem Ordner”. Wir gehen davon aus, dass in diesem Ordner nur Bild-Dateien enthalten sind.

Da wir unseren Code nicht in ein Shell-file schreiben, sondern direkt auf die Kommandozeile sind die Semikolons zwingend. Nach jedem Befehl muss ein Semikolon folgen.

do leitet den Code der Schleife ein. Der erste Befehl der Schleife folgt gleich auf das do, ohne Semikolon. Mit done wird die Schleife abgeschlossen. do und done sind keine Befehle, deshalb muss auf sie nicht direkt ein Semikolon folgen.

Die Bedingung dieser Schleife ist: gehe alle Dateien in diesem Ordner durch. Für jede Datei in diesem Ordner wird die Schleife also genau einmal ausgeführt. Der jeweilige Name der Datei findet sich in der Variable $file.

Nun schreiben wir den Code unserer Schleife. Zuerst müssen wir den eigentlichen Namen (das Prefix) der jeweiligen Datei finden:

for file in ./*; do prefix="${file%.*}"; done

Danach brauchen wir noch das Suffix (Endung; denn in unserem Ordner liegen ja Bild-Dateien von verschiedenen Typen herum herum):

for file in ./*; do prefix="${file%.*}"; suffix="${file##*.}"; done

Nun können wir den Konvertierungsbefehl aus dem obigen Beispiel einfügen (mit einer kleinen Änderung):

for file in ./*; do prefix="${file%.*}"; suffix="${file##*.}"; convert "$file" -resize 20% "$prefix"_"thumb.$suffix"; done

Für unsere gegebenen Dateien sieht das Ergebnis so aus:

Geburtstag1.png
Geburtstag1_thumb.png
Valerie_Donau.jpg
Valerie_Donau_thumb.jpg
Visitenkarte.png
Visitenkarte_thumb.png
Zweiwohnschloss.gif
Zweiwohnschloss_thumb.gif
...

Diese Schleife hat bei mir für 70 Bilder zu jeweils ca. 5 MB etwa 1:30 min gebraucht. Schaffst du das mit dem Gimp auch? :)

=-=-=-=-=
Powered by Blogilo

Bash – rm und der Standard input

Mai 26, 2011 um 12:58 vormittags | Veröffentlicht in Bash, GNU/Linux, Ubuntuusers | 15 Kommentare

Mit einem bestimmten Problem habe ich mich in letzter Zeit herumgeärgert: Ich möchte einen Haufen Dateien deren Namen über den Standardeingabekanal der Bash hereingeflogen kommen mit einem Befehl löschen. Natürlich könnte ich jede einzelne Datei angeben. Diese Methode wird aber sehr aufwändig wenn ich alle Dateien löschen möchte, deren Namen nicht in der Liste auftauchen.

Achtung, dank mehrerer Kommentare habe ich gelesen, dass ls hier die falsche Wahl ist. Eine bessere Alternative scheint find zu sein.

Anwendungsbeispiel

Ich habe mehrere ähnliche Dateien in meinem Directory. Diese werden automatisiert bearbeitet und das Ergebnis jeweils in einer Datei mit dem Zusatz Clean gespeichert. Nun interessieren mich die ursprünglichen Dateien nicht mehr und ich möchte sie ebenso automatisiert löschen. (Nein, ein Ersetzen der Urprungsdateien gleich beim Bearbeiten fällt leider aus technischen Gründen aus.) Ein kurzes ls gibt uns z.B. folgende Liste aus:

taach@prompt:~/test$ ls
Datei 1 Clean.txt
Datei 1.txt
Datei 2 Clean.txt
Datei 2.txt
Datei 3 Clean.txt
Datei 3.txt
Datei 4 Clean.txt
Datei 4.txt
Datei 5 Clean.txt
Datei 5.txt

Zum Glück stellt ls mit --ignore bereits eine Möglichkeit Dateinamen zu ignorieren die bestimmte Muster enthalten:

taach@prompt:~/test$ ls –ignore=*Clean.txt
Datei 1.txt
Datei 2.txt
Datei 3.txt
Datei 4.txt
Datei 5.txt

Diese Variante ist jedoch Case sensitive. Wenn das Muster also in verschiedener Groß-/Kleinschreibung vorliegt, empfiehlt es sich stattdessen grep einzusetzen:

taach@prompt:~/test$ ls | grep -iv Clean
Datei 1.txt
Datei 2.txt
Datei 3.txt
Datei 4.txt
Datei 5.txt

Die Option -i von grep schaltet die Case sensitivity aus und -v negiert die Auswahl. In diesem Fall werden also alle Zeilen entfernt in denen das Muster Clean oder clean vorkommt. An sich könnten wir nun diese Liste rm übergeben. Dafür verwenden wir xargs, welches notwendig ist, dass rm die Liste aus der Standardeingabe auch wirklich übergeben wird. Wenn die Dateinamen jedoch, wie in diesem Fall, Leerzeichen enthalten bringt uns rm diese Fehlermeldung:

taach@prompt:~/test$ ls –ignore=*Clean.txt | xargs rm
rm: Entfernen von „Datei“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „01.txt“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „Datei“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „02.txt“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „Datei“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „03.txt“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „Datei“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „04.txt“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „Datei“ nicht möglich: Datei oder Verzeichnis nicht gefunden
rm: Entfernen von „05.txt“ nicht möglich: Datei oder Verzeichnis nicht gefunden

Das passiert deswegen, weil die Leerzeichen ungeschützt vorliegen und die Bash, bevor die Liste nun endgültig rm übergeben wird, diese als Ende eines Dateinamens interpretiert. Wir müssen also xargs sagen, dass die Leerzeichen maskiert bleiben müssen ('\ ' statt ' '), damit die Bash sie ignoriert. Dazu gibt es die Option -0 welche jegliche Maskierungen aufrecht erhält:

taach@prompt:~/test$ ls –ignore=*Clean.txt | xargs -0 rm
rm: Entfernen von „Datei 01.txt\nDatei 02.txt\nDatei 03.txt\nDatei 04.txt\nDatei 05.txt\n“ nicht möglich: Datei oder Verzeichnis nicht gefunden

Verdammt, was ist denn jetzt schon wieder los? Ah, zwischen den Dateinamen ist plötzlich ein Zeichen aufgetaucht: '\n' Dieses Zeichen, auch Endline genannt, kennzeichnet einen Zeilenumbruch. Da wir xargs gesagt haben, dass alle Maskierungen aufrecht erhalten bleiben sollen, liegt nun auch der Zeilenumbruch als Reintext vor. Und da das Zeichen nun einfacher Text ist wird die ganze lange Wurst wie ein einziger Dateiname behandelt. Nun müssen wir xargs also noch mitteilen, wo die Dateinamen enden. Dazu verwenden die Option -d mit dem Parameter '\n':

taach@prompt:~/test$ ls –ignore=*Clean.txt | xargs -0 -d ‘\n’ rm

und siehe da alle Dateien, mit Ausnahme derer mit Clean im Namen wurde gelöscht:

taach@prompt:~/test$ ls
Datei 1 Clean.txt
Datei 2 Clean.txt
Datei 3 Clean.txt
Datei 4 Clean.txt
Datei 5 Clean.txt

Das ganze geht natürlich auch so:

taach@prompt:~/test$ rm `ls –ignore=*Clean.txt | xargs -0 -d ‘\n’`

bzw. im Bash-Style:

taach@prompt:~/test$ rm $(ls –ignore=*Clean.txt | xargs -0 -d ‘\n’)

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

=-=-=-=-=
Powered by Blogilo

Meta-Daten von PDFs bearbeiten

April 20, 2011 um 12:12 nachmittags | Veröffentlicht in Bash, GNU/Linux, LaTeX, Ubuntuusers | 9 Kommentare

Natürlich. Da will man mal was über ein Tool schreiben und dann kommen einem andere zuvor. :) Deswegen werde ich nicht das Tool allgemein beschreiben sondern nur dessen Einsatz zum Manipulieren von PDFs.

Vor kurzem war es notwendig, dass ich bei einem in LaTeX erstellten PDF die Metadaten etwas manipuliere. Um genau zu sein ging es um den Eintrag zu "Autor". Ich hatte in LaTeX auf die schnelle keine geeignete Möglichkeit gefunden die Titelseite nach meinen Wünschen umzuformatieren, also habe ich, nach einem Zeilenumbruch, zusätzliche Information in den \author{}-Befehl geschrieben. Diese Information sollte jedoch nicht in den Meta-Daten sichtbar sein.

Meine Lösung, die auf Anhieb funktioniert hat (wow …): pdftk.

Zuerst müssen wir die Meta-Daten aus dem PDF extrahieren. Das funktioniert mit folgendem Befehl:
pdftk <PDFName>.pdf dump_data output <InfoDatei>.info

Es wird also die Datei <PDFName>.pdf hergenommen und mittels dump_data dessen Meta-Daten herausgelöst. Gespeichert werden diese Daten nun in <InfoDatei>.info. Diese Textdatei liegt nun im selben Ordner wie das PDF und kann mit jedem beliebigen Texteditor bearbeitet werden. Sobald man damit fertig ist muss man die Meta-Daten wieder zurückschreiben. Das funktioniert so:
pdftk <PDFName>.pdf update_info <InfoDatei>.info output <PDFName>_clean.pdf

Hier wird wieder die Datei <PDFName>.pdf hergenommen und mittels update_info dessen Meta-Daten auf den neuesten Stand gebracht. Die Datei aus der pdftk diese neuen Informationen liest geben wir als <InfoDatei>.info an. Per output wird das Ergebnis in <PDFName>_clean.pdf geschrieben. Dies deswegen, weil ich nicht weiß ob es Probleme macht direkt wieder in die Datei zu schreiben aus der man gerade liest. Man kann aber dann einfach das alte PDF löschen und das Neue umbenennen.

Der Einfachheit halber habe ich das ganze in ein Skript gegossen:

#!/bin/bash

FILENAME=$1
FILENAME=${FILENAME%.*}

pdftk $FILENAME.pdf dump_data output $FILENAME.info

#<InfoDatei>.info bearbeiten
vim $FILENAME.info

pdftk $FILENAME.pdf update_info $FILENAME.info output ${FILENAME}_clean.pdf

echo "Cleaning up"
rm -f $FILENAME.pdf
rm -f $FILENAME.info
mv -f ${FILENAME}_clean.pdf $FILENAME.pdf

Das Skript wird mit <Skriptname>.sh <PDFDatei>.pdf aufgerufen. Zuerst wird der Name der PDF-Datei (das erste Argument des Skripts) in der Variable FILENAME gespeichert. Dann kürzen wir diesen Namen um das Suffix .pdf. Als Nächstes wird pdftk mit den besprochenen Parametern aufgerufen. Der Einfachheit halber wird überall der Name der übergebenen PDF-Datei verwendet. Statt vim $FILENAME.info kann natürlich jeder beliebige Texteditor verwendet werden. Sobald der Editor wieder geschlossen wurde geht die Verarbeitung weiter. Jetzt wird die bearbeitete Meta-Information wieder mit dem PDF kombiniert. Zum Schluss werden die alte PDF-Datei und die info-Datei gelöscht und die neue PDF-Datei wird so benannt wie die Alte.

Kurz und schmerzlos. Ich muss zugeben, ich verwende dieses Skript auch häufig um damit PDFs zu bearbeiten die ich von anderen Leuten bekomme. Mich stört es einfach wenn mir Okular im Titel anzeigt: Irgendein Titel – Microsoft Word

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

=-=-=-=-=
Powered by Blogilo

HowTo: Gestreamte Flash Videos einfach wiederherstellen [2. UPDATE]

Februar 18, 2011 um 8:11 nachmittags | Veröffentlicht in Bash, Free Software/Open Source, GNU/Linux, Multimedia, Programmieren, Ubuntuusers | 38 Kommentare

Ein Satz dessen Bedeutung mir nahezu jeden Tag meines Lebens aufs neue bewusst wird lautet:

Ich verwende Linux, ich kenne keine Einschränkungen.

Dank der UNIX-Philosophie (die im speziellen auch das KISS-Prinzip beinhaltet) existieren unter Linux viele von UNIX übernommene oder neu erdachte kleine Helferlein die, richtig kombiniert, nahezu jede noch so komplexe Aufgabe zu lösen vermögen.

Hinweis: Jeder in diesem Artikel genannte Befehl muss in einem Terminal eingegeben werden (z.B. Konsole, gnome-terminal, …).

Hintergrund

Seit längerer Zeit, wie vielen sicherlich aufgefallen ist, können Flash Videos, einmal gestreamt, nicht mehr einfach aus /tmp herausgepflückt werden. Da sie von Flash als gelöscht markiert werden, werden sie von einem Dateibrowser (und auch vom Befehl ls) nicht mehr angezeigt. Es gibt einen Workaround, der jedoch recht umständlich ist und unnötig Zeit verschwendet, da an sich jeder Schritt durch ein Skript ersetzt werden kann.

Der Workaround im Detail

Zuerst muss man die Prozess-ID (PID) des Flash-Prozesses herausfinden der das Video abspielt. Das passiert mit folgendem Befehl:
ps -e | grep gtk-gnash
gtk-gnash kann durch den Prozessnamen des eigenen Flash-Players ersetzt werden.

Da Flash nicht nur dazu dienen kann Videos abzuspielen, sondern z.B. auch Werbung auf einer Webseite anzeigt, kann es hier sein, dass mehrere solcher Prozesse angezeigt werden:
8766 ? 00:41:48 gtk-gnash
9951 ? 00:00:03 gtk-gnash
9965 ? 00:00:01 gtk-gnash
9979 ? 00:00:00 gtk-gnash

Da es keinerlei Anhaltspunkte dafür gibt hinter welchem Prozess genau denn nun der Flash-Player steckt, muss jeder Prozess einzeln durchgesehen werden. Dazu begibt man sich in das virtuelle Verzeichnis
/proc
In diesem Verzeichnis findet man für jeden laufenden Prozess einen Ordner mit dessen PID als Namen. Wir begeben uns nun in den ersten Ordner der zu einem Flash-Prozess gehört:
/proc/8766
In diesem Ordner finden wir unter anderem ein weiteres Verzeichnis mit dem Namen fd. Und darin befinden sich Links auf alle Dateien die von diesem Prozess verwendet werden. Egal ob es sich hierbei um Gerätedateien handelt (/dev/*), Pipes, Sockets … oder eben gelöschte Dateien.

An diesem Punkt sollte ich wohl anmerken: die Verlinkungen existieren nur solange der Prozess selbst läuft. Ist der Prozess beendet (z.B. der Tab im Browser wird geschlossen), dann ist auch die Verlinkung auf die Dateien weg.

Mit dem einfachen Befehl
ls -l
bekommen wir eine übersichtliche Auflistung der Dateien und ein paar nützliche Hinweise zu ihnen. So werden z.B. ein oder mehrere davon als "(deleted)" markiert. Die Namen der Dateien selbst sind einfache Nummern. Wir müssen nun alle diese Dateien an einen sicheren Ort kopieren (z.B. $HOME/Videos) und hier weiter durchforsten. Unter den als gelöscht markierten Dateien befinden sich auch durchaus Dateien, die keine Videos beinhalten. Faustregel: bei einem Flash Video, selbst mit geringer Qualität, ist im Allgemeinen mit einer Größe von mehreren MegaByte zu rechnen. Dies trennt meist die Video-Dateien vom Rest. Nicht-Video-Dateien haben meist eine Größe von 100-200 KiB. Wenn du dir nicht sicher bist, dann öffne die Datei einfach mit einem Video-Player deiner Wahl (z.B. VLC) und schau, ob dich das gesuchte Video anlächelt oder nicht.

Gratuliere, du hast viel Aufwand betrieben und (hoffentlich) das gesuchte Video gefunden. Aber: der Artikel wäre etwas überflüssig wenn das alles wäre.

Das Skript zum Film

WIe zuvor schon erwähnt lassen sich diese Vorgänge ohne Weiteres automatisieren. Dazu habe ich ein kleines Skript erstellt, welches dem Nutzer alle Vorgänge, bis auf das Durchfiltern der resultierenden Dateien alles abnehmen soll. Das Skript erkennt nun FLV-Dateien an ihrem Header. Das Skript stelle ich unter die GPLv3 und werde es im Sinne der besseren Verständlichkeit hier erklären.

Zuerst wird obligatorisch die Shell bestimmt in der der Code ausgeführt werden soll:
#!/bin/bash

Dann folgen ein paar Variableninitialisierungen. Bei der ersten kann der Nutzer angeben wie sein Flash-Player heißt. Dies ist nicht der eigentliche Name des Programmes, sondern nur der Befehl mit dem es in einem Terminal aufgerufen werden kann. Meistens sind diese Bezeichnungen identisch, aber nicht immer
FLASH=gtk-gnash

Nutzer des offiziellen Flash-Players wollen $FLASH vielleicht so:
FLASH=npviewer.bin
anpassen.

Dann wird die Dateiendung gewählt die die Dateien am Schluss bekommen sollen. Da es sich in unserem Beispiel um Flash Videos handelt, wird die Endung wohl *.flv lauten.
SUFFIX=flv

Zuletzt müssen wir noch angeben wohin die Dateien am Schluss gespeichert werden sollen:
DESTINATION=$HOME/Videos
TEMP_DESTINATION=$DESTINATION/.tmp
Der Ordner /tmp wird angelegt um nicht aus versehen Dateien zu überschreiben oder zu löschen die im Zielordner liegen und nicht den Kriterien entsprechen. Hier werden die Dateien während des Ablaufes des Skriptes zwischengelagert. Dieses temporäre Arbeitsverzeichnis ist in diesem Fall ein Unterordner des Zielverzeichnisses.

Um Fehlermeldungen zu vermeiden versuchen wir das gewünschte Verzeichnis anzulegen. Das if fragt nach, ob ein Verzeichnis (-d = Directory) bereits existiert. Durch das Rufzeichen wird die Antwort ins Gegenteil umgekehrt. Der Code der if-Abfrage wird nur ausgeführt, wenn die Antwort "ja" lautet. Daraus folgt: Wenn das Verzeichnis bereits existiert verkehrt das Rufzeichen nach "nein" und der Code wird nicht ausgeführt, das Verzeichnis also nicht erstellt und umgekehrt. Der Parameter -p weist den Befehl mkdir an das gewünschte Verzeichnis zu erstellen, auch wenn es dazu erst ein oder mehrere Überverzeichnisse erstellen muss. Wir erzeugen also das Zielverzeichnis und das temporäre Arbeitsverzeichnis:
if [ ! -d $DESTINATION ]
then
mkdir -p $DESTINATION
fi
if [ ! -d $TEMP_DESTINATION ]
then
mkdir -p $TEMP_DESTINATION
fi

Jetzt beginnen die ersten Berechnungen. Wir gehen vor, wie oben im Workaround beschrieben. Zuerst muss die PID eines Prozesses herausgefunden werden. Dabei hilft uns das Programm ps:
TEMP=$(ps -C $FLASH -o pid=)
Der Parameter -C übernimmt den Namen des Prozesses (den wir in der Variable $FLASH hinterlegt haben). Der Parameter -o sagt dem Programm, dass wir eine bestimmte Ausgabe erwarten. Gefolgt wird der Parameter von den Feldern die ausgegeben werden sollen, in unserem Fall wollen wir die Prozess-ID, also geben wir an: pid. Das Gleichheitszeichen beendet die Liste. Da wir die PIDs mehrmals benötigen weisen wir sie einer Variable zu.

PID_ARRAY=(${TEMP})
macht aus der vorher verwendeten Variable ein Array mit dem Namen
$PID_ARRAY. Die Verwendung eines Arrays macht es bei mehreren Prozessen einfacher diese geordnet durchzugehen.

Nun da wir die Prozess-IDs bekommen haben wird es Zeit jeden einzelnen Prozess durchzugehen. Dazu verwenden wir eine Schleife die durch das gerade erstellte Array iteriert.

Kleiner Exkurs: eine for-Schleife schaut in Shell-Skript zum Beispiel so aus:
for Zählervariable in Liste von Werten
do
... Code ...
done

Für die Zählervariable kann man jeden beliebigen Text (auch einzelne Buchstaben) verwenden. Die Liste von Werten definiert sich dann aus dem Inhalt einer Datei, einem Array oder einem Regulären Ausdruck (z.B. *.mp3). Im letzteren Fall werden alle Dateien im Verzeichnis mit der Endung .mp3 in die Liste aufgenommen und der Inhalt der Zählervariable ist dann bei jedem Schleifendurchlauf je ein Dateiname aus dieser Liste.

for PID in `seq 0 $[${#PID_ARRAY[@]}-1]`
do

tut also folgendes: In der Zählervariable
$PID ist immer die jeweilige Stelle im Array gespeichert auf die im aktuellen Durchlauf gerade zugegriffen werden soll. Die Liste der PIDs wird vom Array $PID_ARRAY zur Verfügung gestellt.

for PID in $(pgrep -f $FLASH)
do
liefert uns alle PIDs der in $FLASH angegebenen Prozesse und durchläuft mit jedem davon einmal die Schleife.

Nun können wir den Pfad zu den Dateien des jeweiligen Prozesses erzeugen. Diesen legen wir in der Variable $PATH2FILE ab, da wir diesen Pfad später noch einmal brauchen werden. In $PID ist die Prozess-ID gespeichert.
PATH2FILE=/proc/$PID/fd

Jetzt wird es Zeit die Verlinkungen im gerade gefundenen Pfad herauszuholen welche als gelöscht markiert sind. Der Befehl ls -l zeigt uns zum Glück, welche Dateien davon betroffen sind (z.B.):
lrwx------ 1 user user 64 2011-02-18 20:58 0 -> socket:[256465]
l-wx------ 1 user user 64 2011-02-18 20:58 1 -> /home/user/.xsession-errors
...
lrwx------ 1 user user 64 2011-02-18 20:58 6 -> /tmp/tmpfJsOdcv (deleted)
lr-x------ 1 user user 64 2011-02-18 20:58 7 -> /dev/urandom
lrwx------ 1 user user 64 2011-02-18 20:58 8 -> /tmp/tmpfvTLn7o (deleted)

So wenden wir dies nun an:
LOOT=$(ls -l $PATH2FILE | grep deleted | cut -f8 -d\ )
Nachdem ls uns alle Verlinkungen im Pfad geliefert hat, jagen wir das Ergebnis durch eine Pipe "|". Das bewirkt, dass das Ergebnis des Befehls, der links von der Pipe steht an den Befehl übergeben wird der rechts davon steht. Mit dem Befehl grep suchen wir nach dem Begriff "deleted" um alle Zeilen wegzuschneiden welche diesen Begriff nicht beinhalten. Damit bekommen wir eine Liste von Dateinamen welche von Flash als gelöscht markiert wurden. Von dieser Liste benötigen wir aber nur die Namen der Verlinkungen die auf diese Dateien zeigen. Also schicken wir diese Liste über eine weitere Pipe an den Befehl cut. Diesem geben wir über den Parameter -d an, dass wir in den vorliegenden Zeilen die Leerzeichen ('\ ' ist die Escape-Sequenz um Leerzeichen darzustellen) als Trennzeichen verwenden wollen. Der Parameter -f bekommt dann die Nummer des Abschnitts welcher den Namen der Verlinkung beinhält. Da die Ausgabe des Befehls ls -l immer auf dieselbe Art und Weise formatiert ist können wir hier einfach mal an einem Beispiel abzählen und die Ziffer ’8′ hinschreiben. (Der Name der Verlinkung ist die Ziffer nach der Uhrzeit. Der Pfeil zeigt, dass es sich um eine Verlinkung auf die rechts daneben stehende Datei handelt.) Das Ergebnis all dieser Berechnungen wird in der Variable $LOOT untergebracht.

Nun wird auch diese Liste zur leichteren Verarbeitung in ein Array umgewandelt.
ARRAY=(${LOOT})

Genauso wie zuvor wird nun auch dieses Array mittels einer for-Schleife durchlaufen. Beachte, dass wir uns immernoch in der ersten Schleife befinden. Diese zweite Schleife wird also jedesmal ausgeführt, wenn die erste Schleife in einen neuen Durchgang geht.
for FILE in `seq 0 $[${#ARRAY[@]}-1]`
do

Nun kopieren wir die gefundenen Dateien an den temporären Zielort und benennen sie durch gehend nach diesem Muster: PID_AufsteigendeZahl.flv
cp $PATH2FILE/${ARRAY[$FILE]} $TEMP_DESTINATION/${PID}_$FILE.$SUFFIX
Die geschweiften Klammern um $PID sind notwendig, da das Skript sonst den folgenden Unterstrich als Teil des Variablennamens interpretieren würde. Da es die Variable $PID_ aber nicht gibt würde das Skript an dieser Stelle auch nichts einfügen. Das Ergebnis würde dann nur aus einer fortlaufenden Nummer und der Dateiendung bestehen.

done
done

Jede Schleife muss separat durch ein done beendet werden.

Um nun die Spreu vom Weizen zu trennen müssen wir uns jede Datei einzeln ansehen. Zum Glück folgen Flash Videos der Tradition der Magic Numbers. Das sind Zahlen oder Buchstaben die ganz am Anfang der Datei stehen (so wie in diesem Skript z.B. das #!/bin/bash). Bei Flash Videos sind dies die Buchstaben "FLV". Diese Kombination haben wir ganz oben in der Variable $HEADER_INFO gespeichert.

for FILE in $TEMP_DESTINATION/*.$SUFFIX
do

Die Headerdaten bekommen wir mit dem Befehl head. Diese vergleichen wir mit der Variable $HEADER_INFO
if [ $(head -c 3 $FILE) == $HEADER_INFO ]
then

Wenn die Datei diese Kombination im Header aufweist wird sie in das Zielverzeichnis verschoben.
mv $FILE $DESTINATION

Wenn Sie die Kombination nicht hat, wird sie als unbrauchbar gelöscht.
elif
rm $FILE

fi
done
Das if wird mit einem fi abgeschlossen und die Schleife wieder mit eine done.

Jetzt haben wir alle gefundenen Video-Dateien im Zielverzeichnis und das temporäre Arbeitsverzeichnis sollte wieder leer sein.

Abschluss

Den vollständigen Code zusammen mit einer Kopie der GPLv3 gibt es hier herunterzuladen. Wenn du Verbesserungen für das Programm hast, oder einen Weg kennst die Nicht-Video-Dateien von den Video-Dateien zu trennen, dann gib dies bitte in den Kommentaren bekannt. Ich möchte das Projekt gerne auf Sourceforge laden, zur Zeit hat der Server jedoch eine zickige Phase.

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

=-=-=-=-=
Powered by Blogilo

Erstelle eine kostenlose Website oder einen kostenlosen Blog – auf WordPress.com!. | The Pool Theme.
Entries und Kommentare feeds.

Folgen

Erhalte jeden neuen Beitrag in deinen Posteingang.