1 /* This file is part of the 'hgen' project.
2  *
3  * Copyright (c) 2014-2022
4  *     Economic Modeling Specialists, Intl.,
5  *     the D Community.
6  *
7  * Main contributors:
8  *     Ferdinand Majerech
9  *     Eugene Stulin
10  *
11  * Distributed under the Boost Software License, Version 1.0.
12  *
13  * You should have received a copy of the Boost Software License
14  * along with this program. If not, see <http://www.boost.org/LICENSE_1_0.txt>.
15  * This file is offered as-is, without any warranty.
16  */
17 
18 /// Config loading and writing.
19 module config;
20 
21 
22 import std.algorithm;
23 import std.array;
24 import std.conv: to;
25 import std.stdio;
26 import std.string;
27 
28 
29 /*******************************************************************************
30  * Stores configuration data loaded from command-line or config files.
31  *
32  * Note that multiple calls to loadCLI/loadConfigFile are supported;
33  * data loaded with earlier calls is overwritten by later calls
34  * (e.g. command-line overriding config file),
35  * except arrays like macroFileNames/excludes/sourcePaths:
36  * successive calls always add to these arrays instead of overwriting them,
37  * so e.g. extra modules can be excluded with command-line.
38  */
39 struct Config {
40     bool doHelp = false;
41     bool showVersion = false;
42     bool performance = false;
43     bool doGenerateConfig = false;
44     bool doGenerateConfigForLinux = false;
45     string doGenerateCSSPath = null;
46     string[] macroFileNames = [];
47     string indexFileName = null;
48     string[] tocAdditionalFileNames = [];
49     string[] tocAdditionalStrings = [];
50     string cssFileName = null;
51     string outputDirectory = "./doc";
52     string format = "html-aggregated";
53     string projectName = null;
54     bool noMarkdown = false;
55     string projectVersion = null;
56     uint maxFileSizeK = 16384;
57     uint maxModuleListLength = 256;
58     /// Names of packages and modules to exclude from generated documentation.
59     string[] excludes = [];
60     string[] sourcePaths = [];
61 
62     /// Loaded from macroFileNames + default macros;
63     /// not set on the command-line.
64     string[string] macros;
65 
66     /***************************************************************************
67      * Load config options from CLI arguments.
68      *
69      * Params:
70      * cliArgs = Command-line args.
71      */
72     void loadCLI(string[] cliArgs) {
73         import std.getopt;
74 
75         // If the user requests a config file,
76         // we must look for that option  first and process it
77         // before other options so the config file doesn't override CLI options
78         // (it would override them if loaded after processing the CLI options).
79         string configFile;
80         string[] newMacroFiles;
81         string[] newExcludes;
82         try {
83             // -h/--help will not pass through due
84             // to the autogenerated help option
85             auto firstResult = getopt(
86                 cliArgs,
87                 std.getopt.config.caseSensitive,
88                 std.getopt.config.passThrough,
89                 "config|F",
90                 &configFile
91             );
92             doHelp = firstResult.helpWanted;
93             if (configFile !is null) {
94                 loadConfigFile(configFile, true);
95             }
96 
97             auto getoptResult = getopt(
98                 cliArgs, std.getopt.config.caseSensitive,
99                 "css|c",                    &cssFileName,
100                 "generate-css|C",           &doGenerateCSSPath,
101                 "exclude|e",                &newExcludes,
102                 "format|f",                 &format,
103                 "generate-cfg|g",           &doGenerateConfig,
104                 "generate-cfg-linux",       &doGenerateConfigForLinux,
105                 "index|i",                  &indexFileName,
106                 "macros|m",                 &newMacroFiles,
107                 "max-file-size|M",          &maxFileSizeK,
108                 "output-directory|o",       &outputDirectory,
109                 "project-name|p",           &projectName,
110                 "project-version|n",        &projectVersion,
111                 "no-markdown|D",            &noMarkdown,
112                 "toc-additional|t",         &tocAdditionalFileNames,
113                 "toc-additional-direct|T",  &tocAdditionalStrings,
114                 "max-module-list-length|l", &maxModuleListLength,
115                 "version",                  &showVersion,
116                 "performance",              &performance   
117             );
118         } catch(Exception e) {
119             writeln("Failed to parse command-line arguments: ", e.msg);
120             writeln("Maybe try 'hgen -h' for help information?");
121             return;
122         }
123 
124         macroFileNames  ~= newMacroFiles;
125         excludes        ~= newExcludes;
126         sourcePaths     ~= cliArgs[1 .. $];
127     }
128 
129     /***************************************************************************
130      * Load specified config file and add loaded data to the configuration.
131      *
132      * Params:
133      *
134      * fileName        = Name of the config file.
135      * requestedByUser = If true, this is not the default config file and
136      *                   has been explicitly requested by the user, i.e. we have
137      *                   to inform the user if the file was not found.
138      *
139      */
140     void loadConfigFile(string fileName, bool requestedByUser = false) {
141         import std.file: exists, isFile;
142         import std.typecons: tuple;
143 
144         if (!fileName.exists || !fileName.isFile) {
145             if (requestedByUser) {
146                 writefln("Config file '%s' not found", fileName);
147             }
148             return;
149         }
150 
151         writefln("Loading config file '%s'", fileName);
152         try {
153             auto keyValues =
154                 File(fileName)
155                 .byLine
156                 .map!(l => l.until!(c => ";#".canFind(c)))
157                 .map!array
158                 .map!strip
159                 .filter!(s => !s.empty && s.canFind("="))
160                 .map!(l => l.findSplit("="))
161                 .map!(p => tuple(p[0].strip.to!string, p[2].strip.to!string))
162                 .filter!(p => !p[0].empty);
163 
164             foreach (key, value; keyValues) {
165                 processConfigValue(key, value);
166             }
167         } catch(Exception e) {
168             writefln("Failed to parse config file '%s': %s", fileName, e.msg);
169         }
170     }
171 
172 private:
173     void processConfigValue(string key, string value) {
174         // ensures something like "macros = " won't add an empty string value
175         void add(ref string[] array, string value) {
176             if (!value.empty) {
177                 array ~= value;
178             }
179         }
180 
181         switch(key) {
182             case "help":          doHelp = value.to!bool;           break;
183             case "generate-cfg":  doGenerateConfig = value.to!bool; break;
184             case "generate-css":  doGenerateCSSPath = value;        break;
185             case "macros":        add(macroFileNames, value);       break;
186             case "max-file-size": maxFileSizeK = value.to!uint;     break;
187             case "max-module-list-length":
188                 maxModuleListLength = value.to!uint;            break;
189             case "project-name":    projectName = value;        break;
190             case "project-version": projectVersion = value;     break;
191             case "no-markdown":     noMarkdown = value.to!bool; break;
192             case "index":           indexFileName = value;      break;
193             case "toc-additional":
194                 if (value !is null) tocAdditionalFileNames ~= value;   break;
195             case "toc-additional-direct":
196                 if (value !is null) tocAdditionalStrings ~= value;     break;
197             case "css":              cssFileName = value;              break;
198             case "output-directory": outputDirectory = value;          break;
199             case "exclude":          add(excludes, value);             break;
200             case "config": if (value) loadConfigFile(value, true);     break;
201             case "source": add(sourcePaths, value);                    break;
202             default: writefln("Unknown key in config file: '%s'", key);
203         }
204     }
205 }
206 
207 
208 immutable string[string] helpTranslations;
209 shared static this() {
210     helpTranslations["en"] = import("help").strip;
211     helpTranslations["ru"] = import("help_ru").strip;
212 }
213 
214 immutable string versionString = import("version");
215 immutable string defaultConfigString = import("hgen.cfg");