blob: 4a859f8d8b295f7f8da3c5dd9505a3f235b39225 [file] [log] [blame]
% Licensed 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.
%% @doc Saves a Key/Value pair to a ini file. The Key consists of a Section
%% and Option combination. If that combination is found in the ini file
%% the new value replaces the old value. If only the Section is found the
%% Option and value combination is appended to the Section. If the Section
%% does not yet exist in the ini file, it is added and the Option/Value
%% pair is appended.
%% @see couch_config
-module(couch_config_writer).
-include("couch_db.hrl").
-export([save_to_file/2]).
%% @spec save_to_file(
%% Config::{{Section::string(), Option::string()}, Value::string()},
%% File::filename()) -> ok
%% @doc Saves a Section/Key/Value triple to the ini file File::filename()
save_to_file({{Section, Option}, Value}, File) ->
?LOG_DEBUG("saving to file '~s', Config: '~p'", [File, {{Section, Option}, Value}]),
% open file and create a list of lines
{ok, Stream} = file:read_file(File),
OldFileContents = binary_to_list(Stream),
Lines = re:split(OldFileContents, "\r\n|\n|\r|\032", [{return, list}]),
% prepare input variables
SectionName = "[" ++ Section ++ "]",
OptionList = Option,
% produce the contents for the config file
NewFileContents =
case {NewFileContents2, DoneOptions} = save_loop({{SectionName, OptionList}, Value}, Lines, "", "", []) of
% we didn't change anything, that means we couldn't find a matching
% [ini section] in which case we just append a new one.
{OldFileContents, DoneOptions} ->
% but only if we haven't actually written anything.
case lists:member(OptionList, DoneOptions) of
true -> OldFileContents;
_ -> append_new_ini_section({{SectionName, OptionList}, Value}, OldFileContents)
end;
_ ->
NewFileContents2
end,
ok = file:write_file(File, list_to_binary(NewFileContents)),
ok.
%% @doc Iterates over the lines of an ini file and replaces or adds a new
%% configuration directive.
save_loop({{Section, Option}, Value}, [Line|Rest], OldCurrentSection, Contents, DoneOptions) ->
% if we find a new [ini section] (Section), save that for reference
NewCurrentSection = parse_module(Line, OldCurrentSection),
% if the current Section is the one we want to change, try to match
% each line with the Option
NewContents =
case NewCurrentSection of
Section ->
case OldCurrentSection of
NewCurrentSection -> % we already were in [Section]
case lists:member(Option, DoneOptions) of
true -> % we already replaced Option, do nothing
DoneOptions2 = DoneOptions,
Line;
_ -> % we haven't written our Option yet
case parse_variable(Line, Option, Value) of
nomatch ->
DoneOptions2 = DoneOptions,
Line;
NewLine ->
DoneOptions2 = [Option|DoneOptions],
NewLine
end
end;
_ -> % we got into a new [section]
{NewLine, DoneOptions2} = append_var_to_section(
{{Section, Option}, Value},
Line,
OldCurrentSection,
DoneOptions),
NewLine
end;
_ -> % we are reading [NewCurrentSection]
{NewLine, DoneOptions2} = append_var_to_section(
{{Section, Option}, Value},
Line,
OldCurrentSection,
DoneOptions),
NewLine
end,
% clumsy way to only append a newline character if the line is not empty. We need this to
% avoid having a newline inserted at the top of the target file each time we save it.
Contents2 = case Contents of "" -> ""; _ -> Contents ++ "\n" end,
% go to next line
save_loop({{Section, Option}, Value}, Rest, NewCurrentSection, Contents2 ++ NewContents, DoneOptions2);
save_loop({{Section, Option}, Value}, [], OldSection, NewFileContents, DoneOptions) ->
case lists:member(Option, DoneOptions) of
% append Deferred Option
false when Section == OldSection ->
{NewFileContents ++ "\n" ++ Option ++ " = " ++ Value ++ "\n", DoneOptions};
% we're out of new lines, just return the new file's contents
_ -> {NewFileContents, DoneOptions}
end.
append_new_ini_section({{SectionName, Option}, Value}, OldFileContents) ->
OldFileContents ++ "\n" ++ SectionName ++ "\n" ++ Option ++ " = " ++ Value ++ "\n".
append_var_to_section({{Section, Option}, Value}, Line, OldCurrentSection, DoneOptions) ->
case OldCurrentSection of
Section -> % append Option to Section
case lists:member(Option, DoneOptions) of
false ->
{Option ++ " = " ++ Value ++ "\n\n" ++ Line, [Option|DoneOptions]};
_ ->
{Line, DoneOptions}
end;
_ ->
{Line, DoneOptions}
end.
%% @spec parse_module(Line::string(), OldSection::string()) -> string()
%% @doc Tries to match a line against a pattern specifying a ini module or
%% section ("[Section]"). Returns OldSection if no match is found.
parse_module(Line, OldSection) ->
case re:run(Line, "^\\[([a-zA-Z0-9\_-]*)\\]$", [{capture, first}]) of
nomatch ->
OldSection;
{match, [{Start, Length}]} ->
string:substr(Line, Start+1, Length)
end.
%% @spec parse_variable(Line::string(), Option::string(), Value::string()) ->
%% string() | nomatch
%% @doc Tries to match a variable assignment in Line. Returns nomatch if the
%% Option is not found. Returns a new line composed of the Option and
%% Value otherwise.
parse_variable(Line, Option, Value) ->
case re:run(Line, "^" ++ Option ++ "\s?=", [{capture, none}]) of
nomatch ->
nomatch;
match ->
Option ++ " = " ++ Value
end.