Bash

bash スクリプト

set -euxo pipefailについて。

  • set -e: エラーが起きたらすぐに終了
  • set -o pipefail: パイプの左側でエラーが起きても終了
  • set -u: 未定義の変数を参照したら終了
  • set -x: 実行したコマンドをコンソールに表示する
#!/usr/bin/env bash
set -euxo pipefail
SCRIPT_DIR=$(cd $(dirname $0); pwd)

Ref: Safer bash scripts with 'set -euxo pipefail'

文法

;

セミコロンで分の区切りになる。

echo "aaa"; echo "bbb"

\

改行があっても1行として実行。 長い文を見やすくするときに使う。

echo "aaa"; \
echo "bbb"

&&

直前のコマンドが成功(戻り値が 0)なら次のコマンドを実行する。

[ 1 -eq 1 ] && echo "hello"
# -> "hello"

[ 1 -eq 2 ] && echo "hello"
# -> ""

ブレース: {}

bash のブレース展開についてまとめ

$(***)

コマンドの実行結果に置換される。

echo $(date '+%Y')
# -> 2019

-

ハイフン(-)はファイルの代わりに標準出力を使うことを示している。 例えば、標準出力に文字列を出力するコマンドをパイプでつないで、後ろのコマンドでファイルの代わりにその文字列を使いたいときなどに利用できる。

# catはもともとファイルを引数にとるが、"-"にすることでパイプの標準出力を出力する
echo "hoge" | cat -
# -> hoge

:-

左辺が存在しない場合に右辺を返す(デフォルト値)。

echo ${str:-'default'}
# -> default

str=aaa
echo ${str:-'default'}
# -> aaa

利用例。関数の第一引数が指定されていたらそれを、指定されていなければ現在のディレクトリを dir 変数に代入する。

dir=${1:-`pwd`}

!!

直前のコマンドに置換される。

echo hoge
# -> hoge
!!
# -> hoge

!$

直前の引数に展開される。

touch script.sh
chmod +x !$

コマンド

scp: リモートーローカル間でファイルをコピー

# ローカルからリモートにコピー
scp hoge.txt <user>@<host>:<pass>
# ex: hoge.txtをリモートの /home/hako1912 ディレクトリにコピー
scp hoge.txt cloud-user@my-cloud-host:/home/hako1912

# ローカルファイルを踏み台サーバーを経由して直接コピー
scp -o "ProxyCommand ssh <踏み台user>@<踏み台host> -W %h:%p" hoge.txt <user>@<host>:<path>

wc: ファイル行数をカウント

wc -l hoge.txt

mkdir

# パスが存在しない場合は親ディレクトリ含め作成する。エラーも表示しない。
mkdir -p a/b/c

history

コマンド履歴を表示する。

history

# コマンド名でソート
history | sort -k2

# !コマンドの隣の数字でコマンドを実行できる
!10

nc (netcat)

https://qiita.com/yasuhiroki/items/d470829ab2e30ee6203f

# localhostの8080ポートが空いているか確認
nc -zv localhost 8080

dos2unix

インストール。

sudo apt install -y dos2unix
# ファイルは上書きされる
dos2unix <file>

# 再帰的に変換
find <dir name> -type f -print0 | xargs -0 dos2unix

# 拡張子で絞り込んで変換
find . -type f | egrep '\.(kt|yml|yaml|adoc|sh|json|sql)$' | xargs dos2unix

ps

プロセスの一覧を表示する。

ps

# 他のユーザのプロセスも含めすべて表示
ps a

# 詳細表示
ps u

# プロセスの親子関係も表示
ps u

# ユーザー指定して表示
ps -U <user name>
# 指定したユーザのPIDのみ表示
ps -U <user name> o pid --no-headers

date

date '+%Y/%m/%d'
# 2019/03/05

find

# 指定した拡張子のファイルを一覧表示
find . -type f | egrep '\.(kt|yml|yaml|adoc)$'

cut

区切り文字を指定してパースする。

# :で区切られた2番目の項目のみ出力する
cut -d: -f2 hoge.txt

# 例: ユーザ名の一覧を表示する
cut -d: -f1 /etc/passwd

# 例: 最後に登録したユーザ名を表示する
cut -d: -f1 /etc/passwd | tail -n1

tail

最終行から行を表示する。

tail <file name>

# 最後から数えて2行目までを表示
tail -n 2 hoge.txt

# 最初の1行をスキップ。n+1で指定することに注意
tail -n +2 hoge.csv

sed

ファイルの特定の文字列を置換する

# hoge.yamlの'origin'を'replaced'に置換したファイルをfuga.yamlとして出力する
sed 's/origin/replaced/' hoge.yaml > fuga.yaml

# hoge.yamlの'origin'を'replaced'に置換して上書きする
sed -i 's/origin/replaced/' hoge.yaml

chmod

権限を付与する。

# 実行権限を付与する
chmod +x my-script.sh

jq

json のパーサ。windows の場合は jq.exe をインストールする必要がある。 PowerShell を管理者で開いて次のコマンドを実行する。

chocolatey install jq
# jsonを整形してそのまま出力
echo '{ "foo":123 }' | jq

# キーを指定して値を取得する
echo '{ "foo": 123 }' | jq '.foo'
# 123

# ネストしているオブジェクトから取得する
echo '{"a": {"b":123 } }' | jq '.a.b'
# 123

# オブジェクトの配列から特定のキー値のみ取得する
echo '{ "items": [ { "val": 1 }, { "val": 2 }, { "val": 3 } ] }' | jq '.items[].val'
# 1
# 2
# 3

# キーをファイル名を指定して取得する
jq '.dependencies' package.json
# {
#  "express": "^4.16.4",
#  "lodash": "^4.17.11",
# }

# 特殊な値`keys`を使うと、キーの配列を取得できる
jq '.dependencies | keys' package.json
# [
#  "express",
#  "lodash"
# ]

# さらにパイプでつないでパースできる。
jq '.dependencies | keys | .[]' package.json
# "express"
# "lodash"

# さらにrオプションを使うとクォートを取り除ける
jq -r '.dependencies | keys | .[]' package.json
# express
# lodash

# REST APIから取得したjsonをパースして出力する例
curl -s https://api.github.com/repos/facebook/react | jq '.stargazers_count'

# jsonファイルを1行文字列にして出力
cat hoge.json | jq -c
# 出力結果をさらにパイプに渡す場合はフィルタが必要
# '.'はなにもしないフィルタ
cat hoge.json | jq -c . > aaa.txt

# 特殊文字は""で囲む
echo '{"$": 100}' | jq '."$"'

ルートが配列の JSON に対してフィルタリングする例:

JSON='[{"type":"A","value":1},{"type":"A","value":2},{"type":"B","value":3}]'
echo $JSON | jq 'map(select(.type == "A"))' | jq '.[].value'

ssh キー生成

# 鍵生成
ssh-keygen -t rsa -f ~/.ssh/[KEY_FILENAME] -C [USERNAME]

# 秘密鍵を古い形式で出力 `-m PEM`
ssh-keygen -t rsa -f ~/.ssh/[KEY_FILENAME] -C [USERNAME] -m PEM

git pushで以下のようなエラーがでることがある。

Permissions 0777 for '.ssh/id_rsa' are too open

そのときは権限を 600 に変更する。

chmod 600 <id_rsaのパス>

ref: ssh “permissions are too open” error

ln: シンボリックリンク作成

ln -s [実体] [リンク]

ln -s ~/dotfiles/.bashrc ~/.bashrc

# -f: 強制上書き
ln -sf a.txt b.txt

# -n: -fオプションを付けても対象がディレクトリであった場合は上書きされない。-nをつけると上書きしてくれる。
ln -sfn a.txt b.txt

# -v: 経過を表示する
ln -sfnv a.txt b.txt

cp: ファイルコピー

次のファイルツリーの場合。

src
   - a1.txt
dist
# srcディレクトリごとdistディレクトリへコピー
cp src dist
ls dist
# ->
# src

# srcディレクトリの中身をdistディレクトリへコピー
cp src/* dist
ls dist
# ->
# a1.txt

xargs

前のコマンドの出力結果をコマンドライン引数に変換する。

echo "hello" | xargs echo
# -> hello hello

引数の位置を変えたい場合は-Iオプションを使う。

# {} が引数に置き換わる
echo "hello" | xargs -I {} echo "[" {} "]"
# -> [ hello ]

tar: .tgzを解凍する

tar -xzf [ファイル名].tgz

標準入力/標準出力

標準出力の一覧を取得

ls -la /dev/ | grep 'std.*'
# lrwxrwxrwx 1 hatak 197609       15 9月  28 23:15 stderr -> /proc/self/fd/2
# lrwxrwxrwx 1 hatak 197609       15 9月  28 23:15 stdin -> /proc/self/fd/0
# lrwxrwxrwx 1 hatak 197609       15 9月  28 23:15 stdout -> /proc/self/fd/1

リダイレクト

コマンドの結果を出力

# 正常ログのみ出力
ls hoge 1> ls.txt

# エラーのみ出力
ls noexist 2> ls-errs.txt

# 正常もエラーも両方出力
ls noexist > ls.txt 2>&1

コマンドの引数へ入力

# ファイルの中身をコマンドへ渡す
cat < ls.txt

ターミナルのショートカットキー

ctrl + A

行頭へ移動

ctrl + E

行末へ移動

ctrl + K

カーソル位置より後ろを削除

ctrl + W

カーソル位置から直前の単語を削除

ctrl + L

コンソールをクリア

Tips

sudo をパスワードなしで実行できるようにする

USER='hoge'
echo "$USER ALL=NOPASSWD: ALL" | sudo EDITOR='tee -a' visudo

Ref:
linux - How do I edit /etc/sudoers from a script? - Stack Overflow

別のユーザでコマンドを実行する

複数行のスクリプトを実行する場合は以下のようにする。
ヒアドキュメント内で変数を参照する場合は"\$HOME"のようにバックスラッシュを前に置く必要があることに注意。

sudo -i -u $OTHER_USER bash << EOF
#!/usr/bin/env bash
echo "\$USER"
echo "\$HOME"
echo "hoge"
EOF

Ref:
shell script - setting variables inside subshell when using << - Unix & Linux Stack Exchange

引数の必須チェック

if [ -z "$1" ] ; then
    echo "hoge is required."
    exit 1
fi
hoge=$1

GNU parallel で並列処理

インストール。

# mac
brew install parallel
# ubuntu
apt install parallel

parallel <command> ::: <スペース区切りの文字列>で並列処理できる。

parallel echo ::: a b c

# 他のシェルを実行
parallel ./hoge.sh ::: a b c

コマンドの結果を 1 つずつ for でループ

items=$(command...)

for item in $items
do
  echo $item
done

各行ごとに指定した文字列の出現回数をカウント

awk -F'|' 'BEGIN{print "count", "lineNum"}{print gsub(/,/,",") "\t" NR "\t" $0 }' <FILE>

引数の数チェック

$#を使って受け取った引数の数をチェックできる。

# 受け取った引数が2つ未満なら使い方を表示して終了
if [ "$#" -ne 2 ]; then
   echo "Usage:   ./my-script.sh SOURCE_PATH DIST_PATH"
   echo "Example: ./my-script.sh hoge.txt fuga.txt"
   exit
fi

カレントディレクトリ配下のディスク容量を確認する

du -h --max-depth=1 .

ディレクトリを.tarファイルに圧縮する

  • c: アーカイブ作成
  • f: アーカイブファイル名指定
tar cf <圧縮後のファイル名> <圧縮するディレクトリ名>>
# ex: tar cf dir.tar dir

システムが起動してからの時間を分単位で取得する

Ubuntu の場合はapt install bcしておく。

echo $(awk '{print $1}' /proc/uptime) / 60 | bc

Ref: https://askubuntu.com/questions/335592/how-to-display-time-elapsed-since-last-system-boot-using-uptime

コマンドの処理結果が空の場合に任意のコマンドを実行

<コマンド> | [ $(wc -c) -eq 0 ] && <コマンド結果が空だったときに実行するコマンド>

# /home 配下のファイルが120分以上編集されていなかったらシャットダウン
sudo find /home -type f -mmin -120 | [ $(wc -c) -eq 0 ] && shutdown -h now

Ref: https://stackoverflow.com/a/42884374

tar.gz を解凍する

tar -zxvf <file name>

指定したディレクトリ配下のディレクトリ一覧を取得

! -path <path>で結果から自身のパスを除外できる。

DIR='.'
for FILE in $(find $DIR ! -path $DIR -maxdepth 1 -type d); do
    echo "$FILE"
done
### 指定したポートを使用しているプロセスをkillする

```bash
lsof -i :<port> | tail -n +2 | tr -s ' ' | cut -d' ' -f 2 | xargs kill -9

複数行テキストのファイルを変数に代入、表示

複数行文字列を echo するときはダブルクォートで囲む必要がある。

# OK
echo "$(multiline.txt)" > hoge.txt
# NG
echo $(multiline.txt) > hoge.txt

Ref: https://orebibou.com/2014/11/シェルスクリプトで変数に改行コードを含める方/

変数の計算結果を使う

$[<計算処理>]でできる。

COUNT=10
for i in $(seq $[$COUNT -1] 1)
do
  echo "i = $i";
done

Ref: http://omoisan.hatenablog.com/entry/20120307/1331048559

回数を変数にして指定回数ループ

COUNT=10
for i in $(seq 0 $COUNT)
do
  echo "i = $i";
done

Ref: https://stackoverflow.com/questions/169511/how-do-i-iterate-over-a-range-of-numbers-defined-by-variables-in-bash

2 つのファイルで重複行以外を取り出す

uniq -uで重複した行を除外している。

awk '{ print $1 }' 1.txt 2.txt | sort | uniq -u | head -n 10

tsv, csv の整形など

# ヘッダ行を除いて表示
cat hoge.csv | tail -n +2

# ヘッダ行を除いた行数を表示
cat hoge.csv | tail -n +2 | wc -l

# 1列目でソート
cat hoge.csv | tail -n +2 | sort -k 1

Ref: https://stackoverflow.com/a/604871

コマンドのエラーを無視しつつ終了コードを取得する

#!/bin/bash -eu

<command> || echo $?; true

# エラーになっても即終了せずに終了コードを判定して独自の処理を入れる
<command> > /dev/null || true; result=$?
if [ $result -eq 1 ]; then
  echo 'error!'
  exit 1
fi

bash プロンプトに色をつける

Ref:
ANSI エスケープシーケンス チートシート - Qiita

Ascii 文字をエスケープする

プロンプトに色付き文字を表示するときに使える。

FOREGROUND_RED='\e[38;5;160m'
red_string() {
  echo -e "${FOREGROUND_RED}赤い文字"
}

PS1="\$(red_string)"

Ref:
hex - How to type ASCII code "00" and "01" in linux bash? - Stack Overflow

文字列が一致するか判定する

hoge='test'
if [ "$hoge" = 'test' ]; then
    echo "hoge == test"
else
    echo "hoge != test"
fi

Mac で色付き文字をエスケープする

printfを使う。
Ubuntu などではecho -eでも動くが、Mac の場合はecho -eだとうまく動かないのでとりあえずprintfを使うと安全。

RED='\e[48;5;160m'
printf "${RED}赤い文字\n"

# MacでNG
echo -e "${RED}赤い文字"

Ref:
CentOS7 と OS X Mavericks の echo コマンドの違いについて - はらへり日記

.gz 形式のログを見る

zcatで OK だけど MAC の場合はgzcatにしないといけない。

gzcat hoge.gz

.gz 形式のログを日付順にソートして 1 ファイルに結合する

find . -name '*.gz' | sort | xargs gzcat >> hoge.log

半角スペースつきのパスへ cd する

# \でスペースをエスケープできる
cd ~/My\ Code

# 変数に入れた場合は"で囲めばOK
VSCODE_INSIDER_PATH="/c/Users/hoge-user/AppData/Roaming/Code - Insiders/User"
cd "${VSCODE_INSIDER_PATH}"

Ref: https://stackoverflow.com/questions/589149/bash-script-to-cd-to-directory-with-spaces-in-pathname/589210

ディレクトリ内のファイルでループ

カレントディレクトリから深さ 2 までのテキストファイルに対して繰り返すときの例。

for FILE in $(find . -maxdepth 2 -type f -name "*.txt"); do
    program -in $FILE -out $FILE.out
done

フルパスからファイル名だけ取り出し

file='aaa/bbb/ccc.txt'
echo `basename $file`
# => ccc.txt

sedを使うパターン。

echo "/aaa/bbb/ccc/ddd.json" | sed -E 's/.*\///'

ファイル名から拡張子を取り除く

file='ccc.txt'
file_name=`basename $file | sed 's/\.[^\.]*$//'`
echo $file_name
# => ccc

カレントディレクトリ配下の json ファイルを繰り返し処理で POST する

# これでもいい
# for file in ./permissions/*.json; do
for file in `\find . -maxdepth 1 -type f -name "*.json"`; do
    echo $file
    curl -X POST -H 'Content-Type:application/json' -d @${file} localhost:8080
done

/c/tools/配下のbinフォルダへ自動でパスを通す

for FILE in $(find /c/tools -maxdepth 2 -type d -name "bin"); do
    echo $FILE
done

ポートをつかんでいる PID、サービス名を取得

# 3306番ポートを使っているプロセスを表示してPIDを確認
netstat -ano | grep ":3306"

# PIDでフィルタしてサービス名を取得
# ※確認したPIDが`5992`だったときの例
tasklist -fi "PID eq 5992"

指定した URL に接続できるまで待機

Docker で他のコンテナサービスが立ち上がるまで待ちたいときとか。

until curl localhost:3000 ; do
   sleep 3
done
echo "connected..."

自分のコンテナのサービス立ち上げを待機してから初期データを入れたいときは、待機してからデータ挿入する shell をバックグラウンドで実行するようにすればいい。

FROM couchbase

# `waitAndEcho.sh`で、localhost:8091の接続待機後にデータ挿入
RUN ./waitAndEcho.sh &

サブシェル

一時的にcdして違うディレクトリでコマンドを打ちたいけど実際にディレクトリ移動はしたくないときなど。

# 一つ上の階層でls
(cd ..; ls)

sudo でファイルにリダイレクトする

sudoでリダイレクトしたい場合は代わりにteeを使う。

# OK
echo 'hoge' | sudo tee -a hoge.txt
# NG
sudo echo 'hoge' > hoge.txt

Ref: http://yut.hatenablog.com/entry/20111013/1318436872

CLI ツールなどでコマンド出力を変数に入れられないとき

標準エラーに出力しているので、標準エラーを標準入力に出力するようにしてから変数に代入する。

hoge=$([command] 2>&1)

標準エラーのみ捨てる

[command] 2>/dev/null

Ref: Linux / Unix | 標準エラー出力を捨てる

シェル自身のパスを取得

ref: Get the source directory of a Bash script from within the script itself

# シェルがあるディレクトリの絶対パスを取得(おすすめ)
SCRIPT_DIR=$(cd $(dirname $0); pwd)

# 実行する場所によっては動作しないけどシンプルver
dir=`dirname ${0}`

# こっちのほうが安全らしい
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

.shファイルでシェルのパスへ cd するとカレントをシェルのパスとして扱えるので、とりあえず先頭につけておくと便利。

#!/bin/bash
cd `dirname ${0}`

指定したディレクトリのファイル名の一覧を取得

find <directory> -type f -printf "%f\n"

Ref: For files in directory, only echo filename (no path)

ファイルの存在判定

FILE=/etc/resolv.conf
if [[ -f "$FILE" ]]; then
    echo "$FILE exist"
fi

Ref:
How to Check if a File or Directory Exists in Bash | Linuxize

ディレクトリの存在判定

[ -d <path> ]でディレクトリの存在判定ができる。

# VSCodeの設定ファイルのディレクトリがあるか判定する
VSCODE_PROFILE_DIR="/c/Users/${USER}/AppData/Roaming/Code/User"

if [ ! -d $VSCODE_PROFILE_DIR ]; then
    echo 'dir not exists...'
fi

変数が定義されているか判定

[ -z <name> ]でできる。

# HOGEが定義されていればtrue, それ以外でfalse
# 正確にはHOGEの文字列長が0かどうかで判定している
if [ -z $HOGE ]; then
    echo 'HOGE is not defined'
    exit 1
fi

ssh, scp のパスワード入力プロンプトなしで実行

sshpassを入れる。

# mac
brew install https://raw.githubusercontent.com/kadwanev/bigboybrew/master/Library/Formula/sshpass.rb

# alpine
apk add sshpass

Ref: https://stackoverflow.com/a/32258393

次のようにしてログインする。

sshpass -p <password> <user>@<host>

scpのファイルコピーもパスワードなしでできる。

sshpass -p <pass> scp <user>@<host>:/home/hoge/test.log .

ローカルマシンの関数を直接リモートで実行する例。

f() { ls -la; }

sshpass -p <pass> ssh <user>@<host> "`typeset -f f`; f"

Ref: https://codeday.me/jp/qa/20190211/238583.html

特定のポートを使っているプロセスを kill する

lsof -i :<ポート番号>でプロセス ID を調べられる。

lsof -i :80

# 調べたPIDでkillする
kill -9 <PID>

バックグラウンド処理を stdout へ出力

command > /dev/null 2>&1 &

プロセス関連

# プロセスIDを確認する
ps

# 停止したいプロセスのWINPIDを指定して実行する
taskkill -pid [WINPID] -f

bash で ctrl+c が効かなくなったときは、その bash で実行中のプロセスを停止すると復活する。

無限ループ

while true
do
   echo "hello"
done

1 行で書くとこうなる。dodoneの後ろだけセミコロンがいらないことに注意。

while true; do echo "hello"; done

# trueはコロンにできる
while :; do echo "hello"; done

# 1行関数の例
hoge() { while :; do echo "hello"; done }

引数の空判定

ref: Bash Shell Find Out If a Variable Is Empty Or Not

## syntax 1 ##
if [[ -z "$variable" ]]; then
   echo "Empty $variable"
else
   echo "Do whatever you want as \$variable is not empty"
fi
## Syntax 2 ##
[[ -z "$variable" ]] && echo "Empty" || echo "Not empty"
## Syntax 3 ##
[  -z "$var" ] && echo "Empty: Yes" || echo "Empty: No"

ref: How to check if a variable is set in Bash?

if [ -z ${var+x} ]; then echo "var is unset"; else echo "var is set to '$var'"; fi

ワンライナーで引数の空判定をする例。

# 第一引数が空ならfoo、そうでなければbarを$aにいれて、$aを表示
hoge() {  [ -z $1  ] && a='foo' || a='bar'; echo $a ; }

シェルスクリプト内でエラーが起きたら即終了する

# これ以降エラーや未定義変数を参照するとシェルが終了する
set -eu

# 解除
set +eu

シェルスクリプト内で使う場合はシェバンに-euを付けると、シェルスクリプト内でのみ有効(sourceで他のシェルを呼び出した場合、そのシェルには-euは適用されない)となる。

#!/bin/bash -eu

解除もしなくていいのでこっちの方が便利。

ワンライナーで複数行ファイルを sudo で作成

echo '<文字列>' | sudo tee <file path>

# ファイル末尾に追記
echo '<文字列>' | sudo tee --append <file path>

# コンソールに結果を出さない場合
echo '<文字列>' | sudo tee --append <file path> > /dev/null

# 改行コードなどを文字列に使いたい場合
printf "aaa\nbbb\nccc" | sudo tee <file path>

ref: sudo echo “something” >> /etc/privilegedFile doesn't work

ワンライナーでユーザ作成

sudo useradd -p $(openssl passwd -1 <password>) <user name>

ref: How do I create a user with a password in one line, in Bash, on Redhat?

ワンライナーで他ユーザの bash セッション開始

sudo -u <user name> bash

コマンドが成功するか判定

yarnコマンドがある場合のみパスを通す例

if yardn global bin ; then
    export PATH="$PATH:`yarn global bin`"
else
    echo "yarn not installed..."
fi

ユーザのホームディレクトリを変更

usermod -m -d <home directory> <user name>

ツール

ShellCheck

shell 用の Lint。 https://www.shellcheck.net/ https://github.com/koalaman/shellcheck

VSCode の拡張機能もある。 https://github.com/timonwong/vscode-shellcheck

インストール

brew install shellcheck

apt install shellcheck

FAQ

stdout is not a tty, stdin is not a tty

winpty bashしてから同じコマンドを実行すれば動く。