8

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.

  • You need to invoke a shell explicitly, using exec bash -c \' "$@" \'. Unless your command in the first parameter is a recognised as a shell script, then it will be interpreted as a binary to be run directly. Alternatively, and more simply, just miss out the exec and call "@" from your original shell. – AFH Apr 21 '15 at 12:15
8

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
  • In this case, is there any difference between running $@ and exec $@, if I know I am running an actual command? – anol Apr 21 '15 at 12:20
  • 3
    When you use exec, the command replaces the shell. Hence there are no builtins, aliases, special reserved words, defined words anymore, because the executable is executed via system call execve(). That syscall expects an executable file. – chaos Apr 21 '15 at 12:25
  • But from the point of view of an external observer, is it still possible to distinguish them, e.g. with exec $0 there is a single process, while $@ still has two. – anol Apr 21 '15 at 12:30
  • 4
    Yes, $@ has the running shell as parent and the command as child process. But when you want to use builtins, aliases and so on, you have to preserve the shell. Or start a new one. – chaos Apr 21 '15 at 12:37
3

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”. I thought that this was an interesting problem but shouldn’t be too hard to solve. However, getting the quoting right took over almost an hour of trial and error (Bash is great but I’m not a fan of the quoting).

You should replace your last line with:

exec bash -c ""$@""

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

  • For some reason all the other answers weren't showing up when I refreshed this page. Damn! I want my lunch-time back. – Anthony G - justice for Monica Apr 21 '15 at 12:42
2

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
0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.