Scoop のユーザ定義サブコマンドを PowerShell で実装する方法

本記事は PowerShell Advent Calendar 2019 の 6 日目です。

以前投稿した[全 Scoop コマンド解説 その３ ～使用頻度（低）～](https://qiita.com/nimzo6689/items/53c63439f56ccd2d38c1)の alias の解説中に書いたのですが、
Scoop では alias の仕組みを利用することでユーザが自由に Scoop のサブコマンドを追加することができます。[^1]
[^1]: 実際は公式ドキュメントで記載されているはけではないので、非公式な手法となります。

今回はその手法の紹介と実際に一つ Scoop のサブコマンドを作ってみたいと思います。

# Scoop の alias の仕組み

Scoop で alias を作成した場合、内部では一つの alias につき一つの ps1 ファイルが作成されています。
実際に `upgrade` エイリアスを作成した際の挙動がこちらです。

```powershell
# 全ての Scoop アプリを最新版に更新するための upgrade エイリアスを作成
scoop alias add upgrade 'scoop update *' 'Update all apps.'

# すると、 upgrade エイリアスの処理内容を格納した ps1 ファイルが作成されます。
ls ~\scoop\shims\scoop-upgrade.ps1
#=>Mode                LastWriteTime         Length Name
#=>----                -------------         ------ ----
#=>-a----       2018/12/08     16:33             76 scoop-upgrade.ps1

# 中身を確認すると冒頭に alias の説明文があり、その後 alias のスクリプトが記載されているのがわかります。
cat ~\scoop\shims\scoop-remove.ps1
#=># Summary: Update all apps.
#=>scoop update *
```

このように alias の実態は `~\scoop\shims\scoop-${alias_name}.ps1` に過ぎないわけですが、
こちらのスクリプトを編集して作成したいサブコマンドの実装さえすれば、
自由に Scoop 関連の処理を追加することができるという訳です。

# お試し実装

では、どういった手はずでサブコマンドが作成できるのか、ためしに実装してみます。

## 主な要件

Scoop ではインストールに使用したキャッシュや過去に使用していたバージョンのファイル群を
何もしないとずっとディスク上に残すようになっています。
これにより、再インストールしたい場合に再ダウンロードが不要になったり、
以前のバージョンにロールバックしたい場合、 `scoop reset <app>@<pre_version>` を使ってすぐに戻すことができます。

ただ、直前よりも古いバージョンを使用することはほぼないので、
それらのバージョンに関するファイルを一括削除するコマンドを今回作成してみることにしました。

例えば、 git 関連のファイルが以下のようになっていた場合、
現行（2.24.1）とその一つ前のバージョン（2.24.0）に関連するファイルは削除せず、
それよりも古いバージョン（2.23.0）を削除する、という具合です。

```java:削除前
~\scoop
  |-apps
  |  |-git
  |      |-2.23.0.windows.2 // ココを削除
  |      |-2.24.0.windows.2
  |      |-2.24.1.windows.2
  |      |-current -> 2.24.1.windows.2
  |-cache
     |-git#2.23.0.windows.2# // ココを削除
     |-git#2.24.0.windows.2#
     |-git#2.24.1.windows.2#
```

## 実装

では、要件通りに実装始めていきます。
まず、 alias サブコマンドで ps1 ファイルを作成します。

```powershell
# 作成したいサブコマンド名で alias を作成する。
scoop alias add tidy '#Todo'

# Visual Studio Code で、上のコマンドによって作成された ps1 ファイルを開く
code $env:USERPROFILE\scoop\shims\scoop-tidy.ps1
```

次に、上記のファイルを以下のように修正します。

```powershell:scoop-tidy.ps1
# Usage: scoop tidy <app> [options]
# Summary: Cleanup apps and caches by removing old versions except for last latest one.
# Help: 'scoop tidy' cleans Scoop apps and caches by removing old versions except for last latest one.
# 'scoop tidy <app>' cleans up the old versions of that app if said versions exist.
#
# You can use '*' in place of <app> to cleanup all apps and caches.
#
# Options:
#   -s, --skip <int>   Skip count for removing old versions. (default: 1)
#   -g, --global       Cleanup a globally installed app

. "$psscriptroot\..\apps\scoop\current\lib\core.ps1"
. "$psscriptroot\..\apps\scoop\current\lib\manifest.ps1"
. "$psscriptroot\..\apps\scoop\current\lib\buckets.ps1"
. "$psscriptroot\..\apps\scoop\current\lib\versions.ps1"
. "$psscriptroot\..\apps\scoop\current\lib\getopt.ps1"
. "$psscriptroot\..\apps\scoop\current\lib\help.ps1"
. "$psscriptroot\..\apps\scoop\current\lib\install.ps1"

reset_aliases

$opt, $apps, $err = getopt $args 's:g' @('skip=', 'global')
if ($err) { "scoop tidy: $err"; exit 1 }

$global = $opt.g -or $opt.global
$skip = $opt.s + $opt.skip
if (!$skip) { $skip = 1 }
$cache = $true

if (!$apps) { 'ERROR: <app> missing'; my_usage; exit 1 }

# ----------------------------------------------------
# | The following lines are copy-paste code from libexec\scoop-cleanup.ps1.
# ----------------------------------------------------

function tidy($app, $global, $verbose, $cache, $skip) { # Change from original code.
    $current_version = current_version $app $global
    # Remove-Item "$cachedir\$app#*" -Exclude "$app#$current_version#*"

    $versions = versions $app $global | Where-Object { $_ -ne $current_version -and $_ -ne 'current' }

    # -- Change from original code. ---------------------
    $versions = $versions | Select-Object -SkipLast $skip
    $versions | ForEach-Object {
        Remove-Item "$cachedir\$app#$_#*" -Force
    }
    # ---------------------------------------------------

    if (!$versions) {
        if ($verbose) { success "$app is already clean" }
        return
    }

    Write-Host -f yellow "Removing $app`:" -nonewline
    $versions | ForEach-Object {
        $version = $_
        Write-Host " $version" -nonewline
        $dir = versiondir $app $version $global
        # unlink all potential old link before doing recursive Remove-Item
        unlink_persist_data $dir
        Remove-Item $dir -ErrorAction Stop -Recurse -Force
    }
    Write-Host ''
}

if ($apps) {
    $verbose = $true
    if ($apps -eq '*') {
        $verbose = $false
        $apps = applist (installed_apps $false) $false
        if ($global) {
            $apps += applist (installed_apps $true) $true
        }
    } else {
        $apps = Confirm-InstallationStatus $apps -Global:$global
    }

    # $apps is now a list of ($app, $global) tuples
    $apps | ForEach-Object { tidy @_ $verbose $cache $skip } # Change from original code.

    if ($cache) {
        Remove-Item "$cachedir\*.download" -ErrorAction Ignore
    }

    if (!$verbose) {
        success 'Everything is shiny now!'
    }
}

exit 0
```

ここまででサブコマンドの実装は完了です。

全体的にほとんど `libexec\scoop-cleanup.ps1` からのコピペコードとなります。
Scoop コマンドは全て `libexec` ディレクトリ配下にその実装があるので、
ユーザ定義サブコマンドを作成する際は参考にしてみてください。

## 動作確認

最後に、不要ファイルを作成した tidy サブコマンドで実際に削除してみます。

```powershell
# ヘルプを表示
scoop help tidy
#=>Usage: scoop tidy <app> [options]
#=>
#=>'scoop tidy' cleans Scoop apps and caches by removing old versions except for last latest one.
#=>'scoop tidy <app>' cleans up the old versions of that app if said versions exist.
#=>
#=>You can use '*' in place of <app> to cleanup all apps and caches.
#=>
#=>Options:
#=>  -s, --skip <int>   Skip count for removing old versions. (default: 1)
#=>  -g, --global       Cleanup a globally installed app

# git 関連のファイルに現行とその一つ前のバージョン以外のファイルがあることを確認。
scoop cache show git
#=>41.5 MB git (2.23.0.windows.2)       ...
#=>41.8 MB git (2.24.0.windows.2)       ...
#=>41.9 MB git (2.24.1.windows.2)       ...
#=>
#=>Total: 3 files, 125.2 MB

scoop info git
#=>...
#=>Installed:
#=>  C:\Users\nimzo6689\scoop\apps\git\2.23.0.windows.2
#=>  C:\Users\nimzo6689\scoop\apps\git\2.24.0.windows.2
#=>  C:\Users\nimzo6689\scoop\apps\git\2.24.1.windows.2
#=>...

# 削除を実行。
scoop tidy git
#=>Removing git: 2.23.0.windows.2

# 現行とその一つ前のバージョン以外が削除されていることを確認。
scoop cache show git
#=>41.8 MB git (2.24.0.windows.2)       ...
#=>41.9 MB git (2.24.1.windows.2)       ...
#=>
#=>Total: 2 files, 83.7 MB

scoop info git
#=>...
#=>Installed:
#=>  C:\Users\nimzo6689\scoop\apps\git\2.24.0.windows.2
#=>  C:\Users\nimzo6689\scoop\apps\git\2.24.1.windows.2
#=>...

scoop tidy *
#=>Everything is shiny now!
```

問題なく動きました。