Help us understand the problem. What is going on with this article?

標準入力を1行ずつ配列変数に入れるまとめ

More than 5 years have passed since last update.

bashで標準入力を1行ずつ配列変数に入れるのって、なんか色々気にすることが多くて面倒くさいから一つの関数にまとめてみた。
(こんな変態関数作るほうが余程めんどくさいって?うんその通りだと思うわ。でも知見を得たら頭整理の為にも一度書き残しておかないとすぐ忘れるし、もうそういう性なので仕方ない。)

こんな関数を作って

read2arr.sh
# 標準入力を指定の変数名に1行1値の配列として読み込む関数
read2arr() {
  [[ $1 =~ ^[a-zA-Z][a-zA-Z0-9_]*$ ]] || return 1
  local IFS=
  eval "local $1_"
  eval "$1=()"
  eval "while read -r $1_ || [[ -n \$$1_ ]]; do $1+=(\"\$$1_\"); done"
}

こんなふうに使う

$ # 関数を読み込んでおく
$ . read2arr.sh

$ # テスト用ファイルを作る(空行とか行頭行末の空白とかタブ文字やクオート入りとか最終改行無しとか嫌な感じのデータを作る)
$ echo foo > /tmp/file1
$ echo >> /tmp/file1
$ echo $'\ta  b  ' >> /tmp/file1
$ echo 'a\tb\\n' >> /tmp/file1
$ echo "a\"b'c" >> /tmp/file1
$ echo -n bar >> /tmp/file1

$ # arr という名前の変数にfile1の内容を配列として読み込む
$ read2arr arr < /tmp/file1

$ # 確認
$ echo "行数: ${#arr[@]}"
行数: 4
$ # 中身確認
$ for v in "${arr[@]}"; do printf "%s\n" "'$v'"; done | cat -n
     1  'foo'
     2  ''
     3  '   a  b  '
     4  'a\tb\\n'
     5  'a"b'c'
     6  'bar'

今時間ないので説明が少ないがとりあえずメモってことで…。多分もうそろそろ考慮漏れとか無いんじゃないかな。

追記:簡単な解説

注意すべきreadの仕様について

local IFS= または IFS= read

これをしておかないと read 時に行頭行末の空白文字が消えてしまうので注意。そのトリム動作が嬉しい事も多いけどとにかくそのまま読みたい場合は必須。

例えば上記使用例の3行目はデフォルトのIFSのままだと a b の4文字になってしまう。なお行頭行末以外のaとbの間の空白2文字はそのまま残る。
定型句として while IFS= read -r t; do という風に read の瞬間だけIFSをキャンセルしても良いが、今回のケースでは関数内全体で一度だけリセットしても他にIFSが影響する部分はなく問題が起こる余地はないので頭でやっている。

参考: readするときはIFS=を付けておくとstrictな感じで気持ちが良い - Qiita

read -r でバックスラッシュをそのまま読み込む

例えば上記実行例の4行目の値をreadするときは -r が無いとバックスラッシュはエスケープされて atb\n という5文字に減ってしまう。
-rを付けておけば a\tb\\n という7文字として読み込まれてバックスラッシュは文字としてそのまま残る。

while read t; do ...; done だと入力の最終行に改行がないとハマる

readは改行前にEOFを見つける(最終改行無し)だと return 1 する仕様なので、whileがそこで終わってしまって最後の1行分のwhile内処理がスキップされてしまう。その結果、最終行が欠損するという問題が起こり得る。
対策として、while read t || [[ -n $t ]]; do ...; done という風に最終行でreadreturn 1された場合に備えて空チェックも平行して行うことで最終行の処理漏れを防ぐ。

readは最終改行がないとreturn 1をするものの、最終行終端までの読み込み自体はちゃんと行ってくれているので、代入先変数tの値チェックを追加することでwhileの予期せぬ離脱を回避してるわけだ。なお只の空行の場合はread0を返すので後続の || [[ -n $t ]] は評価されず、条件文全体としては0で終わるので期待通りの動作となる。

多分readのこの行末処理の仕様は libcのread関数 の行末処理の仕様に由来しているものと思われる。
実は僕も最近までこの仕様を知らなかったので、過去書いてきた大量のスクリプトの見直しをしないとなぁと思ってるところだ…orz

参考: Linuxに関わる人が一度は読むべきStackOverflowまとめ - Qiita でコメント欄を書いてて気がついた。

その他の枝葉の説明

bashをよく知ってる人も知らない人もいると思うので使っているTipsについて

  • 関数内でしか使わない変数はlocalしないと関数外にだだ漏れするので注意する。
  • IFSはデフォルトで$' \t\n'(半角スペース、タブ、改行)の3文字が入ってる変数でbashはこの文字を単語の区切りとして認識する。
    • 例えば a=($(echo 1 2; echo 3))(1 2 3)の3値の配列になるのはIFSが空白と改行を含むからで、IFS=$'\n'; a=($(echo 1 2; echo 3))だとa=("1 2" 3)のように改行区切りのみで解釈されて2つの文字列が入った配列が出来あがる。
    • また特に1文字目は配列変数を[*]で単一文字化する際に区切り文字として使われるので順番も大切。例えば (IFS=,; a=(1 2); echo "${a[*]}")1,2 が出力される。
    • あと、IFSキャンセルをするのによく local IFS0=$IFS; ...; IFS=$IFS0 という風に元のIFSを保存しておいて必要な処理が終わったら復元するという処理を見かけることもあるが、影響範囲を関数内に留めたいだけなら local IFS= だけで外部への影響なしでIFSリセットがちゃんとできるので記述が煩雑にならなくてよい。
  • 空文字を代入したいなら a='' じゃなくて a= でよい。まぁどっちでもよいので趣味の問題。
  • 変数代入は a="$b" とかしなくても a=$b だけでよい。代入の右辺の式は空白とか改行とか入っていてもダブルクオートで囲まなくても変な挙動にはならずにそのまま代入される。まぁどっちでもよいので趣味の問題。
  • 変数代入は a="$(echo 1; echo 2)" とかしなくても a=$(echo 1; echo 2) だけでよい。サブシェルの出力に改行とか入っていてもダブルクオート無しでもそのまま改行文字として代入されてくれる(ただしサブシェルの出力末尾の改行だけは全部消える、これはダブルクオートがあろうとなかろうと、またIFSも関係なく消える仕様)。
  • eval は基本使わないほうが良いけど、動的に変数セットしたい時などevalでしか実現できないこともあるのでキチンとエスケープした上で使えば便利なときもある。でも気を使うことが凄く増えるので余程のことがない限り使わないほうが良いねw
  • eval local $1_readで使う一時変数をlocalしてるんだが。最初何も考えずにlocal tとかして一時変数を作ってたんだけどこれだと外からread2arr tて呼ばれた時にtがlocalされちゃってて外部に返せなくなるので読みにくいが動的にせざるを得なかった。
  • < <(cat)catの標準出力をwhileに食わせてるだけなんだが cat | while .. だと不味い理由があるのでこうしている。詳しくはこれ参照>パイプ出力を現在のシェル上のwhileに喰わせる上手いやり方 - Qiita
    • しかしよく考えたら上記コードではcatする必要がそもそも無かったので消した。
  • a+=(foo) で配列への要素の追加ができる。a[${#a[@]}]=foo とかでもできるけど +=(...) の方が読みやすいし、複数要素の追加も一度で出来るので便利。
  • [[ $a =~ xxx ]] で正規表現が使えるよ。
  • [[ -n $t ]]$tが空じゃなければ真(exitコードが0)。逆に [[ -z $t ]] は空なら真になる。
  • commmand1 || command2 はcommand1が失敗(exitコードが0以外の時に)したときにcommand2が実行される。文としての全体のステータスは最後に実行されたcommand2の結果になるので後者が真なら全体として真になる。今回は最終行のwhile離脱防止に使っている。
  • あと条件文で [ を使う人がいるけど #!/bin/bash なら [[ を使う方が高機能だし罠も少ないので個人的にお勧め。ちなみにこの違いは似てるけど結構深くて、最大の違いは[またはtestはコマンドで、[[は複合コマンド(通常のコマンドというよりbashの文法の一部)てとこなんだけど、枝葉として書くにはボリュームが…なのでいくつか特徴的な例をあげるだけにしとく。
    • [[はbashの変数のダブルクオート不用。[[ -z $a ]]としてOKだし無い方が罠が少ない、特に正規表現でハマりやすい。逆に[の場合はダブルクオートした方が罠が少ない。
      • 例えば行頭スペースにマッチする p="^ +" てパターンでチェックしたい時は [[ " a" =~ $p ]] とすべきで、[[ $a =~ "$p" ]] だとマッチしない。
    • [[ の中は文ではなく式として評価されるので、大小比較に<>が使えたり&&||が論理演算子として使えるし括弧()で囲ってand/orの複合処理もできる。
    • [[ $x == a* ]] みたいにすると文字列に対してglobマッチングが出来る(この例だと$xがaで始まってれば真。これが [ だと a* の部分がマッチングどころか、カレントディレクトリ内のファイル名に展開されてそれらが引数として扱われて式として崩壊したりする。
    • なお昔は僕もこの[[[の違いがよく分かってなくて指摘してくれた人に失礼な態度したこともあったなぁ。今見返すと指摘は全てまっとうなのに対して、自分にはアホな反発も見られて恥ずかしい…。
  • ${#arr[@]} は配列サイズを取得している。なお ${#foo} だと文字列の長さが得られる。 − 配列変数を回すときは "${arr[@]}" て風にダブルクオートで囲ってやらないと値に空白を含む時などに引数が分割されたり値そのままで処理されなかったりしてハマるので基本常に付けるべき。
  • printf "$s\n" "$v"echo の代わり、echo だとv-nとか-e入ってるとアレなので完全に文字列として単に出力したいときは printf %s とか cat <<<"$v" とかよく使う。
  • $'a\tb' で文字列作る奴。普通は "" とか '' で文字列を作るが、$'' で作ると中に \t とか \n を書くとC言語みたくエスケープされてタブ文字や改行文字な値を作ることが出来るという違いがある。
    • $'' 内でエスケープ可能な記述一覧は man bashのQUOTINGの項 を見ると載ってる。
    • '' はエスケープ処理を一切しないでそのままの文字列が欲しい時に使う。なお中で ' 自身を使いたい場合はエスケープする手段がないので一旦シングルクオートを閉じてから他のリテラルで繋げる必要がある。例えば 'a"b'"'c"a"b'c が得られる等。
    • ついでに書いておくと "" は内部で変数展開とかが出来たり一番良く使って便利なリテラル。エスケープが必要な文字は $\"`のと改行の計5つ。
    • あーあとダブルクオート内に ! は鬼門ね。こいつがあると HISTORY EXPANSION とかいうのが発動して混乱必至で超うざい。しかもダブルクオート内のバックスラッシュエスケープも出来ないし、! を使いたきゃ基本そこだけシングルクオートで囲むのが無難。この機能には事故の経験しかなくて嬉しかった事が皆無なので大嫌い。(HISTORY系操作はこの事故経験のせいで食わず嫌いなまま近寄らないので無理解なだけかもしれんが…)
  • cat -n すると行番号付けて出力されるのでちょっとした確認に便利なやつ。

締切がヤバイせいでついダラダラ書いてしまう…。簡単な解説追記とか言いつつ脳内棚卸しみたいになってるな。試験前に部屋の模様替え始めるアレだ。あーそして仕事ヤバイので離脱。

シェル芸の闇は深い

しかし慣れてくるとあまり迷わず書けるようになってるのが怖いが、上に挙げた枝葉の説明とか見返すとあんな短いコードでこんな色々気にしながら書いてるなんて、もうシェル芸人って完全に変態だな…と改めて思った。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away