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