Running `exec` with a Bash built-in

You defined a bash function. So you are already in a bash shell when invoking that function. So that function could then simply look like:

clock(){
  echo "do something"
  $@
}

That function can be invoked with bash builtins, special reserved words, commands, other defined functions:

An alias:

$ clock type ls
do something
ls is aliased to `ls --color=auto'

A bash builtin:

$ clock type type
do something
type is a shell builtin

Another function:

$ clock clock
do something
do something

An executable:

$ clock date
do something
Tue Apr 21 14:11:59 CEST 2015

The only way to launch a shell builtin or shell keyword is to launch a new shell because exec “replaces the shell with the given command”. You should replace your last line with:

IFS=' '; exec bash -c "$*"

This works with both builtins and reserved words; the principle is the same.


If the wrapper needs to insert code before the given command, an alias would work as they are expanded at a very early stage:

alias clock="do this; do that;"

Aliases are almost literally inserted in place of the aliased word, so the trailing ; is important – it makes clock time foo expand to do this; do that; time foo.

You can abuse this to create magic aliases which even bypass quoting.


For inserting code after a command, you could use the "DEBUG" hook.

shopt -s extdebug
trap "<code>" DEBUG

Specifically:

shopt -s extdebug
trap 'if [[ $BASH_COMMAND == "clock "* ]]; then
          eval "${BASH_COMMAND#clock }"; echo "Whee!"; false
      fi' DEBUG

The hook still runs before the command, but as it returns false it tells bash to cancel the execution (because the hook already ran it via eval).

As another example, you can use this to alias command please to sudo command:

trap 'case $BASH_COMMAND in *\ please) eval sudo ${BASH_COMMAND% *}; false; esac' DEBUG

The only solution I could come up with so far would be to perform a case analysis to distinguish whether the first argument is a command, built-in, or keyword, and fail in the last case:

#!/bin/bash

case $(type -t "$1") in
  file)
    # do stuff
    exec "$@"
    ;;
  builtin)
    # do stuff
    builtin "$@"
    ;;
  keyword)
    >&2 echo "error: cannot handle keywords: $1"
    exit 1
    ;;
  *)
    >&2 echo "error: unknown type: $1"
    exit 1
    ;;
esac

It does not handle time, however, so there might be a better (and more concise) solution.