17

To prevent logging "dangerous" commands in bash history, I have added following line to my .bashrc file:

HISTIGNORE='rm *:mv *:cp *:cat*>*:pv*>*'

this works well, but it has a side effect: I cannot see the complete history of commands executed on a machine. Let's say I have several machines for experiments, and I want to be able to see all commands executed. I would use the bash internal history to display executed commands, and perhaps grep for today's date:

history | grep Sep-28

What I would like do have is to log "dangerous" commands as well, but put a # at the beginning of the line, so that if I happen to execute the command from history by mistake, no damage would be done.

I have no idea if this is possible.

Update and clarification:

The main reason why this is a problem for me is that I am usually connected to my machine from several terminals, and any command executed on one terminal is immediately read into history of other terminals. This is achieved by

PROMPT_COMMAND="history -a; history -c; history -r"

Let's imagine I have two terminals open. In one I have some cat /dev/foo > file.out process running. In the second, I check the progress with ls -lAhF. I keep repeating ls by pressing Up and ENTER (that is, last command from history). As soon as the first command finishes, the last command from history is no longer ls, but cat /dev/foo > file.out. If I am not careful, I will start cat again and overwrite file.out.

What I would like to achieve is that the cat command would be preceded with a #, so that it would not be executed. I would, however, still see it in history and can reuse it (if it is a long command) by un-commenting it.

1
  • 1
    Instead of repeating your command manually, you can use watch ls -lAhF or while sleep 1; do ls -lAhf; done; instead of watching the file size, you can use pv /dev/foo > file.out (ivarch.com/programs/pv.shtml).
    – deltab
    Mar 12, 2014 at 15:26

3 Answers 3

12

I'm not going to exactly answer your question, but maybe give you an alternate solution to your problem.

If I understand correctly you're concerned about mistakes you could make by typing, e.g., !rm if it happened that the previous rm command in history removes something you'd like to keep.

In this case, a nice bash option is histverify. If you shopt -s histverify, and if you recall a command with the bang !, it will not execute immediately, but be loaded in the readline so that you can either decide to execute it or not, and this also gives you the possibility to edit it.

Try it:

  • Without histverify:

    $ touch some_foo
    $ rm some_foo
    $ touch some_foo
    $ !rm
    rm some_foo
    $ # oooops in fact I'd've like to keep it this time
    
  • With histverify:

    $ shopt -s histverify
    $ touch some_foo
    $ rm some_foo
    $ touch some_foo
    $ !rm
    $ rm some_foo <cursor here>
    

    In this case you'll have the cursor at the end of the line, ready to launch it again -- or not -- or to edit it.

If you like this option, put it in your .bashrc:

shopt -s histverify
1
  • A comment from the vantage point of 2023. I feel that ! history searching is a skill rarely utilized today by shell newcommers. C-r searching is much more agile than ! searching. Also, fzf Ctrl+R binding makes it so much more powerful. But I am definitley adding shopt -s histverify to my .bashrc now. Thanks.
    – Lockszmith
    Jun 7, 2023 at 14:30
10
+100

You could do something like:

fixhist() {
   local cmd histnum
   cmd=$(HISTTIMEFORMAT=/ history 1)
   histnum=$((${cmd%%[*/]*}))
   cmd=${cmd#*/} # remove the histnum
   case $cmd in
     (rm\ *|mv\ *|...)
       history -d "$histnum" # delete
       history -s "#$cmd"    # add back with a #
   esac
}
PROMPT_COMMAND=fixhist

The idea being that before each prompt, we check the last history entry (history 1) and if it's one of the dangerous ones, we delete it (history -d) and add it back with a # with history -s.

(obviously, you need to remove your HISTIGNORE setting).

An unwanted side effect of that though is that it alters the history time of those rm, mv... commands.

To fix that, an alternative could be:

fixhist() {
   local cmd time histnum
   cmd=$(HISTTIMEFORMAT='<%s>' history 1)
   histnum=$((${cmd%%[<*]*}))
   time=${cmd%%>*}
   time=${time#*<}
   cmd=${cmd#*>}
   case $cmd in
     (rm\ *|mv\ *|...)
       history -d "$histnum" # delete
       HISTFILE=/dev/stdin history -r <<EOF
#$time
#$cmd
EOF
   esac
}
PROMPT_COMMAND=fixhist

This time, we record the time of the last history, and to add back the history line, we use history -r from a temporary file (the here document) that includes the timestamp.

You'd want the fixhist to be performed before your history -a; history -c; history -r. Unfortunately, the current version of bash has a bug in that history -a doesn't save that extra line that we've added. A work around is to write it instead:

fixhist() {
   local cmd time histnum
   cmd=$(HISTTIMEFORMAT='<%s>' history 1)
   histnum=$((${cmd%%[<*]*}))
   time=${cmd%%>*}
   time=${time#*<}
   cmd=${cmd#*>}
   case $cmd in
     (rm\ *|mv\ *|...)
       history -d "$histnum" # delete
       history -a
       [ -f "$HISTFILE" ] && printf '#%s\n' "$time" "$cmd" >> "$HISTFILE";;
     (*)
       history -a
   esac
   history -c
   history -r
}
PROMPT_COMMAND=fixhist

That is to append the commented command to the HISTFILE ourselves instead of letting history -a do it.

11
  • can you please explain what cmd=${cmd#*[0-9] } does? And where exactly should I put fixhist in my PROMPT_COMMAND? At the moment, it already contains PROMPT_COMMAND="history -a; history -c; history -r" Jan 19, 2014 at 21:47
  • what is the role of HISTTIMEFORMAT=/? I have already set export HISTTIMEFORMAT="%b-%d %H:%M ". By the way, when I add your code into my $HOME/.bashrc, nothing happens. Jan 19, 2014 at 22:27
  • HISTTIMEFORMAT=/ is only for that one invocation of history to get an output like 123 /rm x where it's easier to extract the cmd. There was an issue with some versions of bash where $HISTCMD was bogus in that context, try the new version. Jan 19, 2014 at 22:38
  • still does not work. I am wondering, do the # in the code not act as comments in .bashrc? Jan 20, 2014 at 11:59
  • 1
    @MartinVegter, yes, for modified history entries, bash inserts a * after the history number which was not expected. Should be fixed now. Apr 18, 2014 at 13:41
1

The accepted answer from Stéphane is absolutley amazing. Below is my adoption of it which does the pattern matching a bit different.

Using if statements instead of case allows me to use regex matching ([[ "$cmd" =~ <REGEX> ]]), the code below works by the following logic:

  • Do not add to history in the following case:
    • Command line starts or ends with 2 or more consecutive spaces
      Example:
      • <spc><spc>history
  • Add command commented in the following case:
    • rm or mv commands with cli switches
    • Command line starts with :; or a singel space or ends with ;:
      Examples:
    • rm -R /tmp/somedir

    • echo "doing something dangerous I don't want to trip by accident";:

      This also has cavieats, which I am ok with, for example:

    • echo ";rm -something"

  • Otherwise add to history
    Examples:
    • rm somefile
    • echo ";rm -something"

The Code

_history_hook() {
# The code below is from https://unix.stackexchange.com/a/110056/138012
# history builtiun options used:  https://www.gnu.org/software/bash/manual/html_node/Bash-History-Builtins.html
# history -a : Append new (mem) lines to history (file)
# history -d : Delete range (mem)
# history -c : Clear history list (mem) ++
# history -r : Read (file) append to (mem) ++
# ++ needed to circumvent a bug issue.
   local cmd time histnum
   cmd=$(HISTTIMEFORMAT='<%s>' history 1)
   histnum=$((${cmd%%[<*]*}))
   time="${cmd%%>*}"
   time=${time#*<}
   cmd="${cmd#*>}"
   # different behaviors for different patterns
   if [[ "$cmd" =~ ^\ \ +.*|\ +\ $ ]]; then
   # delete only (two spaces - leading or trailing)
       history -d "$histnum" # delete
   elif [[ "$cmd" =~ ((^|;[[:space:]]*)(rm|mv)\ +-)|^\ |^:\;|\;:$ ]]; then
   # rewrite history as a 'safe version'
       history -d "$histnum" # delete
       history -a
       [ -f "$HISTFILE" ] && printf '# %s\n' "$time" "$cmd" >> "$HISTFILE"
   else
   # append 'as usual'
       history -a
   fi
   # rewriting history file
   history -c
   history -r
}

export HISTCONTROL=erasedups # This does not seem to work right now - need to fix this
export PROMPT_COMMAND="$PROMPT_COMMAND; _history_hook"

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .