シェル、シェルスクリプトの使い方まとめ
自分向けメモ。たまに更新していく。
Shellとは
ユーザーが入力したコマンドを解釈し、該当コマンドを実行する。シェルにはモードが2つある。
上記がどういったものかは割愛する。
単純コマンド
$ echo HelloWorld HelloWorld
コマンド名と任意の引数で構成されるもの。
リダイレクト
- 標準入力
- 標準出力
- 標準出力標準エラー出力
- 任意のファイル記述子
上記をリダイレクトさせることが出来るのが、リダイレクトです。(良い言い方がわからない)
標準入力にファイルを入力する例
$ cat stdin HelloWorld $ < stdin HelloWorld
標準出力をファイルに書き込む例
$ cat stdin HelloWorld $ cat stdin > out $ cat out HelloWorld
標準出力をファイルに追記する例
$ cat stdin HelloWorld $ cat stdin >> out2 $ cat stdin >> out2 $ cat out2 HelloWorld HelloWorld
標準出力・標準エラーを別ファイルに書き込む
ファイル記述子
- 1: 標準出力
- 2: 標準エラー出力
div: echo 1 2
$ make div 1> log_1 2> log_2 $ cat log_1 echo 1 1 2 $ cat log_2 make: 2: No such file or directory make: *** [div] Error 1
標準出力・エラーまとめてファイルに書き込む例
err: echo HelloWorld a
$ make err > log 2>&1 $ cat log echo HelloWorld HelloWorld a make: a: No such file or directory make: *** [err] Error 1
パイプ
構文の組み合わせ
treeの出力をlessに渡す
$ tree / | less
標準エラー出力もパイプに渡す
hoge 2>&1 | less
環境変数の一時変更
LANG=C man ls
&&(AND) ||(OR)
パイプライン1 && パイプライン2はパイプライン1の終了ステータスが成功(0)なら、パイプライン2が実行される。
パイプライン1 || パイプライン2はパイプライン1か2のどちらかが成功(0)すればおk.
err: echo HelloWorld a
$ make err && echo OK echo HelloWorld HelloWorld a make: a: No such file or directory make: *** [err] Error 1 $ make err || echo OK echo HelloWorld HelloWorld a make: a: No such file or directory make: *** [err] Error 1 OK
シェルスクリプト
行頭便利フォーマット
#!/usr/bin/env bash # Fail on unset variables and command errors set -ue -o pipefail # Prevent commands misbehaving due to locale differences export LC_ALL=C
#!/bin/sh -e # オプションつきシェルスクリプト
行頭の意味
シェルスクリプトの行頭に記述する #!
で始まる行shebangという
#!/bin/bash
bashをenvを使って指定
#!/usr/bin/env bash
基本構文
if
引数によっての分岐
#!/bin/bash if [ $# -lt 2 ];then # 引数が2個未満 echo "Usage: $0 file1 file2" 1>&2 # 標準エラー出力に出力 exit 1 else echo "eeyan" fi
$ sh if.sh Usage: if.sh file1 file2 $ sh if.sh ls file1 eeyan
case
unameを使ったOS判定の例
$ uname -sr Darwin 15.6.0
#!/bin/bash case `uname -sr` in Linux*) ls -l --full-time "$@";; FreeBSD* | NetBSD* | OpenBSD* | Darwin*) # Darwinから始まる任意の文字列 ls -lT "$@";; # ここが実行される SunOS' '5.*) ls -E "$@";; *) # 何もひっとしない場合 echo unknown OS 1&>2;; esac
$ sh case.sh total 88 -rw-r--r-- 1 jumpei staff 13 7 5 08:23:24 2018 2 -rw-r--r-- 1 jumpei staff 202 7 5 08:24:23 2018 case.sh -rw-r--r-- 1 jumpei staff 158 7 5 08:18:11 2018 if.sh -rw-r--r-- 1 jumpei staff 48 7 4 08:44:16 2018 sample.sh -rw-r--r-- 1 jumpei staff 56 7 4 08:44:35 2018 sample2.sh -rw-r--r-- 1 jumpei staff 87 7 4 08:52:39 2018 sample3.sh -rw-r--r-- 1 jumpei staff 47 7 4 20:14:09 2018 sample4.sh -rw-r--r-- 1 jumpei staff 55 7 4 20:13:57 2018 sample5.sh -rw-r--r-- 1 jumpei staff 198 7 5 08:03:53 2018 sample6.sh -rw-r--r-- 1 jumpei staff 69 7 5 07:52:58 2018 sample7.sh -rw-r--r-- 1 jumpei staff 81 7 5 07:55:30 2018 sample8.sh
for
*
はカレントディレクトリのファイル名
#!/bin/bash for file in *; do echo $file done
$ sh for.sh LICENSE README.md for.sh for_arg.sh for_bash.sh for_jot.sh for_seq.sh sample sh
引数 $@
を使ったループ
#!/bin/bash for arg in "$@";do echo $arg done for arg;do # 上と同じ意味 echo $arg done
$ sh for_arg.sh a b c a b c a b c
#!/bin/bash for i in {1..10};do echo $i done
$ sh for_bash.sh 1 2 3 4 5 6 7 8 9 10
jotを使ったループ
#!/bin/bash for i in `jot 10`;do echo $i done
$ sh for_jot.sh 1 2 3 4 5 6 7 8 9 10
for i in `seq 1 10`;do echo $i done
1 2 3 4 5 6 7 8 9 10
サブシェル
リストを()で囲むとサブシェルになり、サブシェルはもとのシェルとは別扱いで実行されます。サブシェルの中での変数の変更、umask値を変えても、サブシェルから出ると戻ります。この挙動を利用して、何らか変更をさせて終わったら、元の状態で処理もしたいなどのシチュエーションでは便利です。シェルスクリプトをリダイレクト、パイプでつないでも同じことはできます。
シェル変数 IFS
を :
に変更して、set
コマンドを実行すると :
が削除され、 PATH
の中身が $1 $2 $3
のように位置パラメータに設定されます。これらの変更はサブシェルを抜けると解除されます。
#!/bin/sh echo "IFS=$IFS" ( IFS=: echo "IFS=$IFS" set $PATH echo $3 ) echo "IFS=$IFS"
$ sh sub_shell.sh IFS= IFS=: /Users/jumpei/.anyenv/envs/rbenv/shims IFS=
グループコマンド
リストを{}でかこむとグループコマンドと呼ばれる複合コマンドになります。リダイレクトしたり、パイプに接続したり、次のシェル関数の本体として利用できる。グループコマンドはサブシェルと違って、終わった後も影響があります。
コマンドの結果をまとめてlogfileにリダイレクトしている例。
#!/bin/sh { hostname date who } > logfile
$ sh group.sh $ cat logfile jumpei-no-MacBook-Pro-3.local 2018年 7月 6日 金曜日 08時24分53秒 JST jumpei console Jul 4 07:49 jumpei ttys001 Jul 4 07:49
シェル関数
シェル関数内の変数は引数渡しのため、位置パラメータ以外はグローバル変数になりますそのため、関数内はローカルにしたい場合は、{}
ではなく ()
を使って、サブシェルにすれば可能です
#!/bin/bash greet() { echo "Hello" } greet greet2() { echo $1 "Hello" } greet2 A
$ sh func.sh Hello A Hello
エラー系TIPS
変数の設定漏れ防止
set -u
変数を宣言していないものを使った時に終了ステータスを失敗(0以外)にしてくれる。
#!/bin/bash VAL=foo echo $VAL_TYPO echo FINISH
$ sh sample.sh; echo $? FINISH 0 $ cat sample2.sh #!/bin/bash set -u VAL=foo echo $VAL_TYPO echo FINISH $ sh sample2.sh; echo $? sample2.sh: line 6: VAL_TYPO: unbound variable 1
エラーとしたくないケースを回避
途中まで処理して落としたい。デフォルト値が使える(${parameter:-word})
#!/bin/bash set -u if [ -z "${1:-}" ]; then echo "HOW TO hoge" >&2 exit 2 fi
$ sh sample3.sh HOW TO hoge
エラーになったら中断
set -e
何もつけないとエラーがあっても、最後まで実行される。しかも、ステータスコードも成功になる。
#!/bin/bash FOO=$(ls --l) echo $FOO echo "OK"
$ sh sample4.sh; echo $? ls: illegal option -- - usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...] OK 0
エラー時に途中で終了し、ステータスコードも失敗が帰ってくるようになる
#!/bin/bash set -e FOO=$(ls --l) echo $FOO echo "OK"
$ sh sample5.sh; echo $? ls: illegal option -- - usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...] 1
エラーを無視したいケース
grepで検索ヒットしない時にエラーステータスを返すので、それをいい感じにしたい。
以下のような感じにするか、コマンドでエラーとしないオプションとかを使うか
またはリストを使ったエラー無視などのパターンがあります。
#!/bin/bash set -e if find ./ | grep sample888.sh >/dev/null; then echo "Existing sample.sh" else echo "Nothing sample.sh" fi rm a || true rm a || : # trueの代わりに出来る echo "OK"
$ sh sample6.sh; echo $? Nothing sample.sh rm: a: No such file or directory rm: a: No such file or directory OK 0
パイプライン内のエラーで中断する
set -eはパイプラインの一番右のコマンドのエラーは正しくエラーとしてくれるが、途中のコマンドのエラーは無視されます。
$ cat sample7.sh #!/bin/bash set -e FOO=$(ls - l "$0" | wc -l ) echo $FOO echo "OK"
$ sh sample7.sh; echo $? ls: -: No such file or directory ls: l: No such file or directory 1 OK 0
set -e -o pipefail
をつけるとパイプライン中のエラーを検知してくれる。
#!/bin/bash set -e -o pipefail FOO=$(ls - l "$0" | wc -l ) echo $FOO echo "OK"
$ sh sample8.sh; echo $? ls: -: No such file or directory ls: l: No such file or directory 1
エラーは標準エラー出力に出力しよう
#!/bin/bash echo 'Expect stdout' echo 'Expect stderr' 1>&2
$ sh ./output.sh >/dev/null Expect stderr $ sh ./output.sh 2>/dev/null Expect stdout
bashに依存しているコマンドは#!/bin/bashと書かない
ubuntu上では以下のスクリプトは動きません。bashに依存しているなら、/bin/bashとしよう。
#!/bin/sh list=( ($ls) ) echo $list
環境変数によって動かないを避ける
export LC_ALL=C
参考文献
- ソフトウェアデザイン 2017年 07 月号 シェルのコーナー
- bash スクリプトの先頭によく書く記述のおさらい