Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .claude/skills/csharp-eval/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
name: csharp-eval
description: Run / execute C# snippets non-interactively with the csharprepl CLI to observe real runtime behavior — return values, exceptions, serialized output — and to probe how a NuGet package actually behaves when called. The complement to dotnet-inspect: that tool inspects static API surface without executing; this one runs code. Use whenever you need to know what C# *does*, not just what an API *looks like*.
---

# csharp-eval

Execute C# and see the result. `csharprepl` is a Roslyn-based C# REPL; run non-interactively it
evaluates a snippet, prints the value of the final expression as plain text, and exits.

## When to use this vs. dotnet-inspect

- **"What does this API *look like*?"** (signatures, members, docs, what changed between versions)
→ use **dotnet-inspect**. It reads metadata; it does not run anything.
- **"What does this code *do* when it runs?"** (the actual value, the exception it throws, the JSON it
produces, how a package behaves) → use **csharp-eval**.

They compose well: inspect a method's signature with dotnet-inspect, then run it here to see its output.

## Use it to...

- Check runtime semantics / edge cases: how `string.Split` handles empty entries, default values,
null handling, culture/format behavior, what a regex matches.
- Verify a LINQ chain or algorithm returns what you think before writing it into the project.
- See the actual serialized shape of an object (e.g. `JsonSerializer.Serialize(...)`).
- Probe how a NuGet package behaves at runtime — call it and look at the real output.
- Reproduce/confirm an exception and read its message.

## Running code

### One-liners — `--eval` or `-e`

```
csharprepl -e 'Enumerable.Range(1, 5).Sum()' # -> 15
csharprepl -e 'DateTime.Parse("2026-01-31").DayOfWeek' # -> Saturday
csharprepl -e 'new[] { 3, 1, 2 }.OrderBy(x => x).ToArray()' # -> int[3] { 1, 2, 3 }
```

**Tip: wrap the code in single quotes by default.** C# string literals use double quotes (`"..."`), so
single-quoting the snippet avoids escaping them. Switch to `--eval-file` (below) when the code itself
contains single quotes (C# char literals like `' '` or `'\n'`) rather than fighting the escaping.

The value of the final expression is **auto-printed** as plain text — no `Console.WriteLine` and no
color codes, just the value (collections render compactly, e.g. `int[3] { 1, 2, 3 }`). An explicit
`Console.WriteLine(...)` still works if you want to print more than the final value.

### Multi-line or quote-heavy code — `--eval-file`

For more than a quick expression — multiple statements, or code that's awkward to quote on the command
line (it contains quotes, etc.) — write it to a `.csx` file and run that. The file is raw C#, so there's
no command-line quoting to fight:

```
csharprepl --eval-file snippet.csx
```

`--eval-file` runs the file and exits. (Do NOT pass a `.csx` as a bare argument e.g. `csharprepl snippet.csx`,
that would load it and drop into the *interactive* REPL, hanging on input; use `--eval-file` for automation.)
Piping to stdin also evaluates and exits: `cat snippet.csx | csharprepl`.

### Referencing NuGet packages and assemblies

```
# reference a NuGet package, add a using, and call into it
csharprepl -e 'JsonConvert.SerializeObject(new[] { 1, 2, 3 })' -r 'nuget: Newtonsoft.Json' -u Newtonsoft.Json
```

- `-r "nuget: PackageName"` or `-r "nuget: PackageName, version"` — repeatable. (An in-script
`#r "nuget: ..."` directive works too, e.g. inside an `--eval-file` snippet.)
- `-r <path-to.dll>` / `-r <path-to.csproj>` references a local assembly or project.
- `-u <Namespace>` adds a `using` (repeatable). `-f <framework>` selects the shared framework.
- The evaluation **result** is the last thing on stdout. The first time a package is referenced, NuGet
prints a few restore-progress lines before it; cached runs print just the result.

## Gotchas

- **No state across calls.** Each invocation is a fresh process — variables, `using`s, and references
do not carry over between runs. Make every snippet self-contained (include its own `#r` / `using`).
- **First restore is slow.** The first time a package is referenced it's downloaded; later runs are
fast (cached under `~/.csharprepl/packages`).
- **Errors go to stderr with a nonzero exit code.** Compilation and runtime errors are written to
stderr (stdout stays clean for the result), so check stderr when a run fails.
- **`-e` and `--eval-file` are mutually exclusive.**
1 change: 1 addition & 0 deletions CSharpRepl.Services/CSharpRepl.Services.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<PackageReference Include="OpenAI" Version="2.10.0" />
<PackageReference Include="PrettyPrompt" Version="4.1.1" />
<PackageReference Include="Spectre.Console.Cli" Version="0.55.0" />
<PackageReference Include="Spectre.Console.Ansi" Version="0.55.2" />
<PackageReference Include="System.IO.Abstractions" Version="22.1.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.8" />
</ItemGroup>
Expand Down
13 changes: 11 additions & 2 deletions CSharpRepl.Services/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using PrettyPrompt;
using PrettyPrompt.Consoles;
using PrettyPrompt.Highlighting;
using Spectre.Console.Rendering;

namespace CSharpRepl.Services;

Expand Down Expand Up @@ -53,9 +54,15 @@ public sealed class Configuration
public bool UseUnicode { get; }
public bool UsePrereleaseNugets { get; }
public bool StreamPipedInput { get; set; }

/// <summary>
/// C# to evaluate non-interactively (from --eval or --eval-file) before exiting. Null when running
/// interactively or reading from piped stdin.
/// </summary>
public string? EvaluateInput { get; }
public string? LoadScript { get; }
public string[] LoadScriptArgs { get; }
public FormattedString OutputForEarlyExit { get; }
public IRenderable? OutputForEarlyExit { get; }
public OpenAIConfiguration? OpenAIConfiguration { get; }
public int TabSize { get; }

Expand All @@ -73,10 +80,11 @@ public Configuration(
bool useUnicode = false,
bool usePrereleaseNugets = false,
bool streamPipedInput = false,
string? evaluateInput = null,
int tabSize = 4,
string? loadScript = null,
string[]? loadScriptArgs = null,
FormattedString outputForEarlyExit = default,
IRenderable? outputForEarlyExit = null,
string[]? triggerCompletionListKeyPatterns = null,
string[]? newLineKeyPatterns = null,
string[]? submitPromptKeyPatterns = null,
Expand Down Expand Up @@ -134,6 +142,7 @@ public Configuration(
UseUnicode = useUnicode;
UsePrereleaseNugets = usePrereleaseNugets;
StreamPipedInput = streamPipedInput;
EvaluateInput = evaluateInput;
TabSize = tabSize;
LoadScript = loadScript;
LoadScriptArgs = loadScriptArgs ?? [];
Expand Down
19 changes: 19 additions & 0 deletions CSharpRepl.Services/ConsoleService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

using System;
using PrettyPrompt.Consoles;
using Spectre.Console;

namespace CSharpRepl.Services;

public sealed class ConsoleService : IConsoleService
{
public IConsole PrettyPromptConsole { get; } = new SystemConsole();
IConsole IConsoleService.PrettyPromptConsole => PrettyPromptConsole;

IAnsiConsole IConsoleService.Ansi => AnsiConsole.Console;

public string? ReadLine() => Console.ReadLine();
}
4 changes: 2 additions & 2 deletions CSharpRepl.Services/Dotnet/DotnetBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ namespace CSharpRepl.Services.Dotnet;

internal class DotnetBuilder
{
private readonly IConsoleEx console;
private readonly IConsoleService console;

public DotnetBuilder(IConsoleEx console)
public DotnetBuilder(IConsoleService console)
{
this.console = console;
}
Expand Down
12 changes: 0 additions & 12 deletions CSharpRepl.Services/GlobalSuppressions.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,49 @@

namespace CSharpRepl.Services;

public interface IConsoleEx : IAnsiConsole
/// <summary>
/// CSharpRepl's console abstraction. Wraps the Spectre and PrettyPrompt consoles.
/// </summary>
public interface IConsoleService
{
IConsole PrettyPromptConsole { get; }
// The underlying PrettyPrompt console that's used for the interactive prompt input.
protected IConsole PrettyPromptConsole { get; }

private IAnsiConsole AnsiConsole => this;
// The underlying Spectre console that provides e.g. color coded / wrapped output.
protected IAnsiConsole Ansi { get; }

void Write(string text) => AnsiConsole.Write(text);
/// <summary>Width, in characters, of the console buffer — for layout/wrapping math.</summary>
int BufferWidth => PrettyPromptConsole.BufferWidth;

/// <summary>Rendering profile (capabilities + width) of the underlying console.</summary>
Profile Profile => Ansi.Profile;

/// <summary>Cursor control for the underlying console.</summary>
IAnsiConsoleCursor Cursor => Ansi.Cursor;

/// <summary>Clears the screen.</summary>
void Clear() => Ansi.Clear(home: true);

void Write(IRenderable renderable) => Ansi.Write(renderable);
void Write(string text) => Ansi.Write(text);
void Write(FormattedString text) => PrettyPromptConsole.Write(text);

void WriteLine(string text) => AnsiConsole.WriteLine(text);
void WriteLine() => AnsiConsole.WriteLine();
void WriteLine(string text) => Ansi.WriteLine(text);
void WriteLine() => Ansi.WriteLine();
void WriteLine(FormattedString text) => PrettyPromptConsole.WriteLine(text);

/// <summary>
/// Writes a line of plain, unwrapped text to standard output. Use this for non-interactive output (e.g. --eval / piped results,
/// redirected output). Different from <see cref="WriteLine(string)"/> which writes via Spectre's AnsiConsole and word-wraps to
/// the console width (corrupting a value meant for piping).
/// </summary>
void WriteStandardOutputLine(string text) => PrettyPromptConsole.WriteLine(text);

/// <summary>
/// Similar to <see cref="WriteStandardOutputLine(string)"/> but for standard error.
/// </summary>
void WriteStandardErrorLine(string text) => PrettyPromptConsole.WriteErrorLine(text);

void WriteError(string text)
{
if (PrettyPromptConsole.IsErrorRedirected)
Expand Down
45 changes: 41 additions & 4 deletions CSharpRepl.Services/Nuget/ConsoleNugetLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,25 @@ internal sealed class ConsoleNugetLogger : ILogger
{
private const int NumberOfMessagesToShow = 6;

private readonly IConsoleEx console;
private readonly IConsoleService console;
private readonly Configuration configuration;
private readonly string successPrefix;
private readonly string errorPrefix;
private readonly List<Line> lines = [];
private readonly object linesLock = new(); // Lock for the lines list
private int linesRendered;

public ConsoleNugetLogger(IConsoleEx console, Configuration configuration)
// The normal 'pretty' rendering uses cursor movement and the console buffer width, which only
// work against a real terminal. When stdout is redirected (e.g. piped, --eval, or captured by a
// tool) those operations throw "The handle is invalid", so if we're non-interactive we should
// just write plain text.
private readonly bool interactive;

public ConsoleNugetLogger(IConsoleService console, Configuration configuration)
{
this.console = console;
this.configuration = configuration;
this.interactive = !Console.IsOutputRedirected;

successPrefix = configuration.UseUnicode ? "✅ " : "";
errorPrefix = configuration.UseUnicode ? "❌ " : "";
Expand All @@ -59,6 +66,12 @@ public void LogMinimal(string data)
var line = CreateLine(data, isError: false);
if (line.IsEmpty) return;

if (!interactive)
{
NonInteractiveAppendLine(line);
return;
}

lock (linesLock)
{
lines.Add(line);
Expand All @@ -83,9 +96,17 @@ public void LogMinimal(string data)

public void LogError(string data)
{
var line = CreateLine(data, isError: true);

if (!interactive)
{
NonInteractiveAppendLine(line);
return;
}

lock (linesLock)
{
lines.Add(CreateLine(data, isError: true));
lines.Add(line);
}
RenderLines();
}
Expand All @@ -101,6 +122,17 @@ public void Reset()

public void LogFinish(string text, bool success)
{
if (!interactive)
{
var summary = CreateLine(text, isError: !success);
if (!summary.IsEmpty)
{
NonInteractiveAppendLine(summary);
}

return;
}

//delete rendered lines
for (int i = 0; i < linesRendered; i++)
{
Expand Down Expand Up @@ -130,6 +162,11 @@ public void LogInformation(string data) { /* ignore, we don't need this much out

private Line CreateLine(string data, bool isError) => new(data, isError, isError ? errorPrefix : successPrefix, configuration);

/// <summary>
/// Write the message as plain text. No cursor movement and no ANSI color.
/// </summary>
private void NonInteractiveAppendLine(Line line) => console.WriteStandardOutputLine(line.Text.Text ?? "");

private void RenderLines()
{
try
Expand Down Expand Up @@ -157,7 +194,7 @@ private void RenderLines()
console.WriteLine(line.Text);
}

linesRendered += Math.DivRem(line.Text.Length, console.PrettyPromptConsole.BufferWidth, out var remainder) + (remainder == 0 ? 0 : 1);
linesRendered += Math.DivRem(line.Text.Length, console.BufferWidth, out var remainder) + (remainder == 0 ? 0 : 1);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion CSharpRepl.Services/Nuget/NugetPackageInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal sealed class NugetPackageInstaller
private readonly ConsoleNugetLogger logger;
private readonly bool usePrereleaseNugets;

public NugetPackageInstaller(IConsoleEx console, Configuration configuration)
public NugetPackageInstaller(IConsoleService console, Configuration configuration)
{
this.logger = new ConsoleNugetLogger(console, configuration);
this.usePrereleaseNugets = configuration.UsePrereleaseNugets;
Expand Down
Loading
Loading