目次

ファイル ベース実行

概要

.NET 10 (C# 14 と同世代)で単独の .cs ファイルだけで C# プログラムを実行できるようになりました。 例えば、app1.cs という名前で保存した C# ファイルを dotnet app1.cs という1コマンドだけで実行できます。 それに伴って、C# 14 で #!#: (無視ディレクティブ)という機能が追加されています。

(C# 言語の新文法というよりは、C# コンパイラーの1機能という感じのものです。 バージョン的にも C# 14 である必要はなくて、.NET 10 以降付属の C# コンパイラーであれば言語バージョン問わず #!#: を認識します。)

本項ではこの「単独のファイルでの実行」(ファイル ベース実行)の話と、 C# 14 の #!#: (無視ディレクティブ)について説明します。

サンプル コード: FileBaseApp

ファイル ベース実行

改めて、 .NET 10 で C# ファイルを直接1コマンドで実行できるようになりました。 例えば、以下の1行だけ書いたファイル app1.cs を用意して、

Console.WriteLine("🐈");

以下のようなコマンドを打つと、この C# ファイルを単独で実行できます。

> dotnet app1.cs
🐈

これはスクリプト実行ではなく、通常の※脚注 C# 実行になります。 この仕組みをファイル ベース実行(file-based execution) と言い、これを使って書かれた C# プログラムをファイル ベース アプリ(file-based app)と言ったりします。

この機能の追加に伴い、これまでであればプロジェクト (実体は拡張子 .csproj の XML ファイル)に書いていた設定の類を C# 中に直接書けるようになりました。 以下の2つが追加されています。

  • #! : いわゆる shebang
  • #: : プロジェクト設定の類を書くためのディレクティブで、C# コンパイラーにとっては「単に無視」になる

ちなみに、普通に .csproj ファイルを使って C# プロジェクトをコンパイルする際には、#!#: があるとコンパイル エラーになります。 ただし、.csproj ファイル中に <Features>FileBasedProgram</Features> オプションを書いておくとコンパイルでき、この場合、#!#: から始まる行は単に無視されます。 C# コンパイラーからすると「単に無視するもの」なので、無視ディレクティブ(ignored directive)と呼ばれます。

スクリプト実行が「それ専用の構文がいくつかある」状態なのに対して、 ファイル ベース実行は本当に普通の C# です。 スクリプト実行みたいに「1行1行追加で実行」みたいなことができない一方で、 「コードが多くなってきたから .csproj 形式の通常の C# プロジェクトに切り替えたい」というときにスムーズに移行できます。 移行を自動化するための dotnet project convert というコマンドも用意されています。

shebang

#! (通称 shebang。 sharp + bang が由来)は主に Unix のスクリプト言語で使われるもので、 ソースコードの先頭にこの記号から始まる行を入れると「何を使ってこのスクリプトを実行するか」を指定できます。

C# 14 で、C# にもこの1行を入れることができるようになりました。 例えば前節の app1.cs ファイルにちょっと手を加えて以下のような内容にします。

#!/usr/bin/env dotnet
Console.WriteLine("🐈");

このファイルは bash などの Unix 系シェルで ./app1.cs みたいに直接実行できるようになります。 (実行権限が必要なので、最初に1回 chmod +x などの操作が必要。)

$ ls
app1.cs
$ chmod +x app1.cs
$ ./app1.cs
🐈

用途的に、#! はファイルの先頭にのみ書けます。 #! の前には改行はもちろんのこと、空白文字や BOM を入れることもできません。

: 無視ディレクティブ

#: から始まる行は dotnet コマンドがプロジェクト設定として解釈するために使い、 C# 上は単に無視されます。

例えば、以下のような .cs ファイルをファイル ベース実行するのは、

#:property InvariantGlobalization=true
Console.WriteLine(new DateTime(2000, 1, 2, 3, 4, 5));

以下のような2ファイルを使って既存の .csproj ベースの dotnet run をするのとほぼ同じ意味になります。

app1.csproj:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    </PropertyGroup>
    <InvariantGlobalization>true</InvariantGlobalization>
</Project>

app1.cs:

Console.WriteLine(new DateTime(2000, 1, 2, 3, 4, 5));

(ちなみに InvariantGlobalization を指定すると書式が北米フォーマットになるので、出力される結果は 01/02/2000 03:04:05 (MM/dd/yyyy)になります。)

#: で始まる無視ディレクティブは shebang とコメントを除いて、ファイルの先頭に置く必要があります。 例えば以下のコードでは、5行目(LangVersion の行)は問題なく、 9行目(ImplicitUsings の行)でだけコンパイル エラーを起こします。

#!/usr/bin/env dotnet

// コメントはあってもいい。

#:property LangVersion=13

Console.WriteLine("🐈");

#:property ImplicitUsings=disable

.NET 10 時点で、dotnet コマンドは以下のディレクティブを解釈できます。

ディレクティブ 意味 .csproj での書き方
#:sdk プロジェクト SDK を指定 <Project Sdk="これ">
#:property プロパティ要素 <PropertyGroup> の子要素
#:package パッケージ参照 <PackageReference> 要素
#:project プロジェクト参照 <ProjectReference> 要素

sdk ディレクティブ

#:sdk は、 .csproj では <Project Sdk="Identifier"> と書いていたものです。 省略した場合は Microsoft.NET.Sdk (ライブラリやコンソール プログラムで使う一番シンブルな SDK)になります。 実質的には「ASP.NET プログラムを書きたいときに Microsoft.NET.Sdk.Web にするもの」です。

例えば、以下のようなコードで ASP.NET なコードをファイル ベース実行できます。

#:sdk Microsoft.NET.Sdk.Web

var app = WebApplication.CreateBuilder(args).Build();
app.MapGet("/", () => "Hello World!");
app.Run();

property ディレクティブ

#:property は、 .csproj では <PropertyGroup> の子要素として書いていたものです。 .csproj<Tag>Value</Tag> 要素が #:property Tag=Value という書き方になります。

無視ディレクティブの節の冒頭の InvariantGlobalization の例もこれになります。 その他、例えば unsafe ブロックはオプションを指定しないと使えない構文なわけですが、以下のように書くことでそのオプションを指定できます。

#:property AllowUnsafeBlocks=true

// unsafe ブロックはオプションをつけないと使えない構文。
unsafe
{
    int n = 1;
    int* pn = &n;
    Console.WriteLine($"{(nint)pn:x}");
}

package ディレクティブ

#:package は、 .csproj では <PackageReference> 要素で書いていたものです。 .csproj<PackageReference Include="PackageName" Version="x.y.z" /> 要素が #:package PackageName@x.y.z という書き方になります。

例として Microsoft.CodeAnalysis.CSharp パッケージ(C# 中から C# コンパイラー自身を呼ぶためのライブラリ)を参照したコードを書くと以下のようになります。 (ちなみに、4.14.0 は C# 13 当時のバージョンです。)

#:package Microsoft.CodeAnalysis.CSharp@4.14.0

using Microsoft.CodeAnalysis.CSharp;

var tree = CSharpSyntaxTree.ParseText("class Class1;");
var root = await tree.GetRootAsync();
Console.WriteLine(root.GetFirstToken().Text);

project ディレクティブ

#:project は、 .csproj では <ProjectReference> 要素で書いていたものです。 .csproj<ProjectReference Include="path" /> 要素が #:project path という書き方になります。

例えば以下のような書き方で、.cs のある場所からの相対パスで Lib/Lib.csproj プロジェクトを参照できます。

#:project Lib/Lib.csproj
Console.WriteLine(Lib.Class1.Name);

未対応のディレクティブ

未対応の #: ディレクティブは、ファイル ベース実行するとエラーを起こします。 例えば以下のようなコードを書いて dotnet app1.cs コマンド実行すると、 「認識されないディレクティブ ' aaa' です。」というエラーが出ます。

#:aaa
Console.WriteLine("🐈");

ちなみにこのエラーを出すのはあくまで dotnet コマンドであって、 C# コンパイラー的には「#: で始まるディレクティブはすべて無視」という挙動になっています。 <Features>FileBasedProgram</Features> オプションを書いた .csproj ファイルを用意して、 旧来方式でコンパイルすると #:aaa の行のエラーは出ません。

更新履歴

ブログ