blog · git · desktop · images · contact & privacy · gopher


Vim: Pipes in „:substitute“ nutzen

Vim kann ja grundsätzlich gut mit externen Programmen umgehen. So kann man zum Beispiel eine Datei sortieren lassen:

:%!sort

Dabei wird die ganze Datei ("%") einmal durch "sort" gepiped ("!"). Oder man markiere einen Bereich im visuellen Modus und gebe dann ein (den Bereich fügt er selbst ein):

:'<,'>!column -t

Damit würde aus

hi Statement ctermfg=130 cterm=bold
hi Identifier ctermfg=19 cterm=bold
hi Type ctermfg=70 cterm=bold

das hier

hi  Statement   ctermfg=130  cterm=bold
hi  Identifier  ctermfg=19   cterm=bold
hi  Type        ctermfg=70   cterm=bold

werden.

Äußerst praktisch wird es aber dadurch, dass man auch im "s"-Befehl (":substitute") Pipes nutzen kann. Es ist allerdings nicht gerade offensichtlich. Die Basics des Befehls dürften bekannt sein, das hier ersetzt in der Datei alle Vorkommen von "foo" durch "bar":

:%s/foo/bar/g

Beginnt das Ersetzungspattern jedoch mit "\=", dann kann eine Expression folgen, deren Rückgabewert als Ersetzung verwendet wird -- hier wird jedes "__date__" durch das aktuelle Datum ersetzt:

:%s/__date__/\=strftime("%F")/g

Und nun kommt die Pipe ins Spiel. Die Funktion "system()" kann einen externen Befehl ausführen und seine Ausgabe auffangen. Als zweites Argument kann man einen String angeben, den das Programm von STDIN lesen kann. Innerhalb eines "s"-Befehls kann man hierzu "submatch()" nutzen, um den Treffer abzufragen. Fertig ist die Pipe. Als einfaches, akademisches Beispiel diene folgendes, was jedes Auftreten des Strings "Ümläute" einmal durch "base64" jagt:

:%s/Ümläute/\=system("base64", submatch(0))/g

"submatch(0)" ist der komplette Treffer des Musters. Verwendet man im Muster Gruppierungen, dann kann man hier auswählen, welche Gruppe man gerne hätte.

Zum Abschluss noch ein Real-Life-Beispiel. Hiermit kann man Befehle über explain erklären, indem bestimmte Bereiche des Textes einmal durch das Skript geschickt werden. Sie beginnen mit "<<<explain" und enden mit ">>>", jeweils in einer einzelnen Zeile, dazwischen die normale explain-Notation:

:%s/\n\@<=<<<explain\n\(\_.\{-}\n\)>>>\n/\=system("explain.py", submatch(1))/g
 |  \__________________________________/ \______/ \__________/  \_________/  |
 |                    |                      |          |            |       \- Alles.
 |                    |                      |          |            |
 |                    |                      |          |            \- Wird zu STDIN
 |                    |                      |          |               für das
 |                    |                      |          |               Programm. Hier
 |                    |                      |          |               der erste
 |                    |                      |          |               Submatch des
 |                    |                      |          |               Ausdrucks vorne.
 |                    |                      |          |
 |                    |                      |          \- Zu startendes Programm.
 |                    |                      |
 |                    |                      \- Nutze einerseits zur Ersetzung eine
 |                    |                         Expression ("\=") und andererseits den
 |                    |                         system()-Befehl, um den Text durch eine
 |                    |                         Pipe zu jagen.
 |                    |
 |                    \- Das Muster für den explain-Block: Eingeleitet wird er durch ein
 |                       "<<<explain" in einer einzelnen Zeile, dann der explain-Inhalt
 |                       und abgeschlossen wird es durch ein ">>>" in einer einzelnen
 |                       Zeile.
 |
 \- Ersetze in der gesamten Datei.

Ich persönlich habe mir das in eine Funktion gesteckt und zwei Mappings angelegt:

fun! DoExplainFile(...)
    " Merke dir die alte Position des Cursors. Führe dann die Ersetzung
    " durch.
    let l:prev = getpos(".")
    %s/\n\@<=<<<explain\n\(\_.\{-}\n\)>>>\n/\=system("explain.py", submatch(1))/g

    " Dauerhaft? Oder nur yanken? Im Falle eines optionalen Arguments
    " mit Wert "yank" kopierst du den Inhalt und machst die Ersetzung
    " dann rückgängig.
    if a:0 > 0 && a:1 == "yank"
        silent %y
        u
    endif

    " Cursor wieder an die alte Stelle zurück, Highlight des
    " Suchtreffers abschalten.
    call setpos(".", l:prev)
    noh
endfun

nmap <Leader>e :call DoExplainFile("yank")<CR>
nmap <Leader>E :call DoExplainFile()<CR>

"<Leader>e" führt die Ersetzung durch, kopiert danach die ganze Datei in die Zwischenablage und macht dann die Ersetzung wieder rückgängig. Ist zum Beispiel für Blogposts praktisch: Der explain-Quelltext bleibt erhalten und man hat die verarbeitete Version im Clipboard, sodass man sie im Browser einfügen kann. "<Leader>E" dagegen macht die Ersetzung im Dokument selbst, dauerhaft. So entstand auch dieses Posting. ;)