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 : 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 the script before evaluation.
85 	 *
86 	 * Params:
87 	 *     arg = The argument to escape.
88 	 *
89 	 * Returns:
90 	 *     The escaped script.
91 	 */
92 	private string escapeArg(string arg)
93 	{
94 		// Allow backslashes to be passed as intended to Tcl.
95 		arg = arg.replaceAll(regex(r"\\"), r"\\");
96 
97 		// Braces are used as string delimeters in Tcl so escape those.
98 		arg = arg.replaceAll(regex(r"\}"), r"\}");
99 		arg = arg.replaceAll(regex(r"\{"), r"\{");
100 
101 		return arg;
102 	}
103 
104 	/**
105 	 * Evaluate a script fragment using the interpreter.
106 	 *
107 	 * Params:
108 	 *     script = The script to evaluate, including any format placeholders.
109 	 *     args = variadic list of arguments to provide data for any format placeholders.
110 	 */
111 	public void eval(A...)(string script, A args)
112 	{
113 		foreach (ref arg; args)
114 		{
115 			static if (is(typeof(arg) == string))
116 			{
117 				arg = this.escapeArg(arg);
118 			}
119 		}
120 
121 		debug (log) this._log.eval(script, args);
122 
123 		int result = Tcl_EvalEx(this._interpreter, format(script, args).toStringz, -1, 0);
124 
125 		if (result == TCL_ERROR)
126 		{
127 			string error = Tcl_GetStringResult(this._interpreter).to!(string);
128 
129 			debug (showTclErrors)
130 			{
131 				writeln(error);
132 			}
133 
134 			debug (log) this._log.warning(error);
135 		}
136 	}
137 
138 	/**
139 	 * Set the result of the interpreter.
140 	 * This is sometimes used to set the result to an error if things go bad.
141 	 *
142 	 * Params:
143 	 *     result = The text to set as the result, including any format placeholders.
144 	 *     args = variadic list of arguments to provide data for any format placeholders.
145 	 */
146 	public void setResult(A...)(string result, A args)
147 	{
148 		debug (log) this._log.info("Setting interpreter result '%s'", format(result, args));
149 		Tcl_SetResult(this._interpreter, format(result, args).toStringz, TCL_STATIC);
150 	}
151 
152 	/**
153 	 * Get the result string from the interpreter.
154 	 *
155 	 * Returns:
156 	 *     A string representing the result of the last script fragment evaluated.
157 	 */
158 	public T getResult(T)()
159 	{
160 		string result = Tcl_GetStringResult(this._interpreter).to!(string);
161 		debug (log) this._log.info("Getting interpreter result '%s'", result);
162 		return result.to!(T);
163 	}
164 
165 	/**
166 	 * Create a new command in the Tcl interpreter.
167 	 *
168 	 * Params:
169 	 *     name = The name of the new command.
170 	 *     commandProcedure = A function pointer to the new command.
171 	 *     data = Extra data to be passed to the command on invocation.
172 	 *     deleteProcedure = The procedure to run when deleteCommand is called.
173 	 *
174 	 * Returns:
175 	 *     A command token that can be used to refer to the command created.
176 	 */
177 	public Tcl_Command createCommand(string name, Tcl_CmdProc commandProcedure, ClientData data = null, Tcl_CmdDeleteProc deleteProcedure = null)
178 	{
179 		debug (log) this._log.info("Creating command %s", name);
180 		return Tcl_CreateCommand(this._interpreter, name.toStringz, commandProcedure, data, deleteProcedure);
181 	}
182 
183 	/**
184 	 * Delete a command in the Tcl interpreter.
185 	 *
186 	 * Params:
187 	 *     name = The name of the command to delete.
188 	 */
189 	public void deleteCommand(string name)
190 	{
191 		debug (log) this._log.info("Deleting command %s", name);
192 
193 		int result = Tcl_DeleteCommand(this._interpreter, name.toStringz);
194 
195 		if (result == TCL_ERROR)
196 		{
197 			string error = Tcl_GetStringResult(this._interpreter).to!(string);
198 
199 			debug (showTclErrors)
200 			{
201 				writeln(error);
202 			}
203 
204 			debug (log) this._log.warning(error);
205 		}
206 	}
207 
208 	/**
209 	 * Set the value of a variable.
210 	 * If the variable doesn't exist it is created.
211 	 *
212 	 * Params:
213 	 *     name = The name of the variable to set.
214 	 *     value = The variable's value.
215 	 */
216 	public void setVariable(T)(string name, T value)
217 	{
218 		debug (log) this._log.info("Setting variable %s <- '%s'", name, value);
219 		Tcl_SetVar(this._interpreter, name.toStringz, value.to!(string).toStringz, TCL_GLOBAL_ONLY);
220 	}
221 
222 	/**
223 	 * Get the value of a variable.
224 	 *
225 	 * Params:
226 	 *     name = The name of the variable to get the value of.
227 	 *
228 	 * Returns:
229 	 *     A string containing the variable's value.
230 	 */
231 	public string getVariable(string name)
232 	{
233 		string result = Tcl_GetVar(this._interpreter, name.toStringz, TCL_GLOBAL_ONLY).to!(string);
234 		debug (log) this._log.info("Getting variable %s -> '%s'", name, result);
235 		return result;
236 	}
237 
238 	/**
239 	 * Delete a variable from the interpreter.
240 	 *
241 	 * Params:
242 	 *     name = The name of the variable to delete.
243 	 */
244 	public void deleteVariable(string name)
245 	{
246 		debug (log) this._log.info("Deleting variable %s", name);
247 
248 		int result = Tcl_UnsetVar(this._interpreter, name.toStringz, TCL_GLOBAL_ONLY);
249 
250 		if (result == TCL_ERROR)
251 		{
252 			string error = Tcl_GetStringResult(this._interpreter).to!(string);
253 
254 			debug (showTclErrors)
255 			{
256 				writeln(error);
257 			}
258 
259 			debug (log) this._log.warning(error);
260 		}
261 	}
262 
263 }