@nqounet です。
新シリーズ「シンプルなTodo CLIアプリ」の第1回です。
はじめに - Mooの次は実践アプリを作ろう 「Mooで覚えるオブジェクト指向プログラミング」シリーズを終えた方、お疲れ様でした!
Mooの基礎を身につけた今、次のステップとして実践的なアプリケーション開発に挑戦してみましょう。
このシリーズでは、Todo CLIアプリの開発を通じて、設計の考え方を学んでいきます。最初は素朴なコードから始め、徐々に改善を加えていくことで「なぜそうするのか」を体感できるようにします。
第1回の目標は、if-elsif分岐で動作する最もシンプルなTodo CLIアプリを作成することです。まずは動くものを作り、その限界を確認しましょう。
今回作るもの - 3つのコマンドを持つTodo CLI 完成イメージ まず、今回作成するCLIアプリの処理フローを確認しましょう。
flowchart TD
A[コマンド実行] --> B{サブコマンド?}
B -->|add| C[タスク追加]
B -->|list| D[一覧表示]
B -->|complete| E[タスク完了]
B -->|その他/未指定| F[ヘルプ表示]
C --> G[ファイルに追記]
G --> H[Added: タスク名]
D --> I[ファイルから読み込み]
I --> J[番号付きで表示]
E --> K[ファイルから読み込み]
K --> L[指定タスク削除]
L --> M[ファイル書き戻し]
M --> N[Completed: タスク名]
この図は、Todo CLIの処理フローを示しています。@ARGVからサブコマンドを取得し、if-elsif分岐で適切な処理に振り分けます。各処理は最終的にファイル操作を行い、結果をユーザーに表示します。
今回作るTodo CLIは、以下の3つのサブコマンドを持ちます。
1
2
3
4
5
6
7
8
9
$ perl todo.pl add "牛乳を買う"
Added: 牛乳を買う
$ perl todo.pl list
1. 牛乳を買う
2. メールを返信する
$ perl todo.pl complete 1
Completed: 牛乳を買う
todo add "タスク名" でタスクを追加todo list でタスク一覧を表示todo complete 番号 でタスクを完了(削除)最小限の機能で始める 最初から完璧なものを目指す必要はありません。今回は「動くこと」を最優先にします。
エラー処理は最小限 拡張性は考慮しない 1ファイルで完結 「なぜ改善が必要か」を後の回で実感するために、あえてシンプルに作ります。
環境の準備 必要なもの Perl 5.16以上(// 演算子を使用) テキストエディタ ターミナル ファイル構成 今回は1ファイルで完結します。
1
2
todo.pl # メインスクリプト
todo.txt # タスクの保存先(自動生成)
作業ディレクトリに todo.pl を作成してください。
コマンドライン引数を受け取る @ARGVとは Perlでは、コマンドライン引数は特殊変数 @ARGV に格納されます。
1
$ perl todo.pl add "牛乳を買う"
このコマンドを実行すると、@ARGV には以下の値が入ります。
1
@ARGV = ( 'add' , '牛乳を買う' );
$ARGV[0] は 'add'(サブコマンド)$ARGV[1] は '牛乳を買う'(引数)サブコマンドを取り出す shift 関数を使って、@ARGV の先頭要素を取り出します。
1
my $command = shift @ARGV ; # 'add' を取得、@ARGVは ('牛乳を買う') になる
shift は配列の先頭要素を取り出し、配列から削除します。これにより、残りの @ARGV には追加の引数だけが残ります。
if-elsif分岐でサブコマンドを実装する 基本構造 サブコマンドに応じて処理を分岐させます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env perl
use strict ;
use warnings ;
use utf8 ;
my $command = shift @ARGV // 'help' ;
if ( $command eq 'add' ) {
# タスク追加の処理
}
elsif ( $command eq 'list' ) {
# 一覧表示の処理
}
elsif ( $command eq 'complete' ) {
# 完了処理
}
else {
# ヘルプ表示
}
// 演算子は「左辺が未定義ならば右辺を使う」という意味です。引数がない場合は 'help' がデフォルトになります。
なぜif-elsifから始めるのか if-elsif分岐は最もシンプルで理解しやすい方法です。Perlを学んだばかりの方でもすぐに読めるでしょう。
しかし、この方法には問題もあります。それは後の回で体感しますが、今回はまず「動くもの」を作ることを優先します。
配列でタスクを管理する タスクの保存と読み込み タスクはテキストファイルに1行1タスクで保存します。
1
2
3
牛乳を買う
メールを返信する
報告書を書く
プログラム起動時にファイルを読み込み、終了時に書き戻す方式です。
ファイル操作の基本 Perlでファイルを操作するには open 関数を使います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 読み込みモード
open my $fh , '<' , 'todo.txt' or die "Cannot open: $!" ;
my @lines = <$fh> ; # 全行を配列に読み込む
close $fh ;
# 書き込みモード(上書き)
open my $fh , '>' , 'todo.txt' or die "Cannot open: $!" ;
print $fh "内容\n" ;
close $fh ;
# 追記モード
open my $fh , '>>' , 'todo.txt' or die "Cannot open: $!" ;
print $fh "追加する行\n" ;
close $fh ;
< は読み込みモード> は書き込みモード(既存内容を上書き)>> は追記モード(既存内容の末尾に追加)$! にはエラーメッセージが格納されます。
add - タスクを追加する 引数からタスク内容を取得 サブコマンド add の後に続く引数がタスク内容です。
1
2
3
4
5
6
if ( $command eq 'add' ) {
my $task = shift @ARGV ;
die "Usage: $0 add <task>\n" unless defined $task && $task ne '' ;
# 追加処理...
}
shift @ARGV でタスク文字列を取得空文字や未定義の場合はエラーで終了 ファイルに追記する タスクをファイルの末尾に追記します。
1
2
3
4
5
open my $fh , '>>' , $file or die "Cannot open $file: $!" ;
print $fh "$task\n" ;
close $fh ;
print "Added: $task\n" ;
>> モードで開くと、既存のタスクを消さずに末尾へ追加できます。
動作確認 1
2
3
4
5
6
7
8
9
$ perl todo.pl add "牛乳を買う"
Added: 牛乳を買う
$ perl todo.pl add "メールを返信する"
Added: メールを返信する
$ cat todo.txt
牛乳を買う
メールを返信する
list - タスク一覧を表示する ファイルから読み込む 保存されているタスクを読み込んで表示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
elsif ( $command eq 'list' ) {
unless ( - e $file ) {
print "No tasks.\n" ;
exit ;
}
open my $fh , '<' , $file or die "Cannot open $file: $!" ;
my @tasks = <$fh> ;
close $fh ;
chomp @tasks ; # 各行の改行を削除
# 表示処理...
}
-e $file はファイルの存在確認<$fh> でリストコンテキストに置くと全行を配列に読み込むchomp で各行末の改行を削除番号付きで表示する ユーザーがタスクを指定しやすいよう、番号を付けて表示します。
1
2
3
4
5
my $i = 1 ;
for my $task ( @tasks ) {
print "$i. $task\n" ;
$i ++ ;
}
動作確認 1
2
3
$ perl todo.pl list
1. 牛乳を買う
2. メールを返信する
complete - タスクを完了する 番号でタスクを指定 完了するタスクは番号で指定します。
1
2
3
4
5
6
elsif ( $command eq 'complete' ) {
my $num = shift @ARGV ;
die "Usage: $0 complete <number>\n" unless defined $num && $num =~ /^\d+$/ ;
# 完了処理...
}
指定したタスクを削除 配列から指定番号のタスクを削除し、ファイルを書き直します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open my $fh , '<' , $file or die "Cannot open $file: $!" ;
my @tasks = <$fh> ;
close $fh ;
chomp @tasks ;
my $index = $num - 1 ; # 配列は0始まり
die "Task $num not found.\n" if $index < 0 || $index >= @tasks ;
my $completed = splice @tasks , $index , 1 ; # 指定位置の要素を削除
open $fh , '>' , $file or die "Cannot open $file: $!" ;
print $fh "$_\n" for @tasks ;
close $fh ;
print "Completed: $completed\n" ;
splice @array, $index, 1 は配列から1要素を削除し、その要素を返すファイルを > モードで開いて全タスクを書き直す 動作確認 1
2
3
4
5
6
7
8
9
$ perl todo.pl list
1. 牛乳を買う
2. メールを返信する
$ perl todo.pl complete 1
Completed: 牛乳を買う
$ perl todo.pl list
1. メールを返信する
完成コード - 全体を見てみよう todo.pl の全容 約50行のシンプルなスクリプトです。そのままコピーして動作確認できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env perl
use strict ;
use warnings ;
use utf8 ;
my $file = 'todo.txt' ;
my $command = shift @ARGV // 'help' ;
if ( $command eq 'add' ) {
my $task = shift @ARGV ;
die "Usage: $0 add <task>\n" unless defined $task && $task ne '' ;
open my $fh , '>>' , $file or die "Cannot open $file: $!" ;
print $fh "$task\n" ;
close $fh ;
print "Added: $task\n" ;
}
elsif ( $command eq 'list' ) {
unless ( - e $file ) {
print "No tasks.\n" ;
exit ;
}
open my $fh , '<' , $file or die "Cannot open $file: $!" ;
my @tasks = <$fh> ;
close $fh ;
chomp @tasks ;
my $i = 1 ;
for my $task ( @tasks ) {
print "$i. $task\n" ;
$i ++ ;
}
}
elsif ( $command eq 'complete' ) {
my $num = shift @ARGV ;
die "Usage: $0 complete <number>\n" unless defined $num && $num =~ /^\d+$/ ;
open my $fh , '<' , $file or die "Cannot open $file: $!" ;
my @tasks = <$fh> ;
close $fh ;
chomp @tasks ;
my $index = $num - 1 ;
die "Task $num not found.\n" if $index < 0 || $index >= @tasks ;
my $completed = splice @tasks , $index , 1 ;
open $fh , '>' , $file or die "Cannot open $file: $!" ;
print $fh "$_\n" for @tasks ;
close $fh ;
print "Completed: $completed\n" ;
}
else {
print "Usage: $0 <command> [args]\n" ;
print "Commands:\n" ;
print " add <task> - Add a new task\n" ;
print " list - List all tasks\n" ;
print " complete <num> - Complete a task by number\n" ;
}
動かしてみよう 一連の操作を実行してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ perl todo.pl
Usage: todo.pl <command> [ args]
Commands:
add <task> - Add a new task
list - List all tasks
complete <num> - Complete a task by number
$ perl todo.pl add "牛乳を買う"
Added: 牛乳を買う
$ perl todo.pl add "メールを返信する"
Added: メールを返信する
$ perl todo.pl add "報告書を書く"
Added: 報告書を書く
$ perl todo.pl list
1. 牛乳を買う
2. メールを返信する
3. 報告書を書く
$ perl todo.pl complete 2
Completed: メールを返信する
$ perl todo.pl list
1. 牛乳を買う
2. 報告書を書く
このコードの問題点 - 拡張性の壁 動くTodoアプリが完成しました!しかし、このコードにはいくつかの問題があります。
コマンドが増えたらどうなる? 新しいコマンド(例: edit, priority, search)を追加するたびに、if-elsif の条件分岐が長大化します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if ( $command eq 'add' ) {
# ...
}
elsif ( $command eq 'list' ) {
# ...
}
elsif ( $command eq 'complete' ) {
# ...
}
elsif ( $command eq 'edit' ) { # 追加
# ...
}
elsif ( $command eq 'priority' ) { # 追加
# ...
}
elsif ( $command eq 'search' ) { # 追加
# ...
}
# どんどん長くなる...
見通しが悪くなる 修正箇所が分かりにくくなる 同じパターンの繰り返しが増える テストが書きにくい 処理がスクリプト直下に直接書かれているため、単体テストが困難です。
「add機能だけをテストする」ができない ファイル操作と表示処理が混在している モック(テスト用の代替)を差し込む余地がない コードの重複 ファイル操作のコードが各所に散らばっています。
list でファイルを読み込む処理complete でファイルを読み込む処理ほぼ同じコードが2箇所に存在 これはDRY原則(Don’t Repeat Yourself)に反しています。ファイル形式を変更したくなったら、複数箇所を修正しなければなりません。
次回予告 - サブルーチンで整理する 今回の問題点を、次回はサブルーチンで解決します。
処理をサブルーチンに分割 ファイル操作を共通化 テスト可能な構造へ近づける 「動くコード」から「良いコード」への第一歩を踏み出しましょう。
まとめ 今回は、if-elsif分岐で動くシンプルなTodo CLIを作成しました。
@ARGV からコマンドライン引数を取得する方法を学習if-elsif分岐でサブコマンドを振り分け テキストファイルでタスクを永続化 シンプルだが拡張性に課題があることを確認 まずは動くものを作る。その後で改善点を見つけて直していく。これがソフトウェア開発の自然な流れです。
次回は、このコードをサブルーチンで整理していきます。お楽しみに!