基礎 | Shell
ベストプラクティス
- パラメータ(変数)参照は必ずダブルクォートで囲む
"$var" - コマンド置換
$(command)は必ずダブルクォートで囲む"$(commnad)" - シェル変数は小文字、環境変数は大文字で定義する
本ドキュメントは上記に準拠していません。
コマンドの文法
ref. https://qiita.com/ko1nksm/items/9650ed1fc21d668f2732?utm_source=pocket_mylist
# コマンドはプリフィックスの0回以上の繰り返し コマンド名 サフィックスの0回以上の繰り返しで構成される
$ [プリフィックス]... コマンド名 [サフィックス]...
- プリフィックス:変数代入 or リダイレクト
- 変数代入はコマンドにコマンド内でのみで有効な環境変数を渡す
- サフィックス:引数 or リダイレクト
プレフィックスによる変数代入
変数代入はコマンドに環境変数を渡す処理です。
※ 例は env コマンドを省略していますが、すべての環境で省略できるわけではありません( Mac の Zsh や Bash では省略可能)。
$ date
2021年 11月 8日 月曜日 20時00分26秒 JST$ LANG=C date
Mon Nov 8 20:01:54 JST 2021$ date
2021年 11月 8日 月曜日 20時03分17秒 JSThttps://qiita.com/ko1nksm/items/9650ed1fc21d668f2732?utm_source=pocket_mylist
LANG=C date # 一時的な変数代入
# 厳密にはシェル関数の場合は・・・と長い話があるのですが省略します
LANG=C # 恒久的な変数代入https://qiita.com/ko1nksm/items/9650ed1fc21d668f2732?utm_source=pocket_mylist
※ Linuxのローカライゼーション系LANG変数:langについて
サフィックスによるリダイレクト
$ > /tmp/ls.txt ls -al
# 以下と等価
$ ls -al > /tmp/ls.txt
プロセス
- コマンド実行からアプリケーション実行まで、すべての処理がプロセス単位で実行される。
詳細はlinuxを参照。
サブシェル
サブシェルとは、シェル (sh や csh など) から起動された子プロセスのシェルのこと。
ref. http://x68000.q-e-d.net/~68user/unix/pickup?%A5%B5%A5%D6%A5%B7%A5%A7%A5%EB
サブシェルはシェルから起動した子プロセスのことです。
サブシェルの特徴をまとめます。
- カレントシェル1はサブシェルの実行が終わるまで待機する
- サブシェルはカレントシェル(親シェル)の
シェル変数を引き継がない - サブシェルの
シェル変数はカレントシェル(親シェル)に影響しない - サブシェルはカレントシェル(親シェル)の
環境変数をを引き継ぐ - サブシェルの
環境変更はカレントシェル(親シェル)に影響しない
カレントシェル(親シェル)の環境変数をサブシェルで変更してもカレントシェルの環境変数は変更されない
ref. 入門UNIXシェルプログラミング(p26)
シェルスクリプトの実行とサブシェル
カレントシェルでスクリプトを実行するか、サブシェルで実行するかはスクリプトの実行方法によってきまります。
- パイプ
|:サブシェルで実行 $ sh script.sh:サブシェルで実行
※ “c”オプションは「コマンドを指定された文字列から読み取る」 ref. https://news.mynavi.jp/techplus/article/techp4484/$ ./script.sh:サブシェルで実行(実行権限付与済み$ chmod u+x script.sh)$ . ./script.sh:カレントシェルで実行$ source script.sh:カレントシェルで実行(.コマンドと異なり相対パスまたは絶対パスで指定する必要がない)execコマンド:$ exec:新たに子プロセスを起こさず、現在実行中のシェルプロセス自体をそのコマンドで置き換える(置き換え後、元のシェルには戻れない)
カレントシェルで実行(例 . ./script.sh や source script.sh )
子プロセスを作成せずにカレントシェルのプロセスでスクリプトを実行します。
スクリプト実行後にカレントシェルで script.shで定義した変数(環境変数、シェル変数とも)を使用できます。
サブシェルで実行(例 sh script.sh や ./script.sh )
子プロセスでスクリプトを実行します。そのためスクリプト実行後に、カレントシェルで script.sh で定義した変数(環境変数、シェル変数とも)を使用できません。
# script.sh
hello="hello"
echo "$hello"
$ sh ./script.sh
hello
$ echo "$hello"
// 何も表示されない
typeコマンド
type コマンドはコマンドの種類を表示します。
a オプション:一致したすべてを表示する
$ type -a echo
echo is a shell builtin
echo is /bin/echo
コマンドの種類は以下のとおりです。
- エイリアス
- 関数( function )
- シェルの組み込み関数( shell builtin )
- キーワード( keyword )
- ファイル( file )
変数
基本
ポイントをまとめます。 1. = を使う
2. 値の設定時は変数名の先頭に $ を付けない ($ は参照時のみ)
3. = の前後にスペースを入れない (前後にスペースがあるとエラー)
4. 型は存在しない (declare コマンドや typeset コマンドで似たようなことができるが推奨はしない)
5. 変数名の大文字・小文字は区別される ( VAR と var は別物)
特に「2」「3」は初心者が犯しがちなミスなので注意すること。
| ref. [変数を使用する | UNIX & Linux コマンド・シェルスクリプト リファレンス](http://shellscript.sunone.me/variable.html) |
特殊なシェル変数
$0 # シェルスクリプト名
$1~$9 # 1番目~9番目の引数
$* # すべての引数(ダブルクォートで囲むと1つの文字列として展開される)
$@ # すべての引数(ダブルクォートで囲むと各引数が個別の文字列として展開される)
$# # 引数の数
$? # 直前に実行したコマンドの終了ステータス。0は成功、0以外は失敗
$- # シェルの実行オプション (/bin/bash -opt)
$$ # シェルのプロセスID
$! # 最後に実行したバックグランドプロセスのプロセスID
$_ # 最後に実行したコマンドの最後の引数
引用元:https://www.tohoho-web.com/ex/shell.html#special
※ "$*" と "$@" の違いは特に重要です。引数が "a b" "c" の場合、"$@" は "a b" "c" の2つの単語として展開されますが、"$*" は "a b c" という1つの単語に連結されます。for ループ等で引数を1つずつ扱いたい場合は "$@" を使用してください。
環境変数
環境変数の作成
# シェル変数として代入しただけでは環境変数にはならない
$ FOO=BAR
$ env | grep FOO
// 空
# export することで環境変数になる(この場合はいつでも確実に反映される)
$ export FOO
$ env | grep FOO
// FOO=BAR
# 代入と同時にexportしても環境変数として設定される
$ export FOO=BAR
$ env | grep FOO
// FOO=BAR
# プレフィックス代入 + export(特殊ビルトインである export への
# プレフィックス代入はカレントシェルにも反映される)でも設定される
$ FOO=BAR export FOO
$ env | grep FOO
// FOO=BAR
環境変数の削除
$ unset FOO
printenvコマンド
環境変数を表示します。
// 環境変数一覧を表示
$ printenv
// 指定した環境変数の値を表示
$ printenv SHELL
/bin/bash
env コマンドを引数なしで実行しても環境変数一覧を取得できます。
$ env
// 環境変数一覧を表示( printenv とほぼ同等)
env 本来のユースケースは、コマンド実行時に一時的に環境変数をコマンドに渡すことです。
// date
// 2022年 1月22日 土曜日 10時25分27秒 JST <--- 変更前
$ env LANG=c date
Sat Jan 22 10:25:55 JST 2022 <---- 一時的に変更
// 一時的に環境変数を設定するのでコマンド実行ごはもとに戻る
// $ date
// 2022年 1月22日 土曜日 10時26分06秒 JST <--- もとに戻る
VAR=value command というプレフィックス代入の記法自体はPOSIX準拠のシェル(sh, bash, zsh, dash など)で共通の基本文法であり、env コマンドが無くても動作します。
# これでも大丈夫
$ LANG=c date
Sat Jan 22 10:25:55 JST 2022
シェル変数
シェル変数作成
$ FOO="Bar"
※ set FOO="Bar" はシェル変数の代入にはなりません。set は位置パラメータ($1, $2…)やシェルオプションを設定するコマンドであり、set FOO="Bar" は $1 に文字列 FOO=Bar が設定されるだけです。変数を作りたい場合は必ず FOO="Bar" のように直接代入してください。
シェル変数確認
シェル変数は set コマンドで一覧表示する。
$ set
// すべてのシェル変数を表示
$ Foo="Bar"
// シェル変数定義
$ set | grep Foo
// Foo=Bar
$ printenv | grep Foo
// 結果なし
$ export Foo
$ printenv | grep Foo # 環境変数になる
// Foo=Bar
ref.
https://qiita.com/chihiro/items/bb687903ee284766e879
コマンドとシェル組み込み関数
echoやprintfのように、同じ名前で実行ファイルで存在しているコマンド(ex:/bin/echo)と、シェルの組み込みとして存在しているコマンドがあったりする。 シェルスクリプト上ではパスを指定しないと組み込みのものが使われる。機能に差異があったりするので、実行ファイルのほうを使う場合は明示的にパスを指定するなどして区別する。
ref. https://qiita.com/Ping/items/57fd75465dfada76e633
ワイルドカード
シェルのワイルドカードと正規表現やプログラム言語固有のワイルドカードの違いに把握します。
*:任意の0以上の文字列(0文字以上でも良いことに注意)?:任意の1文字
* と glob 機能
$ echo *
// カレントディレクトリのファイルを表示する
README.md test1.txt test2.txt
$ echo "*"
// * が表示(glob 機能が無効化される)
クォート
- シングルクォート(
''):文字列を表す(パラメータ(変数)を展開しない) - ダブルクォート(
""):文字列を表す(パラメータ(変数)を展開する) - バッククォート( `command` ):中身をコマンドとして実行(コマンド置換)
$(command)も command を実行する。$(command)のほうが視認性もよくネストも可能なので、通常$(command)を使用する
| 文字 | 内容 |
|---|---|
| ’ | すべてキャラクタがエスケープされます。 |
| ” | $ で変数を展開します。$, `,\, “はエスケープする必要があります。 |
$ nowTime=2021-01-01
$ echo '時刻:${nowTime}'
// 時刻:${nowTime}
$ echo "時刻:${nowTime}"
// 時刻:2021-01-01
$ echo "時刻:`date`"
// 時刻:Sat 23 Oct 2021 12:38:37 PM UTC
$ echo "時刻:$(date)"
// 時刻:Sat 23 Oct 2021 12:38:37 PM UTC
シングルクォートで ‘ をエスケープ
- https://qiita.com/sumomo_99/items/5fb9c054c542456a6cdc
文字列詳細
echo Hello World
// Hello World # クォートで囲まなくても表示
echo 'Hello' World
// Hello World # 文字列の連結に演算子は必要ない
MY_NAME='Hiroshi Sawai'
echo "$MY_NAME"
// Hiroshi Sawai
echo ''
// なにも表示されない
シングルクォート内のシングルクォートをエスケープする方法はありません。以下のように '\'' と書いても、バックスラッシュはシングルクォート内でエスケープ文字として機能しないため、単に「\」という1文字を含むクォートが閉じ、直後にもう一つクォートが開いてしまい、対話シェルでは閉じクォート待ちの継続プロンプト(quote>)になります(そのままスクリプトとして実行すると構文エラーになるため注意)。
$ echo '\''
quote>
シングルクォートの外でエスケープすれば ' 自体を出力できます。
$ echo \'foo\'
// 'foo'
$(command) コマンド置換
$(command) はバッククォートと同じように command を実行する。
ただしネストできるなどバッククォートより高機能。
$ echo $(date -d @$(date +%s))
// 数字の前に @ を付けると、UNIX時間での指定になる。ref. https://hydrocul.github.io/wiki/commands/date.html
// date +%s はUnixタイムスタンプを出力
// Sat 23 Oct 2021 12:49:17 PM UTC
※ date -d はGNU版date(Linux)のオプションです。macOS標準(BSD版)の date では -j -f 等の異なる書式になるので注意してください。
コマンド実行結果を変数に代入
`` か $() で実行した結果を変数に代入します。
= の前後にスペースを入れないこと(スペースを入れると変数代入ではなくコマンド実行に解釈され、意図通り動作しません)。
# 引用元:https://qiita.com/Ping/items/57fd75465dfada76e633
TODAY="$(date +'%Y/%m/%d')"
echo "$TODAY"
NOWTIME="$(date +'%H:%M:%S')"
echo "$NOWTIME"
環境変数 PATH
環境変数 PATH の値を確認する。
$ echo "$PATH"
.bash_profile
パスを通すとは、シェル設定ファイル( .bash_profile や .zshrc ) で環境変数 PATH にパスを追加すること。
export で環境変数(この場合は PATH )を更新する。
PATH=$PATH:/path/to/add
export PATH
PATH の TIPS
(1) PATH の優先順位を確認
※ /etc/paths は macOS 固有のファイルです(Linux には存在しません)。
$ cat /etc/paths
// 優先度の高い順に表示
例えば以下のように、 .bash_profile で優先順位を変更できます。
PATH=/path/to/dir:$PATH
export PATH
(2) source コマンド
.bash_profile を変更したときは、ログインし直さないと変更が反映されません。
source コマンドを使うとログインし直さなくても変更が反映されます。
. コマンドは source コマンドとほぼ等価です.
$ source /path/to/.bash_profile
// or
$ . /path/to/.bash_profile
本当に正しい .bashrc と .bash_profile の使ひ分け - Qiita
if 文
#!/bin/bash
#
# e エラー発生時にスクリプトを終了する
# u 未定義変数の使用をエラーにする
set -eu
var="abc"
# test command
if test "$var" = "abc"; then
echo "same"
fi
# [ command
echo "=== [ command ==="
if [ "$var" = "abc" ]; then
echo "same"
fi
# [[ keyword
# [[ keyword は Bash の独自拡張
# 正規表現 =~ などの機能が使用できる
echo "=== [[ keyword ==="
if [[ "$var" =~ ^a.+ ]]; then
echo "match"
fi
# ( ) command substitution
echo "=== ( ) command substitution ==="
echo -e "aaa\nbbb\nccc" >tmp.txt
if (grep "bbb" tmp.txt >/dev/null); then
echo "match"
fi
# (( )) arithmetic operation
echo "=== (( )) arithmetic operation ==="
num=5
if ((num < 10)); then
echo "5 is less than 10."
fi
[ … ] で使用できる表記
( exp ) # exp をグルーピング
! exp # exp が偽であれば
exp1 -a exp2 # exp1 かつ exp2 であれば
exp2 -o exp2 # exp1 または exp2 であれば
str1 = str2 # 文字列 str1 と str2 が等しければ
str1 != str2 # 文字列 str1 と str2 が等しくなければ>
-z str # 文字列 str が0文字であれば(zero)
-n str # 文字列 str が1文字以上(空でなければ)真(not zero)
str # 文字列 str が1文字以上(空でなければ)真(-n str と同じ)
num1 -eq num2 # 数値 num1 が num2 と等しければ(equal)
num1 -ne num2 # 数値 num1 が num2 異なっていれば(not equal)
num1 -ge num2 # 数値 num1 が num2 以上であれば(grater than or equal)
num1 -gt num2 # 数値 num1 が num2 より大きければ(grater than)
num1 -le num2 # 数値 num1 が num2 以下であれば(less than or equal)
num1 -lt num2 # 数値 num1 が num2 より小さければ(less than)
file1 -ef file2 # ファイル file1 が file2 と同一実体であれば(equal file)
file1 -nt file2 # ファイル file1 が file2 より新しければ(newer than)
file1 -ot file2 # ファイル file1 が file2 より古ければ(older than)
-e file # ファイル file が存在していれば
-s file # ファイル file が存在し、かつサイズが0バイトより大きければ(空でなければ)
-f file # ファイル file がレギュラーファイルであれば
-r file # ファイル file が読み込み可能であれば
-w file # ファイル file が書き込み可能であれば
-x file # ファイル file が実行可能(ディレクトリの場合は移動可能)であれば
-d file # ファイル file がディレクトリであれば
-h file # ファイル file がシンボリックリンクファイルであれば(-Lと同義)
-L file # ファイル file がシンボリックリンクファイルであれば(-hと同義)
-b file # ファイル file がブロックデバイスファイルであれば
-c file # ファイル file がキャラクタデバイスファイルであれば
-p file # ファイル file が名前付きパイプであれば
-S file # ファイル file がソケットファイルであれば
-k file # ファイル file にスティッキービットが設定されていれば(chown o+t)
-u file # ファイル file にセットユーザIDビットが設定されていれば(chown u+s)
-g file # ファイル file にセットグループIDビットが設定されていれば(chown g+s)
-O file # ファイル file が実効ユーザIDに所有されていれば
-G file # ファイル file が実効グループIDに所有されていれば
-t fd # ファイルディスクリプタ fd がターミナルとして開かれていれば
引用元:https://www.tohoho-web.com/ex/shell.html#if-statemant (-n/-s の定義は誤りがあったため本ドキュメントで訂正)
[[ … ]]
DB="mysql";
if [[ "$DB" == "mysql" ]]; then
mysql -u root myapp_test < "${BUILD_DIR}/bin/setup.sql";
fi
if [[ "$DB" == "pgsql" ]]; then
psql myapp_test < "${BUILD_DIR}/bin/setup.sql";
fi
- [ … ]は test の糖衣構文。… の前後に半角スペースが必要
- [[ … ]] は Bash 専用の言語構造で [ ] より高機能
for in
#!/bin/bash
total=0
for x in 100 200 300; do
((total = total + ("$x" * 2)))
done
echo $total
# expr is antiquated. (()) is best practice.
total=0
for x in 100 200 300; do
total=$(expr "$total" + "$(expr "$x" \* 2)")
done
echo "$total"
while/read
// input.txt
abc
def
ghi
while read -r line
do
echo "$line"
done < input.txt
※ read には -r を付けるのが安全です(付けないとバックスラッシュがエスケープ文字として解釈されてしまいます)。
標準入出力・標準エラー出力
ファイル・ディスクリプター
| ファイル・ディスクリプター番号 | 入出力先 |
|---|---|
| 0 | 標準入力 |
| 1 | 標準出力 |
| 2 | 標準エラー出力 |
| n | 任意の入出力先 |
ref. https://qiita.com/laikuaut/items/e1cc312ffc7ec2c872fc
出力制御サンプル
output.sh
#!/bin/bash
# e エラー発生時にスクリプトを終了する
# u 未定義変数の使用をエラーにする
set -eu
echo 'standard output' >&1
echo 'standard error output' >&2
exit 1
※ スクリプトを実行するときは chmod u+x で実行権限を付与する。またサブシェルで実行しないとシェル自体が終了する(例 . path/to/script )
ディスクリプタが省略された場合は標準出力 1 を指定したことになる。
$ ./output.sh 1>/dev/null
standard error output // <-- 標準エラーのみ表示。標準出力は /dev/null に出力。
// 以下と等価
$ ./output.sh >/dev/null
standard error output
& のみを指定した場合は標準出力 1 と標準エラー出力 2 を指定したことになる。
$ ./output.sh &>/dev/null
// 出力なし
リダイレクトを複数指定した場合は左から右に評価される。
$ ./output.sh 2>&1 >/dev/null
// standard error output
// 以下は↑と等価
$ ./output.sh 2>&1 1>/dev/null
標準出力 1 および標準エラー出力 2 ともに /dev/null に出力。
$ ./output.sh >/dev/null 2>&1
// 出力なし
set -eu
e: エラー発生時にスクリプトを終了するu: 未定義変数の使用をエラーにする
https://qiita.com/youcune/items/fcfb4ad3d7c1edf9dc96
サンプル集
ディレクトリのファイル一覧を取得。
#!/bin/bash
for f in $(find . -maxdepth 1 -type f)
do
echo "---"
echo "$f"
done
※ “$(find . -maxdepth 1 -type f)” だと以下のように 1 度しかループされない。
ディレクトリ内のファイルサイズを取得。
#!/bin/bash
for f in $(find . -maxdepth 1 -type f)
do
wc -c < "$f"
done
ディレクトリサイズ表示。
#!/bin/bash
for d in $(find . -maxdepth 1 -type d)
do
du -h "$d"
done
文字列結合
#!/bin/bash
lines=''
while read -r line
do
lines="$lines$line"
done < file.txt
echo "$lines"
file.txt
aaa
bbb
ccc
実行
$ ./script.sh
aaabbbccc
Ref
- https://www.tohoho-web.com/ex/shell.html#special
- https://www.tohoho-web.com/ex/shell.html#if-statemant
- https://qiita.com/piroor/items/77233173707a0baa6360
- https://flex.phys.tohoku.ac.jp/texi/sh/node21.html
- https://qiita.com/Ping/items/57fd75465dfada76e633
- http://shellscript.sunone.me/variable.html
- https://qiita.com/chihiro/items/bb687903ee284766e879
- https://qiita.com/youcune/items/fcfb4ad3d7c1edf9dc96
-
メインシェルとも呼びます。 ↩