Bash – Prevent a command from being executed after another command

bashcommand historyshell

I would like the shell to warn me for potential errors I'm about to make.

How can I prevent a command cmd2 from being executed after cmd1.

For example:

$ tar tf file.tar
$ rm !$

Here, I just forgot that I listed the file instead of extracting its content.

I'm looking for a generic solution, that makes a choice based on the name and return value of the previous command, and on the current command.

Best Answer

You're asking for a lot with a general answer, but it should be possible to do it relatively easily to some extent.

However, bash has at least two general mechanisms that can help solve that. You can set the variable PROMPT_COMMAND and this will be executed after anything you run in your prompt:

$ PROMPT_COMMAND='echo BOFH says hi!'
BOFH says hi!
$ man man
BOFH says hi!
$

We can use this to save the last entered command and then reread it, so we can store it in memory for further use (history expansion does not work in this case):

$ PROMPT_COMMAND='history -a; tail -n1 ~/.bash_history'
PROMPT_COMMAND='history -a; tail -n1 ~/.bash_history'
$ wc < test.sh | grep .
 5 11 63
wc < test.sh | grep .

Now comes the logic part, which is completely up to you. Let's use just very simplistic logic to prevent the example in the question. The thing to remember is, that bash functions still work in there, so you don't have to cram everything on a single line, in a single variable.

$ # define the logic check
$ checkSanity() { grep -q "tar  *[tf][[:alpha:]]*[tf] " <<< "$@" && echo "be careful, don't delete this"; }
checkSanity() { grep -q "tar  *[tf][[:alpha:]]*[tf] " <<< "$@" && echo "be careful, don't delete this"; }
$ # feed it the last command
$ PROMPT_COMMAND='history -a; last=$(tail -n1 ~/.bash_history); checkSanity "$last"'
$ # simple test
$ tar tf test.sh 2>/dev/null 
be careful, don't delete this
$ tar xf test.sh 2>/dev/null 
$

Of course, this approach is useful to prevent PEBKAC (especially if you add sleep 3 at the end), but it can't break the next command on its own.

If that's really what you want, trap the DEBUG signal (eg. trap 'echo "I am not deterministic, haha"' DEBUG), since it runs beforehand. Be careful with combining these two approaches, since the output/action will be doubled:

$ df
/.../
$ trap 'tail -n1 ~/.bash_history' DEBUG
df
df
trap 'tail -n1 ~/.bash_history' DEBUG
trap 'tail -n1 ~/.bash_history' DEBUG

To make the trap break a command, you'll have to enable extdebug (shopt -s extdebug). You also don't need to save and reread history constantly, but can inspect $BASH_COMMAND to get the command about to be run. Then you just need to make sure the logic checker return 1 when it detects something bad and 0 otherwise.

          extdebug
                  If set, behavior intended for use by debuggers is enabled:
    /.../
                  2.     If  the command run by the DEBUG trap returns a non-zero value, the next
                         command is skipped and not executed.


$ checkSanity() { if grep -q "tar  *[tf][[:alpha:]]*[tf] " <<< "$1"; then  echo "be careful, don't delete this"; return 1; fi; }
$ trap 'checkSanity "$BASH_COMMAND"' DEBUG
$ # simple test
$ tar tf test.sh 2>/dev/null 
be careful, don't delete this
$ tar xf test.sh 2>/dev/null 
$
Related Question