Bash: ループによって生成された変数の末尾のカンマを削除する方法

Bash: ループによって生成された変数の末尾のカンマを削除する方法

同じ行に 2 つの乱数を出力する次の bash スクリプトがあります。

#!/bin/bash

for i in 1 2; do
   unset var
   until [ "$var" -lt 10000 ] 2>/dev/null; do
      var="$RANDOM"
done
printf "%s," "${var/%,/}"
done

出力は次のようになります。

5751,2129,

私は、$var = 5751,2129 の出力を使用できるように、末尾のカンマを削除しようとしています$var。誰かこれを手伝ってくれませんか?"${var/%,/}"

答え1

次のように割り当てると、var="$RANDOM"変数varには の展開からの文字列が保持されます$RANDOM。Bash では、 は からまで$RANDOMの範囲の 10 進数に展開されます。そこには文字がないので、後での展開から を削除しようとしても意味がありません。032767,,$var

出力で確認されたコンマ文字は、コードのこの部分から取得されました。

printf "%s," "${var/%,/}"
# here    ^

このコマンドはループの各反復で呼び出されたため、各反復で出力に 1 つのコンマ文字が追加されました。

印刷されたものは印刷を取り消すことはできません。ニュアンスには次のようなものがあります。

  • 出力をフィルターにパイプすると、フィルターによって出力の一部が削除されることがあります。フィルターはスクリプトの外側にある場合もあります(たとえば、 を呼び出すなどthe_script | the_filter)。または、一部スクリプトのフィルターをスクリプト内のフィルターに渡す。後者の場合、スクリプト全体の出力がフィルターされるが、スクリプトの一部で印刷されたものは印刷されないわけではない。するフィルターに到達します。フィルターは後でそれを削除します。
  • 端末に印刷する場合、以前の出力の一部を新しいデータで上書きすることができます。カーソルを移動するための文字とシーケンスがありますが、それらはすべて端末に届きます。以前の出力を視覚的にすぐに非表示にすることができますが、出力をファイルにリダイレクトすると (または、使用されているシーケンスを理解しない端末にリダイレクトすると)、すべてがそこに残っていることがわかります。

不要なコンマを取り除く正しい方法は、そもそもそれを印刷しないこと、またはスクリプト内でフィルタリングすることです。それを行う方法はいくつかありますが、そのすべてを見つけるつもりはありません。そのうちのいくつかについて説明します。また、反復回数が 2 回を超えるループの可能なアプローチも知りたいと思います。反復回数が事前にわからないループ、番号で列挙されていないループ、終了しない可能性のあるループ (while trueの代わりになどfor) などです。

注意: を使用しましたがprintf "%s," "${var/%,/}"、末尾の改行文字は印刷されません。可能であれば、この動作を再現してみます。

考えられるアプローチはいくつかあります:

  1. ループの内側は に依存しません$i。ループを削除して、2 つの別々の変数を使用できます。

    unset var1
    until [ "$var1" -lt 10000 ] 2>/dev/null; do
       var1="$RANDOM"
    done
    unset var2
    until [ "$var2" -lt 10000 ] 2>/dev/null; do
       var2="$RANDOM"
    done
    printf '%s,%s' "$var1" "$var2"
    

    ノート:

    • そうではありませんドライ
    • スケーリングがうまくいきません。(もしあったらどうしますかfor i in {1..100}?)
    • 反復回数が事前にわかっていないと面倒になります。
  2. 現在のコードをパイプに入れて、末尾のコンマを除外することができます。例:

    for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    printf "%s," "$var"
    done | sed 's/,$//'
    

    ノート:

    • sed(または使用するフィルター) は、ループによって生成される不完全な行 (改行文字で終了していない行) を処理する場合と処理しない場合があります。その場合はsed実装によって異なります。
    • 処理する場合でもsed、出力が改行で終了する可能性があります。
    • sed(または使用するフィルター) は、長すぎる行を処理できない場合があります。上記の特定のコードでは、適度に短い行が生成されますが、一般的には (多くの反復を想像してください)、長さが問題になる可能性があります。
    • sedは行指向のツールであるため、処理する前に行全体を読み取る必要があります。この場合、入力全体を読み取る必要があります。すべての反復が終了するまで、何も取得されません。
    • ループはサブシェルで実行されます。 一般的には、何らかの理由でメインシェルで動作させたい場合があります。
  3. 現在のコードからの出力を変数にキャプチャできます。最後に、変数を展開しながら末尾のコンマを削除します。

    capture="$(for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    printf "%s," "$var"
    done)"
    printf "%s" "${capture%,}"
    

    ノート:

    • ループはサブシェルで実行されます。 一般的には、何らかの理由でメインシェルで動作させたい場合があります。
    • コードは最後printf(ループの外側) に到達するまで何も表示されません。すべての反復が終了するまで何も表示されません。
    • 一般的にループは任意のバイト数を出力できます。Bashは変数にかなりの量のデータを保存できると思います。printf組み込みなので、おそらくprintf "%s" "${capture%,}"ぶつけずに扱うコマンドラインの長さの制限大量のデータをシェル変数に格納するのはベストプラクティスではないと思うので、これを徹底的にテストしたわけではありません。それでも、出力の長さが制限されていることがわかっている場合は、この方法が正当化される可能性があります。(記録のために: 上記のコードは確かに非常に短い出力を生成します。)
    • Bashは変数にNUL文字を保存できません(ほとんどのシェルは保存できませんが、zshは保存できます)。さらに、$()末尾の改行をすべて削除します。つまり、変数を使用してNUL文字を保存することはできません。任意出力し、後で正確に再現します。(記録のために: 上記のコードでは、内部のフラグメントは$()NUL または末尾の改行を生成しません。)
  4. 出力をキャプチャする代わりに、各反復を何らかの変数に追加することができます。

    capture=''
    for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    capture="$capture$var,"
    done
    printf "%s" "${capture%,}"
    

    ノート:

    • コードはメインシェル(サブシェルではない)で実行されます。
    • 変数にデータを格納する場合の制限 (前の方法を参照) は依然として適用されます。
    • Bashでは、変数に を追加することができますcapture+="$var,"。(注:変数に整数属性が設定されている場合は、=+「追加」ではなく「追加」を意味します
  5. 最後の反復を検出し、次の形式を使用することができます,:

    # this example is more educative with more than two iterations
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       if [ "$i" -eq 5 ]; then
          printf "%s" "$var"
       else 
          printf "%s," "$var"
       fi
    done
    

    ノート:

    • サブシェルはありません。
    • 事前に回数がわからない場合、最後の反復を検出するのは難しくなります。
    • 配列を反復処理する場合 (例for i in "${arr[@]}") はさらに難しくなります。
    • 各反復は即座に印刷され、出力は順番に得られます。これは、ループが無限であっても機能します。
  6. 最初の反復を検出し、 のない形式を使用できます。元のコードでの代わりに,を使用することもできたことに注意してください。その場合、の代わりにが得られます。この変更により、末尾のカンマを回避または削除する上記のメソッドはいずれも、先頭のカンマを回避または削除するメソッドに変換できます。最後のメソッドは次のようになります。,%s%s,,5751,21295751,2129,

    # this example is more educative with more than two iterations
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       if [ "$i" -eq 1 ]; then
          printf "%s" "$var"
       else 
          printf ",%s" "$var"
       fi
    done
    

    ノート:

    • サブシェルはありません。
    • 1常に(または一般に固定された一意の文字列)から開始する場合、最初の反復を検出するのは簡単です。
    • しかし、配列を反復処理する場合、たとえば は難しくなりますfor i in "${arr[@]}"。配列の後の方if [ "$i" = "${arr[1]}" ]に と同一の要素がある可能性があるため、をチェックすべきではありません"${arr[1]}"。これに対処する簡単な方法は、ループのインデックスを保持し (index=1ループの前に を保持し、反復処理の最後に 1 ずつ増分する)、その値を に対してテストすることです1。ただし、このようなコードは多少扱いにくいと思います。
    • 各反復は即座に印刷され、出力は順番に得られます。これは、ループが無限であっても機能します。
  7. 変数から値を取得することもできます,。変数を空にしてループに入り、,各反復の最後に値を設定します。これにより、ループの最後に値が1回だけ変更されます。初め反復。例:

    # this example is more educative with more than two iterations
    separator=''
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       printf "%s%s" "$separator" "$var"
       separator=,
    done
    

    ノート:

    • サブシェルはありません。
    • 配列を反復処理する場合でも正常に動作します。
    • 各反復は即座に印刷され、出力は順番に得られます。これは、ループが無限であっても機能します。

    個人的には、この方法は非常にエレガントだと思います。

一般的な注意事項:

  • 各スニペットは、末尾に改行のない出力を生成します (withsedまたは別のフィルターを使用したスニペットは例外となる可能性があります)。出力全体で適切に終了したテキスト行を形成する必要がある場合は、ループの後にprintf '\n'(または sole ) を実行します。echo
  • ,ターミネータではなくセパレータにする必要があります。つまり、$var反復が空の文字列に展開された場合、反復が正確に 0 回のループは、反復が正確に 1 回のループと同じ空の出力を生成します。この場合、$var毎回空でない文字列に展開され、反復が 0 回以上あることがわかりますが、一般的なケースでは、ターミネータの代わりにセパレータを使用すると、あいまいさが生じる可能性があります。

関連情報