Macros for fun and profit
A brief overview of macros
We have (somewhat whimsically) entitled this section, Macros for fun and profit. You might be persuaded that you could profit from using macros, but maybe you think you won't have much fun writing them. Let's see what we can do to change your mind !
Users sometimes feel that the whole topic of macros is too complicated, and they tend to steer clear of them, without even trying to see what they could accomplish by taking advantage of their capabilities.
Are SPFLite macros really that complicated? Well, you could certainly make them complicated if you were inclined to - but then that would be your doing, and not SPFLite's fault!
There's actually not that much that is inherently "hard" about these macros, once you familiarize yourself with the thinBasic syntax and features. It's really the same as with the rest of SPFLite itself, which has a large array of primary and line commands and keyboard functions at your disposal. That doesn't necessarily make it "hard". It just means you have a lot of tools that you could use if you needed them. The specific commands you use, and how you use them, are driven by your requirements, which in turn are driven by the nature of the data you are creating or modifying.
The same is true with macros. A given macro is really only as complicated as the editing task you are trying to carry out. A trivial task can be done with a trivial macro, but a very complex task calls for an involved macro.
That's only fair. If you wrote some external script in Perl, Rexx or C++ to do a complex editing process, that would be involved, too. This is computation - not magic. You can't get something for nothing.
So, the main point is: don't panic. Most SPFLite users that create macros will probably only need relatively short scripts. But, the tools and features are there to do really cool and complicated things - if that's what you need and want to do.
Let's try and show you how straightforward a macro can be. The best way to do that is with an example, so let's choose a simple one that you should find easy to understand.
Example: The ONLY macro
The ONLY macro implements a two-step SPFLite command sequence:
FIND ALL string
This changes the display to show ONLY those text lines containing a specified string.
In the "good old days" of SPFLite 1.0, as well as in other editors like ISPF, early Tritus SPF, and SPF/SE, this is the only way you could do that. SPFLite presently allows you perform this function directly using the built-in command NEXCLUDE. But, bear with us - imagine that SPFLite didn't have this command built in, but you needed to do it anyway. After all, eventually you will find some task you need to do that SPFLite doesn't do directly, and then you really will need a macro.
What we need is to create a macro which will perform these two commands after we enter the command ONLY string.
Why can't we just enter the commands themselves directly, like we could do in those "good old days" when SPFLite only supported simple .SPM macros? It's because these programmable macros are not merely "dumb" command lines, but are full-fledged programming statements written in the thinBasic language. Everything you write in an SPFLite programmable macro must conform to the syntax and language rules of thinBasic.
Invoking SPFLite commands within a macro is done by calling a function designed for this purpose, the SPF_Cmd function, which is used like this:
SPF_Cmd("some SPFLite primary command")
The command you specify can be any valid thinBasic string expression, but for now we'll start with simple string literals.
By the way, in the example above, "some SPFLite primary command" means that you can't (directly) enter line commands using this function. However, you can use the LINE primary command to achieve the same thing. See the LINE primary command in the main Help documentation for more information.
So, what we want to create is a macro script containing the lines
SPF_Cmd("FIND ALL XXX")
Let's do that, and create the file ONLY.MACRO in the SPFLite \MACROS folder.
FYI, if you're going to get in the business of creating and editing macros frequently, you may find it convenient to set up a FILELIST so you can see all of your macros in one place in the File Manager. See the FILELIST documentation in the main Help file for more information, but basically, simply create a file named MACROS.FILELIST containing the following two lines. Store in the SPFLite data directory.
For XP it would be:
FilePath: C:\Documents and Settings\username\My Documents\SPFLite\MACROS
and for Win 7/8:
FilePath: C:\Users\username\My Documents\SPFLite\MACROS
However, SPFLite has one additional requirement when creating a macro. The first line of it must be a macro prototype (or, header) statement, which takes the form of a thinBasic comment line in a special format. In our example, it requires little more than the name, and our macro now looks like this:
SPF_Cmd("FIND ALL XXX")
That wasn't so bad, was it?
Our macro is complete ... except that right now, it always does a FIND command for a literal string of "XXX". That "XXX" was just a place-holder, while we were busy trying to explain things and setting up the macro. But, we originally wanted to do a FIND operation that looked like FIND ALL string, where string is supplied on the primary command line - remember? Somehow, we need to create this command dynamically, so it includes your string, and not just XXX all the time.
To do that, we need to have the SPF_Cmd function accept a string expression, rather than a string literal. So, let's make a simple change to our macro to do just that:
SPF_Cmd("FIND ALL " + Get_Arg$(1))
Here, the string literal "FIND ALL " and the string returned by the function Get_Arg$(1) are concatenated together to form a single string value. The Get_Arg$(n) function returns the string contents of the specified argument number. The + plus sign is the string concatenation operator (you can also use & ampersand if you like; they both mean the same thing).
Remember to put a blank after the "FIND ALL " string, like we did here. Otherwise, the ALL and the string you got back from Get_Arg$(1) would get "run together". If you did that, a macro command like ONLY ABC would cause the macro to create a command string expression like FIND ALLABC which would either be illegal, or just not what you wanted.
So now, if the command ONLY FRED were issued, Get_Arg$(1) returns the string "FRED", and so the two commands issued would effectively become:
FIND ALL FRED
Exactly what we want!
But, suppose we want to include optional FIND operands like WORD or PREFIX or LAST. This is another simple change, and our final macro will now look like:
SPF_Cmd("FIND ALL " + Get_Arg$(0))
Hmm, what's different? The Get_Arg$(0) instead of Get_Arg$(1) is a request for all the operands, not just the first. (When you provide two or more macro operands to ONLY, the Get_Arg$(0) function returns all of them together, so they are separated from each other by a blank.)
So, if the macro command ONLY FRED WORD were issued, the two commands issued would be:
FIND ALL FRED WORD
Not too shabby, eh?
Enhancing the ONLY macro to work in more than one context
Certainly, that was a fairly simple example, but like most macros, it grows once you start using it and think "It would be nice if ...".
So, what more could we possibly do with this ONLY macro? Well, a lot of the time the ONLY macro would be issued while you were browsing a file and you saw some word of interest within the text. Why should we have to manually type in (or even cut and paste) a word on to the command line, when it's already sitting right there in the text? (There's something to be said for constructive laziness! - RH) Let's change ONLY so that it will use the word that's being pointed to by the cursor.
This is also surprisingly easy. Our macro now becomes:
SPF_Cmd("FIND ALL " + Get_Curr_Word$)
The Get_Curr_Word$ function is designed specifically to address this need. It returns the 'word' that the cursor is sitting on, when the cursor is underneath some string in the data area of the edit window. Now, we can map a keyboard key (let's say, Ctrl-O) to ONLY and then all we need to do is place the cursor anywhere within a word and hit Ctrl-O.
We now have a quite useful macro, and it is still only 3 lines long.
However, we have now lost the ability to actually type in an ONLY string command, since the macro now requires a word to be present under the cursor. If you are typing a command on the command line, there isn't any file data under the cursor, because your cursor is not in the data area.
But, we don't want to give up the command-line usage. What we really need is a way to determine the macro's context - that is, how and where the macro is being used. Then, we tailor the macro to operate differently depending on how it's being used, and then we can use the same macro for more than one purpose.
Managing quoted operands
Before we put this new macro together, there is a little matter of quotes to deal with. If you recall from the basics of SPFLite, you can have quoted string values enclosed in ' single quotes, " double quotes or ` accent quotes, or simple strings can be supplied unquoted.
You also need to be aware that keywords that are reserved elsewhere, like ALL and LAST, are not reserved as operands of a macro used as a primary command.
So, if you supply a macro argument that happens to be a keyword of some other SPFLite command, and you then use that string to dynamically create some command, it could create some invalid syntax. For example, without some "fancy footwork", if we tried to say ONLY FIRST, one of the created commands would be FIND ALL FIRST, and that's not legal.
In your macro, you might be tempted to get around this by always putting quotes around the string you get back from a Get_ARG$ call. But, suppose, as a macro user, you actually placed quotes around a string, like ONLY "FIRST". If your macro added quotes, too, you'd have too many of them. So, if you issued a macro command like ONLY "FIRST" with quotes already around the operand, and the macro logic added quotes, too, you'd end up with a command like FIND ALL ""FIRST"" with improperly-doubled quotes - and that's not what you wanted.
You could check to see if the argument already had quotes on it, and tailor your expression accordingly, but that would be a real nuisance to do all the time. Fortunately, you don't have to. We have created a function called SPF_Quote$. What this does is it adds quotes to a string expression, unless it is already quoted, in which case it leaves it as is. It also takes into account whether a string contains inner quotes of any type, so that it wouldn't create a wrongly-quoted string. It does that by choosing one of the three allowable quote types, to avoid having the enclosing quotes be the same as any inner quotes which are literal data values. These actions make the function what we call a "smart quoter".
The example below uses the smart-quoter function to correctly create SPFLite command strings. Now, even if the operands you provide are already quoted or are SPFLite reserved words, the macro will still work properly.
You would need this smart quoter function whether the operand was supplied on the command line or was obtained from a function like Get_Curr_Word$, because we have no way of knowing if the word under the cursor is reserved either.
Managing argument lists
When you use the ONLY macro, and it has multiple arguments, you can "grab" them all with a function. But if you needed to quote the search string AND you had additional arguments, how could you just quote the first one and not the rest of them?
This is going to take a little tinkering ... but we promise, it won't be too hard.
First of all, you need to understand that macro parameter lists are not like SPFLite command parameters. For example, a FIND command could say FIND ALL ABC or it could say FIND ABC ALL, and they both mean the same thing, because you can supply operands to most SPFLite commands in any order. SPFLite can tell which operands are reserved words and which are user-supplied values, and it sorts them out based on which command you used.
Although Macros can be coded to handle operands this way, for the purposes of this discussion we will ignore that temporarily and keep things simple. If, following this discussion, you want to learn how to handle operands in that manner, review Full Parse Access.
With that in mind, we will agree to design the ONLY macro so that the string operand is always the first one, meaning that you'd always use Get_Arg$(1) to "grab" it. What about all the other operands - if there are any? How do we grab them?
First, we take advantage of the special fact that Get_Arg$ can be called with either one or two arguments of its own. If called with two arguments, you provide a "range" of operands. So, if you wanted arguments 3 through 5, you could get them with Get_Arg$(3,5).
You have to ask for arguments in ascending order. Calling Get_Arg$(5,3) will return nothing (an empty string). And also trigger a failing RC value and associated error message if you cared to check and retrieve them.
Second, if you read closely in the Function Details section, you'll find there is a function called Get_Arg_Count. So, assuming you wanted all arguments starting with the second one, you could say Get_Arg$(2,Get_Arg_Count). That would work fine, but it's kind of wordy. Since it will be a common task to require "all remaining operands" this way, the function allows the second operand to be specified as 0, with a call like Get_Arg$(2,0). When you supply a 0, it is treated the same as if you had used Get_Arg_Count instead. That makes this a lot shorter and easier to type.
A call like Get_Arg$(2,0) is not considered breaking the rule about asking for arguments in ascending order; it's just a short-cut convenience so you don't have to worry so much about exactly how many arguments were present, and so you don't have to type so much.
Finally, if a call to Get_Arg$ asks for more arguments than you supplied when you ran the macro, it will only provide the ones that are there. It's not illegal to ask for "too many".
Just like the single-argument form Get_Arg$(0), when you provide two or more macro operands and use Get_Arg$(2,0) it returns the requested macro operands so they are separated from each other by a blank.
Putting it all together
So, let's finish the job on our macro and flesh it out, adding a bit of error checking as well.
Note that Get_Arg$ will put spaces between the operands it returns, but not before or after. So, in the SPF_Cmd expressions, we have to separate the various "pieces" of the expression with a blank so that we end up creating a valid SPFLite primary command; otherwise macro argument 1 and 2 would get "run together" and things wouldn't work right.
Notice too that we only quote the first argument, but not the remaining ones. Why not? Because we agreed that the "string" argument would always be the first one (remember our discussion from above?) which means the remaining arguments, if there are any, would only be SPFLite keywords like WORD or LAST, which would never be quoted if you wanted to use them as normal keywords and not as data. So, we don't call SPF_Quote$ for them.
if Get_Arg$(0) <> "" then
SPF_Cmd("FIND ALL " + SPF_Quote$(Get_Arg$(1) + " " + Get_Arg$(2,0)))
if Get_Curr_Word$ <> "" then
SPF_Cmd("FIND ALL " + SPF_Quote$(Get_Curr_Word$))
Halt(FAIL, "There is no word under the cursor")
Several changes have been made:
- If a command line argument is present, then that is what is searched for
- If no command line argument and the cursor is on a word, that word is searched for
- If no command line argument and the cursor is not on a word, an error message is issued using the Set_Msg function
- The smart-quoter function SPF_Quote$ ensures that if operand 1 is quoted or is a reserved word, it won't cause an incorrect FIND command to be created.
Our final, fully fleshed-out macro is still only 12 lines, including the header. That's pretty reasonable, and hopefully not too intimidating, even if you're new to writing macros.
Simplifying the ONLY macro with NEXCLUDE
If you look closely, by providing some extra logic to take the context into consideration, you now have a capability that SPFLite doesn't have on its own - even the most current version. Remember we said the basic functionality of ONLY is already covered by the built-in command NEXCLUDE? Well, what ONLY does now is actually more powerful than NEXCLUDE. But, that doesn't prevent you from actually using NEXCLUDE here. We can eliminate two lines from the macro by replacing EXCLUDE and FIND ALL with NEXCLUDE ALL.
This is a case where having a good familiarity with all of SPFLite's array of available commands can help you write macros that are shorter and run faster. Here is how the revised ONLY macro would now look:
if Get_Arg$(0) <> "" then
SPF_Cmd("NEXCLUDE ALL " + SPF_Quote$(Get_Arg$(1)) + " " + Get_Arg$(2,0))
if Get_Curr_Word$ <> "" then
SPF_Cmd("NEXCLUDE ALL " + SPF_Quote$(Get_Curr_Word$))
Halt(FAIL, "There is no word under the cursor")
Extra credit - Part 1
Did you notice how we determined the 'context' of the macro - how it was being used? Calling Get_Arg$(0) returns a string with all the arguments provided to the macro. If none were provided, you'd get an empty string.
Suppose your macro is called AX.MACRO. Under what circumstances would you get an empty string back from Get_Arg$(0)when you used this macro?
- You enter AX on the command line by itself with nothing else, to run the macro as a primary command, and press Enter.
- You enter AX or AXX in the sequence number area of one or two lines, and press Enter. (If you use AX as a line-command macro, only one may be specified at a time. If you use AXX you must specify it in pairs. Otherwise, SPFLite will detect a usage error.)
Is that the only way to determine the context? No, there are a few other ways, too:
- You can call Get_Arg_Count to find out how many arguments were present; if there weren't any, you get 0
- You can call Get_Csr_LPTR to find out the line-pointer where the cursor is located; if it's not in the main edit area, you'd get a 0
- You can call Get_LNUM(Get_Csr_LPTR)) to find out the line-number where the cursor is located; if it's not in the main edit area or it's on a non-data line like BNDS or TABS, you'd get a 0 (since lines like BNDS and TABS have no line number). This is a little more involved test, but it's a "stronger" one.
You can also see whether your macro was called as a primary-command macro, or as a line-command macro. See Primary mode vs. Line mode macros for more information.
Based on your requirements, you'd have to decide how "strict" your test needs to be to ensure it only runs under the right circumstances.
Extra credit - Part 2
Now that we have created the ONLY macro, fine-tuned its behavior, and taken its operating context into account, what do you think:
Is ONLY a primary-command macro or a line-command macro?
Yes, it's a trick question - and the answer is, surprisingly, both.
The ONLY macro works quite well when invoked as a line-command macro. If you enter ONLY in the sequence area of a data line, then move the cursor over the word you want as the operand, and press Enter, the macro will function correctly. In fact, the ONLY macro name could be in the sequence area of one line, and the cursor could be on an entirely different line, and it would still work.
What happens? First, the Get_Arg$(0) call will return an empty string, because the macro has no arguments, per se. It then finds a value returned from the call to Get_Curr_Word$, and the macro logic proceeds from that point.
No, this isn't likely to be a convenient way to type this, and most users would not do it that way, but unless a macro is carefully written to insist on being run only one way or the other, it may very well meet all the requirements for execution as either a primary-command macro or a line-command macro - even if that isn't what you intended when you wrote it.
Most of the time, you will write a macro with the intent of it only running one way, either as a primary-command macro or a line-command macro, and you may neither know no care that it would work the 'other' way.
Created with the Personal Edition of HelpNDoc: Create HTML Help, DOC, PDF and print manuals from 1 single source