1

I need to define a list of functions in my bash profile. They all follow the same pattern. I tried to use variable as function name

for f in a b c
do 
   function $f {
      . mydir/$f;
   }
done

I got the following error from both linux and windows bash:

-bash: `$f': not a valid identifier
-bash: `$f': not a valid identifier
-bash: `$f': not a valid identifier

Is there another way to do this?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
oldpride
  • 761
  • 7
  • 15
  • 1
    Sound like an [x/y problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem); why do you want to do this? Maybe an alias is enough? – 0stone0 Nov 23 '22 at 16:59
  • 1
    alias cannot be overwritten. therefore, alias is less preferred than function. More specifically, to change an alias, you would have to "unalias" the existing one. But then you would have to test whether the old alias still exist at first; otherwise, an error message would complain for non-existing alias. In short, you have to spend more steps to maintain alias. Function can just be overwritten at anytime. – oldpride Nov 23 '22 at 17:09

1 Answers1

2

You could use eval, but I dislike using eval as a general rule. It's incredibly tricky to get all the quoting and escaping right, which means it's very, very easy to do unsafe things. Scripts that use eval are highly prone to input injection attacks where attackers feed in bad input or put weirdly named files on the filesystem and trick scripts into running arbitrary commands.

I would write out all of the function declarations, minimizing boilerplate with a crafty helper function that looks up the name of the caller. Bash provides an array called FUNCNAME that names all of the functions in the call stack. ${FUNCNAME[0]} is the name of the current function and ${FUNCNAME[1]} is the calling function.

helper() {
    . mydir/"${FUNCNAME[1]}"
}

a() { helper; }
b() { helper; }
c() { helper; }

If the names are dynamic, you could cautiously venture to use eval:

cd mydir/
for file in *; do
    [[ $file =~ ^[a-z]+$ ]] || continue
    eval "$file() { helper; }"
done

I've put a validation check in there to skip over any funky file names. We don't want to be led astray by file names containing ; and || and the like.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • Thank you this would work but I would have to update two places when add/delete a function: one update would be under mydir/, the other in the profile. The suggested old solution is better. I only need to update one place. – oldpride Nov 23 '22 at 17:12
  • 1
    One of the nice things about your function body being `{ helper; }`, it's simple and constant enough that you can pretty safely `eval` a string that defines a function with that body; validation just needs to be around the name of the associated file. – Charles Duffy Nov 23 '22 at 17:14
  • I agree with your eval comment. Plus eval is slower – oldpride Nov 23 '22 at 17:15
  • 1
    Don't you have to update two places in your attempted approach: one under `mydir/`, the other in the `for f in a b c` loop? – John Kugelman Nov 23 '22 at 17:17
  • 2
    @oldpride, ...if running `eval "$f() { helper; }"` takes more time than you consider acceptable, I'd like to suggest that bash is the wrong tool for your job. – Charles Duffy Nov 23 '22 at 17:21
  • @JohnKugelman good catch. In the code in my use case, I had "for f in mydir/*". Therefore, I was able to get away from this loop. – oldpride Nov 23 '22 at 17:23
  • @CharlesDuffy, yes, eval is very slow in my cygwin bash. So John's approach may be a good compromise between speed and maintenance. – oldpride Nov 23 '22 at 17:27
  • 1
    @oldpride, I actually find that hard to believe. There are things that are very slow in cygwin -- `fork()` in particular -- but `eval` is just running the parser; no syscalls anywhere. `eval` is just `source` without the file I/O, pretty much; it's _faster_ to run `eval` than `source /dev/stdin <<<"$f() { helper; }"`, f/e. – Charles Duffy Nov 23 '22 at 17:33
  • @oldpride, ...and if running the parser were unacceptably slow, `source` would be unacceptably slow too, and you're using that regardless. – Charles Duffy Nov 23 '22 at 17:35
  • @CharlesDuffy you may be correct. I always thought eval ran a sub-process to get the output. – oldpride Nov 23 '22 at 17:37
  • In `eval "$f() { helper; }`, it _has_ to be run in the current process, not a subprocess -- otherwise the function wouldn't still be defined after the `eval` command exited! – Charles Duffy Nov 23 '22 at 17:43