9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.NETのCUIアプリケーション用フレームワーク『CuiLib』をメジャーアップデートしました

Posted at

以前制作したCUIアプリケーション開発用フレームワーク『CuiLib』を大規模改修し,リリースしました。
クラス設計を見直した部分が多々あり破壊的変更を多く伴うため,メジャーアップデートとしています。
当記事は,執筆時点での最新バージョン「2.0.1」について触れていきます。

リンク

目玉

対応する.NETバージョンを拡大

これまでは.NET 7.0のみでしたが,以下のランタイムにも対応するようになりました。
.NET Standardに対応したため,.NET Framework 4.6.2-4.8.1・.NET Core 2.0-3.1のプロジェクトから当フレームワークを利用できるようになっております。

  • .NET Standard 2.0
  • .NET Standard 2.1
  • .NET 6.0
  • .NET 8.0
  • .NET 9.0

複数ランタイム対応について工夫した点など,今後記事にしていけたらいいなと思っています。

テストを拡充し,不具合炙り出し&修正

以前はテストの書き方について詳しくなかったため,最低限の動作しか保証されていない状態でした。
今回メジャーアップデートするにあたって,単体テストを網羅的に実装しました。
これによって多数の不具合が検出されたため,まとめて修正しました。
具体的な修正内容については冒頭のリリースノートのリンクからご確認ください。

ヘルプメッセージの細かいカスタマイズが可能に

これまでは Command.WriteHelp(TextWriter) メソッドで対象のコマンドのヘルプメッセージを出力していました。
ただ,ヘルプメッセージのカスタマイズを行う際にはこのメソッドをオーバーライドして,一からヘルプメッセージを組み立て直す必要がありました。
当バージョンからは CuiLib.Output.IHelpMessageProvider インターフェイスと CuiLib.Output.HelpMessageProvider クラスが新たに実装されて, WriteHelp メソッドのオーバーロードに WriteHelp(TextWriter, IHelpMessageProvider) が追加されています。
HelpMessageProvider クラスは IHelpMessageProvider インターフェイスのデフォルトの実装で,継承することでヘッダー部分やオプション部分といった部分的なカスタマイズを行うことができます。

サンプルコード
二種類のヘルプメッセージを出力するC#サンプル
using System;
using System.IO;
using System.Linq;
using CuiLib.Checkers;
using CuiLib.Commands;
using CuiLib.Options;
using CuiLib.Output;
using CuiLib.Parameters;

internal class Program
{
    private static void Main(string[] args)
    {
        var command = new SampleCommand();

        // デフォルトのヘルプメッセージ
        command.WriteHelp(Console.Out, messageProvider: null);

        // カスタマイズしたヘルプメッセージ
        command.WriteHelp(Console.Out, new SampleHelpMessageProvider());
    }
}

internal class SampleHelpMessageProvider : HelpMessageProvider
{
    // オプション部分だけカスタマイズ
    public override void WriteOptions(TextWriter writer, Command command)
    {
        writer.WriteLine("Options:");
        foreach (NamedOption option in command.Options.OfType<NamedOption>())
        {
            writer.Write("  ");
            if (option.ShortName is not null)
            {
                writer.Write($"-{option.ShortName}");
                if (option.ValueTypeName is not null) writer.Write($" {option.ValueTypeName.ToUpper()}");
                writer.Write(", ");
            }

            writer.Write($"--{option.FullName}");
            if (option.ValueTypeName is not null) writer.Write($" {option.ValueTypeName.ToUpper()}");
            writer.WriteLine(':');

            writer.WriteLine($"  {option.Description}");
            writer.WriteLine();
        }
    }
}

internal class SampleCommand : Command
{
    private readonly FlagOption optionHelp;
    private readonly FlagOption optionVersion;
    private readonly MultipleValueOption<FileInfo> optionInput;
    private readonly SingleValueOption<int> optionNum;
    private readonly MultipleValueParameter<string> parameters;

    public SampleCommand() : base("sample")
    {
        Description = "Sample command";

        optionHelp = new FlagOption('h', "help")
        {
            Description = "Display help",
        };
        optionVersion = new FlagOption('v', "version")
        {
            Description = "Display version",
        };
        optionInput = new MultipleValueOption<FileInfo>('i', "in")
        {
            Description = "Input files",
            Checker = ValueChecker.ValidSourceFile(),
            Required = true,
        };
        optionNum = new SingleValueOption<int>("number")
        {
            Description = "Some number",
            Required = false,
        };
        parameters = new MultipleValueParameter<string>("params", 0)
        {
            Description = "Other parameters",
        };

        Options.Add(optionHelp);
        Options.Add(optionVersion);
        Options.Add(optionInput);
        Options.Add(optionNum);

        Parameters.Add(parameters);
    }
}
デフォルトの出力
sample

Description:
Sample command

Usage:
sample [-h] [-v] -i file [--number int] <params ..>

Options:
  -h, --help     Display help
  -v, --version  Display version
  -i, --in       Input files
      --number   Some number

Parameters:
  params  Other parameters
カスタマイズした出力
sample

Description:
Sample command

Usage:
sample [-h] [-v] -i file [--number int] <params ..>

Options:
  -h, --help:
  Display help

  -v, --version:
  Display version

  -i FILE, --in FILE:
  Input files

  --number INT:
  Some number


Parameters:
  params  Other parameters

数値の範囲を表す型 ValueRangeValueRangeCollection を実装

コマンドラインアプリケーションを開発する際に, 1-3,5 のような数値の範囲を解析したくなる場合が出てきます。
そこで,範囲を表す型として CuiLib.Data.ValueRange 構造体と CuiLib.Data.ValueRangeCollection クラスを実装しました。
ValueRange 構造体は範囲 1-3,5 のうち, 1-35 といった「連続した範囲」または「単一の値」を表します。
ValueRangeCollection クラスは ValueRange 構造体のコレクションを表します。
両型とも IParsable<T> インターフェイスを実装しており, Parse メソッドで文字列からの変換をサポートしています。
IValueConverter<TIn, IOut> による変換もサポートされており, SingleValueOption<T>SingleValueParameter<T> といった引数用のクラスの型引数に ValueRangeCollection を指定するだけで自動的に変換してくれます。
尚,どちらの型も IEnumerable<T> を実装しているため,LINQによる処理を行うことも可能です。

サンプルプログラム
using System;
using System.IO;
using CuiLib.Commands;
using CuiLib.Data;
using CuiLib.Options;

internal class Program
{
    private static void Main(string[] args)
    {
        using var writer = new StreamWriter("test.txt", false);
        var command = new SampleCommand();

        command.Invoke(["-r", "1-3,5,10-30,100"]);
    }
}

internal class SampleCommand : Command
{
    private readonly SingleValueOption<ValueRangeCollection> optionRange;

    public SampleCommand() : base("sample")
    {
        Description = "Sample command";

        optionRange = new SingleValueOption<ValueRangeCollection>('r', "range")
        {
            Description = "range of value",
            Required = true,
        };

        Options.Add(optionRange);
    }

    protected override void OnExecution()
    {
        ValueRangeCollection ranges = optionRange.Value;

        Console.WriteLine(ranges);
        foreach (ValueRange currentRange in ranges)
        {
            if (currentRange.Start == currentRange.End) Console.WriteLine($"  {currentRange}: represents single value '{currentRange}'");
            else Console.WriteLine($"  {currentRange}: represents the range {currentRange.Start} to {currentRange.End}");
        }
    }
}
出力
1-3,5,10-30,100
  1-3: represents the range 1 to 3
  5: represents single value '5'
  10-30: represents the range 10 to 30
  100: represents single value '100'

新バージョン移行の際の注意点

今回のアップデートに際して破壊的変更が多数行われているため,バージョン移行の手引きをこちらに示しておきます。

命名の修正

命名の大整理を行ったため,バージョンを変えた直後はコンパイルエラーが多発します。
以下の項を基に参照や名前を更新してください。

名前空間の変更

  • CuiLib.Log 名前空間以下の全型 → CuiLib.Logging
  • IValueChecker<T> インターフェイスと ValueChecker クラスについて: CuiLib.OptionsCuiLib.Checkers
  • IValueConverter<TIn, TOut> インターフェイスと ValueConverter クラスについて: CuiLib.OptionsCuiLib.Converters
  • Parameter クラスとその派生型, ParameterCollection クラスについて: CuiLib.OptionsCuiLib.Parameters

クラス名の変更

  • IOHelperIOHelpers

ValueChecker クラスのメソッド

  • AlwaysSuccess -> AlwaysValid
  • StartWith -> StartsWith
  • EndWith -> EndsWith
  • Contains -> ContainedIn
  • Larger -> GreaterThan
  • LargerOrEqual -> GreaterThanOrEqualTo
  • Lower -> LessThan
  • LowerOrEqual -> LessThanOrEqualTo
  • Equals -> EqualTo
  • NotEquals -> NotEqualTo
  • IsRegexMatch -> Matches
  • FileExists -> ExistsAsFile
  • DirectoryExists -> ExistsAsDirectory
  • VerifySourceFile -> ValidSourceFile
  • VerifyDestinationFile -> ValidDestinationFile
  • VerifySourceDirectory -> ValidSourceDirectory

Parameter<T> クラスの設計変更

オプションと取り回しを合わせるため, Parameter<T> クラスの設計を変更しました。
値を一つだけ取るオプションは SingleValueParameter<T> クラスに,複数の値をとるオプションは MultipleValueParameter<T> クラスに分割されています。
そして, Checker プロパティと Converter プロパティが Parameter<T> クラスから削除されて各派生クラスへ移行されています。
そのため Parameter<T>.CheckerParameter<T>.Converter を参照している場合は SingleValueParameter<T>MultipleValueParameter<T> へキャストします。
後者については加えて以下の修正を行います。

  • Values プロパティ → Value プロパティ
  • Parameter<T> として代入している場合 → Parameter<T[]> へ修正

最後に

改修を始めたから半年以上経過しましたが,やっとリリースすることができました。
GitHubのIssueがまだ残っているので,合間を縫ってぼちぼち実装をしていくつもりです。
不具合の報告や機能追加の要望などありましたら,Issueへ遠慮なくお知らせください。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?