9

I defined a shell function (let's call it clock), which I want to use as a wrapper to another command, similar to the time function, e.g. clock ls -R.

My shell function performs some tasks and then ends with exec "$@".

I'd like this function to work even with shell built-ins, e. g. clock time ls -R should output the result of the time built-in, and not the /usr/bin/time executable. But exec always ends up running the command instead.

How can I make my Bash function work as a wrapper that also accepts shell built-ins as arguments?

Edit: I just learned that time is not a Bash built-in, but a special reserved word related to pipelines. I'm still interested in a solution for built-ins even if it does not work with time, but a more general solution would be even better.

anol
  • 1,870

4 Answers4

9

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
chaos
  • 4,304
4

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.

3

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
grawity
  • 501,077
1

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.

grawity
  • 501,077
anol
  • 1,870