3

I'm trying to create a function that creates a function. I want to "hardcode" an argument in the inner function based on what is passed into the outer function, e.g.

outer() {
    inner() { doSomething --context=$1 $@ }
}

Given the above, on calling

outer foo

I want the inner function's definition to be as if I ran

inner() { doSomething --context=foo $@ }

In other words, interpolate the outer function's $1 for the definition of the inner function, but leave $@ to be interpolated when calling the inner function.

How can I achieve this?


Edit, for those wondering about my usecase:

I'm working with a command-line tool with subcommands, e.g.

doSomething dothis somearg
doSomething dothat

It always needs a context flag though, e.g.

doSomething --context foo dothis somearg
doSomething --context foo dothat

And usually once I'm in a context, my next several commands will use the same context.

So the ideal solution would allow me to set the context once and use the inner function like an alias that already knows its context:

outer foo
inner dothis somearg
inner dothat

This could have also been done with an environment variable, e.g.

alias inner="doSomething --context=$CONTEXT"

But in reality there are 2 flags not 1, and this (now with realistic args)—

outer gke_gke-xpn-1_europe-west1_europe-west1-s41l stats_service
inner dothis somearg
inner dothat

—is just a little more friendly to use (for me) than—

CONTEXT=gke_gke-xpn-1_europe-west1_europe-west1-s41l
NAMESPACE=stats_service
inner dothis somearg
inner dothat
1

4 Answers 4

4

I'm trying to create a function that creates a function. I want to "hardcode" an argument in the inner function based on what is passed into the outer function,

What you want is a closure, and I don't think there are those in Bash.

Just use another global variable for the "context" that inner function runs in. E.g.

context=
outer() {
    context=$1
}
inner() {
    if [ -z "$context" ]; then
        echo "context not defined" >&2
        return 1;
    fi
    doSomething --context="$context" "$@"
}

If you need two context variables, just add another one.

Note that you should quote both "$context" and "$@" to make sure values with whitespace or glob characters are not mangled due to word-splitting.

4

Okay, I actually figured it out after an hours' tinkering. The key was to use eval and escape the variables that should delay interpolation. If there are better ways though, I want to know! So please add an answer.

#!/bin/bash
outer() {
    eval "inner() { doSomething --context=$1 \$@ }"
}
10
  • Why do you need a function here? Just eval "doSomthing..." would be fine.
    – White Owl
    Jul 6 at 2:09
  • Made an edit to the answer explaining my use case. Jul 6 at 8:29
  • Just note that with the eval here, you'll have problems if $1 contains whitespace or shell special characters. You'd need to wrap it in single quotes and quote any single quotes inside the value to be able to pass arbitrary values. Or, at least check that the value given is "nice", e.g. case $1 in *[![:alnum:]]*) echo invalid value >&1; return 1;; esac
    – ilkkachu
    Jul 6 at 9:04
  • anyway, there are no closures as such in Bash, and all the eval does is to redefine the function. So I wonder, why not just set a global variable and refer to that in inner?
    – ilkkachu
    Jul 6 at 9:05
  • The clarifications should ideally have been added to the question, not the answer.
    – Kusalananda
    Jul 6 at 9:20
0

That is how it is done in bash:

#!/bin/bash
outer() {
    # define inner function
    inner() {
        echo 1=$1 2=$2 
    }

    # call inner function
    inner $1 $2
}

# this would give an error, 
inner foo 123

# call outer function.
outer bar 456

# now the inner function is defined and ready to be used by itself
inner baz 789
4
  • This didn't quite work for me but I think I underspecified my problem in the question. I will edit. Jul 5 at 16:24
  • Ah I see what I didn't communicate. I didn't want to call inner at the time of calling outer—I wantedinner to be a delayed evaluation using args supplied later. Jul 6 at 8:31
  • I'm not sure what this is supposed to do, other than obscure the code and make sure that inner is called at least once after defined. The function inner that results is as independent from the function outer as any other. Note how the values (bar and 456) don't matter at all when calling inner itself at the end.
    – ilkkachu
    Jul 6 at 9:08
  • The lack of quoting is problematic here. inner "$1" "$2" or just inner "$@" will hold your arguments together, preventing them from being substituted to globbing and expansion. Jul 6 at 14:58
0

In bash, this works:

outer() { . <( echo "inner() { doSomething $1 $2;  }" ); }
3
  • Please explain the "trick" (with source / .) you're using and why this is better (or at least different) than the suggested use of eval. Jul 6 at 13:51
  • ...it certainly has all the same vulnerabilities eval has, and it also adds some portability problems (in a few older versions of bash you couldn't successfully source a process substitution). Jul 6 at 14:57
  • It's basically the same as eval. I feed a string I built myself to the shell to execute as a command. The difference is just that I was able to find a working formulation for source but not for eval. At the time I posted the answer, the eval solution wasn't posted yet. Jul 6 at 16:47

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.