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


17

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

しかし、問題は、変数ごとに異なる関数を作成する必要があることです(たとえば、$CLASSPATHlike assign-classpath()などの別の関数)。bash関数に引数を渡す方法を見つけることができなかったため、参照でアクセスできます。

次のようなものがあればもっといいでしょう-

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

上記のようなことをbashでどのように達成するのですか?


「他の言語」とは何ですか?
チョロバ14

bashは、Cで/ javaのなどのように「参照渡し」ができた場合だけでなく、私が意味することを試みた
ramgorur

1
assign-path /abc追加しません/abc$ PATHが既に含まれている場合、PATHに/abc/def/abcd/def/abc特にあなたが追加することはできませんなどの/binPATHが既に含まれている場合/usr/bin
奇跡173 14

@ miracle173-これは本当です、あなたがする必要があるのは$PATH、次のようなあなたの議論に対するテストを分割して否定することですadd=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}"私の答えでは、私はあなたが好きなだけの引数を使用してこのようにしますIFS=:
mikeserv 14

コロンで区切られたリストに値を追加する特定の(インスタンス)問題に関連する:どうすればきれいに追加でき  $PATHますか? そして、ディレクトリが$PATHまだない場合スーパーユーザー)に追加します
スコット

回答:


17

では、別のコンテンツによって参照される変数を展開するために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

またsubstr、のコロンで区切られたメンバーの1つのサブストリングであるが、bigstrそれ自体のメンバーではない場合は追加されないというバグも修正したことに注意してください。たとえば、これにより、/binPATH既に含む変数に追加できます/usr/bin。このextglobセットを使用して、文字列の先頭または末尾、またはコロンに続いて他のものに一致します。なしextglobでは、代替手段は次のようになります。

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

-gin declareはbashの古いバージョンでは使用できませんが、この下位互換性を確保する方法はありますか?
ramgorur

2
@ramgorur、exportそれをあなたの環境に置くために使用することができます(重要な何かを上書きするリスクがある)またはeval(注意しないとセキュリティを含むさまざまな問題)。使用evalする場合、あなたがそれを好きなら大丈夫でなければなりませんeval "$target=\$substr"\ ただし、忘れると、の内容にスペースが含まれている場合、コマンドが実行される可能性がありsubstrます。
グレアム

9

bash 4.3で新たに-n追加されたdeclare&のオプションlocal

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

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

それは印刷されhello, worldます。


Bashの名前参照に関する唯一の問題は、名前参照自体と同じ名前の変数(関数の外部)を参照する名前参照(たとえば関数内)を持たないことです。ただし、これはリリース4.5では修正される可能性があります。
クサラナナンダ

2

を使用evalしてパラメータを設定できます。ここでこのコマンドの説明を見つけることができます。次の使い方evalは間違っています:

違う(){
  eval $ 1 = $ 2
}

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

割当(){
  eval $ 1 = '$ 2'
}

これらの機能を使用した結果を確認します。

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
$ 
$エコー:$ X1:
:$ X2:
$ echo:$ X2:
:$ X3:
$ echo:$ X3:
:xxx:
$ 
$間違ったY $ X1
$エコー:$ Y:
:$ X3:
$ 
$ assign Y $ X1
$エコー:$ Y:
:$ X2:
$ 
$ Y「ハローワールド」を割り当てます
$ echo:$ Y:
:ハローワールド:
$#次は予想外かもしれません
$ assign Z $ Y
$ echo ":$ Z:"
:ハロー:
$#なので、変数の場合、2番目の引数を引用する必要があります
$ Z "$ Y"を割り当てます
$ echo ":$ Z:"
:ハローワールド:

ただし、を使用しなくても目標を達成できますeval。私はこの方法のほうがより簡単です。

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

増強(){
  ローカルCURRENT = $ 1
  ローカルAUGMENT = $ 2
  ローカルNEW
  if [[-z $ CURRENT]]; それから
    NEW = $ AUGMENT
  エリフ[[!(($ CURRENT = $ AUGMENT)||($ CURRENT = $ AUGMENT:*)|| \
    ($ CURRENT = *:$ AUGMENT)|| ($ CURRENT = *:$ AUGMENT:*))]]; それから
    NEW = $ CURRENT:$ AUGMENT
  そうしないと
    NEW = $ CURRENT
    fi
  エコー「$ 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
::/置き場


「/ usr lib」「/ usr bin」を増やします
/ usr lib:/ usr bin

「/ 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`

あなたの評価文でさえ間違っています。たとえば、次のようになります。v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' -varを割り当てる前に「OHNO!」をエコーし​​ます。 =で評価されることはありません。
mikeserv

@mikeservはコメントをありがとうございますが、これは有効な例ではないと思います。割り当てスクリプトの最初の引数は、変数名、または=割り当てステートメントの左側で使用される変数名を含む変数でなければなりません。私のスクリップはその議論をチェックしていないと主張することができます。それは本当です。引数があるかどうか、または引数の数が有効かどうかもチェックしません。しかし、これは意図的なものです。OPは、必要に応じてこのようなチェックを追加できます。
奇跡173 14

@mikeserv:最初の引数を静かに有効な変数名に変換するという提案は良い考えではないと思います:1)ユーザーが意図していない変数が設定/上書きされます。2)エラーは関数のユーザーから隠されています。それは決して良い考えではありません。その場合は、エラーを発生させる必要があります。
奇跡173 14

@mikeserv:vassign関数の2番目の引数として変数(より良い値)を使用したい場合は興味深いです。したがって、その値は割り当ての右側にある必要があります。関数の引数を引用する必要がありますassign。この微妙さを投稿に追加しました。
奇跡173 14

おそらく本当です-そしてあなたは実際にあなたの最後の例でevalを使用していません-それは賢明です-したがって、それは本当に重要ではありません。しかし、私が言っているのは、evalを使用してユーザー入力を受け取るコードは本質的にリスクが高いということです。あなたの例を使用した場合、少しの労力でパスをrmに変更するように設計された関数を作成できます。
mikeserv 14

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

1
の使用evalは、任意のコマンド実行の影響を受けます。
クリスダウン14

zhe evalラインをより安全にするアイデアがありますか?通常、evalが必要だと思うときは、* shを使用せず、代わりに別の言語に切り替えることにします。一方、のscripsでこれを使用すると、いくつかの変数PATH-のように、それは...文字列定数ではなく、ユーザー入力で実行されますにエントリを追加する

1
eval安全に行うことができます-しかし、それは多くのことを考えます。あなただけのパラメータを参照しようとしている場合、あなたはこのような何かをしたいと思います:eval "$1=\"\$2\""上そのようにeval's最初のパスが、それだけで$ 1評価し、第二に、それは値=「$ 2」ん。しかし、あなたは何か他のことをする必要があります-これはここでは不要です。
mikeserv 14

実際、上記のコメントも間違っています。あなたはやらなければならない"${1##*[;"$IFS"]}=\"\$2\""-そしてそれでも保証なしで来ます。またはeval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\""。簡単ではない。
mikeserv 14

1

名前付き引数は、Bashの構文の設計方法ではありません。Bashは、Bourneシェルを繰り返し改良するように設計されました。そのため、2つのシェル間で可能な限り特定のものが機能することを確認する必要があります。それがされていないので、意味、全体でスクリプトが容易であることをあなたがする上でボーン環境からスクリプトを取る場合ようにしながら、ちょうど良くボーンよりであることを意味していますbash、それはできるだけ簡単にです。多くのシェルがまだボーンを事実上の標準として扱っているため、それは些細なことではありません。人々は(この移植性のために)Bourne互換になるようにスクリプトを書くので、その必要性は引き続き有効であり、変わることはほとんどありません。

python可能であれば、別のシェルスクリプト(など)を完全に確認することをお勧めします。言語の制限に直面している場合、新しい言語の使用を開始する必要があります。


たぶんbash人生の初期にこれは本当だった。しかし、今では特定の規定が作成されています。完全な参照変数が利用可能になりました-derobertの回答をbash 4.3参照してください。
グレアム14

そして、ここを見ると、POSIXポータブルコードだけでも、この種のことが本当に簡単にできることがわかります。unix.stackexchange.com
a

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=、パラメータの展開には、シェル環境が既に設定されている場合にインラインで再割り当てすることに対する組み込みの保護機能があるため、ここで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便利です。


こんにちは、私は従わなかったので、もう少し詳しく説明してもらえますか?ありがとう。
ramgorur

私はまだやっている...ちょうど少数のより多くの瞬間が喜ば...
mikeserv

@ramgorurより良い?申し訳ありませんが、実際の生活が邪魔され、書き込みを完了するのに予想より少し時間がかかりました。
mikeserv 14

ここでも同じですが、実生活に負けています。このことをコーディングするための多くの異なるアプローチのように見えます。
ramgorur

@ramgorur-もちろん、あなたをぶら下げたままにしないでください。これについて-あなたが望むものは何でも選択します。ここにあるような簡潔で移植性のある、または堅牢なソリューションとして提供できると思う他の答えassignはありません。どのように機能するかについて質問がある場合は、お気軽にお答えします。ところで、名前付き引数が本当に必要な場合は、別の関数の引数に指定された関数を宣言する方法を示す私の別の答えを見てみたいかもしれません:unix.stackexchange.com/a/120531/52934
mikeserv

-2

ルビー、パイソンなどのようなものを見つけることができませんが、これは私に近い感じ

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

私の意見では、読みやすさの方が優れています。パラメーター名を宣言するには4行は過剰です。現代の言語にも近いように見えます。


(1)問題はシェル関数についてです。答えは、スケルトンシェル関数です。それを超えて、あなたの答えは質問とは何の関係もありません。(2)あなたはそれを考える増加別々のラインを取ると、セパレータとしてセミコロンで、1行にそれらを連結するために、読みやすさを?私はあなたがそれを後方に持っていると信じています。1行のスタイルは複数行のスタイルよりも読みにくくなっています  。
スコット

不要な部分を減らすことがポイントである場合、引用符とセミコロンはなぜですか?a=$1 b=$2 ...同様に動作します。
ilkkachu
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.