今回のテーマは、Google Colabで、シェルスクリプトの初歩の初歩を学ぶです。
※Google Colabは、気軽に触れるLinuxのサンドボックス環境としても便利です。Google Colabは数時間おきに環境がリフレッシュされるため、データ・プログラムの永続的な保管には適しません。しかし裏を返せば、「うっかり環境を壊しても簡単に初期化でき、気軽にやり直せるメリットがあります。
とくに、シェルスクリプトは、環境構築・インフラ領域で重宝される言語です。気軽に触って・試して・壊して(ときには環境すら壊しながら)学ぶアプローチに、Google Colabは非常に役に立ちます。
対象読者
この記事は、次のような人を対象にしています。
- Google Colabを使ったことがある人
- シェルスクリプトを書いたことがない、もしくはほとんど書いたことがない人
- CIスクリプトなどで見かける
.shファイルの中身を、自力で読めるようになりたい人 - 学んで終わりでなく、コードを「触りながら」「実行しながら」学びたい人
すぐに使えるチートシートはこちら Google Colab版
すぐに実行して、試せるコードレシピはこちら。
学習の進め方
マジックコマンド(%%writefile)で、シェルスクリプトのファイルを作成して使用してください。
%%writefile s01_hello.sh
作成したファイルは、次のコードで実行できます。
!sh s01_hello.sh
同じファイル名を使い回すと、前の内容で上書きされてしまいます。 そのため本記事では、セルごとにs01_, s02_...と連番のファイル名を分けています。ランタイムがリセットされるまでは、全てのファイルがColab上に残るので、後から見返したり、書き換えて再実行したりして試してみてください。
1. まずは基本形 ── シェルスクリプトはシェバン行から始まる
いちばん小さなシェルスクリプトです。
#!/bin/sh
echo "Hello, Shell!"
実行は、次のように行います。
!sh s01_hello.sh
Hello, Shell!
それぞれの意味は、ざっくり次のとおりです。
-
#!/bin/sh
このスクリプトを、/bin/shというインタプリタで実行してほしい、という宣言です。ファイルの1行目に書く決まりごとで、「シェバン行」と呼ばれます。 -
echo
文字を画面に表示するコマンドです。
shは、機能を絞った最小限のシェルです。bashというより高機能なシェルもありますが、CIスクリプトなどでは、環境を選ばず動くshがあえて選ばれることがよくあります。
2. 変数を使う
シェルスクリプトでも、変数に値を入れることができます。
#!/bin/sh
name="Alice"
echo "$name"
Alice
name="Alice"は、nameという変数に"Alice"という文字列を入れています。変数を使うときは$nameのように$を付けます。
注意点: =の前後にスペースを入れてはいけない
シェルスクリプトには、変数の代入について1つ厳格なルールがあります。
#!/bin/sh
name = "Alice"
echo "$name"
s02b_vars_bad.sh: 2: name: not found
name = "Alice"のように、=の前後にスペースを入れると壊れます。シェルはnameというコマンドを実行しようとして、「そんなコマンドは見つからない」というエラーになります。シェルスクリプトでは、=の前後には絶対にスペースを入れない、と覚えておく必要があります。
3. コマンドライン引数を受け取る
スクリプトの実行時に渡した引数は、$1, $2...のように受け取れます。
#!/bin/sh
echo "1つ目の引数: $1"
echo "引数の数: $#"
引数を渡して実行してみます。
!sh s03_args.sh Alice Bob
1つ目の引数: Alice
引数の数: 2
-
$1は1つ目の引数、$2は2つ目の引数 -
$#は、渡された引数の数 - 全部の引数をまとめて使いたい場合は
$@も使えます
このスクリプトはAliceとBobという2つの引数を渡して実行したので、$1がAlice、$#が2になっています。
4. ユーザーからの入力を受け取る read
readを使うと、キーボードからの入力を変数に受け取れます。
#!/bin/sh
echo "名前を入力してください:"
read name
echo "こんにちは、$name さん"
Colabのセルでもそのまま実行でき、実行中に入力欄が表示されます。
!sh s04_read.sh
名前を入力してください:
inclusive solutions
こんにちは、inclusive solutions さん
実際に手元のターミナルで実行する場合も、sh s04_read.shとすれば、その場でキーボード入力を求められます。
5. 変数はダブルクォートで囲む
$nameをそのまま使うか、"$name"のようにダブルクォートで囲むかで、挙動が変わることがあります。
#!/bin/sh
name="Alice Bob"
echo "クォートなし(連続スペースが消える):"
echo $name
echo "クォートあり(そのまま保たれる):"
echo "$name"
クォートなし(連続スペースが消える):
Alice Bob
クォートあり(そのまま保たれる):
Alice Bob
クォートなしの$nameは、いったん単語に分割されてから並べ直されるため、連続するスペースが1つに詰まってしまいます(この現象を"word splitting"と呼びます)。クォートありの"$name"は、変数の中身をそのまま1つの文字列として扱います。
この違いは、見た目の問題だけでは済みません。次の[ ]と組み合わせると、実際にエラーになるケースがあります。
#!/bin/sh
name="Alice Bob"
echo "クォートなしで [ ] に渡すと:"
if [ -n $name ]; then
echo "空じゃない"
fi
クォートなしで [ ] に渡すと:
s05b_quote_test.sh: 5: [: Alice: unexpected operator
$nameがAliceとBobという2つの単語に分かれてしまい、[ -n Alice Bob ]のような、[ ]が想定していない形になってエラーになりました。変数を[ ]に渡すときは、必ずダブルクォートで囲む、というのが鉄則です。これは、目的のコードの[ -n "$unformatted" ]にも直結する重要なポイントです。
6. コマンドの実行結果を変数に入れる ── コマンド置換
$(...)を使うと、コマンドを実行して、その標準出力を文字列として変数に受け取れます。
#!/bin/sh
today=$(date +%Y-%m-%d)
echo "今日の日付: $today"
今日の日付: 2026-07-26
$(date +%Y-%m-%d)は、「date +%Y-%m-%dというコマンドを実行して、その結果を文字列として受け取る」という意味です。これが、目的のコードのunformatted=$(gofmt -l .)の正体です。**「gofmt -l .を実行して、その出力をunformattedという変数に詰め込む」**という処理だった、ということになります。
7. 四則演算をする
シェルスクリプトでは、変数は基本的に「文字列」として扱われます。
#!/bin/sh
a=3
b=4
echo "文字列として+をそのまま書くと: $a + $b"
echo "算術展開を使うと: $((a + b))"
文字列として+をそのまま書くと: 3 + 4
算術展開を使うと: 7
$a + $bとそのまま書いても、文字として3 + 4が表示されるだけで、計算はされません。計算したい場合は、$(( ))という算術展開という書き方を使う必要があります。「基本は文字列、計算したいときは専用の書き方をする」というのが、シェルスクリプトの考え方です。
8. 条件分岐 ── ifの基本形
#!/bin/sh
x=3
if [ "$x" -gt 5 ]; then
echo "big"
elif [ "$x" -lt 5 ]; then
echo "small"
else
echo "equal"
fi
small
ifの基本的な形は、次のとおりです。
- 条件式は
[ ]で囲む - 処理のまとまりは
then〜fiで囲む(fiはifを逆から書いたもの) - 「それ以外の場合」は
elif(else ifを1単語にしたもの) -
thenを同じ行に書く場合は、直前にセミコロン;が必要(改行する場合は不要)
9. [ ]の正体 ── 実はtestという名前の"コマンド"
[ ]は記号に見えますが、実はtestという名前のコマンドの別名です。
#!/bin/sh
x=""
if [ -n "$x" ]; then
echo "空じゃない"
else
echo "空です"
fi
空です
[が「コマンド」だからこそ、[の直後と]の直前には必ずスペースが必要です。試しに詰めて書いてみます。
#!/bin/sh
x="hello"
if [-n "$x" ]; then
echo "空じゃない"
fi
s09b_test_bad.sh: 3: [-n: not found
[-nという、存在しない1つのコマンドとして解釈されてしまい、エラーになりました。[はコマンド名なので、引数(-nなど)との間にスペースが要る、という理屈です。
代表的なテスト演算子を整理します。
| 演算子 | 意味 |
|---|---|
-n "$x" |
文字列が空でない |
-z "$x" |
文字列が空である |
"$a" = "$b" |
文字列として等しい |
"$a" != "$b" |
文字列として等しくない |
"$a" -eq "$b" |
数値として等しい |
"$a" -gt "$b" |
数値として「より大きい」 |
"$a" -lt "$b" |
数値として「より小さい」 |
文字列比較は=/!=、数値比較は-eq/-gt/-ltと、書き方が全く違う点に注意してください。"$a" > "$b"のように書くと、数値の比較ではなく、別の意味(リダイレクト)に解釈されてしまいます。
10. ファイル・ディレクトリの存在確認
[ ]には、ファイルの状態を調べる演算子もあります。
#!/bin/sh
echo "hello" > sample.txt
if [ -f "sample.txt" ]; then
echo "ファイルがあります"
fi
if [ -d "sample.txt" ]; then
echo "ディレクトリです"
else
echo "ディレクトリではありません"
fi
if [ -e "notfound.txt" ]; then
echo "存在します"
else
echo "存在しません"
fi
ファイルがあります
ディレクトリではありません
存在しません
-
-f: 普通のファイルかどうか -
-d: ディレクトリかどうか -
-e: (種類を問わず)存在するかどうか
sample.txtはファイルとして作成したので、-fは真、-dは偽になっています。
11. 終了コード(exit status)という考え方
コマンドは、実行し終わると必ず「終了コード」という数値を返します。直前のコマンドの終了コードは$?で確認できます。
#!/bin/sh
false
echo "false の直後の \$?: $?"
true
echo "true の直後の \$?: $?"
exit 3
スクリプト自体の終了コードも、続けて確認してみます。
!sh s11_exitstatus.sh; echo "スクリプト自体の終了コード: $?"
false の直後の $?: 1
true の直後の $?: 0
スクリプト自体の終了コード: 3
-
0なら成功 -
0以外なら失敗
というのが、Unix共通のルールです。falseは「必ず失敗する」、trueは「必ず成功する」だけのコマンドなので、それぞれ1と0が返っています。exit 3のように、スクリプト自身の終了コードを明示的に指定することもできます。
以前のgo vet入門で見た「go vetが非ゼロの終了コードを返す」という話も、この仕組みの上に成り立っています。
12. コマンドをつなげる && と ||
ifを使わずに、「前のコマンドが成功したら」「失敗したら」で処理をつなげる、簡易な書き方もあります。
#!/bin/sh
mkdir mydir && cd mydir && echo "ディレクトリ作成に成功しました"
cd ..
rmdir mydir
gofmt -l notfound.go 2>/dev/null || echo "gofmtの実行に失敗しました(存在しないファイル)"
ディレクトリ作成に成功しました
gofmtの実行に失敗しました(存在しないファイル)
-
A && B: Aが成功したら、Bも実行する -
A || B: Aが失敗したら、Bを実行する
mkdir mydir && cd mydir && echo ...は、ディレクトリ作成・移動が両方成功して初めてechoまで到達します。gofmt -l notfound.go || echo ...は、存在しないファイルを指定してgofmtが失敗したので、||の後ろが実行されました。
13. for文で繰り返す
まず、対象となるファイルを何個か用意します。
!touch a.go b.go c.txt
#!/bin/sh
for f in *.go; do
echo "$f"
done
a.go
b.go
*.goは、「.goで終わるファイル名すべて」に展開される、シェル特有の記法です(グロブと呼ばれます)。c.txtは対象にならないので表示されません。
14. while文で繰り返す
#!/bin/sh
i=1
while [ "$i" -le 3 ]; do
echo "$i"
i=$((i + 1))
done
1
2
3
[ "$i" -le 3 ]が真である間、繰り返します。i=$((i + 1))のように、算術展開(検証7)を使って1ずつ増やしています。
15. 関数を作る
#!/bin/sh
greet() {
echo "Hello, $1"
}
greet "Alice"
Hello, Alice
シェルスクリプトの関数は、greet() { ... }のように定義します。引数は、関数の中でも$1, $2...で受け取ります(検証3のスクリプト全体への引数と同じ考え方です)。名前を付けて引数を受け取る仕組みはなく、あくまで位置だけで受け取る点が特徴です。
16. パイプでコマンドをつなぐ
あるコマンドの出力を、そのまま次のコマンドの入力として渡すのがパイプ(|)です。
!ls *.go | wc -l
2
ls *.go(.goファイルの一覧)の出力を、wc -l(行数を数えるコマンド)にそのまま渡しています。検証6の「コマンド置換($(...)、結果を変数に詰める)」と、この「パイプ(結果を次のコマンドに渡す)」は似ていますが、変数に残したいか、そのまま次のコマンドに流したいかで使い分けます。検証4のreadのところで使った入力の流し込みも、実はこのパイプの応用です。
17. 標準出力とエラー出力を分ける・リダイレクト
#!/bin/sh
echo "ログ1行目" > log.txt
echo "ログ2行目" >> log.txt
echo "エラーです" 1>&2
echo "log.txtの中身:"
cat log.txt
エラーです
log.txtの中身:
ログ1行目
ログ2行目
-
>: 出力をファイルに書き込む(上書き) -
>>: 出力をファイルに追記する -
1>&2: 標準出力(1)を標準エラー出力(2)に向ける
echo "エラーです" 1>&2の部分だけ、通常の出力(stdout)とは別の経路(stderr)に流れるため、出力される順番が前後することがあります(今回の実行結果でも、コード上は後半にあるecho "エラーです"が一番上に表示されています)。検証12で使ったgofmt -l notfound.go 2>/dev/nullの2>/dev/nullも、この仲間で、「標準エラー出力を捨てる」という意味です。
18. おまけ: set -eでエラー時に即終了させる
CIスクリプトでは、set -eという書き方もよく見かけます。
#!/bin/sh
echo "1行目"
false
echo "2行目(本当は実行してほしくない)"
!sh s19_without_sete.sh; echo "終了コード: $?"
1行目
2行目(本当は実行してほしくない)
終了コード: 0
set -eを付けていないと、false(失敗)があっても、スクリプトはそのまま次の行を実行し続けます。
#!/bin/sh
set -e
echo "1行目"
false
echo "2行目(set -eがあれば、ここは実行されない)"
!sh s19_with_sete.sh; echo "終了コード: $?"
1行目
終了コード: 1
set -eを先頭に書くと、「途中で1つでも失敗したコマンドがあれば、そこでスクリプト全体を即終了する」という動きになります。
ちなみに、今回レビューした目的のコードはset -eを使わず、if [ -n "$unformatted" ]で明示的に判定してexit 1していました。gofmt -l .自体は(整形漏れがあっても)エラー終了するコマンドではないので、set -eだけに頼らず、出力の中身を見て自分で判定する、という設計になっている、と読み取れます。
弊社について
本記事を書いている 合同会社インクルーシブソリューションズ は、データ基盤構築・分析基盤設計・システム改善支援を中心に活動している小規模IT法人です。
主な領域は、
- データマート設計・データパイプライン構築
- SQL / Python を用いたデータ処理設計
- BI導入支援・分析基盤の整備
- 既存システムの運用改善・可視化支援
といった、「データを使える状態にする」ための活動です。
弊社の企業活動に興味がある方は、ぜひ公式サイトも覗いてみてください。