29

How do I create a function named from the contents a variable? I want to write a template script that defines a function named after the script’s own file name. Something like this (which of course doesn't work):

#!/bin/bash
bname="$(basename $0)" # filename of the script itself

someprefix_${bname}() { # function's name created from $bname variable
    echo test
}

So if the script's filename is foo.sh, then it should define a function named someprefix_foo that will echo "test".

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
con-f-use
  • 3,772
  • 5
  • 39
  • 60
  • 4
    Here comes the standard answer to unusual requests: Why do you want to do this? – carlpett Aug 22 '11 at 09:21
  • 2
    Whenever that "answer" comes I find myself tempted to say "I just want to, deal with it!". But the explanation is pretty strange here: I often find myself writing scripts, that define their own auto completion when sourced (and do something else when not). So I figured I might write a template for those. For the auto completion function of those scripts I need an identifier unique to the script - so I thought why not build it form the script's file name. Don't know if that's a good idea but it's better than nothing. – con-f-use Aug 22 '11 at 09:31
  • 4
    Well, I understand your sentiment, but more often than not, unusual ideas come from Doing It Wrong(tm). On the other hand, sometimes it's a fully valid idea, such as this one (and I like it!). But for the majority of these questions, a gentle prod down the Right Way(tm) will be better help than implementing what was originally asked for. – carlpett Aug 22 '11 at 10:49
  • The script in the question actually does work if you use `#!/bin/zsh` – cambunctious Nov 08 '19 at 22:00

6 Answers6

31

You can use eval:

eval "someprefix_${bname}() { echo test; }"

Bash even allows "."s in function names :)

Eugene Yarmash
  • 142,882
  • 41
  • 325
  • 378
  • 1
    You know - there are times when I doubt myself. Could have come up with that myself, but didn't. Thanks a lot, eugene! – con-f-use Aug 22 '11 at 09:32
19

Indeed, it is instructive that the aforementioned construction derails the possibility of having embedded quotation marks there within and is thus potentially bugulant. However, the following construction does proffer similar functionality whilst not having the constraints of the former answer.

For your review:

source /dev/stdin <<EOF
function someprefix_${bname}()
{
     echo "test";
};
EOF
  • IIRC, there was a bug in some versions of bash which were unable to successfully handle sourcing from a pipe. Solved in current releases, and moot if running this code on a version outside the range that was prone to it, but this may not work everywhere. – Charles Duffy Dec 01 '15 at 17:47
  • 1
    This is the best answer here. I'll just add that you have to escape the $ with / when you want to delay the execution to actual run time of the function. – meridius Apr 08 '19 at 14:09
  • 1
    Re @meridius comment you have to escape parameters inside the function declaration to make this work with parameters. Example: `function someprefix_${bname}() { echo "\$@"; }` – sastorsl Feb 16 '22 at 09:10
2

The 'eval and 'source' solutions work IFF you do not have anything in the function that could be evaluated at create time that you actually want delayed until execution time.

source /dev/stdin << EOF
badfunc () 
{
    echo $(date)
}
EOF

$ date;badfunc;sleep 10;date;badfunc
Tue Dec  1 12:34:26 EST 2015 ## badfunc output not current
Tue Dec  1 12:23:51 EST 2015
Tue Dec  1 12:34:36 EST 2015 ## and not changing
Tue Dec  1 12:23:51 EST 2015

Yes, it's a contrived example (just call date and don't echo a subshell call of it), but using the original answers, I tried something a more elaborate that required a run time evaluation and got similar results - evaluation at "compile" time, not run time.

The way that I solved the problem was to build a temp file with the function and then source that. By the judicious use of single and double quotes surrounding the test I print into the file, I can completely control what gets evaluated and when.

tmpsh=$(mktemp)
echo "goodfunc ()" > $tmpsh
echo '{' >> $tmpsh
echo 'echo $(date)' >> $tmpsh
echo '}' >> $tmpsh
. $tmpsh
rm -f $tmpsh

and then

$ date;goodfunc;sleep 10;date;goodfunc
Tue Dec 1 12:36:42 EST 2015 ## current
Tue Dec 1 12:36:42 EST 2015
Tue Dec 1 12:36:52 EST 2015 ## and advancing
Tue Dec 1 12:36:52 EST 2015

Hopefully, this will be of more use to people. Enjoy.

mpersico
  • 766
  • 7
  • 19
  • FYI, every time you run `echo foo >>somewhere`, a new file handle on `somewhere` is opened, and closed at the end of that single command's execution -- quite needless inefficiency. Better to open the file handle once and reuse it. – Charles Duffy Dec 01 '15 at 17:49
  • 6
    Also, the original answer could have been used with only trivial modification: `echo \$(date)` – Charles Duffy Dec 01 '15 at 17:49
  • I sit corrected. By applying backslashes as needed, I was able to take my original code based on the 'source /dev/stdin << EOF' example and get it to work without resorting to the temp file solution. Why that did not occur to me in the first place, I cannot fathom. – mpersico Dec 01 '15 at 18:58
  • I can't seem to get the /dev/stdin version working with Cygwin. I keep getting the error -bash: /dev/stdin: No such file or directory even though `Matthew@monolith /dev $ ls -la std* lrwxrwxrwx 1 Matthew None 15 Aug 27 22:22 stderr -> /proc/self/fd/2 lrwxrwxrwx 1 Matthew None 15 Aug 27 22:22 stdin -> /proc/self/fd/0 lrwxrwxrwx 1 Matthew None 15 Aug 27 22:22 stdout -> /proc/self/fd/1 ` – mpersico Dec 05 '15 at 05:59
  • Cygwin, mingw32 and msys all have their own set of leaky abstractions. I'd go with `eval` there. – Charles Duffy Dec 05 '15 at 20:47
  • Thank you. I actually used cat < file and source file. I think It reads better. – mpersico Dec 06 '15 at 00:17
  • I kinda' hate that from a cleanliness/robustness perspective -- you have to do some work with traps to make sure your temporary files are automatically deleted on erroneous exits; temporary files are prone to various security attacks unless they're generated with appropriate tools (to avoid symlink attacks and other race-condition fun), &c. Granted, you're using `mktemp`, so you're already covered on the latter case. – Charles Duffy Dec 06 '15 at 00:21
  • I was about to argue about string eval vs < – mpersico Dec 06 '15 at 05:02
1

I use the following syntax, which avoids /dev/stdin but still lets you use heredocs:

eval "$(cat <<EOF
  $function_name() {
    commands go here
    but "\$you \$have \$to \$escape $variables"
  }
EOF
)"

(notice $variables will be expanded in the function body, the other vars will not be).

dimo414
  • 47,227
  • 18
  • 148
  • 244
0

To avoid uncertainty of what gets expanded when, I used to put all the stuff I want to make in an auxiliary function with a fixed name and just call that from a function with a dynamic name:

_function_main() {
  date
  printf '%s\n' "$@"
}

eval "function_${dynamicname}() { _function_main \"\$@\"; }"
Golar Ramblar
  • 103
  • 1
  • 7
-2

You can disable expansion of a Here Document like that:

eval "$(cat <<EOF
  $function_name() {
    commands go here
    and "$you $do $not $have $to $escape variables"
  }
EOF
)"

See: How to cat <<EOF >> a file containing code?

Bash Manual: 3.6.6 Here Documents / Here Document

No parameter and variable expansion, command substitution, arithmetic expansion, or filename expansion is performed on word. If any part of word is quoted, the delimiter is the result of quote removal on word, and the lines in the here-document are not expanded. If word is unquoted, all lines of the here-document are subjected to parameter expansion, command substitution, and arithmetic expansion, the character sequence \newline is ignored, and \ must be used to quote the characters \, $, and `.

Léa Gris
  • 17,497
  • 4
  • 32
  • 41
  • This has different behavior than my answer; here `$you` and the other variables will be expanded in the function definition. You still need to `\$escape` any variables you want to preserve in the function definition, Which you'll want depends on the desired behavior, but this does not "disable expansion". – dimo414 Apr 13 '20 at 10:32