ysapでbash入門③
bash学習記録:基礎編(Chapter 04, Section 05まで)
今回はysapのChapter 04 Section 05まで進んだ。かなりヘビーな内容で、配列、コマンド置換、算術演算など、bashの核心的な機能を学んだ。
環境のアップデート
新しいbashのインストール
macOSのデフォルトbash(3.2)では最新機能が使えないため、Homebrewで新しいbashをインストールした。
brew install bash
/opt/homebrew/bin/bash --version
# GNU bash, version 5.3.9
これにより、負のインデックス ${array[-1]} などの新機能が使えるようになった。
Vimプラグインの追加
Daveの設定を参考に、以下のプラグインを追加:
Plug 'tpope/vim-surround' " クォートや括弧の操作
Plug 'tpope/vim-commentary' " コメントの切り替え
vim-surroundの便利な使い方
cs'" " ' を " に変更
cs'` " ' を ` に変更
ysiw[ " 単語を [ ] で囲む
ds" " " を削除
vim-commentaryの使い方
gcc " 現在行をコメント/アンコメント切り替え
5gcc " 5行をコメント切り替え
gc4j " 下4行をコメント切り替え
Daveのハードモード設定
Daveの .vimrc を確認したところ、驚くべき設定が:
let g:ale_linters = {
\ 'bash': [], " bashのLinterは無効!
\ 'sh': [],
\}
set nonumber " 行番号なし
set tabstop=8 " タブ8スペース
colorscheme default
set background=light " ライトモード
完全に「昔ながらのUnixおじs…」,おっと…仙人スタイルですね。初心者にVimを使わせて、さらにLinterなしという鬼畜仕様。ただし set number だけは一度有効化したけど、結局すぐに消した(笑
標準エラー出力とリダイレクト
>&2 の意味
echo 'name required!' >&2
exit 1
>&2 は標準エラー出力(stderr)にリダイレクトする記号。
標準ストリーム
- 標準入力(stdin): 0
- 標準出力(stdout): 1
- 標準エラー出力(stderr): 2
なぜ分けるのか
# 標準出力と標準エラー出力を別々に扱える
./script.sh > output.txt 2> error.txt
# 標準出力だけをパイプで渡せる
./script.sh | grep something # エラーメッセージはパイプに流れない
終了ステータス
exit 0 # 正常終了
exit 1 # エラー終了
# 直前のコマンドの終了ステータスを確認
echo $?
配列の基礎
インデックス配列
array=(foo bar baz)
echo "${array[0]}" # foo
echo "${array[1]}" # bar
echo "${array[2]}" # baz
# 負のインデックス(Bash 4.3+)
echo "${array[-1]}" # baz(最後の要素)
# 配列の長さ
echo "${#array[@]}" # 3
配列のイテレーション
# ✅ 正解:これを使う
for item in "${array[@]}"; do
echo "$item"
done
重要なポイント:
"を付ける:スペースを含む要素を正しく扱える@を使う:各要素を個別に展開する(*は全要素が1つの文字列になる)
連想配列(Associative Array)
# declare -A が必須!
declare -A arr
arr[foo]=1
arr[bar]=2
arr[baz]=3
echo "${arr[foo]}" # 1
# キーの取得(! を付ける)
echo "${!arr[@]}" # foo bar baz
# キーでループ
for key in "${!arr[@]}"; do
value=${arr[$key]}
echo "got $key=$value"
done
declareオプションの意味
-i: integer(整数)-A: Associative array(連想配列)- 必須-a: array(通常の配列)- 省略可能-r: readonly(読み取り専用)-x: export(環境変数)
IFS(Internal Field Separator)
文字列を分割する時に使う区切り文字を定義する特殊変数。
# デフォルト(スペース、タブ、改行)
IFS=$' \t\n'
# カンマで区切りたい
text="foo,bar,baz"
IFS=','
for word in $text; do
echo "$word"
done
# CSV読み込み
while IFS=',' read -r col1 col2 col3; do
echo "Column 1: $col1"
done < data.csv
ベストプラクティス
# 関数スコープに限定
function parse_csv() {
local IFS=','
read -r var1 var2 var3 <<< "$1"
}
# 一時的な変更
IFS=',' read -r a b c <<< "$line"
スクリプト内なら普通に変更してもOK(終了時にリセットされる)。
コマンド置換
従来の方法
# バッククォート(古い書き方)
thing=`whoami`
# $() 推奨
thing=$(whoami)
echo "$thing"
Bash 5.3+の新機能:${ command; }
i=5
my-func() {
i=6
echo "hi"
}
# 従来:サブシェルで実行(グローバル変数は変わらない)
thing=$(my-func)
echo "i is $i" # 5
# 新機能:現在のシェルで実行(グローバル変数も変わる)
thing=${ my-func; }
echo "i is $i" # 6
これがDaveの言う「めっちゃuseful」な機能。グローバル変数も変更しながら、出力も変数に格納できる。
算術演算
(( )) と $(( )) の違い
# $(( )) = 結果を返す(コマンド置換)
result=$((5 + 3))
echo $result # 8
# (( )) = 計算を実行する(終了ステータスを返す)
((i = 5 + 3))
echo $i # 8
算術式の中では $ が不要
i=5
j=3
# (( )) の中では $ なしで変数を参照
((sum = i + j))
echo $sum # 8
演算子
((i = 5 + 3)) # 加算
((i = 5 - 3)) # 減算
((i = 5 * 3)) # 乗算
((i = 5 / 3)) # 除算(整数)
((i = 5 % 3)) # 余り
((i++)) # インクリメント
((i--)) # デクリメント
ビットシフト演算(Daveの罠)
i=2
((i <<= 5)) # 左に5ビットシフト
echo $i # 64
# これは i × 2^5 = i × 32 と同じ
普通のシェルスクリプトではほぼ使わないが、低レベルプログラミングでは一般的。
三項演算子
a=2
b=3
((max = a > b ? a : b))
echo $max # 3
JavaScriptと同じ構文が使える!
真偽値の扱い(ややこしい!)
# 算術式の結果
((0)) # false(終了ステータス1)
((1)) # true(終了ステータス0)
((42)) # true(終了ステータス0)
0以外はすべてtrue、0だけがfalse
実例:偶数/奇数判定
a=10
if ((a % 2)); then # 10 % 2 = 0 → false
echo "number is odd"
else
echo "number is even" # こっちが実行される
fi
set -e との罠
set -e # エラーで即座に終了
i=0
((i++)) # i = 1 になるが、式の結果は0(インクリメント前)
# 結果が0 → 終了ステータス1(失敗)
# set -e で即座に終了!💥
解決策:
((i++)) || true # 失敗しても続行
# または
i=$((i+1)) # こっちの方が安全
8進数の罠
echo $((07)) # 7(8進数の07 = 10進数の7)
echo $((08)) # エラー!(8は8進数に存在しない)
echo $((10)) # 8(8進数の10 = 10進数の8)
0で始まる数字は8進数として扱われる
解決方法:10# を使う
echo $((10#08)) # 8(10進数として強制)
echo $((10#09)) # 9
echo $((10#10)) # 10
よくある罠:時刻の処理
hour=08
((hour + 1)) # エラー!08は無効な8進数
# 正しく
echo $((10#$hour + 1)) # 9
学びと気づき
プログラミング言語の共通概念
Rust、JavaScript、Bashと学んでいて気づいたこと:
- 変数と型
- 条件分岐
- ループ
- 関数
これらの概念はどの言語も同じ。文法(シンタックス)は違うが、考え方は共通している。
一つの言語を深く理解すれば、他の言語も概念は分かっているので、文法を覚えるだけで済む。
Daveのハードモード
- Vim(エディタ戦争で勝ち残った古参)
- Linterなし(自分の目で確認しろ)
- 行番号なし
- ビット演算もガンガン出てくる
完全に「基礎を叩き込む」スタイル。初心者には厳しいが、理解すれば確実に力になる。
次回の課題
- Chapter 04の続き
- より複雑なスクリプトの作成
- Rustとの並行学習
Bashの深さを実感した1日だった。Daveの「後で説明します」が多すぎるのは困りものだが、疑問に思ったことをその場で調べるスタイルで進めていく。
参考リンク: