Creating a File-Based Script Interpreter in ST/V: A Mini-Tutorial by Dan Shafer, Author of "Practical Smalltalk" and "Smalltalk Programming for Windows" Scripting languages are a passion of mine. Ever since my best-selling book, "HyperTalk Programming," appeared in early 1988, I've been fascinated by peoples' fascination with them. At one point, I built the beginnings of a HyperTalk interpreter in Smalltalk, my favorite "real" programming language. Frequently, people in the CIS forum and elsewhere raise the issue of how to undertake the processing of a scripting language from within Smalltalk. This small file is designed to demonstrate the bare-bones of that subject. There is much more that could be said; if enough Smalltalkers ask, I'll write more detail at some point! The Basic Design Idea ===================== Smalltalk/V includes a very powerful message called perform:. It has a variation called perform:with:. These messages take a symbol as a first (or only) argument and an optional parameter as the second argument. (In fact, there are other variations that support 2, 3, and 4 arguments and one, perform:withArguments: that supports an array of a theoretically unlimited number of arguments.) They look for a method named the same as the first argument and then pass the arguments to that method as if th e method had been invoked in some more traditional Smalltalk message-sending way. So here's the core concept: 1. Create a class or add to an existing class. 2. Create a method (I use a class method but that's for convenience in my case) called, for lack of a better term, "script." 3. Create a text file whose format consists of Smalltalk messages with an appropriate number of arguments, one "script" line per line. 4. Have at least one method defined in the class that's useful by the script (presumed). 5. Invoke the script command. Create the Class ================ I defined a class called DoTester. Here's its class definition: Object subclass: #DoTester instanceVariableNames: 'testColl ' classVariableNames: '' poolDictionaries: '' (The instance variable is only here to demonstrate the use of the particular script I wrote for this example.) Creating the Script Method ========================== Here is the code for the simplest script method I can think of. It's not generic, reusable, or, for that matter, very interesting; hopefully it makes up for those lacks by being easy to understand and instructive! script "Opens a text file containing one or more lines of text formatted with CRs. Attempts to perform each line of the file as if it were a method call." | fileName nextLine | nextLine := Array new. fileName := Disk file: 'Test Script 1'. [fileName atEnd] whileFalse: [nextLine := (fileName nextLine) asArrayOfSubstrings. self perform: (nextLine at:1) asSymbol with: (nextLine at:2)]. This is a fairly self-explanatory method. It opens a file, which is presumed to consist of a series of lines, each of which has two "words" in it (actually an array of substrings isn't quite _that_ simple, but almost). The first word is a method name and the second is an argument to that method. Define a Scriptable Method ========================== As far as I can tell, no method is unscriptable. Here's a very simple class method used in this tutorial: foo1: aString "Just brings up a Prompter with some text." Prompter prompt: 'Executing foo1' default: aString. As you can see, all this method does is display a prompt informing you that you have reached the method you thought you were executing. It then displays the string argument in the text box in the Prompter. Pretty simple. (Be sure to note, though, that this is a class method in this sample, as is script.) Create the Text File ==================== The format of the text file in our example is assumed to be pretty minimalist. In fact, here's a small sample file this method will open and carry out as a script: foo1: 'Test1' foo1: 'Test2' foo1: 'EndTest' Invoke the Script Command ========================= To invoke the script command as it is to this point, just type and execute the following code in the Transcript or a Workspace: DoTester script It will display the three prompters just as expected. Scripting an Instance Method ============================ With the basic concept proven, let's go do something a little more adventuresome. Write an instance method called addToColl: that adds an item to the instance variable testColl. Here's the code for my version: addToColl: anObject "adds an item to the instance variable testColl" testColl add: anObject. Now we also need some code to initialize the instance variable if we want to stay out of trouble: initialize "Sets up testColl to be an Array object" testColl := (Bag new). The script method is now a class method, so letUs write an equivalent instance method. I called it scriptIt just to avoid confusion: scriptIt "Opens a text file containing one or more lines of text formatted with CRs. Attempts to perform each line of the file as if it were a method call." | fileName nextLine | nextLine := Array new. fileName := Disk file: 'Test Script 2'. [fileName atEnd] whileFalse: [nextLine := (fileName nextLine) asArrayOfSubstrings. self perform: (nextLine at:1) asSymbol with: (nextLine at:2)]. There are really only two changes aside from the name change: I changed the name of the file so I could test both of these scripting doodads independently; and I added a self just before the perform: statement. We need one more method. We donUt yet have any way to verify that we have added anything to testColl. So weUll write a big, complex method to print out its contents: showColl "Prints the testColl instance variable for testing" Transcript show: testColl printString. OK, now hereUs a snippet that will test the validity of the above code: " Tests the use of the script function" |x| x := DoTester new initialize. x scriptIt. x showColl. The Transcript should display the contents of the Bag testColl. What WeUve Done =============== So what weUve accomplished here is weUve defined a text file that contains a number of lines of "code" that are read in by a Smalltalk method and passed to perform:with: for execution. From here, it's incremental steps to a full-blown scripting environment. You could obviously add arguments to the script: or scriptIt: methods (which don't exist yet, of course) identifying the file to be loaded and perhaps whether the scripts were method or instance scripts. Or you could even add another element to the script files indicating this and parse the result. You'll obviously want to do a more complex and thorough job of parsing the script files, too. One-word arguments, a limitation imposed here only for clarity of code, are not very useful.