|
| 1 | +--- |
| 2 | +title: Build file-based programs |
| 3 | +description: File-based programs are command line utilities that are built and execute without a project file. The build and run commands are implicit. New syntax supports project settings in source. |
| 4 | +ms.date: 08/01/2025 |
| 5 | +ms.topic: tutorial |
| 6 | +#customer intent: As a developer, I want build utilities so that more work is automated. |
| 7 | +--- |
| 8 | + |
| 9 | +# Tutorial: Build file-based C# programs |
| 10 | + |
| 11 | +> [!IMPORTANT] |
| 12 | +> |
| 13 | +> File-based programs are a feature of .NET 10, which is in preview. |
| 14 | +> Some information relates to prerelease product that may be modified before it's released. Microsoft makes no warranties, express or implied, with respect to the information provided here. |
| 15 | +
|
| 16 | +*File-based programs* are programs contained within a single `*.cs` file that are built and run without a corresponding project (`*.csproj`) file. File-based programs are ideal for learning C# because they have less complexity: The entire program is stored in a single file. File-based programs are also useful for building command line utilities. On Unix platforms, file-based programs can be executed using `#!` (shebang) directives. |
| 17 | + |
| 18 | +In this tutorial, you: |
| 19 | + |
| 20 | +> [!div class="checklist"] |
| 21 | +> |
| 22 | +> * Create a file-based program |
| 23 | +> * Run the program using the .NET CLI and `#!` directives |
| 24 | +> * Add features and NuGet packages to the program |
| 25 | +> * Test the final application |
| 26 | +
|
| 27 | +## Prerequisites |
| 28 | + |
| 29 | +- The .NET 10 preview SDK. Download it [here](https://dotnet.microsoft.com/download/dotnet/10.0). |
| 30 | +- Visual Studio Code. Download it [here](https://code.visualstudio.com/Download). |
| 31 | +- (Optional) The C# DevKit extension for Visual Studio code. Download it [here](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit). |
| 32 | + |
| 33 | +If you install the C# DevKit, add this setting: |
| 34 | + |
| 35 | +```json |
| 36 | +"dotnet.projects.enableFileBasedPrograms": true |
| 37 | +``` |
| 38 | + |
| 39 | +Or, in the UI, it's under "Extensions", C#, LSP Server, Dotnet->Projects: Enable File Based programs". |
| 40 | + |
| 41 | +## Create a file-based program |
| 42 | + |
| 43 | +Open Visual Studio code and create a new file named `AsciiArt.cs`. Enter the following text: |
| 44 | + |
| 45 | +```csharp |
| 46 | +Console.WriteLine("Hello, world!"); |
| 47 | +``` |
| 48 | + |
| 49 | +Save the file. Then, open the integrated terminal in Visual Studio code and type: |
| 50 | + |
| 51 | +```dotnetcli |
| 52 | +dotnet run AsciiArt.cs |
| 53 | +``` |
| 54 | + |
| 55 | +The first time you run this program, the `dotnet` host builds the executable from your source file, stores that executable in a temporary folder, then runs the executable. You can verify this by typing `dotnet run AsciiArt.cs` again. This time, the `dotnet` host determines that the executable is current, and just runs the executable without building it again. You won't see any build output. The executable runs without the delay for a build. |
| 56 | + |
| 57 | +The preceding steps demonstrates that file based programs aren't script files. They are C# source files that are built using a default project file in a temporary folder. One of the lines of output when you built the program should look something like this (on Windows): |
| 58 | + |
| 59 | +```dotnetcli |
| 60 | +AsciiArt succeeded (7.3s) → AppData\Local\Temp\dotnet\runfile\AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc\bin\debug\AsciiArt.dll |
| 61 | +``` |
| 62 | + |
| 63 | +On unix platforms, the output folder is something similar to: |
| 64 | + |
| 65 | +```dotnetcli |
| 66 | +AsciiArt succeeded (7.3s) → Library/Application Support/dotnet/runfile/AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc/bin/debug/AsciiArt.dll |
| 67 | +``` |
| 68 | + |
| 69 | +That output tells you where the temporary files and build outputs are placed. Throughout this tutorial anytime you edit the source file, the `dotnet` host updates the executable before it runs. |
| 70 | + |
| 71 | +File based programs are regular C# programs. The only limitation is that they must be written in one source file. You can use top-level statements or a classic `Main` method as an entry point. You can declare any types: classes, interfaces, and structs. You can structure the algorithms in a file based program the same as you would in any C# program. You can even declare multiple namespaces to organize your code. |
| 72 | + |
| 73 | +In this tutorial, you'll build a file-based program that writes text as ASCII art. You'll learn how to include packages in file-based programs, process command input, and read arguments either from the command line or standard input. |
| 74 | + |
| 75 | +As a first step, let's write all arguments on the command line to the output. Replace the current contents of `AsciiArt.cs` with the following code: |
| 76 | + |
| 77 | +```csharp |
| 78 | +if (args.Length > 0) |
| 79 | +{ |
| 80 | + string message = string.Join(' ', args); |
| 81 | + Console.WriteLine(message); |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +You can run this version by typing the following command: |
| 86 | + |
| 87 | +```dotnetcli |
| 88 | +dotnet run AsciiArt.cs -- This is the command line. |
| 89 | +``` |
| 90 | + |
| 91 | +The `--` option indicates that all following command arguments should be passed to the AsciiArt program. The arguments `This is the command line.` are passed as an array of strings, where each string is one word: `This`, `is`, `the`, `command` and `line.`. |
| 92 | + |
| 93 | +This version demonstrates several new concepts: |
| 94 | + |
| 95 | +1. The command line arguments are passed to the program using the predefined variable `args`. This is an array of strings (`string[]`). If the length of `args` is 0, that means no arguments were provided. Otherwise, each word on the argument list is stored in the corresponding entry in the array. |
| 96 | +1. The `string.Join` method joins multiple strings into a single string, with the specfied separator. In this case, the separator is a single space. |
| 97 | + |
| 98 | +That handles command line arguments correctly. Now, add the code to handle reading input from standard input (`stdin`) instead of command line arguments. Add the following `else` clause to the `if` statement you added in the preceding code: |
| 99 | + |
| 100 | +```csharp |
| 101 | +else |
| 102 | +{ |
| 103 | + while (Console.ReadLine() is string line && line.Length > 0) |
| 104 | + { |
| 105 | + Console.WriteLine(line); |
| 106 | + } |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +You can test this by creating a new text file in the same folder. Name the file `input.txt` and add the following lines: |
| 111 | + |
| 112 | +```text |
| 113 | +This is the input file |
| 114 | +for a file based program. |
| 115 | +It prints the messages |
| 116 | +from a file or the |
| 117 | +command line. |
| 118 | +There are options you |
| 119 | +can choose for ASCII |
| 120 | +art and colors. |
| 121 | +``` |
| 122 | + |
| 123 | +Keep the lines short so they format correctly when you add the feature to use ASCII art. Now, run the program again: |
| 124 | + |
| 125 | +```dotnetcli |
| 126 | +cat input.txt | dotnet run AsciiArt.cs |
| 127 | +``` |
| 128 | + |
| 129 | +Now your program can accept either command line arguments or standard input. |
| 130 | + |
| 131 | +> [!NOTE] |
| 132 | +> |
| 133 | +> Support for `#!` directives applies on unix platforms only. There isn't a similar directive for Windows to directly execute a C# program. On Windows you must use `dotnet run` on the command line. |
| 134 | +
|
| 135 | +On unix, you can run file-based programs directly, typing the source file name on the command line instead of `dotnet run`. You need to make two changes. First, set *execute* permissions on the source file: |
| 136 | + |
| 137 | +```bash |
| 138 | +chmod +x AsciiArt.cs |
| 139 | +``` |
| 140 | + |
| 141 | +Then, add a `#!` directive as the first line of the `AsciiArt.cs` file: |
| 142 | + |
| 143 | +```csharp |
| 144 | +#!/usr/local/share/dotnet/dotnet run |
| 145 | +``` |
| 146 | + |
| 147 | +The location of `dotnet` may be different on different unix installations. |
| 148 | + |
| 149 | +After making these two changes, you can run the program from the command line directly: |
| 150 | + |
| 151 | +```bash |
| 152 | +./AsciiArt.cs |
| 153 | +``` |
| 154 | + |
| 155 | +If you prefer, you can rename the file to remove the extension. |
| 156 | + |
| 157 | +## Add features and NuGet packages to the program |
| 158 | + |
| 159 | +Next, add a package that supports ASCII art, [Colorful.Console](https://www.nuget.org/packages/Colorful.Console). To add a package to a file based program, you use the `#:package` directive. Add the following directive after the `#!` directive in your AsciiArt.cs file: |
| 160 | + |
| 161 | +```csharp |
| 162 | + |
| 163 | +``` |
| 164 | + |
| 165 | +> [!IMPORTANT] |
| 166 | +> |
| 167 | +> The version `1.2.15` was the latest version when this tutorial was last updated. If there is a newer version available, use the latest version to ensure you have the latest security packages. |
| 168 | +
|
| 169 | +Next, change the lines that call `Console.WriteLine` to use the `Colorful.Console.WriteAscii` method instead: |
| 170 | + |
| 171 | +```csharp |
| 172 | +Colorful.Console.WriteAscii(message); |
| 173 | +``` |
| 174 | + |
| 175 | +Next, let's add command line parsing. The current version writes each work as a different line of output. The tool doesn't support any command line options. We like to support two new features: |
| 176 | + |
| 177 | +1. Quote multiple words that should be written on one line: |
| 178 | + |
| 179 | + ```dotnetcli |
| 180 | + AsciiArt.cs "This is line one" "This is another line" "This is the last line" |
| 181 | + ``` |
| 182 | + |
| 183 | +1. Add a `--delay` option to pause between each line: |
| 184 | + |
| 185 | + ```dotnetcli |
| 186 | + AsciiArt.cs --delay 1000 |
| 187 | + ``` |
| 188 | + |
| 189 | +Users should be able to use both. |
| 190 | + |
| 191 | +Most command line applications need to parse command line arguments to handle options, commands, and user input effectively. The [`System.CommandLine` library](~/standard/commandline/index.md) provides comprehensive capabilities to handle commands, subcommands, options, and arguments, allowing you to concentrate on what your application does rather than the mechanics of parsing command line input. |
| 192 | + |
| 193 | +The `System.CommandLine` library offers several key benefits: |
| 194 | + |
| 195 | +- Automatic help text generation and validation |
| 196 | +- Support for POSIX and Windows command-line conventions |
| 197 | +- Built-in tab completion capabilities |
| 198 | +- Consistent parsing behavior across applications |
| 199 | + |
| 200 | +To add command line parsing capabilities, first add the `System.CommandLine` package. Add this directive after the existing package directive: |
| 201 | + |
| 202 | +```csharp |
| 203 | + |
| 204 | +``` |
| 205 | + |
| 206 | +> [!IMPORTANT] |
| 207 | +> |
| 208 | +> The version `2.0.0-beta6` was the latest version when this tutorial was last updated. If there is a newer version available, use the latest version to ensure you have the latest security packages. |
| 209 | +
|
| 210 | +Next, add the necessary using statements at the top of your file (after the `#!` and `#:package` directives): |
| 211 | + |
| 212 | +```csharp |
| 213 | +using System.CommandLine; |
| 214 | +using System.CommandLine.Parsing; |
| 215 | +``` |
| 216 | + |
| 217 | +Define the delay option and messages argument. In command-line applications, options typically begin with `--` (double dash) and can accept arguments. The `--delay` option accepts an integer argument that specifies the delay in milliseconds. The `messagesArgument` defines how any remaining tokens after options are parsed as text. Each token becomes a separate string in the array, but text can be quoted to include multiple words in one token. For example, `"This is one message"` becomes a single token, while `This is four tokens` becomes four separate tokens. Add the following code: |
| 218 | + |
| 219 | +```csharp |
| 220 | +Option<int> delayOption = new("--delay") |
| 221 | +{ |
| 222 | + Description = "Delay between lines, specified as milliseconds.", |
| 223 | + DefaultValueFactory = parseResult => 100 |
| 224 | +}; |
| 225 | + |
| 226 | +Argument<string[]> messagesArgument = new("Messages") |
| 227 | +{ |
| 228 | + Description = "Text to render." |
| 229 | +}; |
| 230 | +``` |
| 231 | + |
| 232 | +This application has only one root command, so you'll add the argument and option directly to the root command. Create a root command and configure it with the option and argument: |
| 233 | + |
| 234 | +```csharp |
| 235 | +RootCommand rootCommand = new("Ascii Art file-based program sample"); |
| 236 | + |
| 237 | +rootCommand.Options.Add(delayOption); |
| 238 | +rootCommand.Arguments.Add(messagesArgument); |
| 239 | +``` |
| 240 | + |
| 241 | +Parse the command line arguments and handle any errors. This validates the command line arguments and stores parsed arguments in the `ParseResult` object: |
| 242 | + |
| 243 | +```csharp |
| 244 | +ParseResult result = rootCommand.Parse(args); |
| 245 | +foreach (ParseError parseError in result.Errors) |
| 246 | +{ |
| 247 | + Console.Error.WriteLine(parseError.Message); |
| 248 | +} |
| 249 | +if (result.Errors.Count > 0) |
| 250 | +{ |
| 251 | + return 1; |
| 252 | +} |
| 253 | +``` |
| 254 | + |
| 255 | +## Use parsed command line results |
| 256 | + |
| 257 | +Now, let's finish the utility to use the parsed options and write the output. First, define a record to hold the parsed options. File-based apps can include type declarations, like records. They must be after all top-level statements and local functions. Add a `record` declaration to store the messages and the delay option value: |
| 258 | + |
| 259 | +```csharp |
| 260 | +public record AsciiMessageOptions(string[] Messages, int Delay); |
| 261 | +``` |
| 262 | + |
| 263 | +Now that you've declared the record to store those results, add a local function to process the parse results and store the values in an instance of the record. Add the following local function before the record declaration. This method handles both command line arguments and standard input, and returns a new record instance: |
| 264 | + |
| 265 | +```csharp |
| 266 | +async Task<AsciiMessageOptions> ProcessParseResults(ParseResult result) |
| 267 | +{ |
| 268 | + int delay = result.GetValue(delayOption); |
| 269 | + List<string> messages = [.. result.GetValue(messagesArgument) ?? Array.Empty<string>()]; |
| 270 | + |
| 271 | + if (messages.Count == 0) |
| 272 | + { |
| 273 | + while (Console.ReadLine() is string line && line.Length > 0) |
| 274 | + { |
| 275 | + Colorful.Console.WriteAscii(line); |
| 276 | + await Task.Delay(delay); |
| 277 | + } |
| 278 | + } |
| 279 | + return new ([..messages], delay); |
| 280 | +} |
| 281 | +``` |
| 282 | + |
| 283 | +Next, create a local function to write the ASCII art with the specified delay. This function writes each message in the record with the specified delay between each message: |
| 284 | + |
| 285 | +```csharp |
| 286 | +async Task WriteAsciiArt(AsciiMessageOptions options) |
| 287 | +{ |
| 288 | + foreach(string message in options.Messages) |
| 289 | + { |
| 290 | + Colorful.Console.WriteAscii(message); |
| 291 | + await Task.Delay(options.Delay); |
| 292 | + } |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | +Finally, replace the `if` clause you wrote earlier with the following code to write the process the command line arguments and write the output: |
| 297 | + |
| 298 | +```csharp |
| 299 | +var parsedArgs = await ProcessParseResults(result); |
| 300 | + |
| 301 | +await WriteAsciiArt(parsedArgs); |
| 302 | +return 0; |
| 303 | +``` |
| 304 | + |
| 305 | +## Test the final application |
| 306 | + |
| 307 | +Test the application by running several different commands. If you have trouble, here's the finished sample to compare with what you built: |
| 308 | + |
| 309 | +:::code language="csharp" source="./snippets/file-based-programs/AsciiArt.cs"::: |
| 310 | + |
| 311 | +## Next step -or- Related content |
| 312 | + |
| 313 | +-or- |
| 314 | + |
| 315 | +* [Related article title](link.md) |
| 316 | +* [Related article title](link.md) |
| 317 | +* [Related article title](link.md) |
| 318 | + |
| 319 | +<!-- Optional: Next step or Related content - H2 |
| 320 | +
|
| 321 | +A "Related content" section that lists links to |
| 322 | +1 to 3 articles the user might find helpful. |
| 323 | +
|
| 324 | +--> |
0 commit comments