2024年7月5日金曜日

clap-rs で Rust のコマンドライン引数をパースする

やること

clap-rs/clap: A full featured, fast Command Line Argument Parser for Rust を使ってオプションパースする。

clap-rs には、 Builder を使ってオプションを定義する方法と、 Derive を使ってオプションを定義する方法のふたつがある。

今回は、 Derive を使用してオプションを定義していく。

サブコマンドなどの機能もあるようだが、今回は単純なオプションパースのみを行う。

前提

プロジェクト作成

cargo init
cargo add clap --features derive

実装

オプション用の構造体を作成し、フィールドとしてオプションを作成。 それぞれにアトリビュートを付けることでそれがコマンドラインオプションであることを表す。

アトリビュート詳細は clap::_derive - Rust。

use clap::Parser;


#[derive(Parser, Debug)]
#[command(version, about = "コマンドライン引数パース処理練習プログラム")]
struct Args {
    #[arg(short, long, help = "文字列オプション")]
    string: String,

    #[arg(short, long, default_value = "default", help = "オプション変数はデフォルト値を入れる感じ")]
    default_string: String,

    #[arg(long, help = "u8 オプション")]
    number_u8: u8,

    #[arg(long, help = "u16 オプション")]
    number_u16: u16,

    #[arg(short, long, help = "f32 オプション")]
    f32: f32,

    #[arg(short, long, help = "真偽値オプション")]
    bool: bool,

    #[arg(long, help = "文字配列オプション")]
    string_array: Vec<String>,
}

fn main() {
    let args = Args::parse();

    println!("文字列オプションを渡せますよー: {}", args.string);
    println!("文字列オプションオプションを渡せますよー: {}", args.default_string);
    println!("数値(u8)オプションを渡せますよー: {}", args.number_u8);
    println!("数値(u16)オプションを渡せますよー: {}", args.number_u16);
    println!("数値(f32)オプションを渡せますよー: {}", args.f32);
    println!("真偽値オプションを渡せますよー: {}", args.bool);
    println!("配列オプションを渡せますよー: {:?}", args.string_array);
}

単純なオプションを定義していった。

動作確認

$ cargo run -- --help
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/clap-rs_firststep --help`
コマンドライン引数パース処理練習プログラム

Usage: clap-rs_firststep [OPTIONS] --string <STRING> --number-u8 <NUMBER_U8> --number-u16 <NUMBER_U16> --f32 <F32>

Options:
  -s, --string <STRING>                  文字列オプション
  -d, --default-string <DEFAULT_STRING>  オプション変数はデフォルト値を入れる感じ [default: default]
      --number-u8 <NUMBER_U8>            u8 オプション
      --number-u16 <NUMBER_U16>          u16 オプション
  -f, --f32 <F32>                        f32 オプション
  -b, --bool                             真偽値オプション
      --string-array <STRING_ARRAY>      文字配列オプション
  -h, --help                             Print help
  -V, --version                          Print version


$ cargo run -- -s aaa \
>         --number-u8 8 \
>         --number-u16 16 \
>         --string-array kore \
>         --string-array sore \
>         -b \
>         -f 32.33 \
>         --default-string ddd
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/clap-rs_firststep -s aaa --number-u8 8 --number-u16 16 --string-array kore --string-array sore -b -f 32.33 --default-string ddd`
文字列オプションを渡せますよー: aaa
文字列オプションオプションを渡せますよー: ddd
数値(u8)オプションを渡せますよー: 8
数値(u16)オプションを渡せますよー: 16
数値(f32)オプションを渡せますよー: 32.33
真偽値オプションを渡せますよー: true
配列オプションを渡せますよー: ["kore", "sore"]

うん、良さそう。

以上。

参考資料

2024年7月3日水曜日

Tauri の State で状態を管理する

Tauri に状態の保存を任せられるので、任せられるものは任せていきましょう。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • delete-default-event をベースに作業をしたので、そのディレクトリとの差分を取ってください

実装

今回は、定番のカウンターと、配列操作を作ってみる。

Tauri が提供する State に Arc<Mutex<xxx>> を突っ込むイメージ。

■ src-tauri/src/main.rs:

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc, Mutex,
};

use tauri::State;

// State 用構造体
// AtomicUsize は、既にスレッドセーフなので
// Arc と Mutex で囲む必要はない。
struct Counter {
    count: AtomicUsize,
}

#[tauri::command]
fn inc_count(counter: State<'_, Counter>) -> usize {
    // 最大値のチェックは省略
    counter.count.fetch_add(1, Ordering::Relaxed) + 1
}

#[tauri::command]
fn dec_count(counter: State<'_, Counter>) -> usize {
    if counter.count.load(Ordering::Relaxed) > 0 {
        counter.count.fetch_sub(1, Ordering::Relaxed) - 1
    } else {
        0
    }
}

// けんぱスタック
// `けん` か `ぱ` を要素に持つ配列を記録する。
struct KenPaStack {
    ken_pa: Arc<Mutex<Vec<String>>>,
}

#[tauri::command]
fn put_ken(ken_pa_stack: State<'_, KenPaStack>) {
    ken_pa_stack
        .ken_pa
        .lock()
        .unwrap()
        .append(&mut vec!["けん".to_string()]);
}

#[tauri::command]
fn put_pa(ken_pa_stack: State<'_, KenPaStack>) {
    ken_pa_stack
        .ken_pa
        .lock()
        .unwrap()
        .append(&mut vec!["ぱ".to_string()]);
}

#[tauri::command]
fn get_ken_pa_stack(ken_pa_stack: State<'_, KenPaStack>) -> Vec<String> {
    ken_pa_stack.ken_pa.lock().unwrap().clone()
}

fn main() {
    tauri::Builder::default()
        // State として管理してほしいインスタンスを登録する
        .manage(Counter {
            count: AtomicUsize::new(0),
        })
        .manage(KenPaStack {
            ken_pa: Arc::new(Mutex::new(Vec::new())),
        })
        .invoke_handler(tauri::generate_handler![
            inc_count,
            dec_count,
            put_ken,
            put_pa,
            get_ken_pa_stack
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

■ src/App.tsx:

rust で実装したコマンドを呼び出し、結果を表示する。

import { useState } from "react";
import "./App.css";
import { invoke } from "@tauri-apps/api";

function App() {

  const [count, setCount] = useState(0);

  const [kenPaStack, setKenPaStack] = useState<Array<string>>([]);

  async function incCount() {
    let count = await invoke<number>('inc_count', {});
    setCount(count);
  }

  async function decCount() {
    let count = await invoke<number>('dec_count', {});
    setCount(count);
  }

  async function putKen() {
    invoke<number>('put_ken', {});
  }

  async function putPa() {
    invoke<number>('put_pa', {});
  }

  async function getKenPaStack() {
    let kenPaStack = await invoke<Array<string>>('get_ken_pa_stack', {});
    setKenPaStack(kenPaStack);
  }

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <h2>Counter example:</h2>
      <div>
        <button onClick={decCount}>-</button>
        <input type="text" value={count} readOnly></input>
        <button onClick={incCount}>+</button>
      </div>

      <h2>KEN-PA example:</h2>
        <button onClick={putKen}>けん</button>
        <button onClick={putPa}>ぱ</button>
        <button onClick={getKenPaStack}>けんけんぱできた?</button>
        <div>
          <ul>
          {
            kenPaStack.map((e) => <li>{e}</li>)
          }
          </ul>
        </div>
    </div>
  );
}

export default App;

動作確認

カウンターのインクリメント・デクリメントができていることと、 けんぱスタックにけんとぱが積まれていくことが確認できる。

良さそう。

今回は例の用途が良くなく、あまり意味のないモノになってしまったが、 そもそもフロントエンドに渡さなくていい状態や、 ステートマシンの結果の一部のみをフロントエンドに渡すなどという使い方をするのかな? という感じです。

あと、 State の '_ が何なのか分からないのでご存じの型教えていただけると嬉しいです。

以上。

参考資料

2024年7月2日火曜日

Tauri で XDG Base Directory を取得する

この辺りに則っていると考えることが少なくなるので、利用していきましょう。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • delete-default-event をベースに作業をしたので、そのディレクトリとの差分を取ってください

実装

今回は、以下 3 つの箇所から取得するのを実際に試してみる。

  • 初期化時(setup 関数内)
  • イベント内
  • コマンド処理内

基本的には、 App か AppHandle から PathResolver を取得し、そこから各ディレクトリを取得する。

■ src-tauri/src/main.rs:

前述の 3 つのタイミングでディレクトリを取得して表示するように実装。

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::{api::path::*, AppHandle, Manager};

// コマンドでは、引数に自動挿入される AppHandle から pathresolver が取得できる。
#[tauri::command]
fn print_xdg_base_directories(app_handle: AppHandle) {
    println!("==== コマンド内で取得して表示開始 ====");

    // PathResolver の取得
    let path_resolver = app_handle.path_resolver();

    let resource_dir = path_resolver.resource_dir();
    println!("resource_dir: {:?}", resource_dir);

    let resolve_resource = path_resolver.resolve_resource("../assets/logo.svg");
    println!("resolve_resource: {:?}", resolve_resource);

    let app_config_dir = path_resolver.app_config_dir();
    println!("app_config_dir: {:?}", app_config_dir);

    let app_data_dir = path_resolver.app_data_dir();
    println!("app_data_dir: {:?}", app_data_dir);

    let app_local_data_dir = path_resolver.app_local_data_dir();
    println!("app_local_data_dir: {:?}", app_local_data_dir);

    let app_cache_dir = path_resolver.app_cache_dir();
    println!("app_cache_dir: {:?}", app_cache_dir);

    let app_log_dir = path_resolver.app_log_dir();
    println!("app_log_dir: {:?}", app_log_dir);

    println!("==== コマンド内で取得して表示終了 ====");
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            // setup 関数では、app から path_resolve を取得すれば OK.
            println!("==== setup 関数内で取得して表示開始 ====");

            // PathResolver の取得
            let path_resolver = app.path_resolver();

            let resource_dir = path_resolver.resource_dir();
            println!("resource_dir: {:?}", resource_dir);

            let resolve_resource = path_resolver.resolve_resource("../assets/logo.svg");
            println!("resolve_resource: {:?}", resolve_resource);

            let app_config_dir = path_resolver.app_config_dir();
            println!("app_config_dir: {:?}", app_config_dir);

            let app_data_dir = path_resolver.app_data_dir();
            println!("app_data_dir: {:?}", app_data_dir);

            let app_local_data_dir = path_resolver.app_local_data_dir();
            println!("app_local_data_dir: {:?}", app_local_data_dir);

            let app_cache_dir = path_resolver.app_cache_dir();
            println!("app_cache_dir: {:?}", app_cache_dir);

            let app_log_dir = path_resolver.app_log_dir();
            println!("app_log_dir: {:?}", app_log_dir);

            // Tauri2 で削除予定
            let app_dir = path_resolver.app_dir();
            println!("app_dir: {:?}", app_dir);

            // Tauri2 で削除予定
            let log_dir = path_resolver.log_dir();
            println!("log_dir: {:?}", log_dir);

            // `tauri::api::path::` での取得
            println!("audio_dir: {:?}", audio_dir());
            println!("cache_dir: {:?}", cache_dir());
            println!("config_dir: {:?}", config_dir());
            println!("data_dir: {:?}", data_dir());
            println!("desktop_dir: {:?}", desktop_dir());
            println!("document_dir: {:?}", document_dir());
            println!("download_dir: {:?}", download_dir());
            println!("executable_dir: {:?}", executable_dir());
            println!("font_dir: {:?}", font_dir());
            println!("home_dir: {:?}", home_dir());
            println!("local_data_dir: {:?}", local_data_dir());
            println!("picture_dir: {:?}", picture_dir());
            println!("public_dir: {:?}", public_dir());
            println!("runtime_dir: {:?}", runtime_dir());
            println!("template_dir: {:?}", template_dir());
            println!("video_dir: {:?}", video_dir());

            println!("==== setup 関数内で取得して表示終了 ====");

            {
                // イベントは、クロージャで path_resolve を受け渡せば取得できる
                let path_resolver = app.path_resolver().clone();
                let _ = app.listen_global("event", move |_| {
                    println!("==== イベント内で取得して表示開始 ====");

                    let resource_dir = path_resolver.resource_dir();
                    println!("resource_dir: {:?}", resource_dir);

                    let resolve_resource = path_resolver.resolve_resource("../assets/logo.svg");
                    println!("resolve_resource: {:?}", resolve_resource);

                    let app_config_dir = path_resolver.app_config_dir();
                    println!("app_config_dir: {:?}", app_config_dir);

                    let app_data_dir = path_resolver.app_data_dir();
                    println!("app_data_dir: {:?}", app_data_dir);

                    let app_local_data_dir = path_resolver.app_local_data_dir();
                    println!("app_local_data_dir: {:?}", app_local_data_dir);

                    let app_cache_dir = path_resolver.app_cache_dir();
                    println!("app_cache_dir: {:?}", app_cache_dir);

                    let app_log_dir = path_resolver.app_log_dir();
                    println!("app_log_dir: {:?}", app_log_dir);

                    // Tauri2 で削除予定
                    let app_dir = path_resolver.app_dir();
                    println!("app_dir: {:?}", app_dir);

                    // Tauri2 で削除予定
                    let log_dir = path_resolver.log_dir();
                    println!("log_dir: {:?}", log_dir);

                    println!("==== イベント内で取得して表示終了 ====");
                });
            }

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![print_xdg_base_directories])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

■ src/App.tsx:

単純にコマンド呼び出しとイベント送信をするだけ。

import { invoke } from "@tauri-apps/api";
import "./App.css";
import { emit } from "@tauri-apps/api/event";

function App() {

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <button onClick={() => {
        invoke('print_xdg_base_directories', {});
      }}>コマンド実行</button>
      <button onClick={() => {
        emit('event', {});
      }}>イベント送信</button>

    </div>
  );
}

export default App;

動作確認

起動時・コマンド実行時・イベント送信時、それぞれでディレクトリが取得できていることを確認できる。

...(snip)
==== コマンド内で取得して表示開始 ====
resource_dir: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug")
resolve_resource: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug/_up_/assets/logo.svg")
app_config_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event")
app_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_local_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_cache_dir: Some("/home/node/.cache/dev.mikoto2000.study.tauri.delete-default-event")
app_log_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event/logs")
==== コマンド内で取得して表示終了 ====
==== イベント内で取得して表示開始 ====
resource_dir: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug")
resolve_resource: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug/_up_/assets/logo.svg")
app_config_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event")
app_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_local_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_cache_dir: Some("/home/node/.cache/dev.mikoto2000.study.tauri.delete-default-event")
app_log_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event/logs")
app_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event")
log_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event/logs")
==== イベント内で取得して表示終了 ====

良さそう。

以上。

参考資料

以上。