MicroEmacs is based on a core set of built in commands and variables that provide a a basic set of functionality, almost all of the visible features of MicroEmacs are implemented or controlled by its macro language. NanoEmacs is quite simply this base set of commands of MicroEmacs with no macro support and provides a basic Emacs editor.
MicroEmacs is extended with an interpreted script file that allows new commands to be defined using the base level commands and variables which is referred to as a Macro. Once a macro command has been defined then it appears as a built in command to the user, and as with the existing commands, may have a key binding to invoke the new command.
In addition to the base commands, the macro language includes control statements and logical operators which allow loops and conditional command sequences to control the sequence of operations.
For new users to MicroEmacs writing macros can be quite daunting, some of the macros shipped with MicroEmacs are complex and have evolved over many years. Beginners should start with simple macros which has verifiable output and can be performed interactively. This makes the task of writing and testing easier, some of the behind the scene macros like the mouse driver and auto-spell checker are difficult to get working and requires a greater understanding of MicroEmacs and experience in macro debugging.
A good starting point is to try to write a macro to do something that you find yourself doing all the time, for example reread-file, write-region etc.
In the following example then we walk through the steps to create a macro that saves a region of text to a file (i.e. the text between point and mark). The resultant macro will be called write-region, note that this macro already exists in MicroEmacs.
The first thing to do is walk through the macro that you wish to write interactively in MicroEmacs. Whilst doing this you should decide which approach to take and the commands (or keys) used. Our write-region could either use narrows or copy the region to a temporary buffer, a decision has to be made which approach is best to use. Narrows would be more efficient but there are more complications (such as restoring edit mode, avoid changing the undo stack etc.). For the moment then the temporary buffer approach is probably the best solution.
Walking through the sequence of steps required to save a region of text then the following command sequence would be executed (assuming a marked up region has been selected):-
esc w ; Copy the region. C-x b "tmp-buffer" ; Create a new temporary buffer. C-y ; Yank copied region into temporary buffer. C-x C-w "write_region_file" ; Save the buffer to a file. C-x k RETURN ; Kill the temporary buffer.
This is a fairly easy sequence of steps, the history of key presses can be viewed in the variable $recent-keys(5), the command list-variables(2) includes this variable in its output.
Macros are not written in key sequences, instead the commands associated with the key bindings are used. A key binding command name may be determined using describe-key(2) and describe-bindings(2). Some command bindings include a numeric argument, the numeric argument is important (both the value and absence/presence of the argument) and can change the behavior of the command. C-up is not 'backward-line', it is '5 backward-line' to go back 5 lines, if you supply a numerical value to the command as well, e.g. 'esc 3 C-up' you must multiply the user argument with the numerical argument of the binding:
3 up -> 3 'backward-line' -> 3 * 1 backward-line -> 3 backward-line 3 C-up -> 3 '5 backward-line' -> 3 * 5 backward-line -> 15 backward-line
Translating the keys that we used into commands then our write-region process becomes:-
copy-region find-buffer "tmp-buffer" yank write-buffer "write_region_file" delete-buffer RETURN
The obvious problem here is the last line, 'delete-buffer RETURN'. When running interactively you can simply press RETURN to delete the current buffer however you cannot do this in a macro so you must explicitly specify the buffer name to delete:
The the process of writing the buffer has the side effect of changing the buffer name to reflect the output file name, as delete-buffer follows the write-buffer then the name will have changed.
Correcting the delete buffer name, the sequence of commands is wrapped with a macro definition, which binds a name to the command sequence, and a terminator which terminates the macro.
define-macro write-region copy-region find-buffer "tmp-buffer" yank write-buffer "write_region_file" delete-buffer "write_region_file" !emacro
This macro works but is not very effective as the output filename has been fixed, this creates a file "write_region_file" in the MicroEmacs start up directory - probably not what is required. The specific macro that has been created must be made interactive, this means it must get filenames from the user, cope with a failure and the user doing silly things. So what must we cope with ? The list of defects is surprisingly long considering we are doing so little:
The list of issues is a re-occurring theme, once you have got the hang of this it does become easier. In short reliable macros must:
The last one is really reserved for low level drivers (i.e. mouse, directory listings etc.) which must be bomb proof.
In the previous section we have created a basic macro to save a region of text, in this section we look at that macro is transformed from a static sequence to commands to a fully interactive robust command. Reference should be made to MicroEmacs Macro Language at a Glance for a quick tour of the language syntax.
Forming a temporary buffer is simply a naming issue, temporary buffer names are simply started with a "*", by convention for a simple buffer name then they are terminated with a "*" as well. The temporary buffer name should reflect the name of the original macro hence the buffer name should be something like *write-region-tmp*
define-macro write-region copy-region find-buffer "*write-region-tmp*" yank write-buffer "write_region_file" delete-buffer "write_region_file" !emacro
Looking at the current implementation if the write fails the temporary buffer will be left lying around. This is very likely to happen and if it does then we will get the last region as well as the next region in the output file. The simple solution is to delete the temporary buffer first, our first attempt to correct this:
define-macro write-region copy-region delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" .... !emacro
Unfortunately this fails if the buffer does not exist, so we must tell MicroEmacs to ignore the failure by forcing the execution:
define-macro write-region copy-region !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank write-buffer "write_region_file" delete-buffer "write_region_file" !emacro
If the write_region_file buffer already exists the buffer being written is renamed to "write_region_file<1>" rather than "write_region_file" and so the delete-buffer will delete the wrong buffer. Therefore rather than delete an explicitly named buffer the environment variables are used to query the name of the current buffer. The variable $buffer-bname(5) returns the current buffer's buffer name and may be used instead of an explicit name.
define-macro write-region copy-region !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank write-buffer "write_region_file" delete-buffer $buffer-bname !emacro
Macro variables may be used to get just about any information you could possibly want. For example $buffer-fname(5) could be used after the buffer-write to get the file name, $window-line(5) provides the current line number etc. See
Help -> Variable Glossary
for a full list of system variables. There are also Macro Variables (@,#,...) which are also very useful, e.g. @wl gives the text on the current line as a string, see the Macro Language Glossary for a full list of these and other macro features such as functions and directives.
Getting input from the user is always dangerous, most macros run so quickly that the user will not have sufficient time to abort the process (C-g) and if they do there is not much that can be done. When collecting input from the user the macro must wait for some input, at this point the user can and should be able to abort the process which must be handled. If the user has not aborted the user still has this uncanny ability of doing the unexpected and the input has to be checked, better still get MicroEmacs to get the input and check it.
There are two approaches to the input problem:
We would need to use the first approach if we needed the entered value multiple times, in this case we are not so we can use the simpler second approach but for the sake of this document lets consider the first approach.
set-variable $write-region-file-name @ml "Write-region file" write-buffer $write-region-file-name
This does work but you really do not want to do this ! Consider what happens if the user aborts while entering the file name ? The user would be left in the "*write-buffer-tmp*" buffer and would have to clear up the mess themselves. This may be fixed by catching the abort from the input and deleting the buffer.
!force !force set-variable $write-region-file-name @ml "Write-region file" !if ¬ $status !force delete-buffer $buffer-bname !abort !endif write-buffer $write-region-file-name
The second problem is with the input type, the @ml does not indicate that a file name is required. The type of input should be qualified to first check the input type and to provide other features such as file completion on file name entry. Specifying the input type correctly:
!force !force set-variable $write-region-file-name @ml04 "Write-region file" !if ¬ $status !force delete-buffer $buffer-bname !abort !endif write-buffer $write-region-file-name
In progressing through the example we have used the basic system variables ($var) as they are simple. Other variables exist: user (%var), buffer (:var) or command (.var). All of the previously mentioned variables use valuable system resource and consideration should be given as to whether the value is required once the macro has completed execution. In our case the file name is temporary and is not required outside of the scope of the macro, rather than waste memory then a register variable is preferred as they have zero cost and execute faster than the other variables. Re-writing the macro with register (#lx) variables:
!force !force set-variable #l1 @ml04 "Write-region file" !if ¬ $status !force delete-buffer $buffer-bname !abort !endif write-buffer #l1
At the start of this section we said that there were two techniques by which we could get user input, we selected the first (which was less desirable than the second). The approach that we have taken of passing the filename to write-buffer appears to be correct, however the write-buffer command probably expects a reasonable filename -- what happens if the user enters a directory name ? There are other potential esoteric problems with our current approach, all of which may be overcome, but not here.
The second technique, which is by far the best solution, is to simply get the command write-buffer(2) to get its input directly from the user. The command does this by default when bound to a key and run from the command line however when run from a macro the behavior is different as there is no command line. We can force this behavior by asking the command to be executed as if it were run from the command line:-
When write-buffer asks the MicroEmacs macro processor for a file name the @mn argument tells it to go directly to the user even though ME is currently running a macro. This will only get a single argument from the user, if the file already exits write-buffer asks for confirmation that over-writing the existing file is okay, but this argument has not been given so the command fails. To fix this we could do:
write-buffer @mn "y"
However this has now created even more issues, what happens if this file is already loaded into MicroEmacs, should we allow the user to decide ? We could allow the command line to handle all of the cases:
write-buffer @mn @mn @mn @mn ....
Which is rapidly becoming very messy for each individual argument, instead we use @mna to ask the command to handle all input associated with the command:
Note that we should have used this for the first method to protect write-buffer:
write-buffer #l1 @mna
In this case the file name is provided by the argument #l1 but the remaining arguments are handled internally by the command itself as if invoked from the command line.
Moreover, there is one subtle problem here that can cause a lot of problems especially for other macros trying to use this macro (uses tend to spot the problem themselves and correct it causing only minor frustration). The macro creates a new temporary buffer which has no file name, this means that saving to "./file" will write the region to "file" in the startup directory rather than in the current buffer's file path. For example, if you start MicroEmacs from "/bin", load file "/tmp/foo.txt" and write-region "./bar.txt" you will create the file "/bin/bar.txt" not "/tmp/bar.txt".
To solve this directory issue the file name of the temporary buffer has to be set to the same filename as the current buffer we are copying from. This is safe as this is a temporary buffer (starts with a '*') so MicroEmacs will not attempt to auto save or allow the buffer to be saved using write-buffer and is performed as follows:
define-macro write-region copy-region set-variable #l0 $buffer-fname ; ADDED to fix directory location !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 ; ADDED to fix directory location write-buffer @mna delete-buffer $buffer-bname !emacro
After all of that, the solution is in fact very simple, however the underlying complexity of the user expectations as to the behavior of the command are complicated.
This issue has already been touched on in the previous sections, by using the @mna only the write-buffer is likely to be aborted while the user is prompted for a file name etc. and if the user manages to abort elsewhere there really is not much that can be done about it without severely impacting the complexity of the macro. Practically it is only necessary to catch the exception when the user is being prompted on the command line with write-buffer, as with our existing user input a double !force is required to catch an abort at a prompt.
define-macro write-region copy-region set-variable #l0 $buffer-fname !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 !force !force write-buffer @mna !if ¬ $status !force delete-buffer $buffer-bname !abort !endif delete-buffer $buffer-bname !emacro
The $status(5) variable allows us to catch the exit state of the forced command and test for an abort condition following the forced statement. Note that the $status value is only valid immediately after the last executed command, executing another command changes the value of $status. If the status is required after execution of another command then it may be saved in a variable and used later.
As the write-buffer command should handle write file failures sensibly, and we have enabled it to get input from the user, then the command will automatically handle the failure conditions. Within our macro then it is simply a case of detecting and handling the failure condition -- we have already done this by handling the user input so no additions are required to the macro.
If the user has the current buffer displayed only once then the action of deleting the temporary buffer returns the current window to the previous buffer, as required. If however the buffer is displayed more than once then when the temporary buffer is deleted MicroEmacs will select a different non-displayed buffer to become the current window (unless this is the only buffer) and so the user will not be returned back to their original buffer.
This may be fixed by storing the initial buffer name and explicitly returning to it after the delete-buffer:
define-macro write-region copy-region set-variable #l0 $buffer-fname set-variable #l1 $buffer-bname !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 !force !force write-buffer @mna !if ¬ $status !force delete-buffer $buffer-bname find-buffer #l1 !abort !endif delete-buffer $buffer-bname find-buffer #l1 !emacro
However, when there are two windows into the same buffer the command may inherit the other window's buffer location, i.e. if the current window is on line 100 and the other window display this buffer is at line 200 the above version will leave the user at line 200 and a different region. The commands set-position(2) and goto-position(2) allow the current position to be saved and later restored, the commands both take a single character which identifies the saved position, this allows multiple positions to be saved. Re-writing our macro to save and restore the buffer position we get:
define-macro write-region copy-region set-variable #l0 $buffer-fname set-position "P" !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 !force !force write-buffer @mna !if ¬ $status !force delete-buffer $buffer-bname goto-position "P" !abort !endif delete-buffer $buffer-bname goto-position "P" !emacro
Finally we should consider how to make the macro more like a built in command so that other users and macros can take advantage of this new feature. Firstly let us consider supporting the numerical argument which is passed to all commands and macros. Considering write-buffer the command permits an invocation from the command line of:
esc 0 C-x C-w
or from a macro:
The 0 argument disables validity checks and therefore forces the write. It would be reasonable to expect that a new command that writes a region to a file behaves in exactly the same way, where the commands have consistent behavior it makes it far easier for the user to guess how like commands operate. Unfortunately this behavior is absent from our current macro command. To support the numerical argument then the @? and @# variables are used to test for and retrieve the numerical argument.
define-macro write-region copy-region set-variable #l0 $buffer-fname set-position "P" !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 !if @? !force !force @# write-buffer @mna !else !force !force write-buffer @mna !endif !if ¬ $status !force delete-buffer $buffer-bname goto-position "P" !abort !endif delete-buffer $buffer-bname goto-position "P" !emacro
We can optimize the macro a little by noting that the default behavior of "write-buffer" is the same as "1 write-buffer" (this is not always the case), re-writing:
define-macro write-region copy-region set-variable #l0 $buffer-fname set-position "P" !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 !force !force @# write-buffer @mna !if ¬ $status !force delete-buffer $buffer-bname goto-position "P" !abort !endif delete-buffer $buffer-bname goto-position "P" !emacro
After all of this work there are still some compatibility problems to be resolved for our macro to be a true command replacement. Considering our use of write-buffer, we have effectively have:
The command gets the filename from the macro line and allow us to invoke the command in our own macros. So what about our new write-region macro ? It is not unreasonable to expect that some macro developer in the future will want to write the current region out to a file and if we write this one correctly they will be able to use this command directly. At the moment a command invocation of
in another macro will not have the desired effect. The "myfile" argument is ignored and the user is always prompted for the file name. This can be solved by providing an optional parameter to be passed to the command using the /Hl @1 @1 4 variable as follows:
define-macro write-region copy-region set-variable #l0 $buffer-fname set-position "P" !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 !force set-variable #l1 @1 !if $status !force !force @# write-buffer #l1 @mna !else !force !force @# write-buffer @mna !endif !if ¬ $status !force delete-buffer $buffer-bname goto-position "P" !abort !endif delete-buffer $buffer-bname goto-position "P" !emacro
Setting the register variable #l1 to the first argument passed to the command will fail if no argument was given, so we use a !force to stop the macro from quitting and change the $status of the set-variable command. If the assignment fails then we get the filename from the user as before, if it succeeds then the calling macro has provided the file name.
The write-region macro is now complete and provides both a command line and macro interface. It is important to note that there is a subtle difference between macros and built in commands even though their capabilities are the same. To execute write-buffer in a macro and get the user to supply a file name you must do:
However for a macro, it is not itself a command and you cannot ask a macro to behave like a command. For a macro you must call it as:
The use of '@mna' is only valid for commands, this perhaps is a quirk of the language which should really be fixed in the future.
We started this tutorial with a unusable 5 line macro and now have a 20 line version which should be indistinguishable from a built in MicroEmacs command. The same command is available as part of the macro release and this document was written as part of the process of writing that macro. The only change to be made in making it part of the macro release is to change the set-position label from "P", which a user could use, to something like "\x88" which is much safer to use.
The final macro release version becomes:
define-macro write-region copy-region set-variable #l0 $buffer-fname set-position "\x88" !force delete-buffer "*write-region-tmp*" find-buffer "*write-region-tmp*" yank set-variable $buffer-fname #l0 !force set-variable #l1 @1 !if $status !force !force @# write-buffer #l1 @mna !else !force !force @# write-buffer @mna !endif !if ¬ $status !force delete-buffer $buffer-bname goto-position "\x88" !abort !endif delete-buffer $buffer-bname goto-position "\x88" !emacro
It is perhaps surprising the number of steps it has taken to write something that appears to be simple, however in writing macros there are many re-occurring themes and once familiar with the concepts then then you will typically get them right first time. Creating a macro like this takes approximately half an hour.
We have to question whether it is worth writing these extensions. If you find yourself doing the same thing all the time then definitely yes, it will save time and you will have less aches and pains in your hand. Spending the extra time to write a good macro saves time in the long run, it also provides a better base on which more sophisticated macros may be based. MicroEmacs has actually evolved like this. Hopefully this tutorial will encourage rather than frighten off budding macro writers.
Copyright (c) 1998-2006 JASSPA
Last Modified: 2006/08/13
Generated On: 2006/10/07