📖

【PowerShell】Google 日本語入力の単語辞書を力業でクラウド同期する

2021/07/17に公開

Google 日本語入力でこのような感じの単語登録を無節操に繰り返した結果、気づけばユーザー辞書が1000件を超えていました。

在宅勤務も導入され、会社PCとの間で辞書同期のためにエクスポートとインポートを都度するのも手間なので、色々試行錯誤して PowerShell から無理やり辞書を同期させてみました。

やっていること

  • Google 日本語入力の設定ファイルをクラウド(今回の場合は Dropbox)にコピーする
  • ローカルにある本来の設定ファイルとの間でタイムスタンプを比較
    • もしローカルファイルのほうが新しければ、ローカルをクラウドにアップロードする
    • もしクラウドのファイルのほうが新しければ、クラウドをローカルにコピーする
  • 上記処理を PowerShell の prompt 関数に仕込んでコマンドを実行するたびにチェックする

実際のイメージ

前提確認

環境

> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.1.3
PSEdition                      Core
GitCommitId                    7.1.3
OS                             Microsoft Windows 10.0.19042
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Google 日本語入力

  • バージョン: 2.25.3700.0
  • 辞書等設定ファイルのディレクトリ: C:\Users\(ユーザー名)\AppData\LocalLow\Google\Google Japanese Input
    • 最初、このディレクトリをまるっとクラウドに置いて、シンボリックリンクなりジャンクションなりをローカルにおけば万事解決ではと思ったのですが、実際にやってみたところ「データを読み込めません」的なエラーを吐いてしまいました。無念。
    • 辞書と全般的な設定は config1.db user_dictionary.db に格納されている模様。

事前準備

クラウド側

辞書ファイルをアップロードするディレクトリを作っておきます(今回は (Dropbox のパス)\IME_google\db)。
次に、そのディレクトリ内に .history というサブディレクトリを用意します。詳細は後述。

クラス作成

$PROFILE に下記のクラスを書き込んでおきます。

Class GoogleIME {

    static [PSCustomObject[]] Check () {
        return (@("config1.db", "user_dictionary.db") | ForEach-Object {
            $local = "C:\Users\{0}\AppData\LocalLow\Google\Google Japanese Input" -f $env:USERNAME | Join-Path -ChildPath $_ | Get-Item
            $cloud = "C:\Users\{0}\Dropbox\IME_google\db" -f $env:USERNAME | Join-Path -ChildPath $_ | Get-Item
            $require = "NOTHING"
            if ($local.LastWriteTime -gt $cloud.LastWriteTime) {
                $require = "UPLOAD"
            }
            elseif ($local.LastWriteTime -lt $cloud.LastWriteTime) {
                $require = "DOWNLOAD"
            }
            return [PSCustomObject]@{
                "Name" = $_;
                "Require" = $require;
            }
        })
    }

    static [void] Backup ([System.IO.FileInfo]$dbFile) {
        $ts = Get-Date -Format "yyyyMMddHHmmssff"
        $backupDir = $dbFile.Directory.FullName | Join-Path -ChildPath $(".history\{0}_{1}.db" -f $dbFile.Basename, $ts)
        $dbFile | Copy-Item -Destination $backupDir
    }

    static [void] Refresh () {
        Get-Process | Where-Object ProcessName -In @("GoogleIMEJaConverter", "GoogleIMEJaRenderer") | ForEach-Object {
            $path = $_.Path
            Stop-Process $_
            Start-Process $path
        }
    }

    static [void] Sync ([string]$mode, [string]$name) {
        $local = "C:\Users\{0}\AppData\LocalLow\Google\Google Japanese Input" -f $env:USERNAME | Join-Path -ChildPath $name | Get-Item
        $cloud = "C:\Users\{0}\Dropbox\IME_google\db" -f $env:USERNAME | Join-Path -ChildPath $name | Get-Item
        $origin, $dest = switch ($mode) {
            "UPLOAD" {
                @($local, $cloud); break;
            }
            "DOWNLOAD" {
                @($cloud, $local); break;
            }
        }

        if ($origin.LastWriteTime -gt $dest.LastWriteTime) {
            if ($mode -eq "UPLOAD") {
                [GoogleIME]::Backup($dest)
            }
            $origin | Copy-Item -Destination $dest
            if ($mode -eq "DOWNLOAD") {
                [GoogleIme]::Refresh()
            }

        }

    }

}

別にクラスの静的メソッドの代わりに関数でも良いのですが、メソッドにしておいたほうが目的(クラス名)と処理(メソッド名)が区別しやすくて好きです。

個人的には、

  • パイプラインの途中でフィルタ的に使うもの →関数
  • 単一処理 →静的メソッド

という感じで使い分けたりしています。

以下、各メソッドの解説。

[GoogleIME]::Check()

static [PSCustomObject[]] Check () {
    return (@("config1.db", "user_dictionary.db") | ForEach-Object {
        $local = "C:\Users\{0}\AppData\LocalLow\Google\Google Japanese Input" -f $env:USERNAME | Join-Path -ChildPath $_ | Get-Item
        $cloud = "C:\Users\{0}\Dropbox\IME_google\db" -f $env:USERNAME | Join-Path -ChildPath $_ | Get-Item
        $require = "NOTHING"
        if ($local.LastWriteTime -gt $cloud.LastWriteTime) {
            $require = "UPLOAD"
        }
        elseif ($local.LastWriteTime -lt $cloud.LastWriteTime) {
            $require = "DOWNLOAD"
        }
        return [PSCustomObject]@{
            "Name" = $_;
            "Require" = $require;
        }
    })
}

config1.dbuser_dictionary.db それぞれについてクラウドのコピーとタイムスタンプを比較して、必要なのが UPLOADDOWNLOAD かを Require の値として返します。

最新の状態に同期されていれば Require の値は NOTHING を返すようにしました。

[GoogleIME]::Backup()

static [void] Backup ([System.IO.FileInfo]$dbFile) {
    $ts = Get-Date -Format "yyyyMMddHHmmssff"
    $backupDir = $dbFile.Directory.FullName | Join-Path -ChildPath $(".history\{0}_{1}.db" -f $dbFile.Basename, $ts)
    $dbFile | Copy-Item -Destination $backupDir
}

更新されたローカルファイルをクラウドにアップロードする仕様は、PCを新調したときや Google 日本語入力をインストールし直したときに問題になります。その場合、ローカルのまっさらな辞書データで、これまで育ててきたクラウドの辞書データを上書きして消し飛ばしてしまうので、その対策としてアップロード前に .history へ直前の内容をコピーするようにしています。

[GoogleIME]::Refresh()

static [void] Refresh () {
    Get-Process | Where-Object ProcessName -In @("GoogleIMEJaConverter", "GoogleIMEJaRenderer") | ForEach-Object {
        $path = $_.Path
        Stop-Process $_
        Start-Process $path
    }
}

クラウドから最新の設定ファイルを読み込んだ場合も、すぐにはIMEに反映されません。
GoogleIMEJaConverterGoogleIMEJaRenderer のプロセスを再起動して初めて設定ファイルの内容が適用されるようです。この仕様を発見するまでは辞書を同期するたびに PC を再起動していました。

[GoogleIME]::Sync()


static [void] Sync ([string]$mode, [string]$name) {
    $local = "C:\Users\{0}\AppData\LocalLow\Google\Google Japanese Input" -f $env:USERNAME | Join-Path -ChildPath $name | Get-Item
    $cloud = "C:\Users\{0}\Dropbox\IME_google\db" -f $env:USERNAME | Join-Path -ChildPath $name | Get-Item
    $origin, $dest = switch ($mode) {
        "UPLOAD" {
            @($local, $cloud); break;
        }
        "DOWNLOAD" {
            @($cloud, $local); break;
        }
    }

    if ($origin.LastWriteTime -gt $dest.LastWriteTime) {
        if ($mode -eq "UPLOAD") {
            [GoogleIME]::Backup($dest)
        }
        $origin | Copy-Item -Destination $dest
        if ($mode -eq "DOWNLOAD") {
            [GoogleIme]::Refresh()
        }

    }

    }

これまでの内容を組み合わせて、実際にファイルのコピー操作をするメソッドです。コピー対象のファイル名と操作( UPLOADDOWNLOAD )を指定します。

Prompt に組み込む

これまで書いてきたメソッドを Prompt に組み込みます。
最近ハマっている ANSI エスケープシークエンスで文字に色を付けてみました。PowerShell の Write-Host-ForegroundColor を指定しても同じです。

function prompt {

    [GoogleIME]::Check() | Where-Object {$_.Require -ne "NOTHING"} | ForEach-Object {
        $ansiIdx = @{
            "UPLOAD" = 10 <# Green #>;
            "DOWNLOAD" = 6 <# Blue #>;
        }[$_.Require]
        "[Google IME] `e[38;5;{0}m{1}`e[0m is required on '{2}'!`n==> Execute Sync?" -f $ansiIdx, $_.Require, $_.Name | Write-Host -NoNewline
        if ((Read-Host -Prompt "(y/n)") -eq "y") {
            [GoogleIME]::Sync($_.Require, $_.Name)
            "`e[38;5;{0}m{1} completed: '{2}'`e[0m" -f $ansiIdx, $_.Require, $_.Name | Write-Host
        }
        else {
            "skipped {0} of '{1}'." -f $_.Require, $_.Name | Write-Host -ForegroundColor Magenta
        }

    }

    return "#> "
}

単語登録 Tips

敬語の語幹を単語登録しておくと便利です。「あmす」みたいなタイポもあらかじめ登録しておくと高速化できますね。

丸数字を登録しておくのも便利です。

Google 日本語入力にはコマンドラインオプションがあり、辞書ツールや単語登録ツールを直接呼び出すことができます。
本体のパスは C:\Program Files (x86)\Google\Google Japanese Input\GoogleIMEJaTool.exe で、オプションは下記の通り。

  • 単語登録モードで起動 --mode=word_register_dialog
  • 辞書ツールを起動 --mode=dictionary_tool

個人的には keyhac を使ってホットキーを割り当てるようにしたらスムーズでした(そのせいで1000個も辞書登録してしまったわけですが…)。

Discussion