• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar

Justin Joyce

Practical tips and tutorials about software development.

  • Standing Invitation
  • Featured Posts
  • Latest
  • About

Save your shell history to log files

Posted Mar 28, 2023 — Updated Oct 26, 2023

Every command I enter on my computer is written to a log file1. These logs have come in handy countless times; I tell anyone who will listen to save their logs too. My inspiration for doing this came from this atomic object post many years ago, and I am so glad I followed their advice. Can’t remember that random command you entered a year ago? That’s ok, it’s in your saved log directory; just go in there and grep. Better yet, ripgrep.

Here’s an example. I know I ran some complicated decrypt-related workflow last spring, but I can’t remember what exactly I did:

logs of every time "decrypt" was entered in my command line
All the times I’ve entered “decrypt” in my command line

There we go! I see a cluster of “decrypt” related commands the week of June 13 – 17 last year, I bet my answer is in one of those log files.

Configure zsh to save commands to log files

The article I linked in the intro above has instructions for how to set this up for a bash shell. If you’re using bash, feel free to jump over there and follow the instructions. If you’re on a Mac, however, you’re probably running zsh for your shell; it’s been the default for a few years now. Fear not, I have updated instructions.

Update your .zshrc (or your configuration file of choice)

You can modify your .zshrc file itself, but I like to keep mine clean since that’s the default zsh file and the root of all shell config. For the sake of making this example easy though, let’s just pretend we’re adding this straight into .zshrc. Here’s the full command (with explanation below):

# This is stolen almost exactly from the linked post
preexec() {if [ "$(id -u)" -ne 0 ]; then echo "$(date "+%Y-%m-%d.%H:%M:%S") $(pwd) $ $3" >> ~/.logs/zsh-history-$(date "+%Y-%m-%d").log; fi}

There’s a lot going on in that preexec command, so let’s break it down.

First, what’s preexec?

I’ll let the zsh docs explain:

preexec
 Executed just after a command has been read and is about to be executed.

Preexec2 is a built in zsh hook which allows you to do something just before your command is executed—the perfect time to write a log.

Next is this piece:

if ["$(id -u)" -ne 0]; then

That’s saying if you're not in sudo, then evaluate the rest of this. This isn’t strictly necessary, but it feels better.

Next we have this:

echo "$(date "+%Y-%m-%d.%H:%M:%S") $(pwd) $ $3"

This line is doing a few things:

  • Printing a datetime formatted like this: 2023-03-27.23:25:08
  • Printing the pwd where the command was entered
  • Printing a $ for visual formatting (no effect, I just like it)
  • $3 is the third argument given to preexec, which is “the full text that is being executed”

As an example, this is what the most recent log line in my personal log history looks like:

2023-03-27.23:25:08 /Users/justin $ vim .zprompt

At 11:25PM, in my home directory, I entered vim .zprompt while writing this post.

Final Piece

The last bit of the big command above is:

>> ~/.logs/zsh-history-$(date "+%Y-%m-%d").log; fi}

The >> takes everything from its left and appends it to the file on the right, which is currently ~/.logs/zsh-history-2023-03-27.log. That final semicolon and fi} is just the end of the if statement that started this whole thing.

The full command, one more time

preexec() {if [ "$(id -u)" -ne 0 ]; then echo "$(date "+%Y-%m-%d.%H:%M:%S") $(pwd) $ $3" >> ~/.logs/zsh-history-$(date "+%Y-%m-%d").log; fi}

The plain English recap of what this does:

  • If the user is not sudo
  • Prints a line with the format yyyy-mm-dd.hh:mm:ss /current/directory $ {command}
  • Appends that line to a log file at the path ~/.logs/zsh-history-yyyy-mm-dd.log
  • Makes my life a lot easier

That’s it. I hope you take a minute and configure this also; you won’t regret it.

Why not use the builtin zsh history?

The builtin history is a good option! It requires some small setup to get going, but zsh does have builtin support for a history file. The main reason I like my custom setup is to have the log files split out by day. Sometimes when looking back through my history I have to re-contextualize the command I ran, which I find easier with smaller, day-specific log files. Granted, that wouldn’t be much more difficult via the builtin history, it’s more a matter of personal preference.


Helpful Links

  • zsh documentation
  • Atomic Object post from 2016
  • Better Zsh History
  • Helpful formatting commands for builtin zsh history
  • A very detailed walkthrough of Bash history

Notes

  1. If you’re worried about this taking disk space, you don’t need to be. In three years on this computer my .logs/ directory has only taken up 7M of space. ↩︎
  2. Why not use precmd? Precmd has no arguments, and would have to rely on history, which means it can only write the command before the one you just entered. If you close a terminal window, or a command crashes it, you won’t have a record of the most recent command. ↩︎

Filed Under: Command Line, Featured, Productivity, Tips

Primary Sidebar

Recent Posts

  • Every Built-In Vim Color Scheme (with screenshots)
  • Reverse a string in Python
  • Meeting Cost Calculator
  • Vim find and replace
  • What makes an effective development team

Categories

  • Arrays (5)
  • Command Line (9)
  • Dates (3)
  • Featured (7)
  • Git (7)
  • Golang (5)
  • Javascript (8)
  • Productivity (8)
  • Projects (4)
  • Python (15)
  • Regex (2)
  • Ruby (3)
  • Shell (2)
  • Thoughts (2)
  • Tips (11)
  • Tools (3)
  • Tutorials (1)
  • Vim (4)

Archives

  • July 2024 (1)
  • February 2024 (1)
  • January 2024 (1)
  • December 2023 (1)
  • November 2023 (1)
  • October 2023 (4)
  • September 2023 (1)
  • August 2023 (2)
  • July 2023 (5)
  • June 2023 (3)
  • May 2023 (6)
  • April 2023 (5)
  • March 2023 (5)
  • February 2023 (10)
  • January 2023 (6)
  • December 2022 (7)

Copyright © 2025 · Contact me at justin [at] {this domain}

  • Privacy Policy