Electron アプリのコマンドライン引数

2024/03/13に公開

Electron アプリでコマンドライン引数を取得するのが案外面倒だったのでまとめる。

なお、開発環境が macOS なので macOS での動作に限定する。

Electron アプリには下記の起動方法がある。

  • 開発時に使う electron .
  • パッケージ内のバイナリ /path/to/APP_NAME.app/Contents/MacOS/APP_NAME の実行
  • open -a APP_NAME による実行 (Finder からの起動も同様)

また、単純に起動した場合と app.requestSingleInstanceLock でシングルインスタンスを強制した場合に発生する second-instance イベントでの違いについても記述する。

electron コマンド

electron . foo bar のように指定する。

起動時

process.argv で取得する。

  • 第 1 引数は Electron のバイナリ $PWD/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
  • 第 2 引数は .
  • 第 3 引数以降が取得したいコマンドライン引数となる

second-instance 起動時

second-instance イベントのハンドラの第 2 引数で取得する。

  • 第 1 引数は Electron のバイナリ $PWD/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
  • 第 2 引数は --allow-file-access-from-files
  • 第 3 引数は --enable-avfoundation
  • 第 4 引数は .
  • 第 5 引数以降が取得したいコマンドライン引数となる

Electron のオプションが追加されているが、いかにも Electron のアップデート等で変更されそう。

バイナリの実行

/path/to/APP_NAME.app/Contents/MacOS/APP_NAME foo bar のように指定する

起動時

process.argv で取得する。

  • 第 1 引数はアプリのバイナリ /path/to/APP_NAME.app/Contents/MacOS/APP_NAME
  • 第 2 引数以降が取得したいコマンドライン引数となる

second-instance 起動時

second-instance イベントのハンドラの第 2 引数で取得する。

  • 第 1 引数はアプリのバイナリ /path/to/APP_NAME.app/Contents/MacOS/APP_NAME
  • 第 2 引数は --allow-file-access-from-files
  • 第 3 引数は --enable-avfoundation
  • 第 4 引数以降が取得したいコマンドライン引数となる

open コマンドによる実行

open -a /path/to/APP_NAME.app foo bar または open -a APP_NAME foo bar (/Applications 内にインストール済みの場合) のように指定する。

open コマンドによる実行時は、起動時の process.argv はアプリのバイナリのパスのみ、 second-instance は発生しない。

起動時は即座に (ready イベント発生前に) open-file イベントが発生、起動後の実行でも open-file イベントが発生する。

また、 open-file イベントで得られるファイルのパスは 1 つだけで、複数指定された場合は連続して open-file イベントが発生する。

なお、open コマンドの --args オプションで指定した引数は、 process.argv の第 2 引数以降に入るが、シングルインスタンス強制時、起動中に実行した場合はこれを得るすべはない (たぶん)。この記事では扱わない。

上記に対応したコード

ファイルが渡された場合の処理はレンダラープロセスで open-file-from-main メッセージのハンドラで行うこととする。

second-instance 時の Electron のオプションを無視するために commander ライブラリを利用する。

const { app } = require("electron/mainl")
const { program } = require ("commander")

// second-instance イベント時につくオプションを parse できるようにしておく
program
  .option("--allow-file-access-from-files")
  .option("--enable-avfoundation");

// 実行バイナリの違い / 起動時か second-instance かの違いを吸収してコマンドライン引数をとる
function parseArguments(args: string[]) {
  program.parse(args, {from: "user"})
  const binary = args[0];
  return path.basename(binary) === "Electron" ? program.args.slice(2) : program.args.slice(1)
}

async function createWindow() {
  const w = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  })

  await w.loadFile(path.join(__dirname, "index.html"))
  return w
}

let currentWindow: any = null;
let initialOpenCommandArguments: string[] = [];

// シングルインスタンスの強制
if (!app.requestSingleInstanceLock()) app.exit();

// open コマンドによる起動時のイベントを handle するためここでハンドラ登録
app.on("open-file", (event: Event, path: string) => {
  event.preventDefault();
  if (currentWindow) {
    // すでにウインドウがあればレンダラープロセスへファイルパスを送信
    currentWindow.webContents.send("open-file-from-main", [path]);
  } else {
    // ウインドウがなければウィンドウ作成時まで保持
    initialOpenCommandArguments.push(path);
  }
});

app.whenReady().then(() => {
  // 起動状態でバイナリ起動時
  app.on("second-instance", (_e: Event, argv: string[]) => {
    // レンダラープロセスへファイルパスを送信
    currentWindow.webContents.send("open-file-from-main", parseArguments(argv));
  });

  createWindow().then((w) => {
    // バイナリ起動時のコマンドライン引数を送信
    w.webContents.send("open-file-from-main", parseArguments(process.argv));

    // open コマンド起動時のコマンドライン引数を送信
    for (const file of initialOpenFiles) {
      w.webContents.send("open-file-from-main", [file]);
    }
    currentWindow = w;
  })
});

Discussion