Running `exec` with a Bash built-in

8

1

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

Posted 2015-04-21T11:17:20.663

Reputation: 1 291

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 – 2015-04-21T12:15:30.670

Answers

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

chaos

Posted 2015-04-21T11:17:20.663

Reputation: 3 704

In this case, is there any difference between running $@ and exec $@, if I know I am running an actual command? – anol – 2015-04-21T12:20:44.707

3When 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 – 2015-04-21T12:25:11.823

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 – 2015-04-21T12:30:19.653

4Yes, $@ 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 – 2015-04-21T12:37:40.587

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.

Anthony Geoghegan

Posted 2015-04-21T11:17:20.663

Reputation: 3 095

For some reason all the other answers weren't showing up when I refreshed this page. Damn! I want my lunch-time back. – Anthony Geoghegan – 2015-04-21T12:42:10.770

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

user1686

Posted 2015-04-21T11:17:20.663

Reputation: 283 655

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.

anol

Posted 2015-04-21T11:17:20.663

Reputation: 1 291