🔄

[2023年3月版]GPT-4に'xc'っぽいツールをRustで作成させてみた

2023/03/17に公開

GPT-3.5 のプロンプトをそのままにGPT-4に投げてみる。

前回の記事 では GPT-3.5 のモデルを使って、Go製のコマンドラインツールである xc を参考にした Rust製のコマンドラインツールを作成してみました。
今回は仕様をもうちょいとブラッシュアップしたプロンプト文を与えてコードがどのように変わるか、見てみたいとおもいます。

プロンプトを与える

それではさっそく、以下のようなプロンプトを与えてみます。

プロンプト
あなたは優秀なプログラマーです。
以下の仕様をもとにRust言語のテストコードを作成してください。
また、仕様を満たしあなたが出力したテストコードを通過するプログラムをRust言語で作成してください。

## 仕様
### 前提条件
- タスクランナーのコマンドラインツール

### 動作
- 本コマンドを xmd とする。
- 同一ディレクトリ内の README.md という Markdown 形式のファイルを読み込む。
- "```" で囲まれた箇所をコードブロックとする。
- "###" で始まり次の "###"の間でコードブロックを含む範囲をタスク定義ブロックとする。 
- タスク定義ブロックの "### "以降の文字列をタスク名とする。 
- タスク定義ブロックの "### "の次の行からコードブロックまでの間の行はヘルプ文とする
- タスク名が渡されたらタスク定義ブロック内のコードブロックをシェルスクリプトとして実行する
- タスク名が渡されなければ、すべての"タスク名 タブ 空白文字を除去したヘルプ文"を出力する
- 出力されるヘルプ文は、2行目以降はインデントをそろえる
- README.md というファイルがなければ "README.md がありません"と出力する
- `-f` オプションで README.md 以外のMarkdownファイルも指定できる

なお出力形式は以下とします。
テストコードは同値分割法(Equivalence Partitioning Testing)と境界値分析(Boundary value analysis)を用いてください。

## 出力形式
- 概要
- 短い実装コード

私たちがこのタスクで最高の結果を出すためにステップバイステップで考えてください。

そしてその答え・・・

まず、テストコードを作成し、その後にテストに合格する実装コードを作成します。テストコードでは、同値分割法と境界値分析を使用します。

テストコードの例:

use std::path::PathBuf;
use std::process::Command;

fn execute_xmd(task: Option<&str>, file: Option<&str>) -> (String, bool) {
    let mut cmd = Command::new("cargo");
    cmd.arg("run");
    cmd.arg("--");

    if let Some(t) = task {
        cmd.arg(t);
    }

    if let Some(f) = file {
        cmd.arg("-f");
        cmd.arg(f);
    }

    let output = cmd.output().expect("Failed to execute command");
    let output_str = String::from_utf8_lossy(&output.stdout).to_string();

    (output_str, output.status.success())
}

#[test]
fn test_no_readme() {
    let (output, success) = execute_xmd(None, Some("non_existing_file.md"));
    assert!(!success);
    assert_eq!(output.trim(), "README.md がありません");
}

#[test]
fn test_list_tasks() {
    let (output, success) = execute_xmd(None, None);
    assert!(success);
    assert!(output.contains("タスク名1\tヘルプ文1"));
    assert!(output.contains("タスク名2\tヘルプ文2"));
}

#[test]
fn test_execute_task() {
    let (output, success) = execute_xmd(Some("タスク名1"), None);
    assert!(success);
    assert_eq!(output.trim(), "タスク1の実行結果");
}

#[test]
fn test_invalid_task() {
    let (output, success) = execute_xmd(Some("無効なタスク"), None);
    assert!(!success);
    assert_eq!(output.trim(), "無効なタスクは存在しません");
}

以下に、仕様を満たす実装コードの例を示します。

use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();

    let mut filename = "README.md";
    let mut task_name = None;

    for i in 1..args.len() {
        if args[i] == "-f" {
            if i + 1 < args.len() {
                filename = &args[i + 1];
                i += 1;
            }
        } else {
            task_name = Some(&args[i]);
        }
    }

    let path = PathBuf::from(filename);
    if !path.exists() {
        println!("README.md がありません");
        std::process::exit(1);
    }

    let content = fs::read_to_string(&path).expect("Failed to read file");

    let task_blocks: Vec<&str> = content.split("###").skip(1).collect();
    let mut tasks = Vec::new();

    for task_block in task_blocks {
        let lines: Vec<&str> = task_block.lines().collect();
        let task_name = lines[0].trim();
        let help_text = lines[1..].iter().map(|l| l.trim()).collect::<Vec<&str>>().join(" ");
        let code_block = lines.iter().find(|l| l.trim().starts_with("```")).unwrap();
        let code = code_block.trim_start_matches("```").trim_end_matches("```").trim();
        tasks.push((task_name, help_text, code));
    }

    if let Some(task_name) = task_name {
        if let Some((_, _, code)) = tasks.iter().find(|(name, _, _)| name == &&task_name) {
            let mut cmd = Command::new("bash");
            cmd.arg("-c").arg(code);
            let output = cmd.output().expect("Failed to execute command");
            let output_str = String::from_utf8_lossy(&output.stdout).to_string();
            print!("{}", output_str);
        } else {
            println!("無効なタスクは存在しません");
            std::process::exit(1);
        }
    } else {
        for (name, help, _) in tasks {
            println!("{}\t{}", name, help);
        }
    }
}

この実装コードは、前に提供したテストコードに対して仕様を満たすように作成されています。同一ディレクトリ内の README.md ファイルを読み込み、タスク定義ブロックを解析し、タスク名が与えられた場合にコードブロックをシェルスクリプトとして実行します。タスク名が渡されない場合、すべてのタスク名とヘルプ文が表示されます。-f オプションを使って、README.md 以外のファイルも指定できます。

テストコードはちょっと雑ですが、実装コードの後にしっかりとコードの解説してくれてありがとうございます。

テストコード test.rs の作成

上記のコードを貼り付けて動かしてみます。

まずはテストケース。

test.rs
use std::path::PathBuf;
use std::process::Command;

fn execute_xmd(task: Option<&str>, file: Option<&str>) -> (String, bool) {
    let mut cmd = Command::new("cargo");
    cmd.arg("run");
    cmd.arg("--");

    if let Some(t) = task {
        cmd.arg(t);
    }

    if let Some(f) = file {
        cmd.arg("-f");
        cmd.arg(f);
    }

    let output = cmd.output().expect("Failed to execute command");
    let output_str = String::from_utf8_lossy(&output.stdout).to_string();

    (output_str, output.status.success())
}

#[test]
fn test_no_readme() {
    let (output, success) = execute_xmd(None, Some("non_existing_file.md"));
    assert!(!success);
    assert_eq!(output.trim(), "README.md がありません");
}

#[test]
fn test_list_tasks() {
    let (output, success) = execute_xmd(None, None);
    assert!(success);
    assert!(output.contains("タスク名1\tヘルプ文1"));
    assert!(output.contains("タスク名2\tヘルプ文2"));
}

#[test]
fn test_execute_task() {
    let (output, success) = execute_xmd(Some("タスク名1"), None);
    assert!(success);
    assert_eq!(output.trim(), "タスク1の実行結果");
}

#[test]
fn test_invalid_task() {
    let (output, success) = execute_xmd(Some("無効なタスク"), None);
    assert!(!success);
    assert_eq!(output.trim(), "無効なタスクは存在しません");
}

これはまぁ、ちょっと後で出しなおしましょうか。

実装コード main.rs の作成

で、実装コードは・・・

main.rs
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();

    let mut filename = "README.md";
    let mut task_name = None;

    for i in 1..args.len() {
        if args[i] == "-f" {
            if i + 1 < args.len() {
                filename = &args[i + 1];
                i += 1;
            }
        } else {
            task_name = Some(&args[i]);
        }
    }

    let path = PathBuf::from(filename);
    if !path.exists() {
        println!("README.md がありません");
        std::process::exit(1);
    }

    let content = fs::read_to_string(&path).expect("Failed to read file");

    let task_blocks: Vec<&str> = content.split("###").skip(1).collect();
    let mut tasks = Vec::new();

    for task_block in task_blocks {
        let lines: Vec<&str> = task_block.lines().collect();
        let task_name = lines[0].trim();
        let help_text = lines[1..].iter().map(|l| l.trim()).collect::<Vec<&str>>().join(" ");
        let code_block = lines.iter().find(|l| l.trim().starts_with("```")).unwrap();
        let code = code_block.trim_start_matches("```").trim_end_matches("```").trim();
        tasks.push((task_name, help_text, code));
    }

    if let Some(task_name) = task_name {
        if let Some((_, _, code)) = tasks.iter().find(|(name, _, _)| name == &&task_name) {
            let mut cmd = Command::new("bash");
            cmd.arg("-c").arg(code);
            let output = cmd.output().expect("Failed to execute command");
            let output_str = String::from_utf8_lossy(&output.stdout).to_string();
            print!("{}", output_str);
        } else {
            println!("無効なタスクは存在しません");
            std::process::exit(1);
        }
    } else {
        for (name, help, _) in tasks {
            println!("{}\t{}", name, help);
        }
    }
}

(VSCodeでビルドエラー出てるけど・・・)

実行!

では、cargo 叩いてみます。

bash
$ cargo test
   Compiling xmd v0.1.0 (/home/xxxxx/xmd)
error[E0277]: can't compare `str` with `&String`
  --> src/main.rs:44:75
   |
44 |         if let Some((_, _, code)) = tasks.iter().find(|(name, _, _)| name == &&task_name) {
   |                                                                           ^^ no implementation for `str == &String`
   |
   = help: the trait `PartialEq<&String>` is not implemented for `str`
   = help: the following other types implement trait `PartialEq<Rhs>`:
             <&'a str as PartialEq<OsString>>
             <&'a str as PartialEq<String>>
             <&'b str as PartialEq<Cow<'a, str>>>
             <str as PartialEq<Cow<'a, str>>>
             <str as PartialEq<OsStr>>
             <str as PartialEq<OsString>>
             <str as PartialEq<String>>
             <str as PartialEq>
   = note: required for `&str` to implement `PartialEq<&&String>`
   = note: 1 redundant requirement hidden
   = note: required for `&&str` to implement `~const PartialEq<&&&String>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `xmd` due to previous error

まぁ、コンパイルエラーですね。(エディタで出てたけど。)

第1回 リトライ

ChatGPT にコンパイルエラーをお知らせします。

プロンプト
   |
44 |         if let Some((_, _, code)) = tasks.iter().find(|(name, _, _)| name == &&task_name) {
   |                                                                           ^^ no implementation for `str == &String`
   |
   = help: the trait `PartialEq<&String>` is not implemented for `str`
   = help: the following other types implement trait `PartialEq<Rhs>`:
             <&'a str as PartialEq<OsString>>
             <&'a str as PartialEq<String>>
             <&'b str as PartialEq<Cow<'a, str>>>
             <str as PartialEq<Cow<'a, str>>>
             <str as PartialEq<OsStr>>
             <str as PartialEq<OsString>>
             <str as PartialEq<String>>
             <str as PartialEq>
   = note: required for `&str` to implement `PartialEq<&&String>`
   = note: 1 redundant requirement hidden
   = note: required for `&&str` to implement `~const PartialEq<&&&String>`

すみません、実装コードにエラーがありました。tasks.iter().find(|(name, _, _)| name == &&task_name)の部分を修正します。

修正後の実装コード:

use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();

    let mut filename = "README.md";
    let mut task_name = None;

    for i in 1..args.len() {
        if args[i] == "-f" {
            if i + 1 < args.len() {
                filename = &args[i + 1];
                i += 1;
            }
        } else {
            task_name = Some(&args[i]);
        }
    }

    let path = PathBuf::from(filename);
    if !path.exists() {
        println!("README.md がありません");
        std::process::exit(1);
    }

    let content = fs::read_to_string(&path).expect("Failed to read file");

    let task_blocks: Vec<&str> = content.split("###").skip(1).collect();
    let mut tasks = Vec::new();

    for task_block in task_blocks {
        let lines: Vec<&str> = task_block.lines().collect();
        let task_name = lines[0].trim();
        let help_text = lines[1..].iter().map(|l| l.trim()).collect::<Vec<&str>>().join(" ");
        let code_block = lines.iter().find(|l| l.trim().starts_with("```")).unwrap();
        let code = code_block.trim_start_matches("```").trim_end_matches("```").trim();
        tasks.push((task_name, help_text, code));
    }

    if let Some(task_name) = task_name {
        if let Some((_, _, code)) = tasks.iter().find(|(name, _, _)| name.as_str() == task_name) {
            let mut cmd = Command::new("bash");
            cmd.arg("-c").arg(code);
            let output = cmd.output().expect("Failed to execute command");
            let output_str = String::from_utf8_lossy(&output.stdout).to_string();
            print!("{}", output_str);
        } else {
            println!("無効なタスクは存在しません");
            std::process::exit(1);
        }
    } else {
        for (name, help, _) in tasks {
            println!("{}\t{}", name, help);
        }
    }
}

これでエラーが解決され、仕様を満たす実装コードが完成しました。

直してくれたものを main.rs に貼り付けて再度、cargo test してみます。

bash
$ cargo test
   Compiling xmd v0.1.0 (/home/xxxxxx/xmd)
error[E0599]: no method named `as_str` found for reference `&&str` in the current scope
  --> src/main.rs:44:75
   |
44 |         if let Some((_, _, code)) = tasks.iter().find(|(name, _, _)| name.as_str() == task_name) {
   |                                                                          -^^^^^^-- help: remove this method call

For more information about this error, try `rustc --explain E0599`.
error: could not compile `xmd` due to previous error

ここはまぁ、.as_str() を削除するだけなのでOKとしましょう。

第2回 リトライ

では、cargo test してみます。

bash
$ cargo test
   Compiling xmd v0.1.0 (/home/roomtv/works/xmd)
warning: value assigned to `i` is never read
  --> src/main.rs:16:17
   |
16 |                 i += 1;
   |                 ^
   |
   = help: maybe it is overwritten before being read?
   = note: `#[warn(unused_assignments)]` on by default

error[E0384]: cannot assign twice to immutable variable `i`
  --> src/main.rs:16:17
   |
12 |     for i in 1..args.len() {
   |         -
   |         |
   |         first assignment to `i`
   |         help: consider making this binding mutable: `mut i`
...
16 |                 i += 1;
   |                 ^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
warning: `xmd` (bin "xmd" test) generated 1 warning
error: could not compile `xmd` due to previous error; 1 warning emitted

まぁ、再代入は禁忌ですね。。。

第3回 リトライ

エラーメッセージをプロンプトに貼り付けます。

プロンプト
$ cargo test
   Compiling xmd v0.1.0 (/home/roomtv/works/xmd)
warning: value assigned to `i` is never read
  --> src/main.rs:16:17
   |
16 |                 i += 1;
   |                 ^
   |
   = help: maybe it is overwritten before being read?
   = note: `#[warn(unused_assignments)]` on by default

error[E0384]: cannot assign twice to immutable variable `i`
  --> src/main.rs:16:17
   |
12 |     for i in 1..args.len() {
   |         -
   |         |
   |         first assignment to `i`
   |         help: consider making this binding mutable: `mut i`
...
16 |                 i += 1;
   |                 ^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
warning: `xmd` (bin "xmd" test) generated 1 warning
error: could not compile `xmd` due to previous error; 1 warning emitted

で、応答は・・・

コードの修正が必要です。forループ内でiに再代入することはできません。代わりに、whileループを使用してインデックスiを管理し、-fオプションが見つかった場合にインデックスを増やすことができます。

修正後の実装コード:

use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();

    let mut filename = "README.md";
    let mut task_name = None;

    let mut i = 1;
    while i < args.len() {
        if args[i] == "-f" {
            if i + 1 < args.len() {
                filename = &args[i + 1];
                i += 2;
            }
        } else {
            task_name = Some(&args[i]);
            i += 1;
        }
    }

    let path = PathBuf::from(filename);
    if !path.exists() {
        println!("README.md がありません");
        std::process::exit(1);
    }

    let content = fs::read_to_string(&path).expect("Failed to read file");

    // 以下の実装コードは変更なし

}

これでcargo testが実行できるはずです。これで問題が解決し、テストが正常に実行されることを確認してください。

// 以下の実装コードは変更なし というウルトラCが出ました!すごい!!

で、main.rs をマージすると以下のようになります。

main.rs
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();

    let mut filename = "README.md";
    let mut task_name = None;

    let mut i = 1;
    while i < args.len() {
        if args[i] == "-f" {
            if i + 1 < args.len() {
                filename = &args[i + 1];
                i += 2;
            }
        } else {
            task_name = Some(&args[i]);
            i += 1;
        }
    }

    let path = PathBuf::from(filename);
    if !path.exists() {
        println!("README.md がありません");
        std::process::exit(1);
    }

    let content = fs::read_to_string(&path).expect("Failed to read file");

    let task_blocks: Vec<&str> = content.split("###").skip(1).collect();
    let mut tasks = Vec::new();

    for task_block in task_blocks {
        let lines: Vec<&str> = task_block.lines().collect();
        let task_name = lines[0].trim();
        let help_text = lines[1..].iter().map(|l| l.trim()).collect::<Vec<&str>>().join(" ");
        let code_block = lines.iter().find(|l| l.trim().starts_with("```")).unwrap();
        let code = code_block.trim_start_matches("```").trim_end_matches("```").trim();
        tasks.push((task_name, help_text, code));
    }

    if let Some(task_name) = task_name {
        if let Some((_, _, code)) = tasks.iter().find(|(name, _, _)| name == task_name) {
            let mut cmd = Command::new("bash");
            cmd.arg("-c").arg(code);
            let output = cmd.output().expect("Failed to execute command");
            let output_str = String::from_utf8_lossy(&output.stdout).to_string();
            print!("{}", output_str);
        } else {
            println!("無効なタスクは存在しません");
            std::process::exit(1);
        }
    } else {
        for (name, help, _) in tasks {
            println!("{}\t{}", name, help);
        }
    }
}

まずは cargo runで様子を見ます。

bash
$ cargo test
   Compiling xmd v0.1.0 (/home/roomtv/works/xmd)
    Finished test [unoptimized + debuginfo] target(s) in 0.21s
     Running unittests src/main.rs (target/debug/deps/xmd-428818dfc198b539)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

お、ビルドは問題なく通りました。

では cargo run してみます。

shell
$ cargo run
   Compiling xmd v0.1.0 (/home/xxxx/xmd)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/xmd`
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:40:77
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

let code_block = lines.iter().find(|l| l.trim().starts_with("```")).unwrap();unwrapで怒られている模様。

第4回 リトライ

さっそくChatGPTにフィードバックします。

プロンプト
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:40:77
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

このエラーは、unwrap()がNone値で呼び出されたことを示しています。これは、code_blockがNoneの場合に発生します。そのような状況を回避するために、コードブロックが存在しない場合にはデフォルト値を使用するように変更しましょう。

修正後の実装コード:

rust
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();

    // 省略: 引数解析処理

    let path = PathBuf::from(filename);
    if !path.exists() {
        println!("README.md がありません");
        std::process::exit(1);
    }

    let content = fs::read_to_string(&path).expect("Failed to read file");

    let task_blocks: Vec<&str> = content.split("###").skip(1).collect();
    let mut tasks = Vec::new();

    for task_block in task_blocks {
        let lines: Vec<&str> = task_block.lines().collect();
        let task_name = lines[0].trim();
        let help_text = lines[1..].iter().map(|l| l.trim()).collect::<Vec<&str>>().join(" ");
        let code_block = lines.iter().find(|l| l.trim().starts_with("```")).unwrap_or(&"``` ```");
        let code = code_block.trim_start_matches("```").trim_end_matches("```").trim();
        tasks.push((task_name, help_text, code));
    }

    // 省略: タスク実行処理

}

unwrap_orを使って、code_blockがNoneの場合にデフォルト値を提供します。これにより、パニックが発生することなくプログラムが実行されるようになります。ただし、これは空のコードブロックが存在すると仮定しています。仕様に従ってREADMEファイルが記述されている場合、この問題は発生しないはずです。

なんかあっさり // 省略: 引数解析処理とか // 省略: タスク実行処理 とかブロックに命名してるし・・・
ともかく、cargo run してみます。

bash
$ cargo run
   Compiling xmd v0.1.0 (/home/roomtv/works/xmd)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/xmd`
前提条件        - タスクランナーのコマンドラインツール 
動作    - 同一ディレクトリ内の README.md という Markdown 形式のファイルを読み込む。 - "```" で囲まれた箇所をコードブロックとする。 - "
" で始まり次の "
"の間でコードブロックを含む範囲をタスク定義ブロックとする。     - タスク定義ブロックの "
"以降の文字列をタスク名とする。 - タスク定義ブロックの "
"の次の行からコードブロックまでの間の行はヘルプ文とする - タスク名が渡されたらタスク定義ブロック内のコードブロックをシェルスクリプトとして実行する - タスク名が渡されなければ、すべての"タスク名 タブ 空白文字を除去したヘルプ文"を出力する - 出力されるヘルプ文は、2行目以降はインデントをそろえる - README.md というファイルがなければ "README.md がありません"と出力する - `-f` オプションで README.md 以外のMarkdownファイルも指定できる  なお出力形式は以下とします。 テストコードは同値分割法(Equivalence Partitioning Testing)と境界値分析(Boundary value analysis)を用いてください。  ## 出力形式 - 概要 - 短い実装コード  私たちがこのタスクで最高の結果を出すためにステップバイステップで考えてください。 ```  ## タスクリスト 
test     Rust test を実行します。 いつものあれだし。  ``` cargo test ``` 
echo     Echo "hello world!"  ``` echo "hello world!" ```

ここで使用している README.md は https://github.com/asopitech/xmd/blob/main/README.md に上がっているものです。
なんか挙動は違うよね~

cargo run testしてみます。

$ cargo run test
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/xmd test`

なんもない。

仕様に立ち返って

ちょっと仕様からみなおしてみますが。。。
このままではファイル全体の###が対象になっているので、## タスクリスト 以下の ### タスク名 を対象とし、また、ヘルプ文の説明を修正してみます。

プロンプト
あなたは優秀なプログラマーです。
以下の仕様をもとにRust言語のテストコードを作成してください。
また、仕様を満たしあなたが出力したテストコードを通過するプログラムをRust言語で作成してください。

## 仕様
### 前提条件
- タスクランナーのコマンドラインツール

### 動作
- 本コマンドを xmd とする。
- 同一ディレクトリ内の README.md という Markdown 形式のファイルを読み込む。
- "## タスクリスト" で始まる行以下を対象とする。
- "```" で囲まれた箇所をコードブロックとする。
- "###" で始まり次の "###"の間でコードブロックを含む範囲をタスク定義ブロックとする。 
- タスク定義ブロックの "### "以降の文字列をタスク名とする。 
- タスク定義ブロックの "### "の次の行からコードブロックまでの間のコードブロックを含まない行をヘルプ文とする
- タスク名が渡されたらタスク定義ブロック内のコードブロックをシェルスクリプトとして実行する
- タスク名が渡されなければ、すべての"タスク名 タブ 空白文字を除去したヘルプ文"を出力する
- 出力されるヘルプ文は、2行目以降はインデントをそろえる
- README.md というファイルがなければ "README.md がありません"と出力する
- `-f` オプションで README.md 以外のMarkdownファイルも指定できる

なお出力形式は以下とします。
テストコードは同値分割法(Equivalence Partitioning Testing)と境界値分析(Boundary value analysis)を用いてください。

## 出力形式
- 概要
- 短い実装コード

私たちがこのタスクで最高の結果を出すためにステップバイステップで考えてください。

まず、仕様を簡単に理解するために要点を抽出します。

  1. README.md または -f オプションで指定された Markdown ファイルを読み込む。
  2. "## タスクリスト" 以下を対象にする。
  3. タスク名、ヘルプ文、コードブロックを抽出する。
  4. タスク名が渡された場合、該当するコードブロックをシェルスクリプトとして実行する。
  5. タスク名が渡されない場合、すべてのタスクのヘルプを整形して出力する。
  6. README.md が存在しない場合、エラーメッセージを表示する。
  7. -f オプションで別のファイルを指定できる。

ステップ1: テストコードの作成
まずはテストケースの設計から始めましょう。同値分割法と境界値分析を用いてテストケースを作成します。

rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_file_not_found() {
        // ファイルが存在しない場合のテスト
    }

    #[test]
    fn test_no_task_name_provided() {
        // タスク名が渡されない場合のテスト
    }

    #[test]
    fn test_task_name_provided() {
        // タスク名が渡された場合のテスト
    }

    #[test]
    fn test_alternative_file_with_f_option() {
        // -f オプションで別のファイルを指定した場合のテスト
    }
}

ステップ2: プログラムの実装
次に、プログラムを実装します。この実装は短い実装コードであることが要求されていますので、最小限の機能を持つ実装を提案します。

rust
use std::env;
use std::fs;
use std::process::Command;
use std::io::{BufRead, BufReader};

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut file_name = "README.md".to_string();
    let mut task_name = "".to_string();
    
    // 引数の解析
    for i in 1..args.len() {
        if args[i] == "-f" && i + 1 < args.len() {
            file_name = args[i + 1].clone();
            i += 1;
        } else if task_name.is_empty() {
            task_name = args[i].clone();
        }
    }

    let file_content = match fs::read_to_string(&file_name) {
        Ok(content) => content,
        Err(_) => {
            println!("{} がありません", file_name);
            return;
        }
    };

    let mut tasks: Vec<(String, String, String)> = vec![];
    let mut in_task_list = false;
    let mut current_task_name = String::new();
    let mut current_help_text = String::new();
    let mut current_code_block = String::new();
    let mut in_code_block = false;

    for line in file_content.lines() {
        if line.starts_with("## タスクリスト") {
            in_task_list = true;
            continue;
        }

        if !in_task_list {
            continue;
        }

        if line.starts_with("```") {
            in_code_block = !in_code_block;
            if !in_code_block {
                tasks.push((current_task_name.clone(), current_help_text.clone(), current_code_block.clone()));
                current_task_name.clear();
                current_help_text.clear();
                current_code_block.clear();
            }
            continue;
        }

        if in_code_block {
            current_code_block.push_str(line);
            current_code_block.push('\n');
        } else if line.starts_with("### ") {
            current_task_name = line[4..].to_string();
        } else if !line.is_empty() {
            current_help_text.push_str(line.trim());
            current_help_text.push('\n');
        }
    }

    if task_name.is_empty() {
        for (task_name, help_text, _) in &tasks {
            println!("{}{}", task_name, help_text);
        }
    } else {
        for (name, _, code_block) in tasks {
            if name == task_name {
                let output = Command::new("sh")
                    .arg("-c")
                    .arg(&code_block)
                    .output()
                    .expect("シェルスクリプトの実行に失敗しました");
                println!("{}", String::from_utf8_lossy(&output.stdout));
                break;
            }
        }
    }
}

これで要求された機能を実装した短い実装コードが完成しました。ただし、このコードは簡潔さを重視しており、エラーハンドリングやパフォーマンスに関しては改善の余地があります。また、現状ではシェルスクリプトの実行が Unix 系 OS でのみサポートされています。

まぁテストケースは相変わらずですが、後で出しなしおさせましょう。

再度、トライ!

さて、すでに嫌な予感がしてますが(笑)、実行してみます。

bash
$ cargo test
   Compiling xmd v0.1.0 (/home/roomtv/works/xmd)
warning: unused import: `BufReader`
 --> src/main.rs:4:24
  |
4 | use std::io::{BufRead, BufReader};
  |                        ^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: unused import: `BufRead`
 --> src/main.rs:4:15
  |
4 | use std::io::{BufRead, BufReader};
  |               ^^^^^^^

warning: value assigned to `i` is never read
  --> src/main.rs:15:13
   |
15 |             i += 1;
   |             ^
   |
   = help: maybe it is overwritten before being read?
   = note: `#[warn(unused_assignments)]` on by default

error[E0384]: cannot assign twice to immutable variable `i`
  --> src/main.rs:15:13
   |
12 |     for i in 1..args.len() {
   |         -
   |         |
   |         first assignment to `i`
   |         help: consider making this binding mutable: `mut i`
...
15 |             i += 1;
   |             ^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
warning: `xmd` (bin "xmd" test) generated 3 warnings
error: could not compile `xmd` due to previous error; 3 warnings emitted

同じようにループ内で i に再代入してます。まぁ・・・はい。

プロンプト
error[E0384]: cannot assign twice to immutable variable `i`
  --> src/main.rs:15:13
   |
12 |     for i in 1..args.len() {
   |         -
   |         |
   |         first assignment to `i`
   |         help: consider making this binding mutable: `mut i`
...
15 |             i += 1;
   |             ^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
warning: `xmd` (bin "xmd" test) generated 3 warnings
error: could not compile `xmd` due to previous error; 3 warnings emitted

申し訳ありません。コード内でイテレータ i に対して再代入しようとしていましたが、これはイテレータに対して行うべきではありません。代わりに、enumerate() を使ってインデックスと値を一緒にループさせます。

以下の修正されたコードをお試しください。

rust
use std::env;
use std::fs;
use std::process::Command;
use std::io::{BufRead, BufReader};

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut file_name = "README.md".to_string();
    let mut task_name = "".to_string();
    
    // 引数の解析
    for (i, arg) in args.iter().skip(1).enumerate() {
        if arg == "-f" && i + 1 < args.len() {
            file_name = args[i + 1].clone();
            break;
        } else if task_name.is_empty() {
            task_name = arg.clone();
        }
    }

    let file_content = match fs::read_to_string(&file_name) {
        Ok(content) => content,
        Err(_) => {
            println!("{} がありません", file_name);
            return;
        }
    };

    let mut tasks: Vec<(String, String, String)> = vec![];
    let mut in_task_list = false;
    let mut current_task_name = String::new();
    let mut current_help_text = String::new();
    let mut current_code_block = String::new();
    let mut in_code_block = false;

    for line in file_content.lines() {
        if line.starts_with("## タスクリスト") {
            in_task_list = true;
            continue;
        }

        if !in_task_list {
            continue;
        }

        if line.starts_with("```") {
            in_code_block = !in_code_block;
            if !in_code_block {
                tasks.push((current_task_name.clone(), current_help_text.clone(), current_code_block.clone()));
                current_task_name.clear();
                current_help_text.clear();
                current_code_block.clear();
            }
            continue;
        }

        if in_code_block {
            current_code_block.push_str(line);
            current_code_block.push('\n');
        } else if line.starts_with("### ") {
            current_task_name = line[4..].to_string();
        } else if !line.is_empty() {
            current_help_text.push_str(line.trim());
            current_help_text.push('\n');
        }
    }

    if task_name.is_empty() {
        for (task_name, help_text, _) in &tasks {
            println!("{}{}", task_name, help_text);
        }
    } else {
        for (name, _, code_block) in tasks {
            if name == task_name {
                let output = Command::new("sh")
                    .arg("-c")
                    .arg(&code_block)
                    .output()
                    .expect("シェルスクリプトの実行に失敗しました");
                println!("{}", String::from_utf8_lossy(&output.stdout));
                break;
            }
        }
    }
}

この修正により、コンパイルエラーは解消されるはずです。再度コンパイルして動作確認を行ってください。

何度目の正直?!

というわけで、改めて、以下のような main.rs にしました。

main.rs
use std::env;
use std::fs;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();
    let mut file_name = "README.md".to_string();
    let mut task_name = "".to_string();
    
    // 引数の解析
    for (i, arg) in args.iter().skip(1).enumerate() {
        if arg == "-f" && i + 1 < args.len() {
            file_name = args[i + 1].clone();
            break;
        } else if task_name.is_empty() {
            task_name = arg.clone();
        }
    }

    let file_content = match fs::read_to_string(&file_name) {
        Ok(content) => content,
        Err(_) => {
            println!("{} がありません", file_name);
            return;
        }
    };

    let mut tasks: Vec<(String, String, String)> = vec![];
    let mut in_task_list = false;
    let mut current_task_name = String::new();
    let mut current_help_text = String::new();
    let mut current_code_block = String::new();
    let mut in_code_block = false;

    for line in file_content.lines() {
        if line.starts_with("## タスクリスト") {
            in_task_list = true;
            continue;
        }

        if !in_task_list {
            continue;
        }

        if line.starts_with("```") {
            in_code_block = !in_code_block;
            if !in_code_block {
                tasks.push((current_task_name.clone(), current_help_text.clone(), current_code_block.clone()));
                current_task_name.clear();
                current_help_text.clear();
                current_code_block.clear();
            }
            continue;
        }

        if in_code_block {
            current_code_block.push_str(line);
            current_code_block.push('\n');
        } else if line.starts_with("### ") {
            current_task_name = line[4..].to_string();
        } else if !line.is_empty() {
            current_help_text.push_str(line.trim());
            current_help_text.push('\n');
        }
    }

    if task_name.is_empty() {
        for (task_name, help_text, _) in &tasks {
            println!("{}{}", task_name, help_text);
        }
    } else {
        for (name, _, code_block) in tasks {
            if name == task_name {
                let output = Command::new("sh")
                    .arg("-c")
                    .arg(&code_block)
                    .output()
                    .expect("シェルスクリプトの実行に失敗しました");
                println!("{}", String::from_utf8_lossy(&output.stdout));
                break;
            }
        }
    }
}

で、これを叩いてみます・・・

bash
$ cargo test
   Compiling xmd v0.1.0 (/home/roomtv/works/xmd)
    Finished test [unoptimized + debuginfo] target(s) in 0.23s
     Running unittests src/main.rs (target/debug/deps/xmd-428818dfc198b539)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

コンパイルはOKです。(テストがパスしてるのはおいておいて・・・)

bash
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/xmd`
testRust test を実行します。
いつものあれだし。

echoEcho "hello world!"

さっきよりはよくなりましたね。。
では cargo runでコマンド叩いてみましょう。

$ cargo run echo
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/xmd echo`
hello world!

$ cargo run test
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/xmd test`

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

おーーーー!!!
無事に hello world! でました~!!

雑感

GPT-4 モデル、確かに応答速度は遅くなってます。とはいえ精度はかなり上がっているようです。
前回はリトライすればするほど修正がおかしくなっていことも多々あり、何度も初めからやり直すこともありました。
バグを指摘しても、"そこだけ"しか修正せず、全体的な整合性の取れないコードになってしまったり。
粗い指示でもかなり的確にタスクの理解ができているような感じがします。
有料版にした甲斐がありました。。。w

本日はここまでといたします。

成果は以下に上がっております。

https://github.com/asopitech/xmd

お疲れさまでした!

蛇足

プロンプト
ここまでの流れを表すのに相応しいユニコードの絵文字ってなんだろう?1文字だけ教えて!

ユニコード絵文字の中で、ここまでの流れを表すものとして「🔄」が適切だと思われます。この絵文字は「繰り返し」や「プロセス」を意味し、プログラミングやコーディングの進行を表すのに適しています。

マジ、すげーよコイツ。。。

Discussion