web-dev-qa-db-ja.com

シェルの引数を逆にする方法は?

私は配列を使用して"$@"を逆にすることが可能であることを知っています:

arr=( "$@" )

そして この答えを使用して 、配列を逆にします。

しかし、それには配列を持つシェルが必要です。

tacを使用することもできます。

set -- $( printf '%s\n' "$@" | tac )

ただし、パラメーターにスペース、タブ、または改行($IFSのデフォルト値を想定)またはワイルドカード文字が含まれている場合(グロビングが事前に無効にされていない場合)、空の要素が削除され、GNU tacコマンド(tail -rの使用は、GNUシステムの外部では若干移植性がありますが、一部の実装では大きな入力で失敗します)。

配列を使用せずにシェルの位置引数を移植可能に逆にする方法はありますか?それは、引数に空白、改行、ワイルドカードが含まれていたり、空の場合でも機能しますか?

9
Isaac

移植性があり、配列は必要なく(位置パラメータのみ)、スペースと改行で機能します。

flag=''; for a in "$@"; do set -- "$a" ${flag-"$@"}; unset flag; done

例:

$ set -- one "two 22" "three
> 333" four

$ printf '<%s>' "$@"; echo
<one><two 22><three
333><four>

$ flag=''; for a in "$@"; do set -- "$a" ${flag-"$@"}; unset flag; done

$ printf '<%s>' "$@"; echo
<four><three
333><two 22><one>

flagの値は、${flag-"$@"}の展開を制御します。 flagが設定されると、flagの値に展開されます(空の場合でも)。したがって、flagflag=''の場合、${flag....}は空の値に展開され、引用符で囲まれていないため、シェルによって削除されます。 flagが設定解除されると、${flag-"$@"}の値は-の右側の値に展開されます。つまり、"$@"の展開なので、すべてになります位置引数(引用、空の値は消去されません)。さらに、変数flagは消去(設定解除)され、次のコードには影響しません。

14
Isaac

一時ストレージに配列を使用しない場合は、forループalwaysが変化しない静的セットを反復するという事実を使用できます要素。ある意味では、ループitselfを位置パラメータの一時的なストレージとして使用して、リストを逆の順序で再構築できます。

これを行うには、最初の反復でリストを空にする必要もあります。以下のコードは、単純なフラグを使用して、これを実行する必要があるかどうかを検出します。リストが空になると、フラグが切り替えられます。

flag=true
for value do
    if "$flag"; then
        set --
        flag=false
    fi

    set -- "$value" "$@"
done

位置パラメータのリストが各反復で効果的に再構築されるため、これは残念ながら非常に低速ですset -- some-listはすべての位置パラメータを設定します)。 bashシェルは、1〜10000の整数を反転するのに約50秒かかりますが、zshは15秒強かかります。

Isaacのトリック${flag-"$@"}とともに使用すると(flagが設定されていない場合のみ"$@"に展開されます)、実際には全体の動作が遅くなります。 bashでは1分50秒(!)、zshでは25秒。

これは、シェルが$flagでテストを実行する方法、および"$@"を展開して${flag-"$@"}を展開する方法の実装の特殊性が原因であると想定しています(シェルは"$@"内部的に2回?).


一時的なストレージとして配列を使用することを許可する場合(これは標準ではありませんが、それでもかなりportableスクリプトを記述しているシェルがわかっていることが多いため、現在の値を格納するインデックスとして値$#(位置パラメーターの数)を使用できます。定位置パラメーターのループ。各反復でshiftを使用してこの値を減らすと、配列の末尾から先頭に向かって値を挿入する効果が得られます。

bashでは、配列はインデックス0から始まり、shiftは割り当ての後に来るため、最後の定位置パラメーターは0ではなくインデックス1に格納されます。これはコードに影響を与えません。 bashで動作しますが、正しい結果が生成されますが、zshでも動作します(デフォルトでは1ベースの配列インデックスを使用します)。

コード:

tmp=()
for value do
    tmp[$#]=$value
    shift
done

set -- "${tmp[@]}"

bashまたはzshでは、約0.6秒を使用して、1〜10000の整数を反転させます。

10
Kusalananda

この私の答え から Bash-globを使用して反転されたファイルリストを出力する からコピーして、POSIXlyの位置パラメータのリストを反転します。

eval "set -- $(awk 'BEGIN {for (i = ARGV[1]; i; i--) printf " \"${"i"}\""}' "$#")"

または、いくつかの行で少し読みやすくします:

eval "set -- $(
  awk '
    BEGIN {
      for (i = ARGV[1]; i; i--)
        printf " \"${" i "}\""
    }' "$#"
)"

awkを使用して、set -- "${3}" "${2}" "${1}"に3つの要素がある場合に、eval"$@"シェルコードを生成して解釈できるようにするというアイデア。

大きなリストの場合、シェルループ、特に各反復でリストを再構築するループを使用するよりも、かなり高速になる可能性があります。 awkコードは、(@ mosvyがコメントで示したように)同じ出力を与えるシェルループで置き換えることができますが、bash5 + gawk4.1を使用したテストでは、非常に短いリストを除いて、速度が2倍になります。

zshでは、配列を逆にするように明示的に設計されたOaパラメータフラグを使用します。

set -- "${(Oa)@}"

私のシステム(@Kusalanandaのものより少し遅い)、およびset $(seq 10000)で取得された位置パラメーターのリストで、bash5 + gawk4.2.1を使用すると、そのevalアプローチは0.4秒かかります @ Kusalananda's = 1分かかります @ Isaac's 2分かかります(zshOaアプローチには約2ミリ秒かかります)。

Busybox 1.30.1のshおよびawkでは、これらのタイミングはそれぞれ0.06秒、11秒、11秒になります。

10