_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 shift
ed 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 $ENV
ironment 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
.
for loop?
I mean, variables declared in afor loop
generally disappear - I would expect the same of functions for the same reasons. – mikeserv Mar 20 '14 at 07:52bash -c 'for i in 1; do :; done; echo $i'
=>1
. Thetype
clearly shows that the functions exist outside the scope of the loop. – Adrian Frühwirth Mar 20 '14 at 07:54bash
's dynamic scoping all you can get is alocal
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 alocal
variable in this case. – Adrian Frühwirth Mar 20 '14 at 08:03sh
for testing so I rarely even touchbash.
But I don't mean anything derogatory of course - just curious. Your answer is a lot like mine in result just different in method. I'm just curious how it works. Like look at this: bash -c 'for v in a b c d e f ; do { z="${z}${v}" ; } ; done ; echo "$v"' ⏎ abcdef – mikeserv Mar 20 '14 at 08:22