文字列をBashで配列に分割する

2012年05月14日に質問されました。  ·  閲覧回数 946.8k回  ·  ソース

Lgn picture
2012年05月14日

Bashスクリプトでは、行を分割して配列に格納したいと思います。

この線:

Paris, France, Europe

私はそれらを次のような配列にしたいと思います:

array[0] = Paris
array[1] = France
array[2] = Europe

簡単なコードを使いたいのですが、コマンドの速度は関係ありません。 どうすればいいですか?

回答

Paused until further notice. picture
2012年05月14日
1180
IFS=', ' read -r -a array <<< "$string"

文字ことを注意$IFSこの場合、フィールドは、カンマまたはスペースではなく、2つの文字の配列のいずれかによって分離することができるように、セパレータとして個別に扱われます。 ただし、興味深いことに、入力にコンマスペースが表示されている場合、スペースは特別に扱われるため、空のフィールドは作成されません。

個々の要素にアクセスするには:

echo "${array[0]}"

要素を反復処理するには:

for element in "${array[@]}"
do
    echo "$element"
done

インデックスと値の両方を取得するには:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

最後の例は、Bash配列がまばらであるため便利です。 つまり、要素を削除したり、要素を追加したりすると、インデックスが連続しなくなります。

unset "array[1]"
array[42]=Earth

配列内の要素の数を取得するには:

echo "${#array[@]}"

上記のように、配列はまばらである可能性があるため、最後の要素を取得するために長さを使用しないでください。 Bash4.2以降でできる方法は次のとおりです。

echo "${array[-1]}"

Bashの任意のバージョン(2.05b以降のどこかから):

echo "${array[@]: -1:1}"

負のオフセットが大きいほど、配列の端から遠くに選択されます。 古い形式のマイナス記号の前のスペースに注意してください。 必須です。

bgoldst picture
2017年07月20日
390

この質問に対するすべての答えは、何らかの形で間違っています。


間違った答え#1

IFS=', ' read -r -a array <<< "$string"

1:これは$IFS誤用です。 値$IFS変数が、むしろそれは単一文字列セパレータのセットとして取られる、単一の可変長の文字列の区切りとして各フィールドことに注意されていないreadから離脱し入力行は、セット内の任意の文字(この例ではコンマまたはスペース)で終了できます。

実際、実際のsticklerの場合、 $IFSの完全な意味は少し複雑です。 bashマニュアルから:

シェルはIFSの各文字を区切り文字として扱い、他の展開の結果をこれらの文字をフィールドターミネータとして使用して単語に分割します。 IFSが設定されていない場合、またはその値がデフォルトの<space> <tab> <newline>である場合、前の展開の結果の最初と最後にある<space><tab> 、および<newline>のシーケンスは無視され、先頭または末尾にないIFS文字のシーケンスは単語を区切るのに役立ちます。 IFSの値がデフォルト以外の場合、空白文字がの値にある限り、空白文字<space><tab> 、および<newline>のシーケンスは単語の最初と最後で無視されます。 IFSIFS空白文字)。 空白文字の任意の隣接するIFSと一緒に、空白をIFSされていないIFS内の任意の文字は、フィールドを区切ります。 IFS空白文字のシーケンスも区切り文字として扱われます。 IFSの値がnullの場合、ワード分割は発生しません。

基本的に、デフォルト以外のnull以外の値の$IFS場合、フィールドは(1)すべて「IFS空白文字」のセットからの1つ以上の文字のシーケンス(つまり、 <space><tab> 、および<newline> ( "newline"は改行(LF)を意味します)のいずれかが$IFSどこかに存在するか、(2)「IFS空白文字」以外の文字$IFSに存在し、入力行で「IFS空白文字」で囲まれています。

OPの場合、前の段落で説明した2番目の分離モードが、入力文字列に必要なものである可能性がありますが、最初に説明した分離モードがまったく正しくないことは間違いありません。 たとえば、彼の入力文字列が'Los Angeles, United States, North America'だった場合はどうなりますか?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2:このソリューションを1文字の区切り文字(コンマ自体など、後続のスペースやその他の手荷物なし)で使用する場合でも、 $string変数の値がLFが含まれている場合、 readは、最初のLFに遭遇すると処理を停止します。 readビルトインは、呼び出しごとに1行のみを処理します。 これは、 here-stringメカニズムを使用してこの例で行っているように、入力をreadステートメントにのみパイプまたはリダイレクトしている場合でも当てはまります。したがって、未処理の入力は失われることが保証されます。 readビルトインを強化するコードには、含まれているコマンド構造内のデータフローに関する知識がありません。

これが問題を引き起こす可能性は低いと主張することもできますが、それでも、可能であれば回避する必要がある微妙な危険です。 これは、組み込みのread実際には2つのレベルの入力分割を実行するという事実が原因です。最初は行に、次にフィールドに分割されます。 OPは1レベルの分割のみを必要とするため、このreadビルトインの使用は適切ではなく、回避する必要があります。

3:このソリューションの明らかでない潜在的な問題は、 readが空の場合は常に末尾のフィールドを削除しますが、それ以外の場合は空のフィールドを保持することです。 これがデモです:

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

OPはこれを気にしないかもしれませんが、それでも知っておく価値のある制限です。 これにより、ソリューションの堅牢性と一般性が低下します。

この問題は、後で説明するように、入力文字列をreadに供給する直前に、入力文字列にダミーの末尾区切り文字を追加することで解決できます。


間違った答え#2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

同様のアイデア:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(注:回答者が省略したように見えるコマンド置換の前後に欠落している括弧を追加しました。)

同様のアイデア:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

これらのソリューションは、配列割り当ての単語分割を利用して、文字列をフィールドに分割します。 おかしなことに、 read同様に、一般的な単語分割も$IFS特殊変数を使用しますが、この場合、デフォルト値の<space> <tab> <に設定されていることを意味し

これにより、 readによってコミットされる2つのレベルの分割の問題が解決されます。これは、単語の分割自体が1つのレベルの分割のみを構成するためです。 ただし、前と同じように、ここでの問題は、入力文字列の個々のフィールドにすでに$IFS文字が含まれている可能性があるため、単語分割操作中に不適切に分割されることです。 これは、これらの回答者によって提供されたサンプル入力文字列のいずれにも当てはまりません(どれほど便利か...)が、もちろん、このイディオムを使用したコードベースがリスクを冒すことになるという事実は変わりません。この仮定が将来のある時点で違反された場合、爆破します。 もう一度、 'Los Angeles, United States, North America' (または'Los Angeles:United States:North America' )の反例を考えてみましょう。

また、単語分割が正常に続いてファイル名の拡張行う場合、(グロブ別名パス名展開別名)、あろう潜在的に破損した文字を含む単語*? 、または[後に] [続きます(また、 extglobが設定されている場合は、括弧で囲まれたフラグメントの前に?*+@ 、または! )をファイルシステムオブジェクトと照合し、それに応じて単語( "globs")を展開します。 これら3人の回答者のうち最初の回答者は、事前にset -fを実行してグロブを無効にすることで、この問題を巧みに軽減しています。 技術的にはこれは機能します(ただし、後でset +f追加して、それに依存する可能性のある後続のコードのグロビングを再度有効にする必要があります)が、基本的な文字列をハックするためにグローバルシェル設定をいじる必要は望ましくありません。ローカルコードでの配列解析操作。

この回答のも​​う1つの問題は、空のフィールドがすべて失われることです。 これは、アプリケーションに応じて、問題になる場合と問題にならない場合があります。

注:このソリューションを使用する場合は、コマンド置換(シェルをフォークする)を呼び出す手間をかけるよりも、 ${string//:/ } 「パターン置換」形式のパラメーター展開を使用することをお勧めします。パラメータ展開は純粋にシェル内部操作であるため、パイプラインを起動し、外部実行可能ファイル( trまたはsed )を実行します。 (また、 trおよびsedソリューションの場合、入力変数はコマンド置換内で二重引用符で囲む必要があります。そうしないと、単語分割がechoコマンドで有効になり、場合によってはまた、コマンド置換の$(...)形式は、コマンド置換のネストを簡素化し、テキストエディターによる構文の強調表示を改善できるため、古い`...`形式よりも適しています。)


間違った答え#3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

この答えは#2とほとんど同じです。 違いは、回答者がフィールドが2文字で区切られていることを前提としていることです。一方はデフォルトの$IFSで表され、もう一方はそうではありません。 彼は、パターン置換展開を使用してIFSで表されない文字を削除し、次に単語分割を使用して、存続するIFSで表される区切り文字のフィールドを分割することにより、このかなり特殊なケースを解決しました。

これはあまり一般的な解決策ではありません。 さらに、ここではコンマが実際には「主要な」区切り文字であり、それを削除してからフィールド分割のためにスペース文字に依存することは単に間違っていると主張することができます。 もう一度、私の反例を考えてみましょう: 'Los Angeles, United States, North America'

また、ファイル名の展開によって展開された単語が破損する可能性がありますが、 set -f 、次にset +f使用して割り当てのグロブを一時的に無効にすることで、これを防ぐことができます。

また、空のフィールドはすべて失われます。これは、アプリケーションによっては問題になる場合と問題にならない場合があります。


間違った答え#4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

これは、単語分割を使用してジョブを実行するという点で#2および#3に似ていますが、コードが$IFSを明示的に設定して、入力文字列に存在する1文字のフィールド区切り文字のみを含めるようになりました。 これは、OPのコンマスペース区切り文字などの複数文字のフィールド区切り文字では機能しないことを繰り返してください。 ただし、この例で使用されているLFのような1文字の区切り文字の場合、実際にはほぼ完全になります。 以前の間違った答えで見たように、フィールドを意図せずに途中で分割することはできません。必要に応じて、分割のレベルは1つだけです。

1つの問題は、前述のようにファイル名の展開によって影響を受ける単語が破損することですが、これも重要なステートメントをset -fset +fラップすることで解決できます。

もう1つの潜在的な問題は、LFが前に定義した「IFS空白文字」として適格であるため、 #2および#3と同様に、すべての空のフィールドが失われることです。 もちろん、区切り文字が「IFS空白文字」以外の場合、これは問題にはなりません。アプリケーションによっては問題にならない場合もありますが、ソリューションの一般性が損なわれます。

したがって、要約すると、1文字の区切り文字があり、それが「IFS空白文字」ではないか、空のフィールドを気にせず、重要なステートメントをset -fでラップするとします。およびset +f場合、このソリューションは機能しますが、それ以外の場合は機能しません。

(また、情報のために、bashの変数にLFを割り当てることは、 $'...'構文を使用してより簡単に行うことができます(例: IFS=$'\n'; )。)


間違った答え#5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

同様のアイデア:

IFS=', ' eval 'array=($string)'

このソリューションは、事実上、 #1$IFSをコンマスペースに設定するという点)と#2-4 (単語分割を使用して文字列をフィールドに分割するという点)を組み合わせたものです。 このため、すべての世界で最悪のように、上記のすべての間違った答えを苦しめる問題のほとんどに苦しんでいます。

また、2番目のバリアントに関しては、引数が一重引用符で囲まれた文字列リテラルであり、静的に既知であるため、 eval呼び出しは完全に不要であるように見える場合があります。 しかし、実際には、このようにevalを使用することには非常に明白でない利点があります。 通常、変数の割り当てのみで構成される単純なコマンドを実行すると、つまり、実際のコマンドワードが後に続くことなく、割り当てはシェル環境で有効になります。

IFS=', '; ## changes $IFS in the shell environment

これは、単純なコマンドに複数の変数の割り当てが含まれている場合でも当てはまります。 繰り返しますが、コマンドワードがない限り、すべての変数の割り当てはシェル環境に影響します。

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

ただし、変数の割り当てがコマンド名に付加されている場合(これを「プレフィックス割り当て」と呼びます)、シェル環境に影響せ

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

bashマニュアルからの関連する引用:

コマンド名が表示されない場合、変数の割り当ては現在のシェル環境に影響します。 それ以外の場合、変数は実行されたコマンドの環境に追加され、現在のシェル環境には影響しません。

変数割り当てのこの機能を利用して、 $IFS一時的にのみ変更することができます。これにより、 $OIFS変数で行われているような保存と復元のギャンビット全体を回避できます。最初のバリアント。 ただし、ここで直面する課題は、実行する必要のあるコマンド自体が単なる変数割り当てであるため、 $IFS割り当てを一時的にするためのコマンドワードを必要としないことです。 : builtinようなステートメントにno-opコマンドワードを追加して、 $IFS割り当てを一時的にしないのはなぜでしょうか? $array割り当ても一時的になるため、これは機能しません。

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

ですから、私たちは事実上行き詰まっていて、ちょっとしたキャッチ22です。 ただし、 evalがコードを実行すると、通常の静的ソースコードであるかのようにシェル環境で実行されるため、 eval内で$array割り当てを実行できます。シェル環境で有効にするためのeval引数。一方、 evalコマンドのプレフィックスとなる$IFSプレフィックス割り当ては、 evalコマンドよりも長持ちしません。 これはまさに、このソリューションの2番目のバリアントで使用されているトリックです。

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

したがって、ご覧のとおり、これは実際には非常に巧妙なトリックであり、(少なくとも割り当ての有効化に関して)必要なことをかなり非自明な方法で正確に実行します。 evalが関与しているにもかかわらず、私は実際にはこのトリック全般に反対していません。 セキュリティの脅威から保護するために、引数文字列を一重引用符で囲むように注意してください。

しかし、繰り返しになりますが、「すべての世界で最悪の」問題の集積のため、これは依然としてOPの要件に対する間違った答えです。


間違った答え#6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

えっと…なに? OPには、配列に解析する必要のある文字列変数があります。 この「答え」は、配列リテラルに貼り付けられた入力文字列の逐語的な内容から始まります。 それが一つの方法だと思います。

回答者は、 $IFS変数がすべてのコンテキストのすべてのbash解析に影響を与えると想定しているようですが、これは正しくありません。 bashマニュアルから:

IFS拡張後の単語分割、および組み込みコマンドの読み取りを使用して行を単語に分割するために使用される内部フィールド区切り記号。 デフォルト値は<space> <tab> <newline>です。

したがって、 $IFS特殊変数は、実際には2つのコンテキストでのみ使用されます。(1)展開後に実行される単語分割(bashソースコードを解析するときではないことを意味します)と(2)入力行をreadによって単語に分割するため

これをより明確にしようと思います。 解析実行を区別するのは良いことだと思います。 Bashは、最初にソースコードを解析する必要があります。これは明らかに解析イベントであり、その後、コードを実行します。これは、拡張が全体像に現れるときです。 拡張は実際には実行イベントです。 さらに、上で引用した$IFS変数の説明に問題があります。 単語分割は拡張後に実行されると言うのではなく、単語分割は拡張中に実行される、またはおそらくもっと正確に言えば、単語分割は拡張プロセスの一部であると言えます。 「単語分割」というフレーズは、この拡張ステップのみを指します。 残念ながら、ドキュメントでは「split」や「words」という単語が頻繁に使用されているようですが、bashソースコードの解析を参照するために使用しないでください。 これは、 linux.die.netバージョンのbashマニュアルからの関連する抜粋です。

展開は、単語に分割された後、コマンドラインで実行されます。 実行される展開にはチルダ展開パラメーターと変数の展開コマンド置換算術展開単語分割パス名展開の7種類があります

拡張の順序は次のとおりです。ブレース拡張。 チルダ展開、パラメーターと変数の展開、算術展開、およびコマンド置換(左から右の方法で実行)。 単語分割; およびパス名の展開。

拡張セクションの最初の文で「単語」ではなく「トークン」という単語を選択しているため、 GNUバージョンのマニュアルの方がわずかに優れていると言えます。

拡張は、トークンに分割された後、コマンドラインで実行されます。

重要な点は、 $IFSはbashがソースコードを解析する方法を変更しないということです。 bashソースコードの解析は、実際には非常に複雑なプロセスであり、コマンドシーケンス、コマンドリスト、パイプライン、パラメーター展開、算術置換、コマンド置換など、シェル文法のさまざまな要素の認識が含まれます。 ほとんどの場合、bash解析プロセスは、変数の割り当てなどのユーザーレベルのアクションでは変更できません(実際には、このルールにはいくつかの小さな例外があります。たとえば、さまざまなcompatxxシェル設定を参照してください。変更される可能性があります。オンザフライでの動作の解析の特定の側面)。 この複雑な構文解析プロセスの結果として生じる上流の「単語」/「トークン」は、上記のドキュメントの抜粋に分類されている「拡張」の一般的なプロセスに従って拡張されます。言葉はそのプロセスの単なる一歩です。 単語分割は、前の拡張ステップから吐き出されたテキストにのみ触れます。 ソースバイトストリームからすぐに解析されたリテラルテキストには影響しません。


間違った答え#7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

これは最良の解決策の1つです。 read使用に戻ったことに注意してください。 readは、必要なのが1つだけの場合に、2つのレベルの分割を実行するため、不適切であると前に言いませんでしたか? ここでの秘訣は、 readを呼び出すことができるため、効果的に1レベルの分割のみを実行できることです。具体的には、呼び出しごとに1つのフィールドのみを分割するため、ループ。 手先の早業ですが、機能します。

しかし、問題があります。 最初に: readに少なくとも1つのNAME引数を指定すると、入力文字列から分割された各フィールドの先頭と末尾の空白が自動的に無視されます。 これは、この投稿で前述したように、 $IFSがデフォルト値に設定されているかどうかに関係なく発生します。 現在、OPは特定のユースケースではこれを気にしない可能性があり、実際、これは解析動作の望ましい機能である可能性があります。 しかし、文字列をフィールドに解析したいすべての人がこれを望んでいるわけではありません。 ただし、解決策があります。 readやや自明ではない使用法は、ゼロのNAME引数を渡すことです。 この場合、 readは、入力ストリームから取得した入力行全体を$REPLYという名前の変数に格納し、ボーナスとして、先頭と末尾の空白を削除しません。値。 これは、シェルプログラミングのキャリアで頻繁に利用したread非常に堅牢な使用法です。 動作の違いのデモンストレーションは次のとおりです。

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

このソリューションの2番目の問題は、OPのコンマスペースなどのカスタムフィールドセパレータのケースに実際には対応していないことです。 以前と同様に、複数文字の区切り文字はサポートされていません。これは、このソリューションの残念な制限です。 -dオプションに区切り文字を指定することで、少なくともコンマで分割することができますが、どうなるか見てみましょう。

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

予想通り、説明されていない周囲の空白がフィールド値に取り込まれたため、これは後でトリミング操作によって修正する必要があります(これはwhileループで直接行うこともできます)。 しかし、別の明らかなエラーがあります:ヨーロッパが欠けています! それがどうなったのか? 答えは、 readがファイルの終わり(この場合は文字列の終わりと呼ぶことができます)に達した場合、最後のフィールドで最後のフィールドターミネータに遭遇することなく、失敗したリターンコードを返すということです。 これにより、whileループが途中で中断し、最後のフィールドが失われます。

技術的には、これと同じエラーが前の例にも影響を及ぼしました。 違いは、フィールド区切り文字がLFであると見なされたことです。これは、 -dオプションを指定しない場合のデフォルトであり、 <<< ( "here-string")メカニズムが自動的に指定されます。コマンドへの入力として文字列をフィードする直前に、文字列にLFを追加します。 したがって、これらの場合、入力にダミーのターミネータを無意識に追加することで、最終フィールドがドロップされるという問題を誤って解決しました。 このソリューションを「ダミーターミネーター」ソリューションと呼びましょう。 here-stringでインスタンス化するときに、入力文字列に対してダミーターミネーターソリューションを連結することにより、カスタム区切り文字に手動でダミーターミネーターソリューションを適用できます。

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

そこで、問題は解決しました。 別の解決策は、(1) readが失敗を返し、(2) $REPLYが空の場合、つまりreadが文字を読み取れなかった場合にのみ、whileループを中断することです。ファイルの終わりに到達する前。 デモ:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

このアプローチでは、 <<<リダイレクト演算子によってhere-stringに自動的に追加される秘密のLFも明らかになります。 もちろん、先ほど説明したように、明示的なトリミング操作によって個別に取り除くこともできますが、手動のダミーターミネーターアプローチで直接解決できるので、それで問題ありません。 手動のダミーターミネーターソリューションは、これら2つの問題(ドロップファイナルフィールド問題と追加LF問題)の両方を一度に解決するという点で、実際には非常に便利です。

したがって、全体として、これは非常に強力なソリューションです。 唯一残っている弱点は、複数文字の区切り文字がサポートされていないことです。これについては後で説明します。


間違った答え#8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(これは実際には#7と同じ投稿からのものです。回答者は同じ投稿で2つの解決策を提供しました。)

mapfile同義語であるreadarrayビルトインが理想的です。 これは、バイトストリームを1回のショットで配列変数に解析する組み込みコマンドです。 ループ、条件、置換、またはその他のものをいじることはありません。 また、入力文字列から空白を密かに削除することはありません。 そして( -Oが指定されていない場合)、ターゲット配列に割り当てる前に、ターゲット配列を簡単にクリアします。 しかし、それはまだ完璧ではないので、「間違った答え」としての私の批判。

まず、これを邪魔にならないようにするために、フィールド解析を行うときのreadの動作と同様に、 readarrayは、末尾のフィールドが空の場合にそれを削除することに注意してください。 繰り返しになりますが、これはおそらくOPにとっては問題ではありませんが、一部のユースケースでは問題になる可能性があります。 すぐにこれに戻ります。

次に、以前と同様に、複数文字の区切り文字をサポートしていません。 これについてもすぐに修正します。

第3に、記述されたソリューションはOPの入力文字列を解析せず、実際、そのまま使用して解析することはできません。 これについても少し詳しく説明します。

上記の理由から、私はこれがOPの質問に対する「間違った答え」であると今でも考えています。 以下に、私が正しいと考えるものを示します。


正しい答え

-dオプションを指定するだけで、 #8を機能させる素朴な試みを次に示します。

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

結果は、 #7で説明したループreadソリューションの二重条件付きアプローチから得られた結果と同じであることがわかります。 手動のダミーターミネーターのトリックでこれをほぼ解決できます。

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

ここでの問題は、 <<<リダイレクト演算子が入力文字列にLFを追加したため、 readarrayが末尾のフィールドを保持し、したがって末尾のフィールドが空ではなかった(そうでない場合は削除される)ことです。 )。 事後に最終的な配列要素を明示的に設定解除することで、これを処理できます。

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

実際に関連している残りの2つの問題は、(1)トリミングする必要のある余分な空白と、(2)複数文字の区切り文字のサポートの欠如です。

もちろん、空白は後でトリミングすることもできます(たとえば、 Bash変数から空白をトリミングする方法を参照してください)。 しかし、複数文字の区切り文字をハックできれば、両方の問題を1回で解決できます。

残念ながら、複数文字の区切り文字を機能させる直接的な方法はありません。 私が考えた最善の解決策は、入力文字列を前処理して、複数文字の区切り文字を、入力文字列の内容と衝突しないことが保証される1文字の区切り文字に置き換えることです。 この保証がある唯一の文字はNULバイトです。 これは、bashでは(ちなみにzshではありませんが)、変数にNULバイトを含めることができないためです。 この前処理ステップは、プロセス置換でインラインで実行できます。 awkを使用してそれを行う方法は次のとおりです。

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

ついに! このソリューションは、誤って中央のフィールドを分割したり、途中で切り取ったり、空のフィールドを削除したり、ファイル名の展開時にそれ自体を破損したり、先頭と末尾の空白を自動的に削除したり、最後に密航したLFを残したりしません。ループを必要とせず、1文字の区切り文字で解決しません。


トリミングソリューション

最後に、 readarrayのあいまいな-C callbackオプションを使用して、独自のかなり複雑なトリミングソリューションを示したいと思いました。 残念ながら、Stack Overflowの厳格な30,000文字の投稿制限に対してスペースが足りなくなったため、説明できません。 読者の練習問題として残しておきます。

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
Jim Ho picture
2013年03月14日
230

IFSを設定しない方法は次のとおりです。

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

アイデアは文字列置換を使用することです:

${string//substring/replacement}

$ substringのすべての一致を空白に置き換えてから、置換された文字列を使用して配列を初期化します。

(element1 element2 ... elementN)

注:この回答では、 split + glob演算子を使用しています。 したがって、一部の文字( * )の展開を防ぐために、このスクリプトのグロビングを一時停止することをお勧めします。

Jmoney38 picture
2015年07月14日
103
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

3つ印刷します

user2350426 picture
2015年07月25日
32

受け入れられた回答は、1行の値に対して機能します。
変数に複数の行がある場合:

string='first line
        second line
        third line'

すべての行を取得するには、非常に異なるコマンドが必要です。

while read -r line; do lines+=("$line"); done <<<"$string"

または、はるかに単純なbash readarray

readarray -t lines <<<"$string"

printf機能を利用すると、すべての行を簡単に印刷できます。

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]
Luca Borrione picture
2012年11月02日
30

特にセパレータがキャリッジリターンの場合、受け入れられた回答に記載されている方法が機能しないことが時々ありました。
そのような場合、私は次のように解決しました。

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done
ssanch picture
2016年06月03日
8

これはJmoney38によるアプローチに似ていますが、sedを使用しています。

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

プリント1

Romi Erez picture
2020年08月05日
8

macOSを使用していて、readarrayを使用できない場合は、これを簡単に行うことができます-

MY_STRING="string1 string2 string3"
array=($MY_STRING)

要素を反復処理するには:

for element in "${array[@]}"
do
    echo $element
done
dawg picture
2017年11月27日
6

文字列を配列に分割するための鍵は、 ", "の複数文字の区切り文字です。 IFSは文字列ではなく文字のセットであるため、複数文字の区切り文字にIFSを使用するソリューションは本質的に間違っています。

IFS=", "を割り当てると、文字列は","または" "いずれか、または", " 2文字の区切り文字を正確に表していないそれらの任意の組み合わせで壊れます。

awkまたはsedを使用して、プロセス置換を使用して文字列を分割できます。

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Bashで直接正規表現を使用する方が効率的です。

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

2番目の形式では、サブシェルがなく、本質的に高速になります。


bgoldstによる編集:これは、私のreadarrayソリューションをdawgの正規表現ソリューションと比較するいくつかのベンチマークです。また、 readソリューションも含めました(注:正規表現ソリューションを少し変更して、私の解決策との調和)(投稿の下の私のコメントも参照してください):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("[email protected]");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##
To Kra picture
2019年05月15日
6

これはOSXで私のために働きます:

string="1 2 3 4 5"
declare -a array=($string)

文字列の区切り文字が異なる場合は、最初にそれらをスペースに置き換えます。

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

シンプル:-)