起動時のコマンドライン引数を取得する

CLI プラグインの getMatches()(権限 cli:default)と Rust の std::env::args_os() で起動引数を読む。tauri.conf.json での定義、dev での渡し方、二重起動時の引数も扱う。

システム情報 対象: Tauri 2.x 更新日: 読了目安: 約10分 sys-013
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 2. バックエンドから実装する (Rust)
  4. 動作確認
  5. よくあるエラーと対処法
  6. OS ごとの違いと注意点
  7. 関連レシピ

myapp --verbose で詳しいログを出す、myapp memo.txt や「プログラムから開く」で渡されたファイルを開く、といった動きには起動時の引数が要ります。取り方は 2 つです。CLI プラグインは tauri.conf.json に定義した引数を解析して JS の getMatches() と Rust の app.cli().matches() に渡し、--help も自動で用意します。Rust 標準の std::env::args_os() は、定義なしで生の引数を読めます。使い分けと、開発中の渡し方、二重起動時の引数の扱いも説明します。

前提条件

npm run tauri add cli

CLI プラグインはデスクトップ専用です。tauri add cli はクレートをデスクトップ向けだけに追加し、権限 cli:default(getMatches() を許可する cli:allow-cli-matches を含む)を capabilities/desktop.json に書き込みます。core:default には含まれないので、手で書く場合は次の形にします。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "desktop-capability",
  "platforms": ["macOS", "windows", "linux"],
  "windows": ["main"],
  "permissions": [
    "cli:default"
  ]
}

受け付ける引数は tauri.conf.json の plugins.cli に定義します。tauri add はここまでは書かないので、自分で足します。

{
  "plugins": {
    "cli": {
      "description": "My App",
      "args": [
        { "name": "verbose", "short": "v", "description": "詳しいログを出す" },
        { "name": "lang", "takesValue": true, "possibleValues": ["ja", "en"], "description": "表示言語" },
        { "name": "file", "index": 1, "takesValue": true, "description": "開くファイル" }
      ]
    }
  }
}

index の無い引数は --lang ja のように名前で、index を付けた引数は位置で受け取ります。-h / --help と -V / --version は自動で追加されるので、自分の引数の short には使えません。

2 つの方法は次のように使い分けます。

やりたいこと使うもの
--verbose や --lang ja のようなオプションを受けるCLI プラグイン
ファイルの関連付けなどで渡されたパスを受け取るだけstd::env::args_os()
知らない引数が来てもエラーにしないstd::env::args_os()
起動中にもう一度起動されたときの引数Single Instance プラグインのコールバック

1. フロントエンドから実装する (TypeScript)

getMatches() は起動時の引数を定義に沿って解析した結果を返します。定義した引数は渡されなくてもキーがあり、フラグは false、値を取る引数は null です。value の型は string | boolean | string[] | null なので、型を確かめてから使います。

import { getMatches } from '@tauri-apps/plugin-cli';

export type LaunchOptions = { verbose: boolean; lang: string | null; file: string | null };

export async function readLaunchOptions(): Promise<LaunchOptions> {
  const { args } = await getMatches(); // cli:default が必要
  const str = (v: unknown) => (typeof v === 'string' ? v : null);
  return {
    verbose: args.verbose?.value === true,
    lang: str(args.lang?.value), // "ja" / "en" 以外は解析の段階でエラーになる
    file: str(args.file?.value),
  };
}

--help を付けて起動すると、定義した引数の代わりに args.help.value にヘルプの文面が入ります。アプリは普段どおり起動するので、ページなどに自分で表示します。

import { getMatches } from '@tauri-apps/plugin-cli';

const { args } = await getMatches();
const help = args.help?.value;
if (typeof help === 'string') {
  const pre = document.createElement('pre');
  pre.textContent = help; // 使い方と引数の一覧
  document.body.replaceChildren(pre);
}

2. バックエンドから実装する (Rust)

Rust では tauri_plugin_cli::CliExt を use すると、app.cli().matches() で同じ結果を受け取れます。ウィンドウの表示前に決めたい設定(ログの詳しさなど)は、setup で読みます(アプリ起動・終了時の処理を書く)。

生の引数を見るだけなら std::env::args_os() で足ります。先頭は実行ファイル自身のパスなので飛ばします。std::env::args() は Unicode として不正な引数が 1 つでもあるとパニックするので、ファイルパスを扱うなら args_os() を使います。

getMatches() と matches() が解析するのは、今のプロセスの引数だけです。起動中にもう一度起動されたとき(エクスプローラーで別のファイルを開いたときなど)の引数は Single Instance プラグイン(npm run tauri add single-instance)のコールバックに届くので、matches_from() で同じ定義に沿って解析してページへ送ります。このプラグインは最初に登録し、相対パスはコールバックの cwd(2 回目の起動時の作業フォルダー)を基準に解決します。

use std::path::PathBuf;

use serde::Serialize;
use tauri::{Emitter, Manager};
use tauri_plugin_cli::{CliExt, Matches};

#[derive(Clone, Serialize)]
struct SecondLaunch {
    matches: Matches,
    cwd: String,
}

/// 生の引数から、存在するファイルのパスを 1 つ探す(CLI プラグインを使わない方法)
#[tauri::command]
fn file_from_args() -> Option<String> {
    std::env::args_os()
        .skip(1) // 先頭は実行ファイル自身のパス
        .map(PathBuf::from)
        .find(|p| p.is_file())
        .map(|p| p.to_string_lossy().into_owned())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        // 起動中にもう一度起動されると、その引数がここに届く(最初に登録する)
        .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
            if let Ok(matches) = app.cli().matches_from(argv) {
                let _ = app.emit("second-launch", SecondLaunch { matches, cwd });
            }
            if let Some(window) = app.get_webview_window("main") {
                let _ = window.set_focus();
            }
        }))
        .plugin(tauri_plugin_cli::init())
        .setup(|app| {
            match app.cli().matches() {
                Ok(matches) => {
                    let verbose = matches
                        .args
                        .get("verbose")
                        .and_then(|a| a.value.as_bool())
                        .unwrap_or(false);
                    if verbose {
                        println!("verbose モードで起動しました");
                    }
                }
                // 定義に無い引数などで解析に失敗しても、アプリ自体は起動させる
                Err(e) => eprintln!("{e}"),
            }
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![file_from_args])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import type { CliMatches } from '@tauri-apps/plugin-cli';

// 起動時に渡されたファイル(CLI プラグインの定義に関係なく探す)
const path = await invoke<string | null>('file_from_args');
if (path) console.log('起動時のファイル:', path);

// 起動中にもう一度起動されたとき
await listen<{ matches: CliMatches; cwd: string }>('second-launch', ({ payload }) => {
  const file = payload.matches.args.file?.value;
  if (typeof file === 'string') console.log('2 回目の起動のファイル:', file, '作業フォルダー:', payload.cwd);
});

動作確認

開発中は tauri dev の後ろに -- を 2 回書き、2 回目の後ろに書いたものがアプリに渡ります(1 回目との間は Rust のビルドへの引数)。npm run 経由では npm が最初の -- を 1 つ消費するので、3 回書きます。

npm run tauri dev -- -- -- --verbose --lang ja ./memo.txt

ターミナルに「verbose モードで起動しました」と出て、readLaunchOptions() は { verbose: true, lang: "ja", file: "./memo.txt" } を返します。--lang fr にすると「failed to parse arguments:」で始まるエラーになり、--help にすると 2 つ目のコードでページにヘルプが表示されます。tauri dev で動くアプリの作業フォルダーは src-tauri なので、相対パスの ./memo.txt は src-tauri/memo.txt を指します。

よくあるエラーと対処法

  • 起動直後に「Error deserializing 'plugins.cli' within your Tauri configuration」を含むエラーで止まる: tauri.conf.json に plugins.cli がありません。引数を定義しない場合も "plugins": { "cli": {} } は必要です。
  • 「failed to parse arguments:」で始まるエラー: 定義に無い引数(unexpected argument '--foo' found など)や、possibleValues に無い値が渡されました。ファイルの関連付けで起動されるアプリは、index 付きの引数を定義しておかないと、ファイルのパスも「定義に無い引数」になります。
  • 「Short option names must be unique for each argument」を含むパニック: short に h や V を使い、自動で追加される --help / --version とぶつかっています。開発ビルドで引数を解析したときに起きます。
  • 「cli.cli_matches not allowed. Permissions associated with this command: cli:allow-cli-matches, cli:default」: どの capability にも cli:default がありません。desktop.json を確かめます。サブウィンドウからだけ「cli.cli_matches not allowed on window」で始まるエラーになる場合は、そのラベルを windows に足します。リリースビルドではどちらも「Command plugin:cli|cli_matches not allowed by ACL」になります。

OS ごとの違いと注意点

  • Windows: リリースビルドはコンソールを持たない設定(main.rs の windows_subsystem = "windows")なので、ターミナルから起動しても println! の出力は表示されません。ヘルプやエラーはページかダイアログに出します。
  • macOS: ファイルの関連付けで開いたパスは、Windows と Linux では引数で届きますが、macOS では Rust の RunEvent::Opened で届きます。.app に引数を渡して試すなら、open -a MyApp --args --verbose か MyApp.app/Contents/MacOS/MyApp --verbose で起動します。
  • iOS / Android: CLI プラグインは使えません。
  • 共通: 引数はタスクマネージャーや ps で他のプロセスからも見えるので、パスワードやトークンは渡しません。起動時の設定は 環境変数を取得・設定する の方法でも渡せます。受け取った --lang を表示言語に反映する方法は システムの言語設定(ロケール)を取得する を参照してください。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する