Skip to the content.
defmodule LiterateCompiler.Args do

Purpose

A standard Elixir library has collected and marshalled all the arguments. This module verifies them and ensures that the set of arguments passed are coherent.

Data Structures

This arguments structure is populated when the CLI is called and is then passed around the various arguments

When the inputs are validated, any errors are written to the errors key The consumer of this argument struct checks if it is error free


   defstruct [
      inputdir:    :nil,
      outputdir:   :nil,
      excludes:    :nil,
      print_files: false,
      help:        false,
      make_jekyll: false,
      format:      "markdown",
      print_type:  0,
      errors:      []
   ]

Public API

Three simple self-explanatory functions.


   def parse_args(args) do
      acc = %LiterateCompiler.Args{}
      parse_args(args, acc)
   end

   def print_help() do
    lines = [
      "Help",
      "",
      "literate compiler is a script that converts code into beautiful webpages to read",
      "either directly as HTML or as markdown for github to convert to GitHub pages",
      "",
      "Options:",
      "-h --help       takes no argument",
      "                prints this message",
      "                (optional)",
      "",
      "-e --exclude    takes 1 argument",
      "                which path to and name of an exclude file",
      "                which contains a list of modules to exclude",
      "                one per line, path relative to the directory this script runs in",
      "                exclude files can be generated with the -l --list option",
      "                (optional - default .literate_compiler.exclude in the current directory)",
      "",
      "-f --format     takes 1 argument, one of: [markdown | html]",
      "                specify what the script should output",
      "                if you are intending to publish on github pages",
      "                set to markdown",
      "                (optional - default markdown)",
      "",
      "-i --inputdir   takes 1 argument",
      "                the root directory of the code",
      "                defaults to the current directory",
      "",
      "-j --jekyll     takes no arguments",
      "                generates the contents metadata for jekyll",
      "                (Jekyll is the site generator for Github Pages)",
      "                (optional - defaults to off)",
      "",
      "-l --list       takes no arguments",
      "                doesn't process the files, just prints them",
      "                (optional)",
      "",
      "-o --outputdir  takes 1 argument",
      "                the directory to output the html",
      "                defaults to the current directory",
      "",
      "-t --type       takes 1 argument",
      "                the type of output - usually 0, 1 or 2",
      "                optional - defaults to 0",
      "                0 is all comments shown",
      "                1 is some comments surpressed",
      "                2 is maximum comments supressed",
      "                (but see the documentation of a particular language",
      "                extension for details)",
      "",
      "Either the inputdir or the outputdir must be set explicitly",
      "",
      "Examples:",
      "./literate_compiler -o /some/dir/for/output",
      "./literate_compiler --outputdir /some/dir/for/output",
      "./literate_compiler -i /some/dir/for/output -o /some/dir/for/output",
      "./literate_compiler --help",
      "./literate_compiler -i /some/dir/for/output -l",
      ""
    ]
    for x <- lines, do: IO.puts(x)
   end

   def print_errors(parsedargs) do
    IO.puts("script did not run because of the following errors:")
    for x <- parsedargs.errors, do: IO.puts(x)
    IO.puts("")
   end

Private Functions

parse_args places the inputs into the data structure when the arguments have been consumed it calles validate on the data structure


   defp parse_args([],                      args), do: validate(args)
   defp parse_args(["-h"              | t], args), do: parse_args(t, %LiterateCompiler.Args{args | help:        true})
   defp parse_args(["--help"          | t], args), do: parse_args(t, %LiterateCompiler.Args{args | help:        true})
   defp parse_args(["-e"         , e  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | excludes:    e})
   defp parse_args(["--exclude"  , e  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | excludes:    e})
   defp parse_args(["-f"         , f  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | format:      f})
   defp parse_args(["--format"   , f  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | format:      f})
   defp parse_args(["-i"         , i  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | inputdir:    i})
   defp parse_args(["--inputdir" , i  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | inputdir:    i})
   defp parse_args(["-j"              | t], args), do: parse_args(t, %LiterateCompiler.Args{args | make_jekyll: true})
   defp parse_args(["--jekyll"        | t], args), do: parse_args(t, %LiterateCompiler.Args{args | make_jekyll: true})
   defp parse_args(["-l"              | t], args), do: parse_args(t, %LiterateCompiler.Args{args | print_files: true})
   defp parse_args(["--list"          | t], args), do: parse_args(t, %LiterateCompiler.Args{args | print_files: true})
   defp parse_args(["-o"         , o  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | outputdir:   o})
   defp parse_args(["--outputdir", o  | t], args), do: parse_args(t, %LiterateCompiler.Args{args | outputdir:   o})
   defp parse_args(["-t"         , ty | t], args), do: parse_args(t, %LiterateCompiler.Args{args | print_type:  ty})
   defp parse_args(["--type"     , ty | t], args), do: parse_args(t, %LiterateCompiler.Args{args | print_type:  ty})
   defp parse_args([h | t], args) do
      error = case h do
          <<"-", _rest::binary>> -> "unknown option #{h}"
          _                      -> "unknown action #{h}"
       end
      newerrors = args.errors ++ [error]
      parse_args(t, %LiterateCompiler.Args{args | errors: newerrors})
   end

The validate pipeline places all and any errors on the errors key The reason for this is to avoid the irritating fix and run again problem If the user makes multiple mistakes in their invocation they should be told all of them and not just the first


   defp validate(args) do
      args
      |> validate_usage
      |> validate_formats
      |> validate_paths
      |> validate_print_type
      |> validate_excludes
   end

   defp validate_usage(args) do
      case {args.inputdir, args.outputdir} do
         {:nil, :nil} -> newerrors = ["both inputdir and outputdir can't be unspecified" | args.errors]
                         %{args | errors:   newerrors}
         {:nil, _}    -> %{args | inputdir:  "./"}
         {_,    :nil} -> %{args | outputdir: "./"}
         {_,    _}    ->   args
      end
   end

   defp validate_formats(%{format: "markdown"}       = args), do: %{args | format: "md"}
   defp validate_formats(%{format: "html"}           = args), do: args
   defp validate_formats(%{format: other, errors: e} = args) do
      error = "invalid output format, must be html or markdown: #{other}"
      %{args | errors: [error | e]}
   end

   defp validate_paths(%{errors: []} = args) do
      inputDirIsDir  = File.dir?(args.inputdir)
      outputDirIsDir = File.dir?(args.outputdir)
      case {inputDirIsDir, outputDirIsDir} do
         {true,  true}  -> args
         {true,  false} -> newerrors = ["output dir #{args.outputdir} is not a directory" | args.errors]
                           %{args | errors:   newerrors}
         {false, true}  -> newerrors = ["input dir #{args.inputdir} is not a directory" | args.errors]
                           %{args | errors:   newerrors}
         {false, false} -> newerrors = ["neither input dir#{args.inputdir} nor output dir #{args.outputdir} is a directory" | args.errors]
                           %{args | errors:   newerrors}
      end
   end
   defp validate_paths(args), do: args

   defp validate_print_type(%{print_type: n} = args) when is_integer(n), do: args
   defp validate_print_type(%{print_type: p} = args) do
      case Integer.parse(p) do
         {n, ""} -> %{args | print_type: n}
         {_, _}  -> %{args | errors: "print level must be an integer: #{p}"}
         err     -> %{args | errors: "print level must be an integer: #{err}"}
      end
   end

   defp validate_excludes(%{excludes: e} = args) when e == :nil do
      case File.exists?("./.literate_compiler.exclude") do
         true  -> excludes = read_excludes("./.literate_compiler.exclude")
                  %{args | excludes: excludes}
         false -> %{args | excludes: []}
      end
   end
   defp validate_excludes(%{excludes: e, errors: errs} = args) do
      case File.exists?(e) do
         true  -> excludes = read_excludes(e)
                  %{args | excludes: excludes}
         false -> newerrs = ["exclude file doesn't exist: #{e}" | errs]
                  %{args | errors: newerrs}
      end
   end

   defp read_excludes(file) do
      {:ok, contents} = File.read(file)
      String.split(contents, "\n")
   end

end