Skip to content

Instantly share code, notes, and snippets.

@chikoski
Last active April 19, 2021 14:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chikoski/ac78c4a57a4addcf38b5dd2d5747083e to your computer and use it in GitHub Desktop.
Save chikoski/ac78c4a57a4addcf38b5dd2d5747083e to your computer and use it in GitHub Desktop.
GTUG Girls Rust ハンズオン

Rust ハンズオン

今回のハンズオンでは、UNIX や macOS でよく使われるコマンド grep を素朴に実装します。 実装を通じて、Rust の基礎的な知識のうちのいくつかをカバーできればと思っています。 具体的には、次のトピックを扱います。

  • 基本的な文法
    • 変数と束縛
    • 制御構造
    • 基本的なデータ型
  • エラー処理
  • ライブラリーの利用

ハンズオンで作るプログラム

ハンズオンでは、次の 3 つを実装します。

  • FizzBuzz
  • cat コマンド
  • grep コマンド

FizzBuzz は基本的な文法の確認のため、cat コマンドはエラー処理の例として実装します。

最後の grep コマンドは cat コマンドを発展させる形で作成します。これはライブラリを使う練習も兼ねています。

想定する参加者

  • 初級から中級程度の、他のプログラミング言語での経験をお持ちの方
  • Rust について、名前を聞いたことはある、くらいの経験をお持ちの方

次のような方は、参加されても楽しめないかもしれません。

  • プログラミング経験を豊富にお持ちの方
  • Rust でプログラムを書いた経験をお持ちの方

ハンズオンに参加される方へのお願い

prework.md に書かれている内容を事前に済ませていただけると、ハンズオンの時間をより有効に利用できます。

1 の「コンパイラのインストール」だけでも済ませていただけると、大変助かります。

GTUG Girls 主催の Rust ハンズオンに参加される皆様

今回ハンズオンで講師をします chikoski と申します。よろしくお願いいたします。

ハンズオンを円滑に進めそして学びの多いものとするために、次の 1 から 3 までを事前に行っていただければ幸いです。

  1. コンパイラのインストール
  2. エディタ / IDE の設定
  3. hello world の実行
  4. [オプション] FizzBuzz の実装

4 はハンズオン中にも触れます。余力がある方向けのお願いで、必ずやる必要はありません。ただし事前に済ませておかれると、より理解がすすむかと思われます。

1. コンパイラのインストール

Rust で書いたプログラムを実行するためには、コンパイルが必要です。 ハンズオンの時間を節約するために、事前にコンパイラをインストールいただけると幸いです。

インストール方法は、下記の記事を参照してください:

2. エディタ / IDE の設定

Rust 向けのモードが用意されているエディタはいくつかあります。普段お使いのエディタにも、きっと Rust モードやプラグインが用意されているはずです。 インストールいただけると、開発がグッと楽になると思います。

次にいくつかのエディタでの設定方法を開設したドキュメントをリンクします。参考になれば幸いです。

3. hello world の実行

次のように cargo というコマンドをターミナルから実行すると、hello world のプログラムができます。

$ cargo new --bin hello_world 

次のコマンドで、作成された hello world を実行できます。

$ cd hello_world
$ cargo run 

4. FizzBuzz を実行する

次の例を参考に、ご自身で FizzBuzz の実装をお願いします。

fn main(){
  let mut index = 1;
  while index < 50 {
    if index % 15 == 0{
      println!("FizzBuzz");
    }else if index % 5 == 0{
      println!("Buzz");
    }else if index % 3 == 0{
      println!("Fizz");
    }else{
      println!("{}", index);
    }
    index = index + 1;
  }
}

この例の動作は、こちらのページ で確認できます。 "Run" と書かれたボタンを押すと、プログラムが実行されます。

  • chikoski@ と申します
  • さまざまな開発者のお悩み相談を受け付ける係をしています
  • Rust とは、主にコミュニティイベントの主催者として関わってきました:
    • 2013 年ころから v1.0 になる 2015 年まで、Rust Samurai というイベントを共催していました
    • 2019 年は Rust.Tokyo というイベントを開催しました
    • 2020 年はグローバルカンファレンスの RustFest Global 2020のオーガナイザーを務めました
  • プログラミング言語処理系や、システムプログラミングに興味があります

Rust ハンズオン

このハンズオンについて

  • Rust とは?から始まって、基本的な文法をカバーします
  • 自分なりの grep コマンドを実装します
  • 自分のペースで、できるところまでやりましょう

Rust とは

  • システムプログラミング用の言語としてスタートしました
  • 信頼性が高く、パフォーマンスの出るプログラムを書けるように設計されています
  • 組み込みからWeb まで、さまざまな場面で利用されています

Rust の採用例

ドキュメント

今回の成果物:grep

  • テキストファイルから、パターンに一致する行を抜き出すコマンド
  • 使い方:grep オプション パターン ファイル名
  • 次の例では、Cargo.toml の中から、2018 という文字列が含まれる行を抜き出しています:
edition = "2018"

本日の内容

  1. FizzBuzz を作ろう
  2. テキストファイルを表示するプログラムを作ろう
  3. 2 を改造して、grep コマンドを作ろう

FizzBuzz を作ろう

  • 1 から 100 までの数値を出力します
  • ただし、次の場合は数字の代わりに指定された文字列を出力します
    • 数字が 3 の倍数の場合、Fizz を出力します
    • 数字が 5 の倍数の場合、Buzz を出力します
    • 数字が 3 の倍数で、しかも 5 の倍数でもある場合には、FizzBuzz を出力します

プロジェクトの作成

  • Rust のプロジェクトの操作には、cargo コマンドを使います
  • cargo new を実行すると、プロジェクトが作成されます
% cargo new fizzbuzz
     Created binary (application) `fizzbuzz` package

プロジェクトのフォルダ構成

fizzbuzz
├── Cargo.toml
└── src
   └── main.rs

プロジェクトのビルドと実行

  • cargo build でビルドします
  • cargo run で、ビルドしたプログラムを実行します
    • ビルドされていない場合は、ビルドも行います
    • 標準ではデバッグビルドを実行します
% cd fizzbuzz
% cargo run
   Compiling fizzbuzz v0.1.0 (/Users/chikoski/fizzbuzz)
    Finished dev [unoptimized + debuginfo] target(s) in 0.77s
     Running `target/debug/fizzbuzz`
Hello, world!

main.rs

  • main 関数が定義されています
  • この関数(main)がエントリーポイントです
  • println! は文字列を出力し、最後に改行するマクロです
fn main() {
    println!("Hello, world!");
}

繰り返し:while 文

fn main() {
  let mut counter = 0;
  while counter < 10{
    println!("Hello, world!");
    counter += 1;
  }
}

イテレーターを使った書き換え

  • 0..10 は 0 以上 10 未満の範囲を表すオブジェクトを定義します
  • このオブジェクトは、イテレーターとしての性質を持っています
  • そのため for 文で範囲に含まれる整数を列挙できます
  • 下記では、列挙された値を使わないので、_ を利用しています
fn main() {
  for _ in 0..10{
    println!("Hello, world!");
  }
}

条件分岐:if 式

  • Rust での if は式です。つまり評価値を持ちます
  • 実行したブロックで最後に評価した式の値が、if 式の評価値となります
  • ブロックの最後の式に ; がついていないことがポイントです
fn main() {
  for n in 0..10{
    let output = if n % 15 == 0{
      "FizzBuzz"
    }else{
      format!("{}", n)
    };
    println!("{}", output);
  }
}

コンパイル時の型チェック

  • コンパイル時には型チェックが行われます
  • 次の例では、if 節の評価値が str 型なのに対し、else の評価値が String 型であることが原因でコンパイルエラーとなっています
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:6:9
  |
3 |         let output = if n % 15 == 0{
  |  ____________________-
4 | |         "FizzBuzz"
  | |         ---------- expected because of this
5 | |       }else{
6 | |         format!("{}", n)
  | |         ^^^^^^^^^^^^^^^^ expected `&str`, found struct `String`
7 | |       };
  | |_______- `if` and `else` have incompatible types
  |

String と str

  • String は文字列を表すオブジェクトです
  • str は文字列のスライスを表す値です
  • to_string メソッドで、String オブジェクトへ変換できます
let name = "World";
let message = format!("Hello, {}!", name);
println!("{}", message);
   
let slice_of_message = &message[0..5];
println!("{}", slice_of_message);

let another_string = slice_of_message.to_string();
println!("{}", another_string);

コンパイルエラーの修正

  • if 節の評価値の型が String となるように修正します
  • to_string メソッドを呼ぶことで、String 型の "FizzBuzz" を作成できます
fn main() {
  for n in 0..10{
    let output = if n % 15 == 0{
      "FizzBuzz".to_string()
    }else{
      format!("{}", n)
    };
    println!("{}", output);
  }
}

より詳しくコンパイルエラーについて知りたい場合は

  • rustc に --explain オプションをつけて実行すると、より詳しい解説を読めます
  • 次の例では、E0308 のエラーについて、解説を読みます
  • 同じ解説を Web でも読めます
% rustc --explain E0308
Expected type did not match the received type.

Erroneous code examples:

FizzBuzz: 手続き的なバージョン

  • 以下は、手続き的に書いた FizzBuzz の例です
  • これを少しずつ改変し、Rust の柔軟性をみていきます
fn main() {
  for n in 1..20{
    let output = if n % 15 == 0{
      "FizzBuzz".to_string()
    }else if n % 5 == 0{
      "Buzz".to_string()
    }else if n % 3 == 0{
      "Fizz".to_string()
    }else{
      format!("{}", n)
    };
    println!("{}", output);
  }
}

関数への切り出し

  • fn キーワード で関数を定義できます
  • () 内に引数のリストを、-> の後に返り値の型を書きます
  • 下記は、FizzBuzz の数値から文字列への変換を、関数に切り出した例です
  • u32 を String への変換として実装しています
fn fizzbuzz(n: u32) -> String{
  if n % 15 == 0{
    "FizzBuzz".to_string()
  }else if n % 5 == 0{
    "Buzz".to_string()
  }else if n % 3 == 0{
    "Fizz".to_string()
  }else{
    format!("{}", n)
  }
}

関数呼び出し

  • 実引き数をを与えて関数を呼びます
  • noutput に対して、型を明記していないのは、コンパイラーが型推論を行うためです
fn main() {
  for n in 1..20{
    let output = fizzbuzz(n);
    println!("{}", output);
  }
}

単体テスト

  • #[test] とアノテーションされた関数は、テスト用の関数として処理されます
  • 次の例では、fizzbuzz の振る舞いをテストしています
  • cargo test でテストを実行できます
#[test]
fn test_fizzbuzz_returns_fizzbuzz() {
    let expected = "FizzBuzz".to_string();
    let actual = fizzbuzz(15);
    assert_eq!(expected, actual);
}

変換部分を関数に切り出した結果

fn fizzbuzz(n: u32) -> String{
  if n % 15 == 0{
    "FizzBuzz".to_string()
  }else if n % 5 == 0{
    "Buzz".to_string()
  }else if n % 3 == 0{
    "Fizz".to_string()
  }else{
    format!("{}", n)
  }
}

fn main() {
  for n in 1..20{
    let output = fizzbuzz(n);
    println!("{}", output);
  }
}

FizzBuzz:関数プログラミング的なアプローチ

  • FizzBuzz はデータ変換を行う関数として捉えることもできます
  • 例:数値の範囲 -> 文字列の配列
  • 一つ一つの数値を、文字列に変換する関数は fizzbuzz として用意されています
  • これを上手に使って、関数プログラミング的なアプローチで FizzBuzz を書き直します。

FizzBuzz: map メソッド

  • コレクション中の要素一つ一つに関数を適用して、別のコレクションを作る map と呼ばれる操作は、関数プログラミングで良く利用されます
  • Rust のイテレーターにも map メソッドは用意されています
  • このメソッドは、各要素に、引数に与えた関数を適用した結果を持つイテレーターを返します
fn main() {
  for output in (1..20).map(fizzbuzz){
    println!("{}", output);
 }
}

クロージャー:関数の一種

  • クロージャーは関数の一種で、定義された時にアクセス可能な変数であれば関数本体内で利用できる、という点が特徴です
  • 無名関数やラムダ、といった名前でクロージャーを提供している言語もあります
  • Rust でもクロージャーは利用できます。下記の例では、 fold メソッドの第 2 引数でクロージャーを定義しています
  • 仮引数リストは || の間に記述します
fn main() {
  let output = (1..20).map(fizzbuzz).fold("".to_string(), |accum, line|{
    format!("{}\n{}", accum, line)
  });
  println!("{}", output);
}

関数プログラミング的な FizzBuzz

fn fizzbuzz(n: u32) -> String{
  if n % 15 == 0{
    "FizzBuzz".to_string()
  }else if n % 5 == 0{
    "Buzz".to_string()
  }else if n % 3 == 0{
    "Fizz".to_string()
  }else{
    format!("{}", n)
  }
}

fn main() {
  let output = (1..20).map(fizzbuzz).fold("".to_string(), |accum, line|{
    format!("{}\n{}", accum, line)
  });
  println!("{}", output);
}

FizzBuzz のまとめ

  • 基本的な文法を確認しました
  • 手続き的にも、関数型的にも書けます
  • コンパイラーを良いレビュワーとして付き合っていけると良いと思います

テキストファイルを出力するプログラムを作ろう

  1. 新しいプロジェクトを作ります
  2. 決まったファイルの中身を出力するプログラムを作ります
  3. コマンドライン引数で、読むファイルを指定できるように変更します

プロジェクトの作成

  • mygrep という名前のプロジェクトを作ります
  • 次の例では、ホームディレクトリにプロジェクトを作成しています
% cd
% cargo new mygrep

自分自身を出力するプログラム

  • 手始めに自分自身を出力するプログラムを作成します
  • std::fs:read_to_string は、引数で指定したパスから文字列として内容を読み込みます
fn main() {
  let path = "./src/main.rs";
  
  match std::fs::read_to_string(path) {
    Ok(content) => print!("{}", content),
    Err(reason) => println!("{}", reason)
  }
}

ファイル読み込みには失敗がつきもの

  • Rust では、ある操作の成否を Result を使って表現します
  • Result は成功を表す Ok と エラーを表す Err から成る enum です
  • is_ok メソッドで、OkErr かを判別できます
fn main() {
  let result: Result<u32, String> = Ok(1);
  let message = if result.is_ok(){
    "Success"
  }else{
    "Fail"
  };
  println!("{}", message);
}

成果物とエラーの理由

  • Ok は成果物を値として持てます。* 同様に Err も、エラーの理由を値として持てます
  • 成果物を表すデータ型や、エラーの理由を表すデータ型は、プログラムによって異なります
  • そのため Result を扱う際には、成果物のデータ型とエラーの理由を表すデータ型もあわせて指定します
  • 次の例では、成果物の型に u32 を指定し、エラーの理由は String で与えられるとしています
let result: Result<u32, String> = Ok(1);

成果物の取り出し方(unwrap を使う場合)

  • unwrap メソッドを使って、成果物(もしくはエラーの理由)を取り出せます
  • 次の例では、成功した場合に unwrap メソッドを使って成果物を取り出しています
fn main() {
  let result: Result<u32, String> = Ok(1);
  let message = if result.is_ok(){
    format!("Result = {}", result.unwrap())
  }else{
    "Fail".to_string()
  };
  println!("{}", message);
}

成果物の取り出し方(パターンマッチを使う場合)

  • 条件分岐と unwrap との組み合わせる方法は、パターンマッチを使って簡略化できます
  • パターンマッチは match 式 として記述できます
  • パターンの一部分を、変数に束縛することで、Ok / Err から値を取り出せます
fn main() {
  let result: Result<u32, String> = Ok(1);
  let message = match result {
    Ok(value) => format!("Result = {}", value),
    Err(_) => "Fail".to_string()
  };
  println!("{}", message);
}

Result のエイリアス:std::io::Result

  • エラーの理由を表す型が決まっている、といった理由で Result のエイリアスが作られることは良くあります
  • 代表例は、std::io::Result です
  • 次のように、エラーを std::io::Error で表すと定めています
type Result<T> = Result<T, std::io::Error>;

内容を出力する部分を関数へ切り出し

  • 出力するファイルを指定できるようにするための準備として、内容を出力する部分を関数に切り出します
  • ファイルのパスを String で受け取り、() を返す関数として定義しました
  • これにあわせて path の型が String になっている点に注意してください
fn run(path: String){
  match std::fs::read_to_string(path) {
    Ok(content) => print!("{}", content),
    Err(reason) => println!("{}", reason)
  }  
}

fn main() {
  let path = "./src/main.rs".to_string();
  run(path);
}

出力するファイルの指定方法

  • 出力するファイルを、コマンドライン引数として指定することとします
  • 実行のイメージは次のようになります
  • この例では src/main.rs を出力します
% cargo run src/main.rs

コマンドライン引数の取得

  • std::env::args() は、コマンドライン引数を String として保持するイテレーターを返します
  • nth メソッドで、n 番目の要素を取得できます
  • nth メソッドは Option という値を返します
fn main() {
  let arguments = std::env::args();
  match arguments.nth(1){
    Some(path) => run(path),
    None => println!("No path is specified"),
  }  
}

Option: null かもしれない値の表現

  • Option はオプション、つまり存在するかもしれないし、しないかもしれないといった値を表現します
  • 下記の例では、1 番目のコマンドライン引数を取得しています
  • この値が存在するかどうかは、ユーザーの入力に依存します
fn main() {
  let arguments = std::env::args();
  match arguments.nth(1){
    Some(path) => run(path),
    None => println!("No path is specified"),
  }  
}

Option の値:Some と None

fn main() {
  let arguments = std::env::args();
  match arguments.nth(1){
    Some(path) => run(path),
    None => println!("No path is specified"),
  }  
}

Option からの値取得

  • Some は、実際の値を内部に保持しています
  • Option は Result と同様に、unwrap メソッドがあります。これを利用して実際の値を取得できます
  • また下記のようにパターンマッチを利用しても、保持されている値を取得できます
fn main() {
  let arguments = std::env::args();
  match arguments.nth(1){
    Some(path) => run(path),
    None => println!("No path is specified"),
  }  
}

ここまででできたプログラム

fn run(path: String){
  match std::fs::read_to_string(path) {
    Ok(content) => print!("{}", content),
    Err(reason) => println!("{}", reason)
  }  
}

fn main() {
  let arguments = std::env::args();
  match arguments.nth(1){
    Some(path) => run(path),
    None => println!("No path is specified"),
  }  
}

grep への拡張

  • ここまでで、指定したファイルの中身を文字列として出力するプログラムができました。これを拡張して grep コマンドを実装します
  • grep には 2 つのコマンドライン引数があります。1 つ目がパターン、2 つめがファイルパスです
  • 次のれいでは、version がパターンで、Cargo.toml がファイルパスとなります
% grep version Cargo.toml
version = "0.1.0"

run を変更して grep を実装します

fn grep(content: String, pattern: String){
  for line in content.lines(){
    if line.contains(pattern.as_str()){
      println!("{}", line);
    }
  }
}

fn run(path: String, pattern: String){
  match std::fs::read_to_string(path) {
    Ok(content) => grep(content, pattern),
    Err(reason) => println!("{}", reason)
  }  
}

パターンとファイルパスの取得

  • std::env::args の返り値はイテレーターとしての性質を持っています
  • nth メソッドを呼ぶたびに、内部の状態が変わります
  • 状態変化による面倒を避けるため、都度 std::env::args を呼んでいます
fn main() {
  let pattern = std::env::args().nth(1);
  let path = std::env::args().nth(2);
  if pattern.is_some() && path.is_some(){
    run(path, pattern);
  }
}

ユーザー定義型

  • 今後の発展のために、pathpattern をまとめたデータ構造を作ります
  • データ構造は struct キーワードを使って定義できます
struct MyGrep{
  path: String,
  pattern: String,
}

impl:データ型振る舞いを与えるキーワード

  • impl キーワードを使うと、データ構造に振る舞いを定義できます
  • 例えば、データ構造を作成する関数 new は次のように定義できます
struct MyGrep{
  path: String,
  pattern: String,
}

impl MyGrep{
  fn new(path: String, pattern: String) -> MyGrep{
    MyGrep{
      path, 
      pattern,
    }
  }
}

MyGrep 型を使った書き換え

  • 定義した MyGrep 型を使うようにコードを書き換えます
  • 書き換えるのは、main と run の 2 関数です
  • 変数名とフィールド名を . でつなぐとフィールドの値を参照できます
fn run(mygrep: MyGrep){
  match std::fs::read_to_string(mygrep.path) {
    Ok(content) => grep(content, mygrep.pattern),
    Err(reason) => println!("{}", reason)
  }  
}

fn main(){
  let pattern = std::env::args().nth(1);
  let path = std::env::args().nth(2);

  if pattern.is_some() && path.is_some() {
    run(MyGrep::new(path.unwrap(), pattern.unwrap()))
  }
}

ここまでの状態

struct MyGrep{
  path: String,
  pattern: String,
}

impl MyGrep{
  fn new(path: String, pattern: String) -> MyGrep{
    MyGrep{
      path, 
      pattern,
    }
  }
}

fn grep(content: String, pattern: String){
  for line in content.lines(){
    if line.contains(pattern.as_str()){
      println!("{}", line);
    }
  }
}

fn run(mygrep: MyGrep){
  match std::fs::read_to_string(mygrep.path) {
    Ok(content) => grep(content, mygrep.pattern),
    Err(reason) => println!("{}", reason)
  }  
}

fn main(){
  let pattern = std::env::args().nth(1);
  let path = std::env::args().nth(2);

  if pattern.is_some() && path.is_some() {
    run(MyGrep::new(path.unwrap(), pattern.unwrap()))
  }
}

コマンドラインオプションに対応しよう

  • grep は -n オプションをつけると、行番号をつけて結果を出力します。この機能を実装します
  • オプションの解析には、ライブラリを利用することします
  • ライブラリのことを、Rust では crate(クレート)と呼びます

クレートの追加

  • 今回は structopt という crate を使います
  • Rust のライブラリは、crates.io というレポジトリにまとめられています
  • Cargo.toml の dependencies に、使用する crate を追記することで利用できるようになります
[dependencies]
structopt = "0.3.21"

structopt を利用したコマンドラインオプションの解析

  • structopt を使うと、コマンドラインオプションの解析を宣言的に記述できます
  • 定義したデータ構造の各フィールドにアトリビュートを追加することで、コマンドラインオプションとの対応関係を記述します
  • 以下は、MyGrep 型にアトリビュートを追加した例です
use structopt::StructOpt;

#[derive(StructOpt)]
#[structopt(name="mygrep")]
struct MyGrep{
  #[structopt(name = "PATTERN")]
  pattern: String,
  #[structopt(name = "FILE")]
  path: String,
}

structopt を利用したコマンドラインオプションの解析(つづき)

  • MyGrep::from_args は structopt によって追加されました
  • この関数が、コマンドラインオプションの解析と、MyGrep オブジェクトを作成します
fn main(){
  let mygrep = MyGrep::from_args();
  run(mygrep);
}

ここまでの状態(Cargo.toml)

[package]
name = "mygrep"
version = "0.1.0"
authors = ["自分のなまえ"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
structopt = "0.3.21"

ここまでの状態(main.rs)

  • MyGrep::new は使わなくなったので、削除しました
use structopt::StructOpt;

#[derive(StructOpt)]
#[structopt(name="mygrep")]
struct MyGrep{
  #[structopt(name = "PATTERN")]
  pattern: String,
  #[structopt(name = "FILE")]
  path: String,
}

fn grep(content: String, pattern: String){
  for line in content.lines(){
    if line.contains(pattern.as_str()){
      println!("{}", line);
    }
  }
}

fn run(mygrep: MyGrep){
  match std::fs::read_to_string(mygrep.path) {
    Ok(content) => grep(content, mygrep.pattern),
    Err(reason) => println!("{}", reason)
  }  
}

fn main(){
  let mygrep = MyGrep::from_args();
  run(mygrep);
}

use:名前空間へのシンボルの追加

Derive アトリビュート:コードの自動的な追加

-n オプションの追加

  • line_number という bool 型のフィールドを追加します
  • 追加したフィールドにも、structopt アトリビュートを追加します
  • short はショートオプション、long はロングオプションを受け付けるための記述です
struct MyGrep{
  #[structopt(name = "PATTERN")]
  pattern: String,
  #[structopt(name = "FILE")]
  path: String,
  #[structopt(short = "-n", long)]
  line_number: bool,
}

オプションが追加されたことの確認

  • 次のように --help オプションをつけて、cargo コマンドを実行し、-n オプションが存在することを確認します
  • --help--version は、structopt を Derive した際に追加されています
% cargo run -- --help
(中略)
mygrep 0.1.0

USAGE:
    mygrep [FLAGS] <PATTERN> <FILE>

FLAGS:
    -h, --help           Prints help information
    -n, --line-number
    -V, --version        Prints version information

ARGS:
    <PATTERN>
    <FILE>

-n を実装します

  • grep の第 1 引数を、MyGrep オブジェクトに変更します
fn grep(mygrep: MyGrep, content: String){
  let mut line_number = 1;
  for line in content.lines(){
    if line.contains(mygrep.pattern.as_str()){
      if mygrep.line_number {
        println!("{}: {}", line_number, line);
      }else{
        println!("{}", line);
      }
    }
    line_number += 1;
  }
}

grep のシグネチャ変更にあわせて、run も変更します

fn run(mygrep: MyGrep){
  match std::fs::read_to_string(mygrep.path) {
    Ok(content) => grep(mygrep, content),
    Err(reason) => println!("{}", reason)
  }  
}

所有権の移動に伴うコンパイルエラー

  • mygrep.path の値は read_to_string の引数に所有権が移動しています
  • 所有権が移動したフィールドをもつオブジェクトを利用しているので、コンパイルエラーがおきます
  • 所有権は代入や、関数呼び出しによって移動します
error[E0382]: use of partially moved value: `mygrep`
  --> src/main.rs:68:25
   |
67 |   match std::fs::read_to_string(mygrep.path) {
   |                                 ----------- value partially moved here
68 |     Ok(content) => grep(mygrep, content),
   |                         ^^^^^^ value used here after partial move
   |
   = note: partial move occurs because `mygrep.path` has type `String`, which does not implement the `Copy` trait

error: aborting due to previous error

For more information about this error

参照と借用

  • 所有権を渡す代わりに、一時的に値を貸し出す、ということもできます
  • 貸し出す場合は、値は参照という形で渡します
  • 変数名の前に & をつけることで、値への参照を取得します
fn run(mygrep: MyGrep){
  match std::fs::read_to_string(&mygrep.path) {
    Ok(content) => grep(mygrep, content),
    Err(reason) => println!("{}", reason)
  }  
}

ここまで

use structopt::StructOpt;

#[derive(StructOpt)]
#[structopt(name="mygrep")]
struct MyGrep{
  #[structopt(name = "PATTERN")]
  pattern: String,
  #[structopt(name = "FILE")]
  path: String,
  #[structopt(short = "-n", long)]
  line_number: bool,
}

fn grep(mygrep: MyGrep, content: String){
  let mut line_number = 1;
  for line in content.lines(){
    if line.contains(mygrep.pattern.as_str()){
      if mygrep.line_number {
        println!("{}: {}", line_number, line);
      }else{
        println!("{}", line);
      }
    }
    line_number += 1;
  }
}

fn run(mygrep: MyGrep){
  match std::fs::read_to_string(&mygrep.path) {
    Ok(content) => grep(mygrep, content),
    Err(reason) => println!("{}", reason)
  }  
}

fn main(){
  let mygrep = MyGrep::from_args();
  run(mygrep);
}

-r オプションを実装しよう

  • -r オプションは、指定されたパターンを正規表現として解釈するオプションです
  • 正規表現は regex クレートで実現されています
  • まず Cargo.toml を編集して regex クレートを読み込んだ上で、main.rs に機能を実装します
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment