16. Emacs Extension

16.1. Emacs-Erweiterungen mit Cut-Paste-Modify

Es ist unsinnig - wie immer - mit einer nackten Funktion anzufangen. Das Stichwort ist hier Cut-Paste-Modify. Immer Beispiele kopieren und abändern! So lange, bis der Groschen gefallen ist. Alles andere ist Schule und blanker Unsinn.

Man fängt in den wenigsten Fällen mit einer Funktion oder gar einem Package an. Die Übersicht geht dabei komplett verloren und das Ergebnis ist mager.

Was soll die Funktion also können?

Wenn ich [die Funktion] ausführe, dann wird ein neuer Shell-Buffer geöffnet, der als Namen aber standardmäßig “screenlog-<heutiges-datum>” heißen soll (ggf. mit Präfix ein anderer Name.

Darum …

16.1.1. Erst mal Proof-of-Concept

Wenn man alle notwendigen Elemente per Makro zusammenstellen kann, dann hat man von Anfang an das richtige Konzept und automatisch auch die richtigen Befehle, die man dann in einerm interaktiven Kommando benötigt.

Wie man einen Shell-Buffer bekommt, weißt du ja schon. Es geht mit M-x shell RET. Damit ist auch klar, dass man die Funktion shell benötigt.

Erst mal sehen, was die Funktion so anbietet. Mit C-h f shell RET wirst du dabei fündig (siehe figure 16.1).

shell is an interactive autoloaded compiled Lisp function in
‘shell.el’.

(shell &optional BUFFER)

Run an inferior shell, with I/O through BUFFER (which defaults to ‘*shell*’).
Interactively, a prefix arg means to prompt for BUFFER.
If ‘default-directory’ is a remote file name, it is also prompted
to change if called with a prefix arg.

If BUFFER exists but shell process is not running, make new shell.
If BUFFER exists and shell process is running, just switch to BUFFER.

...

figure 16.1 Hilfe zur Funktion shell

Und siehe! Die Funktion macht prinzipiell schon alles, was du brauchst.

16.1.2. Der Makro

  1. Beim interaktiven Aufruf mit Präfixargument C-u M-x shell wirst du nach dem Namen für den Buffer gefragt, das ist schon die halbe Miete.

  2. Mit C-c u z f werden aktuelles Datum und Uhrzeit in günstigem Format für Dateinamen eingefügt:

    -20220614-175909
    

    Damit bekommst du die gewünschte Zeit. Das zugehörige Kommando erfährst du mit C-h k C-c u z f:

    C-c u z f runs the command ws-time-insert-filename-timestamp-string
    (found in global-map), which is an interactive autoloaded Lisp closure
    in ‘ws-time.el’.
    
    It is bound to C-c u z f.
    
    (ws-time-insert-filename-timestamp-string &optional DATE-TO-USE)
    
    Insert DATE-TO-USE formatted as filename timestamp string at point.
    The time string format defined by
    ‘ws-time-filename-timestamp-format’ is applied.
    When called interactively with prefix arg, DATE-TO-USE is
    prompted for.  Otherwise, the current date and time is used.
    

    Note

    Als DATE-TO-USE kann man Dinge wie yesterday oder tomorrow oder auch 3 days ago und 6 days angeben. Das wäre doch schon eine nette Zusatzfunktion:

    • Mit negativem Argument wird ein Datum in der Vergangenheit benutzt.
    • Mit positivem Argument wird ein Datum in der Zukunft benutzt.
  3. In zusammenfassender Kombination kannst du mit

    C-x ( C-u M-x shell screenlog C-c u z f M-BSP BSP RET C-x ),

    einen Makro definieren der deinen Anforderungen genügt.

  4. Mit M-x name-last-kbd-macro dax-screenlog-shell RET und mif M-x insert-keyboard-macro dax-screenlog-shell RET gibts dann wie gehabt den Makro als Tastenvektorkommando. D.h., es ist interaktiv mit M-x dax-screenlog-shell ausführbar:

    (fset 'dax-screenlog-shell
       [?\C-u ?\M-x ?s ?h ?e ?l ?l return ?s ?c ?r ?e ?e ?n ?l ?o ?g ?\C-c ?u ?z ?f M-backspace backspace return])
    

Damit sind die grundsätzlichen Anforderung erfüllt.

16.1.3. Die Funktion

Warning

Wenn du keine Lust hast, diese Vorarbeiten und Recherchen per eingebauter Hilfe zu betreiben, dann brauchst du nicht mit der Lisp-Programmierung anfangen. Das Ergebnis wäre ähnlich der Shell-Programmierung ohne Man-Pages. Das Rum-Googeln bringt im Ergebnis auch nur den Hinweis was man verwenden kann und dann für Genauers in der Hilfe suchen muss. Kann man sich ziemlich oft sparen und gleich in den lokal vorhandenen Beispielen nachsehen.

Note

Wenn du in den Verzeichnissen /usr/share/emacs/, ~/.emacs.def /usr/local/share/emacs/site-lisp/ mit grep-find nach (shell[ )] suchst, findest du z.B. alle Aufrufe der Funktion shell. Dabei wird dann schnell klar, wie man den Buffer-Namen beeinflusst.

Note

Nach dem Ausführen eines Kommandos mit M-x, kann man in der Liste der interaktiv ausgewerteten Befehle C-x ESC ESC nachschauen, wie die Aktion in einem Programm umgesetzt wird.

16.1.3.1. Was muss man haben

Wie die Makroerstellung gezeigt hat, werden für das fancy-schmanzy Kommando also die folgenden Funktionen benötigt:

  • shell, Syntax (aus der Hilfe zur Funktion):

    (shell &optional BUFFER)
    
  • ws-time-insert-filename-timestamp-string, Syntax (aus der Hilfe zur Funktion):

    (ws-time-insert-filename-timestamp-string &optional DATE-TO-USE)
    

Für einen eventuellen Präfix ist ein optionales Argument nötig.

Note

Ein Kommando kann im Gegensatz zu einer einfachen Funktion interaktiv ausgeführt werden. D.h., es ist mit M-x aufrufbar. Und mit M-x global-set-key RET kann es einer Tasstenfolge zugewiesen werden. Sowohl Kommandos als auch Funktionen werden mit defun definiert. Der Unterschied ist subtil. Ohne Cut-Past-Modify geht die Sache mit Sicherheit schief.

16.1.3.2. Welche Vorlage benutzt man?

Als Vorlage nimmt man am besten ein Kommando, das bereits ein optionales Argument unterstützt. shell ist in diesem Fall so ziemlich der offensichtlichste Kandidat :-;

Wo finde ich also die Definition von shell? Natürlich mit der Hilfe: C-h f shell RET (siehe figure 16.1). Dort ist der Ort der Definition als Hyperlink angegeben (shell.el). Also eifrig Link aktivieren, die Funktion kopieren und die Dokumentation schrumpfen. Dann noch alles, außer der Deklaration interactive fort schmeißen:

(defun shell (&optional buffer)
  "Run an inferior shell, with I/O through BUFFER (which defaults to `*shell*').
Interactively, a prefix arg means to prompt for BUFFER."
  (interactive
   (list
    (and current-prefix-arg
         (prog1
             (read-buffer "Shell buffer: "
                          ;; If the current buffer is an inactive
                          ;; shell buffer, use it as the default.
                          (if (and (eq major-mode 'shell-mode)
                                   (null (get-buffer-process (current-buffer))))
                              (buffer-name)
                            (generate-new-buffer-name "*shell*")))
           (if (file-remote-p default-directory)
               ;; It must be possible to declare a local default-directory.
               ;; FIXME: This can't be right: it changes the default-directory
               ;; of the current-buffer rather than of the *shell* buffer.
               (setq default-directory
                     (expand-file-name
                      (read-directory-name
                       "Default directory: " default-directory default-directory
                       t nil))))))))
  ;; [ ... hier war mal was ... ]
  )

16.1.3.3. Umgang mit Klammerausdrücken

Ohne korrekte Anwendung der Hilfsmittel, ist Lisp eher im BDSM-Bereich angesiedelt.

Note

Lisp besteht vollständig aus geklammerten Ausdrücken (siehe S-expressions). Emacs bietet reichlich Unterstützung für deren Handhabung.

Die wichtigsten Tastenfolgen sind (C-h b, in den Hilfe-Buffer wechseln, dann M-x occur RET sexp RET):

C-M-f     forward-sexp
C-M-b     backward-sexp
C-M-SPC   mark-sexp
C-M-k     kill-sexp
C-x C-e   eval-last-sexp

und zusätzlich:

M-(       insert-parentheses
M-)       move-past-close-and-reindent

sowie (C-h b, in den Hilfe-Buffer wechseln, dann M-x occur RET list$ RET):

C-M-n     forward-list
C-M-p     backward-list
C-M-d     down-list
C-M-u     backward-up-list

Um einen Klammerausdruck zu löschen ist es also sinnvoller auf der öffnenden Klammer C-M-SPC BSP zu drücken (oder auch C-M-k, wenn es in den kill-ring soll), als mühsam per Cursor-Steuerung das Ende zu suchen.

16.1.3.4. Erst mal die absoluten Basics

Die interactive Deklaration ist viel zu komplex, daher wird sie erst mal auf (interactive (list)) reduziert (C-M-k auf der öffnenden Klammer von (and):

(defun shell (&optional buffer)
  "Run an inferior shell, with I/O through BUFFER (which defaults to `*shell*').
Interactively, a prefix arg means to prompt for BUFFER."
  (interactive
   (list
   ))
  ;; [ ... hier war mal was ... ]
  )

Jetzt braucht das Kind noch den richtigen Namen, und es wird schon mal die Funktion shell aufgerufen. Der zukünftige Buffer-Name wird schon mal als lokale Variable buffer-name angelegt:

(defun dax-screenlog-shell (&optional buffer)
  "Run an inferior shell, with I/O through BUFFER (which defaults to `*shell*').
Interactively, a prefix arg means to prompt for BUFFER."
  (interactive (list))
  (let (buffer-name)
    (setq buffer-name nil)
    (shell buffer-name)
   ))
;; (dax-screenlog-shell)

Jezt macht das Kommando dax-screenlog-shell bereits das gleiche wie das Kommando shell ohne Präfixargument.

16.1.3.5. Butter bei die Fische

Als nächstes wird die Zusammenstellung des Buffer-Namens im Grundsatz umgesetzt. Der Name besteht aus einem Präfix, einem Bindestrich und dem Tag. Buffer ohne Dateizuweisung werden außerdem im Emacs mit einem führenden und abschließenden * gekennzeichnet. Der Aufruf von shell wird zum Testen erst mal auskommentiert. Der Rückgabewert ist das Ergebnis des letzten Ausdrucks, der ausgewertet wird. In diesem Fall ist das der Buffer-Name.

Die Funktion wird definiert, indem man nach der letzten Kommentarzeile C-x C-e drückt. Der Rückgabewert wird im Statusbereich angezeigt, wenn man am Ende der letzten Kommentarzeile C-x C-e drückt. Beim aktuellen Stand erscheint hier "*screenlog-*".

(defun dax-screenlog-shell (&optional buffer)
  "Run an inferior shell, with I/O through BUFFER (which defaults to `*shell*').
Interactively, a prefix arg means to prompt for BUFFER."
  (interactive (list))
  (let (buffer-name prefix date)
    (setq prefix "screenlog")
    (setq date nil)
    (setq buffer-name (concat "*" prefix "-" date "*"))
    ;; (shell buffer-name)
    buffer-name
   ))
;; (dax-screenlog-shell)

Wir wissen bereits, dass ws-time-insert-filename-timestamp-string die aktuelle Zeit einfügt. Da das Einfügen aber nicht gewünscht ist, kann die Funktion nicht direkt verwendet werden. Wenn man sich zur Definition begibt, sieht man, dass das Ergebnis der Funktion ws-time-filename-timestamp-string mit insert eingefügt wird:

(insert (ws-time-filename-timestamp-string date-to-use))

Es liegt also nahe, ws-time-filename-timestamp-string direkt zu verwenden. C-x C-e am Ende jeder Kommentarzeile zeigt das Ergebnis der entsprechenden Auswertung:

;; "-20220614-195517"       <= (ws-time-filename-timestamp-string nil)
;; ("" "20220614" "195517") <= (split-string (ws-time-filename-timestamp-string nil) "-")
;; ("20220614" "195517")    <= (cdr (split-string (ws-time-filename-timestamp-string nil) "-"))
;; "20220614"               <= (car (cdr (split-string (ws-time-filename-timestamp-string nil) "-")))

Der letzte Ausdruck liefert wie gewünscht das Datum. und kann direkt in die Funktion dax-screenlog-shell eingefügt werden:

(defun dax-screenlog-shell (&optional buffer)
  "Run an inferior shell, with I/O through BUFFER (which defaults to `*shell*').
Interactively, a prefix arg means to prompt for BUFFER."
  (interactive (list))
  (let (buffer-name prefix date other-date)
    (setq prefix "screenlog")
    (setq date (car (cdr (split-string (ws-time-filename-timestamp-string other-date) "-"))))
    (setq buffer-name (concat "*" prefix "-" date "*"))
    ;; (shell buffer-name)
    buffer-name
   ))
;; (dax-screenlog-shell)

Die Prüfung des erzeugten Buffer-Namens zeigt, dass das erste Ziel damit erreicht ist. Daher kann der Aufruf von shell wieder aktiviert werden:

(defun dax-screenlog-shell (&optional buffer)
  "Run an inferior shell, with I/O through BUFFER (which defaults to `*shell*').
Interactively, a prefix arg means to prompt for BUFFER."
  (interactive (list))
  (let (buffer-name prefix date other-date)
    (setq prefix "screenlog")
    (setq date (car (cdr (split-string (ws-time-filename-timestamp-string other-date) "-"))))
    (setq buffer-name (concat "*" prefix "-" date "*"))
    (shell buffer-name)
    buffer-name
   ))
;; (dax-screenlog-shell)

Nach der Neudefinition präsentiert der Aufruf von M-x dax-screenlog-shell RET eine Shell mit dem gewünschten Namen. Weitere Aufrufe zeigen den existierenden Buffer wieder an und aktivieren ihn.

16.1.3.6. Das Sahnehäubchen

Dann wäre da noch das Präfixargument. In unserem Fall eine positive oder negative Zahl. Weiterhin muss Die Dokumentation angepasst werden und ein paar Optimierungen schaden auch nicht.

(defun dax-screenlog-shell (&optional offset-days)
  "Run an inferior shell with `*screenlog-<date>*' as buffer name.
Interactively, a numeric prefix arg means to add or subtract
OFFSET-DAYS to/from current date."
  (interactive (list (if current-prefix-arg
                         (prefix-numeric-value current-prefix-arg))))
  (let (buffer-name prefix date other-date)
    (cond
     ((and offset-days (< offset-days 0))
      (setq other-date (format "%d days ago" (abs offset-days))))
     ((and offset-days (> offset-days 0))
      (setq other-date (format "%d days" offset-days))))
    (setq prefix "screenlog")
    (setq date
          (cadr (split-string (ws-time-filename-timestamp-string other-date)
                              "-")))
    (setq buffer-name (concat "*" prefix "-" date "*"))
    (shell buffer-name)
    buffer-name))
;; (dax-screenlog-shell)
;; (dax-screenlog-shell -3)
;; (dax-screenlog-shell 5)