paul
paul

Reputation: 13471

Nested functions on bash

I have a function which expect to receive another function to be executed

  a(){
     function=$1
     echo "common log! $function"
     $function --> run the function
  }

What I want is to pass that function argument in my function as a nested function

   b(){
       a f(){ echo "nested function b" }
       echo "since I´m doing more things here"
   }

   c(){
       a f(){ echo "nested function c" }
       echo "since I´m doing more things here"
   }

But seems like the nested function f cannot be done on bash

Any suggestion about how to accomplish this?

Upvotes: 5

Views: 6590

Answers (3)

HTNW
HTNW

Reputation: 29148

As other answerers have stated, Bash only supports global functions. However, that doesn't need to deter you. All you have to do is define your "inner" function as a global one anyway, but just give it a name no one else will use. This is a technique called "defunctionalization." Do note that you have to be really careful to avoid executing malicious code, or it might start playing your script like a violin.

__inner_b_key=0 # one counter per script, or function, or inner function, or whatever
b() {
    local f
    f=__inner_b_f_$((inner_b_key++))
    # Yes, it's evil
    # But, it's powerful
    eval "function ${f}() { echo \"nested function b\"; }"
    a $f
}

You can compose this with another trick, "lambda lifting". If your inner functions are any sort of complicated, the quoting mandated by eval will quickly drive you nuts. Lambda lifting is the process of lifting inner functions into global functions, turning their free variables into parameters. Since Bash already lifts "local" functions to the global scope, you get a really nice effect:

__inner_c_key=0
c() {
   local f
   local computed
   computed=$(doSomething)
   # Do something with computed
   __inner_c_f() {
       # local variables are received as the first arguments
       local computed
       computed=$1
       shift
       # the arguments passed to the wrapper appear after the closure arguments
       # Do another thing with computed
   }
   # a closure is created, by storing the values of c's local variables into
   # the global definition of an anonymous function that wraps the real implementation
   # said anonymous wrapper also forwards the arguments it receives after the arguments
   # consumed to pass the closure
   # ${var@Q} is a Bash 4.4 feature that quotes the value $var such that Bash can
   # reinterpret it back to the same value, which is perfect for eval
   f=__inner_c_f_$((inner_c_key++))
   eval "function ${f}() { __inner_c_f ${computed@Q} \"\$@\"; }"
   higherOrder $f
   # a function with a closure was passed to a higher order function! in Bash!
   # even after this call to c ends, the value of computed lives on inside
   # the definition of $f, which is global and acts as the closure
   # too bad we lack garbage collection!
}

These transformations are exceedingly mechanical, to the point that it seems like you acting as compiler, translating a hypothetical "functional Bash" into normal Bash by utilizing the same techniques as other compilers turning functional code into imperative code. As you are following this mechanical process, you can hopefully make sure, easily, that your evals are sane and not evil.

Upvotes: 3

cdarke
cdarke

Reputation: 44344

You can have nested functions by using a subshell function - use parentheses instead of braces:

#!/bin/bash

a() {
    echo "hello a"
}

# Note the (
b() (
    echo "hello b"
    a() {
        echo "inner a"
    }

    a
)

a
b
a

Gives:

hello a
hello b
inner a
hello a

What you can't do is pass that inner function elsewhere, because the inner function only exists in the subshell. Bash doesn't have references to functions, only global names.

If you want to code something like a closure then use a language like Python.

Upvotes: 10

Siguza
Siguza

Reputation: 23830

Just define and pass by name:

b(){
    f(){ echo "nested function"; }
    a f
    echo "since I´m doing more things here"
}

Note however, that nested functions don't seem to be a thing in bash.
So after running the above code, f will be available in the global scope.
The above code is thus equivalent to:

f(){
    echo "nested function"
}
b(){
    a f
    echo "since I´m doing more things here"
}

Upvotes: 5

Related Questions