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