早崎トップ 研究(気候気象) 研究(大気汚染) データリスト Linux Tips Mac Tips

grep_sed_awk (hysk)

grep, sed, awk 関連

主に自分が使った or 使用中の具体例や, 記憶していない(する必要の無い)細かい文法などを記す. 各コマンドの文法の包括的な情報は,各種解説本・Webページなどを参照されたし.

grep, sed, awk の組み合わせ

  CSV ファイルで空欄のセルにダミー値を挿入

EXCEL などの表計算ソフトで数値データを作成する際, 作成者によってはデータが存在しないときにエラー値などを入れず,空欄にすることがある. EXCELだけで全ての解析作業が済んでしまう人ならば不都合に感じないだろうが, 私のようにLinux 上で解析する人間にとっては,迷惑この上ない.

そんな時の解決方法の一つとして,以下の方法を記録しておく:

  1. 可変長CSV ファイルを固定長CSVに変換(awk 使用)
  2. コンマに挟まれて全文字がスペースで埋められたセルをエラー値で置換(grep 使用)

この方法は非常に面倒.もっと楽にできる方法はいくらでもあるはず.

grep

  指定検索条件に一致しない行の出力

grep のオプション -v を入れる

 grep     -e "hoge"  ifile   # ifile 内の hoge という文字列にマッチした行を出力
 grep -v -e "hoge"  ifile   # ifile 内の hoge という文字列にマッチしない行を出力

  ヘッダ行の削除

ヘッダ行は,行頭に # がある行とする. 以下は,このヘッダ行を削除するスクリプト (rm_header) である:

#!/bin/sh
target=$1
### remove header lines(s) and write to standard output!
grep -e "^[^#]" ${target}

自分は,Fortran などの計算結果をテキスト出力した際に, 実行したプログラム名や出力した計算値に関するメモをヘッダ行に出力するようにしている. これは,データファイル 冒頭の数行を見ることで,対象となる数値データを再利用 or 再計算できるようにするためである.

しかし,ヘッダ情報つきデータを awk などに通すときには,ヘッダ行が邪魔になる. いちいち例外処理するのは面倒. そんな時には,上記のスクリプトでヘッダ行を削除してから awk などに引き渡す.例えばこんな感じ:

rm_header hoge.txt | awk '{print $3}'

rm_header に実行権を与え,パスの通ったディレクトリに置けばよい (see also 自分専用コマンド・関数).

 正規表現 (regular expression)

用例

解説本などでは,最初に正規表現の基本規則を学ぶのが一般的だが,個人的には 「細かい規則は後で調べるから,とにかく使い方をてっとり早く知りたい」 という事が多い. だから,まずは用例を示す.

grep -r -E "^#\!/bin/bash" *
現在のディレクトリ以下で,行頭に "#!/bin/bash" が記述してあるファイルを捜し出す. "!" 記号は特殊な意味を持つので,バックスラッシュでエスケープ."!" という文字そのものを指定する.
grep -r -E "(ps|grd)contour" *
現在のディレクトリ以下で,"pscontour" ないし "grdcontour" という文字列が記述してある ファイルを捜し出す. ちなみに,pscontour, grdcontour は作図ツール GMT のコマンド.

grep の -r オプションは recursive の意.grep -E は egrep と同義である.

基本

以下が正規表現の基本演算子.この8つだけ知ってるだけで,様々な文字列パターンを探索できる.

個人的経験であるが,研究観連で利用する限りでは, これらの演算子と探したい文字列の一部とを単純に組み合わせる程度しか使わない. 様々な文字列クラスの指定方法もあるが,使用頻度が極めて少ない. たまにしか使わない規則など,いつまでも覚えてられない. コンピュータ技術者になるわけでもないので,以下の8つを知ってるだけで実用上は十分.

記号 意味
( ) グループ化.括弧内で複数のパターンを指定するときに使う
| 左右のどちらか.グループ化と併用することで強力な使い方が出来る
? クエスチョンマーク.直前の表現(1文字のみ)が0回か1回の場合
* アスタリスク記号.直前の表現(1文字のみ)が0回以上連続する場合
+ プラス記号.直前の表現(1文字のみ)が1回以上連続する場合
. ピリオド.任意の1文字.1文字だけのワイルドカード.
^ ハット記号.行頭を指定.
$ ダラー記号.行末を指定.

正規表現を使うときは,grep コマンドで -E option を付ける. また,対象ディレクトリより下の階層も search したい時は,-r option も付ける.

[並び]

並びで指定された文字のいずれか1文字(下記の表を参照). 文字列クラスの指定はあまり使わないので忘れてもいいでしょう. 記録の意味で書いておく.

並びの要素 意味
文字 文字それ自身
文字1-文字2 文字コード順に並べ文字1と文字2の間の範囲(両端含む)の文字
[:alnum:] アルファベット(a-zA-Z)または数字 (0-9)
[:alpha:] アルファベット
[:cntrl:] 制御文字
[:digit:] 数字
[:graph:] 空白(スペース)を除く表示可能文字
[:lower:] アルファベット小文字
[:print:] 空白を含む表示可能文字
[:punct:] 空白とアルファベットと数字を除く表示可能文字
[:space:] 空白文字
[:upper:] アルファベット大文字
[:xdigit:] 16進数字
  • 使用例
[D|d]efine
::: Define または define にマッチする
[+-]?[[:digit:]]+
::: 1個以上の数字の並び,あるいは数値の前に正負の記号が付いた文字列にマッチする.
::: まぁ,一般に使う通常の符号付き10進整数のことや.

[^並び]

並びで指定された文字以外の1文字.並びに入る要素は,前記の表を参照.

 指定ディレクトリ以下の全ファイル内の文字列検索

カレントディレクトリ内のファイル中に,ある文字列が含まれるか否かの検索は簡単.

grep hoge *

ディレクトリがあると多少 warning めいたメッセージが出るかもしれないが, 実用上は上記で問題ないだろう. もし通常ファイルだけに限定したければ,下記の方法を使えばよかろう.

同様の事を,あるディレクトリ以下全てのファイルを対象として実施 (要するに全ファイル検索)したい場合は,以下のようにする:

例1: find . -type f -print | xargs grep "interpolate" 
例2: find . -type f -print | xargs grep "[Ll]egendre" 
例3: ???

ここではカレントディレクトリ(".")に限定している. "." を対象ディレクトリで指定すれば,任意のディレクトリ以下のファイル内全文検索が可能になる. find の使い方や応用例は,ちょっと便利なコマンド > find コマンドを参照.

例1 は "interpolate" という文字列を探索. 例2 は,"Legendre" または "legendre" という文字列を探索. 例3 は,(何か思いついたら追記予定)...

sed (Stream EDitor)

  データファイル中の特定文字列を欠測値に置換

あるデータファイル(ASCII text)中にあるアスタリスク("*")4つ並んだ文字列を, 欠測を意味する "9999" に置換する. サンプルデータファイル名を hoge.txt ,修正後の出力先を hoge_modified.txt とする. 以下のスクリプト (replrace_asterisk.sh) では,第一引数: hoge.txt,第二引数: hoge_modified.txt として与えればよい.

#!/bin/bash
ifile=$1
ofile=$2
tmp_sed=replace.sed
cat > ${tmp_sed} << EOF
/\*\*\*\*/{
s//9999/g
}
p
EOF

sed -n -f ${tmp_sed} ${ifile} > ${workfile01}
\rm -f $tmp_sed

記述が繁雑になるため,想定されるエラー処理(引数の過不足,ファイル上書きの確認など)は省略.

 雛形からのGrADS CTL ファイル生成

GrADS の CTL ファイルで,入力ファイル名(DSET ^hogehoge)だけを変更して使いまわしたい時, DSET 部分の文字列をファイル名に置き換えてしまえばいい. どうせ自分個人で使う CTL ファイルの種類は少ない. 頻繁に使うものは,せいぜい10種類程度か. ならば,template ないし skelton と言えるものを用意し,ファイルごとの詳細設定は手動修正する,という方法であれば手っ取り早く CTL ファイルを作成できる.

下記スクリプトを gen_ctl と名付け, パスの通ったディレクトリ($HOME/local/bin/ など)以下に置く. 当然だが,実行権限を付与(chmod 755 gen_ctl).
なお,作成する CTL ファイルの対象となるバイナリファイル名は,拡張子 bin が付いているものとし, 作成する CTL ファイルは拡張子だけを ctl に差し替えた名前で,データと同一ディレクトリに作成する.

#!/bin/bash
i_ctl=$1    # template/skelton CTL file
o_bin=$2    # Give full-path, *.bin

  o_bin_filename=`basename $o_bin`
  o_ctl=`dirname $o_bin`/`basename $o_bin .bin`.ctl

tmp_sed=/dev/shm/rename_$$.sed
cat > ${tmp_sed} << EOF
{
s/DSET \^hoge/DSET \^${o_bin_filename}/g
}
p
EOF

sed -n -f ${tmp_sed} ${i_ctl} > ${o_ctl}
\rm -f $tmp_sed

雛形となる CTL ファイル(例: CTL00_skeltonの DSET 部分は, スクリプトを簡便にするために "DSET ^hoge" で固定しておく. 任意の CTL ファイルからの複製を作りたい場合は,この DSET 部分も引数に与えるようにすればよいだろうが,複雑になりそうなので却下.
なお,title, tdef, xdef, ydef など他の要素を変更したいのなら, 上記を応用すれば容易に作成可能. 雛形の CTL を多数用意する,という手もあるが,数が増えると自分自身でも覚えきれなくなる.

 入出力パスの一部だけを変更

私の場合,GMTによる作図で PDF, PNG形式ファイルを同時作成することが多い. GMT自分ルールでは,PS (EPS) と PDF, PNG形式のファイルは,それぞれ別ディレクトリに置くことにしている. その場合,eps, pdf,png の文字列以外はディレクトリ名が同一となるように設定.

eps_path=$HOME/dv1/ocean/eps/sst
pdf_path=`echo $eps_path | sed -e "s/\/eps\//\/pdf\//g"`
png_path=`echo $eps_path | sed -e "s/\/eps\//\/png\//g"`
  chk_dir $eps_path  $pdf_path $png_path

もし,ディレクトリ名を変える場合が生じても, eps_path 部分だけを修正すれば良い. EPS, PDF, PNG のどの画像形式であっても,ディレクトリ構造が同一になるという点が(自分にとっては)重要.

"/eps/" を "/pdf/" や "/png/" に変換. スラッシュを入れないと,ディレクトリ名の一部に eps が含まれる場合も文字列置換されてしまう. それは私の意図とは異なるので,そのような事態にならないよう,スラッシュ付きで文字列置換している.

bash 内部コマンドを使っても同じことができる. See also シェルスクリプトのメモ(hysk) > 変数 > 変数定義 > 変数の一部を置換

chk_dir は,私個人使用の関数. See also シェルスクリプトのメモ > 自分専用コマンド・関数(hysk)

awk

古いHTML版から移設完了, 旧HTMLファイルは削除済み (2012-06-27).

infile 中の 1列目・4列目を outfile に出力

awk '{print $1,$4}' infile > outfile

行番号と infile 中の 1列目・4列目を outfile に出力

awk '{print NR,$1,$4}' input > outfile
※ nl というコマンドを使っても行番号は出力できる

shell program 中のシェル変数を参照する場合

基本
set yy=89
awk '{print '$yy',$1,$4}' input > outfile

応用
※ 例えば1列目に年が書いてあって、ある年のデータだけを切り出したい時
foreach yy (89 90 91 92 93 94 95 96)
  awk '$1 == '${yy}'{print $1,$2,$3,$4}' input > outfile${yy}
end             # end of foreach loop "yy"

3列目のデータをある範囲だけ抜きだしたい時

awk '$3 >= 300 && $3 <= 500 {print $0}' input > outfile
※ 3列目のデータが 300 以上 500 以下の時に、行全体($0) を出力

特殊記号を出力したい時

awk '$3 == -999 {print ">"}' input > outfile
※ GMT でグラフを書く時に欠測データを書かせないようにする時に使える!
(詳しくは psxy の -M option, easy-ref2.txt参照)

列の区切りがスペースじゃない時

例として ":" で区切られていた時
awk -F : '{print $0}' input
  または
awk 'BEGIN{FS=":"}{print $0}' input
※ FS は Field Separater の略

正規表現の使用例

awk '$3 ~ / 00$/ || $3 ~/12$/ {print $0}' ifile > ofile
※ 3列目のデータの末尾に 00 または 12 という文字列があった場合に出力
awk '$3 ~ /^1988/ || $3 ~/^1989/ {print $0}' ifile > ofile
※ 3列目のデータの先頭に 1988 または 1989 という文字列があった場合に出力
awk '$3 !~ /^1988/ {print $0}' ifile > ofile
※ 3列目のデータの先頭に 1988 という文字列が含まれない場合に出力

awk script をファイルにcat する時

"$1" などの文字列がcatでは意図通りにされない可能性がある。
shell script 中で

cat > tmp.awk << EOF
$1 == 0 {print $0}
EOF
awk -f tmp.awk ifile

としても、tmp.awk を見ると分かるように、$1 や $0 が無くなっている。
これを回避するためには、EOF 文字列をシングルクォートで囲む。

cat > tmp.awk << 'EOF'
$1 == 0 {print $0}
EOF
awk -f tmp.awk ifile

これでうまくいく。

awk で出力体裁を整えたい(format を指定したい)時

printf を使う.

awk '{printf("%6s%8.2f%8.2f%4d\n",$1,$2,$3,$4)}' ifile

% 以下の数字が桁の数
s は文字列(string)
f は浮動小数点(floating number)
e は指数形式(exponential form)
d は十進数(decimal number; iでもいい)
\n は改行を意味する。これが無いと、全部の出力が1行で書かれてしまう

** 応用 **
ファイルリストなどを変数に代入したい時
list=`\ls -l | awk '{printf("%s%c",$9,"\x20")}'`
※ OSによっては ls -l の体裁が異なる場合があるので注意
※ "\x20" は「(半角)スペース」のASCIIコード(16進数)表記

awk で平均を計算したい(ここでは2列目のデータ使用)

※ データ個数(行数)は欠測値(-999, 有効データは -999 より大とする)以外を数える
nobs=`awk '$2 > -999 {print}' ifile | wc -l`
awk '$2 > -999 {sum += $2} ; END{print sum/'${nobs}'}' ifile

事前にデータ個数を別変数で用意するのが面倒. これを記載した当時(多分1997年以前),awk に詳しくなかったので,こんなやり方しかできなかった.

※ 上記と同じだが,awk 内だけで処理する場合.
awk '$2 > -999 {sum += $2; n++} ; END{printf("%7.2f\n", sum/n)}' ifile

※ 平均値をシェル変数 ave に代入する場合.
ave=`awk '$2 > -999 {sum += $2; n++} ; END{printf("%7.2f\n", sum/n)}' ifile`

/etc/passwd ファイルのUser ID と Group ID をチェックする

awk 'BEGIN{FS=":"} {printf ("%10s%6d%6d%22s%25s\n", $1,$3,$4,$5,$6)}' /etc/passwd

SunOS時代の /etc/passwd 用に作った(多分1996-97年頃)ので,Linux とはフィールド番号が一致しないかもしれない.

文字列から行末の空白を除去して出力

stn_name=`awk -F ":" '$1 ~/23472/ {printf ("%-s", gensub(/ +$/, "", "g", $5) )}' station_col.txt`

globalSOD の地点情報ファイル(original は ish-history.txt, 少し修正.
1列目: WMO code, 2列目: 緯度, 3列目: 経度, 4列目: 標高, 5列目, 地点名;FS=:)から地点名を取り出す. 5列目に地点名が記録されているが,固定長ファイルでフィールドセパレータがコロンである.
そのため,それを使って地点名を切り出すと,空白を含む文字列が取り出される. これをシェル変数に代入してGMTで作図すると,空白部分が入るので気持ち悪い. パイプを通して tr コマンドで除去しても良いが,awk だけで閉じてる方が見栄えが良いので,こっちを使う.

gensub() 関数の内訳
1列目 検索する文字列
2列目 置換文字列 (ここでは空白削除なのでダブルクォート囲みで何も指定しない)
3列目 置換の動作フラグ (全ての空白文字列だから g)
4列目 対象とする入力文字列

awk の基礎知識

【awk の基礎知識】
NR : Number of Record (行番号)
NF : Number of Field (1行あたりの列の数)
$1: 1列目、$2: 2列目、 ... $n: n列目

変更履歴 (過去のHTML版の記録を転載)

更新日 内容
2014-11-30 アンカーの付替,sed に追記
2012-10-31 他のファイルとの相互リンクを増やす
2003-12-01 ちょいと体裁を整えた
2003-11-21 HTML版に変更開始(内容変更無し)
2001-08-22 テキスト版の最終更新
1997-11-15 記録が残る中では,最古のファイル編集年月日.テキスト版のメモ書き開始か?