1 //          Copyright Brian Schott 2015.
2 // Distributed under the Boost Software License, Version 1.0.
3 //    (See accompanying file LICENSE.txt or copy at
4 //          http://www.boost.org/LICENSE_1_0.txt)
5 
6 module dfmt.main;
7 static immutable VERSION = () {
8     debug
9     {
10         enum DEBUG_SUFFIX = "-debug";
11     }
12     else
13     {
14         enum DEBUG_SUFFIX = "";
15     }
16 
17     static if (is(typeof(import("VERSION"))))
18     {
19         // takes the `git describe --tags` output and removes the leading
20         // 'v' as well as any kind of newline
21         // if the tag is considered malformed it gets used verbatim
22 
23         enum gitDescribeOutput = import("VERSION");
24 
25         string result;
26 
27         if (gitDescribeOutput[0] == 'v')
28             result = gitDescribeOutput[1 .. $];
29         else
30             result = null;
31 
32         uint minusCount;
33 
34         foreach (i, c; result)
35         {
36             if (c == '\n' || c == '\r')
37             {
38                 result = result[0 .. i];
39                 break;
40             }
41 
42             if (c == '-')
43             {
44                 ++minusCount;
45             }
46         }
47 
48         if (minusCount > 1)
49             result = null;
50 
51         return result ? result ~ DEBUG_SUFFIX
52             : gitDescribeOutput ~ DEBUG_SUFFIX;
53 
54     }
55     else
56     {
57         return "unknown"  ~ DEBUG_SUFFIX ~ "-version";
58     }
59 
60 } ();
61 
62 
63 version (NoMain)
64 {
65 }
66 else
67 {
68     import std.array : front, popFront;
69     import std.stdio : stdout, stdin, stderr, writeln, File;
70     import dfmt.config : Config;
71     import dfmt.formatter : format;
72     import std.path : buildPath, dirName, expandTilde;
73     import dfmt.editorconfig : getConfigFor;
74     import std.getopt : getopt, GetOptException;
75 
76     int main(string[] args)
77     {
78         bool inplace = false;
79         Config optConfig;
80         optConfig.pattern = "*.d";
81         bool showHelp;
82         bool showVersion;
83         string explicitConfigDir;
84 
85         void handleBooleans(string option, string value)
86         {
87             import dfmt.editorconfig : OptionalBoolean;
88             import std.exception : enforceEx;
89 
90             enforceEx!GetOptException(value == "true" || value == "false", "Invalid argument");
91             immutable OptionalBoolean optVal = value == "true" ? OptionalBoolean.t
92                 : OptionalBoolean.f;
93             switch (option)
94             {
95             case "align_switch_statements":
96                 optConfig.dfmt_align_switch_statements = optVal;
97                 break;
98             case "outdent_attributes":
99                 optConfig.dfmt_outdent_attributes = optVal;
100                 break;
101             case "space_after_cast":
102                 optConfig.dfmt_space_after_cast = optVal;
103                 break;
104             case "space_before_function_parameters":
105                 optConfig.dfmt_space_before_function_parameters = optVal;
106                 break;
107             case "split_operator_at_line_end":
108                 optConfig.dfmt_split_operator_at_line_end = optVal;
109                 break;
110             case "selective_import_space":
111                 optConfig.dfmt_selective_import_space = optVal;
112                 break;
113             case "compact_labeled_statements":
114                 optConfig.dfmt_compact_labeled_statements = optVal;
115                 break;
116             case "single_template_constraint_indent":
117                 optConfig.dfmt_single_template_constraint_indent = optVal;
118                 break;
119             default:
120                 assert(false, "Invalid command-line switch");
121             }
122         }
123 
124         try
125         {
126             // dfmt off
127             getopt(args,
128                 "version", &showVersion,
129                 "align_switch_statements", &handleBooleans,
130                 "brace_style", &optConfig.dfmt_brace_style,
131                 "config|c", &explicitConfigDir,
132                 "end_of_line", &optConfig.end_of_line,
133                 "help|h", &showHelp,
134                 "indent_size", &optConfig.indent_size,
135                 "indent_style|t", &optConfig.indent_style,
136                 "inplace|i", &inplace,
137                 "max_line_length", &optConfig.max_line_length,
138                 "soft_max_line_length", &optConfig.dfmt_soft_max_line_length,
139                 "outdent_attributes", &handleBooleans,
140                 "space_after_cast", &handleBooleans,
141                 "selective_import_space", &handleBooleans,
142                 "space_before_function_parameters", &handleBooleans,
143                 "split_operator_at_line_end", &handleBooleans,
144                 "compact_labeled_statements", &handleBooleans,
145                 "single_template_constraint_indent", &handleBooleans,
146                 "tab_width", &optConfig.tab_width,
147                 "template_constraint_style", &optConfig.dfmt_template_constraint_style);
148             // dfmt on
149         }
150         catch (GetOptException e)
151         {
152             stderr.writeln(e.msg);
153             return 1;
154         }
155 
156         if (showVersion)
157         {
158             writeln(VERSION);
159             return 0;
160         }
161 
162         if (showHelp)
163         {
164             printHelp();
165             return 0;
166         }
167 
168         args.popFront();
169         immutable bool readFromStdin = args.length == 0;
170 
171         File output = stdout;
172         version (Windows)
173         {
174             // On Windows, set stdout to binary mode (needed for correct EOL writing)
175             // See Phobos' stdio.File.rawWrite
176             {
177                 import std.stdio : _O_BINARY;
178                 immutable fd = output.fileno;
179                 _setmode(fd, _O_BINARY);
180                 version (CRuntime_DigitalMars)
181                 {
182                     import core.atomic : atomicOp;
183                     import core.stdc.stdio : __fhnd_info, FHND_TEXT;
184 
185                     atomicOp!"&="(__fhnd_info[fd], ~FHND_TEXT);
186                 }
187             }
188         }
189 
190         ubyte[] buffer;
191 
192         Config explicitConfig;
193         if (explicitConfigDir)
194         {
195             import std.file : exists, isDir;
196 
197             if (!exists(explicitConfigDir) || !isDir(explicitConfigDir))
198             {
199                 stderr.writeln("--config_dir|c must specify existing directory path");
200                 return 1;
201             }
202             explicitConfig = getConfigFor!Config(explicitConfigDir);
203             explicitConfig.pattern = "*.d";
204         }
205 
206         if (readFromStdin)
207         {
208             import std.file : getcwd;
209 
210             auto cwdDummyPath = buildPath(getcwd(), "dummy.d");
211 
212             Config config;
213             config.initializeWithDefaults();
214             if (explicitConfigDir != "")
215             {
216                 config.merge(explicitConfig, buildPath(explicitConfigDir, "dummy.d"));
217             }
218             else
219             {
220                 Config fileConfig = getConfigFor!Config(getcwd());
221                 fileConfig.pattern = "*.d";
222                 config.merge(fileConfig, cwdDummyPath);
223             }
224             config.merge(optConfig, cwdDummyPath);
225             if (!config.isValid())
226                 return 1;
227             ubyte[4096] inputBuffer;
228             ubyte[] b;
229             while (true)
230             {
231                 b = stdin.rawRead(inputBuffer);
232                 if (b.length)
233                     buffer ~= b;
234                 else
235                     break;
236             }
237             format("stdin", buffer, output.lockingTextWriter(), &config);
238         }
239         else
240         {
241             import std.file : dirEntries, isDir, SpanMode;
242 
243             if (args.length >= 2)
244                 inplace = true;
245             while (args.length > 0)
246             {
247                 const path = args.front;
248                 args.popFront();
249                 if (isDir(path))
250                 {
251                     inplace = true;
252                     foreach (string name; dirEntries(path, "*.d", SpanMode.depth))
253                         args ~= name;
254                     continue;
255                 }
256                 Config config;
257                 config.initializeWithDefaults();
258                 if (explicitConfigDir != "")
259                 {
260                     config.merge(explicitConfig, buildPath(explicitConfigDir, "dummy.d"));
261                 }
262                 else
263                 {
264                     Config fileConfig = getConfigFor!Config(path);
265                     fileConfig.pattern = "*.d";
266                     config.merge(fileConfig, path);
267                 }
268                 config.merge(optConfig, path);
269                 if (!config.isValid())
270                     return 1;
271                 File f = File(path);
272                 // ignore empty files
273                 if (f.size)
274                 {
275                     buffer = new ubyte[](cast(size_t) f.size);
276                     f.rawRead(buffer);
277                     if (inplace)
278                         output = File(path, "wb");
279                     format(path, buffer, output.lockingTextWriter(), &config);
280                 }
281             }
282         }
283         return 0;
284     }
285 }
286 
287 private version (Windows)
288 {
289     version(CRuntime_DigitalMars)
290     {
291         extern(C) int setmode(int, int) nothrow @nogc;
292         alias _setmode = setmode;
293     }
294     else version(CRuntime_Microsoft)
295     {
296         extern(C) int _setmode(int, int) nothrow @nogc;
297     }
298 }
299 
300 template optionsToString(E) if (is(E == enum))
301 {
302     enum optionsToString = () {
303 
304         string result = "(";
305         foreach (s; [__traits(allMembers, E)])
306         {
307             if (s != "unspecified")
308                 result ~= s ~ "|";
309         }
310         result = result[0 .. $ - 1] ~ ")";
311         return result;
312     } ();
313 }
314 
315 private void printHelp()
316 {
317     writeln(`dfmt `, VERSION, `
318 https://github.com/Hackerpilot/dfmt
319 
320 Options:
321     --help, -h          Print this help message
322     --inplace, -i       Edit files in place
323     --config_dir, -c    Path to directory to load .editorconfig file from.
324     --version           Print the version number and then exit
325 
326 Formatting Options:
327     --align_switch_statements
328     --brace_style               `, optionsToString!(typeof(Config.dfmt_brace_style)),
329             `
330     --end_of_line               `, optionsToString!(typeof(Config.end_of_line)), `
331     --indent_size
332     --indent_style, -t          `,
333             optionsToString!(typeof(Config.indent_style)), `
334     --soft_max_line_length
335     --max_line_length
336     --outdent_attributes
337     --space_after_cast
338     --space_before_function_parameters
339     --selective_import_space
340     --single_template_constraint_indent
341     --split_operator_at_line_end
342     --compact_labeled_statements
343     --template_constraint_style
344         `,
345             optionsToString!(typeof(Config.dfmt_template_constraint_style)));
346 }
347 
348 private string createFilePath(bool readFromStdin, string fileName)
349 {
350     import std.file : getcwd;
351     import std.path : isRooted;
352 
353     immutable string cwd = getcwd();
354     if (readFromStdin)
355         return buildPath(cwd, "dummy.d");
356     if (isRooted(fileName))
357         return fileName;
358     else
359         return buildPath(cwd, fileName);
360 }