1 /**
2  * Tcl interpreter module.
3  *
4  * License:
5  *     MIT. See LICENSE for full details.
6  */
7 module tkd.interpreter.tcl;
8 
9 /**
10  * Imports.
11  */
12 import std.conv;
13 import std.regex : Captures, regex, replaceAll;
14 import std.stdio;
15 import std.string;
16 import tcltk.tcl;
17 import tkd.interpreter.logger;
18 
19 /**
20  * Simple wrapper for the Tcl interpreter.
21  */
22 class Tcl
23 {
24 	/*
25 	 * An instance of the native tcl interpreter.
26 	 */
27 	protected Tcl_Interp* _interpreter;
28 
29 	/**
30 	 * An instance of this tcl interpreter.
31 	 */
32 	private static Tcl _instance;
33 
34 	/*
35 	 * The logger.
36 	 */
37 	protected Logger _log;
38 
39 	/*
40 	 * Create the interpreter and initialise it.
41 	 *
42 	 * Throws:
43 	 *     Exception if Tcl interpreter cannot be initialised.
44 	 */
45 	protected this()
46 	{
47 		debug (log) this._log = new Logger("debug.log");
48 		debug (log) this._log.info("Inititalising Tcl");
49 
50 		this._interpreter = Tcl_CreateInterp();
51 
52 		if (Tcl_Init(this._interpreter) != TCL_OK)
53 		{
54 			string result = Tcl_GetStringResult(this._interpreter).to!(string);
55 			throw new Exception(format("Tcl interpreter could not be initialised. %s", result));
56 		}
57 	}
58 
59 	/*
60 	 * Clean up.
61 	 */
62 	protected ~this()
63 	{
64 		Tcl_DeleteInterp(this._interpreter);
65 	}
66 
67 	/**
68 	 * Get an instance of this class.
69 	 *
70 	 * Returns:
71 	 *     If An instance doesn't exist, one is created and returned.
72 	 *     If one already exists, that is returned.
73 	 */
74 	public static Tcl getInstance()
75 	{
76 		if (Tcl._instance is null)
77 		{
78 			Tcl._instance = new Tcl();
79 		}
80 		return Tcl._instance;
81 	}
82 
83 	/**
84 	 * Escape harmful characters in arguments that are to be used in a script.
85 	 *
86 	 * Params:
87 	 *     args = An array of arguments to escape.
88 	 *
89 	 * Returns:
90 	 *     Escaped arguments.
91 	 */
92 	public string[] escape(string[] args)
93 	{
94 		foreach (ref arg; args)
95 		{
96 			arg = this.escape(arg);
97 		}
98 		return args;
99 	}
100 
101 	/**
102 	 * Escape harmful characters in the passed argument that is to be used in a 
103 	 * script.
104 	 *
105 	 * Params:
106 	 *     arg = The argument to escape.
107 	 *
108 	 * Returns:
109 	 *     The argument.
110 	 */
111 	public string escape(string arg)
112 	{
113 		string replacer(Captures!(string) m)
114 		{
115 			final switch(m.hit)
116 			{
117 				case `\`:
118 					return `\\`;
119 
120 				case `"`:
121 					return `\"`;
122 
123 				case `$`:
124 					return `\$`;
125 
126 				case `[`:
127 					return `\[`;
128 
129 				case `]`:
130 					return `\]`;
131 			}
132 			assert(false);
133 		}
134 		return arg.replaceAll!(replacer)(regex(`\\|"|\$|\[|\]`));
135 	}
136 
137 	/**
138 	 * Evaluate a script fragment using the interpreter.
139 	 *
140 	 * Params:
141 	 *     script = The script to evaluate, including any format placeholders.
142 	 *     args = variadic list of arguments to provide data for any format placeholders.
143 	 */
144 	public void eval(A...)(string script, A args)
145 	{
146 		foreach (ref arg; args)
147 		{
148 			static if (is(typeof(arg) == string))
149 			{
150 				arg = this.escape(arg);
151 			}
152 		}
153 
154 		debug (log) this._log.eval(script, args);
155 
156 		static if (A.length)
157 		{
158 			script = format(script, args);
159 		}
160 
161 		int result = Tcl_EvalEx(this._interpreter, script.toStringz, -1, 0);
162 
163 		if (result == TCL_ERROR)
164 		{
165 			string error = Tcl_GetStringResult(this._interpreter).to!(string);
166 
167 			debug (showTclErrors)
168 			{
169 				writeln(error);
170 			}
171 
172 			debug (log) this._log.warning(error);
173 		}
174 	}
175 
176 	/**
177 	 * Set the result of the interpreter.
178 	 * This is sometimes used to set the result to an error if things go bad.
179 	 *
180 	 * Params:
181 	 *     result = The text to set as the result, including any format placeholders.
182 	 *     args = variadic list of arguments to provide data for any format placeholders.
183 	 */
184 	public void setResult(A...)(string result, A args)
185 	{
186 		static if (A.length)
187 		{
188 			result = format(result, args);
189 		}
190 
191 		debug (log) this._log.info("Setting interpreter result '%s'", result);
192 		Tcl_SetResult(this._interpreter, result.toStringz, TCL_STATIC);
193 	}
194 
195 	/**
196 	 * Get the result string from the interpreter.
197 	 *
198 	 * Returns:
199 	 *     A string representing the result of the last script fragment evaluated.
200 	 */
201 	public T getResult(T)()
202 	{
203 		string result = Tcl_GetStringResult(this._interpreter).to!(string);
204 		debug (log) this._log.info("Getting interpreter result '%s'", result);
205 		return result.to!(T);
206 	}
207 
208 	/**
209 	 * Create a new command in the Tcl interpreter.
210 	 *
211 	 * Params:
212 	 *     name = The name of the new command.
213 	 *     commandProcedure = A function pointer to the new command.
214 	 *     data = Extra data to be passed to the command on invocation.
215 	 *     deleteProcedure = The procedure to run when deleteCommand is called.
216 	 *
217 	 * Returns:
218 	 *     A command token that can be used to refer to the command created.
219 	 */
220 	public Tcl_Command createCommand(string name, Tcl_CmdProc commandProcedure, ClientData data = null, Tcl_CmdDeleteProc deleteProcedure = null)
221 	{
222 		debug (log) this._log.info("Creating command %s", name);
223 		return Tcl_CreateCommand(this._interpreter, name.toStringz, commandProcedure, data, deleteProcedure);
224 	}
225 
226 	/**
227 	 * Delete a command in the Tcl interpreter.
228 	 *
229 	 * Params:
230 	 *     name = The name of the command to delete.
231 	 */
232 	public void deleteCommand(string name)
233 	{
234 		debug (log) this._log.info("Deleting command %s", name);
235 
236 		int result = Tcl_DeleteCommand(this._interpreter, name.toStringz);
237 
238 		if (result == TCL_ERROR)
239 		{
240 			string error = Tcl_GetStringResult(this._interpreter).to!(string);
241 
242 			debug (showTclErrors)
243 			{
244 				writeln(error);
245 			}
246 
247 			debug (log) this._log.warning(error);
248 		}
249 	}
250 
251 	/**
252 	 * Set the value of a variable.
253 	 * If the variable doesn't exist it is created.
254 	 *
255 	 * Params:
256 	 *     name = The name of the variable to set.
257 	 *     value = The variable's value.
258 	 */
259 	public void setVariable(T)(string name, T value)
260 	{
261 		debug (log) this._log.info("Setting variable %s <- '%s'", name, value);
262 		Tcl_SetVar(this._interpreter, name.toStringz, value.to!(string).toStringz, TCL_GLOBAL_ONLY);
263 	}
264 
265 	/**
266 	 * Get the value of a variable.
267 	 *
268 	 * Params:
269 	 *     name = The name of the variable to get the value of.
270 	 *
271 	 * Returns:
272 	 *     A string containing the variable's value.
273 	 */
274 	public string getVariable(string name)
275 	{
276 		string result = Tcl_GetVar(this._interpreter, name.toStringz, TCL_GLOBAL_ONLY).to!(string);
277 		debug (log) this._log.info("Getting variable %s -> '%s'", name, result);
278 		return result;
279 	}
280 
281 	/**
282 	 * Delete a variable from the interpreter.
283 	 *
284 	 * Params:
285 	 *     name = The name of the variable to delete.
286 	 */
287 	public void deleteVariable(string name)
288 	{
289 		debug (log) this._log.info("Deleting variable %s", name);
290 
291 		int result = Tcl_UnsetVar(this._interpreter, name.toStringz, TCL_GLOBAL_ONLY);
292 
293 		if (result == TCL_ERROR)
294 		{
295 			string error = Tcl_GetStringResult(this._interpreter).to!(string);
296 
297 			debug (showTclErrors)
298 			{
299 				writeln(error);
300 			}
301 
302 			debug (log) this._log.warning(error);
303 		}
304 	}
305 
306 }