9

I have these functions in ~/.bashrc:

function guard() {
    if [ -e 'Gemfile' ]; then
    bundle exec guard "$@"
    else
    command guard "$@"
    fi
}
function rspec() {
    if [ -e 'Gemfile' ]; then
    bundle exec rspec "$@"
    else
    command rspec "$@"
    fi
}
function rake() {
    if [ -e 'Gemfile' ]; then
        bundle exec rake "$@"
    else
        command rake "$@"
    fi
}

As you see these functions are very similar. I want to define these 3 functions at once. Is there a way to make it?

environment

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
8
$ cat t.sh
#!/bin/bash

for func in guard rspec rake; do
        eval "
        ${func}() {
                local foo=(command ${func})
                [ -e 'Gemfile' ] && foo=(bundle exec ${func})
                \"\${foo[@]}\" \"\$@\"
        }
        "
done

type guard rspec rake

.

$ ./t.sh
guard is a function
guard ()
{
    local foo=(command guard);
    [ -e 'Gemfile' ] && foo=(bundle exec guard);
    "${foo[@]}" "$@"
}
rspec is a function
rspec ()
{
    local foo=(command rspec);
    [ -e 'Gemfile' ] && foo=(bundle exec rspec);
    "${foo[@]}" "$@"
}
rake is a function
rake ()
{
    local foo=(command rake);
    [ -e 'Gemfile' ] && foo=(bundle exec rake);
    "${foo[@]}" "$@"
}

The usual cautions about eval apply.

  • Does it not get eaten by the for loop? I mean, variables declared in a for loop generally disappear - I would expect the same of functions for the same reasons. – mikeserv Mar 20 '14 at 7:52
  • What makes you think that? bash -c 'for i in 1; do :; done; echo $i' => 1. The type clearly shows that the functions exist outside the scope of the loop. – Adrian Frühwirth Mar 20 '14 at 7:54
  • 1
    @mikeserv Even with bash's dynamic scoping all you can get is a local variable local to the scope of a whole function, variables do definitely not "disappear" after a loop. In fact, since there is no function involved here it's not even possible to define a local variable in this case. – Adrian Frühwirth Mar 20 '14 at 8:03
  • Right - local to the for loop - they're locally scoped. They do disappear as soon as their parent loop shell does. Does this not happen here? – mikeserv Mar 20 '14 at 8:15
  • No, as I just explained there is no such concept as "local to the for loop" in shell scripting and my post and the example in my comment above clearly show that. – Adrian Frühwirth Mar 20 '14 at 8:20
7
_gem_dec() { shift $# ; . /dev/fd/3
} 3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC
for func in guard rspec rake ; do _gem_dec $func ; done
echo "_guard ; _rspec ; _rake are all functions now."

The above will . source /dev/fd/3 which is fed into the _gem_dec() function every time it is called as a pre-evaluated here-document. _gem_dec's only job is to receive one parameter and pre-evaluate it as both the bundle exec target and as the name of the function in which it is targeted.

NOTE: . sourcing shell expansions results in twice-evaluated variables - just like eval. It can be risky.

In the above case though, I don't think there can be any risk.

If the above code-block is copied into a .bashrc file, not only will the shell functions _guard(), _rspec() and _rake() be declared at login, but the _gem_dec() function will also be available for execution at any time at your shell prompt (or otherwise) and so new templated functions can be declared whenever you like with just:

_gem_dec $new_templated_function_name

And thanks to @Andrew for showing me these wouldn't get eaten by a for loop.

BUT HOW?

I use the 3 file descriptor above to keep stdin, stdout, and stderr, or <&0 >&1 >&2 open out of habit - though, as is also the case for a few of the other default precautions I implement here - because the resulting function is so simple, it's really not necessary. It is good practice, though. Calling shift $# is another of those unnecessary precautions.

Still, when a file is specified as <input or >output with [optional num]<file or [optional num]>file redirection the kernel reads it into a file descriptor, which can be accessed via the character device special files in /dev/fd/[0-9]*. If the [optional num] specifier is omitted, then 0<file is assumed for input and 1>file for output. Consider this:

l='line %d\n' ; printf "$l" 1 2 3 4 5 6 >/dev/fd/1
> line 1
> line 2
> line 3
> line 4
> line 5
> line 6

( printf "$l" 4 5 6 >/dev/fd/3 ; printf "$l" 1 2 3 ) >/tmp/sample 3>/tmp/sample2

( cat /tmp/sample2 ) </tmp/sample
> line 4
> line 5
> line 6

( cat /dev/fd/0 ) </tmp/sample
> line 1
> line 2
> line 3

( cat /dev/fd/3 ) </tmp/sample 3</tmp/sample2
> line 4
> line 5
> line 6

And because a here-document is only a means of describing a file inline within a code-block, when we do:

<<'HEREDOC'
[$CODE]
HEREDOC

We might as well do:

echo '[$CODE]' >/dev/fd/0

With one very important distinction. If you do not "'\quote'" the <<"'\LIMITER"' of a here-document then the shell will evaluate it for shell $expansion like:

echo "[$CODE]" >/dev/fd/0

So, for _gem_dec(), the 3<<-FUNC here-document is evaluated as a file on input, the same as it would be if it were 3<~/some.file except that because we leave the FUNC limiter free of quotes, it is first evaluated for $expansion. The important thing about this is that it is input, meaning it only exists for _gem_dec(), but that it is also evaluated before the _gem_dec() function runs because our shell has to read and evaluate its $expansions before handing it off as input.

Lets do guard, for example:

_gem_dec guard

So first the shell has to handle the input, which means reading:

3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC

Into file descriptor 3 and evaluating it for shell expansion. If at this time you ran:

cat /dev/fd/3

Or:

cat <&3

Since they're both equivalent commands you'd see*:

_guard() { [ ! -e 'Gemfile' ] && { 
    command guard "$@" ; return $?
    } || bundle exec guard "$@"
}

...before ever any code in the function executes at all. This is the function's <input, after all. For more examples see my answer to a different question here.

( * Technically this isn't completely true. Because I use a leading -dash before the here-doc limiter, the above would all be left-justified. But I used the -dash so I could <tab-insert> for readability in the first place so I'm not going to strip the <tab-inserts> before offering it to you to read...)

The nicest part about this is the quoting - notice that the '" quotes remain and only the \ quotes were stripped. It is probably for this reason more than any other that if you have to twice-evaluate a shell $expansion I will recommend the here-document because the quotes are much easier than eval.

Anyway, now the above code is exactly like a file fed in like 3<~/heredoc.file just waiting for the _gem_dec() function to get going and accept its input on /dev/fd/3.

So when we do start _gem_dec() the first thing I do is toss all positional parameters, because our next step is a twice-evaluated shell expansion and I don't want any of the contained $expansions to be interpreted as any of my current $1 $2 $3... parameters. So I:

shift $#

shift discards as many positional parameters as you specify and starts from $1 with what remains. So if I called _gem_dec one two three at the prompt _gem_dec's $1 $2 $3 positional parameters would be one two three and the total current positional count, or $# would be 3. If I then called shift 2, the values of one and two would be shifted away, the value of $1 would change to three and $# would expand to 1. So shift $# just throws them all away. Doing this is strictly precautionary and is just a habit I've developed after doing this kind of thing for awhile. Here it is in a (subshell) spread out a little for clarity's sake:

( set -- one two three ; echo "$1 $2 $3" ; echo $# )
> one two three
> 3

( set -- one two three ; shift 2 ; echo "$1 $2 $3" ; echo $# )
> three
> 1

( set -- one two three ; shift $# ; echo "$1 $2 $3" ; echo $# )
>
> 0

Anyway, the next step is where the magic happens. If you . ~/some.sh at the shell prompt then all the functions and environment variables declared in ~/some.sh would then be callable at your shell prompt. The same is true here, except we . source the character device special file for our file descriptor, or . /dev/fd/3 - which is where our here-document in-line file has been pathed - and we've declared our function. And that's how it works.

_guard

Now does whatever your _guard function is supposed to do.

Addendum:

A great way to say save your positionals:

f() { . /dev/fd/3
} 3<<-ARGS
    args='${args:-"$@"}'
ARGS

EDIT:

When I first answered this question I focused more on the problem of declaring a shell function() capable of declaring other functions that would persist in the current shell $ENVironment than I did on what the asker would do with said persistent functions. Since then I've realized that my originally proffered solution in which 3<<-FUNC took the form:

3<<-FUNC
    _${1}() { 
        if [ -e 'Gemfile' ]; then
            bundle exec $1 "\$@"
        else 
            command _${1} "\$@"
    }
FUNC

Would likely not have worked as expected for the asker because I specifically altered the declarative function's name from $1 to _${1} which, if called like _gem_dec guard for example, would result in _gem_dec declaring a function named _guard as opposed to just guard.

Note: Such behavior is a matter of habit for me - I typically operate on the presumption that shell functions should occupy only their own _namespace in order avoid their intrusion on the namespace of shell commands proper.

This is not a universal habit, though, as is evinced in the asker's use of command to call upon $1.

Further examination leads me to believe the following:

The asker wants shell functions named guard, rspec, or rake that, when called, will compile anew a ruby function of the same name if the file Gemfile exists in $PATH OR if Gemfile does not exist, the shell function should execute the ruby function of the same name.

This would not have worked previously because I also altered the $1 called upon by command to read:

command _${1}

Which would not have resulted in execution of the ruby function that the shell function compiled as:

bundle exec $1

I hope you can see (as eventually I did) that it seems the asker is only using command at all to indirectly specify namespace because command will prefer to call an executable file in $PATH over a shell function of the same name.

If my analysis is correct (as I hope the asker will confirm) then this:

_${1}() { [ ! -e 'Gemfile' ] && { 
    command $1 "\$@" ; return \$?
    } || bundle exec $1 "\$@"
}

Should better satisfy those conditions with the exception that calling guard at the prompt will only attempt to execute an executable file in $PATH named guard whereas calling _guard at the prompt will check for Gemfile's existence and compile accordingly or execute the guard executable in $PATH. In this way namespace is protected and, at least as I perceive it, the asker's intent is still fulfilled.

In fact, presuming our shell function _${1}() and the executable ${PATH}/${1} are the only two ways our shell could interpret a call to either $1 or _${1} then the use of command in the function at all is now made entirely redundant. Still, I've let it remain as I don't like to make the same mistake twice... in a row anyway.

If this is unacceptable to the asker and he/she would prefer to do away with the _ entirely then, in its current form, editing the _underscore out should be all the asker need do to meet his/her requirements as I understand them.

Aside from that change I have also edited the function to use && and/or || shell short-circuit conditionals rather than the original if/then syntax. In this way the command statement is only evaluated at all if Gemfile is not in $PATH. This modification does require the addition of return $? however to ensure the bundle statement is not run in the event Gemfile doesn't exist but the ruby $1 function returns anything other than 0.

Last, I should note that this solution implements only portable shell constructs. In other words, this should produce identical results in any shell claiming POSIX compatibility. While it would, of course, be nonsense for me to claim every POSIX-compatible system must handle the ruby bundle directive, at least the shell imperatives calling upon it should behave the same regardless of whether the calling shell is sh or dash. Also the above will work as expected (presuming at least halfway-sane shopts anyway) in both bash and zsh.

  • I put your code into ~/.bashrc and call . ~/.bashrc, then these three function are executed. Maybe the behavior differs by environment, so I added my environment to the question. Besides I couldn't understand why last line _guard ; _rspec ; _rake is needed. I searched about shift and file descriptor, it looks like these are beyond my current understanding. – ironsand Mar 20 '14 at 8:18
  • I just put that there to show they were callable. Sorry - I put in an echo now. So you can call them as functions, as you've demonstrated. – mikeserv Mar 20 '14 at 8:36
  • @Tetsu - does it make better sense now? – mikeserv Mar 20 '14 at 9:03
  • I read through your answer 3 times, but honestly saying I need more knowledge to understand the explanation. Even though I'm very grateful you, I'll read it again when I get more experience. – ironsand Mar 21 '14 at 18:45
  • @Tetsu Perhaps it is clearer now...? I think I realized, and have now corrected, a mistake I made previously. Please let me know, if you would. – mikeserv Mar 29 '14 at 4:18
2
function threeinone () {
    local var="$1"
    if [ $# -ne 1 ]; then
        return 1
    fi
    if ! [ "$1" = "guard" -o "$1" = "rspec" -o "$1" = "rake" ]; then
        return 1
    fi
    shift
    if [ -e 'Gemfile' ]; then
        bundle exec "$var" "$@"
    else
        command "$var" "$@"
    fi
}

threeinone guard
threeinone rspec
threeinone rake

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.