1. 序章

配列はアイテムのコレクションであり、それぞれが一意のキーで識別されます。 アイテム自体が配列である場合、2次元(2D)配列があります。

このチュートリアルでは、LinuxでのBashの2Dアレイの概念について詳しく説明します。 具体的には、さまざまな配列タイプの一般的な構造から始めます。 次に、Bashがその構造をどのように処理するかについて説明します。 次に、2Dアレイのさまざまなシミュレーションに取り組みます。 最後に、いくつかの一般的な警告で締めくくります。

このチュートリアルのコードは、GNU Bash5.0.3を使用したDebian10.10(Buster)でテストしました。 これはPOSIXに準拠しており、そのような環境で機能するはずです。

2. 配列

通常の1次元(1D)配列は、インデックス付きのデータの行または列です

[0][1][2][3]
(a)(b)(c)(d)

ここでは、各項目( element )が括弧で囲まれています。 その上には、角かっこで囲まれた一意のインデックス( key )があります。

特定の要素にアクセスするには、そのインデックスとベースを使用します。 ベースは配列の名前です。 たとえば、配列の item0aと同じです。

アイテムは、サポートされている任意のデータ型にすることができます。

2.1. 2次元配列

次のように2D配列を作成できます。

   [0][1][2][3]
[0](a)(b)(c)(d)
[1](A)(B)(C)(D)

基本的に、元の配列の項目0は列aA –配列になります。 2番目のインデックスを使用してその配列の要素にアクセスします。 たとえば、 item[0,1]Aです。 他のアイテムについても状況は同様です。

2D配列の行と列の長さは、多くの場合固定定数です。

2.2. ハイブリッドアレイ

配列の次元が固定されているため、前のセクションの2D配列を変換できます。

[0][1][2][3][4][5][6][7]
(a)(b)(c)(d)(A)(B)(C)(D)

2行4列の2D配列を8(2 * 4)アイテムの1Dリストにマッピングしました。 要素のインデックスを計算するには、式(COLUMNS * row + col)を使用します。 COLUMNS要素のrow行を「スキップ」します。 その後、現在の行のcol要素を取得します。

重要なことに、インデックスはキーとも呼ばれます。 一部のプログラミング言語では、キーのデータ型はアイテムのデータ型とは異なる場合があります。

ほとんどすべてのプログラミング言語には、配列データ型が含まれています。 実際、ほとんどの弱く型付けされた言語で配列を宣言する方法があります。 Bashも例外ではありません。

Bashの配列アイテムは、arrayを除いて何でもかまいません。 では、Bashで2D配列を作成するにはどうすればよいですか?

4. 2DBash配列

ここでは、2D配列の定義を最小限に抑えます。 この記事では、2つの別々の値を使用して一貫してインデックスを作成できる任意の配列です。

Bashはそのままの状態で2D配列をサポートしていないため、独自の使用方法を考え出す必要があります。 一般的な実装を見てみましょう。

4.1. アイテム2Dアレイシミュレーション

Bashでは、配列は区切り文字を使用した文字列とのみ区別できます。 配列をアイテムとして持つ最も簡単な方法の1つは、その場で文字列から配列を変換することです。

$ sep=','
$ declare -a alpha=()
$ alpha+=("a${sep}b")
$ alpha+=("c${sep}d")
$ row=0
$ col=1
$ IFS="$sep" read -ra alpharow < <(printf '%s' "${alpha[$row]}")
$ echo "${alpharow[$col]}"
b

最初に、列区切り文字 $sepとしてコンマを選択します。 次に、配列が定義され、行ごとに追加されます。 この場合、最初の行は “a、b” で、2番目の行は “c、d”です。

構造的には、 IFS (内部フィールドセパレーター)の特殊なシェル変数が機能します。 デフォルト値には、スペース、タブ、および改行が含まれます。 IFS を使用すると、readなどの組み込み関数で配列項目を区切ることができます。

最後に、上記のスニペットは、 $row$col を使用して、2つのステップではありますが、メイン配列にインデックスを付けます。 まず、行全体 $row$sepで新しい配列$alpharowに分割されます。 その後、 $alpharowは列$col でインデックス付けされ、要求された要素を返します。 定数配列の場合、これで十分です。

明らかに、区切り文字$sepを通常の文字として直接使用することはできません。 この影響は、複数の方法で最小限に抑えることができます。

  • 区切り文字としてレア文字またはエスケープ文字を使用する
  • それぞれの実際の非区切り区切り文字を2倍/乗算またはその他の方法で拡張し、それに応じて処理します
  • 文字の組み合わせを区切り文字として使用する

次に、この特定の欠点に悩まされない実装について説明します。

4.2. 連想2Dアレイシミュレーション

Bashの連想配列の例を次に示します。

$ declare -A alpha=([0,0]=a [0,1]=b [1,0]=c [1,1]=d)
$ echo "${alpha[@]}"
c d b a

2つのインデックスの外観をどのように与えるかに注目してください。 したがって、理論的には、次のような配列を想像できます。

   [0][1]
[0](a)(b)
[1](c)(d)

実際には、これらのインデックスは単なる文字列キーです。

$ for k in "${!alpha[@]}"; do
  printf "alpha[%s]=%s\n" "$k" "${alpha[$k]}"
done
alpha[1,0]=c
alpha[1,1]=d
alpha[0,1]=b
alpha[0,0]=a
$ echo "${alpha["1,0"]}"
c

ループでキーを生成することも、数値インデックスのように計算することもできます

ただし、わずかな逸脱でも問題が発生します。

$ for (( col=0; col<2; col++ )); do
  printf "alpha["1,%s"]=%s\n" "$col" "${alpha["1,$col"]}"
done
alpha[1,0]=c
alpha[1,1]=d
$ for (( col=0; col<2; col++ )); do
  printf "alpha["1,%s"]=%s\n" "$col" "${alpha["1, $col"]}"
done
alpha[1,0]=
alpha[1,1]=

これら2つのループの違いは非常に微妙です。つまり、コンマと $colの間のスペースです。

すでに複雑な構造を管理する場合、そのような複雑さは役に立ちません。 このため、よりシンプルで普遍的なソリューションが存在します。

4.3. ハイブリッド2Dアレイシミュレーション

上記の構造を通常の配列として宣言できます。

$ ROWS=2
$ COLUMNS=2
$ declare -a alpha=()
$ alpha+=(a b)
$ alpha+=(c d)
$ echo "${alpha[@]}"
a b c d

これまでのところ、正確な寸法がわかっている通常の1D配列( COLUMNS ROWS )があります。 2Dに完全に「変換」するには、インデックスの計算も変更する必要があります。

$ row=0
$ col=1
$ index=$((COLUMNS*$row+$col))
$ echo "${alpha[$index]}"

上記の式についてはすでに説明しました。 連想配列の文字列とは異なり、ここでのインデックスは数値のままです。 数値演算の構文はあまり敏感ではないため、これは明らかな利点です。 スペース、先行ゼロ、およびその他のそのようなアーティファクトは、計算をつまずかせません。

1次元2D配列エミュレーションが機能するには、次の2つの条件があります。

  • 配列は正確な「長方形」にマッピング可能である必要があります。 COLUMNS * ROWS = ELEMENTS
  • アレイの正確な寸法は事前にわかっています

これらの制限を克服するには、ある程度の自由を犠牲にする必要があります。 そのために、すでに説明した2つの方法を組み合わせることができます。

4.4. 複雑な2Dアレイの実装

区切り文字に2文字をあきらめる可能性があります。 たとえば、行の場合はコンマ、列の場合は改行です。 これらの条件下では、2D配列全体を最初は文字列にすることができます。

$ alpharaw='a,b,c,d
e,f,g
h,i,,k
l,,n,o'

次に、 readarray を使用して、「arraytobe」文字列$alpharawの行を新しい配列に分離します。

$ readarray -t -d $'\n' alpharows < <(printf '%s' "$alpharaw")

最後に、要素の読み取り、書き込み、削除、挿入のためのBash関数を作成します。 読み取り関数の例のみを提供します。 上記のセクションで説明した概念を使用します。

$ get_element () {
  alpharaw="$1"
  row="$2"
  col="$3"
  sep="${4:-,}"
  local alpharow alpharows
  IFS=$'\n' readarray -t alpharows < <(printf '%s' "$alpharaw")
  if [[ $row -ge ${#alpharows[@]} ]]; then
    echo "Bad row."
    return
  fi
  IFS="$sep" read -ra alpharow < <(printf '%s' "${alpharows[$row]}")
  if [[ $col -ge ${#alpharow[@]} ]]; then
    echo "Bad column."
    return
  fi
  echo "${alpharow[$col]}"
}
$ get_element "$alpharaw" 3 2
n

実装はより複雑ですが、その使用法ははるかに堅牢です。 問題が発生する可能性ははるかに低くなります。

5. 警告

前のセクションで実装の欠点についてはすでに説明しました。 簡潔にするために、主なものを列挙します

  • 実装の複雑さ
  • パフォーマンスの問題
  • スパース配列
  • 複雑で壊れやすい構文
  • セパレータのエスケープ

もちろん、2D配列は複雑な構造であるため、このリストは完全ではありません。

実装を選択するときは、ニーズに合わせてその長所と短所を考慮する必要があります。

6. 概要

このチュートリアルでは、Linuxでの2Dアレイについて説明しました。

まず、配列とその型は一般的に、そしてBashで定義されました。 さらに、2DアレイのさまざまなBash実装と、その欠点についても調査しました。 最後に、いくつかの一般的な警告をリストしました。

Bashには2Dアレイのサポートがありませんが、同様の効果を実現する方法があります。 でも, どの実装も完全に堅牢ではありません.