ysapでbash入門③

·
#CLI#Terminal#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の「後で説明します」が多すぎるのは困りものだが、疑問に思ったことをその場で調べるスタイルで進めていく。


参考リンク: