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