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