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