Bashプログラミング
 Computer >> コンピューター >  >> プログラミング >> Bashプログラミング

あなたが知らないBash:Bash配列入門

ソフトウェアエンジニアは開発の多くの場面でコマンドラインを日常的に使用しますが、配列はおそらくコマンドラインの機能の中でも比較的知られていないものの一つでしょう(正規表現演算子 =~ ほどではありませんが)。その難解さと疑わしい構文はさておき、Bash配列は非常に強力なツールになり得ます。

なぜBash配列なのか?

Bashについて書くのは難しいものです。記事が構文の奇妙さに焦点を当てたマニュアルに堕してしまうのは驚くほど簡単だからです。ご安心ください。本記事の目的は、あなたがマニュアルを読む必要(RTFM)なく理解できるようにすることです。

実際に役立つ実例

そこで、現実的なシナリオとBashがどのように役立つかを考えてみましょう。あなたが社内データパイプラインの実行時間を評価・最適化する新しい取り組みを主導しているとします。第一歩として、パラメータスイープを行い、パイプラインがスレッドをどれだけ効果的に活用できるかを評価したいとします。簡単のため、パイプラインをコンパイル済みのC++ブラックボックスとして扱い、調整可能な唯一のパラメータがデータ処理用に確保するスレッド数であるとします:./pipeline --threads 4

基本

まず、テストしたい --threads パラメータの値を含む配列を定義します:

allThreads=(1 2 4 8 16 32 64 128)

この例ではすべての要素が数値ですが、そうである必要はありません。Bashの配列には数値と文字列の両方を含めることができます。例えば myArray=(1 2 "three" 4 "five") は有効な式です。他のBash変数と同様に、等号の前後にスペースを入れないでください。スペースがあると、Bashは変数名を実行するプログラムとみなし、= をその最初のパラメータとして扱ってしまいます!

配列を初期化したら、いくつかの要素を取得してみましょう。単に echo $allThreads とすると、最初の要素だけが出力されることに気づくでしょう。

なぜそうなるのか理解するために、一歩引いてBashで変数を出力する通常の方法を振り返ってみましょう。以下のシナリオを考えてください:

type="article"
echo "Found 42 $type"

変数 $type が単数形で与えられ、文の末尾に s を付けたいとします。単に $types を追加することはできません。それは別の変数 $types になってしまうからです。echo "Found 42 "$type"s" のようなコードの捻じ曲げも可能ですが、この問題を解決する最良の方法は波括弧を使うことです:echo "Found 42 ${type}s"。これにより、変数名の開始と終了をBashに伝えることができます(興味深いことに、これはJavaScript/ES6でテンプレートリテラルに変数や式を埋め込む際に使われる構文と同じです)。

つまり、Bash変数は一般的に波括弧を必要としませんが、配列の場合は必須です。その結果、インデックスを指定してアクセスできるようになります。例えば echo ${allThreads[1]} は配列の2番目の要素を返します。波括弧を含めない場合、echo $allThreads[1] のようにすると、Bashは [1] を文字列として扱って出力してしまいます。

そう、Bash配列の構文は奇妙ですが、少なくともゼロベースのインデックスです。一部の他の言語とは違います(R のことを見ていますよ)。

配列をループ処理する

上記の例では配列に整数インデックスを使用しましたが、それが当てはまらない2つのケースを考えてみましょう。第一に、$i 番目の要素を取得したい場合、$i が対象のインデックスを含む変数であれば、echo ${allThreads[$i]} でその要素を取得できます。第二に、配列のすべての要素を出力するには、数値インデックスの代わりに @ シンボルを使用します(@all を表すと考えられます):echo ${allThreads[@]}

配列要素をループ処理する

これを踏まえて、$allThreads をループし、各 --threads の値でパイプラインを起動してみましょう:

for t in ${allThreads[@]}; do
  ./pipeline --threads $t
done

配列インデックスをループ処理する

次に、少し異なるアプローチを考えてみます。配列の 要素 ではなく、配列の インデックス をループさせる方法です:

for i in ${!allThreads[@]}; do
  ./pipeline --threads ${allThreads[$i]}
done

これを分解してみましょう:前述の通り、${allThreads[@]} は配列のすべての要素を表します。ここにエクスクラメーションマークを追加して ${!allThreads[@]} とすると、すべての配列インデックスのリストが返されます(この場合は0から7)。つまり、for ループはすべてのインデックス $i をループし、$allThreads から $i 番目の要素を読み取って --threads パラメータの値に設定しています。

この書き方は目に厳しいものがあります。なぜわざわざ紹介するのか疑問に思うかもしれません。それは、ループ内でインデックスと値の両方を知る必要がある場面があるからです。例えば、配列の最初の要素を無視したい場合、インデックスを使えば、別の変数を作ってループ内でインクリメントするといった手間が省けます。

配列に値を詰め込む

ここまでで、各 --threads の値に対してパイプラインを起動できるようになりました。ここで、パイプラインの出力が秒単位の実行時間だと仮定します。この出力を各イテレーションで取得し、別の配列に保存して、後でさまざまな操作ができるようにしたいとします。

便利な構文

コードに入る前に、さらにいくつかの構文を導入する必要があります。まず、Bashコマンドの出力を取得する方法です。以下の構文を使用します:output=$( ./my_script.sh )。これにより、コマンドの出力が変数 $output に格納されます。

次に必要なのは、取得した値を配列に追加する方法です。見慣れた構文になります:

myArray+=( "newElement1" "newElement2" )

パラメータスイープ

すべてを組み合わせると、パラメータスイープを実行するスクリプトは以下のようになります:

allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
  runtime=$(./pipeline --threads $t)
  allRuntimes+=( $runtime )
done

これで完成です!

他には何がある?

この記事では、パラメータスイープに配列を使用するシナリオを扱いました。しかし、Bash配列を使う理由は他にもあります。さらに2つの例を紹介します。

ログアラート

このシナリオでは、アプリがモジュールごとに分かれており、それぞれに独自のログファイルがあります。特定のモジュールでトラブルの兆候が見られたとき、適切な担当者にメールを送るcronジョブスクリプトを書くことができます:

# ログファイルと通知先のリスト
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")

# 各ログでトラブルの兆候を探す
for i in ${!logPaths[@]};
do
  log=${logPaths[$i]}
  stakeholder=${logEmails[$i]}
  numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )

  # 最近5件以上のエラーが見つかったら関係者に警告
  if [[ "$numErrors" -gt 5 ]];
  then
    emailRecipient="$stakeholder"
    emailSubject="WARNING: ${log} showing unusual levels of errors"
    emailBody="${numErrors} errors found in log ${log}"
    echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
  fi
done

APIクエリ

Mediumの投稿で最も多くコメントしているユーザーについて分析を生成したいとします。直接データベースにアクセスできないためSQLは使えませんが、APIなら使えます!

API認証とトークンについて長々と議論するのは避けたいので、代わりにパブリックなAPIテストサービスであるJSONPlaceholderをエンドポイントとして使用します。各投稿をクエリしてコメントした全員のメールアドレスを取得し、それらを結果配列に追加します:

endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()

# 最初の10投稿をクエリ
for postId in {1..10};
do
  # この投稿のコメント投稿者のメールを取得するAPIコール
  response=$(curl "${endpoint}?postId=${postId}")

  # jqを使ってJSONレスポンスを配列としてパース
  allEmails+=( $( jq '.[].email' <<< "$response" ) )
done

ここではコマンドラインからJSONをパースするために jq ツールを使用しています。jq の構文は本記事の範囲外ですが、ぜひ調べてみることを強くお勧めします。

想像できる通り、Bash配列が役立つシナリオは他にも無数にあります。本記事で概説した例が、あなたの思考の糧になれば幸いです。あなた自身の仕事からの他の例があれば、ぜひコメントで共有してください。

まだまだあります!

この記事ではかなりの量の配列構文を扱いました。ここで、扱った内容のまとめと、扱わなかったより高度なトリックをいくつか紹介します:

構文 結果
arr=()空の配列を作成
arr=(1 2 3)配列を初期化
${arr[2]}3番目の要素を取得
${arr[@]}すべての要素を取得
${!arr[@]}配列のインデックスを取得
${#arr[@]}配列のサイズを計算
arr[0]=31番目の要素を上書き
arr+=(4)値を追加
str=$(ls)ls の出力を文字列として保存
arr=( $(ls) )ls の出力をファイルの配列として保存
${arr[@]:s:n}インデックス s から n 個の要素を取得

最後にひとこと

見てきた通り、Bash配列の構文は確かに奇妙です。しかし、この記事でそれが非常に強力であることを納得していただけたなら幸いです。一度構文に慣れてしまえば、Bash配列を頻繁に使うようになるでしょう。

BashかPythonか?

ここで疑問が生じます:Pythonのような他のスクリプト言語ではなく、Bash配列を使うべきなのはいつか?

私にとっては、すべて「依存関係」に帰着します。手元の問題がコマンドラインツールの呼び出しだけで解決できるなら、Bashを使うのが筋です。しかし、スクリプトがより大きなPythonプロジェクトの一部であるなら、Pythonを使うのが筋です。

例えば、パラメータスイープをPythonで実装することもできましたが、結局はBashのラッパーを書くことになったでしょう:

import subprocess

all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []

# 各スレッド数でパイプラインを起動
for t in all_threads:
  cmd = './pipeline --threads {}'.format(t)

  # subprocessモジュールを使って出力を取得
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  output = p.communicate()[0]
  all_runtimes.append(output)

この例ではコマンドラインを避けて通れない以上、直接Bashを使うのが望ましいと言えます。

恥知らずな宣伝の時間

この記事は、OSCONで行ったトークに基づいています。そこで私は You Don't Know Bash というライブコーディングワークショップを発表しました。スライドもクリッカーもなし、ただ私と観客がコマンドラインでタイプし続け、Bashの不思議で素晴らしい世界を探検しました。

  1. 知らないと損をする?USBメモリの意外な活用術5選

    パソコン間でデータを移動するとき、真っ先に思い浮かぶのがUSBメモリ(ペンドライブ)ではないでしょうか。確かに現代ではストレージ技術は大きく進化しましたが、今でも多くの人がUSBメモリをデータ転送だけでなく、保存用としても活用しています。 しかし、USBメモリの用途は単なる記録媒体にとどまりません。実はもっと多彩で、パソコンのロック・解除やPC性能の向上など、さまざまな便利な使い方ができるのです。 信じられないという方は、ぜひこの記事を読んで、知らなかったUSBメモリの5つの活用法をチェックしてみてください。 1. パソコンをロック・ロック解除する データセキュリティは今や必須の時代です。物理

  2. Wi-Fi 6(802.11ax)とは?特徴・メリット・導入のポイントを徹底解説

    テクノロジーの世界では毎年のように新しい驚きが登場しますが、その中でも特に注目すべき存在が「Wi-Fi 6」です。Wi-Fi 6は次世代の無線通信規格で、正式名称は「802.11ax」。従来のWi-Fiに比べて通信速度と性能が大幅に向上しており、特に多くの機器が接続される混雑した環境でこそ真価を発揮します。スマートフォンやタブレット、PCなどデジタル機器が生活に深く浸透するほど、この新世代Wi-Fiの存在は大きな朗報となるでしょう。また、Wi-Fi 6では新しい命名規則が採用され、デバイス上に「Wi-Fi 5」「Wi-Fi 6」といったバージョン番号が表示されるようになりました。これにより、ど