Literate Programming ctags-Unterstützung für Noweb-Markdown CoMet-Arbeitsberichte Meik Teßmer* 2015 Bereich Computergestützte Methoden (� CoMet) Version: 0.1.3 ctags hil bei der Navigation in ellcode- und textbasierten Dateien. Von Haus aus werden viele Sprachen und Markup-Formate unterstützt, allerdings nicht die Kombination Noweb-Markdown. Das hier vorgestellte Werkzeug behebt diesen Mangel, indem es eine ctags-kompatible Ausgabe erzeugt, die eine Integration mit dem Editor Vim über das Tagbar-Plugin ermöglicht. Inhaltsverzeichnis 1 Von Noweb zu Markdown zu ctags 1.1 Navigation mit Outlines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Integration mit Vim über das Tagbar-Plugin . . . . . . . . . . . . . . . . . . . 2 2 2 2 Das Format einer ctags-kompatiblen tags-Datei 3 3 Relevante Informationen für die tags-Datei 4 4 Erstellung der tags-Datei 4.1 Verarbeiten der Eingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Iteratoren und Generatorfunktionen in Python . . . . . . . . . . . . . . . . . . 4.3 Ein Iterator ür die Generation von Tags ür Code- und Dokumentation-Chunks 4.4 Die Klasse Tag und die dazu gehörende Factory-Funktion . . . . . . . . . . . . 4.5 Die Suche nach Code-Chunk-Definitionen . . . . . . . . . . . . . . . . . . . . 4.6 Test auf Vorhandensein einer Dokumentation-Chunk-Definitionen . . . . . . 4.7 Die Suche nach Absatzüberschrien . . . . . . . . . . . . . . . . . . . . . . . . 4.8 Zusammenügen der Lösung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 5 7 8 8 9 9 *[email protected] 1 5 Die Ergebnisdatei 10 6 Lizenz 10 7 Release Notes 11 8 Code Chunks 12 1 Von Noweb zu Markdown zu ctags 1.1 Navigation mit Outlines Viele Editoren helfen bei der Navigation in großen Dokumenten durch sog. Outlines, eine Baumstruktur, die den Textauau übersichtlich darstellt. Das Werkzeug Exuberant ctags, dessen Hauptaufgabe in der Erstellung eines Indexes von Sprachelementen aus ellcode-Dateien besteht, kann auch ür die Erstellung einer solchen Übersicht benutzt werden und kommt daher bei vielen dieser Editoren zum Einsatz. ctags unterstützt laut Website 41 Programmiersprachen (Stand: 2015-03-05) und kann relativ einfach um zusätzliche Sprachen erweitert werden. Literate Programme (zu Literate Programming siehe [Knuth1984 ]) stellen ür das Werkzeug jedoch eine nicht zu überwindende Hürde dar, denn es kann den Unterschied zwischen Code- und Dokumentation-Chunk nicht erkennen. nw2md2ctags erstellt eine zu ctags kompatible tags-Datei aus einem literaten Programm, dessen Dokumentation-Chunks in Markdown-Markup verfasst sind. Dabei wird sowohl die klassische noweb-Syntax akzeptiert als auch die mit nw2md eingeührte erweiterte Syntax. 1.2 Integration mit Vim über das Tagbar-Plugin kann als Ergänzung zu ctags im Vim-Plugin Tagbar benutzt werden. Dazu ist folgender Eintrag in der Vim-Konfigurationsdatei ~/.vimrc nötig: nw2md2ctags let g:tagbar_type_nw2md = { \ ’ctagstype’: ’nw2md’, \ ’ctagsbin’ : ’/path/to/nw2md2ctags’, \ ’ctagsargs’ : ’’, \ ’kinds’ : [ \ ’s:Abschnitte’, \ ’c:Code Chunks’ \ ], \ ’sort’: 0, \ } 2 Damit das Plugin die Ergänzung erkennt, muss Vim via filetype=nw2md mitgeteilt werden, dass es sich um den mit let g:tagbar_type_nw2md angegebenen Dateityp nw2md handelt. Eine sog. Modeline am Anfang oder Ende des literaten Programms erledigt das automatisch: .. vim:filetype=nw2md 2 Das Format einer ctags-kompatiblen tags-Datei Das ür unsere Zwecke erforderliche Dateiformat der tags-Datei ist zeilenweise aufgebaut. Jede Zeile besteht aus folgenden Spalten: tagname<TAB>tagfile<TAB>ex_cmd;”<TAB>field .. Ein Beispiel: markers nw2md2ctags.nw /^# markers$/;” h Es handelt sich hierbei um das Format Version 2, mit dem ctags das Standardformat erweitert. Es ist abwärtskompatibel mit Vi und wird nur von neueren ctags-Versionen wie Exuberant ctags unterstützt. Bezeichner, bspw. der Name einer Funktion. Er darf Leerzeichen enthalten, aber keinen Tabulator. Der Bezeichner kann so auereitet werden, dass bspw. eine Gliederungshierarchie sichtbar wird: Überschrien erster Stufe werden nicht eingerückt, die zweiter Stufe um zwei Leerzeichen, die drier Stufe um 4 Leerzeichen. tagname TAB Ein Tabulator-Zeichen. Der Name der Datei, in der der Bezeichner steht. Die Pfadangabe kann absolut oder relativ sein und kann Umgebungsvariablen und Wildcards enthalten (wobei Letzere nicht verwendet werden sollten). Der Name darf kein Tabulator-Zeichen enthalten. tagfile ex_cmd ;” Der Ex-Befehl, der den Cursor an die Stelle des Bezeichners positioniert. Das Semikolon und die doppelten Anührungszeichen leiten ür Vi den Beginn eines Kommentars ein. Alle folgenden Zeichen werden also von alten Vi-Versionen nicht beachtet. Eine Liste optionaler Schlüssel-Wert-Paare, getrennt durch ein Tabulator-Zeichen Felder. Jedes Feld folgt der Form field .. <TAB>{fieldname}:{value} Der Feldname identifiziert das Feld und darf nur aus Buchstaben bestehen ([a-zA-Z]). Der Wert kann eine beliebige Zeichenkee ohne Tabulator-Zeichen sein. Folgende Zeichen werden speziell behandelt: 3 • • • • \t: Tabulator-Zeichen \r: Carriage Return \n: Newline \\: einzelner Backslash Es gibt ein Feld, das von dieser Regel ausgenommen ist. Das kind-Feld wird so behandelt, als ob der Schlüssel kind: davor stünde (im obigen Beispiel ist dies das Feld h). Das Feld line: gibt an, in welcher Zeile sich das Tag befindet. Mit speziellen Zeilen können zusätzliche Angaben bspw. zur Sortierung oder zur Kodierung gemacht werden. Die nachfolgende Zeile legt die Kodierung auf UTF-8 fest: !_TAG_FILE_ENCODING<TAB>utf-8<TAB>{anything} ~ 3 Relevante Informationen für die tags-Datei Die tags-Datei soll Code Chunks und Abschnisüberschrien auühren. Wir benötigen dazu • die Namen aller Code Chunks, • alle Abschnisüberschrien (zunächst im einfacher zu verarbeitenden Atx-Format) • die Zeilennummern der jeweiligen Tags Die Einträge ür das kind-Feld lauten entsprechend: • • ür Code Chunks s ür Abschnie (Sections) c Der Ex-Befehl ist üblicherweise ein regulärer Ausdruck, der auf das gesuchte Tag passt. Da es aber durchaus Code Chunks mit gleichem Namen gibt, brauchen wir zusätzlich die Zeilennumer. Eine Beispiel-Datei könnte dann aussehen wie folgt: . Die Suche nach Definitionen nw2md2ctags.nw /^## Die Suche nach Definitionen$/;” . Generatorfunktionen nw2md2ctags.nw . Reader nw2md2ctags.nw <<Definitionen>>= /^## Reader$/;” s nw2md2ctags.nw <<Generator-Beispiel>>= /^## Generatorfunktionen$/;” line:2 line:87 /^<<Definitionen>>= (python)$/;” c line:99 nw2md2ctags.nw /^<<Generator-Beispiel>>= (python)$/;” c 4 Erstellung der tags-Datei 4.1 Verarbeiten der Eingabe Die Verabeitung von Textdateien erfolgt in Python o nach folgendem Schema: Durchlaufe die Eingabe zeilenweise mit Hilfe einer for-Schleife und verarbeite jede Zeile separat. Das fileObjekt liefert der Schleife dazu einen Iterator. Für die Erstellung der tags-Datei interessiert uns aber nicht jede Zeile, sondern wir benötigen Chunks. Warum also nicht einen Generator konstruieren, der an Stelle eines Zeileniterators einen Chunk-Iterator bereitstellt? 4 s s line:56 line:191 4.2 Iteratoren und Generatorfunktionen in Python Mit PEP 234 (ab Python Version 2.2) erwarten for-Schleifen keine Sequenz mehr, sondern einen Ausdruck, der als Ergebnis des Aufrufs iter() einen Iterator zurückgibt. Ein Iterator stellt eine Funktion bereit, die ür eine Schleife „das nächste Element“ bereitstellt bzw. None, falls es kein nächstes Element gibt. Eine for-Schleife kann so mit allen Objekten arbeiten, die eine entsprechende Iterator-Funktion vorhalten. Ein Generator erzeugt einen solchen Iterator (s. PEP 255, ebenfalls ab Version 2.2.). Anders als herkömmliche Funktionen verwendet eine Generator-Funktion an Stelle von return das Schlüsselwort yield ür die Rückgabe eines Elements. Der Funktionszustand inkl. lokaler Variablen wird bis zum nächsten Aufruf des Iterators zwischengespeichert. Ein Beispiel: <Generator-Beispiel>= # Verwendung des zurückgegebenen Iterators for value in generator_funktion(): print(value) # oder auch: a, b, c = generator_funktion() def generator_funktion(): yield 1 yield 2 yield 3 > 1 > 2 > 3 An Stelle einer einfachen Rückgabe gibt ein Generator ein Objekt zurück, das dass IteratorProtokoll unterstützt und damit bspw. in for-Schleifen verwendet werden kann. 4.3 Ein Iterator für die Generation von Tags für Code- und Dokumentation-Chunks Der Generator erhält ein file object als Lesequelle. <Tag-Generator>= def chunks(source): ”””read lines from source and yield either code or doc chunks ””” Der Literate Programming-Ansatz beschreibt eine beliebige Abfolge von Code- und Dokumentation-Chunks. Der Generator muss daher ür jede zu verarbeitende Zeile festhalten, zu welcher Chunk-Art sie gehört bzw. in welchem Zustand sich der Generator 5 befindet. Das Flag in_code_chunk übernimmt diese Aufgabe. Standardmäßig wird mit einem Dokumentation-Chunk begonnen. <Tag-Generator 2>= in_code_chunk = False line_no = 0 for line in source: line_no += 1 Zunächst wird aktuelle Zeile nach einer möglichen Code-Chunk-Definition untersucht. Ist die Suche erfolgreich, wird mit der Factory-Funktion create_tag eine Tag-Instanz erzeugt und miels yield an die übergeordnete for-Schleife zurückgegeben. <Tag-Generator 3>= # look for a code chunk definition match = search_for_code_chunk_definition(line) if match: in_code_chunk = True #chunk_name = match[0] yield create_tag(line, line_no, ”c”) continue Ist in der Zeile keine Code-Chunk-Definition zu finden, wird nach einem DokumentationChunk gesucht. Ist die Suche erfolgreich, muss der Zustand des Generators auf geändert werden, da wir uns nicht mehr in einem Code Chunk befinden. Alle Eingabezeilen, die ab jetzt verarbeitet werden, gehören zu einem Dokumentation-Chunk und müssen auf Abschnisüberschrien hin untersucht werden (suche Absatzüberschrien). Für gefundene Überschrien werden Tag-Instanzen erzeugt und zurückgegeben. <Tag-Generator 4>= # is the a new doc chunk? if contains_doc_chunk_header(line): in_code_chunk = False # check for markdown markup, only if we are in_code_chunk if not in_code_chunk: # search for headers match = search_for_header(line) if match and not in_code_chunk: #yield Tag(line.strip(), ”s”) yield create_tag(line, line_no, ”s”) continue # maybe search for more markup? # ... 6 4.4 Die Klasse Tag und die dazu gehörende Factory-Funktion Die Klasse Tag steht ür einen Eintrag in der zu generierenden tags-Datei. Sie hält den ChunkNamen, die Art des Eintrags (kind) und die zugehörige Eingabezeile vor und gibt bei der Ausgabe als Zeichenkee eine zum tags-Format kompatible Repräsentation zurück. Damit kann durch eine simple print-Anweisung ein tags-Eintrag erzeugt werden. Da das tags-Format zusätzlich den Namen der zugehörigen Eingabedatei benötigt, wird zusätzlich eine Klassenvariable namens filename definiert, die vor Erzeugung der ersten Instanz passend belegt werden muss <Tag-Klasse>= class Tag: ”””A tags file entry ””” filename = None def __init__(self, name, kind, line, line_no): self.name = name self.kind = kind self.line = line self.line_no = line_no def __repr__(self): return ”{0}\t{1}\t/^{2}$/;\”\t{3}\tline:{4}”.format(self.name, self.filename, self.line, self.kind, self.line_no) Die Factory-Funktion übernimmt die Erzeugung von Tag-Instanzen und bereitet die Werte entsprechend der kind-Angabe auf. Hier werden bspw. die Doppelkreuze der Abschnisüberschrien durch Punkte ersetzt. Newline muss vom Ende entfernt werden. <Tag-Factory-Funktion>= def create_tag(line, line_no, kind): # remove newline char line = line[:-1] if kind == ”s”: # replace # with spaces name = line.replace(”#”, ”.”) return Tag(name, kind, line, line_no) elif kind == ”c”: return Tag(line, kind, line, line_no) else: # Error 7 return None 4.5 Die Suche nach Code-Chunk-Definitionen Der Generator ist „abwärtskompatibel” zur klassischen Noweb-Syntax: <<Code-Chunk-Bezeichner>>= Er verarbeitet aber vorrangig die mit nw2md (Link?) eingeührte Erweiterung um die Angabe der verwendeten Programmiersprache: <<Code-Chunk-Bezeichner>>= (python) Sämtliche durch pandoc bzw. Pygments unterstützte Sprachen können angegeben werden. Ist die Suche erfolgreich, wird ür die erweiterte Syntax das Paar (Chunk-Name, Sprache) zurückgegeben, ür die alte (Chunk-Name, None); bei erfolgloser Suche ist die Rückgabe None. <suche nach Code-Chunk-Definitionen>= def search_for_code_chunk_definition(line): ”””look for code chunk definition and return chunk name””” # look for annotated code chunk header, e.g. <<code>>= (make) match = re.match(open_mark + ”([^>]+)” + close_mark + ”\s*\((\w+)\)”, line) if not match: # ok, use normal noweb syntax match = re.match(open_mark + ”([^>]+)” + close_mark, line) if match: return (match.group(1), None) else: return None else: return (match.group(1), match.group(2)) 4.6 Test auf Vorhandensein einer Dokumentation-Chunk-Definitionen Die Funktion sucht nach dem Marker ür Dokumentation-Chunks und gibt entsprechend einen boolschen Wert zurück. <enthält die Zeile eine Dokumentation-Chunk-Definition>= def contains_doc_chunk_header(line): match = re.match(doc_chunk_marker + ”[ ]?”, line) if match: return True else: return False 8 4.7 Die Suche nach Absatzüberschrien Diese Funktion sucht nach den definierten Überschrien (header_1 bis header_3) und gibt sie bei erfolgreicher Suche zurück. <suche nach Absatzüberschriften>= def search_for_header(line): for marker in [header_1, header_2, header_3]: match = re.match(marker + ” \s*(.+)\s*$”, line) if match: return (match.group(1)) return None 4.8 Zusammenfügen der Lösung Zunächst müssen wir noch die verwendeten Konstanten definieren, bevor die Gesamtlösung zusammengestellt werden kann. <Definitionen von Konstanten>= # markers of code chunk definitions open_mark = ”<<” close_mark = ”>>=” doc_chunk_marker = ”^@” # markers for headers header_1 = ”#” header_2 = ”##” header_3 = ”###” Das Hauptprogramm setzt zunächst die Klassenvariable filename auf den Dateinamen der Eingabedatei. Anschließend wird diese geöffnet und mit einer for-Schleife der von uns konstruierte Tag-Generator ür die Extraktion von tags-Zeilen verwendet. Hier zeigt sich, wie eine eigene Generatorfunktion den Code vereinfachen kann. Die generierten Tag-Instanzen werden zunächst zwischengespeichert und anschließend ausgegeben. <Hauptprogramm>= def main(): Tag.filename = sys.argv[1] out = [] with open(Tag.filename, ”r”) as handle: 9 for chunk in chunks(handle): out.append(str(chunk)) #for line in sorted(out): for line in out: print(line) 5 Die Ergebnisdatei <nw2md2ctags>= #!/usr/bin/env python3 import re import sys __author__ = ”Meik Teßmer” __email__ = ”[email protected]” __version__ = ”0.1.3” <<Definitionen von Konstanten>> <<Tag-Klasse>> <<Tag-Factory-Funktion>> <<Tag-Generator>> <<suche nach Code-Chunk-Definitionen>> <<enthält die Zeile eine Dokumentation-Chunk-Definition>> <<suche nach Absatzüberschriften>> <<Hauptprogramm>> if __name__ == ”__main__”: main() 6 Lizenz nw2md2ctags generates a ctags-compatible output for noweb/nw2md input Copyright (C) 2015 Meik Teßmer This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 10 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. <build-script>= #!/bin/sh if [ -z ”${NOWEB_SOURCE}” ]; then NOWEB_SOURCE=nw2md2ctags.nw fi if [ -z ”${NOWEB_CODE}” ]; then NOWEB_CODE=‘pwd‘/code fi # check if we need to create target dirs [ -d ${NOWEB_CODE} ] || mkdir -p ${NOWEB_CODE} FILES=”nw2md2ctags” for f in ${FILES}; do tangle -R”$f” ”${NOWEB_SOURCE}” > ”${NOWEB_CODE}/$f” chmod u+x ”${NOWEB_CODE}/$f” done 7 Release Notes [v0.1.3] • base-build-script: deleted file. • Makefile, base-build-script, metadata.yaml, nw2md2ctags.nw, nw2md2ctags.py: – added line number to tags line for precise cursor positioning – fixed the fix ;-) – added Release Notes [v0.1.2] 11 • Makefile, nw2md2ctags.nw: removed strip • Makefile: tried some fixes for fragment generation [v0.1.1] • Makefile, nw2md2ctags.nw: fixed HTML fragment generation and removed link placeholder to nw2md [v0.1.0] • Makefile, nw2md2ctags.nw, nw2md2ctags.py: relase 0.1.0 8 Code Chunks • Definitionen von Konstanten • Generator-Beispiel • Hauptprogramm • Tag-Factory-Funktion • Tag-Generator – – – – Tag-Generator Tag-Generator 2 Tag-Generator 3 Tag-Generator 4 • Tag-Klasse • build-script • enthält die Zeile eine Dokumentation-Chunk-Definition • nw2md2ctags • suche nach Absatzüberschrien • suche nach Code-Chunk-Definitionen 12