Package: jml
Java Musterlösung (kurzjml) ist ein kleines Skript mit dem mehrere Projektversionen aus einer Musterlösung generiert werden können.jmlwurde ursprünglich für Java-Projekte entwickelt, kann aber durch diverse Optionen auch für andere Projekte genutzt werden.MotivationZur Differenzierung im Informatikunterricht erstelle ich gerne mehrere Projektversionen mit mehr oder weniger Hilfestellungen, um dem Leistungsstand der Schülerinnen und Schüler besser gerecht zu werden. Inklusive einer Musterlösung können so schnell drei bis vier Projektversionen entstehen, die parallel weiterentwickelt werden müssen.Befindet sich ein Fehler im Code, sollen die Aufgaben angepasst werden oder hat sich einfach ein Rechtschreibfehler in einen Kommentar eingeschlichen, müssen alle Projektversionen angepasst werden. Dabei habe ich schnell mal eine Version vergessen oder einen neuen Fehler eingebaut.Vor einigen Jahren habe ich den Prozess mitjmlvereinfacht. Das Skript generiert aus einer Basisversion, die mit Markierungen für Aufgaben und Lösungen versehen ist, die verschiedenen Projektversionen, die für den Unterricht nötig sind. Anpassungen sind nur noch im Basisprojekt notwendig.Beispiele zur Verwendung finden sich inmeinen Projekt-Repositoryund den
dazugehörigenProjektversionen.InstallationDie Installation wird wie gewohnt mit pip durchgeführt:$pip3installjmlBei erfolgreicher Installation ist nun dasjmlKommando verfügbar.$jml--versionjml, version 0.3.6jmlbenötigt Python 3.8 oder neuer.Manuelle Installationjmlkann auch ohnepipeinfach als Skriptdatei genutzt werden. Es werden keine externen Abhängigkeiten genutzt. Dazu die aktuelle Version der Dateijml.pyaus dem Repository kopieren und auf der Festplatte speichern. Das Skript kann dann mit Python 3 ausgeführt werden:$python3jml.py--versionjml, version 0.3.6Benutzungjmlbenötigt im einfachsten Fall den Pfad des Basisprojektes und den Pfad des Ausgabeordners:$jmlpfad/zum/Basisprojekt--outpfad/zum/ausgabeordnerNach Ausführung befinden sich inpfad/zum/ausgabeordnerdie Musterlösung und die Projektversionen.Die Inhalte und Anzahl an Versionen werden durch die Inhalte der Dateien im Basisprojekt bestimmt.Angenommen inBasisprojektliegt eine Datei mit dem NamenBeispiel.javamit folgendem Inhalt:classBeispiel{/*aufg*
    // TODO: Erstelle eine Objektvariable "zahl" vom Typ int
    *aufg*///ml*privateintzahl;//*mlpublicintadd(intpAndereZahl){/*aufg*
        // TODO: Gib die Summe aus der Objektvariablen "zahl" und "pAndereZahl" zurück.
        return 0;
        *aufg*///ml*returnzahl+pAndereZahl;//*ml}}Dann erzeugtjmldiese Ordnerstruktur inpfad/zum/ausgabeordner:pfad/zum/ausgabeordner/
├── Basisprojekt/
│   └── Beispiel.java
└── Basisprojekt_ML/
    └── Beispiel.javaInhalt vonBasisprojekt_ML/Beispiel.java:classBeispiel{privateintzahl;publicintadd(intpAndereZahl){returnzahl+pAndereZahl;}}Inhalt vonBasisprojekt/Beispiel.java:classBeispiel{// TODO: Erstelle eine Objektvariable "zahl" vom Typ intpublicintadd(intpAndereZahl){// TODO: Gib die Summe aus der Objektvariablen "zahl" und "pAndereZahl" zurück.return0;}}Mehrere VersionenAls Standard wird wie oben nur die Projektversion0erstellt.jmlkann aber auch mehrere Projekte erstellen, wenn die Aufgaben-Markierungen mit einer entsprechenden Nummer versehen werden.Angenommen dieBeispiel.javavon oben sieht so aus:classBeispiel{/*aufg*
    // TODO: Erstelle eine Objektvariable "zahl" vom Typ int
    *aufg*///ml*privateintzahl;//*mlpublicintadd(intpAndereZahl){/*aufg*
        // TODO: Gib die Summe aus der Objektvariablen "zahl" und "pSummand" zurück.
        return 0;
        *aufg*///ml*returnzahl+pAndereZahl;//*ml}/*aufg* 2
    public int sub( int pAndereZahl ) {
        // TODO: Gib die Differenz aus der Objektvariablen "zahl" und "pSummand" zurück.
        return 0;
    }
    *aufg*/}Dann werden statt der Version0die Projektversionen1und2erzeugt, da im zweiten Aufgaben-Marker eine konkrete Versionsnummer angegeben wurde:pfad/zum/ausgabeordner/
├── Basisprojekt_1/
│   └── Beispiel.java
├── Basisprojekt_2/
│   └── Beispiel.java
└── Basisprojekt_ML/
    └── Beispiel.javaEs ist auch möglich eine Markierung für mehrere Projektversionen zu nutzen:/*aufg* >1
// Taucht nur in Projektversionen nach Version 1 auf.
*aufg*//*aufg* !=2
// Taucht in allen Projektversionen außer 2 auf.
*aufg*//*aufg* <=2
// Taucht nur in Projektversionen 1 und 2 auf.
*aufg*/Seit Version 0.3.4 ist es möglich, auch einem Lösungs-Tag eine Versionsnummer wie
für Aufgaben zu geben. Dadurch wird der Inhalt zusätzlich zur Musterlösung auch in
den spezifizierten Aufgabenversionen gesetzt://ml* >1// Taucht in der Musterlösung und Versionen größer 1 auf//*mlOptionenDie Funktion vonjmlist durch eine Vielzahl von Optionen anpassbar. Die Optionen können als Kommandozeilen-Argumente übergeben, oder in Konfigurationsdateien gespeichert werden.Eine Übersicht der verfügbaren Kommandozeilen-Argumente ist mit-habrufbar$jml-hAlle Optionen (und noch eine Handvoll mehr) lassen sich auch in einer von mehreren Konfigurationsdateien festlegen.jmlsucht dazu bei jedem Start nach.jmlDateien im Basisprojekt, im Wurzelverzeichnis (--project-root) und im Home-Ordner des angemeldeten Nutzers (~/.jml).Die Konfigurationen werden dann in umgekehrter Reihenfolge geladen, die Einstellungen im Basisprojekt haben also die höchste Priorität. Sie werden nur noch von Kommandozeilen-Argumente überschrieben.Für das Beispiel oben könnte der Aufbau so aussehen:~/
├── .jml
├── pfad/zur/wurzel/
│   ├── .jml
│   └── Basisprojekt/
│       ├── .jml
│       └── Beispiel.java
└── pfad/zum/ausgabeordnerInhalt von~/.jml:[settings]task open=/*<aufgabe>task close=</aufgabe>*/solution open=//<loesung>solution close=//</loesung>Inhalt von~/pfad/zur/wurzel/.jml:[settings]zip=yesml suffix=Loesungname format={project}-{version}include=*.java,*.txtInhalt von~/pfad/zur/wurzel/Basisprojekt/.jml:[settings]task open=/*aufgabe*task close=*aufgabe*/encoding=iso-8859-1name=MaeusekampfDer Aufruf vonjmlsieht dann so aus:$jml--project-root"~/pfad/zur/wurzel""~/pfad/zur/wurzel/Basisprojekt"--out"pfad/zum/ausgabeordner"jmllädt nun zunächst~/.jmlund setzt die Start- und Endmarkierungen auf eine XML-Variante.Danach wird~/pfad/zur/wurzel/.jmlgeladen, da dies per--project-rootArgument als Wurzelverzeichnis gesetzt wurde. Für diese Projektgruppe werden ZIP-Dateien der Projektversionen erzeugt, außerdem wird das Suffix für die Musterlösung vonMLaufLoesunggeändert. Das Format der Projektnamen wird angepasst (_durch-ersetzt) und es werden auch.txtDateien nach den Aufgaben- und Lösungs-Markierungen durchsucht.Als drittes wird~/pfad/zur/wurzel/Basisprojekt/.jmlgeladen. Hier werden speziell für dieses eine Projekt die Aufgaben-Marker erneut verändert und die Datei-Codierung aufiso-8859-1(stattutf-8) festgelegt. Schließlich wird noch der Projektname aufMaeusekampffestgelgt, anstatt den OrdnernamenBasisprojektzu verwenden.Die Ausgabe sieht dann so aus (sofern die Aufgaben- und Lösungs-Markierungen inBeispiel.javaangepasst wurden):~/
├── .jml
├── pfad/zur/wurzel/
│   ├── .jml
│   └── Basisprojekt/
│       ├── .jml
│       └── Beispiel.java
└── pfad/zum/ausgabeordner/
    ├── Maeusekampf-Loesung/
    │   └── Beispiel.java
    ├── Maeusekampf-1/
    │   └── Beispiel.java
    ├── Maeusekampf-2/
    │   └── Beispiel.java
    ├── Maeusekampf-Loesung.zip
    ├── Maeusekampf-1.zip
    └── Maeusekampf-2.zipListe der OptionenDie Tabelle zeigt eine Übersicht aller Optionen, die in einer Konfigurationsdatei oder per Kommandozeilen-Argument gesetzt werden können.Optionsname in KonfigurationsdateienKommandozeilen-ArgumentBeschreibungoutput dir-o / –outLegt den Zielordner für die Ausgabe der Projektversionen fest. Beachte, dass der finale Ausgabeordner unterhalb vonoutdirabhängig von--project-rootnoch variieren kann.name-n / –nameSetzt den Namen der erstellten Projektversionen. Im Namensformat wird{project}durch den Namen ersetzt. Als Standard wird der Ordnername des Basisprojektes verwendet.name format–name-formatLegt das Format fest, nach dem die Projektversionen benannt werden. Der Wert ist ein Python-Formatierungsstring und kann die Variablen{project}für den Namen,{version}für die Versionsnummer und{date}für das aktuelle Datum enthalten. Auf diese Weise können Projekte beispielsweise mit einer Jahreszahl versehen werden (name format ={date:%Y}_{project}-v{version}). Standard ist{project}_{version}.task open-to / –tag-openSetzt die Anfangsmarkierung für Aufgaben. Die Markierung sollte nach einem öffnenden Block-Kommentar stehen, damit die Aufgabenstellung in der Basisversion auskommentiert ist. Standard ist/*aufg*.task close-tc / –tag-closeSetzt die Endmarkierung für Aufgaben. Die Markierung sollte vor einem schließenden Block-Kommentar stehen, damit die Aufgabenstellung in der Basisversion auskommentiert ist. Standard ist*aufg*/.task comment prefixDiese Option erlaubt die Modifikation der Zeilen in einer Aufgabe. Wenn die Option auf eine Zeichenkette gesetzt wird, wird in jeder Zeile in einer Aufgabe das erste vorkommen der Zeichenkette entfernt. Auf diese Weise können Aufgaben auch für Programmiersprachen, die keine Blockkommentare unterstützen, aaskommentiert werden.task comment prefix = #würde zum Beispiel Zeilenkommentare in Python entfernen. Beginnt die Zeichenkette allerdings mit einem/, dann wird in den fortgeschrittenen Modus geschaltet. Hier wird direkt ein regulärer Ausdruck und eine Ersetzung angegeben. Dazu muss der Wert das Format/regex/replace/haben./muss durch\/maskiert werden. In jeder Zeile wird dannregexdurchreplaceersetzt. Details sind in derPython Dokumentation zu regulären Ausdrückenzu finden.solution open-mlo / –ml-openSetzt die Anfangsmarkierung für Lösungen. Die Markierung sollte nach einem Zeilen-Kommentar stehen, damit die Musterlösung in der Basisversion lauffähig bleibt. Standard ist/ml*.solution close-mlc / –ml-closeSetzt die Endmarkierung für Lösungen. Die Markierung sollte nach einem Zeilen-Kommentar stehen, damit die Musterlösung in der Basisversion lauffähig bleibt. Standard ist//*ml.solution comment prefixWietask comment prefixfür Lösungen.solution suffix-mls / –ml-suffixSetzt die Versionsnummer der Musterlösung. Standard istML.clear–no-clearVerhindert, dass die Ordner der Projektversionen zuerst vollständig gelöscht werden. Vorhandene Dateien werden dann überschrieben, aber Dateien, die nicht im Basisprojekt sind (oder in den Excludes stehen) werden nicht berührt und verbleiben in den Projektversionen.project root–project-rootSetzt das Wurzelverzeichnis, anhand dessen die Ordnerstruktur im Zielordner festgelegt wird. Das Verzeichnis sollte ein Elternverzeichnis des Basisprojektes sein. Im Wurzelverzeichnis wird außerdem nach einer.jmlDatei gesucht, die vor der Konfigurationsdatei im Basisprojekt geladen wird.include-i / –includeSetzt die Liste derSuchmuster für Dateien, in denen nach Aufgaben- und Lösungs-Markierungen gesucht werden soll. Standard ist*.java.+includeAuftauchen und ergänzt die Liste der Includes um weitere Suchmuster, anstatt sie zu ersetzen.-includeAuftauchen und entfernt Suchmuster aus der Liste der Includes.exclude-e / –excludeSetzt die Liste derSuchmuster für Dateien, die komplett ignoriert werden soll. Diese Dateien tauchen nicht in den Projektverisonen auf. Excludes haben Vorrang vor Includes. Standard ist*.class,*.ctxt,.DS_Store,Thumbs.db,.vscode,.eclipse,*.iml.+excludeErgänzt die Liste der Excludes um weitere Suchmuster, anstatt sie zu ersetzen.-excludeEntfernt Suchmuster aus der Liste der Excludes.additional filesEine Liste von Dateien, die zusätzlich in alle Projektversionen kopiert werden sollen. Die Dateien werden nicht nach Markierungen durchsucht und exakt kopiert.+additional filesErgänzt die Liste der zusätzlichen Dateien um weitere Dateien, anstatt sie zu ersetzen.-additional filesEntfernt Dateien aus der Liste der zusätzlichen Dateien.-v / –versionsListe von Versionsnummern von Projektversionen, die erstellt werden sollen. Bezieht sich nicht auf die Musterlösung. Diese kann mit--delete-mlabgestellt werden.encoding–encodingZeichenkodierung der Dateien. Standard istutf-8.delete empty files–delete-emptyWenn gesetzt werden Dateien, die nach dem kompilieren keinen Inhalt mehr haben, nicht in die Projektversionen kopiert.create zip-z / –zipErstellt zu jeder Projektversion zusätzlich eine ZIP-Datei mit demselben Namen.create zip onlyErstellt nur die ZIP-Dateien. Impliziertcreate zip = yes.creat zip dirAusgabeverzeichnis für die ZIP-Dateien, falls dieses von OUT abweicht. Standard ist dasselbe Verzeichnis wie für die Ausgabe der Projektversionen. (Also OUT bzw. ein Unterverzeichnis von OUT, wenn ein Wurzelverzeichnis angegeben wurde.)delete solution–no-mlLöscht die Musterlösung nach Ablauf des Programms. Unabhängig von dieser Einstellung wird die Musterlösung immer als erstes erstellt, um in den Dateien nach Aufgaben-Markierungen zu suchen und so die zu erstellenden Projektversionen zu ermitteln. Diese Einstellung löscht den Ordner der Musterlösung aber danach wieder. Es wird dann auch keine ZIP-Datei mehr erstellt.–debugSchaltet die Debug-Ausgaben ein.–dry-runSchaltet den Debug-Modus ein und gibt alle Änderungen auf der Konsole aus. Es werden aber keine Ordner und Dateien erstellt. Mit dieser Option kann vor der Ausführung geprüft werden, ob die Konfiguration korrekt ist.BeispieleDas folgende Beispiel kopiert die.classDateien mit in die Projektversionen und ignoriert dafür einige ausgewählte.javaQuelltexte. Dies kann sinnvoll sein, wenn das Projekt einige vordefinierte Klassen enthält (zum Beispiel eine fertige GUI), die von den Schüler:innen aber nicht bearbeitet werden sollen.~/
├── wurzel/verzeichnis/
│   ├── .jml
│   ├── files/
│   │   ├── .gitignore
│   │   └── package.bluej
│   └── Basisprojekt/
│       └── .jml
└── ausgabe/ordnerInhalt von~/wurzel/verzeichnis/.jml:[settings]output dir=~/ausgabe/ordnertask open=/*<aufg>task close=</aufg>*/solution open=//<ml>solution close=//</ml>name format={project}-v{version}create zip only=yesadditional files=~/wurzel/verzeichnis/files/.gitignore,~/wurzel/verzeichnis/files/package.bluejInhalt von~/wurzel/verzeichnis/Basisprojekt/.jml:project root=~/wurzel/verzeichnis+exclude=MyMailGUI.java,MyMailSettingsGUI.java-exclude=*.classAndere SprachenMit diesen Einstellungen kannjmlzum Beispiel auch für ein HTML-Projekt genutzt werden:~/
├── wurzel/verzeichnis/
│   ├── files/
│   │   ├── .gitignore
│   │   └── package.bluej
│   └── Basisprojekt/
│   │   ├── .jml
│       └── index.html
└── ausgabe/ordnerInhalt von~/wurzel/verzeichnis/Basisprojekt/.jml:[settings]task open=<!--aufgtask close=/aufg-->solution open=<!--ml-->solution close=<!--/ml-->include=*.html,*.htmSprachen ohne BlockkommentareEinige Programmiersprachen (wie Python oder TeX) besitzen keine
Block-, sondern nur Zeilenkommentare. Für diese Fälle gibt es die
Optionentask comment prefixundsolution comment prefix,
mit denen eine Zeichenkette festgelegt werden kann, die am Anfang
jeder Zeile entfernt wird.~/
├── wurzel/verzeichnis/
│   ├── files/
│   │   ├── .gitignore
│   │   └── package.bluej
│   └── Basisprojekt/
│   │   ├── .jml
│       └── beispiel.py
└── ausgabe/ordnerInhalt von~/wurzel/verzeichnis/Basisprojekt/.jml:[settings]task open=# aufgb:task close=# aufgtask comment prefix=#solution open=# ml:solution close=# mlinclude=*.pyMarkdown BeispielMarkdown-Dokumente besitzen in der Original-Syntax keine Kommentare, daher werden die Markierungen im Basisprojekt angezeigt. Auch die Aufgabenstellungen lassen sich dann nicht verstecken. Ansonsten funktioniert ˋjmlˋ ohne weiteres auch mit Markdown-Dateien.Wird die Markdown-Datei in HTML konvertiert, dann wird nach Konverter werden aber auch HTML-Kommentare interpretiert und in der Ausgabe versteckt. Daher könnte dieselbe Konfiguration wie oben verwendet werden.Das Beispiel zeigt eine Konfiguration, um aus einer Markdown-Datei zwei Arbeitsblätter mit Rechenaufgaben zu generieren.[settings]task open=<!--aufgtask close=/aufg-->solution open=<!--ml-->solution close=<!--/ml-->include=*.md,*.mdown,*.markdown# Kopfrechnen

## Aufgabe 1
<!--aufg 1
1. \[ 5+8\cdot 12 = \]
/aufg-->
<!--aufg 2
1.
/aufg-->
<!--ml-->
### Variante 1
1. \[ 5+8\cdot 12 = 101 \]

### Variante 2
<!--/ml-->

Weitere Aufgaben ...TeX BeispielFür TeX wird die Python-Konfiguration oben angepasst, so dass ˋ%ˋ als Kommentarzeichen interpretiert wird:[settings]task open=%\begin{aufg}task close=%\end{aufg}task comment prefix=%solution open=%\begin{ml}solution close=%\end{ml}include=*.texEine andere Herangehensweise wäre es, im Kopf der TeX-Datei Umgebungen zu definieren, die den Inhalt beim Satz der Basisversion verstecken. Dieses Vorgehen kann z.B. für den Satz von Klassenarbeiten mit mehreren Varianten hilfreich sein.[settings]task open=\begin{jmlaufgabe}task close=\end{jmlaufgabe}solution open=\begin{jml}solution close=\end{jml}include=*.texErweiterte Ersetzungentask comment prefixundsolution comment prefixkönnen auch in einem
erweiterten Modus genutzt werden. Wenn die Optionen auf einen Wert im Format:/regex/replace/gesetzt wird, wirdregexals regulärer Ausdruck benutzt und die erste
Fundstelle durchreplaceersetzt. Dabei lassen sich auch Gruppen ausregexinreplacemit\1,\2, usw. referenzieren.Ein Slash/muss durch\/maskiert werden, wenn er inregexoderreplacevorkommen soll.Dieser Modus kann hilfreich sein, um komplexere Kommentar-Syntax oder ganz
andere Einsatzzwecke zu ermöglichen.Diese.jmlDatei würde die Aufgaben-Markierungen auf ein HTML-Format
ändern und innerhalb des Aufgaben-Blocks in jeder Zeile die Kommentare
entfernen, die einTODO:enthalten.[settings]task open=<!--aufg start-->task close=<!--aufg end-->task comment prefix=/<!-- TODO: (.+) -->/\\1/Zum Beispiel würde aus<!--aufg start--><!-- Füge hier deine Lösung ein --><!-- TODO: <div></div> --><!--aufg end-->dann die Ausgabe<!-- Füge hier deine Lösung ein --><div></div>Wie man sieht, bleibt der Kommentar ohneTODO:erhalten.Integration in IDEsIn Entwicklungsumgebungen wieEclipse,IntelliJundvscodelässt
sichjmlrelativ einfach als externes Tool einbinden. Die IDEs erlauben
in der Regel die Verwendung von Platzhaltervariablen, mit denen der Aufruf
vonjmlangepasst werden kann.