Mancher Power-User mag sich wahrscheinlich fragen, ob ich denn nicht ganz bescheuert bin, über solche essentielle Dinge zu schreiben, aber vielleicht ist das eine oder andere noch für jemanden Neuland und ich selbst muss immer wieder bei manchen Funktionen, die ich nur selten benötige, auf meinem Spickzettel nachschauen, wie das denn nochmals genau war.

Im Folgenden einerseits ein paar nützliche Shell-Befehle für den täglichen Gebrauch, Tipps, wie man die Shell etwas an seine Gewohnheiten anpasst und wie man aus dem "Herr der Shell" (das Terminal) noch etwas herauskitzelt.

Mein .bash_profile

Ich weiß nicht, ob Sie's schon wuuuuussten, aaaaber... Im eigenen Benutzerordner gibt es die (unsichtbare) Datei .bash_profile (auf dem Mac – bei Linux-Systemen wahrscheinlich eher .bash_aliases) in der sich Befehls-Aliase und Funktionen ablegen lassen, die einem das Leben unheimlich erleichtern.
Als Unix-User weiß man das sicher, aber als Mac-User, der nur gelegentlich auf die Shell zurückgreift, nicht unbedingt.

Diese Datei wird immer beim Öffnen einer neuen Shell geladen. Das heißt, man könnte hier auch irgendwelche Automatismen oder export-Variablen hinterlegen.

Man öffne und bearbeite diese Datei mit dem Shell-Texteditor seiner Wahl...meine Wahl ist (ja, so isses) pico:

pico ~/.bash_profile
(bzw. bash_aliases oder wie auch immer)

Alias

Ein Alias ist ein Kürzel für einen längeren Befehl, den man oft tippt oder sich nicht so einfach merken kann und hat die Syntax

alias <kuerzel>='<befehl>'

Einige meiner Aliase sind:

alias ..='cd ..'             # Eine Verzeichnisebene rauf mit ..
alias home='cd ~'            # Mit home direkt zum Home-Verzeichnis
alias ls='ls -AGp'           # Befehl ls soweit aufgebohrt, dass er standardmäßig
                             # (A) unsichtbare Dateien (G) farbig anzeigt und
                             # (p) hinter Verzeichnisnamen ein / anhängt
alias lsl='ls -lAhOp'        # Wie oben, nun allerdings als ausführliche Liste, hierbei
                             # (h) die Dateigröße menschenlesbar und (O) Mac-File-FLags anzeigen
alias lsc='ls -lAhp | wc -l' # Zeigt die Anzahl der Dateien im aktuellen  Verzeichnis an
alias du='du -h'             # Ausgabe von 'du' mit menschenlesbaren Dateigrößen
alias cls='printf "\033c"'   # Leert den Bildschirminhalt des aktuellen Konsolen-Terminals
alias server='ssh apfelz@192.168.3.20' # SSH-Verbindung zum vorgegebenen Server aufbauen

So muss ich zum Beispiel nur noch den Befehl server eingeben, um eine ssh-Verbindung zu einem bestimmten Server aufzubauen.
Wie ich mir noch dazu erspare, darauf das Account-Passwort eingeben zu müssen, habe ich in diesem Artikel erklärt.

Funktionen

Funktionen sind schon eher kleine Skripte. Mit ihnen wird nicht nur ein Befehl aufgerufen.
Man kann mehrere Befehle, Schleifen, Logik und Variablen mit einbringen. Hiermit erzeugt man lapidar gesagt "eigene Befehle".

Remote Copy

Hier zum Beispiel mein Befehl xcp – eine Variante des normalen cp-Befehles, mit dem ich allerdings nicht eine Datei von einem Ort zu einem anderen Ort auf meinem Computer kopiere, sondern von meinem Computer via ssh auf einen bestimmten anderen Computer/Server:

function xcp() {
  if [[ $2 == "" ]]; then
    SENDTO=/Users/meinserveruser/Desktop;
  else
    SENDTO=$2;
  fi
  if [[ -d $1 ]]; then
    scp -r $1 serveradmin@192.168.3.20:$SENDTO;
  else
    scp $1 serveradmin@192.168.3.20:$SENDTO;
  fi  
  }

$1 und $2 sind Parameter, die ich hinter xcp schreiben kann, wenn ich den Befehl aufrufe. Der erste Parameter ist Pflicht – er gibt an, welche Datei (oder Ordner) kopiert werden soll, der zweite Parameter ist optional: Er ist der Zielpfad auf dem entfernten Rechner und ist er nicht gesetzt, wird die Datei auf dem entfernten Rechner auf den Desktop kopiert.

Aufruf-Beispiel:

xcp ~/Desktop/info.txt
xcp /Library/Preferences/com.apple.systempreferences.plist /Library/Preferences

Laufende Prozesse suchen

Der Befehl top mag zwar ganz praktisch sein, ein Programm zu finden, das gerade 100% Prozessorleistung frisst (stört soetwas heutzutage bei 8 Prozessorkernen überhaupt noch?), aber wenn ich die Prozess-ID eines bestimmten Prozesses suche, dann ist mir die Liste immer etwas zu lang. Hier hilft es, sich nur Prozesse aufzulisten, die einen bestimmten Namen haben und dies habe ich mit ftop etwas vereinfacht.

function ftop() {
  ps -ef | grep -i $1 | grep -viE 'grep'
  }

Beispiel:

ftop firefox

Inzwischen habe ich auf meinem Server recht viele Perl-Skripte laufen, die entweder dauerhaft Sensoren abfragen, zeitlich gesteuert sind oder per CGI-Skript angestoßen werden.
Um hier einen kleinen Überblick zu behalten, was gerade so ausgeführt wird, habe ich mir folgende Funktion erstellt (An sich nichts anderes, als die Ausführung von ftop perl):

function ptop() {
  ps -ef | grep perl | grep -viE 'grep'
  }

Archive enpacken vereinfacht

Ich enpacke selten eine tar-Datei per Kommandozeile. Wenn ich es dann doch mal tun muss, muss ich immer wieder googeln, wie der Befehl denn anzuwenden ist. Deshalb habe ich mir utar gebastelt, wo ich mir auch keine Gedanken über die verwendete Kompression machen muss.

function utar() {
  if [[ $1 =~ .gz$ ]]; then tar xvzf $1;    
  elif [[ $1 =~ .tgz$ ]]; then tar xvzf $1;    
  elif [[ $1 =~ .bz2$ ]]; then tar xvjf $1;    
  elif [[ $1 =~ .tbz$ ]]; then tar xvjf $1;    
  elif [[ $1 =~ .tar$ ]]; then tar xvf $1;    
  else
    echo "Keine TAR-Datei"
  fi
  }

Verwendung:

utar meinedatei.tbz

Archive packen vereinfacht

In die andere Richtung gibt's natürlich auch noch Vereinfachungspotential. Wie sind nochmals die Optionen für ein komprimiertes tar-Archiv?

function mktar {
  if [[ $1 == "" || $2 == "" ]]; then
    echo "  Verwendung:"
    echo "  \$ mktar <datei oder ordner> <ausgabe-tar>"
    echo "  Erstellt die Archiv-Datei"
    return 0
  fi
  if [[ ! -e "$1" ]]; then
    echo "Datei oder Verzeichnis nicht gefunden"
    return 0
  fi
  if [[ -d $1 ]]; then
    tar -cjf $2.tbz -C "$1" .
  else
    tar -cjf $2.tbz -C $(dirname "$1") $(basename "$1")
  fi
  echo "$2.tbz erstellt"
  }

Verwendung:

mktar meinordner archivname   # -> archivname.tbz
mktar meineda.tei archivname  # -> archivname.tbz

Bilder verkleinern

(Benötigt installiertes ImageMagick)

Bilder von Digitalkameras sind inzwischen recht groß geworden. Doch nicht immer werden die vollen 20 Megapixel benötigt. Ich muss Bilder sehr oft mit Photoshop verkleinern, weil zum Beispiel der digitale Bilderrahmen sowieso nur 1280x800 Pixel anzeigen kann oder für die Website ein 6000 Pixel breites Bild nur unnötigen Traffic erzeugt (ich bin da noch von der alten Schule und knalle Websites nicht mit Datenklötzen zu, weil ja "sowieso jeder eine 100MBit-Leitung oder LTE hat").

Mit ImageMagick geht das verkleinern von Bildern um einiges schneller als mit einer Photoshop-Stapelverarbeitung (die ich irgendwie immer wieder erst neu einrichten muss, weil ich dann doch wieder eine andere Bildergröße haben will).
Das Dumme nur: Die Kommandozeilen-Argumente muss ich mir immer wieder auf's neue zusammensuchen.

Deshalb auch hier eine kleine Funktion im .bash_profile so dass ich alle Bilder eines Ordner mit nur einem einzigen kleinen Befehl einfach verkleinern kann.

function resize-all() {
  if [ ! -d resized ]; then mkdir resized; fi
  mogrify -path resized -resize $1x$1 -quality 80 -format jpg *
  }

Anwendung:

cd /pfad/zu/den/bildern # in den Bilderorderner wechseln
resize-all 1280         # Alle Bilder im aktuellen Ordner
                        # auf maximal 1280px Breite oder Höhe konvertieren

Es wird ein Unterordner namens resized erstellt, in dem die konvertierten Bilder (immer als JPEG, Qualitätsstufe 80%) gespeichert werden.
Bei Querformat-Bildern wird die angegebene Größe als Breite genommen, bei Hochformat-Bildern als Höhe. Höhe, respektive Breite werden proportional angepasst.

Bild-Metadaten auf anderes Bild übertragen

(Benötigt installiertes ExifTool)

Es passt bei der besten Kamera nicht immer alles vor die Linse, also knipse ich ab und an im Urlaub mehrere Bilder vom Objekt und füge diese dann später am Computer zu einem Panorama-Bild zusammen.
Das Panorama-Bild ist immer recht gut...allerdings sind die EXIF-Informationen (Kamerinformationen, GPS-Position, Erstellungszeit) dann weg und das Datei-Erstellungsdatum des Panorama-Bildes ist natürlich da, wo das Bild zusammengerechnet wurde und nicht da, wo ich Tage zuvor die einzelnen Bilder geschossen hatte.

Hierfür ein kleines Skript, welches auf ExifTool zurück greift, um die EXIF-Informationen vom Ursprungs-Bild auf das neue Bild zu übertragen und zusätzlich Datei-Erstellungs- und Modifikations-Datum überträgt.

function copyMeta() {
  original=`stat -f%SB -t "%m/%d/%y %H:%M" "$1"`
  exiftool -TagsFromFile "$1" "-all:all>all:all" "$2"
  setFile -md "$original" "$2"
  }

Anwendung:

copyMeta /pfad/zu/ursprungsbild.jpg /pfad/zu/neuemBild.jpg

Exif-Erstellungsdatum als Datei-Erstellungsdatum verwenden

(Benötigt installiertes ExifTool)

Ich habe schon einige Male per Dropbox-Link gemeinsame Urlaubsfotos von Freunden erhalten. Leider hatten alle Bilder das Erstellungs- und Änderungsdatum des Up- oder Downloads und nicht das Erstellungs- und Änderungsdatum, als das Bild geschossen wurde. Das sorgt für Unordnung in meinem digitalen Fotoalbum!

Folgendes Skript setzt Erstellungs- und Änderungsdatum einer JPEG-Datei auf die Exif-Erstellungszeit (falls vorhanden):

function exifdatetofiledate() {
  CREATION=`exiftool "$1" | grep "Create Date" | sed -E 's,.*([0-9]{4}):([0-9]{2}):([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2}).*,\2\/\3\/\1 \4:\5:\6,g'`
  if [[ $CREATION == "" ]]; then
    echo "Keine Exif-Information in Datei gefunden."
  else
    setFile -md "$CREATION" "$1"
  fi
  }

Und wie wende ich diesen Befehl (den ich im .bash_profile hinzugefügt habe) auf einen ganzen Ordner mit JPEG-Bildern an?

for f in /pfad/zu/ordner/*.jpg; do exifdatetofiledate "$f"; done

launchctl Shortcut

Viele Automatismen werden auf meinem Server per launchd ausgeführt. Um mir etwas Tipparbeit beim Auflisten der aktiven LaunchDaemons, starten, stoppen und neuladen zu ersparen, habe ich folgende Funktion meinem .bash_profile hinzugefügt.

Hierbei gilt zu beachten, dass meine sämtlichen relevanten LaunchDaemons im Verzeichnis /Library/LaunchDaemons/ liegen und nach dem Schema net.apfelz.**bezeichnung**.plist benannt sind.
Liegen die .plist-Dateien im Benutzer-Library-Ordner oder haben eine andere Domain-Bezeichnung, ist die Funktion natürlich dementsprechend anzupassen.

function lctl() {
  if [[ $1 != "load" && $1 != "unload" && $1 != "start" && $1 != "stop" && $1 != "list" && $1 != "reload" && $1 != "r$
    echo "
 Verwendung:"
    echo "
 \$ lctl <load|unload|start|stop|list|reload|refresh|activate|deactivate|edit> <launchDaemon>"
    echo "
 Lädt/entlädt/refresht/editiert den angegebene net.apfelz-launchDaemon oder listet alle net.apfelz-launchDaemo$
    echo "  activate und deactivate aktiviert und deaktiviert den LaunchDaemon über den Neustart hinweg"
    echo "  Präfix'net.apfelz.' und Postfix '.plist' müssen weg gelassen werden"
    return 0
  fi
  if [[ $1 == "list" ]]; then
    sudo launchctl list > /tmp/list.tmp
    for f in /Library/LaunchDaemons/net.apfelz.*; do
      NAME=`echo "  $f" | sed -E 's,/Library/LaunchDaemons/net.apfelz.,,g' | sed -E 's,.plist,,g'`
      LABEL=`cat $f | awk 'f{print;f=0} /Label/{f=1}' | sed -E 's,</?string>,,g' | sed -E 's, ,,g'`
      STATUS=`cat /tmp/list.tmp | grep "$LABEL$" | awk '{ FS = " " ; print $1 }' | sed -E 's,-,  X  ,g'`
      printf "  [%5s]" "$STATUS"
      echo "$NAME "
      done
    rm /tmp/list.tmp
    echo " "
  elif [[ $2 == "" ]]; then
    echo "! Es wurde kein LaunchDaemon angegeben"
    echo " "
  elif [[ ! -e /Library/LaunchDaemons/net.apfelz.$2.plist ]]; then
    echo "! /Library/LaunchDaemons/net.apfelz.$2.plist existiert nicht"
    echo " "
  else
    if [[ $1 == "reload" || $1 == "refresh" ]]; then
        echo "Unloading..."
        sudo launchctl unload /Library/LaunchDaemons/net.apfelz.$2.plist
        sleep 3
        echo "Loading..."
        sudo launchctl load /Library/LaunchDaemons/net.apfelz.$2.plist
    elif [[ $1 == "start" ]]; then
        sudo launchctl load /Library/LaunchDaemons/net.apfelz.$2.plist
    elif [[ $1 == "stop" ]]; then
        sudo launchctl unload /Library/LaunchDaemons/net.apfelz.$2.plist
   elif [[ $1 == "activate" ]]; then
        sudo launchctl load -w /Library/LaunchDaemons/net.apfelz.$2.plist
    elif [[ $1 == "deactivate" ]]; then
        sudo launchctl unload -w /Library/LaunchDaemons/net.apfelz.$2.plist
    elif [[ $1 == "edit" ]]; then
        sudo pico /Library/LaunchDaemons/net.apfelz.$2.plist
    else
        sudo launchctl $1 /Library/LaunchDaemons/net.apfelz.$2.plist
    fi
  fi
  }

Verwendung:

# Alle eigenen LaunchDaemons auflisten
sudo launchctl list | grep net.apfelz   # normaler Weg
lctl list                               # kurzer Weg
# 'lctl list' listet hierbei nicht nur alle geladenen LaunchDaemons auf, sondern
# alle in /Library/LaunchDaemons enthaltenen LaunchDaemons - ob geladen oder nicht -
# und zeigt per Präfix an, ob der LaunchDaemon geladen/aktiv ist oder nicht
# > $lctl list
# > [     ] hotfolderkontrolle      # <-- nicht geladen
# > [  X  ] backup                  # <-- geladen, aber derzeit nicht aktiv
# > [  376] serial2mysql            # <-- geladen, als Prozess ID 376 aktiv

sudo launchctl load /Library/LaunchDaemons/net.apfelz.hotfolderkontrolle.plist   # normaler Weg
lctl load hotfolderkontrolle                                                     # kurzer Weg
# statt load kann auch start verwendet werden

sudo launchctl unload /Library/LaunchDaemons/net.apfelz.hotfolderkontrolle.plist # normaler Weg
lctl unload hotfolderkontrolle                                                   # kurzer Weg
# Statt unload kann auch stop verwendet werden

sudo launchctl unload /Library/LaunchDaemons/net.apfelz.hotfolderkontrolle.plist
sudo launchctl load   /Library/LaunchDaemons/net.apfelz.hotfolderkontrolle.plist # normaler Weg
lctl reload hotfolderkontrolle                                                   # kurzer Weg
#statt reload kann auch refresh verwendet werden

sudo launchctl unload -w /Library/LaunchDaemons/net.apfelz.hotfolderkontrolle.plist # normaler Weg
lctl deactivate hotfolderkontrolle                                                  # kurzer Weg

sudo launchctl load -w /Library/LaunchDaemons/net.apfelz.hotfolderkontrolle.plist # normaler Weg
lctl activate hotfolderkontrolle                                                  # kurzer Weg

sudo pico /Library/LaunchDaemons/net.apfelz.hotfolderkontrolle.plist # normaler Weg
lctl edit hotfolderkontrolle                                         # kurzer Weg

Achtung: Als LaunchDaemon-Bezeichnung zum laden/entladen ist die Bezeichnung des .plist-Dateinamens zu verwenden, nicht das Label, das in der XML-.plist-Verwendet wird und vom Dateinamen abweichen kann!

Terminal-Farben

Nicht unbedingt am Mac, aber bei anderen Unix-Systemen kann einem bei Datei-Listings möglicherweise das große Grauen packen:
Dateien mit 777er-Berechtigung sind zum Beispiel hellgrau auf hellgrünem Hintergrund. Das kann doch keiner lesen...

Die Farbgebung im Terminal ändert man wie folgt:

Aktuelle Terminal-Farben in Textdatei speichern:
dircolors -p > ~/.dircolors
und diese Textdatei bearbeiten. Der problematische Farbcode ist an der Stelle OTHER_WRITABLE zu finden. Ich habe die Hintergrundfarbe einfach auf 40 (Schwarz) geändert:

~/.dircolors
OTHER_WRITABLE 34;40 # dir that is other-writable (o+w) and not sticky

Diese Änderung wendet man mit dem Befehl

eval $(dircolors ~/.dircolors)
an, welchen man am besten auch dem
~/.bash_profile
hinzufügt, so dass die Farbe bei jeder neuen Session wie gewünscht angepasst wird.

Bash-Kniffe

Vielleicht ist das eine oder andere Tastenkürzel ja noch nicht bekannt...

Cursor-HochLetzten Befehl aufrufen

ctrl-aZum Anfang der Zeile

ctrl-eZum Ende der Zeile

ctrl-wWort links vom Cursor löschen

ctrl-kAlles rechts vom Cursor löschen

ctrl-lBildschirm leeren

ctrl-zGesamte Zeile löschen / Abbrechen

Diese hier kennen sicher die Wenigsten: Prozess anhalten

ctrl-zProzess anhalten

Hält den aktuell im Terminal laufenden Prozess (z.B. top oder eine langwierige Kompilierung) an. Im Terminal erscheint [1]+ Stopped und die wieder freie Eingabeaufforderung.
Der Prozess lässt sich mit fg ("foreground") wieder fortführen. Sollten mehrere Prozesse im aktuellen Fenster pausiert sein, muss noch die Nummer mitgegeben werden (z.B. fg 1).

Prozesse lassen sich alternativ auch mit bg (statt fg) in den Hintergrund verschieben. Hierbei läuft der Prozess weiter, das Terminalfenster behält allerdings die freie Eingabeaufforderung und stdOut-Ausgaben des noch laufenden Hintergrundprozesses erscheinen weiterhin im Terminal-Fenster (was das Arbeiten leicht unübersichtlich machen kann; da öffnet man dann doch lieber ein neues Terminal-Fenster).

ctrl-sProzess-Ausgabe pausieren

Hierbei wird nicht der Prozess an sich angehalten, sondern nur seine stdOut-Ausgabe, so dass man in Ruhe die Ausgabe lesen kann, ohne dass weitere Ausgabe sie nach oben drückt.
Erfordert der Prozess eine weitere Eingabe auf der Kommandozeile, wird er ab da dann pausiert.
Mit Hilfe von ctrl-q lässt sich der Prozess dann fortführen.

Der eingegebene Befehl hat mal wieder nicht funktioniert, weil Root-Rechte erforderlich waren?

Keine Angst, man muss nicht wieder alles neu eintippen. Wer sich die Tabelle oben genau angeschaut hat, wird die Idee haben, einmal auf die Cursor-Hoch-Taste zu drücken, dann ctrl-a und sudo gefolgt von einem Leerzeichen einzugeben. Gute Idee.

Auch funktionieren würde sudo !! da !! immer den letzten Befehl nochmals wiederholt.

cp boeserwurm /var/bin/   # Mist...geht nicht. Keine Berechtigung
sudo !!                   # Jetzt is'ser drinne!

Das Terminal

(Auf dem Mac)

Einfacher Zugriff

Um mal schnell etwas zu kopieren oder auf einem Server zu prüfen, kann es hilfreich sein, ein neues Terminalfenster nur mit einem Tastenkürzel aufrufen zu können. Hier hilft unter Umständen TotalTerminal (aka "Visor") aus.

Wenn ich zweimal hintereinander auf die ctrl-Taste drücke, schiebt sich bei mir aus der Menüleiste ein Terminal-Fenster heraus, das bei nochmaligem doppelten ctrl-Drücken wieder verschwindet.

Einfache Tasten-Navigation

Ich liebe es, mit gedrückter Wahltaste mit den Cursortasten wortweise vor und zurück zu springen. Dies kann im Terminal bei langen Befehlszeilen auch sehr hilfreich sein und lässt sich einfach bewerkstelligen.

Neues Fenster hat das Basic-Profil
In den Voreinstellungen des Terminals auf Einstellungen > Basic (soweit unter "Start" eingestellt ist, dass sich ein neues Fenster mit den Basic-Einstellungen öffnen soll; Siehe Bild 1), unter dem Reiter Tastatur auf das + klicken, dann:

Taste: Linkspfeil,  Sondertaste: Wahltaste, Text an Shell senden: \033b
Taste: Rechtspfeil, Sondertaste: Wahltaste, Text an Shell senden: \033f

Achtung: Alle Tastatureingaben werden aufgezeichnet, wenn der Cursor im Feld "Aktion" ist!
Zum Löschen von Zeichen den Button "Ein Zeichen löschen" und nicht die Löschtaste verwenden, zum Bestätigen oder Abbrechen ebenfalls auf den vorgesehenen Button klicken.

Auf Passworteingabe bei 'sudo' verzichten

Normalerweise wird man bei Verwendung des sudo-Befehles nach seinem Passwort gefragt. Immerhin: Verwendet man sudo innerhalb einer gewissen Zeitspanne mehrmals hintereinander, wird nicht wieder nach dem Passwort gefragt.
Nerven kann die Passworteingabe mit der Zeit aber trotzdem (obwohl sie eine gute Warnung "Pass' auf, was Du da machst!" darstellt).

Durch den Befehl

sudo visudo
 kann man in der sudoers-Datei eine Zeile hinzufügen, die die Passworteingabe ab dem nächsten Shell-Login nicht mehr verlangt.

In dem Bereich "user privilege specification" habe ich zu den bestehenden beiden Einträgen einen weiteren Eintrag für den Benutzer "apfelz" hinzugefügt:

# User privilege specification
root    ALL=(ALL) ALL
%admin  ALL=(ALL) ALL
apfelz  ALL=(ALL) NOPASSWD: ALL

Achtung: Bei mir wird diese Datei mit dem Texteditor vi geöffnet, dessen Bedienung nicht unbedingt jedem sehr intuitiv erscheint (wenn man ihn nicht öfters benutzt):
Mit den Cursortasten in die gewünschte Zeile navigieren, mit der Taste i in den Insert-Modus wechseln, Zeile hinzufügen, mit der Taste Esc aus dem Insert-Modus springen und mit :w (Doppelpunkt und 'w') und Enter speichern, mit :q und Enter beenden.