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, mkdir; 46 import std.algorithm : all, any, filter, each, canFind; 47 import std.path : baseName, buildPath; 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] [-d <dir>] <file>... 65 Options: 66 -h --help Show this message 67 -v --verbose Show verbose output 68 -d --dir <dir> The directory to print output files. [default: ./] 69 "; 70 71 int main(string[] args){ 72 73 auto arguments = docopt.docopt(doc, args[1..$], true, "dexpect 0.0.1"); 74 bool verbose = arguments["--verbose"].toString.to!bool; 75 if(verbose) writefln("Command line args:\n%s", arguments); 76 77 auto fList = arguments["<file>"].asList; 78 if(!arguments["<file>"].asList 79 .all!(fName => fName.exists)){ 80 writefln("Error, a filename does not exist\n%s", fList.to!string); 81 return 1; 82 } 83 string outDir = arguments["--dir"].toString; 84 if(!outDir.exists){ 85 mkdir(outDir); 86 } 87 import std.typecons : Tuple; 88 import std.array : array; 89 import std.traits; 90 alias fname_text = Tuple!(string, "fname", string, "text"); 91 alias fname_grammar = Tuple!(string, "fname", ParseTree, "parsedGrammar"); 92 alias fname_handler = Tuple!(string, "fname", ScriptHandler, "handler"); 93 alias fname_result = Tuple!(string, "fname", bool, "result"); 94 auto parsedScripts = fList 95 .map!(a => fname_text(a, a.readText)) 96 .map!(b => fname_grammar(b.fname, ScriptGrammar(b.text))); 97 98 bool[string] results; 99 100 auto failedParsing = parsedScripts.filter!(a => !a.parsedGrammar.successful); 101 if(!failedParsing.empty){ 102 writefln("Parsing failure"); 103 failedParsing.each!(a => writefln(" - %s", a.fname)); 104 return 1; 105 } 106 foreach(script; parsedScripts){ 107 string fname = 108 [outDir, format("%s_%s.dexpectOutput", 109 Clock.currTime.toISOString.stripToFirst('.'), script.fname.baseName)].buildPath; 110 File[] outFiles = [File(fname, "w")]; 111 if(verbose){ 112 stdout.writefln("\nExecuting script: %s", script.fname.baseName); 113 outFiles ~= stdout; 114 } 115 ScriptHandler s = ScriptHandler(script.parsedGrammar.children[0], outFiles); 116 117 results[script.fname] = s.run(); 118 if(verbose) stdout.writefln(""); 119 s.printVariables; 120 s.printAllData; 121 } 122 123 if(results.values.any!(a => a==true)) 124 writefln("----- Succesful -----"); 125 results.keys.filter!(key => results[key]==true) 126 .each!(key => writefln("%s", key)); 127 128 if(results.values.any!(a => a==false)) 129 writefln("\n----- Failures -----"); 130 results.keys.filter!(key => results[key]==false) 131 .each!(key => writefln("%s", key)); 132 133 return 0; 134 } 135 136 struct ScriptHandler{ 137 ParseTree theScript; 138 Expect expect; 139 File[] outFiles; 140 string[string] variables; 141 alias variables this; // referencing 'this' will now point to variables 142 143 /** 144 * Overloads the index operators so when "timeout" is set, 145 * it is propogated to the Expect variable 146 */ 147 void opIndexAssign(string value, string name){ 148 if(name == "timeout" && this.expect !is null) 149 expect.timeout = value.to!long; 150 if(name == "?") 151 throw new ExpectScriptException("Trying to set a variable named '?'"); 152 this.variables[name] = value; 153 } 154 string opIndex(string name){ 155 return this.variables[name]; 156 } 157 158 @disable this(); 159 this(ParseTree t){ 160 this.theScript = t; 161 } 162 this(ParseTree t, File[] files){ 163 this.theScript = t; 164 this.outFiles = files; 165 } 166 /** 167 * Runs this script. 168 * Returns true if the script succeeds 169 */ 170 bool run(){ 171 try{ 172 this.handleScript(theScript); 173 } catch(ExpectException e){ 174 return false; 175 } catch(ExpectScriptParseException e){ 176 stderr.writefln("An error occured.\n%s", e.msg); 177 return false; 178 } 179 return true; 180 } 181 182 void printAllData(){ 183 if(expect !is null){ 184 expect.sink.put("Data from spawn:\n>>>>>\n%s\n<<<<<", expect.data); 185 } 186 } 187 void printVariables(){ 188 if(expect !is null){ 189 expect.sink.put("Variables used:\n%s", this.variables); 190 } 191 } 192 /** 193 * Handles the script, delegating the work down to it's helper functions 194 */ 195 void handleScript(ParseTree script){ 196 assert(script.name == "ScriptGrammar.Script"); 197 auto blocks = script.children; 198 blocks.each!(block => this.handleBlock(block, expect)); 199 } 200 201 /* 202 * Handles a Block, parsing it's Attributes if required, and delegating its children to 203 * handleEnclosedBlock or handleStatement, depending on it's type. 204 */ 205 void handleBlock(ParseTree block, ref Expect e){ 206 // checks whether we should run this block 207 // A block should be run if it has no ScriptGrammar.OSAttr attribute, or if it has no 208 // ScriptGrammar.OSAttr not pointing at this os 209 auto doRun = block.getAttributes("ScriptGrammar.OSAttr") 210 .map!(attr => attr.children[0]) 211 .filter!(osAttr => osAttr.matches[0] != os) 212 .empty; 213 if(doRun == false){ 214 return; 215 } 216 217 block.children 218 .filter!(child => child.name != "ScriptGrammar.Attribute") // remove attribute blocks, as we dont need em anymore 219 .filter!(child => child.children.length > 0) // remove empty blocks 220 .each!((node){ 221 switch(node.name){ 222 case "ScriptGrammar.EnclosedBlock": 223 handleEnclosedBlock(node, e); 224 break; 225 case "ScriptGrammar.OpenBlock": 226 handleStatement(node.children[0], e); 227 break; 228 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", node)); 229 } 230 }); 231 } 232 233 void handleEnclosedBlock(ParseTree block, ref Expect e){ 234 block.children 235 .each!((node){ 236 switch(node.name){ 237 case "ScriptGrammar.Block": 238 handleBlock(node, e); 239 break; 240 case "ScriptGrammar.Statement": 241 handleStatement(node, e); 242 break; 243 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", node)); 244 } 245 }); 246 } 247 248 void handleStatement(ParseTree statement, ref Expect e){ 249 statement.children 250 .each!((child){ 251 switch(child.name){ 252 case "ScriptGrammar.Spawn": 253 handleSpawn(child, e); 254 break; 255 case "ScriptGrammar.Expect": 256 handleExpect(child, e); 257 break; 258 case "ScriptGrammar.Set": 259 handleSet(child); 260 break; 261 case "ScriptGrammar.Send": 262 handleSend(child, e); 263 break; 264 case "ScriptGrammar.Import": 265 handleImport(child, e); 266 break; 267 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 268 } 269 }); 270 } 271 272 void handleImport(ParseTree _import, ref Expect e){ 273 if(_import.children.length != 1) 274 throw new ExpectScriptParseException("Error parsing import command"); 275 string importHelper(ParseTree toImport){ 276 string str; 277 foreach(child; toImport.children){ 278 switch(child.name){ 279 case "ScriptGrammar.String": 280 str ~= child.matches[0]; 281 break; 282 case "ScriptGrammar.Variable": 283 str ~= child.matches[0]; 284 break; 285 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 286 } 287 } 288 return str; 289 } 290 // import the file into text 291 auto importText = importHelper(_import.children[0]).readText; 292 auto newParseTree = ScriptGrammar(importText); 293 handleScript(newParseTree.children[0]); 294 295 } 296 void handleSend(ParseTree send, ref Expect e){ 297 if(send.children.length == 0) 298 throw new ExpectScriptParseException("Error parsing set command"); 299 300 string sendHelper(ParseTree toSend){ 301 string str; 302 foreach(child; toSend.children){ 303 switch(child.name){ 304 case "ScriptGrammar.String": 305 str ~= child.matches[0]; 306 break; 307 case "ScriptGrammar.Variable": 308 str ~= this[child.matches[0]]; 309 break; 310 case "ScriptGrammar.ToSend": 311 str ~= sendHelper(child); 312 break; 313 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 314 } 315 } 316 return str; 317 } 318 e.sendLine(sendHelper(send.children[0])); 319 } 320 void handleSet(ParseTree set){ 321 if(set.children.length != 2) 322 throw new ExpectScriptParseException("Error parsing set command"); 323 string setHelper(ParseTree toSet){ 324 string str; 325 foreach(child; toSet.children){ 326 switch(child.name){ 327 case "ScriptGrammar.String": 328 str ~= child.matches[0]; 329 break; 330 case "ScriptGrammar.Variable": 331 str ~= this[child.matches[0]]; 332 break; 333 case "ScriptGrammar.SetVal": 334 str ~= setHelper(child); 335 break; 336 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 337 } 338 } 339 return str; 340 } 341 string name, value; 342 foreach(child; set.children){ 343 if(child.name == "ScriptGrammar.SetVar") 344 name = child.matches[0]; 345 else if(child.name == "ScriptGrammar.SetVal") 346 value = setHelper(child); 347 } 348 this[name] = value; 349 } 350 351 void handleExpect(ParseTree expect, ref Expect e){ 352 if(expect.children.length ==0 ) 353 throw new ExpectScriptParseException("Error parsing expect command"); 354 if(e is null) 355 throw new ExpectScriptParseException("Cannot call expect before spawning"); 356 string expectHelper(ParseTree toExpect){ 357 string str; 358 foreach(child; toExpect.children){ 359 switch(child.name){ 360 case "ScriptGrammar.String": 361 str ~= child.matches[0]; 362 break; 363 case "ScriptGrammar.Variable": 364 str ~= this[child.matches[0]]; 365 break; 366 case "ScriptGrammar.ToExpect": 367 str ~= expectHelper(child); 368 break; 369 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 370 } 371 } 372 return str; 373 } 374 variables["$?"] = e.expect(expectHelper(expect.children[0])).to!string; 375 } 376 377 void handleSpawn(ParseTree spawn, ref Expect e){ 378 if(spawn.children.length == 0) 379 throw new ExpectScriptParseException("Error parsing spawn command"); 380 string spawnHelper(ParseTree toSpawn){ 381 string str; 382 foreach(child; toSpawn.children){ 383 switch(child.name){ 384 case "ScriptGrammar.String": 385 str ~= child.matches[0]; 386 break; 387 case "ScriptGrammar.Variable": 388 str ~= this[child.matches[0]]; 389 break; 390 case "ScriptGrammar.ToSpawn": 391 str ~= spawnHelper(child); 392 break; 393 default: throw new ExpectScriptParseException(format("Error parsing ParseTree - data %s", child)); 394 } 395 } 396 return str; 397 } 398 e = new Expect(spawnHelper(spawn.children[0]), this.outFiles); 399 if(this.keys.canFind("timeout")) 400 e.timeout = this["timeout"].to!long; 401 } 402 } 403 auto getAttributes(ParseTree tree){ //checks if this tree has attributes 404 return tree.children 405 .filter!(child => child.name == "ScriptGrammar.Attribute"); 406 } 407 auto getAttributes(ParseTree tree, string attrName){ //checks if this tree has attributes 408 return tree.getAttributes 409 .filter!(attr => attr.children.length > 0) 410 .filter!(attr => attr.children[0].name == attrName); 411 } 412 413 mixin(grammar(scriptGrammar)); 414 415 /// Grammar to be parsed by pegged 416 /// Potentially full of bugs 417 enum scriptGrammar = ` 418 ScriptGrammar: 419 # This is a simple testing bed for grammars 420 Script <- (EmptyLine / Block)+ :eoi 421 Block <- Attribute? (:' ' Attribute)* (EnclosedBlock / OpenBlock) :Whitespace* 422 423 Attribute <- ( OSAttr ) 424 OSAttr <- ('win' / 'linux') 425 426 EnclosedBlock <- :Whitespace* '{' 427 ( :Whitespace* (Statement / Block) :Whitespace* )* 428 :Whitespace* '}' :Whitespace* 429 OpenBlock <- (:Whitespace* Statement :Whitespace*) 430 431 Statement <- :Whitespace* (Comment / Spawn / Send / Expect / Set / Import) :Spacing* :Whitespace* 432 433 Comment <: :Spacing* '#' (!eoi !endOfLine .)* 434 435 Spawn <- :"spawn" :Spacing* ToSpawn 436 ToSpawn <- (~Variable / ~String) :Spacing ('~' :Spacing ToSpawn)* 437 438 Send <- :"send" :Spacing* ToSend 439 ToSend <- (~Variable / ~String) :Spacing ('~' :Spacing ToSend)* 440 441 Expect <- :"expect" :Spacing* ToExpect 442 ToExpect <- (~Variable / ~String) :Spacing ('~' :Spacing ToExpect)* 443 444 Set <- :'set' :Spacing* SetVar :Spacing :'=' Spacing SetVal 445 SetVar <- ~VarName 446 SetVal <- (~Variable / ~String) :Spacing ('~' :Spacing SetVal)* 447 448 Import <- 'import' :Spacing* ToImport 449 ToImport <- (~Variable / ~String) 450 451 Keyword <~ ('#' / 'set' / 'expect' / 'spawn' / 'send') 452 KeywordData <~ (!eoi !endOfLine .)+ 453 454 Variable <- :"$(" VarName :")" 455 VarName <- (!eoi !endOfLine !')' !'(' !'$' !'=' !'~' .)+ 456 457 Text <- (!eoi !endOfLine !'~' .)+ 458 DoubleQuoteText <- :doublequote 459 (!eoi !doublequote .)+ 460 :doublequote 461 SingleQuoteText <- :"'" 462 (!eoi !"'" .)+ 463 :"'" 464 String <- ( 465 ~DoubleQuoteText / 466 ~SingleQuoteText / 467 ~Text 468 ) 469 Whitespace <- (Spacing / EmptyLine) 470 EmptyLine <- ('\n\r' / '\n')+ 471 `; 472 473 /+ --------------- Utils --------------- +/ 474 475 // Strits a string up to the first instance of c 476 string stripToFirst(string str, char c){ 477 return str[0..str.indexOf(c)]; 478 } 479 /** 480 * Exceptions thrown during expecting data. 481 */ 482 class ExpectScriptParseException : Exception { 483 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null){ 484 super(message, file, line, next); 485 } 486 } 487 class ExpectScriptException : Exception { 488 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null){ 489 super(message, file, line, next); 490 } 491 } 492 493 } 494 else{} 495