一个bash函数像其他语言一样接受参数?


17

我有一个bash函数来设置$PATH这样-

assign-path()
{
    str=$1
    # if the $PATH is empty, assign it directly.
    if [ -z $PATH ]; then
        PATH=$str;
    # if the $PATH does not contain the substring, append it with ':'.
    elif [[ $PATH != *$str* ]]; then
        PATH=$PATH:$str;
    fi
}

但是问题是,我必须为不同的变量编写不同的函数(例如,为$CLASSPATHlike assign-classpath()等编写另一个函数)。我找不到将参数传递给bash函数的方法,这样我就可以通过引用来访问它。

如果我有类似的东西会更好-

assign( bigstr, substr )
{
    if [ -z bigstr ]; then
        bigstr=substr;
    elif [[ bigstr != *str* ]]; then
        bigstr=bigstr:substr;
    fi
}

任何想法,如何在bash中实现上述效果?


什么是“其他语言”?
choroba 2014年

好吧,我试图表示bash是否允许“通过引用传递”,例如c / java等
。– ramgorur 2014年

1
assign-path /abc不会追加/abc到PATH如果$ PATH中已经包含/abc/def/abcd/def/abc等等。特别是你不能添加/bin如果PATH已包含/usr/bin
miracle173'2014-4-2

@ miracle173-的确如此,您需要做的是拆分$PATH并否定测试,例如:add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}"在我的回答中,我只用来使用尽可能多的参数来执行此操作IFS=:
mikeserv 2014年

与将值添加到以冒号分隔的列表有关的特定(实例)问题有关:如何干净地添加到  $PATH 并添加目录($PATH如果尚未存在)(在超级用户上)。
斯科特,

Answers:


17

bash您可以${!varname}用来扩展由另一个内容引用的变量。例如:

$ var=hello
$ foo () { echo "${!1}"; }
$ foo var
hello

从手册页:

${!prefix*}
${!prefix@}
       Names matching prefix.  Expands to the names of variables whose names
       begin with prefix, separated by the first character of the IFS special
       variable.  When @ is used  and the expansion appears within double quotes,
       each variable name expands to a separate word.

另外,要设置由内容引用的变量(没有的危险eval),可以使用declare。例如:

$ var=target
$ declare "$var=hello"
$ echo "$target"
hello

因此,您可以这样编写函数(请注意,因为如果declare在函数中使用,则必须给定,-g否则变量将是局部变量):

shopt -s extglob

assign()
{
  target=$1
  bigstr=${!1}
  substr=$2

  if [ -z "$bigstr" ]; then
    declare -g -- "$target=$substr"
  elif [[ $bigstr != @(|*:)$substr@(|:*) ]]; then
    declare -g -- "$target=$bigstr:$substr"
  fi
}

并像这样使用它:

assign PATH /path/to/binaries

请注意,我还纠正了一个错误,如果该错误substr已经是的一个冒号分隔成员之一的子字符串bigstr,但不是其自己的成员,则不会添加。例如,这将允许添加/binPATH已经包含的变量中/usr/bin。它使用extglob集合来匹配字符串的开头/结尾或冒号,然后再匹配其他任何内容。没有extglob,替代方法是:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]

-gdeclarebash的较早版本中不可用,有什么方法可以使我向后兼容吗?
ramgorur 2015年

2
@ramgorur,您可以export将其放在您的环境中(冒着覆盖重要内容的风险)或eval(如果不小心的话,可能会遇到包括安全性在内的各种问题)。如果使用eval应该是好的,如果你不喜欢它eval "$target=\$substr"。如果您忘记了\ ,则如果内容中有空格,它将有可能执行命令substr
Graeme,2015年

9

bash 4.3的新增功能是&的-n选项:declarelocal

func() {
    local -n ref="$1"
    ref="hello, world"
}

var='goodbye world'
func var
echo "$var"

打印出来hello, world


Bash中nameref的唯一问题是,您不能拥有一个nameref(例如在函数中)引用与nameref本身同名的变量(在函数外部)。不过,此问题可能在版本4.5中已修复。
库萨兰达

2

您可以eval用来设置参数。可以在此处找到此命令的说明。以下用法eval是错误的:

错误(){
  评估$ 1 = $ 2
}

关于额外评估eval,您应该使用

分配(){
  评估$ 1 ='$ 2'
}

检查使用这些功能的结果:

$ X1 ='$ X2'
$ X2 ='$ X3'
$ X3 ='xxx'
$ 
$ echo:$ X1:
:$ X2:
$ echo:$ X2:
:$ X3:
$ echo:$ X3:
:xxx:
$ 
$错误Y $ X1
$ echo:$ Y:
:$ X3:
$ 
$分配Y $ X1
$ echo:$ Y:
:$ X2:
$ 
$分配Y“你好世界”
$ echo:$ Y:
:你好世界:
$#以下可能是意外的
$分配Z $ Y
$ echo“:$ Z:”
:你好:
$#,因此如果它是变量,则必须引用第二个参数
$指定Z“ $ Y”
$ echo“:$ Z:”
:你好世界:

但是您无需使用即可实现您的目标eval。我喜欢这样更简单。

以下功能以正确的方式进行替换(我希望)

增加(){
  本地货币= $ 1
  本地AUGMENT = $ 2
  本地新
  如果[[-z $ CURRENT]]; 然后
    新= $ AUGMENT
  小精灵[[!((($ CURRENT = $ AUGMENT)||($ CURRENT = $ AUGMENT:*)|| \
    ($ CURRENT = *:$ AUGMENT)|| ($ CURRENT = *:$ AUGMENT:*))]]; 然后
    NEW = $ CURRENT:$ AUGMENT
  其他
    NEW = $ CURRENT
    科幻
  回显“ $ NEW”
}

检查以下输出

增加/ usr / bin / bin
/ usr / bin:/ bin

增加/ usr / bin:/ bin / bin
/ usr / bin:/ bin

增加/ usr / bin:/ bin:/ usr / local / bin / bin
/ usr / bin:/ bin:/ usr / local / bin

增加/ bin:/ usr / bin / bin
/ bin:/ usr / bin

增加/ bin / bin
/箱


增加/ usr / bin:/ bin
/ usr / bin :: / bin

增加/ usr / bin:/ bin:/ bin
/ usr / bin:/ bin:

增加/ usr / bin:/ bin:/ usr / local / bin:/ bin
/ usr / bin:/ bin:/ usr / local / bin:

增加/ bin:/ usr / bin:/ bin
/ bin:/ usr / bin:

增加/ bin:/ bin
/ bin:


增加:/ bin
:: / bin


扩充“ / usr lib”“ / usr bin”
/ usr库:/ usr bin

扩充“ / usr lib:/ usr bin”“ / usr bin”
/ usr库:/ usr bin

现在,您可以通过augment以下方式使用该函数来设置变量:

PATH =`扩展路径/ bin`
CLASSPATH =`扩展CLASSPATH / bin`
LD_LIBRARY_PATH =`增强LD_LIBRARY_PATH / usr / lib`

甚至您的评估陈述是错误的。例如:v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' -在分配var之前将回显“ OHNO!”。您可以“ $ {v ## * [;” $ IFS“]} ='$ l'”以确保字符串不能扩展为任何将不会与=被评估。
mikeserv

@mikeserv感谢您的评论,但我认为这不是有效的示例。Assign脚本的第一个参数应该是变量名称或包含在=assignment语句左侧使用的变量名称的变量。您可以辩解说,我的股票未检查其论点。那是真实的。我什至不检查是否有任何参数或参数数量是否有效。但这是出于故意。OP可以根据需要添加此类检查。
miracle173

@mikeserv:我认为您的建议将第一个参数静默转换为有效的变量名不是一个好主意:1)设置/覆盖了一些用户不想要的变量。2)该错误对函数的用户隐藏。那绝不是一个好主意。如果发生这种情况,则只应提出一个错误。
miracle173

@mikeserv:有趣的是,您想使用变量v(更好的值)作为assign函数的第二个参数。因此,其值应位于分配的右侧。引用函数的参数是必要的assign。我在帖子中添加了这种微妙之处。
miracle173

可能是正确的-在最后一个示例中您实际上并没有使用eval-这是明智的做法-因此,这并不重要。但是我要说的是,任何使用eval并接受用户输入的代码都具有内在的风险-如果您确实使用了示例,那么我可以毫不费力地使该函数设计为将路径更改为rm。
mikeserv 2014年

2

通过一些技巧,您实际上可以将命名参数以及数组(在bash 3和4中测试)一起传递给函数。

我开发的方法允许您访问传递给以下函数的参数:

testPassingParams() {

    @var hello
    l=4 @array anArrayWithFourElements
    l=2 @array anotherArrayWithTwo
    @var anotherSingle
    @reference table   # references only work in bash >=4.3
    @params anArrayOfVariedSize

    test "$hello" = "$1" && echo correct
    #
    test "${anArrayWithFourElements[0]}" = "$2" && echo correct
    test "${anArrayWithFourElements[1]}" = "$3" && echo correct
    test "${anArrayWithFourElements[2]}" = "$4" && echo correct
    # etc...
    #
    test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
    test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
    #
    test "$anotherSingle" = "$8" && echo correct
    #
    test "${table[test]}" = "works"
    table[inside]="adding a new value"
    #
    # I'm using * just in this example:
    test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
}

fourElements=( a1 a2 "a3 with spaces" a4 )
twoElements=( b1 b2 )
declare -A assocArray
assocArray[test]="works"

testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..."

test "${assocArray[inside]}" = "adding a new value"

换句话说,不仅可以通过名称来调用参数(这构成了更易读的内核),而且实际上可以传递数组(以及对变量的引用-该功能仅在bash 4.3中有效)!另外,映射变量都在本地范围内,就像$ 1(及其他)一样。

使这项工作有效的代码非常轻巧,并且可以在bash 3和bash 4中使用(这是我测试过的唯一版本)。如果您对更多类似的技巧感兴趣,这些技巧使使用bash进行开发变得更加轻松便捷,则可以查看我的Bash Infinity Framework,下面的代码就是为此目的而开发的。

Function.AssignParamLocally() {
    local commandWithArgs=( $1 )
    local command="${commandWithArgs[0]}"

    shift

    if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
    then
        paramNo+=-1
        return 0
    fi

    if [[ "$command" != "local" ]]
    then
        assignNormalCodeStarted=true
    fi

    local varDeclaration="${commandWithArgs[1]}"
    if [[ $varDeclaration == '-n' ]]
    then
        varDeclaration="${commandWithArgs[2]}"
    fi
    local varName="${varDeclaration%%=*}"

    # var value is only important if making an object later on from it
    local varValue="${varDeclaration#*=}"

    if [[ ! -z $assignVarType ]]
    then
        local previousParamNo=$(expr $paramNo - 1)

        if [[ "$assignVarType" == "array" ]]
        then
            # passing array:
            execute="$assignVarName=( \"\${@:$previousParamNo:$assignArrLength}\" )"
            eval "$execute"
            paramNo+=$(expr $assignArrLength - 1)

            unset assignArrLength
        elif [[ "$assignVarType" == "params" ]]
        then
            execute="$assignVarName=( \"\${@:$previousParamNo}\" )"
            eval "$execute"
        elif [[ "$assignVarType" == "reference" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        elif [[ ! -z "${!previousParamNo}" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        fi
    fi

    assignVarType="$__capture_type"
    assignVarName="$varName"
    assignArrLength="$__capture_arrLength"
}

Function.CaptureParams() {
    __capture_type="$_type"
    __capture_arrLength="$l"
}

alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$@\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
alias @param='@trapAssign local'
alias @reference='_type=reference @trapAssign local -n'
alias @var='_type=var @param'
alias @params='_type=params @param'
alias @array='_type=array @param'

1
assign () 
{ 
    if [ -z ${!1} ]; then
        eval $1=$2
    else
        if [[ ${!1} != *$2* ]]; then
            eval $1=${!1}:$2
        fi
    fi
}

$ echo =$x=
==
$ assign x y
$ echo =$x=
=y=
$ assign x y
$ echo =$x=
=y=
$ assign x z
$ echo =$x=
=y:z=

这样合适吗


嗨,我尝试过像您一样的操作,但是不起作用,对不起,您是新手。您能告诉我此脚本有什么问题吗?
ramgorur'4

1
您对的使用eval容易受到任意命令执行的影响。
克里斯·唐纳

您有一个使这些eval线路更安全的想法吗?通常,当我认为需要评估时,我决定不使用* sh而是改用其他语言。另一方面,在脚本中使用此代码将条目追加到一些类似PATH的变量中,它将使用字符串常量而不是用户输入来运行...

1
您可以eval安全地进行操作-但需要花费很多时间。如果您只是尝试引用参数,则需要执行以下操作:eval "$1=\"\$2\""这样,在eval's第一次传递时,它仅求值$ 1,而在第二次传递时,它求值=“ $ 2”。但是您需要做其他事情-这在这里是不必要的。
mikeserv 2014年

实际上,我的上述评论也是错误的。您必须做"${1##*[;"$IFS"]}=\"\$2\""-甚至不附带任何保证。或者eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\""。这并不容易。
mikeserv

1

命名参数根本不是Bash语法的设计方式。Bash旨在对Bourne shell进行迭代改进。因此,它需要确保某些事物尽可能在两个外壳之间工作。因此,这并不意味着总体上编写脚本更容易,而只是比Bourne更好,同时还确保了如果您将脚本从Bourne环境迁移到整个过程中,bash则尽可能地容易。这并不是小菜一碟,因为许多贝壳仍然将Bourne视为事实上的标准。由于人们将其脚本编写为与Bourne兼容(出于这种可移植性),因此这种需求仍然存在,并且永远不会改变。

如果可能的话,最好python还是完全看一下其他的shell脚本(如之类的东西)。如果您遇到某种语言的限制,则需要开始使用一种新的语言。


也许在bash生命的早期,这是真的。但是,现在做出了具体规定。现在可以使用完整的参考变量,bash 4.3请参阅derobert的答案
Graeme 2014年

而且,如果您在这里看,您会发现即使使用POSIX便携式代码,您也可以非常轻松地完成这种事情:unix.stackexchange.com/a/120531/52934
mikeserv 2014年

1

使用标准sh语法(不仅可以在中使用bash,而且可以使用bash),您可以执行以下操作:

assign() {
  eval '
    case :${'"$1"'}: in
      (::) '"$1"'=$2;;   # was empty, copy
      (*:"$2":*) ;;      # already there, do nothing
      (*) '"$1"'=$1:$2;; # otherwise, append with a :
    esac'
}

就像使用bash的解决方案一样,只要包含有效的变量名declare,它也是安全的$1


0

在命名的商品上:

这非常简单,bash根本不需要-这是POSIX通过参数扩展指定的基本分配行为:

: ${PATH:=this is only assigned to \$PATH if \$PATH is null or unset}

以类似于@Graeme的方式进行演示,但以可移植的方式进行演示:

_fn() { echo "$1 ${2:-"$1"} $str" ; }

% str= ; _fn "${str:=hello}"
> hello hello hello

而且我只做str=确保它具有空值,因为参数扩展具有内置的保护措施,以防止在已经设置内联环境的情况下直接重新分配外壳程序环境。

解:

对于您的特定问题,我认为命名参数不是必需的,尽管它们确实是可能的。使用$IFS来代替:

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- $PATH ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH= ; echo "${PATH:="$*"}" ; IFS=$oFS
}

这是我运行它时得到的:

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% dir="/some crazy/dir"
% p=`assign /usr/bin /usr/bin/new "$dir"`
% echo "$p" ; echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new

注意,它仅添加了尚未存在$PATH或之前已存在的参数吗?甚至根本不需要一个以上的论点?$IFS很方便。


嗨,我没有关注,所以您可以再详细说明一下吗?谢谢。
ramgorur'4

我已经在这样做了...请再
等一下

@ramgorur有没有更好的选择?抱歉,但是现实生活受到干扰,我花了比我预期更长的时间写完这篇文章。
mikeserv 2014年

同样也屈服于现实生活。看起来有很多不同的方法可以编写此代码,让我花一些时间来研究最好的代码。
ramgorur 2014年

@ramgorur-当然,只是想确保我没有让你挂掉。关于这个-伙计,您可以选择任何您想要的。我不会说其他任何答案,我可以看到它提供的简洁,可移植或强大的解决方案assign。如果您对它的工作方式有疑问,我很乐意回答。顺便说一句,如果您真的想要命名的参数,您可能想看看我的另一个答案,其中我展示了如何声明一个为另一个函数的参数命名的函数:unix.stackexchange.com/a/120531/52934
mikeserv

-2

我找不到像ruby,python等之类的东西,但是感觉离我更近

foo() {
  BAR="$1"; BAZ="$2"; QUUX="$3"; CORGE="$4"
  ...
}

在我看来,可读性更好,声明参数名称需要4行。看起来也更接近现代语言。


(1)问题是关于shell函数的。您的答案提出了骨架外壳函数。除此之外,您的答案与问题无关。(2)您认为以分号作为分隔符将单独的行并将其连接为一行会提高可读性吗?我相信你已经倒退了。与单行样式  相比,单行样式的可读性较差
斯科特,

如果要减少不必要的部分是重点,那么为什么要使用引号和分号?a=$1 b=$2 ...效果也一样。
ilkkachu
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.