The Blunt Idea of Patching Code with Ex
Patching code with the patch command or the more elaborate quilt tool is common to make small changes to software before packaging it. And how about Ex?
1 Tools for Simple-Minded Patching
I'm not going to introduce either patch or quilt for the simple reason that I don't use them as often as I should. In my packaging scripts, I have the – undoubtedly nasty – habit of using sed or AWK instead. This is because they are much more familiar to me. And also because the context they rely on is more based on lines than on meaning.
Consider this simple file called foo
:
one
two
three
four
five
I would like to insert the line three-and-a-half
below the one containing three
. If I create a patch with quilt to do so, it would result in a diff looking like:
@@ -1,5 +1,6 @@
one
two
three
+three-and-a-half
four
five
The mere header @@ -1,5 +1,6 @@
suggests how it's all line-based. If I remove the first line, i.e. delete one
, having quilt apply the patch will upset it so much that it will stubbornly refuse to do anything. In real life, changes to code are typically part of patches, or subject to a new set of patches, for instance like when there's a new version of the software.
But I can't be bothered to update my patches every time the code changes. I want to be able to patch things in a way that's fuzzy enough that it can work even with changes to the source code. In this particular example, I would have wanted to be able to say something like “find the next occurrence of the string three, then add a line below it with three-and-half”.
2 Vim and Ex
This is when I forget about the patch and quilt commands and prefer using sed or AWK. They can search strings, they can edit lines, they can perform all sorts of amusing operations. If I'm fairly fluent with both of these tools for common operations, it occurred to me that there's another one I'm even more familiar with: Vim.
Coming back to the previous example with my file called foo
, the following command will do the business:
vim +'call search("three")' \
+'normal othree-and-a-half' \
+x \
foo
The +
argument executes an Ex command after the file foo
was read. I can add several of them and they will be executed in turn:
- The first one calls the
search()
function to jump to the stringthree
. - The second one executes Normal mode commands, those commands that I run when moving about, editing or doing anything that's not an Ex command which I would have typed in the command line after a
:
. Here, the Normal mode command I'm using iso
to insert a new line below the current one and enter Insert mode. Imagine literally typing o t h r e e - etc. - At this stage, I would have hit Esc to exit Insert mode, but there's no need for that as I'm moving on to my third and last command, which is just
x
, to save changes tofoo
and exit Vim. Note that I previously used single quotes to surround commands with spaces. No need for this here, there's no spaces.
Why call a function for searching the string and not using the Normal mode and /
? Because I would have had to add a carriage return character generated with Ctrl V Enter which would have appeared as follows and would have caused some unexpected behaviours when copied and pasted about:
+'normal /three^M'
You may have noticed some terminal flickering when running the whole Vim command line. That's because Vim actually opens its user interface for the briefest of moments. If I've got several such patching commands, I'm in for quite a bit of flickering, which is both annoying and time consuming. Vim does come with the -s
option to make it silent and prevent that. Except that it doesn't. Ex does. Ex is the Vi mode whereby I enter those commands after :
.
Fascinatingly enough, you'll find that, unlike Vim, Ex disagrees with executing /
in Normal mode with a trailing carriage return (^M
). All the more reasons for using the search()
function instead. This will cause no more flickering and run faster:
ex +'call search("three")' \
+'normal othree-and-a-half' \
+x \
foo
Naturally, it will stop working if the line containing three
disappears. But it will be resilient to most of what would befall the surrounding lines. In real life, it helps to use the context smartly and have an idea of what's likely to stay and what's likely to change. Don't forget that the search()
function alone is flexible and can for instance make use of regular expressions.
3 A More Elaborate Example
The reason I originally did all this was darktable and its rather annoying reluctance to overwrite output files. All credit to this very good raw editor for keeping all operations non-destructive, but this is UNIX after all, I'm expecting to be able to perform destructive actions and shoot myself in the foot with a rocket launcher if I so wish.
I wanted to change the behaviour of the darktable CLI implemented in src/cli/main.c
to overwrite output files instead of renaming them on conflict, which meant setting the overwrite
member of the dt_imageio_disk_t
type to TRUE
. However, src/cli/main.c
doesn't know about the dt_imageio_disk_t
type which is rather inconveniently defined in src/imageio/storage/disk.c
, i.e. not a header file which I could have simply included from src/cli/main.c
. Instead, I decided to copy the whole definition into src/cli/main.c
. This is blunt, of course, I should've written the missing header file, but I couldn't be bothered.
Copying the whole dt_imageio_disk_t
definition would have been rather cumbersome with AWK, sed and I'm not talking about the consequences of changes across versions if I'd used quilt or patch. But with Vim and Ex, I could literally copy across the paragraph making up the definition and it won't matter if it even changes in the future:
ex -s
+'call search("saved params")'
+'normal yap'
+'edit src/cli/main.c'
+'call search("progname")'
+'normal P'
+x
src/imageio/storage/disk.c;
- The first command takes me to the paragraph in
src/imageio/storage/disk.c
wheredt_imageio_disk_t
is defined. - I yank-a-paragraph, effectively yanking the whole definition of
dt_imageio_disk_t
, and it doesn't matter if anything changes inside of it. - I edit (open)
src/cli/main.c
, meaning thatsrc/imageio/storage/disk.c
is no longer the file I'll be working with. See how the last file argument supplied to Ex is there to open the first file, but it doesn't mean I can't change file as I go along. - I search a location in the file where to put the
dt_imageio_disk_t
definition, and I found that placing it near the stringprogname
seemed like a good idea. - I put (paste) the definition I yanked before the cursor.
- I save and exit Ex.