1 /++ Command-Line-Interface (CLI) utilities.
2 
3 	Expose CLI based on a root type T, where T is typically a struct, class or
4 	module. By default it creates an instance of an aggregate and expose members
5 	as sub-command. This handles D types, files, etc.
6 
7     Reflects on `nxt.path`, `string`, and other built-in types using
8     `arg.to!Arg`.
9 
10     Constructor of `T` is also reflected as flags before sub-command.
11 
12 	TODO: Use in MLParser: - `--requestedFormats=C,D` or `--fmts` because arg is
13 	a Fmt[] - `scan dirPath`
14 
15     Auto-gen of help also works if any arg in args is --help or -h.
16  +/
17 module nxt.cli;
18 
19 /++ Match method to use when matching CLI sub-commands with member functions. +/
20 enum Match {
21 	exact,
22 	prefix,
23 	acronym,
24 }
25 
26 struct Flags {
27 	@disable this(this);
28 	Match match;
29 }
30 
31 /++ Evaluate `cmd` as a CLI-like sub-command calling a member of `agg` of type `T`.
32 	Typically called with the `args` passed to a `main` function.
33 
34 	Returns: `true` iff the command `cmd` result in a call to `T`-member named `cmd[1]`.
35 
36 	TODO: Use parameter `flags`
37 
38 	TODO: Auto-generate help description when --help is passed.
39 
40 	TODO: Support both
41 	- EXE scan("/tmp/")
42 	- EXE scan /tmp/
43 	where the former is inputted in bash as
44       EXE 'scan("/tmp/")'
45  +/
46 bool evalMemberCommand(T)(ref T agg, in string exe, in const(string)[] cmd, in Flags flags = Flags.init)
47 if (is(T == struct) || is(T == class)) {
48 	import nxt.path : FilePath, DirPath;
49 	import nxt.algorithm : canFind;
50 	import nxt.stdio;
51 	if (cmd.length == 0)
52 		return false;
53 	const showHelp = cmd.canFind("--help") || cmd.canFind("-h");
54 	if (showHelp) {
55 		debug writeln("Usage: ", exe ? exe : "", " [SUB-COMMAND]");
56 		debug writeln("Sub Commands:");
57 	}
58 	foreach (const mn; __traits(allMembers, T)) { /+ member name +/
59 		// TODO: Use std.traits.isSomeFunction or it's inlined definition.
60 		// is(T == return) || is(typeof(T) == return) || is(typeof(&T) == return) /+ isSomeFunction +/
61 		static immutable qmn = T.stringof ~ '.' ~ mn; /+ qualified +/
62 		alias member = __traits(getMember, agg, mn);
63 		static if (__traits(getVisibility, member) == "public") { // TODO: perhaps include other visibilies later on
64 			static if (!is(member) /+ not a type +/ && !(mn.length >= 2 && mn[0 .. 2] == "__")) /+ non-generated members like `__ctor` +/ {
65 				if (showHelp) {
66 					debug writeln("  ", mn);
67 				}
68 				switch (cmd.length) {
69 				case 0: // nullary
70 					static if (__traits(compiles, { mixin(`agg.`~mn~`();`); })) { /+ nullary function +/
71 						mixin(`agg.`~mn~`();`); // call
72 						return true;
73 					}
74 					break;
75 				case 1: // unary
76 					static if (__traits(compiles, { mixin(`agg.`~mn~`(FilePath.init);`); })) {
77 						mixin(`agg.`~mn~`(FilePath(cmd[0]));`); // call
78 						return true;
79 					}
80 					static if (__traits(compiles, { mixin(`agg.`~mn~`(DirPath.init);`); })) {
81 						mixin(`agg.`~mn~`(DirPath(cmd[0]));`); // call
82 						return true;
83 					}
84 					break;
85 				default:
86 					break;
87 				}
88 			}
89 		}
90 	}
91 	return false;
92 }
93 
94 ///
95 @safe pure unittest {
96 	import nxt.path : DirPath;
97 
98 	struct S {
99 	version (none) @disable this(this);
100 	@safe pure nothrow @nogc:
101 		void f1() scope {
102 			f1Count += 1;
103 		}
104 		void f2(int inc = 1) scope { // TODO: support f2:32
105 			f2Count += inc;
106 		}
107 		void scan(DirPath path) {
108 			_path = path;
109 			_scanDone = true;
110 		}
111 		private uint f1Count;
112 		uint f2Count;
113 		DirPath _path;
114 		bool _scanDone;
115 	}
116 	S s;
117 
118 	assert(!s.evalMemberCommand(null, []));
119 	assert(s.evalMemberCommand(null, [""]));
120 	assert(s.evalMemberCommand(null, ["_"]));
121 
122 	assert(s.f1Count == 0);
123 	s.evalMemberCommand(null, ["f1"]);
124 	// TODO: assert(s.f1Count == 1);
125 
126 	assert(s.f2Count == 0);
127 	s.evalMemberCommand(null, ["f2"]); // TODO: call as "f2", "42"
128 	// TODO: assert(s.f2Count == 1);
129 
130 	// TODO: assert(s._path == DirPath.init);
131 	// TODO: assert(!s._scanDone);
132 	// TODO: assert(s.evalMemberCommand(null, ["scan", "/tmp"]));
133 	// TODO: assert(s._path == DirPath("/tmp"));
134 	assert(s._scanDone);
135 }
136 
137 version (unittest) {
138 import nxt.debugio;
139 }