Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

In python we can decorate functions with code that is automatically applied and executed against functions.

Is there any similar feature in bash?

In the script I'm currently working on, I have some boilerplate that test the required arguments and exit if they don't exist - and display some messages if the the debugging flag is specified.

Unfortunately I have to reinsert this code into every function and if I want to change it, I'll have to modify every function.

Is there a way to remove this code from each function and have it applied to all functions, similar to decorators in python?

share|improve this question

3 Answers 3

That would be a lot easier with zsh that has anonymous functions and a special associative array with function codes. With bash however you could do something like:

decorate() {
  eval "
    _inner_$(typeset -f "$1")
    $1"'() {
      echo >&2 "Calling function '"$1"' with $# arguments"
      _inner_'"$1"' "$@"
      local ret=$?
      echo >&2 "Function '"$1"' returned with exit status $ret"
      return "$ret"
    }'
}

f() {
  echo test
  return 12
}
decorate f
f a b

Which would output:

Calling function f with 2 arguments
test
Function f returned with exit status 12

You can't call decorate twice to decorate your function twice though.

With zsh:

decorate()
  functions[$1]='
    echo >&2 "Calling function '$1' with $# arguments"
    () { '$functions[$1]'; } "$@"
    local ret=$?
    echo >&2 "function '$1' returned with status $ret"
    return $ret'
share|improve this answer
    
Stephane - is typeset necessary? Would it not declare it otherwise? –  mikeserv Apr 23 at 13:12
    
@mikeserv, eval "_inner_$(typeset -f x)" creates _inner_x as an exact copy of the original x (same as functions[_inner_x]=$functions[x] in zsh). –  Stéphane Chazelas Apr 23 at 13:21
    
I get that - but why do you need two at all? –  mikeserv Apr 23 at 13:27
    
You need a different context otherwise you wouldn't be able to catch the inner's return. –  Stéphane Chazelas Apr 23 at 13:36
1  
I don't follow you there. My answer is an attempt as a close map of what I understand python decorators to be –  Stéphane Chazelas Apr 23 at 14:25

I've already discussed the hows and whys of the way the below methods work on several occasions before so I won't do it again. Personally, my own favorites on the topic are here and here.

If you're not interested in reading that but still curious just understand that the here-docs attached to the function's input are evaluated for shell expansion before the function runs, and that they are generated anew in the state they were when the function was defined every time the function is called.

DECLARE

You just need a function that declares other functions.

_fn_init() { . /dev/fd/4 ; } 4<<INIT
    ${1}() { $(shift ; printf %s\\n "$@")
     } 4<<-REQ 5<<-\\RESET
            : \${_if_unset?shell will ERR and print this to stderr}
            : \${common_param="REQ/RESET added to all funcs"}
        REQ
            _fn_init $(printf "'%s' " "$@")
        RESET
INIT

RUN IT

Here I call upon _fn_init to declare me a function called fn.

set -vx
_fn_init fn \
    'echo "this would be command 1"' \
    'echo "$common_param"'

#OUTPUT#
+ _fn_init fn 'echo "this would be command 1"' 'echo "$common_param"'
shift ; printf %s\\n "$@"
++ shift
++ printf '%s\n' 'echo "this would be command 1"' 'echo "$common_param"'
printf "'%s' " "$@"
++ printf ''\''%s'\'' ' fn 'echo "this would be command 1"' 'echo "$common_param"'
#ALL OF THE ABOVE OCCURS BEFORE _fn_init RUNS#
#FIRST AND ONLY COMMAND ACTUALLY IN FUNCTION BODY BELOW#
+ . /dev/fd/4

    #fn AFTER _fn_init .dot SOURCES IT#
    fn() { echo "this would be command 1"
        echo "$common_param"
    } 4<<-REQ 5<<-\RESET
            : ${_if_unset?shell will ERR and print this to stderr}
            : ${common_param="REQ/RESET added to all funcs"}
        REQ
            _fn_init 'fn' \
               'echo "this would be command 1"' \
               'echo "$common_param"'
        RESET

REQUIRED

If I want to call this function it will die unless the environment variable _if_unset is set.

fn

#OUTPUT#
+ fn
/dev/fd/4: line 1: _if_unset: shell will ERR and print this to stderr

Please note the order of the shell traces - not only does the fn fail when called when _if_unset is unset, but it never runs in the first place. This is the most important factor to understand when working with here-document expansions - they must always occur first because they are <<input after all.

The error comes from /dev/fd/4 because the parent shell is evaluating that input before handing it off to the function. It's the simplest, most efficient way to test for requisite environment.

Anyway, the failure is easily remedied.

_if_unset=set fn

#OUTPUT#
+ _if_unset=set
+ fn
+ echo 'this would be command 1'
this would be command 1
+ echo 'REQ/RESET added to all funcs'
REQ/RESET added to all funcs

FLEXIBLE

The variable common_param is evaluated to a default value on input for every function declared by _fn_init. But that value is also changeable to any other which will also be honored by every function similarly declared. I'll leave off the shell traces now - we're not going into any uncharted territory here or anything.

set +vx
_fn_init 'fn' \
               'echo "Hi! I am the first function."' \
               'echo "$common_param"'
_fn_init 'fn2' \
               'echo "This is another function."' \
               'echo "$common_param"'
_if_unset=set ;

Above I declare two functions and set _if_unset. Now, before calling either function, I'll unset common_param so you can see they will set it themselves when I call them.

unset common_param ; echo
fn ; echo
fn2 ; echo

#OUTPUT#
Hi! I am the first function.
REQ/RESET added to all funcs

This is another function.
REQ/RESET added to all funcs

And now from the caller's scope:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

But now I want it to be something else entirely:

common_param="Our common parameter is now something else entirely."
fn ; echo 
fn2 ; echo

#OUTPUT#
Hi! I am the first function.
Our common parameter is now something else entirely.

This is another function.
Our common parameter is now something else entirely.

And if I unset _if_unset?

unset _if_unset ; echo
echo "fn:"
fn ; echo
echo "fn2:"
fn2 ; echo

#OUTPUT#
fn:
dash: 1: _if_unset: shell will ERR and print this to stderr

fn2:
dash: 1: _if_unset: shell will ERR and print this to stderr

RESET

If you need to reset the function's state at any time it is easily done. You need only do (from within the function):

. /dev/fd/5

I saved the arguments used to initially declare the function in the 5<<\RESET input file-descriptor. So .dot sourcing that in the shell at any time will repeat the process that set it up in the first place. It's all pretty easy, really, and pretty much fully portable if you're willing to overlook the fact that POSIX doesn't actually specify the file-descriptor device node paths (which are a necessity for the shell's .dot).

You could easily expand on this behavior and configure different states for your function.

MORE?

This barely scratches the surface, by the way. I often use these techniques to embed little helper functions declarable at any time into the input of a main function - for instance, for additional positional $@arrays as needed. In fact - as I believe, it must be something very close to this that the higher order shells do anyway. You can see they're very easily programmatically named.

I also like to declare a generator function which accepts a limited type of parameter and then defines a single-use or otherwise scope-limited burner-function along the lines of a lambda - or an in-line function - that simply unset -f's itself when through. You can pass a shell function around.

share|improve this answer
    
What's the advantage of that extra complexity with file descriptors over using eval? –  Stéphane Chazelas Apr 22 at 6:37
    
@StephaneChazelas There's no added complexity from my perspective. In fact, I see it the other way round. Also, the quoting is far easier, and .dot works with files and streams so you don't run into the same kind of argument list problems you might otherwise. Still, it's probably a matter of preference. I certainly think it's cleaner - especially when you get into evaling eval - that's a nightmare from where I sit. –  mikeserv Apr 22 at 7:18
    
@StephaneChazelas There is one advantage though - and it's a pretty good one. The initial eval and the second eval need not be back to back with this method. The heredocument is evaluated on input, but you don't have to .dot source until you're good and ready - or ever. This enables you a little more freedom in testing its evaluations. And it provides the flexibility of state on input - which can be handled other ways - but it's far less dangerous from that perspective than is eval. –  mikeserv Apr 22 at 7:22

Maybe the decorator examples in http://sourceforge.net/projects/oobash/ project can help you (oobash/docs/examples/decorator.sh).

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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