1 module clid.core.help; 2 3 import std.traits : hasUDA, getUDAs; 4 import std.meta : Alias; 5 import std.string : leftJustify; 6 import std.uni : toUpper; 7 8 import clid.attributes; 9 import clid.core.util; 10 11 /** 12 * Print help information for a given configuration struct. 13 * Params: c = The configuration struct to print help information for. 14 */ 15 void printHelp(C)(C c) 16 { 17 import std.stdio : stderr; 18 19 validateStruct!C(); 20 stderr.write(getHelp(c)); 21 } 22 23 private string getHelp(C)(C c) // @suppress(dscanner.suspicious.unused_parameter) 24 { 25 import std.ascii : newline; 26 27 string str = usage!C() ~ newline; 28 29 if (hasUnnamedParameters!C) 30 { 31 str ~= newline ~ "Arguments:" ~ newline; 32 } 33 34 if (hasNamedParameters!C) 35 { 36 str ~= newline ~ "Options:" ~ newline; 37 static foreach (member; __traits(allMembers, C)) 38 { 39 static if (isNamedParameter!(C, member)) 40 { 41 str ~= describe!(value!(C, member)) ~ newline; 42 } 43 } 44 45 static if (!hasShortParameter!(C)('h') && !hasLongParameter!C("help")) 46 { 47 str ~= "\t-h, --help".leftJustify(30) ~ "Show this help\n"; 48 } 49 else static if (!hasLongParameter!C("help")) 50 { 51 str ~= "\t --help".leftJustify(30) ~ "Show this help\n"; 52 } 53 else static if (!hasShortParameter!C('h')) 54 { 55 str ~= "\t-h ".leftJustify(30) ~ "Show this help\n"; 56 } 57 } 58 return str; 59 } 60 61 private template getFlagName(alias E) 62 { 63 static if (getParameter!(E).shortName == ' ') 64 { 65 alias getFlagName = Alias!(" --" ~ getParameter!(E).longName); 66 } 67 else 68 { 69 alias getFlagName = Alias!("-" ~ getParameter!(E) 70 .shortName ~ ", --" ~ getParameter!(E).longName); 71 } 72 } 73 74 private template getFlagUnit(alias E, string prefix = " ") 75 { 76 static if (hasDescription!(E) && getDescription!(E).optionUnit.length > 0) 77 { 78 alias getFlagUnit = Alias!(prefix ~ getDescription!(E).optionUnit); 79 } 80 else static if (is(typeof(E) : bool)) 81 { 82 alias getFlagUnit = Alias!(""); 83 } 84 else 85 { 86 alias getFlagUnit = Alias!(prefix ~ typeof(E).stringof.toUpper); 87 } 88 } 89 90 private template getFlag(alias E) 91 { 92 alias getFlag = Alias!(getFlagName!(E) ~ getFlagUnit!(E)); 93 } 94 95 private template getOnlyDescription(alias E) 96 { 97 static if (hasDescription!(E)) 98 alias getOnlyDescription = Alias!(getDescription!(E).description); 99 else 100 alias getOnlyDescription = Alias!(""); 101 } 102 103 private template getFullDescription(alias E) 104 { 105 static if (hasUDA!(E, Required)) 106 alias getFullDescription = Alias!("[REQUIRED] " ~ getOnlyDescription!(E)); 107 else 108 alias getFullDescription = getOnlyDescription!(E); 109 } 110 111 private template describe(alias E) 112 { 113 static if (!hasUDA!(E, Description) && !hasUDA!(E, Required)) 114 { 115 alias describe = Alias!('\t' ~ getFlag!(E)); 116 } 117 else 118 { 119 alias describe = Alias!(leftJustify('\t' ~ getFlag!(E), 30) ~ getFullDescription!(E)); 120 } 121 } 122 123 private string usage(C)() 124 { 125 import core.runtime : Runtime; 126 127 return "Usage: " ~ Runtime.args()[0] ~ " [OPTION]..." ~ getRequiredUsage!C() 128 ~ getUnnamedUsage!C(); 129 } 130 131 private string getUnnamedUsage(C)() 132 { 133 string str; 134 135 static foreach (member; __traits(allMembers, C)) 136 { 137 static if (!isNamedParameter!(C, member)) 138 { 139 static if (isRequired!(C, member)) 140 { 141 str ~= getFlagUnit!(value!(C, member)).toUpper; 142 } 143 else 144 { 145 str ~= getFlagUnit!(value!(C, member), " [").toUpper ~ "]"; 146 } 147 } 148 } 149 return str; 150 } 151 152 private string getRequiredUsage(C)() 153 { 154 string str; 155 156 static foreach (member; __traits(allMembers, C)) 157 { 158 static if (isRequired!(C, member)) 159 { 160 str ~= " --" ~ getParameter!(value!(C, member)) 161 .longName ~ getFlagUnit!(value!(C, member)).toUpper; 162 } 163 } 164 165 return str; 166 } 167 168 // ====================== 169 // Unit tests start here. 170 // ====================== 171 172 unittest 173 { 174 struct Config 175 { 176 @Parameter("foo", 'f') 177 @Description("Foobar") 178 int value; 179 180 @Parameter("str", 's') 181 @Description("Just a string") 182 string stringValue; 183 184 @Parameter("time", 't') 185 @Description("Interval time", "secs") 186 @Required int time; 187 188 @Parameter("bar") 189 @Description("A random description") 190 @Required bool other; 191 192 @Parameter() string something; 193 } 194 195 immutable Config config; 196 static assert(getFlag!(config.value) == "-f, --foo INT"); 197 static assert(getFullDescription!(config.value) == "Foobar"); 198 199 printHelp(config); 200 } 201 202 unittest 203 { 204 struct Config 205 { 206 @Parameter("foo") 207 int value; 208 } 209 210 immutable Config config; 211 static assert(getFlag!(config.value) == " --foo INT"); 212 }