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

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

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

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

环境

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

答案1

$ 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

答案2

_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 exectarget 并作为其目标函数的名称。

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

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

如果将上述代码块复制到.bashrc文件,不仅会shell功能_guard(), _rspec()_rake()在登录时声明,但是_gem_dec()函数也可以随时在 shell 提示符下执行(或其他方式)所以您可以随时声明新的模板化函数,只需:

_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

并且因为一个here-document只是在代码块中内联描述文件的一种方法,当我们这样做时:

<<'HEREDOC'
[$CODE]
HEREDOC

我们不妨这样做:

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

与一个很重要区别。如果你不"'\quote'"<<"'\LIMITER"'的一个here-document然后 shell 会评估它的 shell$expansion喜欢:

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

因此对于_gem_dec(),3<<-FUNC here-document被评估为输入时的文件,与它的情况相同3<~/some.file 除了因为我们离开了FUNC限制器不含引号,首先对其进行评估$expansion.重要的是它是输入,这意味着它只存在于_gem_dec(),但在之前也会对其进行评估_gem_dec()函数运行是因为我们的 shell 必须读取并评估它$expansions将其作为输入传递。

让我们做guard,例如:

_gem_dec guard

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

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,毕竟。有关更多示例,请参阅我对另一个问题的回答这里

-dash( * 从技术上讲,这并不完全正确。因为我在 之前使用了前导here-doc limiter,所以上面的内容都会左对齐。但我使用了,-dash所以我<tab-insert>首先可以提高可读性,所以我不会删除<tab-inserts>之前的内容提供给您阅读...)

关于这一点最好的部分是引用 - 请注意'"保留报价,仅保留\引号被删除。如果您必须两次评估 shell,可能正是出于这个原因$expansion我会推荐here-document因为引号是很多eval

不管怎样,现在上面的代码就像一个输入的文件一样3<~/heredoc.file只是等待_gem_dec()函数开始并接受其输入/dev/fd/3

所以当我们开始时_gem_dec()我做的第一件事就是扔全部位置参数,因为我们的下一步是两次评估的 shell 扩展,并且我不想要任何包含的$expansions被解释为我当前的任何$1 $2 $3...参数。所以我:

shift $#

shift丢弃尽可能多的positional parameters如您指定并从$1与剩下的。所以如果我打电话_gem_dec one two three根据提示_gem_dec's $1 $2 $3位置参数将是one two three和当前位置总数,或者$#就是 3。如果我随后打电话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 提示符下声明的所有函数和环境变量~/some.sh然后可以在 shell 提示符下调用。这里也是同样的情况,只不过我们. sourcecharacter device我们的文件描述符的特殊文件,或者. /dev/fd/3- 这就是我们的here-document内联文件已被指定路径 - 并且我们已经声明了我们的函数。这就是它的工作原理。

_guard

现在做任何你想做的事_guard函数应该做的。

附录:

保存位置的好方法:

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

编辑:

当我第一次回答这个问题时,我更关注声明 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为了避免他们的侵入namespace壳的commands恰当的。

然而,这并不是一个普遍的习惯,正如提问者使用command呼吁$1

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

提问者想要命名为 shell 函数guard, rspec, or rake当被调用时,将重新编译ruby同名函数if文件Gemfile存在于$PATH 或者 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根据提示将仅有的尝试执行可执行文件$PATH命名的guard打电话时_guard在提示符下将检查Gemfile's存在并进行相应编译或者执行guard可执行于$PATH这样namespace受到保护,至少在我看来,提问者的意图仍然得到实现。

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

如果询问者无法接受这一点,并且他/她宁愿取消该请求_然后,以其当前的形式,编辑_underscore据我了解,out 应该是提问者满足他/她的要求所需要做的全部事情。

除了这一更改之外,我还编辑了要使用的函数&&和/或||短路条件而不是原来的if/then句法。这样一来command语句仅被评估根本不如果Gemfile不在$PATH此修改确实需要添加return $?然而为了确保bundle语句不在事件中运行Gemfile不存在但是ruby $1函数返回除0。

最后,我应该指出,该解决方案仅实现可移植的 shell 结构。换句话说,这应该在任何声称 POSIX 兼容性的 shell 中产生相同的结果。当然,如果我声称每个 POSIX 兼容系统都必须处理ruby bundle指令,至少调用它的 shell 命令应该表现相同,无论调用 shell 是否是sh或者dash上述也将按预期工作(假设至少有一半理智shopts同时bashzsh

答案3

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

相关内容