Skip to content

Commit c527ec9

Browse files
chsienkiRikkiGibsonjjonescz
authored
Support scripts without extension (#49332)
Co-authored-by: Rikki Gibson <[email protected]> Co-authored-by: Jan Jones <[email protected]>
1 parent 1b55da9 commit c527ec9

File tree

3 files changed

+51
-6
lines changed

3 files changed

+51
-6
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ The command takes a path which can be either
4747
## Target path
4848

4949
The path passed to `dotnet run ./some/path.cs` is called *the target path*.
50-
The target path must be a file which has the `.cs` file extension.
50+
The target path must be a file which either has the `.cs` file extension,
51+
or a file whose contents start with `#!`.
5152
*The target directory* is the directory of the target file.
5253

5354
## Integration into the existing `dotnet run` command
@@ -57,7 +58,7 @@ specifically `file.cs` is passed as the first command-line argument to the targe
5758
We preserve this behavior to avoid a breaking change.
5859
The file-based build and run kicks in only when:
5960
- a project file cannot be found (in the current directory or via the `--project` option), and
60-
- if the target file exists and has the `.cs` file extension.
61+
- if the target file exists, and has the `.cs` file extension or contents that start with `#!`.
6162

6263
File-based programs are processed by `dotnet run` equivalently to project-based programs unless specified otherwise in this document.
6364
For example, the remaining command-line arguments after the first argument (the target path) are passed through to the target app

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,28 @@ public static void RemoveDirectivesFromFile(ImmutableArray<CSharpDirective> dire
874874

875875
public static bool IsValidEntryPointPath(string entryPointFilePath)
876876
{
877-
return entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) && File.Exists(entryPointFilePath);
877+
if (!File.Exists(entryPointFilePath))
878+
{
879+
return false;
880+
}
881+
882+
if (entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
883+
{
884+
return true;
885+
}
886+
887+
// Check if the first two characters are #!
888+
try
889+
{
890+
using var stream = File.OpenRead(entryPointFilePath);
891+
int first = stream.ReadByte();
892+
int second = stream.ReadByte();
893+
return first == '#' && second == '!';
894+
}
895+
catch
896+
{
897+
return false;
898+
}
878899
}
879900
}
880901

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,37 @@ Hello from App
246246
}
247247

248248
/// <summary>
249-
/// Only <c>.cs</c> files can be run without a project file,
250-
/// others fall back to normal <c>dotnet run</c> behavior.
249+
/// When a file is not a .cs file, we probe the first characters of the file for <c>#!</c>, and
250+
/// execute as a single file program if we find them.
251251
/// </summary>
252252
[Theory]
253253
[InlineData("Program")]
254254
[InlineData("Program.csx")]
255255
[InlineData("Program.vb")]
256-
public void NonCsFileExtension(string fileName)
256+
public void NonCsFileExtensionWithShebang(string fileName)
257+
{
258+
var testInstance = _testAssetsManager.CreateTestDirectory();
259+
File.WriteAllText(Path.Join(testInstance.Path, fileName), """
260+
#!/usr/bin/env dotnet
261+
Console.WriteLine("hello world");
262+
""");
263+
264+
new DotnetCommand(Log, "run", fileName)
265+
.WithWorkingDirectory(testInstance.Path)
266+
.Execute()
267+
.Should().Pass()
268+
.And.HaveStdOutContaining("hello world");
269+
}
270+
271+
/// <summary>
272+
/// When a file is not a .cs file, we probe the first characters of the file for <c>#!</c>, and
273+
/// fall back to normal <c>dotnet run</c> behavior if we don't find them.
274+
/// </summary>
275+
[Theory]
276+
[InlineData("Program")]
277+
[InlineData("Program.csx")]
278+
[InlineData("Program.vb")]
279+
public void NonCsFileExtensionWithNoShebang(string fileName)
257280
{
258281
var testInstance = _testAssetsManager.CreateTestDirectory();
259282
File.WriteAllText(Path.Join(testInstance.Path, fileName), s_program);

0 commit comments

Comments
 (0)