1 /+ 2 Author: 3 Colin Grogan 4 github.com/grogancolin 5 6 Description: 7 Implementation of the expect tool (http://expect.sourceforge.net/) in D. 8 9 License: 10 Boost Software License - Version 1.0 - August 17th, 2003 11 12 Permission is hereby granted, free of charge, to any person or organization 13 obtaining a copy of the software and accompanying documentation covered by 14 this license (the "Software") to use, reproduce, display, distribute, 15 execute, and transmit the Software, and to prepare derivative works of the 16 Software, and to permit third-parties to whom the Software is furnished to 17 do so, all subject to the following: 18 19 The copyright notices in the Software and this entire statement, including 20 the above license grant, this restriction and the following disclaimer, 21 must be included in all copies of the Software, in whole or in part, and 22 all derivative works of the Software, unless such copies or derivative 23 works are solely in the form of machine-executable object code generated by 24 a source language processor. 25 26 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 29 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 30 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 31 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 32 DEALINGS IN THE SOFTWARE. 33 +/ 34 35 module expectapp; 36 37 38 version(DExpectMain){ 39 import docopt; 40 import std.stdio : File, writefln, stdout, stderr; 41 import dexpect : Expect, ExpectSink, ExpectException; 42 import std.datetime : Clock; 43 import pegged.grammar; 44 import std..string : format, indexOf; 45 import std.file : readText, exists; 46 import std.algorithm : all, any, filter, each, canFind; 47 import std.path : baseName; 48 49 version(Windows){ 50 enum isWindows=true; 51 enum isLinux=false; 52 enum os="win"; 53 } 54 version(Posix){ 55 enum isWindows=false; 56 enum isLinux=true; 57 enum os="linux"; 58 } 59 60 /// Usage string for docopt 61 const string doc = 62 "dexpect 63 Usage: 64 dexpect [-h] [-v] <file>... 65 Options: 66 -h --help Show this message 67 -v --verbose Show verbose output 68 "; 69 70 int main(string[] args){ 71 72 auto arguments = docopt.docopt(doc, args[1..$], true, "dexpect 0.0.1"); 73 bool verbose = arguments["--verbose"].toString.to!bool; 74 if(verbose) writefln("Command line args:\n%s\n", arguments); 75 76 auto fList = arguments["<file>"].asList; 77 if(!arguments["<file>"].asList 78 .all!(fName => fName.exists)){ 79 writefln("Error, a filename does not exist\n%s", fList.to!string); 80 return 1; 81 } 82 import std.typecons : Tuple; 83 import std.array : array; 84 import std.traits; 85 alias fname_text = Tuple!(string, "fname", string, "text"); 86 alias fname_grammar = Tuple!(string, "fname", ParseTree, "parsedGrammar"); 87 alias fname_handler = Tuple!(string, "fname", ScriptHandler, "handler"); 88 alias fname_result = Tuple!(string, "fname", bool, "result"); 89 auto parsedScripts = fList 90 .map!(a => fname_text(a, a.readText)) 91 .map!(b => fname_grammar(b.fname, ScriptGrammar(b.text))); 92 93 bool[string] results; 94 95 auto failedParsing = parsedScripts.filter!(a => !a.parsedGrammar.successful); 96 if(!failedParsing.empty){ 97 writefln("Parsing failure"); 98 failedParsing.each!(a => writefln(" - %s", a.fname)); 99 return 1; 100 } 101 foreach(script; parsedScripts){ 102 string fname = 103 format("%s_%s.dexpectOutput", 104 Clock.currTime.toISOString.stripToFirst('.'), script.fname.baseName); 105 File[] outFiles = [File(fname, "w")]; 106 if(verbose){ 107 stdout.writefln("Executing script: %s", script.fname.baseName); 108 outFiles ~= stdout; 109 } 110 ScriptHandler s = ScriptHandler(script.parsedGrammar.children[0], outFiles); 111 112 results[script.fname] = s.run(); 113 writefln(""); 114 } 115 116 if(results.values.any!(a => a==true)) 117 writefln("----- Succesful -----"); 118 results.keys.filter!(key => results[key]==true) 119 .each!(key => writefln("%s", key)); 120 121 if(results.values.any!(a => a==false)) 122 writefln("\n----- Failures -----"); 123 results.keys.filter!(key => results[key]==false) 124 .each!(key => writefln("%s", key)); 125 126 return 0; 127 } 128 129 struct ScriptHandler{ 130 ParseTree theScript; 131 Expect expect; 132 File[] outFiles; 133 string[string] variables; 134 alias variables this; // referencing 'this' will now point to variables 135 136 /** 137 * Overloads the index operators so when "timeout" is set, 138 * it is propogated to the Expect variable 139 */ 140 void opIndexAssign(string value, string name){ 141 if(name == "timeout" && this.expect !is null) 142 expect.timeout = value.to!long; 143 if(name == "?") 144 throw new ExpectScriptException("Trying to set a variable named '?'"); 145 this.variables[name] = value; 146 } 147 string opIndex(string name){ 148 return this.variables[name]; 149 } 150 151 @disable this(); 152 this(ParseTree t){ 153 this.theScript = t; 154 } 155 this(ParseTree t, File[] files){ 156 this.theScript = t; 157 this.outFiles = files; 158 } 159 /** 160 * Runs this script. 161 * Returns true if the script succeeds 162 */ 163 bool run(){ 164 try{ 165 this.handleScript(theScript); 166 } catch(ExpectException e){ 167 return false; 168 } catch(ExpectScriptParseException e){ 169 stderr.writefln("An error occured.\n%s", e.msg); 170 return false; 171 } 172 return true; 173 } 174 175 /** 176 * Handles the script, delegating the work down to it's helper functions 177 */ 178 void handleScript(ParseTree script){ 179 assert(script.name == "ScriptGrammar.Script"); 180 auto blocks = script.children; 181 blocks.each!(block => this.handleBlock(block, expect)); 182 } 183 184 /* 185 * Handles a Block, parsing it's Attributes if required, and delegating its children to 186 * handleEnclosedBlock or handleStatement, depending on it's type. 187 */ 188 void handleBlock(ParseTree block, ref Expect e){ 189 // checks whether we should run this block 190 // A block should be run if it has no ScriptGrammar.OSAttr attribute, or if it has no 191 // ScriptGrammar.OSAttr not pointing at this os 192 auto doRun = block.getAttributes("ScriptGrammar.OSAttr") 193 .map!(attr => attr.children[0]) 194 .filter!(osAttr => osAttr.matches[0] != os) 195 .empty; 196 if(doRun == false){ 197 return; 198 } 199 200 block.children 201 .filter!(child => child.name != "ScriptGrammar.Attribute") // remove attribute blocks, as we dont need em anymore 202 .filter!(child => child.children.length > 0) // remove empty blocks 203 .each!((node){ 204 switch(node.name){ 205 case "ScriptGrammar.EnclosedBlock": 206 handleEnclosedBlock(node, e); 207 break; 208 case "ScriptGrammar.OpenBlock": 209 handleStatement(node.children[0], e); 210 break; 211 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", node)); 212 } 213 }); 214 } 215 216 void handleEnclosedBlock(ParseTree block, ref Expect e){ 217 block.children 218 .each!((node){ 219 switch(node.name){ 220 case "ScriptGrammar.Block": 221 handleBlock(node, e); 222 break; 223 case "ScriptGrammar.Statement": 224 handleStatement(node, e); 225 break; 226 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", node)); 227 } 228 }); 229 } 230 231 void handleStatement(ParseTree statement, ref Expect e){ 232 statement.children 233 .each!((child){ 234 switch(child.name){ 235 case "ScriptGrammar.Spawn": 236 handleSpawn(child, e); 237 break; 238 case "ScriptGrammar.Expect": 239 handleExpect(child, e); 240 break; 241 case "ScriptGrammar.Set": 242 handleSet(child); 243 break; 244 case "ScriptGrammar.Send": 245 handleSend(child, e); 246 break; 247 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 248 } 249 }); 250 } 251 252 void handleSend(ParseTree send, ref Expect e){ 253 if(send.children.length == 0) 254 throw new ExpectScriptParseException("Error parsing set command"); 255 256 string sendHelper(ParseTree toSend){ 257 string str; 258 foreach(child; toSend.children){ 259 switch(child.name){ 260 case "ScriptGrammar.String": 261 str ~= child.matches[0]; 262 break; 263 case "ScriptGrammar.Variable": 264 str ~= this[child.matches[0]]; 265 break; 266 case "ScriptGrammar.ToSend": 267 str ~= sendHelper(child); 268 break; 269 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 270 } 271 } 272 return str; 273 } 274 e.sendLine(sendHelper(send.children[0])); 275 } 276 void handleSet(ParseTree set){ 277 if(set.children.length != 2) 278 throw new ExpectScriptParseException("Error parsing set command"); 279 string setHelper(ParseTree toSet){ 280 string str; 281 foreach(child; toSet.children){ 282 switch(child.name){ 283 case "ScriptGrammar.String": 284 str ~= child.matches[0]; 285 break; 286 case "ScriptGrammar.Variable": 287 str ~= this[child.matches[0]]; 288 break; 289 case "ScriptGrammar.SetVal": 290 str ~= setHelper(child); 291 break; 292 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 293 } 294 } 295 return str; 296 } 297 string name, value; 298 foreach(child; set.children){ 299 if(child.name == "ScriptGrammar.SetVar") 300 name = child.matches[0]; 301 else if(child.name == "ScriptGrammar.SetVal") 302 value = setHelper(child); 303 } 304 this[name] = value; 305 } 306 307 void handleExpect(ParseTree expect, ref Expect e){ 308 if(expect.children.length ==0 ) 309 throw new ExpectScriptParseException("Error parsing expect command"); 310 if(e is null) 311 throw new ExpectScriptParseException("Cannot call expect before spawning"); 312 string expectHelper(ParseTree toExpect){ 313 string str; 314 foreach(child; toExpect.children){ 315 switch(child.name){ 316 case "ScriptGrammar.String": 317 str ~= child.matches[0]; 318 break; 319 case "ScriptGrammar.Variable": 320 str ~= this[child.matches[0]]; 321 break; 322 case "ScriptGrammar.ToExpect": 323 str ~= expectHelper(child); 324 break; 325 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 326 } 327 } 328 return str; 329 } 330 variables["$?"] = e.expect(expectHelper(expect.children[0])).to!string; 331 } 332 333 void handleSpawn(ParseTree spawn, ref Expect e){ 334 if(spawn.children.length == 0) 335 throw new ExpectScriptParseException("Error parsing spawn command"); 336 string spawnHelper(ParseTree toSpawn){ 337 string str; 338 foreach(child; toSpawn.children){ 339 switch(child.name){ 340 case "ScriptGrammar.String": 341 str ~= child.matches[0]; 342 break; 343 case "ScriptGrammar.Variable": 344 str ~= this[child.matches[0]]; 345 break; 346 case "ScriptGrammar.ToSpawn": 347 str ~= spawnHelper(child); 348 break; 349 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 350 } 351 } 352 return str; 353 } 354 e = new Expect(spawnHelper(spawn.children[0]), this.outFiles); 355 if(this.keys.canFind("timeout")) 356 e.timeout = this["timeout"].to!long; 357 } 358 } 359 auto getAttributes(ParseTree tree){ //checks if this tree has attributes 360 return tree.children 361 .filter!(child => child.name == "ScriptGrammar.Attribute"); 362 } 363 auto getAttributes(ParseTree tree, string attrName){ //checks if this tree has attributes 364 return tree.getAttributes 365 .filter!(attr => attr.children.length > 0) 366 .filter!(attr => attr.children[0].name == attrName); 367 } 368 369 mixin(grammar(scriptGrammar)); 370 371 /// Grammar to be parsed by pegged 372 /// Potentially full of bugs 373 enum scriptGrammar = ` 374 ScriptGrammar: 375 # This is a simple testing bed for grammars 376 Script <- (EmptyLine / Block)+ :eoi 377 Block <- Attribute? (:' ' Attribute)* (EnclosedBlock / OpenBlock) :Whitespace* 378 379 Attribute <- ( OSAttr ) 380 OSAttr <- ('win' / 'linux') 381 382 EnclosedBlock <- :Whitespace* '{' 383 ( :Whitespace* (Statement / Block) :Whitespace* )* 384 :Whitespace* '}' :Whitespace* 385 OpenBlock <- (:Whitespace* Statement :Whitespace*) 386 387 Statement <- :Whitespace* (Comment / Spawn / Send / Expect / Set) :Spacing* :Whitespace* 388 389 Comment <: :Spacing* '#' (!eoi !endOfLine .)* 390 391 Spawn <- :"spawn" :Spacing* ToSpawn 392 ToSpawn <- (~Variable / ~String) :Spacing ('~' :Spacing ToSpawn)* 393 394 Send <- :"send" :Spacing* ToSend 395 ToSend <- (~Variable / ~String) :Spacing ('~' :Spacing ToSend)* 396 397 Expect <- :"expect" :Spacing* ToExpect 398 ToExpect <- (~Variable / ~String) :Spacing ('~' :Spacing ToExpect)* 399 400 Set <- :'set' :Spacing* SetVar :Spacing :'=' Spacing SetVal 401 SetVar <- ~VarName 402 SetVal <- (~Variable / ~String) :Spacing ('~' :Spacing SetVal)* 403 404 Keyword <~ ('#' / 'set' / 'expect' / 'spawn' / 'send') 405 KeywordData <~ (!eoi !endOfLine .)+ 406 407 Variable <- :"$(" VarName :")" 408 VarName <- (!eoi !endOfLine !')' !'(' !'$' !'=' !'~' .)+ 409 410 Text <- (!eoi !endOfLine !'~' .)+ 411 DoubleQuoteText <- :doublequote 412 (!eoi !endOfLine !doublequote .)+ 413 :doublequote 414 SingleQuoteText <- :"'" 415 (!eoi !endOfLine !"'" .)+ 416 :"'" 417 String <- ( 418 ~DoubleQuoteText / 419 ~SingleQuoteText / 420 ~Text 421 ) 422 Whitespace <- (Spacing / EmptyLine) 423 EmptyLine <- ('\n\r' / '\n')+ 424 `; 425 426 /+ --------------- Utils --------------- +/ 427 428 // Strits a string up to the first instance of c 429 string stripToFirst(string str, char c){ 430 return str[0..str.indexOf(c)]; 431 } 432 /** 433 * Exceptions thrown during expecting data. 434 */ 435 class ExpectScriptParseException : Exception { 436 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null){ 437 super(message, file, line, next); 438 } 439 } 440 class ExpectScriptException : Exception { 441 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null){ 442 super(message, file, line, next); 443 } 444 } 445 446 } 447 else{} 448