Sukhpinder Singh

数週間に一度、20行程度の小さなプログラムが必要になることがあります。ビルドエージェントのディスクを圧迫している原因を探したり、CSVの重複を排除したり、フォルダのハッシュを検証したりするような作業です。15年間、「どの言語を使うか?」と聞かれたときの正直な答えはC#ではないでした。mkdirdotnet new console、また別の使い捨てcsprojに名前を付ける作業を終えた頃には、すでにその瞬間は過ぎ去っていたからです。そこでそうした小さなタスクはbashやPythonに任され、私は毎回静かに不満を漏らしていました。

.NET 10はその儀式を取り除きました。1つの.csファイルを書いて実行するだけです。実際にスクリプトとしてどれほど実用的に使えるのか確認したかったので、今週テストしてみました。派手なものではなく、Linuxコンテナ1つとストップウォッチだけです。

1ファイル、プロジェクト不要

以下はbiggest.csという、指定ディレクトリ配下の最大ファイルを一覧表示する小さなユーティリティです。プログラム全体がこの1ファイルだけで、csprojは一切ありません。

#!/usr/bin/env dotnet
#:package Humanizer@3.0.10

using Humanizer;

var root = args.Length > 0 ? args[0] : ".";
var top = args.Length > 1 && int.TryParse(args[1], out var n) ? n : 10;

var files = new DirectoryInfo(root)
    .EnumerateFiles("*", new EnumerationOptions
    {
        RecurseSubdirectories = true,
        IgnoreInaccessible = true,
        AttributesToSkip = FileAttributes.ReparsePoint
    })
    .OrderByDescending(f => f.Length)
    .Take(top)
    .ToList();

foreach (var f in files)
{
    var size = f.Length.Bytes().Humanize("#.#");
    var age = (DateTime.UtcNow - f.LastWriteTimeUtc).Humanize();
    Console.WriteLine($"{size,10}  {f.FullName}  (modified {age} ago)");
}

Enter fullscreen mode Exit fullscreen mode

新しく追加されたのは2行です。#:package [email protected]はNuGet参照をソース内のディレクティブとして記述したものです。shebangについては後述します。それ以外は、トップレベルステートメントを含む、普段書いているC#そのものです。

$ dotnet run biggest.cs -- ~/.dotnet 5

Top 5 files under /root/.dotnet:

   37.6 MB  .../FSharp.Compiler.Service.dll  (modified 46 seconds ago)
   18.7 MB  .../Microsoft.CodeAnalysis.CSharp.dll  (modified 46 seconds ago)
   18.7 MB  .../Roslyn/bincore/Microsoft.CodeAnalysis.CSharp.dll  (modified 45 seconds ago)
   14.9 MB  .../System.Private.CoreLib.dll  (modified 45 seconds ago)
   11.4 MB  .../native/singlefilehost  (modified 47 seconds ago)

Total: 101.1 MB

Enter fullscreen mode Exit fullscreen mode

はい、.NET SDKで最も大きいファイルはF#コンパイラでした。思った以上に面白かったです。

ストップウォッチの計測

明らかな懸念点:Python並みの反復速度で使えるのか、それとも実行のたびにコンパイルの代償を払うのか?SDK 10.0.302をLinuxコンテナで計測しました。ラボ環境ではなく、実用的な比率を確認するためのもので、ウォーム時の数値は3回の最良値です。

初回実行は10.5秒かかりました。これは設計上最悪ケースで、Humanizerの復元、コンパイル、CLIの初回起動ウェルカムバナーまで含まれています。その後は、変更のないスクリプトは約0.3秒で実行され、編集直後の再実行も0.5秒をわずかに下回ります。保存から出力まで0.5秒は、スクリプト言語の領域に十分入っています。コンパイラの存在を意識しなくなったことが、まさにこの機能の目的です。

もう1つ確認したのは、フォルダにファイルが1つだけ残ることです。bin/obj/は生成されません。ビルド成果物は~/.local/share/dotnet/runfile/配下のコンテンツハッシュ付きフォルダに格納され、スクリプトがある場所はbashスクリプトのフォルダと同じくらいクリーンに保たれます。dotnet biggest.csも動作します。runと入力するのが煩わしい場合に便利です。

chmod +x、試してみる

shebang行は飾りではありません:

$ chmod +x biggest.cs
$ ./biggest.cs /tmp 3

   22.3 MB  /tmp/phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2  (modified 12 weeks ago)
   ...

Enter fullscreen mode Exit fullscreen mode

C#ファイルが本格的なシェルスクリプトのように振る舞い、cronやCIで実行可能です。私の意見として、1つのパイプとgrepを超える用途では、以前管理していたbashの山よりもこちらを選びます。bashでの文字列処理は週末を費やしてしまう原因であり、それらのスクリプトは本物のJSONパーサーやHTTPクライアントを1つの#:packageで手に入れられます。

パーティーの終わり

少なくともテストしたSDKでは、完全に単一ファイルです。Helperクラスをスクリプトの隣にあるutil.csに配置すると、error CS0103: The name 'Helper' does not exist in the current contextというエラーが発生しました。ファイルを1つ実行すると、そのファイルだけがコンパイルされます。

私はこの率直さが気に入っています。なぜなら、2つ目のファイルが必要になるのは、まさにスクリプトでなくなった瞬間だからです。卒業への道は1つのコマンドです:

$ dotnet project convert biggest.cs

Enter fullscreen mode Exit fullscreen mode

これによりbiggest/フォルダが生成され、ソースと適切なcsprojが含まれ、shebangとディレクティブは削除され、#:package行は本物のPackageReferenceに変換されます。面白い点として、生成されたプロジェクトには最初からPublishAotPackAsToolがtrueに設定されていました。SDKはスクリプトが成長して何になるかについて強い意見を持っています。

使うべきでないケース:チームメイトと共同でメンテナンスするもの、テストが必要なもの、1ファイル以上のアイデアを含むもの。また、コールドスタートを忘れないでください。初回実行時の10秒間の復元は、NuGetフォルダをキャッシュしない限り、エフェメラルCIコンテナが毎回代償を払うことを意味します。

しかし20行程度のタスクについては?Pythonに手を伸ばすのをやめました。一度10秒、以後ずっと0.3秒、そして実際に私が考える言語です。

完全な実行可能なサンプル:https://github.com/ssukhpinder/dev-to-code-samples/tree/main/016-file-based-csharp

これまでソリューション全体を作成した中で最も小さなタスクは何でしたか?私の場合は3行と、今でも恥ずかしい.slnファイルでした。皆さんの経験をコメントで教えてください。

— 誰も頼んでいないのに計測を続けている人