This is a repository of dotnet-csi which is an interactive tool for running C# scripts. It can installed as a command-line tool on Windows, Linux, or macOS.
The tool requires .NET 6+ runtime.
After installing tool you can use this tool to run C# scripts from the command line. dotnet-csi is available as a NuGet package.
Before installing dotnet-csi as a local tool dot not forget to create .NET local tool manifest file if it is not exist:
dotnet new tool-manifest
Install the tool and add to the local tool manifest:
dotnet tool install dotnet-csi
Or install the tool for the current user:
dotnet tool install dotnet-csi -g
Launch the tool in the interactive mode:
dotnet csi
Run a specified script with a given argument:
dotnet csi Samples/Scripts/hello.csx World
Run a single script located in the MyDirectory directory:
dotnet csi Samples/Build
Usage:
dotnet csi [options] [--] [script] [script arguments]
Executes a script if specified, otherwise launches an interactive REPL (Read Eval Print Loop).
Supported arguments:
| Option | Description | Alternative form |
|---|---|---|
| script | The path to the script file to run. If no such file is found, the command will treat it as a directory and look for a single script file inside that directory. | |
| script arguments | Script arguments are accessible in a script via the global list Args[index] by an argument index. | |
| -- | Indicates that the remaining arguments should not be treated as options. | |
| --help | Show how to use the command. | /?, -h, /h, /help |
| --version | Display the tool version. | /version |
| --source | Specify the NuGet package source to use. Supported formats: URL, or a UNC directory path. | -s, /s, /source |
| --property <key=value> | Define a key-value pair(s) for the script properties called Props, which is accessible in scripts. | -p, /property, /p |
| --property:<key=value> | Define a key-value pair(s) in MSBuild style for the script properties called Props, which is accessible in scripts. | -p:<key=value>, /property:<key=value>, /p:<key=value>, --property:key1=val1;key2=val2 |
| @file | Read the response file for more options. |
using HostApi; directive in a script allows you to use host API types without specifying the fully qualified namespace of these types.
Install the C# script template CSharpInteractive.Templates
dotnet new -i CSharpInteractive.Templates
Create a console project "Build" containing a script from the template build
dotnet new build -o ./Build
This projects contains the script ./Build/Program.csx. To run this script from the command line from the directory Build:
dotnet csi Build
To run as a .NET console application:
dotnet run --project Build
- Global state
- Logging
- Command Line API
- Docker API
- .NET build API
- Build a project
- Build a project using MSBuild
- Clean a project
- Pack a project
- Publish a project
- Restore a project
- Restore local tools
- Run a custom .NET command
- Run a project
- Run tests under dotCover
- Test a project
- Test a project using the MSBuild VSTest target
- Test an assembly
- Shuts down build servers
- NuGet API
- TeamCity Service Messages API
Args have got from the script arguments.
if (Args.Count > 0) { WriteLine(Args[0]); } if (Args.Count > 1) { WriteLine(Args[1]); }
Properties Props have got from TeamCity system properties automatically.
WriteLine(Props["TEAMCITY_VERSION"]); WriteLine(Props["TEAMCITY_PROJECT_NAME"]); // This property will be available at the next TeamCity steps as system parameter _system.Version_ // and some runners, for instance, the .NET runner, pass it as a build property. Props["Version"] = "1.1.6";
Host is actually the provider of all global properties and methods.
var packages = Host.GetService<INuGet>(); Host.WriteLine("Hello");
This method might be used to get access to different APIs like INuGet or ICommandLine.
GetService<INuGet>(); var serviceProvider = GetService<IServiceProvider>(); serviceProvider.GetService(typeof(INuGet));
Besides that, it is possible to get an instance of System.IServiceProvider to access APIs.
public void Run() { var serviceProvider = GetService<IServiceCollection>() .AddTransient<MyTask>() .BuildServiceProvider(); var myTask = serviceProvider.GetRequiredService<MyTask>(); var exitCode = myTask.Run(); exitCode.ShouldBe(0); } private class MyTask(ICommandLineRunner runner) { public int? Run() => runner.Run(new CommandLine("whoami")); }
WriteLine("Hello");
WriteLine();
WriteLine("Hello", Header);
Error("Error info", "Error identifier");
Warning("Warning info");
Info("Some info");
Trace("Some trace info");
// Adds the namespace "Script.Cmd" to use Command Line API using HostApi; // Creates and run a simple command line "whoami".AsCommandLine().Run(); // Creates and run a simple command line new CommandLine("whoami").Run(); // Creates and run a command line with arguments new CommandLine("cmd", "/c", "echo", "Hello").Run(); // Same as previous statement new CommandLine("cmd", "/c") .AddArgs("echo", "Hello") .Run(); (new CommandLine("cmd") + "/c" + "echo" + "Hello").Run(); "cmd".AsCommandLine("/c", "echo", "Hello").Run(); ("cmd".AsCommandLine() + "/c" + "echo" + "Hello").Run(); // Just builds a command line with multiple environment variables var cmd = new CommandLine("cmd", "/c", "echo", "Hello") .AddVars(("Var1", "val1"), ("var2", "Val2")); // Same as previous statement cmd = new CommandLine("cmd") + "/c" + "echo" + "Hello" + ("Var1", "val1") + ("var2", "Val2"); // Builds a command line to run from a specific working directory cmd = new CommandLine("cmd", "/c", "echo", "Hello") .WithWorkingDirectory("MyDyrectory"); // Builds a command line and replaces all command line arguments cmd = new CommandLine("cmd", "/c", "echo", "Hello") .WithArgs("/c", "echo", "Hello !!!");
// Adds the namespace "HostApi" to use Command Line API using HostApi; var exitCode = GetService<ICommandLineRunner>().Run(new CommandLine("cmd", "/c", "DIR")); exitCode.ShouldBe(0); // or the same thing using the extension method exitCode = new CommandLine("cmd", "/c", "DIR").Run(); exitCode.ShouldBe(0); // using operator '+' var cmd = new CommandLine("cmd") + "/c" + "DIR"; exitCode = cmd.Run(); exitCode.ShouldBe(0); // with environment variables cmd = new CommandLine("cmd") + "/c" + "DIR" + ("MyEnvVar", "Some Value"); exitCode = cmd.Run(); exitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use Command Line API using HostApi; int? exitCode = await GetService<ICommandLineRunner>().RunAsync(new CommandLine("cmd", "/C", "DIR")); // or the same thing using the extension method exitCode = await new CommandLine("cmd", "/c", "DIR").RunAsync();
// Adds the namespace "HostApi" to use Command Line API using HostApi; var lines = new List<string>(); int? exitCode = new CommandLine("cmd", "/c", "SET") .AddVars(("MyEnv", "MyVal")) .Run(output => lines.Add(output.Line)); lines.ShouldContain("MyEnv=MyVal");
// Adds the namespace "HostApi" to use Command Line API using HostApi; Task<int?> task = new CommandLine("cmd", "/c", "DIR").RunAsync(); int? exitCode = new CommandLine("cmd", "/c", "SET").Run(); task.Wait();
The cancellation will kill a related process.
// Adds the namespace "HostApi" to use Command Line API using HostApi; var cancellationTokenSource = new CancellationTokenSource(); Task<int?> task = new CommandLine("cmd", "/c", "TIMEOUT", "/T", "120").RunAsync(default, cancellationTokenSource.Token); cancellationTokenSource.CancelAfter(TimeSpan.FromMilliseconds(100)); task.IsCompleted.ShouldBeFalse();
If timeout expired a process will be killed.
// Adds the namespace "HostApi" to use Command Line API using HostApi; int? exitCode = new CommandLine("cmd", "/c", "TIMEOUT", "/T", "120").Run(default, TimeSpan.FromMilliseconds(1)); exitCode.HasValue.ShouldBeFalse();
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force" var result = new DotNetNew("xunit", "-n", "MyLib", "--force").Build(); result.ExitCode.ShouldBe(0); // Builds the library project, running a command like: "dotnet build" from the directory "MyLib" result = new DotNetBuild().WithWorkingDirectory("MyLib").Build(); // The "result" variable provides details about a build result.Errors.Any(message => message.State == BuildMessageState.StdError).ShouldBeFalse(); result.ExitCode.ShouldBe(0); // Runs tests in docker result = new DotNetTest().WithWorkingDirectory("MyLib").Build(); result.ExitCode.ShouldBe(0); result.Summary.Tests.ShouldBe(1); result.Tests.Count(test => test.State == TestState.Finished).ShouldBe(1);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force" var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build(); result.ExitCode.ShouldBe(0); // Builds the library project, running a command like: "dotnet build" from the directory "MyLib" result = new DotNetBuild().WithWorkingDirectory("MyLib").Build(); result.ExitCode.ShouldBe(0); // Clean the project, running a command like: "dotnet clean" from the directory "MyLib" result = new DotNetClean().WithWorkingDirectory("MyLib").Build(); // The "result" variable provides details about a build result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Gets the dotnet version, running a command like: "dotnet --version" NuGetVersion? version = default; var exitCode = new DotNetCustom("--version").Run(message => NuGetVersion.TryParse(message.Line, out version)); exitCode.ShouldBe(0); version.ShouldNotBeNull();
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force" var result = new DotNetNew("mstest", "-n", "MyTests", "--force").Build(); result.ExitCode.ShouldBe(0); // Runs tests via a command like: "dotnet msbuild /t:VSTest" from the directory "MyTests" result = new MSBuild() .WithTarget("VSTest") .WithWorkingDirectory("MyTests").Build(); // The "result" variable provides details about a build result.ExitCode.ShouldBe(0); result.Summary.Tests.ShouldBe(1); result.Tests.Count(test => test.State == TestState.Finished).ShouldBe(1);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force" var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build(); result.ExitCode.ShouldBe(0); // Creates a NuGet package of version 1.2.3 for the project, running a command like: "dotnet pack /p:version=1.2.3" from the directory "MyLib" result = new DotNetPack() .WithWorkingDirectory("MyLib") .AddProps(("version", "1.2.3")) .Build(); result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force" var result = new DotNetNew("classlib", "-n", "MyLib", "--force", "-f", "net8.0").Build(); result.ExitCode.ShouldBe(0); // Publish the project, running a command like: "dotnet publish --framework net6.0" from the directory "MyLib" result = new DotNetPublish().WithWorkingDirectory("MyLib").WithFramework("net8.0").Build(); result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force" var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build(); result.ExitCode.ShouldBe(0); // Restore the project, running a command like: "dotnet restore" from the directory "MyLib" result = new DotNetRestore().WithWorkingDirectory("MyLib").Build(); result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new console project, running a command like: "dotnet new console -n MyApp --force" var result = new DotNetNew("console", "-n", "MyApp", "--force").Build(); result.ExitCode.ShouldBe(0); // Runs the console project using a command like: "dotnet run" from the directory "MyApp" var stdOut = new List<string>(); result = new DotNetRun().WithWorkingDirectory("MyApp").Build(message => stdOut.Add(message.Text)); result.ExitCode.ShouldBe(0); // Checks StdOut stdOut.ShouldBe(new[] {"Hello, World!"});
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force" var result = new DotNetNew("mstest", "-n", "MyTests", "--force").Build(); result.ExitCode.ShouldBe(0); // Runs tests via a command like: "dotnet test" from the directory "MyTests" result = new DotNetTest().WithWorkingDirectory("MyTests").Build(); // The "result" variable provides details about a build result.ExitCode.ShouldBe(0); result.Summary.Tests.ShouldBe(1); result.Tests.Count(test => test.State == TestState.Finished).ShouldBe(1);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force" var exitCode = new DotNetNew("mstest", "-n", "MyTests", "--force").Run(); exitCode.ShouldBe(0); // Creates the tool manifest and installs the dotCover tool locally // It is better to run the following 2 commands manually // and commit these changes to a source control exitCode = new DotNetNew("tool-manifest").Run(); exitCode.ShouldBe(0); exitCode = new DotNetCustom("tool", "install", "--local", "JetBrains.dotCover.GlobalTool").Run(); exitCode.ShouldBe(0); // Creates a test command var test = new DotNetTest().WithProject("MyTests"); var dotCoverSnapshot = Path.Combine("MyTests", "dotCover.dcvr"); var dotCoverReport = Path.Combine("MyTests", "dotCover.html"); // Modifies the test command by putting "dotCover" in front of all arguments // to have something like "dotnet dotcover test ..." // and adding few specific arguments to the end var testUnderDotCover = test.Customize(cmd => cmd.ClearArgs() + "dotcover" + cmd.Args + $"--dcOutput={dotCoverSnapshot}" + "--dcFilters=+:module=TeamCity.CSharpInteractive.HostApi;+:module=dotnet-csi" + "--dcAttributeFilters=System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"); // Runs tests under dotCover via a command like: "dotnet dotcover test ..." var result = testUnderDotCover.Build(); // The "result" variable provides details about a build result.ExitCode.ShouldBe(0); result.Tests.Count(i => i.State == TestState.Finished).ShouldBe(1); // Generates a HTML code coverage report. exitCode = new DotNetCustom("dotCover", "report", $"--source={dotCoverSnapshot}", $"--output={dotCoverReport}", "--reportType=HTML").Run(); exitCode.ShouldBe(0); // Check for a dotCover report File.Exists(dotCoverReport).ShouldBeTrue();
// Adds the namespace "HostApi" to use .NET build API using HostApi; var projectDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()[..4]); Directory.CreateDirectory(projectDir); // Creates a local tool manifest var exitCode = new DotNetNew("tool-manifest").WithWorkingDirectory(projectDir).Run(); exitCode.ShouldBe(0); // Restore local tools exitCode = new DotNetToolRestore().WithWorkingDirectory(projectDir).Run(); exitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force" var result = new DotNetNew("mstest", "-n", "MyTests", "--force").Build(); result.ExitCode.ShouldBe(0); // Builds the test project, running a command like: "dotnet build -c Release" from the directory "MyTests" result = new DotNetBuild().WithWorkingDirectory("MyTests").WithConfiguration("Release").WithOutput("MyOutput").Build(); result.ExitCode.ShouldBe(0); // Runs tests via a command like: "dotnet vstest" from the directory "MyTests" result = new VSTest() .AddTestFileNames(Path.Combine("MyOutput", "MyTests.dll")) .WithWorkingDirectory("MyTests") .Build(); // The "result" variable provides details about a build result.ExitCode.ShouldBe(0); result.Summary.Tests.ShouldBe(1); result.Tests.Count(test => test.State == TestState.Finished).ShouldBe(1);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force" var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build(); result.ExitCode.ShouldBe(0); // Builds the library project, running a command like: "dotnet msbuild /t:Build -restore /p:configuration=Release -verbosity=detailed" from the directory "MyLib" result = new MSBuild() .WithWorkingDirectory("MyLib") .WithTarget("Build") .WithRestore(true) .AddProps(("configuration", "Release")) .WithVerbosity(DotNetVerbosity.Detailed) .Build(); // The "result" variable provides details about a build result.Errors.Any(message => message.State == BuildMessageState.StdError).ShouldBeFalse(); result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API using HostApi; // Shuts down all build servers that are started from dotnet. var exitCode = new DotNetBuildServerShutdown().Run(); exitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use INuGet using HostApi; IEnumerable<NuGetPackage> packages = GetService<INuGet>().Restore(new NuGetRestoreSettings("IoC.Container").WithVersionRange(VersionRange.All));
// Adds the namespace "HostApi" to use INuGet using HostApi; var packagesPath = Path.Combine( Path.GetTempPath(), Guid.NewGuid().ToString()[..4]); var settings = new NuGetRestoreSettings("IoC.Container") .WithVersionRange(VersionRange.Parse("[1.3, 1.3.8)")) .WithTargetFrameworkMoniker("net5.0") .WithPackagesPath(packagesPath); IEnumerable<NuGetPackage> packages = GetService<INuGet>().Restore(settings);
// Adds the namespace "HostApi" to use .NET build API and Docker API using HostApi; // Creates a base docker command line var dockerRun = new DockerRun() .WithAutoRemove(true) .WithInteractive(true) .WithImage("mcr.microsoft.com/dotnet/sdk") .WithPlatform("linux") .WithContainerWorkingDirectory("/MyProjects") .AddVolumes((ToAbsoluteLinuxPath(Environment.CurrentDirectory), "/MyProjects")); // Creates a new library project in a docker container var exitCode = dockerRun .WithCommandLine(new DotNetCustom("new", "classlib", "-n", "MyLib", "--force")) .Run(); exitCode.ShouldBe(0); // Builds the library project in a docker container var result = dockerRun .WithCommandLine(new DotNetBuild().WithProject("MyLib/MyLib.csproj")) .Build(); // The "result" variable provides details about a build result.Errors.Any(message => message.State == BuildMessageState.StdError).ShouldBeFalse(); result.ExitCode.ShouldBe(0); string ToAbsoluteLinuxPath(string path) => "/" + path.Replace(":", "").Replace('\\', '/');
// Adds the namespace "HostApi" to use Command Line API and Docker API using HostApi; // Creates some command line to run in a docker container var cmd = new CommandLine("whoami"); // Runs the command line in a docker container var result = new DockerRun(cmd, "mcr.microsoft.com/dotnet/sdk") .WithAutoRemove(true) .Run(); result.ShouldBe(0);
For more details how to use TeamCity service message API please see this page. Instead of creating a root message writer like in the following example:
using JetBrains.TeamCity.ServiceMessages.Write.Special; using var writer = new TeamCityServiceMessages().CreateWriter(Console.WriteLine);
use this statement:
using JetBrains.TeamCity.ServiceMessages.Write.Special; using var writer = GetService<ITeamCityWriter>();
This sample opens a block My Tests and reports about two tests:
// Adds a namespace to use ITeamCityWriter using JetBrains.TeamCity.ServiceMessages.Write.Special; using var writer = GetService<ITeamCityWriter>(); using (var tests = writer.OpenBlock("My Tests")) { using (var test = tests.OpenTest("Test1")) { test.WriteStdOutput("Hello"); test.WriteImage("TestsResults/Test1Screenshot.jpg", "Screenshot"); test.WriteDuration(TimeSpan.FromMilliseconds(10)); } using (var test = tests.OpenTest("Test2")) { test.WriteIgnored("Some reason"); } }
For more information on TeamCity Service Messages, see this page.