1 module postrock;
2 
3 import beard.io;
4 import beard.vector : pushBack, pushFront;
5 
6 import std.array : split, join;
7 import std.bigint : BigInt;
8 import std.process : getenv;
9 
10 /// Generic command line error.
11 class CmdLineError : Throwable {
12     this(string error) { super(error); }
13 }
14 
15 /// Thrown when a user supplies a malformed command-line argument such as ---arg
16 class BadCommandLineArgument : CmdLineError {
17     this(string error) { super(error); }
18 }
19 
20 /// When a user supplies a flag that is not known.
21 class UnknownCommandLineArgument : CmdLineError {
22     this(string error) { super(error); }
23 }
24 
25 /// This is thrown when an argument requires a value but the user supplied none.
26 class BadCommandLineArgumentValue : CmdLineError {
27     this(string error) { super(error); }
28 }
29 
30 int maxLeftColumnWidth = 40;
31 
32 /// Command-line argument parser object.
33 class Parser {
34     struct State {
35         this(string[] *_args) { args = _args; }
36 
37         bool empty() { return argIdx >= args.length; }
38         ref string front() { return (*args)[argIdx]; }
39 
40         char firstChar() { return front()[argOffset]; }
41         char charAt(int idx) { return front()[argOffset + idx]; }
42 
43         string substring() { return front()[argOffset..$]; }
44 
45         void advanceOffset(int incr) {
46             argOffset += incr;
47             if (argOffset >= front().length)
48                 popArgument();
49         }
50 
51         // advance pointer, keeping argument in args
52         void saveArgument() {
53             if (nextSaveIdx < argIdx)
54                 (*args)[nextSaveIdx] = (*args)[argIdx];
55             nextSaveIdx += 1;
56 
57             popArgument();
58         }
59 
60         void popArgument() {
61             argIdx += 1;
62             argOffset = 0;
63         }
64 
65         string[] *args;
66         // which argument currently looking at
67         int       argIdx = 1;
68         // offset used to keep track of where parser is within current option
69         int       argOffset = 0;
70         // idx where last saved argument was
71         int       nextSaveIdx = 1;
72     }
73 
74     interface AbstractValue {
75         void parse(ref State state);
76     }
77 
78     class Value(T) : AbstractValue {
79         this(T *ptr) { valPtr_ = ptr; }
80 
81         private static void parseHelper(U)(ref U val, ref State state) {
82             static if (is(U : bool)) {
83                 val = true;
84             }
85             else static if (is(U : string)) {
86                 if (state.empty)
87                     throw new BadCommandLineArgumentValue(state.front);
88                 val = state.substring;
89                 state.popArgument;
90             }
91             else static if (is(U V : V[])) {
92                 val.length += 1;
93                 parseHelper(val[val.length - 1], state);
94             }
95             else {
96             }
97         }
98 
99         void parse(ref State state) {
100             parseHelper(*valPtr_, state);
101         }
102 
103         T* valPtr_;
104     }
105 
106     class ShowHelp : AbstractValue {
107         this(Parser parser) { parser_ = parser; }
108         private Parser parser_;
109 
110         void parse(ref State state) { parser_.showHelp; }
111     }
112 
113   private:
114     struct Help {
115         this(string _help) { help = _help; }
116 
117         ulong leftColWidth() {
118             auto ret = (args.length - 1) * 2;
119             foreach (arg ; args) {
120                 ret += 2;
121                 if (arg.length > 1)
122                     ret += arg.length;
123             }
124             return ret;
125         }
126 
127         string help;
128         string[] args;
129     }
130 
131     void addDefaultHelpOption() {
132         if ("h" in optionMap_ && "help" in optionMap_)
133             return;
134 
135         auto helpShower = new ShowHelp(this);
136 
137         auto help = Help("show help");
138         if (! ("h" in optionMap_)) {
139             pushBack(help.args, "h");
140             optionMap_["h"] = helpShower;
141         }
142 
143         if (! ("help" in optionMap_)) {
144             pushBack(help.args, "help");
145             optionMap_["help"] = helpShower;
146         }
147 
148         pushFront(helps_, help);
149     }
150 
151   public:
152     ref Parser opCall(T)(string args, T *storage, string helpString) {
153         auto vals = split(args, ",");
154         auto optValue = new Value!T(storage);
155         auto help = Help(helpString);
156 
157         foreach (val ; vals) {
158             pushBack(help.args, val);
159             optionMap_[val] = optValue;
160         }
161 
162         pushBack(helps_, help);
163 
164         return this;
165     }
166 
167     ref Parser banner(string banner) {
168         banner_ = banner;
169         return this;
170     }
171 
172     /// @brief Parse the command line.
173     /// @detailed This will also add -h/--help options to show the help if
174     ///           such options have not already been added.
175     void parse(string[] *args) {
176         auto state = State(args);
177 
178         addDefaultHelpOption;
179 
180         while (! state.empty) {
181             if ('-' != state.firstChar) {
182                 state.saveArgument;
183                 continue;
184             }
185 
186             auto front = state.front;
187             if (1 == front.length)
188                 throw new BadCommandLineArgument(front);
189 
190             if ('-' == state.charAt(1)) {
191                 if (2 == front.length) {
192                     state.popArgument;
193                     while (! state.empty)
194                         state.saveArgument;
195                     break;
196                 }
197 
198                 string search = front[2..$];
199                 state.popArgument; // advance past long option
200                 auto value = optionMap_.get(search, null);
201                 if (! value)
202                     throw new UnknownCommandLineArgument(front);
203 
204                 value.parse(state);
205             }
206             else {
207                 // parse short option.. maybe more than one
208                 state.advanceOffset(1);
209                 do {
210                     string search = "" ~ state.firstChar;
211 
212                     auto value = optionMap_.get(search, null);
213                     if (! value)
214                         throw new UnknownCommandLineArgument(front);
215 
216                     state.advanceOffset(1); // advance past option
217                     value.parse(state);
218                 } while (state.argOffset);
219             }
220         }
221 
222         args.length = state.nextSaveIdx;
223     }
224 
225     // show help (-h/--help)
226     void showHelp() {
227         shownHelp_ = true;
228         if (banner_.length)
229             println(banner_);
230 
231         // get maximum column width
232         ulong leftColWidth = 0;
233         foreach (help ; helps_) {
234             auto width = help.leftColWidth;
235             if (width > leftColWidth)
236                 leftColWidth = width;
237         }
238 
239         leftColWidth += 4; // 2 spaces either side
240         if (leftColWidth > maxLeftColumnWidth)
241             leftColWidth = maxLeftColumnWidth;
242 
243         foreach (help ; helps_) {
244             string leftCol = "";
245             foreach (arg ; help.args) {
246                 if (leftCol.length) leftCol ~= ", ";
247                 if (arg.length > 1)
248                     leftCol ~= "--" ~ arg;
249                 else
250                     leftCol ~= "-" ~ arg;
251             }
252 
253             print("  " ~ leftCol);
254             auto nSpaces = leftColWidth - leftCol.length - 4;
255             while (nSpaces) {
256                 print(' ');
257                 nSpaces -= 1;
258             }
259             print("  ");
260             println(help.help);
261         }
262     }
263 
264     /// Has the help been displayed yet?
265     bool shownHelp() { return shownHelp_; }
266 
267   private:
268     bool                  shownHelp_ = false;
269     AbstractValue[string] optionMap_;
270     Help[]                helps_;
271     string                banner_;
272 }
273 // vim:ts=4 sw=4