Advanced Vim macros

Vim’s massive command set in both normal and command mode makes the concept of a macro especially powerful. Most Vim users, if they ever use macros at all, will perhaps quickly record an edit to one line, starting with qq and finishing with q, executing with @q, and otherwise never give the concept much thought.

For slightly more advanced Vim users, macros may be more frequently useful, but they perhaps still believe that macros are a niche Vim function unto themselves, and not really open to the deft manipulation of text that pretty much everything else in Vim is.

As is typical in Vim, the rabbit hole of functionality goes much deeper than most users will ever plumb.

Vanilla Vim macros

Suppose I had a plaintext table loaded into a Vim buffer containing the name, subject matter expertise, birth year, and nationality of a few well-known programmers:

Stallman  Richard GNU 1953  USA
Wall  Larry   Perl  1954  USA
Moolenar  Bram  Vim 1961  Netherlands
Tridgell  Andrew  Samba  1967  Australia
Matsumoto  Yukihiro  Ruby  1965  Japan
Ritchie  Dennis  C  1941  USA
Thompson  Ken  Unix  1943  USA

Actually, that’s kind of untidy. Let’s start by formatting it a bit, by running it through Unix’s column tool.

:%!column -t

Stallman   Richard   GNU    1953  USA
Wall       Larry     Perl   1954  USA
Moolenar   Bram      Vim    1961  Netherlands
Tridgell   Andrew    Samba  1967  Australia
Matsumoto  Yukihiro  Ruby   1965  Japan
Ritchie    Dennis    C      1941  USA
Thompson   Ken       Unix   1943  USA

May as well sort it by surname too:

:%!sort -k1

Matsumoto  Yukihiro  Ruby   1965  Japan
Moolenar   Bram      Vim    1961  Netherlands
Ritchie    Dennis    C      1941  USA
Stallman   Richard   GNU    1953  USA
Thompson   Ken       Unix   1943  USA
Tridgell   Andrew    Samba  1967  Australia
Wall       Larry     Perl   1954  USA

That’s better.

Now, suppose we’ve got the task of replacing the fourth column of this table with the approximate age of the person, which we can get naturally enough by sutracting their birth year from the current year. This is a little awkward to do in pure ex, so we’ll record a macro for doing it on one line.

Experimenting a bit, we find that the following works well:

03wdei^R=2012-^R"^M^[0j

Broken down, this does the following:

  • 0 — Move to the start of the line
  • 3w — Skip three words, in this case to the fourth column
  • de — Delete to the end of the word
  • i — Enter insert mode
  • ^R= — Insert the contents of the special = register, which accepts an expression to evaluate
  • 2012-^R"^M — Enter the expression 2012-(birth year) and press Enter (literal ^M), which completes the operation, and inserts the result
  • ^[ — Leave insert mode
  • 0 — Return to the start of the line
  • j — Move down a line

So we record it into a macro a (to stand for “age”, no other reason) as we fix up the first line:

qa03wdei^R=2012-^R"^M^[0jq

Matsumoto  Yukihiro  Ruby   47  Japan
Moolenar   Bram      Vim    1961  Netherlands
Ritchie    Dennis    C      1941  USA
Stallman   Richard   GNU    1953  USA
Thompson   Ken       Unix   1943  USA
Tridgell   Andrew    Samba  1967  Australia
Wall       Larry     Perl   1954  USA

This now means that for each line, we can run the macro by just tapping @a a few times:

Matsumoto  Yukihiro  Ruby   47  Japan
Moolenar   Bram      Vim    51  Netherlands
Ritchie    Dennis    C      71  USA
Stallman   Richard   GNU    59  USA
Thompson   Ken       Unix   69  USA
Tridgell   Andrew    Samba  45  Australia
Wall       Larry     Perl   58  USA

That’s all pretty stock-standard macro stuff that you’ll likely have learned in other tutorials. The only thing that’s slightly voodoo (and certainly not vi-compatible) is the arithmetic done with the special = register. You can read about that in :help @=, if you’re curious.

Repeating Vim macros

As a first very simple hint, if you’re running a macro several times, don’t forget that you can prepend a count to it; in our case, 6@a would have fixed up all the remaining lines. To take advantage of this, it’s good practice to compose your macros so that they make sense when run multiple times; in the example above, note that the end of the macro is moving down onto the next line, ready to run the macro again if appropriate.

Similarly, if you’ve already run a macro once, you can run the same one again by just tapping the @ key twice, @@. This repeats the last run macro. Again, you can prepend a count to this, @a5@@.

The true nature of Vim macros

The registers that hold macros are the same registers that you probably use more frequently for operations like deleting, yanking, and pasting. Running a macro is simply instructing Vim to take the contents of a register and execute them as keystrokes, as if you were actually typing them. Thinking about macros this way has a couple of interesting consequences.

First of all, it means you needn’t restrict yourself to the single-keystroke vi commands for Vim when you compose macros. You can include ex commands as well, for example to run a substitution during a macro:

qb:s/foo/bar/g^Mq
@b

Secondly, and more interestingly, all of the operations that you can apply to registers in general work with what we normally call macros, with the old standards, delete, yank, and paste. You can test this with the example macro demonstrated above, by typing "ap, which will dump the raw text straight into the buffer. Also like other registers, it’ll show up in the output of the :registers command.

All this means is that like everything else in Vim, macros are just text, and all of Vim’s clever tools can be used to work with them.

Editing Vim macros in a buffer

If you’ve used macros even a little bit you’ll be very familiar with how frustrating they are to record. For some reason, as soon as you press qa to start recording a macro into register a, your normal fluency with Vim commands goes out the window and you keep making mistakes that will make the macro unsuitable to apply.

So, don’t compose macros on the fly. Just stop doing it. I recommend that if a macro is more than three keystrokes, you should compose it directly in a scratch Vim buffer so that you can test it iteratively.

Here’s an example using the macro above; suppose I realise partway through writing it that I made a mistake in typing 2011 instead of 2012. I finish recording the rest of the macro anyway, and dump the broken keystrokes into a new scratch buffer:

:enew
"ap

This gives me the contents of the macro in plain text in the buffer:

qa03wdei^R=2011-^R"^M^[0jq

So now all I have to do is change that bad year to 2012, and then yank the whole thing back into register a:

^"ay$

Now I can test it directly with @a on the appropriate file, and if it’s still wrong, I just jump back to my scratch buffer and keep fixing it up until it works.

One potential snag here is that you have to enter keystrokes like Ctrl+R as literal characters, but once you know you can enter any keystroke in Vim literally in insert or command mode by prefixing it with Ctrl+V, that isn’t really a problem. So to enter a literal Ctrl+R in insert mode, you type Ctrl+V, then Ctrl+R.

Running a Vim macro on a set of lines

It’s occasionally handy to be able to run a macro that you’ve got ready on a specific subset of lines of the file, or perhaps just for every line. Fortunately, there’s a way to do this, too.

Using the :normal command, you’re able to run macros from the ex command line:

:normal @a

All it takes is prefixing this with any sort of range definition to allow you to run a macro on any set of lines that you’re able to define.

Run the macro on each line of the whole buffer:

:% normal @a

Between lines 10 and 20:

:10,20 normal @a

On the lines in the current visual selection:

:'<,'> normal @a

On the lines containing the pattern vim:

:g/vim/ normal @a

When you get confident using this, :norm is a nice abbreviation to use.

Moving a Vim macro into a function

For really useful macros of your own devising, it’s occasionally handy to put it into a function for use in scripts or keystroke mappings. Here again the :normal command comes in handy.

Suppose I wanted to keep the age calculation macro defined above for later use on spreadsheets of this kind. I’d start by dumping it into a new buffer:

:enew
"ap

The macro appears as raw text:

03wdei^R=2012-^R"^M^[0j

I prefix it with a :normal call, and wrap a function definition around it:

function! CalculateAge()
    normal 03wdei^R=2012-^R"^M^[0j 
endfunction

Then all I need to do is include that in a file that gets loaded during Vim’s startup, possibly just .vimrc. I can call it directly from ex:

:call CalculateAge()

But given that I wanted it to be a quick-access macro, maybe it’s better to bind it to \a, or whatever your chosen <leader> character is:

nnoremap <leader>a :call CalculateAge()<CR>

Saving a Vim macro

If you want to have a macro always available to you, that is, always loaded into the appropriate register at startup, that can be done in your .vimrc file with a call to let to fill the register with the literal characters required:

let @a='03wdei^R=2012-^R"^M^[0j'

Appending extra keystrokes to a Vim macro

If you just want to tack extra keystrokes onto an existing macro and don’t care to edit it in a Vim buffer, you can do that by recording into it with its capital letter equivalent. So, if you wanted to add more keystrokes into the register b, start recording with qB, and finish with the usual q.

Recursive Vim macros

If you’re crazy enough to need this, and I never have, there’s an excellent Vim Tip for it. But personally, I think if you need recursion in your text processing then it’s time to bust out a real programming language and not Vimscript to solve your problem.

If the issue for which you think you need recursion is running a macro on every line of a buffer with an arbitrary number of lines, then you don’t need recursion; just record a one-line version of the macro and call it with :% normal @a to run it on every line.

Vim macro gotchas

Here are a few gotchas which will save you some frustration if you know about them ahead of time:

  • When you have a hammer, everything starts to look like a nail. Don’t try to solve problems with macros when there are better solutions in the ex command set, or available in external programs. See the Vim koans page for a couple of examples.
  • You need to insert keystrokes like Enter as literal Ctrl+M, so that they look like ^M. The convenience abbreviations in mappings, like <CR>, simply don’t work. You’re likely to find yourself chording Ctrl+V a lot.
  • Macros tend to stop, sometimes for apparently no reason and with no warning messages, as soon as they hit some sort of error. One particularly annoying instance of this is when you’re performing several substitutions in a macro, because if it doesn’t find any instances of a pattern it will throw an error and stop executing. You can prevent this by adding the e flag to the substitution call:

    :s/foo/bar/e
    

Thanks to user bairui for suggesting a safer alternative for yanking macro lines from buffers, which I’ve changed. He’s written a whole blog post replying to this one. It’s a good read if you’re interested in going in to even more depth about when to use macros in Vim.

Local .vimrc files

If you can, it’s a good idea to set up your .vimrc file using conditionals so that it’s compatible on all of the systems with which you need to work. Using one .vimrc file enables you to include it as part of a centralised set of dotfiles that you can keep under version control.

However, if on a particular machine there’s a special case which means you need to load some Vim directives for that machine, you can achieve this by way of a local Vim file kept in .vimrc.local, only on one particular machine, and detecting its existence before attempting to load it in your master .vimrc file with the following stanza:

if filereadable(glob("~/.vimrc.local")) 
    source ~/.vimrc.local
endif

As an example, on one of the nameservers that I manage, I wanted to make sure that the correct filetype was loaded when editing zone files ending in .nz or .au for New Zealand and Australian domains. The following line in .vimrc.local did the trick:

autocmd BufNewFile,BufRead *.au,*.nz set filetype=bindzone

If the .vimrc.local file doesn’t exist on any particular machine, Vim will simply not attempt to load it on startup.

Besides machine-specific code, this kind of setup may be advisable if you keep secret or potentially sensitive information in your .vimrc file that you wouldn’t want published to a public version control tracker like GitHub, such as API keys, usernames, machine hostnames, or network paths.

Testing HTTP/1.1 responses

Before changing DNS A records for a website, it’s prudent to check that the webserver with the IP address to which you’re going to change the records will actually serve a website with the relevant hostname; that is, if it’s an Apache HTTPD webserver, that it has a valid VirtualHost definition for the site.

If you don’t actually have administrative access to the webserver to check this, there are many basic ways to test it; from the command line, three of the most useful include using curl, wget, or plain old telnet. For each, the method comprises manipulating the HTTP/1.1 request of the target webserver such that the website you want to test is used as the hostname in the Host header.

Using curl

Perhaps the quickest and tidiest way to check this from a Unix command line is using curl, the binary frontend to the libcurl library. You do this by making an HTTP/1.1 request of the target server’s IP address, while including an explicitly specified value for the Host. This is done using the -H option:

$ curl -H "Host: sanctum.geek.nz" 120.138.30.239

This spits out quite a lot of information, including some on stderr, so you may choose to filter it and just check for the <title> tag, with a little bit of context, to make sure the site you expected really is being returned as the appropriate response:

$ !! 2>/dev/null | grep -C3 '<title>'
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Arabesque | Systems, Tools, and Terminal Science</title>
<link rel="profile" href="http://gmpg.org/xfn/11" />
<link rel="stylesheet" type="text/css" media="all" href="https://sanctum...
<link rel="pingback" href="https://blog.sanctum.geek.nz/xmlrpc.php" />

Using wget

An equivalent to the curl method can be achieved using the --header option for the commonly available wget:

$ wget --header="Host: sanctum.geek.nz/arabesque" 69.163.229.57 -q -O -

Using telnet

If you don’t have curl available, Telnet works just as well on both Windows and Unix-like systems, though it’s a little more awkward to work with, as you have to type the request and its headers straight into the TCP session:

$ telnet 69.163.229.57 80
Trying 69.163.229.57...
Connected to 69.163.229.57.
Escape character is '^]'.
GET / HTTP/1.1
Host: sanctum.geek.nz/arabesque

Note that you need to press Enter twice after writing in the hostname to check to complete the HTTP request. If this spits the HTML of your expected page back at you and closes the connection, then you’ve got some indication that things are configured correctly.

Yet another option to test this, particularly if you want to actually view the site in a browser, is to change your system’s hosts file to force DNS resolution to be different for the appropriate hostname on your local system.

Thanks to commenter Jaime Herazo for suggesting the wget method in the comments, and commenter sam for suggesting the -C option for grep.

Default grep options

When you’re searching a set of version-controlled files for a string with grep, particularly if it’s a recursive search, it can get very annoying to be presented with swathes of results from the internals of the hidden version control directories like .svn or .git, or include metadata you’re unlikely to have wanted in files like .gitmodules.

GNU grep uses an environment variable named GREP_OPTIONS to define a set of options that are always applied to every call to grep. This comes in handy when exported in your .bashrc file to set a “standard” grep environment for your interactive shell. Here’s an example of a definition of GREP_OPTIONS that excludes a lot of patterns which you’d very rarely if ever want to search with grep:

GREP_OPTIONS=
for pattern in .cvs .git .hg .svn; do
    GREP_OPTIONS="$GREP_OPTIONS --exclude-dir=$pattern
done
export GREP_OPTIONS

Note that --exclude-dir is a relatively recent addition to the options for GNU grep, but it should only be missing on very legacy GNU/Linux machines by now. If you want to keep your .bashrc file compatible, you could apply a little extra hackery to make sure the option is available before you set it up to be used:

GREP_OPTIONS=
if grep --help | grep -- --exclude-dir &>/dev/null; then
    for pattern in .cvs .git .hg .svn; do
        GREP_OPTIONS="$GREP_OPTIONS --exclude-dir=$pattern"
    done
fi
export GREP_OPTIONS

Similarly, you can ignore single files with --exclude. There’s also --exclude-from=FILE if your list of excluded patterns starts getting too long.

Other useful options available in GNU grep that you might wish to add to this environment variable include:

  • --color — On appropriate terminal types, highlight the pattern matches in output, among other color changes that make results more readable
  • -s — Suppresses error messages about files not existing or being unreadable; helps if you find this behaviour more annoying than useful.
  • -E, -F, or -P — Pick a favourite “mode” for grep; devotees of PCRE may find adding -P for grep‘s experimental PCRE support makes grep behave in a much more pleasing way, even though it’s described in the manual as being experimental and incomplete

If you don’t want to use GREP_OPTIONS, you could instead simply set up an alias:

alias grep='grep --exclude-dir=.git'

You may actually prefer this method as it’s essentially functionally equivalent, but if you do it this way, when you want to call grep without your standard set of options, you only have to prepend a backslash to its call:

$ \grep pattern file

Commenter Andy Pearce also points out that using this method can avoid some build problems where GREP_OPTIONS would interfere.

Of course, you could solve a lot of these problems simply by using ack … but that’s another post.

Safe MySQL updates

If you use the MySQL command line client a lot, you could consider adding the following to your .bashrc:

alias mysql='mysql --safe-updates'

Log out, then in again, or source your .bashrc file. This will quietly add in the --safe-updates parameter to any call you make for the mysql binary, which will prevent you from performing UPDATE or DELETE operations on any table if you neither specify a LIMIT condition nor a WHERE condition based on an indexed field. This stops you from accidentally deleting or updating every row in a table with queries like DELETE FROM `table` or UPDATE `table` SET `field` = 'value'.

If you really do want to delete a table’s entire contents, TRUNCATE `table` is more efficient anyway, and something you’re considerably less likely to fat-finger.

You can check this is loaded by typing alias with no arguments:

$ alias
alias mysql='mysql --safe-updates'

This very seldom gets in the way of actual operations you intend to run, and at the same time it prevents you from running potentially disastrous queries over your whole table. The MySQL manual lists it as for beginners, but regardless of your experience level in production environments this kind of safety valve can be a lifesaver, particularly if you’re not at your most lucid.

Like any Bash alias, if you want to call mysql directly without going through the alias, perhaps because you really do want to update a field in every single row of a table, then just prepend a backslash:

$ \mysql

Funnily enough, a valid alias of this option is --i-am-a-dummy. Pick whichever you think is more appropriate.

Vim as Debian default

The default text editor in installations of Debian and its derivatives is Nano, largely because it’s a simple and small editor. If you’re a Vim user, you might find it a little jarring to be presented with a modeless editor when you run commands like visudo or vipw.

Debian’s alternatives system makes this reasonably easy to adjust. If you have Vim installed, it should be available as one of the possible implementations of the editor alternative. You can check this with the update-alternatives command:

# update-alternatives --list editor
/bin/ed
/usr/bin/nano
/usr/bin/vim.basic

This shows that Vim is available as an alternative with /usr/bin/vim.basic, so you can update the symlink structure that defines the default editor like so:

# update-alternatives --set editor /usr/bin/vim.basic
... using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in manual mode.

Now if you fire up visudo, vipw, or sudo -e you should find that Vim is fired up instead of the editor you didn’t want.

On my own workstation I have the latest Vim compiled from Mercurial and installed into /usr/local via checkinstall, so I had to add this to the alternatives system before I could use it:

# update-alternatives --install /usr/bin/editor editor /usr/local/bin/vim 200 \
    --slave /usr/share/man/man1/editor.1.gz editor.1.gz /usr/local/share/man/man1/vim.1.gz
# update-alternatives --set editor /usr/local/bin/vim
... using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in manual mode.

Other relevant alternatives include the vi implementation for your system, which of course may not necessarily be Vim; some operating systems install the smaller and more vi-faithful nvi.

Tmux environment variables

The user configuration file for the tmux terminal multiplexer, .tmux.conf, supports defining and using environment variables in the configuration, with the same syntax as most shell script languages:

TERM=screen-256color
set-option -g default-terminal $TERM

This can be useful for any case in which it may be desirable to customise the shell environment when inside tmux, beyond setting variables like default-terminal. However, if you repeat yourself in places in your configuration file, it can also be handy to use them as named constants. An example could be establishing colour schemes:

TMUX_COLOUR_BORDER="colour237"
TMUX_COLOUR_ACTIVE="colour231"
TMUX_COLOUR_INACTIVE="colour16"

set-window-option -g window-status-activity-bg $TMUX_COLOUR_BORDER
set-window-option -g window-status-activity-fg $TMUX_COLOUR_ACTIVE
set-window-option -g window-status-current-format "#[fg=$TMUX_COLOUR_ACTIVE]#I:#W#F"
set-window-option -g window-status-format "#[fg=$TMUX_COLOUR_INACTIVE]#I:#W#F"

The explicit commands to work with environment variables in .tmux.conf are update-environment, set-environment, and show-environment, and are featured in the manual.

Packaging built software

The Debian package repository is enormous, and the Sid distribution is in most cases reasonably up-to-date, but it’s still sometimes desirable to build an application and install it into /usr/local when a packaged implementation either isn’t available or is too out of date, or if you’re involved in the development of a project and want to try out a fresh build that hasn’t been packaged yet.

The usual cycle of configuring, compiling, and installing many open-source applications for Unix-like systems applies here:

$ ./configure
$ make
# make install

The above is normally the approach taken to install code compiled on the machine, rather than through packages. One problem with this approach is that it doesn’t allow many of the advantages that a system running purely on packages does; dpkg -l will no longer give you a complete overview of all the system’s software, and to remove the software and its configuration files you may have to manually delete it rather than using apt-get purge.

Fortunately, there exists a tool called checkinstall to allow having the best of both worlds. After installing this tool via apt-get install checkinstall, you’re able to build a package for your locally built software according to the rules defined in its Makefile, and install that the same way as any other package.

Instead of typing make install, type checkinstall, and you will be prompted for details about the package, including its description, which is then built and installed. In this example, I’m compiling Vim from source, which works very well. I’ve also successfully installed tools like pam_ssh_agent_auth and tmux this way.

With this done, the package’s files are installed in /usr/local, and it appears in the list of installed packages along with my explanation of its contents:

$ ./configure
$ make
$ sudo -s
# checkinstall
...
*****************************************
**** Debian package creation selected ***
*****************************************

This package will be built according to these values:

0 -  Maintainer: [ Tom Ryder <tom@sanctum.geek.nz> ]
1 -  Summary: [ Custom build of latest Vim 7.3 ]
2 -  Name:    [ vim ]
3 -  Version: [ 2:7.4 ]
4 -  Release: [ 1 ]
5 -  License: [ GPL ]
6 -  Group:   [ checkinstall ]
7 -  Architecture: [ amd64 ]
8 -  Source location: [ vim ]
9 -  Alternate source location: [  ]
10 - Requires: [  ]
11 - Provides: [ vim ]
12 - Conflicts: [  ]
13 - Replaces: [  ]

Note that I’m assigning it a version number greater than the Debian repository’s vim package, which is a simple way to prevent it being replaced. You can also do this via the /etc/apt/preferences file to prevent replacement of all packages from the checkinstall group.

With this done, my custom build of Vim now shows in the package list, and its files are correctly installed in /usr/local:

# dpkg -l | grep vim
ii  vim  2:7.4-1 Custom build of latest Vim 7.3
# dpkg -S vim
vim: /usr/local/share/vim/vim73/bugreport.vim
vim: /usr/local/share/vim/vim73/plugin
vim: /usr/local/share/vim/vim73/ftplugin/postscr.vim
vim: /usr/local/share/man/it.UTF-8/man1/vim.1.gz
...

The .deb package built by checkinstall is also present in my build directory for me to keep for later, or for installation on another compatible server:

$ ls *.deb
vim_7.4-1_amd64.deb

It’s worth noting that checkinstall is not a Debian-specific tool; it works for other packaging systems like RPM and Slackware, too.

Faster Vim search/replace

Entering search patterns and replacement strings in Vim can sometimes be a pain, particularly if the search or replacement text is already available in a register or under the cursor. There are a few ways to make working with search and replace in Vim quicker and less cumbersome for some common operations, and thereby save a bit of error-prone typing.

Insert contents of a register

As in command mode or insert mode, you can insert the contents of any register by pressing Ctrl+R and then the register key. For example, to directly insert the contents of register a, you can type Ctrl+R and then a while typing a search pattern or replacement string.

Reference contents of a register

Similarly, if you want to use the contents of a register in a pattern or replacement but don’t want to directly insert it, you can instead reference the contents of register a with \=@a:

:s/search/\=@a/

Both of the above tips work for both the search and replacement patterns, and for special registers like " (default unnamed register) and / (last searched text) as well as the alphabetical ones.

Insert word under cursor

If you happen to be hovering over a word that you want to use as as a search or replacement string, as a special case of the above you can do so by typing Ctrl+R and then Ctrl+W for a normal word, or Ctrl+R and then Ctrl+A for a space-delimited word.

Empty search string

You can use the previous search as a search pattern or replacement string by including it from the special / register, in the same way as any other register above, by inserting it with Ctrl+R and then /, or representing it with \=@/. There’s a convenient shorthand for this however in just using an empty search string:

:s//replacement/

This will replace all occurences of the previous search string with replacement. It turns out to be a particularly convenient shorthand when searching for words by pressing * or #.

Typo correction in Bash

If you make a typo in a long command line in Bash, instead of typing it all out again, you can either recall it from your history, or use caret notation to repeat the command with an appropriate substitution. This looks like the following:

# sudo apache2ctl restrat
Action 'restrat' failed.
The Apache error log may have more information.
# ^strat^start
sudo apache2ctl restart

The string after the first caret is the text to match, and the one after the second string is the text with which it should be replaced. This provides a convenient method of not only quickly correcting typos, but to change small parts of the command line in general quickly:

$ touch filea.txt
$ ^filea^fileb
touch fileb.txt
$ ^fileb^filec
touch filec.txt

For the particular case of correcting small errors in long paths for cd calls, it’s helpful to use the cdspell option for Bash, which I discuss in my earlier article on smarter directory navigation.