Whenever you use your shell (Bash, Zsh, Fish, Powershell, Nushell, etc.), each command you run is saved in a history file.

It’s handy to re-run a similar command in the future thanks to Ctrl-R, but you can also use to as a source of information to determine which part of your workflow you could improve.

1. Anatomy of a command

A command usually looks like this:

command argument_0 argument_1 <argument_n+1>

Looking at the history of commands, I want to see which command I run the most often, but also see which arguments I use them with the most.

2. Example

I wrote a small program that generates an html file that shows how many times a command has been run, with a collapsible list of arguments it was used with:

It basically looks like this (click on each line to expand that section):

166 - podman
  • build --format docker --arch amd64 .

  • build --format docker --arch amd64 .

  • container list

  • inspect docker.io/caddy/caddy:latest

  • (etc.)

154 - cd
  • emtpy

  • empty

  • ~/dev

  • ~/dev

  • ~/dev

  • ~/dev

  • ~/dev

  • -

  • (etc.)

48 - ls
  • emtpy

  • empty

  • empty

  • -l

  • (etc.)

47 - jq
  • '' ./v.json

  • '. | @csv' ./values.json

  • --argjson i '2' '.perfCampaigns[$i]' ./v.json

  • (etc.)

In my case, I saw that I often use cd with very similar arguments, and with a limited set of directories.

I could probably create an alias for cd in conjunction with fzf to go to where I often go, without relying on tab auto-completion and speed things up.

Looking at my podman section, I can also see that I should look into some aliases as well.

3. How to generate the report and limitations

This analysis only helps for commands that start with the same keyword, but it does not help with commands in the middle of a pipe.

If you want to try this, save the following code block as History.java (java 17+).

Then, run: javac History.java && java History .bash_history > output.html

Finally, open output.html in your browser.

Feel free to replace .bash_history by the history file of your shell (only tested with Bash, Fish and Powershell history file.)

import java.util.*;
import java.nio.file.*;
import java.io.IOException;

record Command(String command, List<String> args) {}

class History {
  public static void main(String[] args) throws IOException {
    final var filename = args[0];
    final Path path = Paths.get(filename);

    final List<String> lines = Files.readAllLines(path);
    final Map<String, List<String>> buckets = new HashMap<>();
    lines.forEach(
        line -> {
          final var parts = Arrays.asList(line.split(" "));
          final var value = buckets.getOrDefault(parts.get(0), new ArrayList<>());
          value.add(String.join(" ", parts.subList(1, parts.size())));
          buckets.put(parts.get(0), value);
        });
    final List<Command> commands = new ArrayList<>();
    buckets
        .forEach((key, value) -> commands.add(new Command(key, value)));
    commands.sort(Comparator.comparing(c -> c.args().size()));
    Collections.reverse(commands);

    final List<String> htmlLines = generateHtml(commands);
    htmlLines.forEach(System.out::println);
  }

  private static List<String> generateHtml(final List<Command> commands) {
    final List<String> htmlLines = new ArrayList<>();
    htmlLines.add("""
<!DOCTYPE html>
<html>
  <head>
    <style>
      details {
        border: 1px solid #aaa;
        border-radius: 4px;
        padding: .5em;
        margin-block: .5em;
        background-color: lavender;
        overflow-x: auto;
      }

      summary {
        font-weight: bold;
        margin: -0.5em -0.5em 0;
        padding: 0.5em;
        background-color: white;
      }

      details[open] {
        padding: 0.5em;
      }

      details[open] summary {
        border-bottom: 1px solid #aaa;
        margin-bottom: 0.5em;
      }
      pre {
          height: 2em;
      }
      body {
        max-width: 130ch;
        margin-inline: auto;
      }
    </style>
  </head>
  <body>
""");
    commands.forEach(
        c -> {
          htmlLines.add("<details>");
          htmlLines.add("<summary>" + c.args().size() + " - " + c.command() + "</summary>");
          htmlLines.add("<ul>");
          Collections.sort(c.args());
          c.args()
              .forEach(
                  s -> {
                    htmlLines.add("<li><pre>");
                    htmlLines.add(s.isEmpty() ? "<em>empty</em>" : s);
                    htmlLines.add("</pre></li>");
                  });
          htmlLines.add("</ul>");
          htmlLines.add("</details>");
        });
    htmlLines.add("  </body>");
    htmlLines.add("</html>");
    return htmlLines;
  }
}