2

It's not uncommon that I make a typo when entering commands in my bash interactive shell. I'd like to be able to correct the typo in my bash history so that the incorrect command does not pollute it and lead me to accidentally re-execute it later. In particular, I'd like to edit the last command.

There are quite a number of questions asking about how to prevent accidentally editing bash history. I want the opposite: I want to explicitly edit history.

Based on some of the explanations from the referenced questions, I tried doing:

$ echo foo

pressing Up, changing it to:

$ echo foobar

pressing Down, but that does nothing, and if I then press Enter, it will execute the modified command and leave both

echo foo
echo foobar

in my history.

I am aware that I can manually delete history entries with history -d, but I haven't devised a good way to use that conveniently. I don't want to make a shell function to unconditionally delete the last history entry because I still want to be able to use Up to load the last entry so that I can correct it. I could make the correction and then delete the second-to-last history entry, but that feels clumsy and it's particularly annoying for a long-running command since I'd either need to remember to perform extra steps later or would need to temporarily suspend it, do those steps, and resume.

What I want:

  • Ideally what I'd like to be able to do is to press Up, make a correction to my previous command, and press some special keybinding or add some magic token to the command-line to cause it to replace the history entry when executed.

  • Also acceptable would be to press some other key sequence to retrieve and edit a command from history (similar to Ctrl+R) that overwrites the history entry when executed.

  • A robust shell function that removes the second-to-last history entry would be tolerable but non-ideal.

I imagine that surely other people make typos too and are similarly annoyed when such commands pollute their history. What do other people do?

jamesdlin
  • 838
  • I already stated that I know how to remove a single history line, but by itself those mechanisms (history -d or editing .bash_history in an editor) are too inconvenient to use. I'm specifically asking for more convenient approaches, particularly in the context of correcting an existing line (which might not entail complete removal of the original). – jamesdlin Jul 17 '18 at 02:59
  • If bash doesn't support replacing lines, then you have to write this feature yourself. The good news: bash is opensource. – Ipor Sircer Jul 17 '18 at 03:05
  • @IporSircer I'm asking if bash supports replacing lines. Clearly it has some mechanism for doing so since there are a lot of questions asking how to prevent that from happening. Is it unreasonable to ask if there's a way to harness that power for good instead of evil? – jamesdlin Jul 17 '18 at 03:07
  • Read the fine manual (man bash); all features are documented. There are no hidden secrets. – Ipor Sircer Jul 17 '18 at 03:08
  • @IporSircer I think all of the people asking how to prevent accidental editing of history would beg to differ. And there is certainly the possibility that I have missed or misunderstood something in the manual. – jamesdlin Jul 17 '18 at 03:09

2 Answers2

5

Option #1 - Manually

I'd simply open the ~/.bash_history file in an editor such as vim and make whatever changes you need to that file and save.

$ vim ~/.bash_history

Before editing it make sure your current terminals history is committed to this file as well:

$ history -a

Keep in mind that your history file is located where ever this environment variable is pointing to:

$ echo $HISTFILE
/Users/user1/.bash_history

Option #2 - HSTR

There's a CLI tool called HSTR that you can use to manage your ~/.bash_history file in a more systematic way. The main website for HSTR was videos and good details on using it.

It also mentions this blurb:

HSTR can also manage your command history (for instance you can remove commands that are obsolete or contain a sensitive information) or bookmark your favorite commands.

Refer to the full docs for more on this: HSTR DOCUMENTATION.

References

slm
  • 369,824
  • If I find history -d too inconvenient to use, this isn't really any better, especially if I need to remember to do those steps after I execute the corrected command. – jamesdlin Jul 17 '18 at 03:04
  • Can you please add to your question what you actually want as a potential solution then? A shell function to run? Not sure how to address your question beyond showing you the bits and you can roll them into whatever solution works best for your style. – slm Jul 17 '18 at 03:10
  • @jamesdlin - your Q reminded my of hstr might be something worth a look for your use case. It sets up a keybinding that you can use to kill the last run command from history - https://github.com/dvorka/hstr/blob/master/DOCUMENTATION.md. – slm Jul 17 '18 at 03:16
  • 1
    I've adjusted my question. hstr looks interesting! I'll look into that. Thanks! – jamesdlin Jul 17 '18 at 03:17
0

HSTR seemed appealing but turned out to be a bit too heavyweight for my taste.

I instead wrote my own bash function:

# Removes the last command from the history that matches the given search
# string and re-executes it using the given replacement string.
#
# Usage:
#   historysub SEARCH [REPLACEMENT]
#   historysub -d SEARCH
#
#   REPLACEMENT may be omitted to remove SEARCH from the executed command.
#
#   -d removes the last command matching SEARCH from the history without
#   executing a new command.
historysub()
{
  local delete=0
  local usage=0
  local OPTIND=1
  while getopts ":hd" option; do
    case "${option}" in
      d) delete=1 ;;
      h) usage=1 ;;
      ?) usage=2 ;;
    esac
  done
  shift $((OPTIND - 1))

  local search="${1-}"
  local replacement="${2-}"

  usage_text=
  usage_text+="Usage: ${FUNCNAME[0]} SEARCH [REPLACEMENT]\n"
  usage_text+="       ${FUNCNAME[0]} -d SEARCH\n"

  if (( usage )); then
    echo -e "${usage_text}"
    return $(( usage - 1 ))
  fi

  if [[ -z "${search}" ]]; then
    echo -e "${usage_text}" >&2
    return 1
  fi

  # RE to parse the `history` output.
  local hist_re='^[ \t]*([0-9]+)[ \t]+(.+)$'

  local hist_full
  hist_full=$(HISTTIMEFORMAT="" history)

  # We don't want the search string to accidentally match against history
  # numbers, so split the `history` output so that we can search against just
  # the commands.
  local hist_nums hist_cmds
  hist_nums=$(sed -E "s/${hist_re}/\1/" <<< "${hist_full}")
  hist_cmds=$(sed -E "s/${hist_re}/\2/" <<< "${hist_full}")

  # Find the last matching history entry (excluding ourself).
  local matches last_match
  matches=$(grep -nF -- "${search}" <<< "${hist_cmds}")
  last_match=$(grep -vF -- "${FUNCNAME[0]}" <<< "${matches}" | tail -n 1)

  if [[ -z "${last_match}" ]]; then
    echo "${FUNCNAME[0]}: \"${search}\" not found." >&2
    return 1
  fi

  # RE to parse the `grep -n` output.
  local line_re='^([0-9]+):[ \t]*(.+)$'

  # Note that the line number and the history number might not be the same, so
  # we need to retrieve the original history number.
  local line_num hist_cmd hist_num
  line_num=$(sed -E "s/${line_re}/\1/" <<< "${last_match}")
  hist_cmd=$(sed -E "s/${line_re}/\2/" <<< "${last_match}")
  hist_num=$(tail -n +${line_num} <<< "${hist_nums}" | head -n 1)

  history -d "${hist_num}"
  if (( delete )); then
    echo "Removed: ${hist_cmd}"
    return 0
  fi

  local cmd="${hist_cmd/${search}/${replacement}}"
  echo "${cmd}"

  # Add the new command to the history.
  history -s -- "${cmd}"
  eval -- "${cmd}"
}

So now I can run historysub TYPO CORRECTION to re-execute a command with a correction. It's not as good as being able to interactively edit the old command, but I think it should be good enough for my needs.

jamesdlin
  • 838