自作の.NET製CLIツールにタブ補完機能を付ける（その２）～入力候補を動的に取得する

# タブ補完の入力候補を動的に取得する
この記事は、以前の記事の続きとなります。

https://qiita.com/pierusan2010/items/0885a78d5616601d013a

前回は、.NETのコンソールアプリケーションにタブ補完機能を追加するサンプルを紹介しました。

今回は、そのサンプルアプリを修正し、Web APIなどから動的に取得した値をコマンドパラメータの入力候補として利用できるようにします。

## ソースコード
ソースコードは以下のリンクからご覧いただけます。

https://github.com/pierre3/MyCLI

## Googleサジェストから入力候補を動的に取得する
今回のサンプルでは、以下のようなコマンドを作成します。

- Google検索を行うコマンドを作ります
- `--query`パラメータに検索ワードを指定して検索します
- 検索ワードには、入力した文字からGoogleのサジェストAPIを使って予測候補を提示します

![google-command.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/da55ceb5-745b-469a-01ed-a3a3cacbf6bb.gif)

`–query`オプションの後に検索キーワードを入力します。その後、[Tab]キーまたは[Ctrl]+[Space]キーを押すと、コード補完が作動します。これにより、入力したキーワードに関連する候補が表示されます。

### Googleコマンド
まず、Google検索を行うコマンドを実装します。指定した検索クエリを付けたGoogle検索のURLでブラウザを開くだけのシンプルなコマンドです。

```csharp:Commends\Google.cs
partial class MyCommands
{
    /// <summary>
    /// 指定したクエリでGoogle検索ページを開きます
    /// </summary>
    /// <param name="query">検索クエリ</param>
    [Command("google")]
    public void GoogleSearch(string query)
    {
        //既定のブラウザURLを開く
        var process = new Process()
        {
            StartInfo = new ProcessStartInfo()
            {
                FileName = "pwsh",
                Arguments = $"-Command start https://www.google.com/search?q={System.Web.HttpUtility.UrlEncode(query)}"
            }
        };
        process.Start();
    }
}
```

### ICommandCompletionItemの実装
次に、Googleコマンド用の補完候補を返す`ICommandCompletionItem`を実装したクラスを作成します。

```csharp:Commends\Google.cs
/// <summary>
/// Googleコマンド用 ICommandCompletionItem の実装
/// </summary>
class GoogleCompletionItem : ICommandCompletionItem
{
    private HttpClient _httpClient;

    private static readonly IEnumerable<string> options = ["--query"];

    public GoogleCompletionItem(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public string CommandName => "google";

    public async Task<IEnumerable<string>> GetCompletionItemsAsync(string optionName, string wordToComplete)
    {
        if (optionName != "--query" || wordToComplete=="") { return []; }

        var response = await _httpClient.GetAsync($"https://google.com/complete/search?q={System.Web.HttpUtility.UrlEncode(wordToComplete)}&output=toolbar");
        using var stream = await response.Content.ReadAsStreamAsync();
        var xml = XDocument.Load(stream);
        return xml.Descendants("suggestion")
            .Select(element => $"\"{(element.Attribute("data")?.Value ?? "")}\"");
    }

    public IEnumerable<string> GetAllOptions() => options;

    public IEnumerable<string> GetOptions(string wordToComplete) 
        => options.Where(o => o.Contains(wordToComplete, StringComparison.InvariantCultureIgnoreCase));
}
```

### HttpClientの利用　
Googleサジェストへのリクエストには`HttpClient`を使用します。`HttpClient`のインスタンスはコンストラクタで受け取ります。

### メソッドの非同期化
`GetCompletionItems`メソッドは、非同期メソッド`GetCompletionItemsAsync`に変更されています。これはメソッドの中で`HttpClient`の非同期メソッドを利用するためです。この非同期化の修正は`MyCommands`クラスの`Complete`メソッドまで伝播します。

```csharp
interface ICommandCompletionItem
{
    //...(省略)
    //非同期対応
    Task<IEnumerable<string>> GetCompletionItemsAsync(string optionName, string wordToComplete);
}

```
```csharp
interface ICommandCompletionProvider
{
    void Add(ICommandCompletionItem item);
    //非同期対応
    Task<IEnumerable<string>> CompleteAsync(string wordToComplete, string input, int cursorPosition);
}
```
```csharp
partial class MyCommands
{
    //非同期対応
    public async Task Complete(string wordToComplete, string input, int cursorPosition)
    {
        var items = await CompletionProvider
            .CompleteAsync(wordToComplete, input, cursorPosition);
        foreach (var item in items)
        {
            Console.WriteLine(item);
        }
    }
}
```

### `HttpClient`をDIサービスに登録
使用する`HttpClient`のインスタンスはDIを利用して`IHttpClientFactory`経由で取得します。
`AddHttpClient()`メソッドを利用するにはパッケージ`Microsoft.Extensions.Http`を追加する必要があります。

https://learn.microsoft.com/ja-jp/dotnet/core/extensions/httpclient-factory


```csharp:Program.cs
using ConsoleAppFramework;
using Microsoft.Extensions.DependencyInjection;

//HttpClientをサービスに登録
var services = new ServiceCollection()
    .AddHttpClient();
using var serviceProvider = services.BuildServiceProvider();
ConsoleApp.ServiceProvider = serviceProvider;

var app = ConsoleApp.Create();
app.Add<MyCommands>();
await app.RunAsync(args);
```

### CommandCompletionProviderに登録
最後に、`MyCommands`のコンストラクタで`CompletionProvider`に`GoogleCompletionItem`を登録します。
`GoogleCompletionItem`を生成する際、コンストラクタに`HttpClient`を渡します。

```csharp:Program.cs
partial class MyCommands
{
    private readonly CommandCompletionProvider CompletionProvider;

    public MyCommands(HttpClient httpClient)//DIでHttpClientを注入
    {
        CompletionProvider =
            [
                //Google用CompletionItemの登録
                new GoogleCompletionItem(httpClient),
                new CommandCompletionItem("search")
                {
                    {"--category", ["books","movies","music"]},
                    {"--sort", ["relevance","date","popularity"]},
                    {"--filter", ["free","paid","all"]}
                },
                //...(省略)
            ];
    }
}
```

# まとめ
今回の修正により、タブ補完の入力候補を動的に取得できるようになりました。この仕組みを利用すれば、より柔軟に入力候補を提供することが可能になります。

これにより、CLIツールの使いやすさと効率性が向上し、より多様なシチュエーションでの使用が可能になると思います。