メインコンテンツまでスキップ

シェル・ワンライナー 160 本ノック

Tags: Linux, Shell

まとめ

general

  • ファイルの内容を調べる
    • file some_image.jpg
  • 実行時間を計測する
    • time ワンライナー
  • for 文
    • for(i=1;i<$1;i++)printf(" ")

seq

順番に数値を出力する。

# 昇順
seq 5

# 降順
seq 5 1

sed

  • 入力データに置き換え処理を施したうえで再出力する
  • -nオプションをつけると、処理対象となった行のみを出力する

置き換え(s///のパターン)

  • 一回だけ置き換える
echo あいうえおあいうえお | sed 's/あ/か/'
# かいうえおあいうえお
  • 何回も置き換える(g)
echo あいうえおあいうえお | sed 's/あ/か/g'
# かいうえおかいうえお
  • 複数の置き換え条件を指定するには;で区切る
echo あいうえおあいうえお | sed 's/あ/か/g;s/い/き/g'
# かきうえおかきうえお
  • 検索対象の文字を使う(&)
echo クロロエチルエーテル | sed 's/エチル/&&/'
# クロロエチルエチルエーテル
  • 後方参照(\1\2など)
    • -Eは拡張正規表現を有効にする
      • -rでも同じ意味
      • 無駄にエスケープ文字を入れなくて済むようになる
      • 基本正規表現だけ使えればいいなら不要
echo クロロエチルメチルエーテル | sed -E 's/(エチル)(メチル)/\2\1/g'

# 以下のようにもかける
echo クロロエチルメチルエーテル | sed -E 's/(エ..)(...)/\2\1/g'

検索(//pのパターン)

sed -n '/正規表現/p'

# 期間抽出(正規表現1に一致する行から、正規表現2に一致する行までを出力する)
sed -n '/正規表現1/,/正規表現2/p'

grep

  • デフォルトで正規表現が使える
  • 入力は 1 行ごとでも、スペース区切りでも OK
# 0を含むもの
seq 100 | grep "0"

# 8で始まるもの
seq 100 | grep "^8"

# 8で終わるもの
seq 100 | grep "8$"

# 80台
seq 100 | grep "8."

# 1, 10, 100, ...
seq 100 | grep "^10*$"

# 偶数
seq 100 | grep "[02468]$"

# 奇数
seq 100 | grep "[^02468]$"
  • -o
    • マッチした部分のみが出力される
    • 複数行で出力される
echo 中村 山田 田代 上田 | grep -o '[^ ]田'
# 山田
# 上田
  • -A 10, -B 10, -C 10
    • 後 10 行、前 10 行、前後 10 行を出力する
  • -lオプション - 一致した部分ではなくファイル名を出力する
  • -Rオプション - ディレクトリ内のファイルを再帰的に読み込む
  • grep some_pattern ./* - 特定のディレクトリ内のファイル内容を検索

awk

  • grep にプログラム機能を加えたもの
  • awk '/正規表現/'の書き方にすれば正規表現が使える
  • $0は「すべての列(行全体)」を表す
  • $1は「1列目の文字列 or 数値」を表す
    • データの n 列目を「第 n フィールド」と呼ぶ
  • printは自動的に間のスペースと行末の改行を入れてくれる。awk ではよく使う。
  • 構成要素
    • Pattern
      • 抽出条件のこと
      • 正規表現 or 計算式
      • e.g. $1%2==0, /[a..b]/
    • Action
      • 処理のこと
      • {}で囲まれている
      • e.g. {print(...)}
      • デフォルトは print
    • Rule
      • パターンとアクションの組み合わせのこと
    • パターンだけ、アクションだけでも実行可能
# 正規表現で抜き出す
seq 5 | awk '/[24]/'

# 計算で抜き出す
seq 5 | awk '$1%2==2'

# マッチした行に処理を加える(以下の2つは等価)
seq 5 | awk '$1%2==0{printf("%s 偶数\n", $1)}'
seq 5 | awk '$1%2==0{print($1,"偶数")}'

# 2つ以上のルールを使う
seq 5 | awk '$1%2==0{print($1,"偶数")}$1%2==1{print($1,"奇数")}'

# 三項演算子を使う
seq 5 | awk '{print($1%2==0 ? "奇数" : "偶数")}'
  • 期間抽出
    • 正規表現 1 に一致する行から、正規表現 2 に一致する行までを出力する
awk '/正規表現1/,/正規表現2/'
  • 列の結合(と比較)
awk '$1$2=="abcd"'
#`$1" "$2`は$1と$2をスペースでつなげたものになる
awk '$1" "$2=="abcd"'
  • -Fオプション
    • 区切り文字を明示的に指定する
    • awk -F: だとコロンが区切りになる
    • デフォルトはスペース
  • 組み込み変数
    • NF - 総フィールド数
    • NR - 行番号

Pattern, Action のどちらでも利用可能なもの

  • 比較
    • $1<"20191001
  • Regex
    • $2~/REGEX_PATTERN/
  • or は||

Pattern

  • BEGIN パターンは最初の行の処理前に実行される
  • 無名のパターンは全ての行で実行される
  • END パターンは最終行の処理後に実行される
awk 'BEGIN{a=0}{a+=1}END{print a}'

Action

  • 三項演算子で条件分岐
awk '{print ($1<12?"午前":"午後")}'
  • 三項演算子で条件分岐(列を追加する)
awk '{tax=($1<"20191001")?1.08:1.1;print $0,tax}'
  • 一つのアクション内で2つのコマンドを実行したいときは;で区切る。
awk '{printf("a");printf("b")}'
  • 列の値で計算する
awk '{print int($3*$4)}'

xargs

  • 入力を横に並べて、出力する
    • デフォルトではechoが指定されたものとみなされる
  • 入力を横に並べて、コマンドに 引数(入力ではない) として渡したうえ実行してもらう
    • 本来の使い方
seq 4 | xargs mkdir # => `mkdir 1 2 3 4`になる
seq 4 | xargs rmdir # => `rmdir 1 2 3 4`になる
  • 決まった数ずつコマンドに渡していく
seq 4 | xargs -n2 mv
# `mv 1 2`と`mv 3 4`になる
  • 下記のように数個ずつ画面出力するのにも使える。
    • この際空行は削除される
    • セキュリティには注意すること
seq 10 | xargs -n5
  • 入力を改変してコマンドに渡す(@マークは別の文字に変えても OK)
seq 4 | xargs -I@ mkdir dir_@
  • 並列実行する
xargs -P4 SOME_COMMAND

bc

計算をするために使う。

echo '1+1' | bc

sort, uniq

  • データの中に何がいくつあるのか数えるときは、「sort で並び替えて uniq で数える」のが鉄板
    • ただし、sort -uで同じことを一発で実現できる
  • なぜ sort が必要なのか?
    • uniq に与えられるデータはソートされていることが前提のため(一つのことだけやる。プログラムを簡素にする。UNIX 的な思想。)
seq 5 | awk '{print($1%2==0 ? "奇数" : "偶数")}' | sort | uniq -c
# 2列目から2列目を使って辞書順でソート(10 then 1)
sort -k2,2

# 2列目から2列目を使って数値順でソート(1 then 10)
sort -k2,2n

パイプ

  • |
  • 左の結果を右に渡す
  • 異なるプロセス間でのデータの受け渡し方法(プロセス間通信)の一つ

標準入出力

  • しっかりしたコマンドは
    • 別のコマンドに渡すべきデータを標準出力から出す
    • 別のコマンドに渡すべきでないデータを標準エラー出力から出す
  • デフォルトでは
    • 標準出力と標準エラー出力は端末画面につながっている
    • 標準入力はキーボードにつながっている
  • ファイル記述子(ファイルディスクリプタ)
    • 0 - 標準入力
    • 1 - 標準出力
    • 2 - 標準エラー出力

標準出力のリダイレクト

test.sh > file.txt
test.sh 1> file.txt

標準エラー出力のリダイレクト

test.sh 2> file.txt

# stderrの出力先を、いまstdoutがつながっている先に振り向ける(マージされる)
test.sh 2>&1

# パイプを使った方法(stdoutのみならずstderrも渡される)
test.sh |& cat

標準入力のリダイレクト

cat < file.txt
cat 0< file.txt

# 番外編)catとpipeを組み合わせる方法
cat a.txt | wc -l

# 番外編)引数にファイル名を指定
wc -l a.txt

read

標準入力を読み込んで変数にセットする

echo 123 | read v ; echo "$v" #=> 123

変数

値のセット

  • スペースを入れてはだめ(コマンドとして解釈されるから)
a=ほげほげ

値の利用

  • コマンドが実行される前に変数が文字列に入れ替わる
$a
${a} # この書き方が必要になることもある。変数名のあとに文字を続けたい場合など。

文字列操作

  • ${a//}${a::}などは Parameter Expansion(変数展開)と呼ぶ
a=あいうえお
b=かきくけこ

# 結合
echo $a$b # あいうえおかきくけこ

# 追加(破壊的)
$a+=$b ; echo $a # あいうえおかきくけこ

# 開始位置と長さ
echo ${a:1:3} # いうえ

# 置き換え
echo ${a/えお/けこ} # あいうけこ

計算

  • 算術式展開$(())を使う
    • 括弧内で変数を使うとき、頭の$は不要
    • 扱えるのは整数のみ
a=6
b=2
echo $((a+b)) # 8

クォート

  • シングルクォート
    • 変数($1など)をシェルで解釈せずに、そのままコマンドに渡す
    • 空白の入った引数をひとまとめにして引き渡す
      • e.g. awk の'{print 1+1}'など
  • ダブルクオート
    • 変数がシェルで解釈される
    • シェルスクリプトでは変数はダブルクォートの中に配置するのが無難
      • 変数の中身が空文字だと、なかったものとして扱われるため
re=""

grep "$re" /etc/passwd # ok
grep $re /etc/passwd # grep /etc/passwd と解釈されるため不正

配列、連想配列

配列

a=("$SHELL" "$LANG" "$USER")

# 取り出し(zshでは1スタート)
echo ${a[1]}

# 取り出し(全て)
echo ${a[*]}
echo ${a[@]}

# 要素数のカウントは#
echo ${#a[@]}

連想配列

declare -A b
b["SHELL"]="$SHELL"
b["LANG"]="$LANG"
b["USER"]="$USER"

# 取り出し
echo ${b["SHELL"]}

# 取り出し(全て)
echo ${b[*]}
echo ${b[@]}

# 取り出し(key, valueなど)
echo ${(k)b}
echo ${(v)b}
echo ${(kv)b}

# 要素数のカウントは#
echo ${#b[@]}
  • {}は必須。
    • これがないと文字列の結合になってしまう
  • @*はクォートした時の動作が異なる。
    • *をクオートした時には、IFS(デフォルトではスペース)で連結された文字列になる

位置パラメータ

  • 主にシェルスクリプト内において、与えられた引数を取得する時に使われる
    • $1 1 番目の引数
    • ${22} 22 番目の引数 (二桁以上なら{}が必須)
    • $@ 1 つ目以降の全ての引数
    • $* 1 つ目以降の全ての引数 (ダブルクオートしたときに IFS で連結された文字列になる)
  • 位置パラメータを手動でセットにはsetコマンドを使う
set aa bb cc
echo $2 #=> bb

for 文における位置パラメータの利用

for x in "$1" "$2" "$3" ; do echo $x ; done
# or
for x in "$@" ; do echo $x ; done

終了ステータス

  • 以下で確認できる
    • $? - 最後のコマンドの終了ステータス
    • $pipestatus[@] or $PIPESTATUS[@] - パイプライン全ての終了ステータス
  • ステータスの意味は各コマンドのmanで調べられる
  • 128 以上の終了ステータスについては、128 を引くことで異常終了の原因となったシグナルを特定できる

ヒアストリング

パイプの別の書き方。以下の二つは等価。

echo $a | grep '[02468]$'
grep '[02468]$' <<< "$a"

while 文

  • 終了ステータスが正常である限り実行が続く
    • 以下の例ではreadが成功する限り
seq 3 | while read x ; do printf "%s " $x ; done

if 文

if 文の後に実行したコマンドの終了ステータスにより処理分岐される

a=5

if grep '[02468]$' <<< "$a" ; then
echo 偶数 ;
elif grep '[13579]$' <<< "$a" ; then
echo 奇数 ;
else
echo その他 ;
fi

テストコマンド

[ または test で値の評価を行う。結果は終了ステータスとして得ることができる。

# 数値比較
a=0
[ "$a" -gt 10 ]
echo $? #=> 1

test "$a" -lt 10
echo $? #=> 0

# 文字列比較
b="hola"
[ "$a" = "hello" ]

# ファイル存在チェック
[ -e some.txt ]

# ファイル存在チェック(ディレクトリやリンクを除く)
[ -f some.txt ]

&&||

# command1が成功したらcommand2も実行される
command1 && command2

# command1が成功したらcommand2は実行されない
command1 || command2

コマンド置き換え

  • $(コマンド)
    • 該当部分がコマンドの実行結果に差し替えられる
a=きたうらわ
echo ${a}を逆さにすると$(echo $a | rev)
# きたうらわを逆さにするとわらうたき
  • $(<ファイル)
    • ファイルの中身を引数に置き換える
    • e.g. echo "$(</etc/passwd)" > ~/passwd2.txt

プロセス置き換え

  • <(コマンド)
  • コマンドの実行結果をファイル内容として、<()全体をファイル名として扱う
a=きたうらわ
cat <(echo $a) <(echo を逆さにすると) <(echo $a | rev)
# きたうらわを
# 逆さにすると
# わらうたき

コマンドのグループ化(サブシェル)

()で囲んだものごとに、別の bash を立ち上げて実行する

(command1 && echo "hello1") || (command2 && echo "hello2")

# 一時的にcdしたい時などにも便利
(cd /etc ; ls *.conf)

fork と exec

  • fork
    • プロセスの分身を作る
    • 変数等は全て子にコピーされる
    • e.g. サブシェルはこの仕組みで動く
  • exec
    • 自プロセスを別のプログラム(別のバイナリ実行用プロセス)に置き換える
    • 例えば、シェルでexec 化けたいコマンドすることで、同一 PID のままで、bash が sleep に化ける。
    • ファイル記述子と環境変数は引き継がれる
    • シェル変数は引き継がれない
  • シェルがコマンドを呼び出すと実際に何が起こるか?
    • シェルがforkされ子シェルが生まれる
    • 子シェルが瞬時にexecしてコマンドになる
    • この仕組みはfork-execと呼ばれる

プロセス

  • プロセス
    • OS がコマンドやプログラムを管理する単位のこと
    • 固有の番号が振られる(プロセス番号、プロセス ID、PID)
    • ps - プロセス一覧
    • pstree - プロセス親子関係一覧

コマンドの種類

  • 外部コマンド
    • 実体のファイルがある
    • ポータビリティが高い
  • ビルトインコマンド
    • ファイルがマシン上のどこにも存在しない
    • シェルに直接プログラムされている
    • シェルさえ動けば必ず使えるという利点がある
    • 外部コマンドより高速
      • プロセスを用意するという重い処理が不要になるため

/dev/null とは

  • 入力された文字をそのまま捨てる特殊なファイル
  • ビットバケツ、デブヌルなどと呼ばれる
  • 用途
    • 標準出力・標準エラー出力のいずれか又は両方を端末で見たくない場合
    • ベンチマーク測定をするとき

ログ

1.2.a 端末を使う

  • プロンプト - $マークのこと
  • コマンド - echo $0のようなもの
  • シェル - ソフトウェアのこと
    • プロンプトを出したり、コマンドを受け取ったりしている
    • bash はシェルの名前
  • 端末 | terminal
    • 遠くにあるコンピュータと接続して作業するのに使う機械のこと
    • 元々は本当に機械だった
      • データをコンピュータに送信し、返ってきたデータを印字出力するなど
    • 今はエミュレータ

どこだったかな

  • | - パイプ。左の結果を右に渡す。
  • 以下の通り、コマンド関連の用語はあいまいである
    • コマンド
      • 1 単語の場合はコマンドとして用いられるソフトウェアのこと
        • e.g. bcecho
      • 2 単語以上の場合はシェルの受け付ける命令のこと
        • e.g. echo $0
    • ワンライナー - コマンドを 2 つ以上組み合わせたもの
    • パイプライン - パイプにコマンドがつながったもの
    • コマンドライン - 打ち込んだ 1 行分の命令

1.2.b コマンドの止め方

  • Ctrl + C - 強制終了。非常によく使う。
  • Ctrl + D - 端末にこれ以上ユーザが入力するものがないことを伝える。次点としておぼえておけば OK。

1.2.d ファイルへの保存

echo '1+1' | bc > result.txt
  • >は、リダイレクト記号という

1.2.e ファイルとディレクトリの操作

echo あいうえお > somefile
mkdir tmp
mv somefile tmp/
# tmpフォルダの内容を一覧
ls -l tmp/
rm tmp/somefile
rmdir tmp/

1.2.f ファイルのパーミッション

chmod -r somefile
chmod +r somefile
  • パーミッション
    • ファイルの所有者
    • ファイルの所有グループ
    • それ以外
  • sudoを毎回するのが面倒なときはsudo -s

1.2.g コマンドの調査

man は章立てになっている。たいていは 1 章に求めるものが書いてある。

man some_command

# 5章を見る
man 5 some_command

該当行とその次の1行を出力する

man ls | grep -A 1 '^  *-a'

1.3.g bash によるメタプログラミング

seq 4 | awk '{print "mkdir " ($1%2==0 ? "even_" : "odd_") $1 }' | bash

# 以下の4つのフォルダが作られる
# mkdir odd_1
# mkdir even_2
# mkdir odd_3
# mkdir even_4
  • シェルスクリプトとは
    • シェルにやってもらいたいことを順番に書いたファイル
    • bash ./somefileで実行できる
  • シェルスクリプトをコマンドとして使うには
    • 1行目に shebang(#!/bin/bash)を入れる
      • OS が shebang に書いたコマンドを呼び出し、そのコマンドが 2 行目以降を読み込むよう手配される
    • ファイルにchmod +xで実行権限を与える
    • これで./somefileで実行できるようになる

Q1 ファイル名の検索

テキストファイルからの抽出の例:

# 王道
cat ./qdata/1/files.txt | grep '\.exe$'
cat ./qdata/1/files.txt | awk '/\.exe$/'

# - `-n`は各行を自動的に出力しない
# - `/正規表現/p`でマッチする行だけ出力する
cat ./qdata/1/files.txt | sed -n '/\.exe$/p'

Q2 画像ファイルの一括変換

あるフォルダ内にある png ファイルを全て jpg 形式に変換する方法

# 私の回答
ls *.png \
| sed -E 's/(.*)\.png$/\1/g' \
| awk '{print "convert " $1".png " $1".jpg"}'
| bash

# 鮮やかな回答
ls *.png | sed 's/\.png//' | xargs -I@ convert @.png @.jpg
# 発展型(時間計測と、並列実行による高速化)
time ls *.png | sed 's/\.png//' | xargs -P$(nproc) -I@ convert @.png @.jpg

Q3 ファイル名の一括変更

1から10000までのファイルがあるとして、0 埋めにファイル名を変更せよ

seq 1000 | xargs -P2 touch

ls | awk '{printf("%d %04d ",$1,$1)}' | xargs -n2 mv

Q4 特定の内容のファイルを探す

grep -l -R MY_FINDING_STRING | xargs rm

Q5 設定ファイルからの情報抽出

# 私の回答
cat ntp.conf | awk '/^pool/{print $2}'

# 模範解答
cat qdata/5/ntp.conf | awk '$1=="pool"{print $2}'

Q6 端末に模様を書く

# 回答例1
seq 5 | awk '{for(i=1;i<$1;i++)printf(" ");print "x"}' | sort

# 回答例2
seq 4 0 | awk '{for(i=$1;i>0;i--)printf(" ");printf("x\n")}'

Q7 消費税

20190901 ゼロカップ大関 10000
20190902 *キャベツ二郎 130
20191105 外食 13000
20191106 ストロングワン 13000
20191106 *ねるねるねるねる 30
20190912 外食 13000
cat qdata/7/kakeibo.txt | \
awk '{tax=($1<"20191001"||$2~/^あ/)?1.08:1.1;print $0,tax}' | \
awk '{print int($3*$4)}' |
awk '{sum+=$1}END{print sum}'

Q8 ログの集計

183.YY.129.XX - - [07/Nov/2017:22:37:38 +0900]
192.Y.220.XXX - - [08/Nov/2017:02:17:16 +0900]
66.YYY.79.XXX - - [07/Nov/2017:14:42:48 +0900]
::1 - - [07/Nov/2017:13:37:54 +0900]
133.YY.23.XX - - [07/Nov/2017:09:41:48 +0900]
cat qdata/8/access.log | \
awk -F: '{print $(NF-2)}' | \
awk '{print ($1<12?"午前":"午後")}' | \
sort | \
uniq -c

# awkのところの別解。こっちが直感的かも。
grep -o "....:.." | sed s/....://

Q9 期間抽出

# sed
qdata/9/log_range.log | sed -n '/24\/Dec\/2016 21:..:../,/25\/Dec\/2016 03:..:../p'
# awk
qdata/9/log_range.log | awk '/24\/Dec\/2016 21:..:../,/25\/Dec\/2016 03:..:../'

Q10 マークダウンの変換

cat qdata/10/headings.md | \
sed -E 's/^# +(.*)/\1\n===/g' | \
sed -E 's/^## +(.*)/\1\n---/g'

Q11 議事録の整理

cat qdata/11/gijiroku.txt | \
xargs -n2 | \
sed 's/すず/鈴木/g;s/さと/佐藤/g;s/やま/山田/g;s/ /: /g'

Q13 存在しなければファイル作成

[ -f some.txt ] || touch some.txt

Q14 羊を数える

# 共通部分
do
echo "羊が${n}匹"
n=$((n+1))
sleep 1
done

# while+変数
n=1
while [ $n -le 10 ]

# seq
seq 5 | while read n

# for + seq
for n in $(seq 100)

# シーケンス式
for n in {1..100}