Sinclair Target

Configuring Bash to Make Your Command History Useful

Oct 12, 2024

Bash has been around for a long time. Newer shells (like Fish) come with default settings that make sense in 2024, but Bash is stuck with defaults that were first decided on decades ago.

Julia Evans wrote about this recently, pointing out in particular how the command history in Fish works much better by default than it does in Bash.

I’ve never been happy with how the command history works in Bash. I often find myself searching backward for commands I know I ran recently only to come up empty handed. Or sometimes I run a command in one window and wonder why I can’t recall it from my command history in another window.

I’ve been able to improve things by changing some of Bash’s default settings and learning how the command history feature works. I had to spend some time reading through the Bash man page to figure it all out, so I’m sympathetic to anyone who’d rather use a shell that just works. For those wanting to stick with Bash: You can make it better, but not without some patience for Bash’s quirks.

The In-Memory History

The two most important settings governing how the command history behaves are the HISTSIZE and HISTFILESIZE shell variables. There is some debate about how these two settings interact and whether it ever makes sense to set them to different values. To understand what these two settings do, it’s helpful to know how Bash saves the commands you run.

Bash saves every command you execute to a command history that lives in memory. If you imagine a simple world in which you only ever have one instance of Bash active (say, you only ever use a single window or tab in your terminal emulator), and also that you use that instance of Bash forever (by never shutting down your computer), then this in-memory command history would serve all your needs. Every command you search for could be found in that in-memory buffer as long as you actually ran it.

Memory isn’t infinite, of course, so it makes sense to have an upper limit on how many commands are stored in this buffer. That’s what the HISTSIZE setting controls. Once the in-memory buffer contains HISTSIZE commands, each new command saved to the history pushes out the oldest command. In this simple world of a single, immortal instance of Bash, the HISTSIZE setting is all we need.

By default, HISTSIZE is set to only 500, which likely isn’t sufficient to retrieve a command you ran a few days ago or maybe even just yesterday. Modern computers have enough memory to spare that you can bump this up to something like 100,000 without issue. Assuming you run 250 or so commands a day, that should be enough to hold more than a year’s worth of shell commands.

To make Bash’s command history more useful, bumping up HISTSIZE to 100,000 or more is the first thing you should do.

The History File

In reality, people stop and start instances of Bash all the time and might have several running at once. Let’s ignore the possibility that multiple instances of Bash could be running at once for now and just consider what happens when you exit and restart Bash.

If Bash only stored commands in memory, this would lead to a situation where each instance of Bash would have its own entirely separate command history. Each time you opened your terminal, you’d start with a completely clean slate. And every time you exited your terminal, your command history for that session would be lost.

Bash prevents this by storing your command history in an additional location: on disk. There is a Bash setting called HISTFILE that governs where on disk your command history is stored. The default location, if you haven’t defined this variable, is ~/.bash_history.

When you exit an instance of Bash, it saves all the commands from the in-memory command history to this file. Next time you start an instance of Bash, the in-memory command history is initialized with the contents of the file. This preserves your command history between Bash sessions.

Disk space, like memory, isn’t infinite, though certainly we have much more of it available. So the HISTFILESIZE setting is the on-disk analog of the HISTSIZE setting—it defines an upper limit on the number of commands to store on disk. The Bash man page says that the history file is truncated to contain no more than HISTFILESIZE lines when 1) the file is read into the in-memory command history of a new Bash instance and 2) the file is written to after a Bash instance exits. So the file should never contain more lines than the specified upper limit.

Since we have so much more disk space available than memory, it might seem natural to set HISTFILESIZE to a higher number than HISTSIZE. You have to remember though that when you search backward through your command history with Bash, you are always searching backward through the in-memory command history of the current Bash process. The in-memory command history never contains more than HISTSIZE commands, even if those commands were read from the history file when the Bash process started. So setting HISTFILESIZE alone to a really big number does not mean that you now have a long command history. You would have to ensure that HISTSIZE is set to a large number as well, otherwise all those extra commands in your history file just won’t fit in the in-memory buffer.

I’m sure this confuses many people. I’ve had this misconfigured for years.

There are never more than HISTSIZE commands available in the Bash command history. If you set HISTSIZE to 1000 and HISTFILESIZE to 2000, you will have up to 1000 commands in your command history, not 2000.

So why is there a separate HISTFILESIZE setting at all? Some people might want to set HISTFILESIZE to a lower number than HISTSIZE if they are concerned about disk space. Alternatively, you might want HISTFILESIZE to be larger than HISTSIZE if you plan to search the history file using an external tool. I think there’s at least one good use case for this that helps you manage command histories from multiple active shell sessions. Let’s talk about that now.

A Global Command History

If you’re running more than one shell session at a time, even if you’ve set HISTSIZE and HISTFILESIZE to large numbers, you can still end up in a situation where you know you ran a command recently but aren’t able to find it in your command history. This will happen whenever you run a command in one shell session then try to search for it in the other.

The problem is that, for a command executed in session A to appear in the command history for session B, session A first has to write the command from its in-memory history to the history file and then session B has to read it. Usually these writes and reads only happen when sessions exit and start, so two active Bash processes will never merge their command histories, at least not while they are running.

There are ways to force Bash to write and read the command history file after every single command. This ensures the in-memory command history of every session and the on-disk command history are always in sync. It’s not obvious to me though that this is the best solution. Sometimes you have a shell session open for some particular category of activity and actually it’s useful for the commands you run in that session not to show up in the command history of your other sessions. If you are constantly writing and reading the command history file, you only have a single, global command history.

Here’s the setup that I think makes more sense. Treat the in-memory command history of each Bash session, governed by HISTSIZE, as your “local” command history. Then set HISTFILESIZE to a number many multiples of HISTSIZE. The history file will be your “global” history, which you search when you are looking for a command that you ran a long time ago or in another session.

You should also set the histappend shell option, which ensures that Bash always appends to the history file and never overwrites it in its entirety. You can do this by adding shopt -s histappend to your .bashrc.

Because HISTSIZE is smaller than HISTFILESIZE, you can’t use Bash to search backward through your global command history. Instead, you only use Bash to search through your local command history. When you want to search through your global command history, you use an external tool (like fzf) to search the history file directly. I have a nice shell alias set up to do that for me.

The nice thing about this setup is that you get two distinct search modes handled by two distinct commands. When you want to find a command you know you ran in your current session, you can do that. If you want to expand the search to your global command history, you can choose to do that too.

There’s still one piece missing here though—we have to ensure that all our commands from our local command histories are inserted into the global command history at some point. Remember that, by default, this won’t happen on its own until those Bash sessions exit.

One way to do this is to run history -a. The history command is a Bash builtin that helps you manage the in-memory command history. Running history -a appends the current in-memory history to the history file. If you start looking for something in your global command history and realize it’s not in there because the command is still in the local history of another shell session, you can go to that session, run history -a, then resume your global search.

I think this approach involves the least magic. An alternative would be to take the approach I alluded to above of forcing Bash to write and read the command history after each command it executes—except I think you should only configure it to write and not to read. That ensures that you global command history is always up-to-date without polluting your local command history with commands that you don’t want to see. You can do this by setting PROMPT_COMMAND=history -a; whatever you have set as PROMPT_COMMAND will be run each time Bash displays its prompt to you.

Phew! After all that, you get a command history that sort of works. Just not quite as well, of course, as things work out of the box in Fish. I’ve been keen to stick with Bash because of its ubiquity, but truthfully the more I have to configure it to get the experience I want the more I think I should just make the jump to something new.