こんにちは。
新政水墨です。
先日、このコマンドの書き方ってなんだろう?どういう挙動するの?と、シンプルだけど知らなかったものがあったので、意外とみんなが知らなそう・使ったことなさそうなものを10個セレクトしてみました。
おそらく中級者向けレベルになっております。
1. > file 2>&1 と 2>&1 > file は別物
背景
エラーをファイルにリダイレクトしたいとき、なんとなくコピペで済ませていませんか? この2つは見た目が似ていますが、動作がまったく異なります。
コード例
# ✅ 正しい:stdoutもstderrもfileに書かれる
command > file 2>&1
# ❌ 意図と違う:stderrはターミナルに出続ける
command 2>&1 > file
解説
リダイレクトは左から順に評価されます。
command 2>&1 > file の評価順:
-
2>&1→ 「stderrをいまのstdout(=ターミナル)に向ける」 -
> file→ 「stdoutをfileに向ける」(stderrはもう変わらない)
結果、stderrはターミナルに出続けます。
command > file 2>&1 の評価順:
-
> file→ 「stdoutをfileに向ける」 -
2>&1→ 「stderrをいまのstdout(=file)に向ける」
両方fileに書かれます。&>file は > file 2>&1 の短縮形として bash 4.0+ で使えます。
# bash限定の短縮形
command &> file
command &> /dev/null
2. プロセス置換 <(command) でコマンド出力をファイルのように扱う
背景
diff で2つのコマンド出力を比べたいとき、いちいち一時ファイルに書き出していませんか? プロセス置換を使えばその手間が消えます。
コード例
# 2つのディレクトリのファイル一覧を比較
diff <(ls dir1/) <(ls dir2/)
# ソートしてから比較
diff <(sort file1.txt) <(sort file2.txt)
# コマンド出力をファイル引数に渡す
comm -23 <(sort list1.txt) <(sort list2.txt)
解説
<(command) は /dev/fd/63 のような仮想ファイルに展開されます。コマンドを引数にファイルパスを要求するツールへ渡せるため、一時ファイルなしで複雑なパイプラインを組めます。
出力側の >(command) も使えます。
# teeで分岐しながらパイプへ渡す
cat access.log | tee >(grep ERROR > errors.log) | wc -l
3. ヒアストリング <<< で echo | grep を卒業する
背景
変数の内容を grep や sed に渡すとき、echo "$var" | grep ... と書くのが習慣になっていませんか? <<< を使うと1つのサブシェルを省けます。
コード例
# 従来の書き方
echo "$response" | grep -o '"status":"[^"]*"'
# <<< を使った書き方
grep -o '"status":"[^"]*"' <<< "$response"
# read への応用
read -r first rest <<< "hello world foo"
echo "$first" # hello
echo "$rest" # world foo
# base64のデコード
base64 --decode <<< "aGVsbG8="
解説
<<< はヒアストリング(here-string)と呼ばれ、文字列を直接stdinに渡します。echo "$var" | command との違いは、パイプがサブシェルを生成しないため read の結果が現在のシェルに残ること。read をパイプで使うと変数がサブシェルに閉じてしまう問題を避けられます。
# NG: パイプ内のreadは現在シェルに反映されない
echo "hello world" | read first rest
echo "$first" # 空になる
# OK
read first rest <<< "hello world"
echo "$first" # hello
4. パラメータ展開4種 ${:-} ${:+} ${:?} ${:=}
背景
シェルスクリプトの引数チェックや環境変数のデフォルト設定を if [ -z "$VAR" ] で書いていませんか? パラメータ展開を使えばずっと簡潔に書けます。
コード例
# ${var:-default} : 未セットまたは空なら default を使う(変数は変えない)
echo "${NAME:-anonymous}"
# ${var:+value} : セットされていて空でないなら value を使う
echo "debug mode: ${DEBUG:+on}"
# ${var:?message} : 未セットまたは空ならエラーで終了
: "${API_KEY:?ERROR: API_KEY is not set}"
# ${var:=default} : 未セットまたは空なら default を代入して使う
echo "${TMPDIR:=/tmp}"
解説
| 記法 | 意味 |
|---|---|
${var:-default} |
未セット・空 → default を返す(変数は変わらない) |
${var:+value} |
セット済み・非空 → value を返す |
${var:?msg} |
未セット・空 → エラー終了 |
${var:=default} |
未セット・空 → default を代入してから返す |
スクリプトの先頭で必須変数を一気にチェックするときは :? が便利です。
#!/bin/bash
: "${DB_HOST:?}"
: "${DB_USER:?}"
: "${DB_PASS:?}"
5. set -euo pipefail ―スクリプトの安全3点セット
背景
set -e だけ書いてあるスクリプトをよく見かけますが、それだけでは穴があります。3つをセットで使う理由を知っていますか?
コード例
#!/bin/bash
set -euo pipefail
解説
-e(errexit):コマンドが非ゼロで終了したらスクリプトを即終了。ただし、パイプラインの途中のコマンド失敗や未定義変数は検知しません。
-u(nounset):未定義変数を参照したらエラー終了。タイポによるバグを防ぎます。
set -e
# -e だけだと気づかず通ってしまう
echo $UNDEF_VAR # 空文字として展開され、エラーにならない
-o pipefail:パイプラインのどこかのコマンドが失敗したら、パイプライン全体の終了コードを失敗にします。
set -e
# -e だけだとパイプ最後のコマンドが成功すれば通ってしまう
grep "pattern" nonexistent_file | sort
# grep は失敗するが sort が成功するため -e では止まらない
3つをまとめて書く慣用句として set -euo pipefail をスクリプト冒頭に置く習慣をつけましょう。
6. trap ... EXIT でスクリプト終了時のクリーンアップ
背景
一時ファイルを使うスクリプトで、正常終了のときだけ rm を呼んでいませんか? trap を使えばエラー終了や Ctrl+C でも確実にクリーンアップできます。
コード例
#!/bin/bash
set -euo pipefail
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT # EXIT: 終了時に必ず実行される
# 処理
curl -s https://example.com/data > "$TMPFILE"
process "$TMPFILE"
解説
trap 'コマンド' シグナル で、指定したシグナルや終了イベントにフックを登録できます。
| シグナル | タイミング |
|---|---|
EXIT |
スクリプトがどんな理由でも終了するとき |
ERR |
コマンドが非ゼロで終了したとき |
INT |
Ctrl+C が押されたとき |
TERM |
kill コマンドで終了させられるとき |
複数の後処理がある場合は関数にまとめるのが定石です。
cleanup() {
rm -f "$TMPFILE"
echo "cleaned up" >&2
}
trap cleanup EXIT
7. column -t で出力を一瞬でテーブル整形
背景
スペース区切りの出力がずれていて読みにくいとき、awk で頑張って整形していませんか? column -t で一発です。
コード例
# mountの出力を整形
mount | column -t
# 区切り文字を指定(/etc/passwdをコロン区切りで整形)
column -t -s: /etc/passwd
# コマンド出力をパイプで整形
{ echo "NAME STATUS AGE"; kubectl get pods --no-headers; } | column -t
解説
column -t は入力の各行を空白区切りのフィールドとして解析し、列幅を自動調整します。ログやCSVを素早く目視確認したいときに便利です。
# Before
alice 30 engineer
bob 25 designer
charlie 40 manager
# After (column -t)
alice 30 engineer
bob 25 designer
charlie 40 manager
-s で区切り文字を指定でき、-o で出力区切り文字も変えられます(GNU coreutils版)。
8. xargs -P n で並列実行
背景
ファイルを1つずつ処理するループを書いていませんか? xargs -P でCPUコア数に合わせた並列実行に切り替えられます。
コード例
# ログファイルを4並列でgzip圧縮
find /var/log -name "*.log" | xargs -P 4 -I{} gzip {}
# URLリストを8並列でcurl
cat urls.txt | xargs -P 8 -I{} curl -s -o /dev/null -w "%{http_code} {}\n" {}
# 画像ファイルを並列でリサイズ
find . -name "*.jpg" | xargs -P $(nproc) -I{} convert {} -resize 800x {}
解説
-P n は最大 n 個のプロセスを同時に実行します。-P 0 にするとシステムが許す限り並列化します。$(nproc) でCPUコア数を自動取得するのが定番です。
-I{} は入力行を {} に展開するプレースホルダーです。-I を使わない場合、xargs はデフォルトでコマンドの末尾に引数を追加します。
# -I なし(引数はコマンドの末尾にまとめて渡される)
echo "a b c" | xargs -P 3 echo # echo a b c が1回
# -I{} あり(1行ごとに1プロセス)
printf "a\nb\nc\n" | xargs -P 3 -I{} echo {} # 3並列でecho
9. find -exec {} + は -exec {} ; より速い
背景
find -exec を使うとき、ほとんどの人が ; で終わらせています。+ に変えるだけで劇的に速くなるケースがあります。
コード例
# ❌ 遅い:ファイルごとに chmod を1回ずつ実行
find . -name "*.sh" -exec chmod +x {} \;
# ✅ 速い:ファイルをまとめてchmodに渡す
find . -name "*.sh" -exec chmod +x {} +
# 実際の実行イメージの違い
# \; → chmod +x a.sh, chmod +x b.sh, chmod +x c.sh(3回)
# + → chmod +x a.sh b.sh c.sh(1回)
解説
-exec command {} \; はマッチしたファイル1つごとにコマンドを1回実行します。-exec command {} + はファイルをできる限りまとめて1回のコマンドに渡します(xargs と同じ動作)。
ファイル数が多いほど差が出ます。ただし、+ は {} がコマンドの末尾にないと使えない制約があります。
# NG: {} が末尾でないので + は使えない
find . -name "*.txt" -exec cp {} /backup/ + # エラー
# OK: この場合は ; を使う
find . -name "*.txt" -exec cp {} /backup/ \;
10. watch -d で変化点だけをハイライト
背景
watch コマンドで繰り返し実行していても、どこが変わったか一目でわからないことがあります。-d をつけるだけで変化点が強調表示されます。
コード例
# 1秒ごとにdf -hを実行し、前回との差分をハイライト
watch -d -n 1 'df -h'
# ネットワーク統計を監視
watch -d -n 2 'ss -s'
# 特定プロセスのメモリ使用量を監視
watch -d -n 1 'ps aux | grep nginx | grep -v grep'
# Kubernetesのpod状態を監視
watch -d -n 5 'kubectl get pods'
解説
-d(--differences)は前回の出力と比較し、変化した部分を反転表示します。-d=cumulative にすると、セッション開始から一度でも変化した箇所をすべて強調し続けます。
| オプション | 意味 |
|---|---|
-n 秒 |
更新間隔(デフォルト2秒) |
-d |
変化箇所をハイライト |
-d=cumulative |
変化したことがある箇所を累積ハイライト |
-t |
ヘッダーを非表示 |
-e |
コマンドがエラーで終了したら強調して停止 |
デプロイ中のログ監視、ディスク使用量の変化追跡など、「何かが変わった瞬間を見たい」場面で重宝します。
まとめ
| # | 記法・コマンド | 一言 |
|---|---|---|
| 1 | > file 2>&1 |
リダイレクトは順序が命 |
| 2 | <(command) |
コマンド出力をファイルとして扱う |
| 3 | <<< |
変数をstdinに渡す最短ルート |
| 4 |
${:-} ${:+} ${:?} ${:=}
|
引数チェック・デフォルト値の定石 |
| 5 | set -euo pipefail |
スクリプトの安全3点セット |
| 6 | trap ... EXIT |
後処理の確実な登録 |
| 7 | column -t |
出力整形を一行で |
| 8 | xargs -P n |
ループを並列化 |
| 9 | find -exec {} + |
; より速いfind実行 |
| 10 | watch -d |
変化点の可視化 |
シェルスクリプトの信頼性と日常作業の効率に直結するものを選びました。どれも「知ってから使い始めると、以前には戻れない」類のものです。