「ls って中で何やってるの?」「grep って魔法なの?」——そんな疑問、ありませんか?
実は、普段ターミナルで当たり前のように叩いているコマンドたち、Go 言語なら驚くほどシンプルに再現できるんです。この記事では 7 つの定番コマンドを、1行ずつコメント付きで解説しながら自作していきます。読み終わる頃には「なんだ、こんな仕組みだったのか!」となっているはずです。
この記事ではあえて実行結果を載せないので皆さんの環境で実行してみてください!
_,,_,*^____ _____``*g*\"*,
/ __/ /' ^. / \ ^@q f
[ @f | @)) | | @)) l 0 _/
\`/ \~____ / __ \_____/ \
| _l__l_ I @@@@ .--.
} [______] I @@@@@@ |o_o |
] | | | | @@@@@@ |:_/ |
] ~ ~ | @@@@ // \ \
| | \ / (| | )
\/ /'\_ _/`\
\___)=(___/
GoのマスコットのGopherの原作者はRenée Frenchさんです
📋 今回つくるコマンド一覧
| コマンド | ひとことで言うと |
|---|---|
ls |
「このフォルダに何がある?」を教えてくれる |
echo |
「オウム返し」するだけの最シンプルコマンド |
cat |
ファイルの中身を「ぜんぶ見せて!」 |
wc |
ファイルの行数・単語数を「数えてくれる係」 |
head |
「最初のほうだけ見せて」に応えてくれる |
grep |
ファイルの中から「これどこにある?」を探す探偵 |
tree |
フォルダ構造を「家系図」みたいに表示 |
それでは、さっそく作っていきましょう!
1. ls — 「このフォルダに何がある?」
どんなコマンド?
ディレクトリの中にあるファイルやフォルダの名前を一覧表示します。ターミナルを開いて最初に打つコマンド No.1 と言っても過言ではありません。
cdとlsはめっちゃ使います
使用例
ls # 今いるディレクトリの中身を表示
ls -l # サイズや更新日時など詳細情報つき
ls -a /tmp # 隠しファイル(.で始まるやつ)も含めて表示
Go で書いてみよう
package main // Go のプログラムは必ず package 宣言から始まる。mainは「実行可能プログラムですよ」の合図
import (
"fmt" // 画面に文字を出力するためのパッケージ。printfのお友達
"log" // エラーが起きたときに「ヤバいよ!」と教えてくれるやつ
"os" // OS(ファイルやディレクトリ)とやりとりするためのパッケージ
"strings" // 文字列を切ったり繋いだりする便利屋さん
)
func main() { // プログラムはここからスタート!
dir := "." // "." はカレントディレクトリ(今いる場所)を意味する
if len(os.Args) > 1 { // コマンドライン引数が渡されていたら…
dir = os.Args[1] // 最初の引数をディレクトリパスとして使う
}
entries, err := os.ReadDir(dir) // ディレクトリの中身を読み取る(ここが ls の心臓部!)
if err != nil { // もしエラーが起きたら…
log.Fatal(err) // エラーメッセージを出してプログラム終了(お疲れ様でした)
}
names := make([]string, 0, len(entries)) // ファイル名を入れるスライス(箱)を用意
for _, e := range entries { // 取得したエントリを1つずつ見ていく
names = append(names, e.Name()) // ファイル名だけ取り出して箱に追加
}
fmt.Println(strings.Join(names, " ")) // スペース2つ区切りで一気に表示!完成!
}
💡 ここがおもしろい
os.ReadDir が返してくれるエントリは最初からアルファベット順にソートされています。つまり自分でソートする必要ゼロ。Go、気が利きすぎる。
2. echo — 世界一シンプルなコマンド
どんなコマンド?
渡された文字列をそのまま画面に表示します。「それだけ?」と思うかもしれませんが、シェルスクリプトでは変数の確認やデバッグに大活躍する縁の下の力持ちです。
使用例
echo Hello World # "Hello World" と表示される
echo -n "no newline" # 末尾の改行を入れない(地味に便利)
echo $HOME # 環境変数の値を表示
Go で書いてみよう
package main // おなじみの宣言
import (
"fmt" // 出力担当
"os" // コマンドライン引数を受け取る
"strings" // 文字列をスペースで繋ぐために使う
)
func main() {
args := os.Args[1:] // os.Args[0] はプログラム名自体なので、[1:]で「本当の引数」だけ取り出す
noNewline := false // -n オプションが指定されたかどうかのフラグ
if len(args) > 0 && args[0] == "-n" { // 最初の引数が "-n" なら…
noNewline = true // 「改行しないモード」をONにする
args = args[1:] // "-n" 自体は出力したくないので引数リストから除外
}
output := strings.Join(args, " ") // 残りの引数をスペースで繋げて1つの文字列にする
if noNewline { // 改行なしモードなら…
fmt.Print(output) // Print は末尾に改行を入れない
} else {
fmt.Println(output) // Println は末尾に改行を入れてくれる。この1文字の差が大きい!
}
}
💡 ここがおもしろい
echo の実装はたったこれだけ。プログラミング初心者が最初に作るコマンドとして最適です。「え、これでもうコマンドなの?」という驚きが、自作コマンドの入口になります。
3. cat — ファイルの中身、ぜんぶ見せて!
どんなコマンド?
ファイルの内容をまるごと標準出力に表示します。複数ファイルを指定すると、順番に連結して出力してくれます。名前の由来は "concatenate"(連結する)です。猫じゃないんです。
使用例
cat file.txt # ファイルの中身をそのまま表示
cat a.txt b.txt # 2つのファイルを繋げて表示
cat -n file.txt # 行番号付きで表示
Go で書いてみよう
package main
import (
"fmt" // 出力とエラー表示に使う
"os" // ファイル読み込みと引数の取得
)
func main() {
if len(os.Args) < 2 { // 引数がなかったら…
fmt.Fprintln(os.Stderr, "usage: cat <file> ...") // 使い方を教えてあげる(標準エラー出力に)
os.Exit(1) // 終了コード1で終了(「失敗しました」の意味)
}
for _, path := range os.Args[1:] { // 引数で渡されたファイルを1つずつ処理
data, err := os.ReadFile(path) // ファイルの中身を一括で読み込む(どーんと全部!)
if err != nil { // ファイルが見つからない等のエラーが起きたら…
fmt.Fprintf(os.Stderr, "cat: %s: %v\n", path, err) // エラーを表示するけど…
continue // 止まらず次のファイルへ進む(本家catと同じ挙動!)
}
fmt.Print(string(data)) // 読み込んだバイト列を文字列に変換して出力。これで終わり!
}
}
💡 ここがおもしろい
エラーが起きても continue で処理を続けるのがポイント。cat a.txt typo.txt b.txt と打ったとき、typo.txt が存在しなくても a.txt と b.txt はちゃんと表示されます。1つの失敗で全部止まらない、Unix の「やれることはやる」精神が表れています。
4. wc — ファイルの「数えてくれる係」
どんなコマンド?
ファイルの行数・単語数・バイト数をカウントして表示します。"word count" の略ですが、行数やバイト数も数えてくれるお得なコマンドです。
使用例
wc file.txt # 行数 単語数 バイト数 を一気に表示
wc -l file.txt # 行数だけ表示(ログの行数確認でよく使う)
wc -w file.txt # 単語数だけ表示(レポートの文字数チェックに)
Go で書いてみよう
package main
import (
"fmt" // 結果のフォーマット出力
"log" // エラー処理
"os" // ファイル読み込み
"strings" // 改行のカウントや単語分割に大活躍
)
func main() {
if len(os.Args) < 2 { // ファイルが指定されなかったら使い方を表示
log.Fatal("usage: wc <file>")
}
path := os.Args[1] // 対象ファイルのパスを取得
data, err := os.ReadFile(path) // ファイルを丸ごと読み込む
if err != nil {
log.Fatal(err) // 読めなかったら終了
}
text := string(data) // バイト列を文字列に変換
lines := strings.Count(text, "\n") // "\n"(改行)の数を数える → それが行数!
words := len(strings.Fields(text)) // Fields は空白で分割してスライスにする → その長さが単語数!
bytes := len(data) // バイト列の長さ → そのままバイト数!
fmt.Printf(" %d %d %d %s\n", lines, words, bytes, path) // 本家と同じフォーマットで出力
}
💡 ここがおもしろい
strings.Fields は空白文字(スペース、タブ、改行)をまとめて区切り文字として扱ってくれます。つまり "hello world" みたいにスペースが複数あっても、ちゃんと 2 単語としてカウントしてくれるんです。地味だけど超賢い。
5. head — 「最初のほうだけ見せて」
どんなコマンド?
ファイルの先頭から指定した行数だけを表示します。デフォルトは 10 行。数千行のログファイルを全部表示するのは辛い……そんなときに head の出番です。
使用例
head file.txt # 先頭10行だけ表示
head -n 5 file.txt # 先頭5行だけ表示
head -c 100 file.txt # 先頭100バイトだけ表示
Go で書いてみよう
package main
import (
"bufio" // ファイルを1行ずつ読むためのパッケージ(大きなファイルでも安心)
"fmt" // 出力
"log" // エラー処理
"os" // ファイル操作と引数
"strconv" // 文字列を数値に変換する("-n 5" の "5" を int にする)
)
func main() {
n := 10 // デフォルトの表示行数は10行(本家headと同じ)
args := os.Args[1:] // コマンドライン引数を取得
if len(args) >= 2 && args[0] == "-n" { // "-n" オプションがあれば…
var err error
n, err = strconv.Atoi(args[1]) // 次の引数を数値に変換("5" → 5)
if err != nil {
log.Fatal("invalid line count") // 数値じゃなかったら怒る
}
args = args[2:] // "-n" と数値の分を引数リストから削除
}
if len(args) < 1 { // ファイルが指定されてなかったら…
log.Fatal("usage: head [-n lines] <file>")
}
f, err := os.Open(args[0]) // ファイルを開く(ReadFileと違って「ストリーム」として扱える)
if err != nil {
log.Fatal(err)
}
defer f.Close() // 関数が終わるとき自動でファイルを閉じてくれる魔法の一行
sc := bufio.NewScanner(f) // ファイルを1行ずつ読むためのスキャナーを作成
for i := 0; i < n && sc.Scan(); i++ { // n行読むか、ファイルの終わりまでループ
fmt.Println(sc.Text()) // 読み取った1行を出力
}
// n行読んだらループ終了。残りのファイルは読まない → メモリにも優しい!
}
💡 ここがおもしろい
cat と違って os.ReadFile ではなく bufio.Scanner を使っています。こうすることで10GB のログファイルでも最初の 10 行だけを読んで即終了できます。全部メモリに載せる必要がないんです。この「必要な分だけ読む」という設計思想、Unix の美学ですね。
6. grep — ファイル界の名探偵
どんなコマンド?
ファイルの中からパターン(文字列や正規表現)にマッチする行を探し出して表示します。ログからエラーを探す、コードの中から関数名を探す……とにかく「探す」場面で最強のコマンドです。
使用例
grep "error" log.txt # "error" を含む行を全部表示
grep -i "hello" file.txt # 大文字・小文字を気にせず検索
grep -rn "TODO" ./src # フォルダの中を再帰的に検索 & 行番号つき
Go で書いてみよう
package main
import (
"bufio" // 1行ずつ読むスキャナー
"fmt" // 出力
"log" // エラー処理
"os" // ファイル操作
"regexp" // 正規表現!これがgrepのパワーの源
)
func main() {
if len(os.Args) < 3 { // パターンとファイルの両方が必要
log.Fatal("usage: grep <pattern> <file>")
}
pattern := os.Args[1] // 検索パターンを取得(例: "error", "[0-9]+" など)
path := os.Args[2] // 検索対象のファイルパス
re, err := regexp.Compile(pattern) // パターンを正規表現としてコンパイル(事前準備で高速化!)
if err != nil {
log.Fatalf("invalid pattern: %v", err) // 正規表現がおかしかったら教えてあげる
}
f, err := os.Open(path) // ファイルを開く
if err != nil {
log.Fatal(err)
}
defer f.Close() // お片付けは defer にお任せ
sc := bufio.NewScanner(f) // おなじみの1行ずつスキャナー
lineNum := 0 // 行番号カウンター。0からスタートして…
for sc.Scan() { // ファイルを1行ずつ読んでいく
lineNum++ // 行番号を+1(人間は1から数えるので)
if re.MatchString(sc.Text()) { // この行がパターンにマッチしたら…
fmt.Printf("%d: %s\n", lineNum, sc.Text()) // 行番号つきで出力!犯人発見!
}
}
}
💡 ここがおもしろい
regexp.Compile を使うことで、単純な文字列だけでなく [0-9]+ や ^Error.*timeout$ のような正規表現も使えます。もし「正規表現は重いから固定文字列だけでいい」なら strings.Contains に差し替えるだけで OK。たった 1 行の差し替えで性能特性が変わるのが、自作コマンドの醍醐味です。
7. tree — フォルダ構造の「家系図」
どんなコマンド?
ディレクトリの中身を再帰的にたどって、ツリー状に表示します。プロジェクトの構造を一目で把握したいときや、READMEにフォルダ構成を載せたいときに大活躍します。
使用例
tree # 今いるディレクトリをツリー表示
tree /project # /project 以下を表示
tree -L 2 # 深さ2階層までに制限
Go で書いてみよう
package main
import (
"fmt" // 出力
"os" // ディレクトリ操作と引数
)
// tree はディレクトリを再帰的にたどってツリーを描く関数
// dir: 対象ディレクトリのパス
// prefix: 現在の階層の左側に付ける罫線文字(深くなるほど長くなる)
func tree(dir, prefix string) {
entries, err := os.ReadDir(dir) // ディレクトリの中身を取得
if err != nil {
return // 読めなかったら静かにスキップ(権限エラー等)
}
for i, e := range entries { // エントリを1つずつ処理
isLast := i == len(entries)-1 // これが最後のエントリかどうか判定
// --- ここが tree の見た目を決める核心部分! ---
connector := "├── " // 途中のエントリには「├──」を使う
childPrefix := prefix + "│ " // 子の左側には「│」の縦線を引く
if isLast { // 最後のエントリなら…
connector = "└── " // 「└──」で終端を示す
childPrefix = prefix + " " // 子の左側は空白にする(線が途切れる)
}
fmt.Printf("%s%s%s\n", prefix, connector, e.Name()) // 罫線 + 名前を出力
if e.IsDir() { // もしディレクトリだったら…
tree(dir+"/"+e.Name(), childPrefix) // 再帰!自分自身をもう一度呼び出す
}
}
}
func main() {
dir := "." // デフォルトはカレントディレクトリ
if len(os.Args) > 1 { // 引数があれば…
dir = os.Args[1] // そのパスを使う
}
fmt.Println(dir) // まずルートディレクトリ名を表示
tree(dir, "") // ツリーの描画開始!最初の prefix は空文字
}
💡 ここがおもしろい
tree コマンドの実装で一番面白いのは、「├──」と「└──」の使い分けだけであのキレイな罫線が描けるということ。最後のエントリかどうかで 2 種類の記号を切り替えるだけで、あの見慣れたツリー表示が完成します。再帰関数のお手本のような実装です。
まとめ — 使った標準ライブラリの振り返り
| コマンド | 主役の標準ライブラリ | ひとこと |
|---|---|---|
ls |
os.ReadDir |
ソート済みで返ってくるのがありがたい |
echo |
fmt.Print |
Print と Println、たった2文字の差 |
cat |
os.ReadFile |
一括読み込みのシンプルさ |
wc |
strings.Fields |
空白をよしなに処理してくれる天才 |
head |
bufio.Scanner |
必要な行だけ読む省エネ設計 |
grep |
regexp |
正規表現を1行でコンパイル |
tree |
os.ReadDir + 再帰 |
再帰のきれいなお手本 |
こうして見ると、Go の標準ライブラリだけで Unix の定番コマンドを一通り再現できることがわかります。
自作コマンドは、壊しても誰にも怒られません。思いっきり遊んでみてください!