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