1 /** 2 * Handling of interaction with users via standard input. 3 * 4 * Provides functions for simple and common interacitons with users in 5 * the form of question and answer. 6 * 7 * Copyright: Copyright Jesse Phillips 2010 8 * License: zlib/libpng 9 * Authors: Jesse Phillips 10 * 11 * Synopsis: 12 * 13 * -------- 14 * import scriptlike.interact; 15 * 16 * auto age = userInput!int("Please Enter you age"); 17 * 18 * if(userInput!bool("Do you want to continue?")) 19 * { 20 * auto outputFolder = pathLocation("Where you do want to place the output?"); 21 * auto color = menu!string("What color would you like to use?", ["Blue", "Green"]); 22 * } 23 * 24 * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 25 * -------- 26 */ 27 module scriptlike.interact; 28 29 import std.conv; 30 import std.file; 31 import std.functional; 32 import std.range; 33 import std.stdio; 34 import std.string; 35 import std.traits; 36 37 /** 38 * The $(D userInput) function provides a means to accessing a single 39 * value from the user. Each invocation outputs a provided 40 * statement/question and takes an entire line of input. The result is then 41 * converted to the requested type; default is a string. 42 * 43 * -------- 44 * auto name = userInput("What is your name"); 45 * -------- 46 * 47 * Returns: User response as type T. 48 * 49 * Where type is bool: 50 * 51 * true on "ok", "continue", 52 * and if the response starts with 'y' or 'Y'. 53 * 54 * false on all other input, include no response (will not throw). 55 * 56 * Throws: $(D NoInputException) if the user does not enter anything. 57 * $(D ConvError) when the string could not be converted to the desired type. 58 */ 59 T userInput(T = string)(string question = "") 60 { 61 write(question ~ "\n> "); 62 auto ans = readln(); 63 64 static if(is(T == bool)) 65 { 66 switch(ans.front) 67 { 68 case 'y', 'Y': 69 return true; 70 default: 71 } 72 switch(ans.strip()) 73 { 74 case "continue": 75 case "ok": 76 return true; 77 default: 78 return false; 79 } 80 } else 81 { 82 if(ans == "\x0a") 83 throw new NoInputException("Value required, " 84 "cannot continue operation."); 85 static if(isSomeChar!T) 86 { 87 return to!(T)(ans[0]); 88 } else 89 return to!(T)(ans.strip()); 90 } 91 } 92 93 version(unittest_scriptlike_d) 94 unittest 95 { 96 mixin(selfCom(["10PM"])); 97 mixin(selfCom()); 98 auto s = userInput("What time is it?"); 99 assert(s == "10PM", "Expected 10PM got" ~ s); 100 outfile.rewind; 101 assert(outfile.readln().strip == "What time is it?"); 102 } 103 104 /** 105 * Gets a valid path folder from the user. The string will not contain 106 * quotes, if you are using in a system call and the path contain spaces 107 * wrapping in quotes may be required. 108 * 109 * -------- 110 * auto confFile = pathLocation("Where is the configuration file?"); 111 * -------- 112 * 113 * Throws: NoInputException if the user does not provide a path. 114 */ 115 string pathLocation(string action) 116 { 117 string ans; 118 119 do 120 { 121 if(ans !is null) 122 writeln("Could not locate that file."); 123 ans = userInput(action); 124 // Quotations will generally cause problems when 125 // using the path with std.file and Windows. This removes the quotes. 126 ans = ans.removechars("\";").strip(); 127 ans = ans[0] == '"' ? ans[1..$] : ans; // removechars skips first char 128 } while(!exists(ans)); 129 130 return ans; 131 } 132 133 /** 134 * Creates a menu from a Range of strings. 135 * 136 * It will require that a number is selected within the number of options. 137 * 138 * If the the return type is a string, the string in the options parameter will 139 * be returned. 140 * 141 * Throws: NoInputException if the user wants to quit. 142 */ 143 T menu(T = ElementType!(Range), Range) (string question, Range options) 144 if((is(T==ElementType!(Range)) || is(T==int)) && 145 isForwardRange!(Range)) 146 { 147 string ans; 148 int maxI; 149 int i; 150 151 while(true) 152 { 153 writeln(question); 154 i = 0; 155 foreach(str; options) 156 { 157 writefln("%8s. %s", i+1, str); 158 i++; 159 } 160 maxI = i+1; 161 162 writefln("%8s. %s", "No Input", "Quit"); 163 ans = userInput!(string)("").strip(); 164 int ians; 165 166 try 167 { 168 ians = to!(int)(ans); 169 } catch(ConvException ce) 170 { 171 bool found; 172 i = 0; 173 foreach(o; options) 174 { 175 if(ans.toLower() == to!string(o).toLower()) 176 { 177 found = true; 178 ians = i+1; 179 break; 180 } 181 i++; 182 } 183 if(!found) 184 throw ce; 185 186 } 187 188 if(ians > 0 && ians <= maxI) 189 static if(is(T==ElementType!(Range))) 190 static if(isRandomAccessRange!(Range)) 191 return options[ians-1]; 192 else 193 { 194 take!(ians-1)(options); 195 return options.front; 196 } 197 else 198 return ians; 199 else 200 writeln("You did not select a valid entry."); 201 } 202 } 203 204 version(unittest_scriptlike_d) 205 unittest 206 { 207 mixin(selfCom(["1","Green", "green","2"])); 208 mixin(selfCom()); 209 auto color = menu!string("What color?", ["Blue", "Green"]); 210 assert(color == "Blue", "Expected Blue got " ~ color); 211 212 auto ic = menu!int("What color?", ["Blue", "Green"]); 213 assert(ic == 2, "Expected 2 got " ~ ic.to!string); 214 215 color = menu!string("What color?", ["Blue", "Green"]); 216 assert(color == "Green", "Expected Green got " ~ color); 217 218 color = menu!string("What color?", ["Blue", "Green"]); 219 assert(color == "Green", "Expected Green got " ~ color); 220 outfile.rewind; 221 assert(outfile.readln().strip == "What color?"); 222 } 223 224 225 /** 226 * Requires that a value be provided and valid based on 227 * the delegate passed in. It must also check against null input. 228 * 229 * -------- 230 * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 231 * -------- 232 * 233 * Throws: NoInputException if the user does not provide any value. 234 * ConvError if the user does not provide any value. 235 */ 236 T require(T, alias cond)(in string question, in string failure = null) 237 { 238 alias unaryFun!(cond) call; 239 T ans; 240 while(1) 241 { 242 ans = userInput!T(question); 243 if(call(ans)) 244 break; 245 if(failure) 246 writeln(failure); 247 } 248 249 return ans; 250 } 251 252 version(unittest_scriptlike_d) 253 unittest 254 { 255 mixin(selfCom(["1","11","3"])); 256 mixin(selfCom()); 257 auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 258 assert(num == 1, "Expected 1 got" ~ num.to!string); 259 num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 260 assert(num == 3, "Expected 1 got" ~ num.to!string); 261 outfile.rewind; 262 assert(outfile.readln().strip == "Enter a number from 1 to 10"); 263 } 264 265 266 /** 267 * Used when input was not provided. 268 */ 269 class NoInputException: Exception 270 { 271 this(string msg) 272 { 273 super(msg); 274 } 275 } 276 277 version(unittest_scriptlike_d) 278 private string selfCom() 279 { 280 string ans = q{ 281 auto outfile = File.tmpfile(); 282 auto origstdout = stdout; 283 scope(exit) stdout = origstdout; 284 stdout = outfile;}; 285 286 return ans; 287 } 288 289 version(unittest_scriptlike_d) 290 private string selfCom(string[] input) 291 { 292 string ans = q{ 293 auto infile = File.tmpfile(); 294 auto origstdin = stdin; 295 scope(exit) stdin = origstdin; 296 stdin = infile;}; 297 298 foreach(i; input) 299 ans ~= "infile.writeln(`"~i~"`);"; 300 ans ~= "infile.rewind;"; 301 302 return ans; 303 } 304