如何一次定义类似的bash函数


10

我在这些功能~/.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
}

如您所见,这些功能非常相似。我想一次定义这三个功能。有办法吗?

环境

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)

Answers:


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[@]}" "$@"
}

通常的注意事项eval适用。


for loop?我的意思是,它不会被吃掉,for loop通常声明的变量会消失-由于相同的原因,我期望相同的功能。
mikeserv

什么让你有那个想法?bash -c 'for i in 1; do :; done; echo $i'=> 1。所述type清楚地表明,功能循环的范围之外存在。
AdrianFrühwirth2014年

1
@mikeserv即使使用bash的动态作用域,您所能获得local的只是整个函数范围内的局部变量,变量绝对不会在循环后“消失”。实际上,由于这里不涉及任何函数,因此local在这种情况下甚至无法定义变量。
AdrianFrühwirth2014年

正确-在for循环本地-它们在本地范围内。它们确实会在其父循环外壳程序消失后立即消失。这不是在这里发生吗?
mikeserv

不,正如我刚刚解释的那样,shell脚本中不存在“局部于for循环”的概念,我的帖子和我上面评论中的示例清楚地表明了这一点。
AdrianFrühwirth2014年

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."

上面的遗嘱. source /dev/fd/3_gem_dec()每次被称为here-document. _gem_dec's仅预评估作业时都会馈入该函数,即接收一个参数并将其既作为bundle exec目标又作为目标函数的名称进行预评估。

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

但在上述情况下,我认为不会有任何风险。

如果将以上代码块复制到.bashrc文件中,不仅将在登录时声明外壳函数_guard(), _rspec()_rake()声明外壳函数,而且_gem_dec()还可以随时在您的外壳提示符下(或以其他方式)执行该函数,因此新的模板函数可以只要您愿意就可以声明:

_gem_dec $new_templated_function_name

感谢@Andrew向我展示了这些不会被 for loop.

但是如何?

我使用3上面的文件描述符来保持stdin, stdout, and stderr, or <&0 >&1 >&2习惯-尽管我在这里实现的其他一些默认预防措施也是如此-因为生成的函数是如此简单,所以实际上没有必要。不过,这是个好习惯。呼叫shift $#是这些不必要的预防措施中的另一个。

尽管如此,当文件指定为,<input>output通过[optional num]<file[optional num]>file重定向指定时,内核会将其读取到文件描述符中,可以通过中的character device特殊文件进行访问/dev/fd/[0-9]*如果[optional num]省略了说明符,则0<file假定用于输入和1>file输出。考虑一下:

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

而且由于a here-document只是在代码块中内联描述文件的一种方式,所以当我们这样做时:

<<'HEREDOC'
[$CODE]
HEREDOC

我们不妨这样做:

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

有一个非常重要的区别。如果您不"'\quote'"使用<<"'\LIMITER"'here-document那么shell会$expansion像下面这样对shell进行评估:

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

因此,对于_gem_dec()3<<-FUNC here-document在输入时将其评估为文件,与之相同,3<~/some.file 只是因为我们使FUNC限制器不带引号,所以首先对其进行评估。$expansion.关于这一点的重要一点是输入,这意味着它仅存在,_gem_dec(),但是在_gem_dec()函数运行之前也会对其进行评估,因为我们的外壳程序必须先读取并评估它,$expansions然后再将其作为输入传递。

让我们做个guard,例子:

_gem_dec guard

因此,首先外壳必须处理输入,这意味着读取:

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

进入文件描述符3并对其进行评估以用于Shell扩展。如果此时您运行了:

cat /dev/fd/3

要么:

cat <&3

由于它们都是等效的命令,因此您会看到*:

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

...之前,函数中的任何代码都无法执行。毕竟<input这是函数的。有关更多示例,请参阅我的回答不同的问题在这里

(*技术上这并不完全正确,因为我使用的是领先-dashhere-doc limiter,上面都会是左对齐的,但我用了-dash,所以我可以<tab-insert>在首位可读性,所以我不打算在剥离<tab-inserts>之前提供给您阅读...)

最好的部分是引号-请注意,'"引号仍然保留,只有\引号被删除。可能是因为这个原因比其他任何原因都多,所以如果您必须对shell进行两次评估,$expansion我会推荐,here-document因为引号容易得多eval

无论如何,现在上面的代码完全就像一个馈入的文件,就像3<~/heredoc.file只是等待_gem_dec()函数开始执行并接受上的输入一样/dev/fd/3

因此,当我们开始做_gem_dec()的第一件事时,我要做的是扔掉所有位置参数,因为我们的下一步是两次评估的外壳扩展,并且我不希望将其中的任何内容$expansions解释为我当前的任何$1 $2 $3...参数。所以我:

shift $#

shift丢弃positional parameters您指定的数量并从$1剩余的内容开始。所以,如果我叫_gem_dec one two three在提示符_gem_dec's $1 $2 $3位置参数将one two three和总电流位置数,或者$#是如果我当时叫shift 2,的价值onetwo将被shift了,值$1将变为three$#会扩大到1所以shift $#只把它们全部扔掉。严格来说这是预防措施,只是我在做这种事情一段时间后养成了的习惯。(subshell)为了清楚起见,这里有点分散:

( 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

无论如何,下一步就是魔术发生的地方。如果. ~/some.sh在shell提示符下,则在shell提示符下~/some.sh可以调用其中声明的所有函数和环境变量。除了文件描述符. sourcecharacter device特殊文件,或者. /dev/fd/3- here-document内联文件已在其中存放路径的文件-并且我们已经声明了函数外,这里也是如此。这就是它的工作原理。

_guard

现在执行您的_guard功能应该执行的操作。

附录:

保存位置信息的好方法:

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

编辑:

当我第一次回答这个问题时,我更关注于声明一个shell,该shell function()能够声明将在当前shell持久化过程中继续存在的其他功能,而不是我对$ENV问问者对所述持久性功能的处理。从那时起,我意识到我最初提供的解决方案3<<-FUNC采用以下形式:

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

如预期的提问者,因为我专门从修改声明函数的名称很可能不会管用$1_${1},如果像叫_gem_dec guard例如,将导致_gem_dec声明名为函数_guard,而不是只guard

注意:这种行为对我来说是个习惯问题-我通常以这样的假设为前提:shell函数应占据自己的函数_namespace,以避免它们侵入namespaceshellcommands本身。

但是,这不是普遍的习惯,就像问问者使用commandcall所证明的那样$1

进一步检查使我相信以下内容:

询问器需要命名为shell函数guard, rspec, or rake,该壳函数在被调用时将重新编译ruby同名函数,if该文件Gemfile存在于$PATH OR if Gemfile不存在的情况下,shell函数应执行ruby同名函数。

以前这是行不通的,因为我还更改了的$1要求command,改为:

command _${1}

不会导致执行rubyshell函数编译为的函数:

bundle exec $1

我希望您能看到(就像最终我一样),问问者似乎根本就只是command在使用间接指定,namespace因为它command更喜欢$PATH通过同名的Shell函数来调用可执行文件。

如果我的分析是正确的(我希望申请者确认),那么可以这样做:

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

应该更好地满足这些条件,但guard在提示符处调用只会尝试执行$PATHnamed中的可执行文件,guard_guard在提示符处进行调用将检查是否Gemfile's存在并相应地编译执行中的guard可执行文件$PATH以这种方式namespace受到保护,并且至少按照我的理解,询问者的意图仍然得以实现。

实际上,假定我们的shell函数_${1}()和可执行文件${PATH}/${1}是我们的shell解释对其中一个调用的唯一两种方式,$1或者_${1}然后再command在该函数中使用完全是多余的。尽管如此,我还是保留了它,因为我不想连续两次犯同样的错误。

如果问询者无法接受,并且他/她希望_完全放弃,那么就其目前的形式而言,_underscore根据我的理解,应根据要求满足他/她的要求来进行编辑。

除了所做的更改外,我还编辑了使用&&和/或||外壳短路条件if/then语句的功能,而不是原始语法。这样的command说法只计算在所有的,如果Gemfile在不$PATH此修改确实需要添加,return $?但是要确保在不存在bundle该事件Gemfile但该ruby $1函数返回0以外的的情况下不运行该语句

最后,我应注意,此解决方案仅实现可移植的shell构造。换句话说,在任何声称具有POSIX兼容性的shell中,这应该产生相同的结果。当然,声称每个POSIX兼容系统都必须处理该ruby bundle指令对我来说是胡说八道,但是至少调用它的shell命令必须具有相同的行为,而不管调用的shell是sh还是dash同样,上述方法在中都将按预期工作shopts无论如何至少应该保持中立)bashzsh


我将您的代码放入~/.bashrc并调用. ~/.bashrc,然后执行这三个函数。也许行为因环境而异,所以我在问题中添加了环境。此外,我不明白为什么_guard ; _rspec ; _rake需要最后一行。我搜索了shift文件描述符,看起来这些超出了我目前的理解。
ironsand

我只是将其放在此处以表明它们是可调用的。抱歉-我现在回声了。如您所展示的,因此您可以将它们称为函数。
mikeserv

@Tetsu-现在更有意义了吗?
mikeserv

我通读了您的答案3次,但老实说,我需要更多的知识来理解解释。尽管我非常感谢您,但我会在获得更多经验时再次阅读。
ironsand

@Tetsu也许现在更清楚了...?我想我意识到了,现在已经纠正了我以前犯的一个错误。如果可以的话,请告诉我。
mikeserv 2014年

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.