web-dev-qa-db-ja.com

他の言語のように引数を取るbash関数?

このように_$PATH_を設定するbash関数があります-

_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
}
_

しかし問題は、変数ごとに異なる関数を記述しなければならないことです(たとえば、assign-classpath()などの_$CLASSPATH_の別の関数)。参照でアクセスできるようにbash関数に引数を渡す方法が見つかりませんでした。

こんなものがあったらいいのに…

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

上記のようなものをbashで実現するにはどうすればよいですか?

17
ramgorur

bashでは、${!varname}を使用して、別のコンテンツによって参照される変数を展開できます。例えば:

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

Manページから:

${!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

substrbigstrのコロンで区切られたメンバーの1つの部分文字列であるが、それ自体のメンバーではない場合、追加されないというバグも修正しました。たとえば、これにより、すでに/binが含まれているPATH変数に/usr/binを追加できます。 extglobセットを使用して、文字列の先頭/末尾またはコロンのいずれかと一致し、それ以外のすべてと一致します。 extglobがない場合、代替案は次のようになります。

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

Bash 4.3の新機能は-nオプションdeclarelocal

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

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

それはhello, world

9
derobert

evalを使用してパラメーターを設定できます。このコマンドの説明は here にあります。次のevalの使い方は間違っています:

 wrong(){
 eval $ 1 = $ 2 
} 

追加の評価に関しては、evaldoesを使用する必要があります

 assign(){
 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 :
:hallo world:
 $#以下は予期しない可能性があります
 $割り当てZ $ Y 
 $ echo ":$ Z:" 
 :hallo:
 $#2番目の引数が変数の場合は引用符で囲む必要があります
 $はZ "$ Y" 
 $ echo ":$ Z:" [.____を割り当てます。]:ハローワールド:

しかし、evalを使用しなくても目標を達成できます。私はこの方が簡単です。

次の関数は正しい方法で置換を行います(私は願っています)

 augment(){
 local CURRENT = $ 1 
 local AUGMENT = $ 2 
 local NEW 
 if [[-z $ CURRENT]];次に
 NEW = $ AUGMENT 
 Elif [[! (($ CURRENT = $ AUGMENT)||($ CURRENT = $ AUGMENT:*)||\
($ CURRENT = *:$ AUGMENT)||($ CURRENT = *:$ AUGMENT:*)) ]];次に
 NEW = $ CURRENT:$ AUGMENT 
 else 
 NEW = $ CURRENT 
 fi 
 echo "$ NEW" 
} 

次の出力を確認してください

 augment/usr/bin /bin
/usr/bin:/bin

augment/usr/bin:/ bin /bin
/ usr/bin:/ bin 
 
 augment/usr/bin:/ bin:/ usr/local/bin /bin
/usr/bin:/bin:/usr/local /bin

augment/bin:/ usr/bin /bin
/bin:/usr/bin

augment/bin/bin 
/bin 
 
 
 augment/usr/bin:/bin
/usr/bin::/bin

 augment/usr/bin:/ bin:/bin
/usr/bin:/bin:

augment/usr/bin:/ bin:/ usr/local/bin:/bin
/usr/bin:/bin:/usr/local/bin:

augment/bin:/ usr/bin:/bin
 /bin:/usr/bin:

augment/bin:/bin
/bin:


augment:/ bin 
 ::/bin 
 
 
 augment "/ usr lib" "/ usr bin" 
/usr lib:/ usr bin 
 
 augment "/ usr lib:/ usr bin" "/ usr bin" 
/usr lib:/ usr bin 
 

これで、augment関数を次のように使用して変数を設定できます。

 PATH = `augment PATH /bin`
CLASSPATH=`augment CLASSPATH /bin`
LD_LIBRARY_PATH=`augment LD_LIBRARY_PATH /usr/lib`
2
miracle173

いくつかのトリックを使用すると、名前付きパラメーターを配列とともに関数に渡すことができます(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'
2
niieani
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=

これは合いますか?

1
user62916

名前付き引数は、Bashの構文が設計された方法ではありません。 Bashは、Bourne Shellの反復的な改善として設計されました。そのため、2つのシェル間で特定のことができる限り機能するようにする必要があります。つまり、全体的にスクリプトを記述しやすくすることはmeantではなく、Bourne環境からbashにスクリプトを取得する場合に、Bourneよりも優れていることを意味します。できるだけ簡単です。多くのシェルがまだボーンを事実上の標準として扱っているので、それは簡単なことではありません。人々は、この互換性のためにBourne互換になるようにスクリプトを作成しているため、ニーズは依然として有効であり、変更される可能性はほとんどありません。

可能であれば、別のシェルスクリプト(pythonなど)を完全に見た方がよいでしょう。言語の制限に直面している場合は、新しい言語の使用を開始する必要があります。

1
Bratchley

標準の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'
}

bashdeclareを使用するソリューションと同様に、$1に有効な変数名が含まれている限り、それはsafeです。

1

名前付きARGS:

これは非常に簡単に行われ、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=のみを実行して、null値が含まれるようにします。これは、パラメータ拡張には、シェル環境をインラインで再割り当てしないための組み込みの保護機能があるためです。すでに設定されている場合。

解決:

あなたの特定の問題については、名前付き引数は必要ないと思いますが、それらは確かに可能です。代わりに$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が便利です。

0
mikeserv

または...バックスティックを使用

function foo(){
     # $1 is first param
     # $2 is second param ...
 }

呼び出し:

 result=`foo "param1" "param2" ...`
 # they will go the right way.
0
Sergio Abreu