You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by sg...@apache.org on 2020/07/01 06:18:12 UTC

[freemarker-generator] branch master updated: FREEMARKER-149 Support multiple template transformation on the command line (#17)

This is an automated email from the ASF dual-hosted git repository.

sgoeschl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/freemarker-generator.git


The following commit(s) were added to refs/heads/master by this push:
     new b27c6ba  FREEMARKER-149 Support multiple template transformation on the command line (#17)
b27c6ba is described below

commit b27c6ba7ceefbaafed91182fbb0fd32223149303
Author: Siegfried Goeschl <si...@gmail.com>
AuthorDate: Wed Jul 1 08:18:05 2020 +0200

    FREEMARKER-149 Support multiple template transformation on the command line (#17)
---
 .../generator/base/datasource/DataSources.java     |  2 +-
 .../generator/base/file/RecursiveFileSupplier.java |  6 +-
 .../generator/base/template/TemplateOutput.java    |  8 +++
 .../base/template/TemplateTransformation.java      |  8 +++
 .../template/TemplateTransformationsBuilder.java   | 34 ++++++----
 .../freemarker/generator/base/uri/NamedUri.java    |  2 +-
 .../freemarker/generator/base/util/ListUtils.java  |  4 ++
 freemarker-generator-cli/CHANGELOG.md              |  8 ++-
 freemarker-generator-cli/README.md                 | 29 ++++----
 freemarker-generator-cli/pom.xml                   |  2 +
 .../org/apache/freemarker/generator/cli/Main.java  | 42 +++++++-----
 .../freemarker/generator/cli/config/Settings.java  | 30 ++++----
 .../freemarker/generator/cli/config/Suppliers.java |  2 +-
 .../markdown/cli/advanced/cli-configuration.md     |  2 +-
 .../site/markdown/cli/concepts/template-loading.md |  2 +-
 .../site/markdown/cli/concepts/transformation.md   | 74 ++++++++++++++++++--
 .../markdown/cli/introduction/getting-started.md   | 79 +++++++++++-----------
 .../freemarker/generator/cli/ExamplesTest.java     | 17 ++++-
 .../freemarker/generator/cli/ManualTest.java       |  3 +-
 .../freemarker/generator/cli/PicocliTest.java      |  8 +++
 .../generator/cli/config/SettingsTest.java         |  5 +-
 .../generator/tools/excel/ExcelTool.java           |  2 +-
 22 files changed, 247 insertions(+), 122 deletions(-)

diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
index 9db5552..62838a1 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
@@ -124,7 +124,7 @@ public class DataSources implements Closeable {
      * Find data sources based on their group and and globbing pattern.
      *
      * @param wildcard the wildcard string to match against
-     * @return list of mathching data sources
+     * @return list of matching data sources
      */
     public List<DataSource> findByGroup(String wildcard) {
         return dataSources.stream()
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java
index ef60fe7..d532df8 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java
@@ -53,7 +53,7 @@ public class RecursiveFileSupplier implements Supplier<List<File>> {
     /** File filter to apply */
     private final IOFileFilter fileFilter;
 
-    /** Diectory filter to apply */
+    /** Directory filter to apply */
     private final IOFileFilter directoryFilter;
 
     public RecursiveFileSupplier(Collection<String> sources, Collection<String> includes, Collection<String> excludes) {
@@ -119,10 +119,10 @@ public class RecursiveFileSupplier implements Supplier<List<File>> {
             return emptyList();
         }
 
-        return excludes.stream().map(RecursiveFileSupplier::exludeFilter).collect(toList());
+        return excludes.stream().map(RecursiveFileSupplier::excludeFilter).collect(toList());
     }
 
-    private static IOFileFilter exludeFilter(String exclude) {
+    private static IOFileFilter excludeFilter(String exclude) {
         return isEmpty(exclude) ?
                 VISIBLE :
                 new NotFileFilter(new OrFileFilter(new WildcardFileFilter(exclude), HIDDEN));
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java
index 426c0f1..818d66d 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java
@@ -75,6 +75,14 @@ public class TemplateOutput {
         return writer != null ? writer : fileWriter();
     }
 
+    @Override
+    public String toString() {
+        return "TemplateOutput{" +
+                "writer=" + writer +
+                ", file=" + file +
+                '}';
+    }
+
     private FileWriter fileWriter() {
         Validate.notNull(file, "Output file is null");
 
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
index b13a799..8c19478 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
@@ -41,4 +41,12 @@ public class TemplateTransformation {
     public TemplateOutput getTemplateOutput() {
         return templateOutput;
     }
+
+    @Override
+    public String toString() {
+        return "TemplateTransformation{" +
+                "templateSource=" + templateSource +
+                ", templateOutput=" + templateOutput +
+                '}';
+    }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
index fe62017..c738670 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
@@ -28,6 +28,7 @@ import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Collections.singletonList;
@@ -50,7 +51,7 @@ public class TemplateTransformationsBuilder {
     private final List<String> excludes;
 
     /** Optional output file or directory */
-    private final List<File> outputs;
+    private final List<String> outputs;
 
     /** Optional user-supplied writer */
     private Writer writer;
@@ -73,12 +74,12 @@ public class TemplateTransformationsBuilder {
         final List<TemplateTransformation> result = new ArrayList<>();
 
         if (hasInteractiveTemplate()) {
-            final File outputFile = outputs.isEmpty() ? null : outputs.get(0);
+            final File outputFile = getOutputFile(0).orElse(null);
             result.add(resolveInteractiveTemplate(outputFile));
         } else {
             for (int i = 0; i < sources.size(); i++) {
                 final String source = sources.get(i);
-                final File output = i < outputs.size() ? outputs.get(i) : null;
+                final File output = getOutputFile(i).orElse(null);
                 result.addAll(resolve(source, output));
             }
         }
@@ -133,27 +134,20 @@ public class TemplateTransformationsBuilder {
         return this;
     }
 
-    public TemplateTransformationsBuilder addOutput(String output) {
-        if (StringUtils.isNotEmpty(output)) {
-            this.outputs.add(new File(output));
+    public TemplateTransformationsBuilder addOutputs(Collection<String> outputs) {
+        if (outputs != null) {
+            this.outputs.addAll(outputs);
         }
         return this;
     }
 
-    public TemplateTransformationsBuilder addOutput(File output) {
-        if (output != null) {
+    public TemplateTransformationsBuilder addOutput(String output) {
+        if (StringUtils.isNotEmpty(output)) {
             this.outputs.add(output);
         }
         return this;
     }
 
-    public TemplateTransformationsBuilder addOutputs(Collection<String> outputs) {
-        if (outputs != null) {
-            outputs.forEach(this::addOutput);
-        }
-        return this;
-    }
-
     public TemplateTransformationsBuilder setWriter(Writer writer) {
         this.writer = writer;
         return this;
@@ -252,6 +246,16 @@ public class TemplateTransformationsBuilder {
         return template != null;
     }
 
+    private Optional<File> getOutputFile(int i) {
+        if (outputs.isEmpty()) {
+            return Optional.empty();
+        } else if (i < outputs.size()) {
+            return Optional.of(new File(outputs.get(i)));
+        } else {
+            return Optional.of(new File(outputs.get(0)));
+        }
+    }
+
     private static File getTemplateOutputFile(File templateDirectory, File templateFile, File outputDirectory) {
         final String relativePath = relativePath(templateDirectory, templateFile);
         final String relativeOutputFileName = mapExtension(relativePath);
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
index 45fb595..1b1c63e 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
@@ -25,7 +25,7 @@ import static org.apache.freemarker.generator.base.util.StringUtils.emptyToNull;
 import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
 
 /**
- * Caputeres the information of a user-supplied "named URI".
+ * Captures the information of a user-supplied "named URI".
  */
 public class NamedUri {
 
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
index 369bf90..613b3e1 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
@@ -26,6 +26,10 @@ public class ListUtils {
         return list == null || list.isEmpty();
     }
 
+    public static <T> boolean isNotEmpty(final List<T> list) {
+        return !isNullOrEmpty(list);
+    }
+
     /**
      * Transposes the given tabular data, swapping rows with columns.
      *
diff --git a/freemarker-generator-cli/CHANGELOG.md b/freemarker-generator-cli/CHANGELOG.md
index 32ab747..70d43a1 100644
--- a/freemarker-generator-cli/CHANGELOG.md
+++ b/freemarker-generator-cli/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. We try to a
 ## 0.1.0-SNAPSHOT
 
 ### Added
+* [FREEMARKER-149] Support multiple template transformations on the command line
 * [FREEMARKER-144] Proof Of Concept for providing DataFrames
 * [FREEMARKER-142] Support Transformation Of Directories
 * [FREEMARKER-139] freemarker-cli: Provide GsonTool to align with Maven plugin
@@ -21,11 +22,11 @@ All notable changes to this project will be documented in this file. We try to a
 * [FREEMARKER-136] Fix broken `site:stage` build
 * [FREEMARKER-134] Rename `Document` to `Datasource` which also changes `--document` to `--datasource`
 * [FREEMARKER-129] Use `freemarker.configuration.setting` in `freemarker-cli.properties` to configure FreeMarker
-* [FREEMARKER-129] Provide a `toString()` metheod for all tools
+* [FREEMARKER-129] Provide a `toString()` method for all tools
 * [FREEMARKER-129] Use version "0.X.Y" to cater for API changes according to [Semantic Versioning](https://semver.org)
 
 ### Fixed 
-* [FREEMARKER-147] Complete Maven site documenation
+* [FREEMARKER-147] Complete Maven site documentation
 * [FREEMARKER-127] Site build fails with missing "org/apache/maven/doxia/siterenderer/DocumentContent"
 
 [FREEMARKER-127]: https://issues.apache.org/jira/browse/FREEMARKER-127
@@ -39,4 +40,5 @@ All notable changes to this project will be documented in this file. We try to a
 [FREEMARKER-142]: https://issues.apache.org/jira/browse/FREEMARKER-142
 [FREEMARKER-144]: https://issues.apache.org/jira/browse/FREEMARKER-144
 [FREEMARKER-146]: https://issues.apache.org/jira/browse/FREEMARKER-146
-[FREEMARKER-147]: https://issues.apache.org/jira/browse/FREEMARKER-147
\ No newline at end of file
+[FREEMARKER-147]: https://issues.apache.org/jira/browse/FREEMARKER-147
+[FREEMARKER-149]: https://issues.apache.org/jira/browse/FREEMARKER-149
\ No newline at end of file
diff --git a/freemarker-generator-cli/README.md b/freemarker-generator-cli/README.md
index 097a4b5..acfcc67 100644
--- a/freemarker-generator-cli/README.md
+++ b/freemarker-generator-cli/README.md
@@ -8,26 +8,27 @@ This module provides provides the CLI for `Apache FreeMarker`.
 
 Now you can have a look at the command line options
 
-```
+```text
 freemarker-cli -h
 Usage: freemarker-cli (-t=<templates> [-t=<templates>]... |
                       -i=<interactiveTemplate>) [-hV] [--stdin] [-b=<baseDir>]
-                      [--config=<configFile>] [--data-source-exclude=<exclude>]
-                      [--data-source-include=<include>] [-e=<inputEncoding>]
-                      [-l=<locale>] [-o=<outputFile>]
+                      [--config=<configFile>]
+                      [--data-source-exclude=<dataSourceExcludePattern>]
+                      [--data-source-include=<dataSourceIncludePattern>]
+                      [-e=<inputEncoding>] [-l=<locale>]
                       [--output-encoding=<outputEncoding>] [--times=<times>]
                       [-D=<String=String>]... [-m=<dataModels>]...
-                      [-P=<String=String>]... [-s=<dataSources>]...
-                      [<sources>...]
+                      [-o=<outputs>]... [-P=<String=String>]...
+                      [-s=<dataSources>]... [<sources>...]
 Apache FreeMarker CLI
       [<sources>...]        data source files and/or directories
-  -b, --basedir=<baseDir>   optional template base directory
+  -b, --basedir=<baseDir>   additional template base directory
       --config=<configFile> FreeMarker CLI configuration file
   -D, --system-property=<String=String>
                             set system property
-      --data-source-exclude=<exclude>
+      --data-source-exclude=<dataSourceExcludePattern>
                             file exclude pattern for data sources
-      --data-source-include=<include>
+      --data-source-include=<dataSourceIncludePattern>
                             file include pattern for data sources
   -e, --input-encoding=<inputEncoding>
                             encoding of data source
@@ -37,30 +38,30 @@ Apache FreeMarker CLI
   -l, --locale=<locale>     locale being used for the output, e.g. 'en_US'
   -m, --data-model=<dataModels>
                             data model used for rendering
-  -o, --output=<outputFile> output file or directory
+  -o, --output=<outputs>    output files or directories
       --output-encoding=<outputEncoding>
                             encoding of output, e.g. UTF-8
   -P, --param=<String=String>
                             set parameter
   -s, --data-source=<dataSources>
-                            data source used for redering
+                            data source used for rendering
       --stdin               read data source from stdin
   -t, --template=<templates>
-                            template to process
+                            templates to process
       --times=<times>       re-run X times for profiling
   -V, --version             Print version information and exit.
 ```
 
 Check the version of the `Apache FreeMarker CLI`
 
-```
+```text
 freemarker-cli -V
 version=0.1.0-SNAPSHOT, time=2020-06-25T21:48:02+0200, commit=b320d00094be8789086ad6153d9d3fcaf4b8c75f
 ```
 
 Or run the examples 
 
-```
+```text
 ./run-examples.sh 
 templates/info.ftl
 examples/templates/demo.ftl
diff --git a/freemarker-generator-cli/pom.xml b/freemarker-generator-cli/pom.xml
index 81ee0ac..20d7ce2 100644
--- a/freemarker-generator-cli/pom.xml
+++ b/freemarker-generator-cli/pom.xml
@@ -73,6 +73,8 @@
                             <target>
                                 <copy file="CHANGELOG.md" todir="./target/appassembler" />
                                 <copy file="README.md" todir="./target/appassembler" />
+                                <copy file="LICENSE" todir="./target/appassembler" />
+                                <copy file="NOTICE" todir="./target/appassembler" />
                                 <copy file="./src/main/scripts/run-examples.sh" todir="./target/appassembler" />
                                 <copy todir="./target/appassembler/templates">
                                     <fileset dir="templates" />
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
index 7e86f85..8b3b853 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
@@ -18,7 +18,7 @@ package org.apache.freemarker.generator.cli;
 
 import org.apache.freemarker.generator.base.parameter.ParameterModelSupplier;
 import org.apache.freemarker.generator.base.util.ClosableUtils;
-import org.apache.freemarker.generator.base.util.StringUtils;
+import org.apache.freemarker.generator.base.util.ListUtils;
 import org.apache.freemarker.generator.cli.config.Settings;
 import org.apache.freemarker.generator.cli.picocli.GitVersionProvider;
 import org.apache.freemarker.generator.cli.task.FreeMarkerTask;
@@ -59,7 +59,7 @@ public class Main implements Callable<Integer> {
     TemplateSourceOptions templateSourceOptions;
 
     public static final class TemplateSourceOptions {
-        @Option(names = { "-t", "--template" }, description = "template to process")
+        @Option(names = { "-t", "--template" }, description = "templates to process")
         public List<String> templates;
 
         @Option(names = { "-i", "--interactive" }, description = "interactive template to process")
@@ -81,8 +81,8 @@ public class Main implements Callable<Integer> {
     @Option(names = { "-m", "--data-model" }, description = "data model used for rendering")
     List<String> dataModels;
 
-    @Option(names = { "-o", "--output" }, description = "output file or directory")
-    String outputFile;
+    @Option(names = { "-o", "--output" }, description = "output files or directories")
+    List<String> outputs;
 
     @Option(names = { "-P", "--param" }, description = "set parameter")
     Map<String, String> parameters;
@@ -94,10 +94,10 @@ public class Main implements Callable<Integer> {
     String configFile;
 
     @Option(names = { "--data-source-include" }, description = "file include pattern for data sources")
-    String include;
+    String dataSourceIncludePattern;
 
     @Option(names = { "--data-source-exclude" }, description = "file exclude pattern for data sources")
-    String exclude;
+    String dataSourceExcludePattern;
 
     @Option(names = { "--output-encoding" }, description = "encoding of output, e.g. UTF-8", defaultValue = "UTF-8")
     String outputEncoding;
@@ -174,13 +174,13 @@ public class Main implements Callable<Integer> {
             final FreeMarkerTask freeMarkerTask = new FreeMarkerTask(settings);
             return freeMarkerTask.call();
         } finally {
-            if (settings.hasOutputFile()) {
+            if (settings.hasOutputs()) {
                 ClosableUtils.closeQuietly(settings.getWriter());
             }
         }
     }
 
-    private void validate() {
+    void validate() {
         // "-d" or "--data-source" parameter shall not contain wildcard characters
         if (dataSources != null) {
             for (String source : dataSources) {
@@ -189,6 +189,16 @@ public class Main implements Callable<Integer> {
                 }
             }
         }
+
+        // does the templates match the expected outputs?!
+        // -) no output means it goes to stdout
+        // -) for each template there should be an output
+        final List<String> templates = templateSourceOptions.templates;
+        if (templates != null && templates.size() > 1) {
+            if (outputs != null && outputs.size() != templates.size()) {
+                throw new ParameterException(spec.commandLine(), "Template output does not match specified templates");
+            }
+        }
     }
 
     private Settings settings(Properties configuration, List<File> templateDirectories) {
@@ -198,28 +208,28 @@ public class Main implements Callable<Integer> {
                 .isReadFromStdin(readFromStdin)
                 .setArgs(args)
                 .setConfiguration(configuration)
-                .setDataSourceIncludePattern(include)
-                .setDataSourceExcludePattern(exclude)
+                .setDataModels(dataModels)
+                .setDataSources(getCombinedDataSources())
+                .setDataSourceIncludePattern(dataSourceIncludePattern)
+                .setDataSourceExcludePattern(dataSourceExcludePattern)
                 .setInputEncoding(inputEncoding)
                 .setInteractiveTemplate(templateSourceOptions.interactiveTemplate)
                 .setLocale(locale)
                 .setOutputEncoding(outputEncoding)
-                .setOutputFile(outputFile)
+                .setOutputs(outputs)
                 .setParameters(parameterModelSupplier.get())
-                .setDataSources(getCombinedDataSources())
-                .setDataModels(dataModels)
                 .setSystemProperties(systemProperties != null ? systemProperties : new Properties())
                 .setTemplateDirectories(templateDirectories)
                 .setTemplateNames(templateSourceOptions.templates)
-                .setWriter(writer(outputFile, outputEncoding))
+                .setWriter(writer(outputs, outputEncoding))
                 .build();
     }
 
-    private Writer writer(String outputFile, String outputEncoding) {
+    private Writer writer(List<String> outputFiles, String outputEncoding) {
         try {
             if (userSuppliedWriter != null) {
                 return userSuppliedWriter;
-            } else if (StringUtils.isEmpty(outputFile)) {
+            } else if (ListUtils.isNullOrEmpty(outputFiles)) {
                 return new BufferedWriter(new OutputStreamWriter(System.out, outputEncoding));
             } else {
                 return null;
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
index 16dc76f..245a2a8 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
@@ -17,6 +17,7 @@
 package org.apache.freemarker.generator.cli.config;
 
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Model;
+import org.apache.freemarker.generator.base.util.ListUtils;
 import org.apache.freemarker.generator.base.util.LocaleUtils;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
 
@@ -74,8 +75,8 @@ public class Settings {
     /** Enable verbose mode (currently not used) **/
     private final boolean verbose;
 
-    /** Optional output file or directory if not written to stdout */
-    private final File output;
+    /** Optional output files or directories if not written to stdout */
+    private final List<String> outputs;
 
     /** Optional include pattern for recursive directly search of data source files */
     private final String dataSourceIncludePattern;
@@ -115,7 +116,7 @@ public class Settings {
             Charset inputEncoding,
             Charset outputEncoding,
             boolean verbose,
-            File output,
+            List<String> outputs,
             String dataSourceIncludePattern,
             String dataSourceExcludePattern,
             Locale locale,
@@ -138,7 +139,7 @@ public class Settings {
         this.inputEncoding = inputEncoding;
         this.outputEncoding = outputEncoding;
         this.verbose = verbose;
-        this.output = output;
+        this.outputs = outputs;
         this.dataSourceIncludePattern = dataSourceIncludePattern;
         this.dataSourceExcludePattern = dataSourceExcludePattern;
         this.locale = requireNonNull(locale);
@@ -199,8 +200,8 @@ public class Settings {
         return verbose;
     }
 
-    public File getOutput() {
-        return output;
+    public List<String> getOutputs() {
+        return outputs;
     }
 
     public String getDataSourceIncludePattern() {
@@ -235,8 +236,8 @@ public class Settings {
         return userSystemProperties;
     }
 
-    public boolean hasOutputFile() {
-        return output != null;
+    public boolean hasOutputs() {
+        return ListUtils.isNotEmpty(outputs);
     }
 
     public Writer getWriter() {
@@ -277,7 +278,7 @@ public class Settings {
                 ", inputEncoding=" + inputEncoding +
                 ", outputEncoding=" + outputEncoding +
                 ", verbose=" + verbose +
-                ", outputFile=" + output +
+                ", outputs=" + outputs +
                 ", include='" + dataSourceIncludePattern + '\'' +
                 ", exclude='" + dataSourceExcludePattern + '\'' +
                 ", locale=" + locale +
@@ -298,7 +299,7 @@ public class Settings {
         private String inputEncoding;
         private String outputEncoding;
         private boolean verbose;
-        private String outputFile;
+        private List<String> outputs;
         private String dataSourceIncludePattern;
         private String dataSourceExcludePattern;
         private String locale;
@@ -380,8 +381,10 @@ public class Settings {
             return this;
         }
 
-        public SettingsBuilder setOutputFile(String outputFile) {
-            this.outputFile = outputFile;
+        public SettingsBuilder setOutputs(List<String> outputs) {
+            if (outputs != null) {
+                this.outputs = outputs;
+            }
             return this;
         }
 
@@ -449,7 +452,6 @@ public class Settings {
             final Charset inputEncoding = Charset.forName(this.inputEncoding);
             final Charset outputEncoding = Charset.forName(this.outputEncoding);
             final String currLocale = locale != null ? locale : getDefaultLocale();
-            final File currOutputFile = outputFile != null ? new File(outputFile) : null;
 
             return new Settings(
                     configuration,
@@ -462,7 +464,7 @@ public class Settings {
                     inputEncoding,
                     outputEncoding,
                     verbose,
-                    currOutputFile,
+                    outputs,
                     dataSourceIncludePattern,
                     dataSourceExcludePattern,
                     LocaleUtils.parseLocale(currLocale),
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
index 54e0758..6412b4d 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
@@ -73,7 +73,7 @@ public class Suppliers {
                 .addSources(settings.getTemplates())
                 .addInclude(settings.getTemplateFileIncludePattern())
                 .addExclude(settings.getTemplateFileExcludePattern())
-                .addOutput(settings.getOutput())
+                .addOutputs(settings.getOutputs())
                 .setWriter(settings.getWriter())
                 .build();
     }
diff --git a/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md b/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md
index cde2cd1..0c3e842 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md
@@ -40,7 +40,7 @@ Changing this file allows to tweak the underlying `Apache FreeMarker Configurati
 
 ### Storing User-Specific Templates
 
-Over the time you will accumalate more and more `Apache FreeMarker` templates - some of them are stored within a project but some of the more general might be free-floating and you don't want to store them in the installation directory.
+Over the time you will accumulate more and more `Apache FreeMarker` templates - some of them are stored within a project but some of the more general might be free-floating and you don't want to store them in the installation directory.
 
 To give those free-floating templates a home `Apache FreeMarker CLI` tries to read templates from `~/freemarker-cli`, e.g.
 
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md
index f54228e..c512d5c 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md
@@ -40,7 +40,7 @@ and [Template Includes](https://freemarker.apache.org/docs/ref_directive_include
 
 ### Free-Style Template Loading
 
-The previosly described `Template Loaders` do not support absolute template files or arbitraRY URLS - this behaviour 
+The previosly described `Template Loaders` do not support absolute template files or arbitrary URLS - this behaviour 
 stems from security aspects when running `Apache FreeMarker` on the server side. For a command-line tool this is mostly
 irrelevant therefore any template file outside of the template loader directories can be loaded 
 
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
index 388a681..73477eb 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
@@ -1,14 +1,74 @@
 ## Transformation
 
-The `freemarker-cli` generates text output based on FreeMarker templates and data
+The `freemarker-cli` generates text output based on processing FreeMarker templates and data 
 
 * A command line invocation requires 1..n `templates` and 0..n `data sources` / `data models` 
-* A command line invocation is mapped to a series of `transformations`
-* The `transformation` consists of exactly one `template`, 0..n `data sources` / `data models` and an `output`
-* An `output` is either written to 
+* A command line invocation is mapped to a series of `template transformations`
+* The `transformation` consists of exactly one `template`, 0..n `data sources` / `data models` written to an `output`
+* The `output` is either written to
     * `stdout`
-    * an output file
-    * an output directory
+    * one or more output files
+    * one or output directories
 * When the output is written to a directory
     * the structure of the input directory is preserved
-    * a `ftl` file externsion is removed
+    * a `ftl` file extension is removed
+
+### Examples
+
+Transforming a single template to a single output file 
+
+```
+freemarker-cli \
+-t templates/csv/md/transform.ftl examples/data/csv/contract.csv \
+-o target/contract.md
+```
+
+Transforming multiple templates to multiple output files (1:1 mapping between templates and outputs)
+
+```
+> freemarker-cli \
+-t templates/csv/md/transform.ftl -o target/contract.md \
+-t templates/csv/html/transform.ftl -o target/contract.html \
+examples/data/csv/contract.csv
+
+> tree target 
+target
+|-- contract.html
+`-- contract.md
+```
+
+Transforming single template directory to single output directory
+
+```
+> freemarker-cli \
+-t examples/data/template -o target/template1
+
+> tree target     
+target
+`-- template1
+    |-- application.properties
+    `-- nginx
+        `-- nginx.conf
+
+
+```
+
+Transforming multiple template directories to multiple output directories
+
+```
+freemarker-cli \
+-t examples/data/template -o target/template1 \
+-t examples/data/template -o target/template2 
+
+> tree target     
+target
+|-- template1
+|   |-- application.properties
+|   `-- nginx
+|       `-- nginx.conf
+`-- template2
+    |-- application.properties
+    `-- nginx
+        `-- nginx.conf
+
+```
diff --git a/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md b/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md
index a288348..e2f0de4 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md
@@ -36,45 +36,46 @@ version=0.1.0-SNAPSHOT, time=2020-06-25T21:48:02+0200, commit=b320d00094be878908
 
 ```
 > freemarker-cli -h
-Usage: freemarker-cli (-t=<templates> [-t=<templates>]... |
-                      -i=<interactiveTemplate>) [-hV] [--stdin] [-b=<baseDir>]
-                      [--config=<configFile>] [--data-source-exclude=<exclude>]
-                      [--data-source-include=<include>] [-e=<inputEncoding>]
-                      [-l=<locale>] [-o=<outputFile>]
-                      [--output-encoding=<outputEncoding>] [--times=<times>]
-                      [-D=<String=String>]... [-m=<dataModels>]...
-                      [-P=<String=String>]... [-s=<dataSources>]...
-                      [<sources>...]
-Apache FreeMarker CLI
-      [<sources>...]        data source files and/or directories
-  -b, --basedir=<baseDir>   optional template base directory
-      --config=<configFile> FreeMarker CLI configuration file
-  -D, --system-property=<String=String>
-                            set system property
-      --data-source-exclude=<exclude>
-                            file exclude pattern for data sources
-      --data-source-include=<include>
-                            file include pattern for data sources
-  -e, --input-encoding=<inputEncoding>
-                            encoding of data source
-  -h, --help                Show this help message and exit.
-  -i, --interactive=<interactiveTemplate>
-                            interactive template to process
-  -l, --locale=<locale>     locale being used for the output, e.g. 'en_US'
-  -m, --data-model=<dataModels>
-                            data model used for rendering
-  -o, --output=<outputFile> output file or directory
-      --output-encoding=<outputEncoding>
-                            encoding of output, e.g. UTF-8
-  -P, --param=<String=String>
-                            set parameter
-  -s, --data-source=<dataSources>
-                            data source used for redering
-      --stdin               read data source from stdin
-  -t, --template=<templates>
-                            template to process
-      --times=<times>       re-run X times for profiling
-  -V, --version             Print version information and exit.
+  Usage: freemarker-cli (-t=<templates> [-t=<templates>]... |
+                        -i=<interactiveTemplate>) [-hV] [--stdin] [-b=<baseDir>]
+                        [--config=<configFile>]
+                        [--data-source-exclude=<dataSourceExcludePattern>]
+                        [--data-source-include=<dataSourceIncludePattern>]
+                        [-e=<inputEncoding>] [-l=<locale>]
+                        [--output-encoding=<outputEncoding>] [--times=<times>]
+                        [-D=<String=String>]... [-m=<dataModels>]...
+                        [-o=<outputs>]... [-P=<String=String>]...
+                        [-s=<dataSources>]... [<sources>...]
+  Apache FreeMarker CLI
+        [<sources>...]        data source files and/or directories
+    -b, --basedir=<baseDir>   additional template base directory
+        --config=<configFile> FreeMarker CLI configuration file
+    -D, --system-property=<String=String>
+                              set system property
+        --data-source-exclude=<dataSourceExcludePattern>
+                              file exclude pattern for data sources
+        --data-source-include=<dataSourceIncludePattern>
+                              file include pattern for data sources
+    -e, --input-encoding=<inputEncoding>
+                              encoding of data source
+    -h, --help                Show this help message and exit.
+    -i, --interactive=<interactiveTemplate>
+                              interactive template to process
+    -l, --locale=<locale>     locale being used for the output, e.g. 'en_US'
+    -m, --data-model=<dataModels>
+                              data model used for rendering
+    -o, --output=<outputs>    output files or directories
+        --output-encoding=<outputEncoding>
+                              encoding of output, e.g. UTF-8
+    -P, --param=<String=String>
+                              set parameter
+    -s, --data-source=<dataSources>
+                              data source used for rendering
+        --stdin               read data source from stdin
+    -t, --template=<templates>
+                              templates to process
+        --times=<times>       re-run X times for profiling
+    -V, --version             Print version information and exit.
 ```
 
 ### The Info Template
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
index 0c3820c..e66d457 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
@@ -121,12 +121,24 @@ public class ExamplesTest extends AbstractMainTest {
     }
 
     @Test
-    public void shouldTransformTemplateDirectory() throws IOException {
+    public void shouldTransformSingleTemplateDirectory() throws IOException {
         assertTrue(execute("-t examples/data/template").contains("server.name=127.0.0.1"));
         assertTrue(execute("-t examples/data/template -PNGINX_HOSTNAME=my.domain.com").contains("server.name=my.domain.com"));
     }
 
     @Test
+    public void shouldTransformMultipleTemplateDirectories() throws IOException {
+        assertValid(execute("-t examples/data/template -t examples/data/template"));
+        assertValid(execute("-t examples/data/template -o target/out/template1 -t examples/data/template -o target/out/template2"));
+    }
+
+    @Test
+    public void shouldTransformMultipleTemplates() throws IOException {
+        assertValid(execute("-t templates/csv/md/transform.ftl -t templates/csv/html/transform.ftl examples/data/csv/contract.csv"));
+        assertValid(execute("-t templates/csv/md/transform.ftl -o target/contract.md -t templates/csv/html/transform.ftl -o target/contract.html examples/data/csv/contract.csv"));
+    }
+
+    @Test
     @Ignore("Manual test to check memory consumption and resource handling")
     public void shouldCloseAllResources() throws IOException {
         for (int i = 0; i < 500; i++) {
@@ -141,7 +153,8 @@ public class ExamplesTest extends AbstractMainTest {
             shouldRunXmlExamples();
             shouldRunGrokExamples();
             shouldRunInteractiveTemplateExamples();
-            shouldTransformTemplateDirectory();
+            shouldTransformSingleTemplateDirectory();
+            shouldTransformMultipleTemplates();
             shouldRunWithExposedEnvironmentVariableExamples();
         }
     }
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
index 2b7f456..ab10101 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
@@ -24,10 +24,11 @@ import java.util.Arrays;
 public class ManualTest {
 
     private static final String SPACE = " ";
-    private static final String CMD = "-V";
+    // private static final String CMD = "-V";
     // private static final String CMD = "-PCSV_SOURCE_FORMAT=DATAFRAME -t examples/templates/dataframe/example.ftl https://raw.githubusercontent.com/nRo/DataFrame/master/src/test/resources/users.csv";
     // private static final String CMD = "-PCSV_SOURCE_WITH_HEADER=false -PCSV_SOURCE_FORMAT=DEFAULT -PCSV_TARGET_FORMAT=EXCEL -PCSV_TARGET_WITH_HEADER=true -t templates/csv/csv/transform.ftl examples/data/csv/contract.csv";
     // private static final String CMD = "-t examples/templates/json/dataframe/github-users.ftl examples/data/json/github-users.json";
+    private static final String CMD = "-t templates/csv/md/transform.ftl -o target/contract.md -t templates/csv/html/transform.ftl examples/data/csv/contract.csv";
 
 
     public static void main(String[] args) {
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
index 800e8be..34077d5 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
@@ -18,6 +18,7 @@ package org.apache.freemarker.generator.cli;
 
 import org.junit.Test;
 import picocli.CommandLine;
+import picocli.CommandLine.ParameterException;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
@@ -111,6 +112,13 @@ public class PicocliTest {
         assertEquals(INTERACTIVE_TEMPLATE, main.templateSourceOptions.interactiveTemplate);
     }
 
+    @Test(expected = ParameterException.class)
+    public void shouldThrowParameterExceptionForMismatchedTemplateOutput() {
+        final Main main = parse("-t", "foo.ftl", "-t", "bar.ftl", "-o", "foo.out");
+
+        main.validate();
+    }
+
     private static Main parse(String... args) {
         final Main main = new Main();
         new CommandLine(main).parseArgs(args);
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
index 90c7696..cbd838a 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
@@ -20,6 +20,7 @@ import org.apache.freemarker.generator.cli.config.Settings.SettingsBuilder;
 import org.junit.Test;
 
 import java.io.StringWriter;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -54,7 +55,7 @@ public class SettingsTest {
         assertEquals(ANY_INCLUDE, settings.getDataSourceIncludePattern());
         assertEquals(ANY_INPUT_ENCODING, settings.getInputEncoding().name());
         assertEquals(ANY_OUTPUT_ENCODING, settings.getOutputEncoding().name());
-        assertEquals(ANY_OUTPUT_FILE, settings.getOutput().getName());
+        assertEquals(ANY_OUTPUT_FILE, settings.getOutputs().get(0));
         assertEquals(ANY_TEMPLATE_NAME, settings.getTemplates().get(0));
         assertNotNull(settings.getDataSources());
         assertNotNull(settings.getUserParameters());
@@ -74,7 +75,7 @@ public class SettingsTest {
                 .setInteractiveTemplate(ANY_INTERACTIVE_TEMPLATE)
                 .setLocale(ANY_LOCALE)
                 .setOutputEncoding(ANY_OUTPUT_ENCODING)
-                .setOutputFile(ANY_OUTPUT_FILE)
+                .setOutputs(Collections.singletonList(ANY_OUTPUT_FILE))
                 .setParameters(ANY_USER_PARAMETERS)
                 .setDataSources(ANY_SOURCES)
                 .setSystemProperties(ANY_SYSTEM_PROPERTIES)
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java
index d9bb473..406a927 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java
@@ -154,7 +154,7 @@ public class ExcelTool {
      * See https://stackoverflow.com/questions/15710888/reading-time-values-from-spreadsheet-using-poi-api.
      *
      * @param cell Cell containing some sort of date or time
-     * @return The corresponding Java istance
+     * @return The corresponding Java instance
      */
     private static synchronized Object toDateCellValue(Cell cell) {
         final Date date = cell.getDateCellValue();