Current section
Files
Jump to
Current section
Files
src/conf_parse.peg
%% -------------------------------------------------------------------
%%
%% conf_parse: for all your .conf parsing needs.
%%
%% Copyright (c) 2013 Basho Technologies, Inc. All Rights Reserved.
%% Copyright (c) 2019 Pivotal Software, Inc. All rights reserved.
%% Copyright (c) 2020 VMware, Inc. or its affiliates. All rights reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
% -------------------------------------------------------------------
% NOTE: IMPORTANT
%
% After using neotoma to re-generate conf_parse.erl, you MUST
% edit that file to change the exported file/1 function to this code:
%
% -spec file(file:name()) -> any().
% file(Filename) ->
% AbsFilename = filename:absname(Filename),
% case erl_prim_loader:get_file(AbsFilename) of
% {ok, Bin, _} -> parse(Bin);
% error -> {error, undefined}
% end.
%
% The reason is that the above code allows for reading cuttlefish
% schemas from .ez archives
% -------------------------------------------------------------------
%% A configuration file may have zero-or-more lines.
config <- line* %{
[ L || L <- Node, is_setting(L) ]
%};
%% Lines are actual settings, includes, comments, or horizontal whitespace,
%% terminated by an end-of-line or end-of-file.
line <- ((setting / include / comment / ws+) (crlf / eof)) / crlf %{
case Node of
[ Line, _EOL ] -> Line;
Line -> Line
end
%};
%% A setting is a key and a value, joined by =, with surrounding
%% whitespace ignored.
setting <- ws* key ws* "=" ws* (multiline_value / escaped_value / unescaped_value) ws* comment? %{
[ _, Key, _, _Eq, _, Value, _, _ ] = Node,
{Key, try_unicode_characters_to_list(Value, Idx)}
%};
%% A key is a series of dot-separated identifiers.
key <- head:word tail:("." word)* %{
[{head, H}, {tail, T}] = Node,
[try_unicode_characters_to_list(H, Idx)| [try_unicode_characters_to_list(W, Idx) || [_, W] <- T]]
%};
%% A multiline value is any character between two pairs of triple single quotes, spanning lines
multiline_value <- "'''" ((!"'''" .)* / crlf) "'''" %{
Node0 = string:trim(Node, both, [$',$\n,[$\r,$\n]]),
try_unicode_characters_to_list(Node0 Idx)
%};
%% An escaped value is any character between single quotes except for EOF
escaped_value <- "'" (!"'" .)* "'" %{
Stripped = string:trim(Node, both, [$']),
try_unicode_characters_to_list(Stripped, Idx)
%};
%% A value is any character, with trailing whitespace stripped.
unescaped_value <- (!((ws* crlf) / comment) .)+ %{
try_unicode_characters_to_list(Node, Idx)
%};
%% A comment is any line that begins with a # sign, leading whitespace
%% allowed.
comment <- ws* "#" (!crlf .)* `comment`;
%% An include is a line that begins with 'include' and something.
include <- ws* "include" ws+ included_file_or_dir comment? %{
[_, _Include, _, Included, _] = Node,
{include, Included}
%};
included_file_or_dir <- [A-Za-z0-9-\_\.\*\/]+ %{
try_unicode_characters_to_binary(Node, Idx)
%};
%% A word is one or more of letters, numbers and dashes or
%% underscores.
word <- ("\\." / [A-Za-z0-9_-])+ %{
unescape_dots(try_unicode_characters_to_list(Node, Idx))
%};
%% An end-of-line is signified by a line-feed with an optional
%% preceding carriage-return.
crlf <- "\r"? "\n" `ws`;
%% The end-of-file is where no character matches.
eof <- !. `ws`;
%% Whitespace is either spaces or tabs.
ws <- [ \t]+ `ws`;
% Erlang code
%{
%% -------------------------------------------------------------------
%%
%% conf_parse: for all your .conf parsing needs.
%%
%% Copyright (c) 2013 Basho Technologies, Inc. All Rights Reserved.
%% Copyright (c) 2019 Pivotal Software, Inc. All rights reserved.
%% Copyright (c) 2020 VMware, Inc. or its affiliates. All rights reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
%% This module implements the parser for a sysctl-style
%% configuration format. Example:
%%
%% ```
%% riak.local.node = riak@127.0.0.1
%% riak.local.http = 127.0.0.1:8098
%% riak.local.pb = 127.0.0.1:8087
%% riak.local.storage.backend = bitcask'''
%%
%% This would parse into the following flat proplist:
%%
%% ```
%% [{<<"riak.local.node">>,<<"riak@127.0.0.1">>},
%% {<<"riak.local.http">>,<<"127.0.0.1:8098">>},
%% {<<"riak.local.pb">>,<<"127.0.0.1:8087">>},
%% {<<"riak.local.storage.backend">>,<<"bitcask">>}]'''
%%
%% Other modules in this application interpret and validate the
%% result of a successful parse.
%% @end
-define(line, true).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
%% @doc Only let through lines that are not comments or whitespace.
is_setting(ws) -> false;
is_setting([ws]) -> false;
is_setting(comment) -> false;
is_setting(_) -> true.
%% @doc Removes escaped dots from keys
unescape_dots([$\\,$.|Rest]) ->
[$.|unescape_dots(Rest)];
unescape_dots([]) -> [];
unescape_dots([C|Rest]) ->
[C|unescape_dots(Rest)].
try_unicode_characters_to_binary(Node, Idx) ->
case unicode:characters_to_binary(Node) of
{incomplete, _List, _RestBin} ->
throw({error, {conf_to_unicode, line(Idx)}});
{error, _List, _RestData} ->
throw({error, {conf_to_unicode, line(Idx)}});
Chardata when is_binary(Chardata)->
Chardata
end.
try_unicode_characters_to_list(Node, Idx) ->
case unicode:characters_to_list(Node) of
{incomplete, _List, _RestBin} ->
throw({error, {conf_to_unicode, line(Idx)}});
{error, _List, _RestData} ->
throw({error, {conf_to_unicode, line(Idx)}});
Chardata when is_list(Chardata)->
Chardata
end.
-ifdef(TEST).
file_test() ->
Conf = conf_parse:file("test/riak.conf"),
?assertEqual([
{["ring_size"],"32"},
{["anti_entropy"],"debug"},
{["log","error","file"],"/var/log/error.log"},
{["log","console","file"],"/var/log/console.log"},
{["log","syslog"],"on"},
{["listener","http","internal"],"127.0.0.1:8098"},
{["listener","http","external"],"10.0.0.1:80"}
], Conf),
ok.
included_file_test() ->
Conf = conf_parse:file("test/include_file.conf"),
?assertEqual([
{include,<<"riak.conf">>}
], Conf),
ok.
included_dir_test() ->
Conf = conf_parse:file("test/include_dir.conf"),
?assertEqual([
{include,<<"conf.d/*.conf">>}
], Conf),
ok.
escaped_dots_are_removed_test() ->
Conf = conf_parse:parse("#comment\nsetting\\.0 = thing0\n"),
?assertEqual([
{["setting.0"],"thing0"}
], Conf),
ok.
utf8_test() ->
Expected = [{["setting"], [116,104,105,110,103,338]}],
Actual = conf_parse:parse("setting = thing" ++ [338] ++ "\n"),
?assertMatch(Expected, Actual),
ok.
invalid_included_file_test() ->
Conf = conf_parse:file("test/invalid_include_file.conf"),
?assertMatch({[], _PathWithNewLineAndCarriage, {{line,_}, {column, _}}}, Conf),
ok.
invalid_included_dir_test() ->
Conf = conf_parse:file("test/invalid_include_dir.conf"),
?assertMatch({[], _PathWithNewLineAndCarriage, {{line, _},{column, _}}}, Conf),
ok.
escaped_string_test() ->
Expected = [{["setting"],"e9238-7_49%#sod7"}],
Actual = conf_parse:parse("setting = 'e9238-7_49%#sod7'" ++ "\n"),
?assertMatch(Expected, Actual),
ok.
gh_1_two_tab_test() ->
Conf = conf_parse:parse("setting0 = thing0\n\t\t\nsetting1 = thing1\n"),
?assertEqual([
{["setting0"],"thing0"},
{["setting1"],"thing1"}
], Conf),
ok.
gh_1_three_tab_test() ->
Conf = conf_parse:parse("setting0 = thing0\n\t\t\t\nsetting1 = thing1\n"),
?assertEqual([
{["setting0"],"thing0"},
{["setting1"],"thing1"}
], Conf),
ok.
-endif.
%}