Added first version of ENI parser.
authorStanislaw Klekot <dozzie@jarowit.net>
Mon, 10 Feb 2014 19:21:03 +0000 (20:21 +0100)
committerStanislaw Klekot <dozzie@jarowit.net>
Mon, 10 Feb 2014 19:21:03 +0000 (20:21 +0100)
It's fairly simple, so I think this will be last one, barring documentation
updates.

examples/run [new file with mode: 0755]
src/.gitignore [new file with mode: 0644]
src/eni.erl [new file with mode: 0644]
src/eni_lexer.xrl [new file with mode: 0644]
src/eni_parser.yrl [new file with mode: 0644]

diff --git a/examples/run b/examples/run
new file mode 100755 (executable)
index 0000000..10bba6d
--- /dev/null
@@ -0,0 +1,61 @@
+#!/usr/bin/escript
+%%! -pz ebin
+
+main([]) ->
+  main(["--help"]);
+main(["--help"]) ->
+  io:fwrite("Usage:~n  ~s [--parser] filename~n  ~s --lexer filename~n"
+                    "  ~s --eni filename~n",
+            [escript:script_name(), escript:script_name(), escript:script_name()]),
+  ok;
+
+main(["--lexer", File]) ->
+  io:fwrite("# lexer~n"),
+  {ok, B} = file:read_file(File),
+  case eni_lexer:string(binary_to_list(B)) of
+    {ok, Tokens, _EOFLineNum} ->
+      io:fwrite("~p~n", [Tokens]);
+    {error, Reason} ->
+      io:fwrite("!! ~p~n", [Reason])
+  end,
+  ok;
+
+main(["--eni", File]) ->
+  io:fwrite("eni~n"),
+  case eni:file(File) of
+    {ok, {A, B}} ->
+      io:fwrite("# options without section~n"),
+      [io:fwrite("  - ~p -> ~p~n", [N,V]) || {N,V} <- A],
+      io:fwrite("# sections~n"),
+      [io:fwrite("[~p] => ~p~n", [N,V]) || {N,V} <- B],
+      ok;
+    {error, Reason} ->
+      io:fwrite("!! ENI error:~n~p~n", [Reason])
+  end,
+  ok;
+
+main([File]) ->
+  main(["--parser", File]);
+main(["--parser", File]) ->
+  io:fwrite("parser~n"),
+  {ok, B} = file:read_file(File),
+  case eni_lexer:string(binary_to_list(B)) of
+    {ok, Tokens, _EOFLineNum} ->
+      case eni_parser:parse(Tokens) of
+        {ok, {NoSection, Sections}} ->
+          io:fwrite("~p~n", [NoSection]),
+          [io:fwrite("~p -> ~p~n", [N,V]) || {N,V} <- Sections];
+        {error, Reason} ->
+          io:fwrite("@! ~p~n", [Reason]);
+        _Any ->
+          io:fwrite("WTF? ~p~n", [_Any])
+      end;
+    {error, Reason} ->
+      io:fwrite("*! ~p~n", [Reason])
+  end,
+  ok;
+
+main(_) ->
+  main(["--help"]).
+
+%% vim:ft=erlang
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644 (file)
index 0000000..8cd8cdb
--- /dev/null
@@ -0,0 +1,2 @@
+eni_lexer.erl
+eni_parser.erl
diff --git a/src/eni.erl b/src/eni.erl
new file mode 100644 (file)
index 0000000..2b80b26
--- /dev/null
@@ -0,0 +1,66 @@
+%%%---------------------------------------------------------------------------
+%%% @doc
+%%%   INI file parser.
+%%% @end
+%%%---------------------------------------------------------------------------
+
+-module(eni).
+
+-export([file/1, string/1]).
+
+%%%---------------------------------------------------------------------------
+%%% types
+
+%% @type config() = {option_list(), [section()]}.
+
+-type config() :: {option_list(), [section()]}.
+
+%% @type section() = {section_name(), option_list()}.
+
+-type section() :: {section_name(), option_list()}.
+
+%% @type section_name() = atom().
+
+-type section_name() :: atom().
+
+%% @type option_list() = [{atom(), term()}].
+
+-type option_list() :: [{atom(), term()}].
+
+%%%---------------------------------------------------------------------------
+
+%% @doc Load configuration from file.
+%% @spec file(string()) -> config() | {error,Reason}
+
+-spec file(string()) -> config() | {error,term()}.
+
+file(File) ->
+  case file:read_file(File) of
+    {ok, Content} ->
+      string(Content);
+    {error, _Reason} = Error ->
+      Error
+  end.
+
+%% @doc Load configuration from string.
+%% @spec string(string()) -> config() | {error,Reason}
+
+-spec string(string() | binary()) -> config() | {error,term()}.
+
+string(String) when is_binary(String) ->
+  string(binary_to_list(String));
+string(String) when is_list(String) ->
+  case eni_lexer:string(String) of
+    {ok, Tokens, _EOFLineNum} ->
+      case eni_parser:parse(Tokens) of
+        {ok, {_NoSectionOpts, _Sections}} = Result ->
+          Result;
+        {error, _Reason} = Error ->
+          Error
+      end;
+    {error, Reason, _LineNum} ->
+      {error, Reason}
+  end.
+
+%%%---------------------------------------------------------------------------
+%%% vim:ft=erlang:foldmethod=marker
diff --git a/src/eni_lexer.xrl b/src/eni_lexer.xrl
new file mode 100644 (file)
index 0000000..7a18ee8
--- /dev/null
@@ -0,0 +1,69 @@
+Definitions.
+
+ID = [a-zA-Z0-9._-]+
+S  = [\s\t\r]*
+
+%%%---------------------------------------------------------------------------
+
+Rules.
+
+% comment
+{S}([%#;].*)?\n : skip_token.
+
+% section header
+\[{ID}\]{S}\n     : {token, {section, TokenLine, section(TokenChars)}}.
+
+% regular option (string = string)
+{S}{ID}{S}={S}.* : {token, {option,  TokenLine, option(TokenChars)}}.
+% Erlang term option, ended with period
+{S}{ID}{S}:={S}.*\.{S} :
+  case option(TokenChars) of
+    {ok, Value} ->
+      {token, {option, TokenLine, Value}};
+    {error, _Reason} ->
+      {error, "Invalid token"}
+  end.
+% Erlang term option, not ended with period
+{S}{ID}{S}:={S}.* :
+  case option(TokenChars ++ ".") of
+    {ok, Value} ->
+      {token, {option, TokenLine, Value}};
+    {error, _Reason} ->
+      {error, "Invalid token"}
+  end.
+
+\n : skip_token.
+
+%%%---------------------------------------------------------------------------
+
+Erlang code.
+
+% I'm sure it will start with "[" and will contain "]"
+section("[" ++ String) ->
+  P = string:chr(String, $]),
+  list_to_atom(string:substr(String, 1, P - 1)).
+
+option(String) ->
+  try
+    {Name, Value} = split(String),
+    {ok, {list_to_atom(Name), Value}}
+  catch
+    % TODO: better error reporting
+    _:_ -> {error, badarg}
+  end.
+
+split("=" ++ Value) ->
+  {"", string:strip(Value)}; % TODO: strip "\t" and "\r"
+split(":=" ++ Value) ->
+  % TODO: catch errors here?
+  {ok, Tokens, _LineNo} = erl_scan:string(Value),
+  {ok, Term} = erl_parse:parse_term(Tokens),
+  {"", Term};
+split([C | String]) when C == $  orelse C == $\t ->
+  split(String); % skip
+split([C | String]) ->
+  {ID, Value} = split(String),
+  {[C | ID], Value}.
+
+%%%---------------------------------------------------------------------------
+%%% vim:ft=erlang
diff --git a/src/eni_parser.yrl b/src/eni_parser.yrl
new file mode 100644 (file)
index 0000000..d11a1fe
--- /dev/null
@@ -0,0 +1,44 @@
+%%%---------------------------------------------------------------------------
+
+%Header
+%  "%%% ..."
+%.
+
+Nonterminals
+  config
+  option_list
+  section_list one_section
+.
+
+Terminals
+  option section
+.
+
+Rootsymbol config.
+
+%%%---------------------------------------------------------------------------
+
+config -> option_list section_list : {lists:reverse('$1'), lists:reverse('$2')}.
+
+option_list -> option_list option : [value('$2') | '$1'].
+option_list -> '$empty' : [].
+
+section_list -> section_list one_section : ['$2' | '$1'].
+section_list -> '$empty' : [].
+
+one_section -> section option_list : {value('$1'), lists:reverse('$2')}.
+
+%%%---------------------------------------------------------------------------
+
+Erlang code.
+
+%terminal({TermName, _Line}) ->
+%  TermName;
+%terminal({TermName, _Line, _Value}) ->
+%  TermName.
+
+value({_TermName, _Line, Value}) ->
+  Value.
+
+%%%---------------------------------------------------------------------------
+%%% vim:ft=erlang