DEV Community

Sushant Bajracharya
Sushant Bajracharya

Posted on • Edited on

Emulate bash "history" but for iex

Personally, I think the bash command, history, is a neat productivity tool. I use it all the time history | grep and I wanted the same thing in IEx as well.

To search through the history in IEx, you need to first enable it. Then either use up arrow keys or ctrl + r to search. That's cool but still not exactly what I need.

So, the first thing I had to do was to find the place where the history is stored.

# run it in your IEx
:filename.basedir(:user_cache, "erlang-history")

The history files are written by erlang's disk_log, so there will be weird characters when you open it in your editor. My initial thought was, if the history is written by disk_log:log/2 which uses erlang:term_to_binary/1, then I can read those history files with erlang:binary_to_term/1. But it turns out when writing history, disk_log appends some kind of weird binaries which you can find in disk_log.hrl eg <<12,33,44,55>>. So, I tried disk_log:chunk/2 to parse it.

# this will print the content of the history file with header
# in IEx, it will print charlists
# in erl, it will print strings

:disk_log.chunk(:'$#group_history', :start)

It did parse the history but it had weird headers and also didn't give me the whole content of all the history files.

{{:continuation, #PID<0.81.0>, {1, 52393}, []},
 [
   {:vsn, {0, 1, 0}},

I found a file called group_history.erl. It has two public api load/0 and 'add/2'. The load/0 api parsed and returned the whole history just as I wished.

:group_history.load()

And then I finally wrote the rest of the code to emulate bash history command

defmodule History do
  def search(term) do
    load_history()
    |> Stream.filter(&String.match?(&1, ~r/#{term}/))
    |> Enum.reverse()
    |> Stream.with_index(1)
    |> Enum.each(fn {value, index} ->
      IO.write("#{index}  ")
      IO.write(String.replace(value, term, "#{IO.ANSI.red()}#{term}#{IO.ANSI.default_color()}"))
    end)
  end

  def search do
    load_history()
    |> Enum.reverse()
    |> Stream.with_index(1)
    |> Enum.each(fn {value, index} ->
      IO.write("#{index}  #{value}")
    end)
  end

  defp load_history, do: :group_history.load() |> Stream.map(&List.to_string/1)
end

I have put this code in my .iex.exs file so that I can call it whenever I am in my IEx.

History.search("map")
History.search()

Hi there!!

Hi, I am looking for awesome opportunities to work with Elixir. If you are hiring or need a hand to finish your pet/side project then let's get connected. Here is my email sussyoung9[at]gmail[dot]com

Top comments (0)