├── .nvmrc ├── test ├── test-configs │ ├── dir1 │ │ ├── dir2 │ │ │ └── dir3 │ │ │ │ └── .gitignore │ │ └── global.json │ ├── no-scripts │ │ └── global.json │ ├── malformed │ │ └── global.json │ └── script-names │ │ └── global.json ├── ProjectLoaderTests.Should_find_in_root.verified.txt ├── ProjectLoaderTests.Should_look_up_the_tree.verified.txt ├── CommandGroupRunnerTests.Should_run_a_single_script_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_run_a_single_script_isWindows=True.verified.txt ├── CommandBuilderTests.SetUpEnvironment_should_fall_back_to_cmd_on_windows.verified.txt ├── ProjectLoaderTests.Should_treat_script_names_as_lowercase.verified.txt ├── CommandBuilderTests.SetUpEnvironment_should_use_custom_shell_isWindows=False.verified.txt ├── CommandBuilderTests.SetUpEnvironment_should_use_custom_shell_isWindows=True.verified.txt ├── RunScriptCommandTests.RunResults.Should_handle_multiple_success_results.verified.txt ├── RunScriptCommandTests.RunResults.Should_handle_single_result_exitCode=0.verified.txt ├── RunScriptCommandTests.RunResults.Should_handle_single_result_exitCode=1.verified.txt ├── RunScriptCommandTests.RunResults.Should_handle_single_result_exitCode=13.verified.txt ├── CommandBuilderTests.SetUpEnvironment_should_default_to_comspec_env_var_only_on_windows_isWindows=False.verified.txt ├── CommandBuilderTests.SetUpEnvironment_should_default_to_comspec_env_var_only_on_windows_isWindows=True.verified.txt ├── CommandGroupRunnerTests.Should_run_with_a_post_script_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_run_with_a_post_script_isWindows=True.verified.txt ├── CommandGroupRunnerTests.Should_run_with_a_pre_script_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_run_with_a_pre_script_isWindows=True.verified.txt ├── CommandGroupRunnerTests.Should_return_first_error_hit_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_return_first_error_hit_isWindows=True.verified.txt ├── RunScriptCommandTests.RunResults.Should_handle_any_error_result_exitCode=1.verified.txt ├── RunScriptCommandTests.RunResults.Should_handle_any_error_result_exitCode=13.verified.txt ├── RunScriptCommandTests.RunResults.Should_handle_multiple_error_results.verified.txt ├── GlobalCommandsTests.Should_log_all_available_environment_variables.verified.txt ├── CommandGroupRunnerTests.Should_run_with_a_pre_and_post_script_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_run_with_a_pre_and_post_script_isWindows=True.verified.txt ├── CommandGroupRunnerTests.Should_emit_environment_variable_list_when_env_script_not_defined_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_emit_environment_variable_list_when_env_script_not_defined_isWindows=True.verified.txt ├── Integration │ ├── CommandBuilderTests.UnixPlatforms.Should_execute_single_script_in_shell_shellOverride=default.verified.txt │ ├── CommandBuilderTests.UnixPlatforms.Should_execute_single_script_in_shell_shellOverride=pwsh.verified.txt │ ├── CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=default.verified.txt │ ├── CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=pwsh.verified.txt │ ├── CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=cmd.exe.verified.txt │ ├── CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=bash.verified.txt │ └── CommandBuilderTests.cs ├── CommandGroupRunnerTests.Should_run_env_script_if_defined_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_run_env_script_if_defined_isWindows=True.verified.txt ├── ModuleInitializer.cs ├── UnixFactAttribute.cs ├── UnixTheoryAttribute.cs ├── WindowsFactAttribute.cs ├── WindowsTheoryAttribute.cs ├── Logging │ ├── GitHubActionsLogGroupTests.Should_escape_group_name.verified.txt │ └── GitHubActionsLogGroupTests.cs ├── .editorconfig ├── GlobalCommandsTests.Should_log_all_available_scripts.verified.txt ├── CommandGroupRunnerTests.Should_emit_environment_variable_list_and_pre_and_post_scripts_when_env_script_not_defined_isWindows=False.verified.txt ├── CommandGroupRunnerTests.Should_emit_environment_variable_list_and_pre_and_post_scripts_when_env_script_not_defined_isWindows=True.verified.txt ├── TestEnvironment.cs ├── ConsoleConverter.cs ├── Tests.csproj ├── ConsoleHelpersTests.cs ├── GlobalCommandsTests.cs ├── ProjectLoaderTests.cs ├── CommandBuilderTests.cs ├── ArgumentBuilderTests.cs ├── ValueStringBuilderTests.cs ├── RunScriptCommandTests.cs └── CommandGroupRunnerTests.cs ├── assets ├── icon.png ├── README.md └── icon.svg ├── .vscode ├── extensions.json └── settings.json ├── src ├── RunResult.cs ├── ScriptResult.cs ├── Logging │ ├── SilentLogGroup.cs │ ├── IConsoleWriterExtensions.cs │ └── GitHubActionsLogGroup.cs ├── EnvironmentVariables.cs ├── GlobalArguments.cs ├── Project.cs ├── IEnvironment.cs ├── RunScriptException.cs ├── GlobalOptions.cs ├── ProcessContext.cs ├── ICommandRunner.cs ├── ICommandGroupRunner.cs ├── EnvironmentWrapper.cs ├── IConsoleWriter.cs ├── ConsoleHelpers.cs ├── Serialization │ └── CaseInsensitiveDictionaryConverter.cs ├── Program.cs ├── GlobalCommands.cs ├── run-script.csproj ├── ProjectLoader.cs ├── CommandGroupRunner.cs ├── CommandBuilder.cs ├── StreamForwarder.cs ├── CommandRunner.cs ├── ValueStringBuilder.cs ├── ConsoleWriter.cs ├── RunScriptCommand.cs └── ArgumentBuilder.cs ├── .markdownlint.json ├── .github ├── workflows │ ├── fixup-commits.yml │ ├── dotnet-sdk-updater.yml │ ├── dependabot-auto-merge.yml │ ├── cleanup.yml │ ├── codeql-analysis.yml │ ├── release.yml │ └── ci.yml └── dependabot.yml ├── package.json ├── Directory.Build.targets ├── .config └── dotnet-tools.json ├── nuget.config ├── .gitattributes ├── analysis ├── sdk.editorconfig ├── idisposable.editorconfig └── roslynator.editorconfig ├── LICENSE ├── global.json ├── Directory.Build.props ├── docs └── README.md ├── run-script.sln ├── CHANGELOG.md ├── README.md ├── .editorconfig └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /test/test-configs/dir1/dir2/dir3/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test-configs/no-scripts/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": "1.2.3" 3 | } 4 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt0rted/dotnet-run-script/HEAD/assets/icon.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "mrmlnc.vscode-json5" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/RunResult.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | internal record RunResult(string Name, int ExitCode); 4 | -------------------------------------------------------------------------------- /src/ScriptResult.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | internal record ScriptResult(string Name, bool Exists); 4 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD024": false, 4 | "MD033": false, 5 | "MD045": false 6 | } 7 | -------------------------------------------------------------------------------- /test/test-configs/dir1/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "echo \"dir1\" && exit 1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/ProjectLoaderTests.Should_find_in_root.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Scripts: { 3 | test: echo "dir1" && exit 1 4 | } 5 | } -------------------------------------------------------------------------------- /test/ProjectLoaderTests.Should_look_up_the_tree.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Scripts: { 3 | test: echo "dir1" && exit 1 4 | } 5 | } -------------------------------------------------------------------------------- /test/test-configs/malformed/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "echo \"malformed\" && exit 1", 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations" : { 3 | "global.json" : "json5" 4 | }, 5 | "cSpell.words": [ 6 | "Xunit" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/test-configs/script-names/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "bUiLD": "build", 4 | "TEST": "test", 5 | "pack": "pack" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_a_single_script_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: clean, 4 | Cmd: echo clean 5 | }, 6 | {} 7 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_a_single_script_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: clean, 4 | Cmd: echo clean 5 | }, 6 | {} 7 | ] -------------------------------------------------------------------------------- /test/CommandBuilderTests.SetUpEnvironment_should_fall_back_to_cmd_on_windows.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Shell: cmd, 3 | IsCmd: true, 4 | WorkingDirectory: /test/path 5 | } -------------------------------------------------------------------------------- /test/ProjectLoaderTests.Should_treat_script_names_as_lowercase.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Scripts: { 3 | build: build, 4 | pack: pack, 5 | test: test 6 | } 7 | } -------------------------------------------------------------------------------- /test/CommandBuilderTests.SetUpEnvironment_should_use_custom_shell_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Shell: pwsh, 3 | IsCmd: false, 4 | WorkingDirectory: /test/path 5 | } -------------------------------------------------------------------------------- /test/CommandBuilderTests.SetUpEnvironment_should_use_custom_shell_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Shell: pwsh, 3 | IsCmd: false, 4 | WorkingDirectory: /test/path 5 | } -------------------------------------------------------------------------------- /src/Logging/SilentLogGroup.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript.Logging; 2 | 3 | internal sealed class SilentLogGroup : IDisposable 4 | { 5 | public void Dispose() 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/RunScriptCommandTests.RunResults.Should_handle_multiple_success_results.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false 5 | } -------------------------------------------------------------------------------- /test/RunScriptCommandTests.RunResults.Should_handle_single_result_exitCode=0.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false 5 | } -------------------------------------------------------------------------------- /test/RunScriptCommandTests.RunResults.Should_handle_single_result_exitCode=1.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false 5 | } -------------------------------------------------------------------------------- /test/RunScriptCommandTests.RunResults.Should_handle_single_result_exitCode=13.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false 5 | } -------------------------------------------------------------------------------- /test/CommandBuilderTests.SetUpEnvironment_should_default_to_comspec_env_var_only_on_windows_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Shell: sh, 3 | IsCmd: false, 4 | WorkingDirectory: /test/path 5 | } -------------------------------------------------------------------------------- /test/CommandBuilderTests.SetUpEnvironment_should_default_to_comspec_env_var_only_on_windows_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Shell: C:\WINDOWS\system32\cmd.exe, 3 | IsCmd: true, 4 | WorkingDirectory: /test/path 5 | } -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_with_a_post_script_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: test, 4 | Cmd: echo test 5 | }, 6 | { 7 | Name: posttest, 8 | Cmd: echo posttest 9 | }, 10 | {} 11 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_with_a_post_script_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: test, 4 | Cmd: echo test 5 | }, 6 | { 7 | Name: posttest, 8 | Cmd: echo posttest 9 | }, 10 | {} 11 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_with_a_pre_script_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: prebuild, 4 | Cmd: echo prebuild 5 | }, 6 | { 7 | Name: build, 8 | Cmd: echo build 9 | }, 10 | {} 11 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_with_a_pre_script_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: prebuild, 4 | Cmd: echo prebuild 5 | }, 6 | { 7 | Name: build, 8 | Cmd: echo build 9 | }, 10 | {} 11 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_return_first_error_hit_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: prepack, 4 | Cmd: echo pack 5 | }, 6 | { 7 | Name: pack, 8 | Cmd: echo pack 9 | }, 10 | {}, 11 | {} 12 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_return_first_error_hit_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: prepack, 4 | Cmd: echo pack 5 | }, 6 | { 7 | Name: pack, 8 | Cmd: echo pack 9 | }, 10 | {}, 11 | {} 12 | ] -------------------------------------------------------------------------------- /test/RunScriptCommandTests.RunResults.Should_handle_any_error_result_exitCode=1.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | ERROR: "test" exited with 1 7 | 8 | } -------------------------------------------------------------------------------- /test/RunScriptCommandTests.RunResults.Should_handle_any_error_result_exitCode=13.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | ERROR: "test" exited with 13 7 | 8 | } -------------------------------------------------------------------------------- /.github/workflows/fixup-commits.yml: -------------------------------------------------------------------------------- 1 | name: Block on fixup commits 2 | 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: read 7 | 8 | jobs: 9 | message: 10 | uses: xt0rted/.github/.github/workflows/fixup-commits.yml@main 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotnet-run-script", 3 | "private": true, 4 | "scripts": { 5 | "test": "markdownlint \"**/*.md\" --ignore node_modules" 6 | }, 7 | "devDependencies": { 8 | "markdownlint-cli": "^0.45.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/EnvironmentVariables.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | internal static class EnvironmentVariables 4 | { 5 | public static string GitHubActions => "GITHUB_ACTIONS"; 6 | 7 | public static string RunScriptChildProcess => "DOTNET_R_CHILDPROCESS"; 8 | } 9 | -------------------------------------------------------------------------------- /test/RunScriptCommandTests.RunResults.Should_handle_multiple_error_results.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | ERROR: "build" exited with 13 7 | ERROR: "test" exited with 99 8 | 9 | } -------------------------------------------------------------------------------- /src/GlobalArguments.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | public static class GlobalArguments 4 | { 5 | public static readonly Argument Scripts = new("scripts", "One or more scripts to run") 6 | { 7 | Arity = ArgumentArity.ZeroOrMore, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/GlobalCommandsTests.Should_log_all_available_environment_variables.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > env 7 | 8 | value1=value 1 9 | value2=value 2 10 | value3=value 3 11 | value4=value 4 12 | 13 | } -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_with_a_pre_and_post_script_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: prepack, 4 | Cmd: echo pack 5 | }, 6 | { 7 | Name: pack, 8 | Cmd: echo pack 9 | }, 10 | { 11 | Name: postpack, 12 | Cmd: echo pack 13 | }, 14 | {} 15 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_with_a_pre_and_post_script_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | Name: prepack, 4 | Cmd: echo pack 5 | }, 6 | { 7 | Name: pack, 8 | Cmd: echo pack 9 | }, 10 | { 11 | Name: postpack, 12 | Cmd: echo pack 13 | }, 14 | {} 15 | ] -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_emit_environment_variable_list_when_env_script_not_defined_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > env 7 | 8 | value1=value 1 9 | value2=value 2 10 | value3=value 3 11 | 12 | } -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_emit_environment_variable_list_when_env_script_not_defined_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > env 7 | 8 | value1=value 1 9 | value2=value 2 10 | value3=value 3 11 | 12 | } -------------------------------------------------------------------------------- /test/Integration/CommandBuilderTests.UnixPlatforms.Should_execute_single_script_in_shell_shellOverride=default.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > Using shell: sh 7 | 8 | 9 | > test 10 | > echo testing 11 | 12 | testing 13 | } -------------------------------------------------------------------------------- /test/Integration/CommandBuilderTests.UnixPlatforms.Should_execute_single_script_in_shell_shellOverride=pwsh.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > Using shell: pwsh 7 | 8 | 9 | > test 10 | > echo testing 11 | 12 | testing 13 | } -------------------------------------------------------------------------------- /test/Integration/CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=default.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > Using shell: cmd 7 | 8 | 9 | > test 10 | > echo testing 11 | 12 | testing 13 | } -------------------------------------------------------------------------------- /test/Integration/CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=pwsh.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > Using shell: pwsh 7 | 8 | 9 | > test 10 | > echo testing 11 | 12 | testing 13 | } -------------------------------------------------------------------------------- /test/Integration/CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=cmd.exe.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > Using shell: cmd.exe 7 | 8 | 9 | > test 10 | > echo testing 11 | 12 | testing 13 | } -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_env_script_if_defined_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | console: { 3 | IsErrorRedirected: false, 4 | IsInputRedirected: false, 5 | IsOutputRedirected: false 6 | }, 7 | commandRunners: [ 8 | { 9 | Name: env, 10 | Cmd: echo env 11 | }, 12 | {} 13 | ] 14 | } -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_run_env_script_if_defined_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | console: { 3 | IsErrorRedirected: false, 4 | IsInputRedirected: false, 5 | IsOutputRedirected: false 6 | }, 7 | commandRunners: [ 8 | { 9 | Name: env, 10 | Cmd: echo env 11 | }, 12 | {} 13 | ] 14 | } -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "run-script": { 6 | "version": "0.6.0", 7 | "commands": [ 8 | "r" 9 | ] 10 | }, 11 | "rimraf": { 12 | "version": "0.3.1", 13 | "commands": [ 14 | "rimraf" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/Integration/CommandBuilderTests.WindowsPlatform.Should_execute_single_script_in_shell_shellOverride=bash.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | > Using shell: C:\Program Files\Git\bin\bash.exe 7 | 8 | 9 | > test 10 | > echo testing 11 | 12 | testing 13 | } -------------------------------------------------------------------------------- /test/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Runtime.CompilerServices; 4 | 5 | public static class ModuleInitializer 6 | { 7 | [ModuleInitializer] 8 | public static void Init() 9 | { 10 | VerifierSettings.AddExtraSettings(settings => settings.Converters.Add(new ConsoleConverter())); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/UnixFactAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | public class UnixFactAttribute : FactAttribute 6 | { 7 | public UnixFactAttribute() 8 | { 9 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 10 | { 11 | Skip = "Ignored on Windows"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Project.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | using RunScript.Serialization; 6 | 7 | public class Project 8 | { 9 | public string? ScriptShell { get; set; } 10 | 11 | [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] 12 | public Dictionary? Scripts { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /test/UnixTheoryAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | public class UnixTheoryAttribute : TheoryAttribute 6 | { 7 | public UnixTheoryAttribute() 8 | { 9 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 10 | { 11 | Skip = "Ignored on Windows"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/WindowsFactAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | public class WindowsFactAttribute : FactAttribute 6 | { 7 | public WindowsFactAttribute() 8 | { 9 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 10 | { 11 | Skip = "Ignored on Unix"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/WindowsTheoryAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | public class WindowsTheoryAttribute : TheoryAttribute 6 | { 7 | public WindowsTheoryAttribute() 8 | { 9 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 10 | { 11 | Skip = "Ignored on Unix"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/IEnvironment.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections; 4 | 5 | internal interface IEnvironment 6 | { 7 | string CurrentDirectory { get; } 8 | 9 | bool IsWindows { get; } 10 | 11 | string? GetEnvironmentVariable(string variable); 12 | 13 | IDictionary GetEnvironmentVariables(); 14 | 15 | void SetEnvironmentVariable(string variable, string? value); 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.jpg binary 5 | *.png binary 6 | *.gif binary 7 | 8 | *.cs text diff=csharp 9 | *.csproj text 10 | *.sln text eol=crlf 11 | 12 | *.verified.txt text eol=lf working-tree-encoding=UTF-8 13 | *.verified.xml text eol=lf working-tree-encoding=UTF-8 14 | *.verified.json text eol=lf working-tree-encoding=UTF-8 15 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | The source for the logo is available on [Figma](https://www.figma.com/file/FBPemu54a3c2fWO55x8bDr/open-source-projects?node-id=311%3A2). 4 | It's the v1.0.6 `console` Heroicon outline variant. 5 | 6 | The color used is `#6b7280` (`gray-500` from Tailwind CSS). 7 | 8 | Format | Preview 9 | :-- | :--: 10 | `.png` | ![Icon preview](icon.png) 11 | `.svg` | ![Icon preview](icon.svg) 12 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-sdk-updater.yml: -------------------------------------------------------------------------------- 1 | name: .NET SDK updater 2 | 3 | on: 4 | schedule: 5 | - cron: "0 12 * * 1-5" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update: 10 | uses: xt0rted/.github/.github/workflows/dotnet-sdk-updater.yml@main 11 | secrets: 12 | DOTNET_UPDATER_APP_ID: ${{ secrets.DOTNET_UPDATER_APP_ID }} 13 | DOTNET_UPDATER_PRIVATE_KEY: ${{ secrets.DOTNET_UPDATER_PRIVATE_KEY }} 14 | -------------------------------------------------------------------------------- /src/RunScriptException.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | public class RunScriptException : Exception 4 | { 5 | public RunScriptException() 6 | { 7 | } 8 | 9 | public RunScriptException(string? message) 10 | : base(message) 11 | { 12 | } 13 | 14 | public RunScriptException(string? message, Exception? innerException) 15 | : base(message, innerException) 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/Logging/GitHubActionsLogGroupTests.Should_escape_group_name.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | ::group::dotnet r plain 7 | ::endgroup:: 8 | ::group::dotnet r percent: %2525 9 | ::endgroup:: 10 | ::group::dotnet r carriage return: %0D 11 | ::endgroup:: 12 | ::group::dotnet r line feed: %0A 13 | ::endgroup:: 14 | ::group::dotnet r everything: %25%0D%0A 15 | ::endgroup:: 16 | 17 | } -------------------------------------------------------------------------------- /test/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | # CA1861: Avoid constant arrays as arguments 3 | dotnet_diagnostic.CA1861.severity = none 4 | 5 | # IDISP001: Dispose created 6 | dotnet_diagnostic.IDISP001.severity = none 7 | 8 | # IDE0022: Use expression body for methods 9 | dotnet_diagnostic.IDE0022.severity = none 10 | 11 | # RCS1046: Asynchronous method name should end with 'Async' 12 | dotnet_diagnostic.RCS1046.severity = none 13 | 14 | # RCS1016: Use block body or expression body. 15 | dotnet_diagnostic.RCS1016.severity = none 16 | -------------------------------------------------------------------------------- /test/GlobalCommandsTests.Should_log_all_available_scripts.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | IsErrorRedirected: false, 3 | IsInputRedirected: false, 4 | IsOutputRedirected: false, 5 | Out: 6 | Available via `dotnet r`: 7 | 8 | clean 9 | echo clean 10 | 11 | prebuild 12 | echo prebuild 13 | 14 | build 15 | echo build 16 | 17 | test 18 | echo test 19 | 20 | posttest 21 | echo posttest 22 | 23 | prepack 24 | echo pack 25 | 26 | pack 27 | echo pack 28 | 29 | postpack 30 | echo pack 31 | 32 | 33 | } -------------------------------------------------------------------------------- /src/GlobalOptions.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | public static class GlobalOptions 4 | { 5 | public static readonly Option IfPresent = new("--if-present", "Don't exit with an error code if the script isn't found"); 6 | 7 | public static readonly Option ScriptShell = new("--script-shell", "The shell to use when running scripts (cmd, pwsh, sh, etc.)") 8 | { 9 | ArgumentHelpName = "shell", 10 | }; 11 | 12 | public static readonly Option Verbose = new(new[] { "-v", "--verbose" }, "Enable verbose output"); 13 | } 14 | -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_emit_environment_variable_list_and_pre_and_post_scripts_when_env_script_not_defined_isWindows=False.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | console: { 3 | IsErrorRedirected: false, 4 | IsInputRedirected: false, 5 | IsOutputRedirected: false, 6 | Out: 7 | > env 8 | 9 | value1=value 1 10 | value2=value 2 11 | value3=value 3 12 | 13 | }, 14 | commandRunners: [ 15 | { 16 | Name: preenv, 17 | Cmd: echo preenv 18 | }, 19 | { 20 | Name: postenv, 21 | Cmd: echo postenv 22 | }, 23 | {} 24 | ] 25 | } -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.Should_emit_environment_variable_list_and_pre_and_post_scripts_when_env_script_not_defined_isWindows=True.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | console: { 3 | IsErrorRedirected: false, 4 | IsInputRedirected: false, 5 | IsOutputRedirected: false, 6 | Out: 7 | > env 8 | 9 | value1=value 1 10 | value2=value 2 11 | value3=value 3 12 | 13 | }, 14 | commandRunners: [ 15 | { 16 | Name: preenv, 17 | Cmd: echo preenv 18 | }, 19 | { 20 | Name: postenv, 21 | Cmd: echo postenv 22 | }, 23 | {} 24 | ] 25 | } -------------------------------------------------------------------------------- /src/ProcessContext.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | internal sealed class ProcessContext 4 | { 5 | private ProcessContext(string shell, bool isCmd, string workingDirectory) 6 | { 7 | Shell = shell; 8 | IsCmd = isCmd; 9 | WorkingDirectory = workingDirectory; 10 | } 11 | 12 | public string Shell { get; } 13 | 14 | public bool IsCmd { get; } 15 | 16 | public string WorkingDirectory { get; } 17 | 18 | public static ProcessContext Create(string shell, bool isCmd, string workingDirectory) 19 | => new(shell, isCmd, workingDirectory); 20 | } 21 | -------------------------------------------------------------------------------- /src/ICommandRunner.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | /// 4 | /// Execute scripts in a separate process. 5 | /// 6 | internal interface ICommandRunner 7 | { 8 | /// 9 | /// Run the specified command. 10 | /// 11 | /// The name of the command to run. 12 | /// The command to run. 13 | /// Any arguments to pass to the command. 14 | /// The exit code of the command. 15 | Task RunAsync(string name, string cmd, string[]? args); 16 | } 17 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ICommandGroupRunner.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | /// 4 | /// Execute a script and any pre & post scripts. 5 | /// 6 | internal interface ICommandGroupRunner 7 | { 8 | /// 9 | /// Run the script and it's pre & post scripts. 10 | /// 11 | /// The name of the script to run. 12 | /// 13 | /// Any arguments to pass to the executing script. 14 | /// Will not be passed to the pre or post scripts. 15 | /// 16 | /// 0 if there were no errors; otherwise the exit code of the failed script. 17 | Task RunAsync(string name, string[]? scriptArgs); 18 | } 19 | -------------------------------------------------------------------------------- /src/EnvironmentWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections; 4 | using System.Runtime.InteropServices; 5 | 6 | internal class EnvironmentWrapper : IEnvironment 7 | { 8 | public string CurrentDirectory => Environment.CurrentDirectory; 9 | 10 | public bool IsWindows { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 11 | 12 | public string? GetEnvironmentVariable(string variable) 13 | => Environment.GetEnvironmentVariable(variable); 14 | 15 | public IDictionary GetEnvironmentVariables() 16 | => Environment.GetEnvironmentVariables(); 17 | 18 | public void SetEnvironmentVariable(string variable, string? value) 19 | => Environment.SetEnvironmentVariable(variable, value); 20 | } 21 | -------------------------------------------------------------------------------- /src/IConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | internal interface IConsoleWriter 4 | { 5 | void Raw(string? message); 6 | 7 | void VerboseBanner(); 8 | 9 | void BlankLine(); 10 | 11 | void BlankLineVerbose(); 12 | 13 | void Line(string? message, params object?[] args); 14 | 15 | void LineVerbose(string? message = null, params object?[] args); 16 | 17 | void SecondaryLine(string? message, params object?[] args); 18 | void VerboseSecondaryLine(string? message, params object?[] args); 19 | 20 | void Banner(params string?[] messages); 21 | 22 | void Error(string? message, params object?[] args); 23 | 24 | string? ColorText(ConsoleColor color, int value); 25 | string? ColorText(ConsoleColor color, string? value); 26 | } 27 | -------------------------------------------------------------------------------- /src/Logging/IConsoleWriterExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript.Logging; 2 | 3 | internal static class IConsoleWriterExtensions 4 | { 5 | public static IDisposable Group(this IConsoleWriter writer, IEnvironment environment, string name) 6 | { 7 | var isRunningOnActions = string.Equals( 8 | environment.GetEnvironmentVariable(EnvironmentVariables.GitHubActions), 9 | "true", 10 | StringComparison.OrdinalIgnoreCase); 11 | 12 | var isChildProcess = string.Equals( 13 | environment.GetEnvironmentVariable(EnvironmentVariables.RunScriptChildProcess), 14 | "true", 15 | StringComparison.OrdinalIgnoreCase); 16 | 17 | return isRunningOnActions && !isChildProcess 18 | ? new GitHubActionsLogGroup(writer, name) 19 | : new SilentLogGroup(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Logging/GitHubActionsLogGroup.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript.Logging; 2 | 3 | internal sealed class GitHubActionsLogGroup : IDisposable 4 | { 5 | private readonly IConsoleWriter _writer; 6 | 7 | public GitHubActionsLogGroup(IConsoleWriter writer, string name) 8 | { 9 | _writer = writer ?? throw new ArgumentNullException(nameof(writer)); 10 | 11 | _writer.Raw("::group::" + EscapeData("dotnet r " + name) + Environment.NewLine); 12 | } 13 | 14 | public void Dispose() 15 | => _writer.Raw("::endgroup::" + Environment.NewLine); 16 | 17 | private static string EscapeData(string value) 18 | => value 19 | .Replace("%", "%25", StringComparison.OrdinalIgnoreCase) 20 | .Replace("\r", "%0D", StringComparison.OrdinalIgnoreCase) 21 | .Replace("\n", "%0A", StringComparison.OrdinalIgnoreCase); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | 3 | on: pull_request_target 4 | 5 | permissions: 6 | contents: read 7 | pull-requests: read 8 | 9 | jobs: 10 | dependabot: 11 | uses: xt0rted/.github/.github/workflows/dependabot-auto-merge.yml@main 12 | secrets: 13 | GITHUB_APP_ID: ${{ secrets.DEPENDAMERGE_APP_ID }} 14 | GITHUB_APP_PRIVATE_KEY: ${{ secrets.DEPENDAMERGE_PRIVATE_KEY }} 15 | with: 16 | allowed-groups: | 17 | { 18 | "github_actions": [ 19 | "github-actions", 20 | "my-actions", 21 | ], 22 | "nuget": [ 23 | "analyzers", 24 | "testing", 25 | ], 26 | } 27 | allowed-packages: | 28 | { 29 | "nuget": [ 30 | "rimraf", 31 | "run-script", 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /test/TestEnvironment.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections; 4 | using System.Runtime.InteropServices; 5 | 6 | internal class TestEnvironment : IEnvironment 7 | { 8 | private readonly Dictionary _variables = new(StringComparer.OrdinalIgnoreCase); 9 | 10 | public TestEnvironment(string? currentDirectory = null, bool? isWindows = null) 11 | { 12 | CurrentDirectory = currentDirectory ?? AttributeReader.GetProjectDirectory(GetType().Assembly); 13 | IsWindows = isWindows ?? RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 14 | } 15 | 16 | public string CurrentDirectory { get; } 17 | 18 | public bool IsWindows { get; } 19 | 20 | public string? GetEnvironmentVariable(string variable) 21 | => _variables.TryGetValue(variable, out var value) ? value : null; 22 | 23 | public IDictionary GetEnvironmentVariables() 24 | => _variables; 25 | 26 | public void SetEnvironmentVariable(string variable, string? value) 27 | => _variables[variable] = value; 28 | } 29 | -------------------------------------------------------------------------------- /analysis/sdk.editorconfig: -------------------------------------------------------------------------------- 1 | is_global = true 2 | 3 | # 4 | # Rules 5 | 6 | # CA1062: Validate arguments of public methods 7 | dotnet_diagnostic.CA1062.severity = error 8 | 9 | # CA1303: Do not pass literals as localized parameters 10 | dotnet_diagnostic.CA1303.severity = error 11 | 12 | # CA1304: Specify CultureInfo 13 | dotnet_diagnostic.CA1304.severity = error 14 | 15 | # CA1305: Specify IFormatProvider 16 | dotnet_diagnostic.CA1305.severity = error 17 | 18 | # CA1307: Specify StringComparison for clarity 19 | dotnet_diagnostic.CA1307.severity = error 20 | 21 | # CA1308: Normalize strings to uppercase 22 | dotnet_diagnostic.CA1308.severity = error 23 | 24 | # CA1309: Use ordinal StringComparison 25 | dotnet_diagnostic.CA1309.severity = error 26 | 27 | # CA1310: Specify StringComparison for correctness 28 | dotnet_diagnostic.CA1310.severity = error 29 | 30 | # CA1838: Avoid StringBuilder parameters for P/Invokes 31 | dotnet_diagnostic.CA1838.severity = error 32 | 33 | # CA2101: Specify marshalling for P/Invoke string arguments 34 | dotnet_diagnostic.CA2101.severity = error 35 | -------------------------------------------------------------------------------- /.github/workflows/cleanup.yml: -------------------------------------------------------------------------------- 1 | name: Cleanup 2 | 3 | on: 4 | schedule: 5 | - cron: "0 6 * * 1" 6 | workflow_dispatch: 7 | 8 | env: 9 | package_name: run-script 10 | 11 | jobs: 12 | gpr: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v5.0.0 18 | 19 | - name: Get project version 20 | id: project 21 | run: | 22 | _version="$(jq -r '.tools."${{ env.package_name }}".version' ./.config/dotnet-tools.json)" 23 | _version="${_version//'.'/'\.'}" 24 | _version="^${_version}\$" 25 | echo "version=${_version}" >> $GITHUB_OUTPUT 26 | 27 | # Keep the last 13 versions as well as the version used by the project itself 28 | - uses: actions/delete-package-versions@v5.0.0 29 | with: 30 | package-name: ${{ env.package_name }} 31 | package-type: nuget 32 | delete-only-pre-release-versions: true 33 | min-versions-to-keep: 13 34 | ignore-versions: ${{ steps.project.outputs.version }} 35 | -------------------------------------------------------------------------------- /test/Logging/GitHubActionsLogGroupTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript.Logging; 2 | 3 | using System.CommandLine.IO; 4 | using System.CommandLine.Rendering; 5 | 6 | [Trait("category", "unit")] 7 | public class GitHubActionsLogGroupTests 8 | { 9 | [Fact] 10 | public async Task Should_escape_group_name() 11 | { 12 | // Given 13 | var console = new TestConsole(); 14 | var consoleFormatProvider = new ConsoleFormatInfo 15 | { 16 | SupportsAnsiCodes = false, 17 | }; 18 | var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: false); 19 | 20 | // When 21 | createGroup("plain"); 22 | createGroup("percent: %25"); 23 | createGroup("carriage return: \r"); 24 | createGroup("line feed: \n"); 25 | createGroup("everything: %\r\n"); 26 | 27 | // Then 28 | await Verify(console); 29 | 30 | void createGroup(string groupName) 31 | { 32 | using var _ = new GitHubActionsLogGroup(consoleWriter, groupName); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Brian Surowiec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ConsoleHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.CommandLine.Rendering; 4 | 5 | internal static class ConsoleHelpers 6 | { 7 | public static ConsoleFormatInfo FormatInfo(IEnvironment environment) 8 | => ConsoleFormatInfo.ReadOnly(FormatInfoBuilder(environment)); 9 | 10 | private static ConsoleFormatInfo FormatInfoBuilder(IEnvironment environment) 11 | { 12 | var consoleFormatProvider = new ConsoleFormatInfo 13 | { 14 | SupportsAnsiCodes = ConsoleFormatInfo.CurrentInfo.SupportsAnsiCodes, 15 | }; 16 | 17 | if (environment.GetEnvironmentVariable("NO_COLOR") is not null) 18 | { 19 | consoleFormatProvider.SupportsAnsiCodes = false; 20 | 21 | return consoleFormatProvider; 22 | } 23 | 24 | var envVar = environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION"); 25 | 26 | if (envVar is not null) 27 | { 28 | consoleFormatProvider.SupportsAnsiCodes = envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase); 29 | } 30 | 31 | return consoleFormatProvider; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/ConsoleConverter.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.CommandLine; 4 | 5 | public class ConsoleConverter : WriteOnlyJsonConverter 6 | { 7 | public override void Write(VerifyJsonWriter writer, IConsole value) 8 | { 9 | if (writer is null) throw new ArgumentNullException(nameof(writer)); 10 | if (value is null) throw new ArgumentNullException(nameof(value)); 11 | 12 | var errorOutput = value.Error.ToString(); 13 | var output = value.Out.ToString(); 14 | 15 | writer.WriteStartObject(); 16 | writer.WriteMember(value, value.IsErrorRedirected, nameof(value.IsErrorRedirected)); 17 | writer.WriteMember(value, value.IsInputRedirected, nameof(value.IsInputRedirected)); 18 | writer.WriteMember(value, value.IsOutputRedirected, nameof(value.IsOutputRedirected)); 19 | 20 | if (!string.IsNullOrEmpty(errorOutput)) 21 | { 22 | writer.WriteMember(value, errorOutput, nameof(value.Error)); 23 | } 24 | 25 | if (!string.IsNullOrEmpty(output)) 26 | { 27 | writer.WriteMember(value, output, nameof(value.Out)); 28 | } 29 | 30 | writer.WriteEndObject(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Serialization/CaseInsensitiveDictionaryConverter.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript.Serialization; 2 | 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | internal class CaseInsensitiveDictionaryConverter : JsonConverter> 7 | { 8 | public override Dictionary Read( 9 | ref Utf8JsonReader reader, 10 | Type typeToConvert, 11 | JsonSerializerOptions options) 12 | { 13 | var dict = (Dictionary)JsonSerializer 14 | .Deserialize(ref reader, typeToConvert, options)!; 15 | 16 | #pragma warning disable CA1308 // Normalize strings to uppercase 17 | return dict.ToDictionary( 18 | i => i.Key.ToLowerInvariant(), 19 | i => i.Value, 20 | StringComparer.OrdinalIgnoreCase); 21 | #pragma warning restore CA1308 // Normalize strings to uppercase 22 | } 23 | 24 | public override void Write( 25 | Utf8JsonWriter writer, 26 | Dictionary value, 27 | JsonSerializerOptions options) 28 | => JsonSerializer.Serialize( 29 | writer, 30 | value, 31 | value.GetType(), 32 | options); 33 | } 34 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine.Builder; 2 | using System.CommandLine.Parsing; 3 | 4 | using RunScript; 5 | 6 | var environment = new EnvironmentWrapper(); 7 | var consoleFormatProvider = ConsoleHelpers.FormatInfo(environment); 8 | var rootCommand = new RunScriptCommand( 9 | environment, 10 | consoleFormatProvider, 11 | environment.CurrentDirectory); 12 | 13 | var parser = new CommandLineBuilder(rootCommand) 14 | .UseVersionOption() 15 | .UseHelp() 16 | .UseEnvironmentVariableDirective() 17 | .UseParseDirective() 18 | .UseSuggestDirective() 19 | .RegisterWithDotnetSuggest() 20 | .UseParseErrorReporting() 21 | .UseExceptionHandler((ex, ctx) => 22 | { 23 | var verbose = ctx.ParseResult.HasOption(GlobalOptions.Verbose); 24 | var writer = new ConsoleWriter(ctx.Console, consoleFormatProvider, verbose); 25 | 26 | if (verbose) 27 | { 28 | writer.Error(ex.ToString()); 29 | } 30 | else 31 | { 32 | writer.Error(ex.Message); 33 | } 34 | }) 35 | .CancelOnProcessTermination() 36 | .EnableLegacyDoubleDashBehavior() 37 | .Build(); 38 | 39 | var parseResult = parser.Parse(args); 40 | 41 | return await parseResult.InvokeAsync(); 42 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.413" 4 | }, 5 | "scripts": { 6 | // project scripts 7 | "clean": "dotnet r clean:*", 8 | "clean:artifacts": "dotnet rimraf artifacts", 9 | "clean:bin": "dotnet rimraf **/bin **/obj", 10 | 11 | "build": "dotnet build", 12 | 13 | "test": "dotnet test", 14 | 15 | "test:unit": "dotnet r test -- --filter \"category=unit\"", 16 | "test:int": "dotnet r test -- --filter \"category=integration\"", 17 | 18 | "prepack": "dotnet r clean:artifacts", 19 | "pack": "dotnet pack --output \"./artifacts\"", 20 | 21 | "build:release": "dotnet r build -- --configuration Release", 22 | "test:release": "dotnet r test -- --configuration Release", 23 | "pack:release": "dotnet r pack -- --configuration Release", 24 | 25 | // integration tests 26 | "integration:ci": "dotnet r clean build && dotnet test --no-build", 27 | 28 | // test scripts 29 | "info": "dotnet r dotnet:version dotnet:info", 30 | "dotnet:info": "dotnet --info", 31 | "dotnet:version": "dotnet --version", 32 | "prestuff": "pwsh -c echo \"`u{001b}[34mpre-stuff`u{001b}[0m\"", 33 | "stuff": "pwsh -c echo \"`u{001b}[35mstuff`u{001b}[0m\"", 34 | "poststuff": "pwsh -c echo \"`u{001b}[36mpost-stuff`u{001b}[0m\"" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: "25 0 * * 5" 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | env: 17 | DOTNET_NOLOGO: true 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | 23 | runs-on: ubuntu-latest 24 | 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [csharp] 34 | 35 | steps: 36 | - name: Check out repository 37 | uses: actions/checkout@v5.0.0 38 | 39 | - name: Set up .NET 40 | uses: xt0rted/setup-dotnet@v1.5.0 41 | with: 42 | source-url: https://nuget.pkg.github.com/xt0rted/index.json 43 | nuget_auth_token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | 50 | - name: Autobuild 51 | uses: github/codeql-action/autobuild@v3 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | registries: 4 | nuget-github: 5 | type: nuget-feed 6 | url: https://nuget.pkg.github.com/xt0rted/index.json 7 | token: ${{ secrets.GPR_READ_TOKEN }} 8 | 9 | updates: 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | groups: 15 | github-actions: 16 | patterns: 17 | - "actions/*" 18 | my-actions: 19 | patterns: 20 | - "xt0rted/*" 21 | 22 | - package-ecosystem: "npm" 23 | directory: "/" 24 | versioning-strategy: "increase" 25 | schedule: 26 | interval: "weekly" 27 | open-pull-requests-limit: 99 28 | 29 | - package-ecosystem: "nuget" 30 | directory: "/" 31 | registries: "*" 32 | schedule: 33 | interval: "weekly" 34 | groups: 35 | analyzers: 36 | patterns: 37 | - "IDisposableAnalyzers" 38 | - "Roslynator.*" 39 | system-commandline: 40 | patterns: 41 | - "System.CommandLine" 42 | - "System.CommandLine.*" 43 | testing: 44 | patterns: 45 | - "FakeItEasy" 46 | - "FakeItEasy.*" 47 | - "GitHubActionsTestLogger" 48 | - "Microsoft.NET.Test.Sdk" 49 | - "Shouldly" 50 | - "Verify.Xunit" 51 | - "xunit" 52 | - "xunit.*" 53 | -------------------------------------------------------------------------------- /test/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RunScript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers 22 | 23 | 24 | 25 | 26 | 27 | 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | latest 6 | enable 7 | enable 8 | false 9 | true 10 | 11 | 12 | 13 | 0.6.0 14 | Brian Surowiec 15 | 16 | 17 | 18 | true 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers 29 | 30 | 31 | all 32 | runtime; build; native; contentfiles; analyzers 33 | 34 | 35 | all 36 | runtime; build; native; contentfiles; analyzers 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/ConsoleHelpersTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | [Trait("category", "unit")] 4 | public class ConsoleHelpersTests 5 | { 6 | [Theory] 7 | [InlineData("1")] 8 | [InlineData("true")] 9 | [InlineData("TrUe")] 10 | [InlineData("TRUE")] 11 | [InlineData("0")] 12 | [InlineData("false")] 13 | [InlineData("FaLsE")] 14 | [InlineData("FALSE")] 15 | [InlineData("no")] 16 | [InlineData("anything else")] 17 | public void FormatInfoBuilder_should_support_NO_COLOR_env_var(string value) 18 | { 19 | // Given 20 | var environment = new TestEnvironment(); 21 | 22 | environment.SetEnvironmentVariable("NO_COLOR", value); 23 | 24 | // When 25 | var result = ConsoleHelpers.FormatInfo(environment); 26 | 27 | // Then 28 | result.SupportsAnsiCodes.ShouldBeFalse(); 29 | } 30 | 31 | [Theory] 32 | [InlineData("1", true)] 33 | [InlineData("true", true)] 34 | [InlineData("TrUe", true)] 35 | [InlineData("TRUE", true)] 36 | [InlineData("0", false)] 37 | [InlineData("false", false)] 38 | [InlineData("FaLsE", false)] 39 | [InlineData("FALSE", false)] 40 | [InlineData("no", false)] 41 | [InlineData("anything else", false)] 42 | public void FormatInfoBuilder_should_support_DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION_env_var(string value, bool expected) 43 | { 44 | // Given 45 | var environment = new TestEnvironment(); 46 | 47 | environment.SetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", value); 48 | 49 | // When 50 | var result = ConsoleHelpers.FormatInfo(environment); 51 | 52 | // Then 53 | result.SupportsAnsiCodes.ShouldBe(expected); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/GlobalCommands.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | internal static class GlobalCommands 4 | { 5 | /// 6 | /// The help command that lists all the scripts available in the global.json. 7 | /// 8 | /// The console logger instance to use. 9 | /// The project's scripts. 10 | public static void PrintAvailableScripts(IConsoleWriter writer, IDictionary scripts) 11 | { 12 | writer.Line("Available via `{0}`:", writer.ColorText(ConsoleColor.Blue, "dotnet r")); 13 | writer.BlankLine(); 14 | 15 | foreach (var script in scripts.Keys) 16 | { 17 | writer.Line(" {0}", script); 18 | writer.SecondaryLine(" {0}", scripts[script]); 19 | writer.BlankLine(); 20 | } 21 | } 22 | 23 | /// 24 | /// Custom "script" that lists all available environment variables that will be available to the executing scripts. 25 | /// 26 | /// The console logger instance to use. 27 | /// The environment wrapper to use. 28 | /// 29 | public static void PrintEnvironmentVariables(IConsoleWriter writer, IEnvironment environment) 30 | { 31 | ArgumentNullException.ThrowIfNull(environment); 32 | 33 | writer.Banner("env"); 34 | 35 | foreach (var (key, value) in environmentVariables(environment).OrderBy(v => v.key, StringComparer.InvariantCulture)) 36 | { 37 | writer.Line("{0}={1}", key, value); 38 | } 39 | 40 | static IEnumerable<(string key, string value)> environmentVariables(IEnvironment environment) 41 | { 42 | var variables = environment.GetEnvironmentVariables(); 43 | 44 | foreach (var key in variables.Keys) 45 | { 46 | yield return new((string)key!, (string)variables[key!]!); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | After many years using `npm` it always bothered me that there wasn't an easy way to define `build`, `test`, and `package` scripts for my .NET projects. 4 | I'd usually end up using PowerShell scripts in a `/scripts` folder or a tool like [Cake](https://cakebuild.net/), both of which can be cumbersome to discover and use, especially for newcomers to a project who most likely aren't familiar with them. 5 | 6 | When .NET Core introduced the `dotnet` cli it became easier to do these things, but you still needed to pass additional arguments to build in release mode or put test results in a common folder to upload to Codecov or Coveralls. 7 | 8 | A great example of this is collecting code coverage of unit tests with [Coverlet](https://github.com/coverlet-coverage/coverlet). 9 | 10 | ```shell 11 | > dotnet test --configuration Debug --verbosity minimal --no-build --collect:"XPlat Code Coverage" --results-directory "./coverage" 12 | ``` 13 | 14 | Now this can be configured in your `global.json` like so: 15 | 16 | ```json 17 | { 18 | "scripts": { 19 | "test": "dotnet test --configuration Debug --verbosity minimal --no-build", 20 | "test:coverage": "dotnet r test -- --collect:\\\"XPlat Code Coverage\\\" --results-directory \"./.codecoverage\"" 21 | } 22 | } 23 | ``` 24 | 25 | Now a basic test run can be done by calling: 26 | 27 | ```shell 28 | > dotnet r test 29 | ``` 30 | 31 | Or code coverage can be collected by calling: 32 | 33 | ```shell 34 | > dotnet r test:coverage 35 | ``` 36 | 37 | ## What this isn't 38 | 39 | This tool is not a replacement for a more robust build tool like [Cake](https://cakebuild.net/) or [Fake](https://fake.build/). 40 | Instead it's meant to be used as a way to more easily call them by defining a common set of commands like `clean`, `build`, `test`, `package`, or `publish` that can be added to all of your projects without the user knowing, or caring, how they work or what they do. 41 | By defining them in the project's `global.json` they're easy to find, and running `dotnet r` or `dotnet r --help` will give a list of available scripts so you don't have to go looking for them. 42 | -------------------------------------------------------------------------------- /src/run-script.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RunScript 5 | Exe 6 | true 7 | embedded 8 | true 9 | true 10 | true 11 | r 12 | 13 | 14 | 15 | dotnet tool to run arbitrary commands from a project's "scripts" object 16 | true 17 | true 18 | icon.png 19 | MIT 20 | README.md 21 | See https://github.com/xt0rted/dotnet-run-script/blob/main/CHANGELOG.md for more info 22 | dotnet, tool, cli, build, scripts 23 | true 24 | main 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | assets 39 | 40 | 41 | assets 42 | 43 | 44 | assets 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/ProjectLoader.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Text.Json; 4 | 5 | internal static class ProjectLoader 6 | { 7 | private static readonly JsonSerializerOptions _jsonOptions = new() 8 | { 9 | PropertyNameCaseInsensitive = true, 10 | ReadCommentHandling = JsonCommentHandling.Skip, 11 | }; 12 | 13 | public static async Task<(Project, string)> LoadAsync(string executingDirectory) 14 | { 15 | var jsonPath = CheckFolderForFile(executingDirectory, "global.json"); 16 | 17 | if (jsonPath is null) 18 | { 19 | throw new RunScriptException("No global.json found in folder path"); 20 | } 21 | 22 | var rootPath = Path.GetDirectoryName(jsonPath)!; 23 | var workingDirectory = rootPath != executingDirectory 24 | ? rootPath 25 | : executingDirectory; 26 | 27 | var project = await LoadGlobalJsonAsync(jsonPath); 28 | 29 | if (project is null) 30 | { 31 | throw new RunScriptException("Error parsing global.json"); 32 | } 33 | 34 | if (project.Scripts is null || project.Scripts.Count == 0) 35 | { 36 | throw new RunScriptException("No scripts found in the global.json"); 37 | } 38 | 39 | return (project, workingDirectory); 40 | } 41 | 42 | private static string? CheckFolderForFile(string path, string file) 43 | { 44 | var filePath = Path.Combine(path, file); 45 | 46 | if (File.Exists(filePath)) 47 | { 48 | return filePath; 49 | } 50 | 51 | var parentPath = Directory.GetParent(path)?.FullName; 52 | 53 | if (parentPath is null) 54 | { 55 | return null; 56 | } 57 | 58 | return CheckFolderForFile(parentPath, file); 59 | } 60 | 61 | private static async Task LoadGlobalJsonAsync(string jsonPath) 62 | { 63 | var json = await File.ReadAllTextAsync(jsonPath); 64 | 65 | try 66 | { 67 | return JsonSerializer.Deserialize( 68 | json, 69 | _jsonOptions); 70 | } 71 | catch 72 | { 73 | return null; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /run-script.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "run-script", "src\run-script.csproj", "{AEF0FF86-3911-45DE-9291-EDC8E96329F4}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BF5FAA4A-0A8F-4E88-B55D-D9704B694019}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitattributes = .gitattributes 12 | .gitignore = .gitignore 13 | CHANGELOG.md = CHANGELOG.md 14 | Directory.Build.props = Directory.Build.props 15 | Directory.Build.targets = Directory.Build.targets 16 | global.json = global.json 17 | LICENSE = LICENSE 18 | nuget.config = nuget.config 19 | README.md = README.md 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "test\Tests.csproj", "{0A834097-CB5A-4E14-845D-1AA0E8EBEA29}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {AEF0FF86-3911-45DE-9291-EDC8E96329F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {AEF0FF86-3911-45DE-9291-EDC8E96329F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {AEF0FF86-3911-45DE-9291-EDC8E96329F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {AEF0FF86-3911-45DE-9291-EDC8E96329F4}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {0A834097-CB5A-4E14-845D-1AA0E8EBEA29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {0A834097-CB5A-4E14-845D-1AA0E8EBEA29}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {0A834097-CB5A-4E14-845D-1AA0E8EBEA29}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {0A834097-CB5A-4E14-845D-1AA0E8EBEA29}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {B804D0AF-1644-4348-A806-62E219B7412D} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /test/GlobalCommandsTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections.Generic; 4 | using System.CommandLine.IO; 5 | using System.CommandLine.Rendering; 6 | using System.Threading.Tasks; 7 | 8 | [Trait("category", "unit")] 9 | public class GlobalCommandsTests 10 | { 11 | [Fact] 12 | public async Task Should_log_all_available_scripts() 13 | { 14 | // Given 15 | var console = new TestConsole(); 16 | var consoleFormatProvider = new ConsoleFormatInfo 17 | { 18 | SupportsAnsiCodes = false, 19 | }; 20 | var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: true); 21 | 22 | var scripts = new Dictionary(StringComparer.OrdinalIgnoreCase) 23 | { 24 | { "clean", "echo clean" }, 25 | { "prebuild", "echo prebuild" }, 26 | { "build", "echo build" }, 27 | { "test", "echo test" }, 28 | { "posttest", "echo posttest" }, 29 | { "prepack", "echo pack" }, 30 | { "pack", "echo pack" }, 31 | { "postpack", "echo pack" }, 32 | }; 33 | 34 | // When 35 | GlobalCommands.PrintAvailableScripts(consoleWriter, scripts); 36 | 37 | // Then 38 | await Verify(console); 39 | } 40 | 41 | [Fact] 42 | public async Task Should_log_all_available_environment_variables() 43 | { 44 | // Given 45 | var console = new TestConsole(); 46 | var consoleFormatProvider = new ConsoleFormatInfo 47 | { 48 | SupportsAnsiCodes = false, 49 | }; 50 | var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: true); 51 | 52 | var environment = new TestEnvironment("/test/path", isWindows: true); 53 | 54 | // These are reversed to verify they come back sorted 55 | environment.SetEnvironmentVariable("value4", "value 4"); 56 | environment.SetEnvironmentVariable("value3", "value 3"); 57 | environment.SetEnvironmentVariable("value2", "value 2"); 58 | environment.SetEnvironmentVariable("value1", "value 1"); 59 | 60 | // When 61 | GlobalCommands.PrintEnvironmentVariables(consoleWriter, environment); 62 | 63 | // Then 64 | await Verify(console); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /analysis/idisposable.editorconfig: -------------------------------------------------------------------------------- 1 | is_global = true 2 | 3 | # 4 | # Rules 5 | 6 | # IDISP001: Dispose created 7 | dotnet_diagnostic.IDISP001.severity = error 8 | 9 | # IDISP002: Dispose member 10 | dotnet_diagnostic.IDISP002.severity = error 11 | 12 | # IDISP003: Dispose previous before re-assigning 13 | dotnet_diagnostic.IDISP003.severity = error 14 | 15 | # IDISP004: Don't ignore created IDisposable 16 | dotnet_diagnostic.IDISP004.severity = error 17 | 18 | # IDISP005: Return type should indicate that the value should be disposed 19 | dotnet_diagnostic.IDISP005.severity = error 20 | 21 | # IDISP006: Implement IDisposable 22 | dotnet_diagnostic.IDISP006.severity = error 23 | 24 | # IDISP007: Don't dispose injected 25 | dotnet_diagnostic.IDISP007.severity = error 26 | 27 | # IDISP008: Don't assign member with injected and created disposables 28 | dotnet_diagnostic.IDISP008.severity = error 29 | 30 | # IDISP009: Add IDisposable interface 31 | dotnet_diagnostic.IDISP009.severity = error 32 | 33 | # IDISP010: Call base.Dispose(disposing) 34 | dotnet_diagnostic.IDISP010.severity = error 35 | 36 | # IDISP011: Don't return disposed instance 37 | dotnet_diagnostic.IDISP011.severity = error 38 | 39 | # IDISP012: Property should not return created disposable 40 | dotnet_diagnostic.IDISP012.severity = error 41 | 42 | # IDISP013: Await in using 43 | dotnet_diagnostic.IDISP013.severity = error 44 | 45 | # IDISP014: Use a single instance of HttpClient 46 | dotnet_diagnostic.IDISP014.severity = error 47 | 48 | # IDISP015: Member should not return created and cached instance 49 | dotnet_diagnostic.IDISP015.severity = error 50 | 51 | # IDISP016: Don't use disposed instance 52 | dotnet_diagnostic.IDISP016.severity = error 53 | 54 | # IDISP017: Prefer using 55 | dotnet_diagnostic.IDISP017.severity = error 56 | 57 | # IDISP018: Call SuppressFinalize 58 | dotnet_diagnostic.IDISP018.severity = error 59 | 60 | # IDISP019: Call SuppressFinalize 61 | dotnet_diagnostic.IDISP019.severity = error 62 | 63 | # IDISP020: Call SuppressFinalize(this) 64 | dotnet_diagnostic.IDISP020.severity = error 65 | 66 | # IDISP021: Call this.Dispose(true) 67 | dotnet_diagnostic.IDISP021.severity = error 68 | 69 | # IDISP022: Call this.Dispose(false) 70 | dotnet_diagnostic.IDISP022.severity = error 71 | 72 | # IDISP023: Don't use reference types in finalizer context 73 | dotnet_diagnostic.IDISP023.severity = error 74 | 75 | # IDISP024: Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer 76 | dotnet_diagnostic.IDISP024.severity = error 77 | 78 | # IDISP025: Class with no virtual dispose method should be sealed 79 | dotnet_diagnostic.IDISP025.severity = error 80 | -------------------------------------------------------------------------------- /src/CommandGroupRunner.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections.Immutable; 4 | 5 | internal class CommandGroupRunner : ICommandGroupRunner 6 | { 7 | private readonly IConsoleWriter _writer; 8 | private readonly IEnvironment _environment; 9 | private readonly IDictionary _scripts; 10 | private readonly ProcessContext _processContext; 11 | private readonly bool _captureOutput; 12 | private readonly CancellationToken _cancellationToken; 13 | 14 | public CommandGroupRunner( 15 | IConsoleWriter writer, 16 | IEnvironment environment, 17 | IDictionary scripts, 18 | ProcessContext processContext, 19 | bool captureOutput, 20 | CancellationToken cancellationToken) 21 | { 22 | _writer = writer ?? throw new ArgumentNullException(nameof(writer)); 23 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 24 | _scripts = scripts ?? throw new ArgumentNullException(nameof(scripts)); 25 | _processContext = processContext ?? throw new ArgumentNullException(nameof(processContext)); 26 | _captureOutput = captureOutput; 27 | _cancellationToken = cancellationToken; 28 | } 29 | 30 | public virtual ICommandRunner BuildCommand() 31 | => new CommandRunner( 32 | _writer, 33 | _processContext, 34 | _captureOutput, 35 | _cancellationToken); 36 | 37 | public async Task RunAsync(string name, string[]? scriptArgs) 38 | { 39 | var scriptNames = ImmutableArray.Create([ 40 | "pre" + name, 41 | name, 42 | "post" + name, 43 | ]); 44 | 45 | foreach (var subScript in scriptNames.Where(scriptName => _scripts.ContainsKey(scriptName) || scriptName == "env")) 46 | { 47 | // At this point we should have done enough checks to make sure the only not found script is `env` 48 | if (!_scripts.ContainsKey(subScript)) 49 | { 50 | GlobalCommands.PrintEnvironmentVariables(_writer, _environment); 51 | 52 | continue; 53 | } 54 | 55 | var command = BuildCommand(); 56 | 57 | var args = subScript == name 58 | ? scriptArgs 59 | : null; 60 | 61 | var result = await command.RunAsync( 62 | subScript, 63 | _scripts[subScript]!, 64 | args); 65 | 66 | if (result != 0) 67 | { 68 | return result; 69 | } 70 | } 71 | 72 | return 0; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/CommandBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Text.RegularExpressions; 4 | 5 | internal partial class CommandBuilder 6 | { 7 | private readonly IConsoleWriter _writer; 8 | private readonly IEnvironment _environment; 9 | private readonly Project _project; 10 | private readonly string _workingDirectory; 11 | private readonly bool _captureOutput; 12 | 13 | public CommandBuilder( 14 | IConsoleWriter writer, 15 | IEnvironment environment, 16 | Project project, 17 | string workingDirectory, 18 | bool captureOutput) 19 | { 20 | if (string.IsNullOrEmpty(workingDirectory)) throw new ArgumentException($"'{nameof(workingDirectory)}' cannot be null or empty.", nameof(workingDirectory)); 21 | 22 | _writer = writer ?? throw new ArgumentNullException(nameof(writer)); 23 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 24 | _project = project ?? throw new ArgumentNullException(nameof(project)); 25 | _workingDirectory = workingDirectory; 26 | _captureOutput = captureOutput; 27 | } 28 | 29 | public ProcessContext? ProcessContext { get; private set; } 30 | 31 | // This is the same regex used by npm's run-script library 32 | [GeneratedRegex("(?:^|\\\\)cmd(?:\\.exe)?$", RegexOptions.IgnoreCase)] 33 | public static partial Regex IsCmdCheck(); 34 | 35 | public void SetUpEnvironment(string? scriptShellOverride) 36 | { 37 | var (scriptShell, isCmd) = GetScriptShell(scriptShellOverride ?? _project.ScriptShell); 38 | 39 | ProcessContext = ProcessContext.Create(scriptShell, isCmd, _workingDirectory); 40 | 41 | _writer.LineVerbose("Using shell: {0}", scriptShell); 42 | _writer.BlankLine(); 43 | } 44 | 45 | public ICommandGroupRunner CreateGroupRunner(CancellationToken cancellationToken) 46 | => new CommandGroupRunner( 47 | _writer, 48 | _environment, 49 | _project.Scripts!, 50 | ProcessContext!, 51 | _captureOutput, 52 | cancellationToken); 53 | 54 | /// 55 | /// Gets the script shell to use. 56 | /// 57 | /// A optional custom shell to use instead of the system default. 58 | /// The shell to use and if it's cmd or not. 59 | private (string shell, bool isCmd) GetScriptShell(string? shell) 60 | { 61 | shell ??= _environment.IsWindows 62 | ? _environment.GetEnvironmentVariable("COMSPEC") ?? "cmd" 63 | : "sh"; 64 | 65 | var isCmd = IsCmdCheck().IsMatch(shell); 66 | 67 | return (shell, isCmd); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | DOTNET_NOLOGO: true 10 | CONFIGURATION: Release 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: write 18 | packages: write 19 | 20 | steps: 21 | - name: Get version from tag 22 | id: tag_name 23 | run: echo "current_version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 24 | 25 | - name: Check out repository 26 | uses: actions/checkout@v5.0.0 27 | 28 | - name: Set up .NET 29 | uses: xt0rted/setup-dotnet@v1.5.0 30 | with: 31 | source-url: https://nuget.pkg.github.com/xt0rted/index.json 32 | nuget_auth_token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Get changelog entry 35 | uses: mindsers/changelog-reader-action@v2.2.3 36 | id: changelog_reader 37 | with: 38 | version: ${{ steps.tag_name.outputs.current_version }} 39 | 40 | - run: dotnet tool restore 41 | 42 | - run: dotnet r build 43 | 44 | - run: dotnet r test -- --no-build --logger GitHubActions 45 | 46 | - run: dotnet r pack -- --no-build 47 | 48 | - name: Upload artifacts 49 | uses: actions/upload-artifact@v4.6.2 50 | with: 51 | name: nupkg 52 | path: ./artifacts/*.nupkg 53 | 54 | - name: Upload release assets 55 | uses: softprops/action-gh-release@v2 56 | id: release_updater 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | body: ${{ steps.changelog_reader.outputs.changes }} 61 | files: ./artifacts/*.nupkg 62 | 63 | - name: Create discussion for release 64 | run: | 65 | gh api \ 66 | --method PATCH \ 67 | -H "Accept: application/vnd.github+json" \ 68 | /repos/${{ github.repository }}/releases/${{ steps.release_updater.outputs.id }} \ 69 | -f discussion_category_name='Announcements' 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 72 | 73 | - name: Publish to GPR 74 | run: | 75 | dotnet nuget push "./artifacts/*.nupkg" \ 76 | --api-key ${{ secrets.GITHUB_TOKEN }} \ 77 | --source https://nuget.pkg.github.com/${{ github.repository_owner }} 78 | 79 | - name: Publish to nuget.org 80 | run: | 81 | dotnet nuget push "./artifacts/*.nupkg" \ 82 | --api-key ${{ secrets.NUGET_TOKEN }} \ 83 | --source https://api.nuget.org/v3/index.json 84 | 85 | - name: Upload test results 86 | if: failure() 87 | uses: actions/upload-artifact@v4.6.2 88 | with: 89 | name: verify-test-results 90 | path: | 91 | **/*.received.* 92 | -------------------------------------------------------------------------------- /src/StreamForwarder.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Text; 4 | 5 | // https://github.com/dotnet/sdk/blob/a758a468b71e15303198506a8de1040649aa0f35/src/Cli/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs 6 | internal sealed class StreamForwarder 7 | { 8 | private static readonly char[] _ignoreCharacters = ['\r']; 9 | 10 | private const char FlushBuilderCharacter = '\n'; 11 | 12 | private StringBuilder? _builder; 13 | #pragma warning disable IDISP006 // Implement IDisposable 14 | private StringWriter? _capture; 15 | #pragma warning restore IDISP006 // Implement IDisposable 16 | 17 | public string? CapturedOutput 18 | { 19 | get 20 | { 21 | if (_capture is null) 22 | { 23 | return null; 24 | } 25 | 26 | var capture = _capture 27 | .GetStringBuilder() 28 | .ToString() 29 | .TrimEnd('\r', '\n'); 30 | 31 | return capture.Length == 0 32 | ? null 33 | : capture; 34 | } 35 | } 36 | 37 | public StreamForwarder Capture() 38 | { 39 | if (_capture is not null) 40 | { 41 | throw new InvalidOperationException("Already capturing stream!"); 42 | } 43 | 44 | #pragma warning disable IDISP003 // Dispose previous before re-assigning 45 | _capture = new StringWriter(); 46 | #pragma warning restore IDISP003 // Dispose previous before re-assigning 47 | 48 | return this; 49 | } 50 | 51 | public Task BeginReadAsync(TextReader reader) 52 | => Task.Run(() => Read(reader)); 53 | 54 | public void Read(TextReader reader) 55 | { 56 | ArgumentNullException.ThrowIfNull(reader); 57 | 58 | const int bufferSize = 1; 59 | 60 | char currentCharacter; 61 | 62 | var buffer = new char[bufferSize]; 63 | _builder = new StringBuilder(); 64 | 65 | // Using Read with buffer size 1 to prevent looping endlessly 66 | // like we would when using Read() with no buffer 67 | while ((_ = reader.Read(buffer, 0, bufferSize)) > 0) 68 | { 69 | currentCharacter = buffer[0]; 70 | 71 | if (currentCharacter == FlushBuilderCharacter) 72 | { 73 | WriteBuilder(); 74 | } 75 | else if (!_ignoreCharacters.Contains(currentCharacter)) 76 | { 77 | _builder.Append(currentCharacter); 78 | } 79 | } 80 | 81 | // Flush anything else when the stream is closed 82 | // Which should only happen if someone used console.Write 83 | if (_builder.Length > 0) 84 | { 85 | WriteBuilder(); 86 | } 87 | } 88 | 89 | private void WriteBuilder() 90 | { 91 | if (_builder is not null) 92 | { 93 | _capture?.WriteLine(_builder.ToString()); 94 | _builder.Clear(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/ProjectLoaderTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | [Trait("category", "unit")] 4 | public class ProjectLoaderTests 5 | { 6 | [Fact] 7 | public void Should_throw_if_no_globaljson_found_in_tree() 8 | { 9 | // Given / When 10 | var action = () => ProjectLoader.LoadAsync(Path.GetTempPath()); 11 | 12 | // Then 13 | var ex = action.ShouldThrow(); 14 | ex.Message.ShouldBe("No global.json found in folder path"); 15 | } 16 | 17 | [Fact] 18 | public void Should_throw_if_malformed_file() 19 | { 20 | // Given 21 | var testPath = TestPath("malformed"); 22 | 23 | // When 24 | var action = () => ProjectLoader.LoadAsync(testPath); 25 | 26 | // Then 27 | var ex = action.ShouldThrow(); 28 | ex.Message.ShouldBe("Error parsing global.json"); 29 | } 30 | 31 | [Fact] 32 | public void Should_throw_if_no_scripts_found() 33 | { 34 | // Given 35 | var testPath = TestPath("no-scripts"); 36 | 37 | // When 38 | var action = () => ProjectLoader.LoadAsync(testPath); 39 | 40 | // Then 41 | var ex = action.ShouldThrow(); 42 | ex.Message.ShouldBe("No scripts found in the global.json"); 43 | } 44 | 45 | [Fact] 46 | public async Task Should_find_in_root() 47 | { 48 | // Given 49 | var testPath = TestPath("dir1"); 50 | 51 | // When 52 | var (project, workingDirectory) = await ProjectLoader.LoadAsync(testPath); 53 | 54 | // Then 55 | project.ShouldNotBeNull(); 56 | 57 | await Verify(project); 58 | 59 | workingDirectory.ShouldBe(TestPath("dir1")); 60 | } 61 | 62 | [Fact] 63 | public async Task Should_look_up_the_tree() 64 | { 65 | // Given 66 | var testPath = TestPath("dir1", "dir2", "dir3"); 67 | 68 | // When 69 | var (project, workingDirectory) = await ProjectLoader.LoadAsync(testPath); 70 | 71 | // Then 72 | project.ShouldNotBeNull(); 73 | 74 | await Verify(project); 75 | 76 | workingDirectory.ShouldBe(TestPath("dir1")); 77 | } 78 | 79 | [Fact] 80 | public async Task Should_treat_script_names_as_lowercase() 81 | { 82 | // Given 83 | var testPath = TestPath("script-names"); 84 | 85 | // When 86 | var (project, _) = await ProjectLoader.LoadAsync(testPath); 87 | 88 | // Then 89 | project.Scripts?.Comparer.ShouldBe(StringComparer.OrdinalIgnoreCase); 90 | 91 | await Verify(project); 92 | } 93 | 94 | private string TestPath(params string[] folders) 95 | { 96 | return Path.Join(segments().ToArray()); 97 | 98 | IEnumerable segments() 99 | { 100 | yield return AttributeReader.GetProjectDirectory(GetType().Assembly); 101 | yield return "test-configs"; 102 | 103 | foreach (var folder in folders) 104 | { 105 | yield return folder; 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/Integration/CommandBuilderTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript.Integration; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.CommandLine.IO; 6 | using System.CommandLine.Rendering; 7 | 8 | public static class CommandBuilderTests 9 | { 10 | // Passing `bash` works locally, but not on CI due to no WSL so the next best thing is using git bash 11 | [Trait("category", "integration")] 12 | public class WindowsPlatform 13 | { 14 | [WindowsTheory] 15 | [InlineData(null)] 16 | [InlineData("cmd.exe")] 17 | [InlineData("pwsh")] 18 | [InlineData(@"C:\Program Files\Git\bin\bash.exe")] 19 | public async Task Should_execute_single_script_in_shell(string? shellOverride) 20 | { 21 | await CommandBuilderTests.Should_execute_single_script_in_shell( 22 | isWindows: true, 23 | shellOverride); 24 | } 25 | } 26 | 27 | [Trait("category", "integration")] 28 | public class UnixPlatforms 29 | { 30 | [UnixTheory] 31 | [InlineData(null)] 32 | [InlineData("pwsh")] 33 | public async Task Should_execute_single_script_in_shell(string? shellOverride) 34 | { 35 | await CommandBuilderTests.Should_execute_single_script_in_shell( 36 | isWindows: false, 37 | shellOverride); 38 | } 39 | } 40 | 41 | private static async Task Should_execute_single_script_in_shell(bool isWindows, string? shellOverride) 42 | { 43 | var console = new TestConsole(); 44 | var consoleFormatProvider = new ConsoleFormatInfo 45 | { 46 | SupportsAnsiCodes = false, 47 | }; 48 | var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: true); 49 | 50 | var environment = new TestEnvironment(isWindows: isWindows); 51 | 52 | var project = new Project 53 | { 54 | Scripts = new Dictionary(StringComparer.OrdinalIgnoreCase) 55 | { 56 | { "test", "echo testing" }, 57 | }, 58 | }; 59 | 60 | var cb = new CommandBuilder( 61 | consoleWriter, 62 | environment, 63 | project, 64 | environment.CurrentDirectory, 65 | // This lets us verify the output 66 | captureOutput: true); 67 | 68 | cb.SetUpEnvironment(scriptShellOverride: shellOverride); 69 | 70 | using var ct = new CancellationTokenSource(TimeSpan.FromMinutes(1)); 71 | 72 | var gr = cb.CreateGroupRunner(ct.Token); 73 | 74 | var result = await gr.RunAsync( 75 | name: "test", 76 | scriptArgs: null); 77 | 78 | await Verify(console).UseParameters(ShellName(shellOverride)); 79 | 80 | result.ShouldBe(0); 81 | } 82 | 83 | private static string ShellName(string? shell) 84 | { 85 | if (shell is null) 86 | { 87 | return "default"; 88 | } 89 | 90 | if (shell.StartsWith(@"c:\", StringComparison.OrdinalIgnoreCase)) 91 | { 92 | return Path.GetFileNameWithoutExtension(shell); 93 | } 94 | 95 | return shell; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/CommandRunner.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Diagnostics; 4 | 5 | internal class CommandRunner : ICommandRunner 6 | { 7 | private readonly IConsoleWriter _writer; 8 | private readonly ProcessContext _processContext; 9 | private readonly bool _captureOutput; 10 | private readonly CancellationToken _cancellationToken; 11 | 12 | public CommandRunner( 13 | IConsoleWriter writer, 14 | ProcessContext processContext, 15 | bool captureOutput, 16 | CancellationToken cancellationToken) 17 | { 18 | _writer = writer ?? throw new ArgumentNullException(nameof(writer)); 19 | _processContext = processContext ?? throw new ArgumentNullException(nameof(processContext)); 20 | _captureOutput = captureOutput; 21 | _cancellationToken = cancellationToken; 22 | } 23 | 24 | public async Task RunAsync(string name, string cmd, string[]? args) 25 | { 26 | _cancellationToken.ThrowIfCancellationRequested(); 27 | 28 | _writer.Banner(name, ArgumentBuilder.ConcatenateCommandAndArgArrayForDisplay(cmd, args)); 29 | 30 | using (var process = new Process()) 31 | { 32 | process.StartInfo.WorkingDirectory = _processContext.WorkingDirectory; 33 | process.StartInfo.FileName = _processContext.Shell; 34 | 35 | process.StartInfo.Environment[EnvironmentVariables.RunScriptChildProcess] = "true"; 36 | 37 | var outStream = new StreamForwarder(); 38 | var errStream = new StreamForwarder(); 39 | 40 | Task? taskOut = null; 41 | Task? taskErr = null; 42 | 43 | if (_captureOutput) 44 | { 45 | process.StartInfo.RedirectStandardOutput = true; 46 | process.StartInfo.RedirectStandardError = true; 47 | 48 | outStream.Capture(); 49 | errStream.Capture(); 50 | } 51 | 52 | if (_processContext.IsCmd) 53 | { 54 | process.StartInfo.Arguments = string.Concat( 55 | "/d /s /c \"", 56 | ArgumentBuilder.EscapeAndConcatenateCommandAndArgArrayForCmdProcessStart(cmd, args), 57 | "\""); 58 | } 59 | else 60 | { 61 | process.StartInfo.ArgumentList.Add("-c"); 62 | process.StartInfo.ArgumentList.Add(ArgumentBuilder.EscapeAndConcatenateCommandAndArgArrayForProcessStart(cmd, args)); 63 | } 64 | 65 | process.Start(); 66 | 67 | if (_captureOutput) 68 | { 69 | taskOut = outStream.BeginReadAsync(process.StandardOutput); 70 | taskErr = errStream.BeginReadAsync(process.StandardError); 71 | } 72 | 73 | await process.WaitForExitAsync(_cancellationToken); 74 | 75 | if (_captureOutput) 76 | { 77 | await taskOut!.WaitAsync(_cancellationToken); 78 | await taskErr!.WaitAsync(_cancellationToken); 79 | 80 | _writer.Raw(outStream.CapturedOutput); 81 | _writer.Error(errStream.CapturedOutput); 82 | } 83 | 84 | return process.ExitCode; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/CommandBuilderTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections.Generic; 4 | using System.CommandLine.IO; 5 | using System.CommandLine.Rendering; 6 | 7 | [Trait("category", "unit")] 8 | public class CommandBuilderTests 9 | { 10 | private const string DefaultComSpec = @"C:\WINDOWS\system32\cmd.exe"; 11 | 12 | [Theory] 13 | [InlineData("cmd")] 14 | [InlineData("cmd.exe")] 15 | [InlineData(@"c:\dir\cmd")] 16 | [InlineData(@"c:\dir\cmd.exe")] 17 | public void IsCmdCheck_should_match_cmd_variations(string shell) 18 | { 19 | // Given / When / Then 20 | CommandBuilder.IsCmdCheck().IsMatch(shell).ShouldBeTrue(); 21 | } 22 | 23 | [Theory] 24 | [InlineData("pwsh")] 25 | [InlineData("sh")] 26 | public void IsCmdCheck_should_not_match_non_cmd_variations(string shell) 27 | { 28 | // Given / When / Then 29 | CommandBuilder.IsCmdCheck().IsMatch(shell).ShouldBeFalse(); 30 | } 31 | 32 | [Theory] 33 | [InlineData(true)] 34 | [InlineData(false)] 35 | public async Task SetUpEnvironment_should_default_to_comspec_env_var_only_on_windows(bool isWindows) 36 | { 37 | // Given 38 | var builder = SetUpTest(isWindows); 39 | 40 | // When 41 | builder.SetUpEnvironment(scriptShellOverride: null); 42 | 43 | // Then 44 | await Verify(builder.ProcessContext).UseParameters(isWindows); 45 | } 46 | 47 | [Fact] 48 | public async Task SetUpEnvironment_should_fall_back_to_cmd_on_windows() 49 | { 50 | // Given 51 | var builder = SetUpTest(isWindows: true, comSpec: null); 52 | 53 | // When 54 | builder.SetUpEnvironment(scriptShellOverride: null); 55 | 56 | // Then 57 | await Verify(builder.ProcessContext); 58 | } 59 | 60 | [Theory] 61 | [InlineData(true)] 62 | [InlineData(false)] 63 | public async Task SetUpEnvironment_should_use_custom_shell(bool isWindows) 64 | { 65 | // Given 66 | var builder = SetUpTest(isWindows); 67 | 68 | // When 69 | builder.SetUpEnvironment(scriptShellOverride: "pwsh"); 70 | 71 | // Then 72 | await Verify(builder.ProcessContext).UseParameters(isWindows); 73 | } 74 | 75 | [Theory] 76 | [InlineData(true)] 77 | [InlineData(false)] 78 | public void CreateGroupRunner_should_create_a_runner(bool isWindows) 79 | { 80 | // Given 81 | var builder = SetUpTest(isWindows); 82 | builder.SetUpEnvironment(scriptShellOverride: null); 83 | 84 | // When 85 | var groupRunner = builder.CreateGroupRunner(default); 86 | 87 | // Then 88 | groupRunner.ShouldNotBeNull(); 89 | groupRunner.ShouldBeOfType(); 90 | } 91 | 92 | private static CommandBuilder SetUpTest(bool isWindows, string? comSpec = DefaultComSpec) 93 | { 94 | var console = new TestConsole(); 95 | var consoleFormatProvider = new ConsoleFormatInfo 96 | { 97 | SupportsAnsiCodes = false, 98 | }; 99 | var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: true); 100 | 101 | var project = new Project 102 | { 103 | Scripts = new Dictionary(), 104 | }; 105 | 106 | var environment = new TestEnvironment( 107 | "/test/path", 108 | isWindows); 109 | 110 | if (isWindows) 111 | { 112 | environment.SetEnvironmentVariable("COMSPEC", comSpec); 113 | } 114 | 115 | return new CommandBuilder( 116 | consoleWriter, 117 | environment, 118 | project, 119 | "/test/path", 120 | captureOutput: true); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | - Build and test on each platform version available 6 | - macOS 13 7 | - macOS 14 8 | - macOS 15 9 | - Ubuntu 22.04 10 | - Ubuntu 24.04 11 | - Windows 2022 12 | - Windows 2025 13 | - Build and test on ARM 14 | - Ubuntu 22.04 15 | - Ubuntu 24.04 16 | - Windows 11 17 | 18 | ## [0.6.0](https://github.com/xt0rted/dotnet-run-script/compare/v0.5.0...v0.6.0) - 2024-04-10 19 | 20 | - Dropped support for .NET Core 3.1 21 | - Dropped Support for .NET 6 22 | - Added support for .NET 8 23 | - Adjusted globbing so `:` acts like a path separator ([#131](https://github.com/xt0rted/dotnet-run-script/pull/131)) 24 | - `foo:*` will match `foo:bar` but not `foo:bar:baz` 25 | - `foo:*:baz` will match `foo:bar:baz` 26 | - `foo:**` will match `foo:bar` and `foo:bar:baz` 27 | 28 | ## [0.5.0](https://github.com/xt0rted/dotnet-run-script/compare/v0.4.0...v0.5.0) - 2022-10-11 29 | 30 | ### Added 31 | 32 | - New environment variable called `DOTNET_R_CHILDPROCESS` which is set to `true` when executing a script. Use this to check if you're running inside `dotnet r` or not. ([#87](https://github.com/xt0rted/dotnet-run-script/pull/87)) 33 | - Support for [GitHub Actions log groups](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines) ([#87](https://github.com/xt0rted/dotnet-run-script/pull/87)) 34 | 35 | ## [0.4.0](https://github.com/xt0rted/dotnet-run-script/compare/v0.3.0...v0.4.0) - 2022-08-12 36 | 37 | > **Note** 38 | > This version drops support for .NET 5 which is no longer supported, but it will continue to work with .NET 5 SDKs. 39 | 40 | ### Added 41 | 42 | - Ability to run multiple scripts in a single call (e.g. `dotnet r build test pack`) ([#10](https://github.com/xt0rted/dotnet-run-script/pull/10)) 43 | - Support for globbing in script names (e.g `dotnet r test:*` will match `test:unit` and `test:integration`) ([#79](https://github.com/xt0rted/dotnet-run-script/pull/79 44 | )) 45 | 46 | ### Updated 47 | 48 | - Bumped `System.CommandLine` from 2.0.0-beta3.22114.1 to 2.0.0-beta4.22272.1 49 | - Bumped `System.CommandLine.Rendering` from 0.4.0-alpha.22114.1 to 0.4.0-alpha.22272.1 50 | 51 | ## [0.3.0](https://github.com/xt0rted/dotnet-run-script/compare/v0.2.0...v0.3.0) - 2022-04-24 52 | 53 | ### Fixed 54 | 55 | - Don't escape the script passed to `cmd.exe`, just the double dash arguments 56 | 57 | ## [0.2.0](https://github.com/xt0rted/dotnet-run-script/compare/v0.1.0...v0.2.0) - 2022-04-23 58 | 59 | > **Note** 60 | > This version broke conditional script execution (`cmd1 && cmd2`) in `cmd.exe` 61 | 62 | ### Added 63 | 64 | - Force color output with the `DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION` environment variable. 65 | - Note: this tool with output color on all platforms including when output is redirected, but the dotnet cli only supports this on Unix platforms currently. This means script results might not be colored in places like GitHub Actions build logs when using the Windows VMs. 66 | - Added `-v` alias to enable verbose output. 67 | 68 | ### Fixed 69 | 70 | - Escape arguments for non-cmd shells 71 | - Quote additional arguments passed after `--` 72 | - Escape scripts with `^` passed to `cmd.exe` 73 | 74 | ### Updated 75 | 76 | - Switched from [actions/setup-dotnet](https://github.com/actions/setup-dotnet) to [xt0rted/setup-dotnet](https://github.com/xt0rted/setup-dotnet) 77 | 78 | ## [0.1.0](https://github.com/xt0rted/dotnet-run-script/releases/tag/v0.1.0) - 2022-03-26 79 | 80 | - Run scripts with `dotnet r ...` 81 | - Uses `cmd` on Windows and `sh` on Linux 82 | - Support for `pre` and `post` scripts 83 | - Support for setting a custom shell such as `pwsh` 84 | - Set via the `scriptShell` global.json setting 85 | - Set via the `--script-shell` parameter 86 | - Built-in `env` command to list available environment variables 87 | - Flow parameters after `--` to the running script 88 | - Skip commands that aren't found with the `--if-present` parameter 89 | -------------------------------------------------------------------------------- /test/ArgumentBuilderTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | // https://github.com/dotnet/sdk/blob/09b31215867d1ffe4955fd5b7cd91eb552d3632c/src/Tests/Microsoft.DotNet.Cli.Utils.Tests/ArgumentEscaperTests.cs 4 | [Trait("category", "unit")] 5 | public class ArgumentBuilderTests 6 | { 7 | [Theory] 8 | [InlineData("cmd", null, "cmd")] 9 | [InlineData("cm \"d\"", null, "cm \"d\"")] 10 | [InlineData("c m d", null, "c m d")] 11 | [InlineData("c m d", new string[0], "c m d")] 12 | [InlineData("c m d", new[] { "one", "two", "three" }, "c m d \"one\" \"two\" \"three\"")] 13 | [InlineData("c m d", new[] { "line1\nline2", "word1\tword2" }, "c m d \"line1\nline2\" \"word1\tword2\"")] 14 | [InlineData("c m d", new[] { "with spaces" }, "c m d \"with spaces\"")] 15 | [InlineData("c m d", new[] { @"with\backslash" }, @"c m d ""with\backslash""")] 16 | [InlineData("c m d", new[] { @"""quotedwith\backslash""" }, @"c m d ""\""quotedwith\backslash\""""")] 17 | [InlineData("c m d", new[] { @"C:\Users\" }, @"c m d ""C:\Users\""")] 18 | [InlineData("c m d", new[] { @"C:\Program Files\dotnet\" }, @"c m d ""C:\Program Files\dotnet\""")] 19 | [InlineData("c m d", new[] { @"backslash\""preceedingquote" }, @"c m d ""backslash\\\""preceedingquote""")] 20 | [InlineData("c m d", new[] { @""" hello """ }, @"c m d ""\"" hello \""""")] 21 | public void EscapeAndConcatenateCommandAndArgArrayForProcessStart(string command, string[]? args, string expected) 22 | { 23 | // Given / When 24 | var result = ArgumentBuilder.EscapeAndConcatenateCommandAndArgArrayForProcessStart(command, args); 25 | 26 | // Then 27 | result.ShouldBe(expected); 28 | } 29 | 30 | [Theory] 31 | [InlineData("cmd", null, "cmd")] 32 | [InlineData("cm \"d\"", null, "cm \"d\"")] 33 | [InlineData("c m d", null, "c m d")] 34 | [InlineData("c m d", new string[0], "c m d")] 35 | [InlineData("c m d", new[] { "one", "two", "three" }, "c m d^ ^o^n^e^ ^t^w^o^ ^t^h^r^e^e")] 36 | [InlineData("c m d", new[] { "line1\nline2", "word1\tword2" }, "c m d^ ^\"^l^i^n^e^1^\n^l^i^n^e^2^\"^ ^\"^w^o^r^d^1^\t^w^o^r^d^2^\"")] 37 | [InlineData("c m d", new[] { "with spaces" }, "c m d^ ^\"^w^i^t^h^ ^s^p^a^c^e^s^\"")] 38 | [InlineData("c m d", new[] { @"with\backslash" }, @"c m d^ ^w^i^t^h^\^b^a^c^k^s^l^a^s^h")] 39 | [InlineData("c m d", new[] { @"""quotedwith\backslash""" }, @"c m d^ ^""^q^u^o^t^e^d^w^i^t^h^\^b^a^c^k^s^l^a^s^h^""")] 40 | [InlineData("c m d", new[] { @"C:\Users\" }, @"c m d^ ^C^:^\^U^s^e^r^s^\")] 41 | [InlineData("c m d", new[] { @"C:\Program Files\dotnet\" }, @"c m d^ ^""^C^:^\^P^r^o^g^r^a^m^ ^F^i^l^e^s^\^d^o^t^n^e^t^\^""")] 42 | [InlineData("c m d", new[] { @"backslash\""preceedingquote" }, @"c m d^ ^b^a^c^k^s^l^a^s^h^\^""^p^r^e^c^e^e^d^i^n^g^q^u^o^t^e")] 43 | [InlineData("c m d", new[] { @""" hello """ }, @"c m d^ ^""^""^ ^h^e^l^l^o^ ^""^""")] 44 | public void EscapeAndConcatenateCommandAndArgArrayForCmdProcessStart(string command, string[]? args, string expected) 45 | { 46 | // Given / When 47 | var result = ArgumentBuilder.EscapeAndConcatenateCommandAndArgArrayForCmdProcessStart(command, args); 48 | 49 | // Then 50 | result.ShouldBe(expected); 51 | } 52 | 53 | [Theory] 54 | [InlineData(null, "c m d")] 55 | [InlineData(new string[0], "c m d")] 56 | [InlineData(new[] { "one", "two", "three" }, "c m d one two three")] 57 | [InlineData(new[] { "line1\nline2", "word1\tword2" }, "c m d line1\nline2 word1\tword2")] 58 | [InlineData(new[] { "with spaces" }, "c m d with spaces")] 59 | [InlineData(new[] { @"with\backslash" }, @"c m d with\backslash")] 60 | [InlineData(new[] { @"""quotedwith\backslash""" }, @"c m d ""quotedwith\backslash""")] 61 | [InlineData(new[] { @"C:\Users\" }, @"c m d C:\Users\")] 62 | [InlineData(new[] { @"C:\Program Files\dotnet\" }, @"c m d C:\Program Files\dotnet\")] 63 | [InlineData(new[] { @"backslash\""preceedingquote" }, @"c m d backslash\""preceedingquote")] 64 | [InlineData(new[] { @""" hello """ }, @"c m d "" hello """)] 65 | public void ConcatinateCommandAndArgArrayForDisplay(string[]? args, string expected) 66 | { 67 | // Given / When 68 | var result = ArgumentBuilder.ConcatenateCommandAndArgArrayForDisplay("c m d", args); 69 | 70 | // Then 71 | result.ShouldBe(expected); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ValueStringBuilder.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDISP008 // Don't assign member with injected and created disposables 2 | #pragma warning disable RCS1162 // Avoid chain of assignments. 3 | 4 | namespace RunScript; 5 | 6 | using System.Buffers; 7 | using System.Diagnostics; 8 | using System.Runtime.CompilerServices; 9 | 10 | /// 11 | /// This is a stripped down version of 12 | /// 13 | /// with only the minimal parts needed for our use. 14 | /// 15 | internal ref struct ValueStringBuilder 16 | { 17 | private char[]? _arrayToReturnToPool; 18 | private Span _chars; 19 | private int _pos; 20 | 21 | public ValueStringBuilder(Span initialBuffer) 22 | { 23 | _arrayToReturnToPool = null; 24 | _chars = initialBuffer; 25 | _pos = 0; 26 | } 27 | 28 | public readonly int Length => _pos; 29 | 30 | public override string ToString() 31 | { 32 | var s = _chars[.._pos].ToString(); 33 | 34 | Dispose(); 35 | 36 | return s; 37 | } 38 | 39 | public bool TryCopyTo(Span destination, out int charsWritten) 40 | { 41 | if (_chars[.._pos].TryCopyTo(destination)) 42 | { 43 | charsWritten = _pos; 44 | 45 | Dispose(); 46 | 47 | return true; 48 | } 49 | 50 | charsWritten = 0; 51 | 52 | Dispose(); 53 | 54 | return false; 55 | } 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public void Append(char c) 59 | { 60 | var pos = _pos; 61 | 62 | if ((uint)pos < (uint)_chars.Length) 63 | { 64 | _chars[pos] = c; 65 | _pos = pos + 1; 66 | } 67 | else 68 | { 69 | GrowAndAppend(c); 70 | } 71 | } 72 | 73 | public void Append(char c, int count) 74 | { 75 | if (_pos > _chars.Length - count) 76 | { 77 | Grow(count); 78 | } 79 | 80 | _chars.Slice(_pos, count).Fill(c); 81 | 82 | _pos += count; 83 | } 84 | 85 | public void Append(ReadOnlySpan value) 86 | { 87 | var pos = _pos; 88 | 89 | if (pos > _chars.Length - value.Length) 90 | { 91 | Grow(value.Length); 92 | } 93 | 94 | value.CopyTo(_chars[_pos..]); 95 | 96 | _pos += value.Length; 97 | } 98 | 99 | [MethodImpl(MethodImplOptions.NoInlining)] 100 | private void GrowAndAppend(char c) 101 | { 102 | Grow(1); 103 | Append(c); 104 | } 105 | 106 | /// 107 | /// Resize the internal buffer either by doubling current buffer size or 108 | /// by adding to 109 | /// whichever is greater. 110 | /// 111 | /// 112 | /// Number of chars requested beyond current position. 113 | /// 114 | [MethodImpl(MethodImplOptions.NoInlining)] 115 | private void Grow(int additionalCapacityBeyondPos) 116 | { 117 | Debug.Assert(additionalCapacityBeyondPos > 0); 118 | Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); 119 | 120 | // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative 121 | var poolArray = ArrayPool.Shared.Rent((int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_chars.Length * 2)); 122 | 123 | _chars[.._pos].CopyTo(poolArray); 124 | 125 | var toReturn = _arrayToReturnToPool; 126 | 127 | _chars = _arrayToReturnToPool = poolArray; 128 | 129 | if (toReturn is not null) 130 | { 131 | ArrayPool.Shared.Return(toReturn); 132 | } 133 | } 134 | 135 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 136 | public void Dispose() 137 | { 138 | var toReturn = _arrayToReturnToPool; 139 | 140 | this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again 141 | 142 | if (toReturn is not null) 143 | { 144 | ArrayPool.Shared.Return(toReturn); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/ConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.CommandLine.Rendering; 4 | using System.Globalization; 5 | 6 | internal class ConsoleWriter : IConsoleWriter 7 | { 8 | private readonly IConsole _console; 9 | private readonly IFormatProvider? _consoleFormatProvider; 10 | 11 | private readonly bool _verbose; 12 | 13 | public ConsoleWriter(IConsole console, IFormatProvider consoleFormatProvider, bool verbose) 14 | { 15 | _console = console ?? throw new ArgumentNullException(nameof(console)); 16 | _consoleFormatProvider = consoleFormatProvider ?? throw new ArgumentNullException(nameof(consoleFormatProvider)); 17 | _verbose = verbose; 18 | } 19 | 20 | private static AnsiControlCode TextDim { get; } = $"{Ansi.Esc}[2m"; 21 | 22 | private void WriteLine(AnsiControlCode modifierOn, AnsiControlCode modifierOff, string? message, params object?[] args) 23 | { 24 | if (message is not null) 25 | { 26 | _console.Out.Write(modifierOn.ToString(null, _consoleFormatProvider)); 27 | 28 | if (args?.Length > 0) 29 | { 30 | _console.Out.Write(string.Format(CultureInfo.CurrentCulture, message, args)); 31 | } 32 | else 33 | { 34 | _console.Out.Write(message); 35 | } 36 | 37 | _console.Out.Write(modifierOff.ToString(null, _consoleFormatProvider)); 38 | _console.Out.Write(Environment.NewLine); 39 | } 40 | } 41 | 42 | public void Raw(string? message) 43 | => _console.Out.Write(message); 44 | 45 | public void VerboseBanner() 46 | => LineVerbose("Verbose mode is on. This will print more information."); 47 | 48 | public void BlankLine() 49 | => _console.Out.Write(Environment.NewLine); 50 | 51 | public void BlankLineVerbose() 52 | { 53 | if (_verbose) 54 | { 55 | _console.Out.Write(Environment.NewLine); 56 | } 57 | } 58 | 59 | public void Line(string? message, params object?[] args) 60 | => WriteLine(Ansi.Color.Foreground.LightGray, Ansi.Color.Foreground.Default, message, args); 61 | 62 | public void LineVerbose(string? message = null, params object?[] args) 63 | { 64 | if (_verbose) 65 | { 66 | WriteLine(Ansi.Color.Foreground.LightGray, Ansi.Color.Foreground.Default, "> " + message, args); 67 | } 68 | } 69 | 70 | public void SecondaryLine(string? message, params object?[] args) 71 | => WriteLine(TextDim, Ansi.Text.BoldOff, message, args); 72 | 73 | public void VerboseSecondaryLine(string? message, params object?[] args) 74 | { 75 | if (_verbose) 76 | { 77 | WriteLine(TextDim, Ansi.Text.BoldOff, "> " + message, args); 78 | } 79 | } 80 | 81 | internal void Information(string? message, params object?[] args) 82 | => WriteLine(Ansi.Color.Foreground.Cyan, Ansi.Color.Foreground.Default, message, args); 83 | 84 | public void Banner(params string?[] messages) 85 | { 86 | if (messages is null) return; 87 | 88 | BlankLine(); 89 | 90 | foreach (var message in messages) 91 | { 92 | Information("> {0}", message); 93 | } 94 | 95 | BlankLine(); 96 | } 97 | 98 | public void Error(string? message, params object?[] args) 99 | => WriteLine(Ansi.Color.Foreground.Red, Ansi.Color.Foreground.Default, message, args); 100 | 101 | public string? ColorText(ConsoleColor color, int value) 102 | => ColorText(color, value.ToString(CultureInfo.CurrentCulture)); 103 | 104 | public string? ColorText(ConsoleColor color, string? value) 105 | { 106 | if (string.IsNullOrEmpty(value)) 107 | { 108 | return null; 109 | } 110 | 111 | var colorControlCode = color switch 112 | { 113 | ConsoleColor.DarkBlue => Ansi.Color.Foreground.Blue, 114 | ConsoleColor.DarkGreen => Ansi.Color.Foreground.Green, 115 | ConsoleColor.DarkCyan => Ansi.Color.Foreground.Cyan, 116 | ConsoleColor.DarkRed => Ansi.Color.Foreground.Red, 117 | ConsoleColor.DarkMagenta => Ansi.Color.Foreground.Magenta, 118 | ConsoleColor.DarkYellow => Ansi.Color.Foreground.Yellow, 119 | ConsoleColor.Gray => Ansi.Color.Foreground.LightGray, 120 | ConsoleColor.DarkGray => Ansi.Color.Foreground.DarkGray, 121 | ConsoleColor.Blue => Ansi.Color.Foreground.LightBlue, 122 | ConsoleColor.Green => Ansi.Color.Foreground.LightGreen, 123 | ConsoleColor.Cyan => Ansi.Color.Foreground.LightCyan, 124 | ConsoleColor.Red => Ansi.Color.Foreground.LightRed, 125 | ConsoleColor.Magenta => Ansi.Color.Foreground.LightMagenta, 126 | ConsoleColor.Yellow => Ansi.Color.Foreground.LightYellow, 127 | ConsoleColor.White => Ansi.Color.Foreground.White, 128 | _ => throw new ArgumentOutOfRangeException(nameof(color), color, null), 129 | }; 130 | 131 | return string.Concat( 132 | colorControlCode.ToString(null, _consoleFormatProvider), 133 | value, 134 | Ansi.Color.Foreground.Default.ToString(null, _consoleFormatProvider)); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/ValueStringBuilderTests.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA1305 // Specify IFormatProvider 2 | 3 | namespace RunScript; 4 | 5 | using System.Text; 6 | 7 | // https://github.com/dotnet/runtime/blob/c78bf2f522b4ce5a449faf6a38a0752b642a7f79/src/libraries/Common/tests/Tests/System/Text/ValueStringBuilderTests.cs 8 | [Trait("category", "unit")] 9 | public class ValueStringBuilderTests 10 | { 11 | [Fact] 12 | public void Ctor_Default_CanAppend() 13 | { 14 | var vsb = default(ValueStringBuilder); 15 | Assert.Equal(0, vsb.Length); 16 | 17 | vsb.Append('a'); 18 | Assert.Equal(1, vsb.Length); 19 | Assert.Equal("a", vsb.ToString()); 20 | } 21 | 22 | [Fact] 23 | public void Ctor_Span_CanAppend() 24 | { 25 | var vsb = new ValueStringBuilder(new char[1]); 26 | Assert.Equal(0, vsb.Length); 27 | 28 | vsb.Append('a'); 29 | Assert.Equal(1, vsb.Length); 30 | Assert.Equal("a", vsb.ToString()); 31 | } 32 | 33 | [Fact] 34 | public void Append_Char_MatchesStringBuilder() 35 | { 36 | var sb = new StringBuilder(); 37 | var vsb = new ValueStringBuilder(); 38 | for (var i = 1; i <= 100; i++) 39 | { 40 | sb.Append((char)i); 41 | vsb.Append((char)i); 42 | } 43 | 44 | Assert.Equal(sb.Length, vsb.Length); 45 | Assert.Equal(sb.ToString(), vsb.ToString()); 46 | } 47 | 48 | [Fact] 49 | public void Append_String_MatchesStringBuilder() 50 | { 51 | var sb = new StringBuilder(); 52 | var vsb = new ValueStringBuilder(); 53 | for (var i = 1; i <= 100; i++) 54 | { 55 | var s = i.ToString(); 56 | sb.Append(s); 57 | vsb.Append(s); 58 | } 59 | 60 | Assert.Equal(sb.Length, vsb.Length); 61 | Assert.Equal(sb.ToString(), vsb.ToString()); 62 | } 63 | 64 | [Theory] 65 | [InlineData(0, 4 * 1024 * 1024)] 66 | [InlineData(1025, 4 * 1024 * 1024)] 67 | [InlineData(3 * 1024 * 1024, 6 * 1024 * 1024)] 68 | public void Append_String_Large_MatchesStringBuilder(int initialLength, int stringLength) 69 | { 70 | var sb = new StringBuilder(initialLength); 71 | var vsb = new ValueStringBuilder(new char[initialLength]); 72 | 73 | var s = new string('a', stringLength); 74 | sb.Append(s); 75 | vsb.Append(s); 76 | 77 | Assert.Equal(sb.Length, vsb.Length); 78 | Assert.Equal(sb.ToString(), vsb.ToString()); 79 | } 80 | 81 | [Fact] 82 | public void Append_CharInt_MatchesStringBuilder() 83 | { 84 | var sb = new StringBuilder(); 85 | var vsb = new ValueStringBuilder(); 86 | for (var i = 1; i <= 100; i++) 87 | { 88 | sb.Append((char)i, i); 89 | vsb.Append((char)i, i); 90 | } 91 | 92 | Assert.Equal(sb.Length, vsb.Length); 93 | Assert.Equal(sb.ToString(), vsb.ToString()); 94 | } 95 | 96 | [Fact] 97 | public void ToString_ClearsBuilder_ThenReusable() 98 | { 99 | const string text1 = "test"; 100 | var vsb = new ValueStringBuilder(); 101 | 102 | vsb.Append(text1); 103 | Assert.Equal(text1.Length, vsb.Length); 104 | 105 | var s = vsb.ToString(); 106 | Assert.Equal(text1, s); 107 | 108 | Assert.Equal(0, vsb.Length); 109 | Assert.Equal(string.Empty, vsb.ToString()); 110 | Assert.True(vsb.TryCopyTo(Span.Empty, out _)); 111 | 112 | const string text2 = "another test"; 113 | vsb.Append(text2); 114 | Assert.Equal(text2.Length, vsb.Length); 115 | Assert.Equal(text2, vsb.ToString()); 116 | } 117 | 118 | [Fact] 119 | public void TryCopyTo_FailsWhenDestinationIsTooSmall_SucceedsWhenItsLargeEnough() 120 | { 121 | var vsb = new ValueStringBuilder(); 122 | 123 | const string text = "expected text"; 124 | vsb.Append(text); 125 | Assert.Equal(text.Length, vsb.Length); 126 | 127 | Span dst = new char[text.Length - 1]; 128 | Assert.False(vsb.TryCopyTo(dst, out var charsWritten)); 129 | Assert.Equal(0, charsWritten); 130 | Assert.Equal(0, vsb.Length); 131 | } 132 | 133 | [Fact] 134 | public void TryCopyTo_ClearsBuilder_ThenReusable() 135 | { 136 | const string text1 = "test"; 137 | var vsb = new ValueStringBuilder(); 138 | 139 | vsb.Append(text1); 140 | Assert.Equal(text1.Length, vsb.Length); 141 | 142 | Span dst = new char[text1.Length]; 143 | Assert.True(vsb.TryCopyTo(dst, out var charsWritten)); 144 | Assert.Equal(text1.Length, charsWritten); 145 | Assert.Equal(text1, new string(dst)); 146 | 147 | Assert.Equal(0, vsb.Length); 148 | Assert.Equal(string.Empty, vsb.ToString()); 149 | Assert.True(vsb.TryCopyTo(Span.Empty, out _)); 150 | 151 | const string text2 = "another test"; 152 | vsb.Append(text2); 153 | Assert.Equal(text2.Length, vsb.Length); 154 | Assert.Equal(text2, vsb.ToString()); 155 | } 156 | 157 | [Fact] 158 | public void Dispose_ClearsBuilder_ThenReusable() 159 | { 160 | const string text1 = "test"; 161 | var vsb = new ValueStringBuilder(); 162 | 163 | vsb.Append(text1); 164 | Assert.Equal(text1.Length, vsb.Length); 165 | 166 | vsb.Dispose(); 167 | 168 | Assert.Equal(0, vsb.Length); 169 | Assert.Equal(string.Empty, vsb.ToString()); 170 | Assert.True(vsb.TryCopyTo(Span.Empty, out _)); 171 | 172 | const string text2 = "another test"; 173 | vsb.Append(text2); 174 | Assert.Equal(text2.Length, vsb.Length); 175 | Assert.Equal(text2, vsb.ToString()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | DOTNET_NOLOGO: true 17 | CONFIGURATION: Release 18 | 19 | jobs: 20 | lint-markdown: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Check out repository 25 | uses: actions/checkout@v5.0.0 26 | 27 | - name: Install Node 28 | uses: actions/setup-node@v5.0.0 29 | with: 30 | node-version-file: .nvmrc 31 | 32 | - uses: xt0rted/markdownlint-problem-matcher@v3.0.0 33 | 34 | - run: npm ci 35 | 36 | - run: npm test 37 | 38 | build: 39 | runs-on: ${{ matrix.os }} 40 | 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | os: 45 | - macos-13 46 | - macos-14 47 | - macos-15 48 | - ubuntu-22.04 49 | - ubuntu-22.04-arm 50 | - ubuntu-24.04 51 | - ubuntu-24.04-arm 52 | - windows-11-arm 53 | - windows-2022 54 | - windows-2025 55 | 56 | permissions: 57 | contents: read 58 | packages: read 59 | 60 | steps: 61 | - name: Check out repository 62 | uses: actions/checkout@v5.0.0 63 | 64 | - name: Build version suffix (main) 65 | if: github.event_name == 'push' 66 | run: echo "VERSION_SUFFIX=beta.${{ github.run_number }}" >> $GITHUB_ENV 67 | shell: bash 68 | 69 | - name: Build version suffix (pr) 70 | if: github.event_name == 'pull_request' 71 | run: echo "VERSION_SUFFIX=alpha.${{ github.event.number }}" >> $GITHUB_ENV 72 | shell: bash 73 | 74 | - name: Set up .NET 75 | uses: xt0rted/setup-dotnet@v1.5.0 76 | with: 77 | source-url: https://nuget.pkg.github.com/xt0rted/index.json 78 | nuget_auth_token: ${{ secrets.GITHUB_TOKEN }} 79 | 80 | - run: dotnet tool restore 81 | 82 | - run: dotnet r build 83 | 84 | - run: dotnet r test -- --no-build --logger GitHubActions 85 | 86 | - run: dotnet r pack -- --no-build --version-suffix ${{ env.VERSION_SUFFIX }} 87 | 88 | - name: Upload artifacts 89 | if: matrix.os == 'ubuntu-24.04' 90 | uses: actions/upload-artifact@v4.6.2 91 | with: 92 | name: nupkg 93 | path: ./artifacts/*.nupkg 94 | 95 | - name: Upload test results 96 | if: failure() 97 | uses: actions/upload-artifact@v4.6.2 98 | with: 99 | name: build-verify-test-results 100 | path: | 101 | **/*.received.* 102 | 103 | integration: 104 | needs: 105 | - build 106 | - lint-markdown 107 | 108 | runs-on: ${{ matrix.os }} 109 | 110 | strategy: 111 | fail-fast: false 112 | matrix: 113 | os: 114 | - macos-13 115 | - macos-14 116 | - macos-15 117 | - ubuntu-22.04 118 | - ubuntu-22.04-arm 119 | - ubuntu-24.04 120 | - ubuntu-24.04-arm 121 | - windows-11-arm 122 | - windows-2022 123 | - windows-2025 124 | shell: 125 | - default 126 | - bash 127 | - pwsh 128 | exclude: 129 | - os: windows-11-arm 130 | shell: bash 131 | - os: windows-2022 132 | shell: bash 133 | - os: windows-2025 134 | shell: bash 135 | include: 136 | - os: windows-11-arm 137 | shell: C:\Program Files\Git\bin\bash.exe 138 | - os: windows-2022 139 | shell: C:\Program Files\Git\bin\bash.exe 140 | - os: windows-2025 141 | shell: C:\Program Files\Git\bin\bash.exe 142 | 143 | permissions: 144 | contents: read 145 | packages: read 146 | 147 | steps: 148 | - name: Check out repository 149 | uses: actions/checkout@v5.0.0 150 | 151 | - name: Set up .NET 152 | uses: xt0rted/setup-dotnet@v1.5.0 153 | with: 154 | source-url: https://nuget.pkg.github.com/xt0rted/index.json 155 | nuget_auth_token: ${{ secrets.GITHUB_TOKEN }} 156 | 157 | - name: Download nupkg 158 | uses: actions/download-artifact@v5.0.0 159 | with: 160 | name: nupkg 161 | path: .nuget 162 | 163 | - run: dotnet tool restore 164 | 165 | - run: dotnet tool uninstall run-script 166 | 167 | - run: | 168 | file=(.nuget/*.nupkg) 169 | pattern="run-script\.(.*)\.nupkg" 170 | if [[ $file =~ $pattern ]]; then 171 | dotnet tool install run-script --version "${BASH_REMATCH[1]}" --add-source "${{ github.workspace }}/.nuget" 172 | else 173 | echo "::error::nupkg not found" 174 | exit 1 175 | fi 176 | shell: bash 177 | 178 | - run: | 179 | if [ "${{ matrix.shell }}" == "default" ]; then 180 | dotnet r build -v 181 | else 182 | dotnet r build -v --script-shell "${{ matrix.shell }}" 183 | fi 184 | shell: bash 185 | 186 | - run: | 187 | if [ "${{ matrix.shell }}" == "default" ]; then 188 | dotnet r test --verbose -- --no-build --logger GitHubActions 189 | else 190 | shellName="${{ matrix.shell }}" 191 | 192 | if [[ "$shellName" == *\.exe ]]; then 193 | shellName="bash" 194 | fi 195 | 196 | dotnet r test --verbose --script-shell "${{ matrix.shell }}" -- --no-build --logger GitHubActions 197 | fi 198 | shell: bash 199 | 200 | - run: dotnet r integration:ci --verbose -- --no-build --logger GitHubActions 201 | 202 | - run: dotnet r clean:bin build test --verbose 203 | 204 | - name: Upload test results 205 | if: failure() 206 | uses: actions/upload-artifact@v4.6.2 207 | with: 208 | name: integration-verify-test-results 209 | path: | 210 | **/*.received.* 211 | 212 | release: 213 | if: github.event_name == 'push' 214 | 215 | needs: 216 | - build 217 | - integration 218 | 219 | runs-on: ubuntu-latest 220 | 221 | permissions: 222 | packages: write 223 | 224 | steps: 225 | - name: Download nupkg 226 | uses: actions/download-artifact@v5.0.0 227 | with: 228 | name: nupkg 229 | 230 | - name: Publish to GPR 231 | run: | 232 | dotnet nuget push "./*.nupkg" \ 233 | --api-key ${{ secrets.GITHUB_TOKEN }} \ 234 | --source https://nuget.pkg.github.com/${{ github.repository_owner }} 235 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-run-script 2 | 3 | [![CI build status](https://github.com/xt0rted/dotnet-run-script/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/xt0rted/dotnet-run-script/actions/workflows/ci.yml) 4 | [![NuGet Package](https://img.shields.io/nuget/v/run-script?logo=nuget)](https://www.nuget.org/packages/run-script) 5 | [![GitHub Package Registry](https://img.shields.io/badge/github-package_registry-yellow?logo=nuget)](https://nuget.pkg.github.com/xt0rted/index.json) 6 | [![Project license](https://img.shields.io/github/license/xt0rted/dotnet-run-script)](LICENSE) 7 | 8 | A `dotnet` tool to run arbitrary commands from a project's "scripts" object. 9 | If you've used `npm` this is the equivalent of `npm run` with almost identical functionality and options. 10 | It is compatible with .NET Core 3.1 and newer. 11 | 12 | See [the about page](docs/README.md) for more information on how this tool came to be and why it exists at all. 13 | 14 | ## Installation 15 | 16 | This tool is meant to be used as a dotnet local tool. 17 | To install it run the following: 18 | 19 | ```console 20 | dotnet new tool-manifest 21 | dotnet tool install run-script 22 | ``` 23 | 24 | > [!WARNING] 25 | > Installing this tool globally is not recommended. 26 | > PowerShell defines the alias `r` for the `Invoke-History` command which prevents this from being called. 27 | > You'll also run into issues calling this from your scripts since global tools don't use the `dotnet` prefix. 28 | 29 | ## Keeping current 30 | 31 | Tools like [Dependabot](https://github.com/github/feedback/discussions/13825) and [Renovate](https://github.com/marketplace/renovate) don't currently support updating dotnet local tools. 32 | One way to automate this is to use a [GitHub Actions workflow](https://github.com/xt0rted/dotnet-tool-update-test) to check for updates and create PRs when new versions are available, which is what this repo does. 33 | 34 | ## Options 35 | 36 | Name | Description 37 | -- | -- 38 | `--if-present` | Don't exit with an error code if the script isn't found 39 | `--script-shell` | The shell to use when running scripts (cmd, pwsh, sh, etc.) 40 | `-v`, `--verbose` | Enable verbose output 41 | `--version` | Show version information 42 | `--help` | Show help and usage information 43 | 44 | Arguments passed after the double dash are passed through to the executing script. 45 | 46 | ```console 47 | dotnet r build --verbose -- --configuration Release 48 | ``` 49 | 50 | ### Color output 51 | 52 | This tool supports the `DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION` environment variable. 53 | Setting this to `1` or `true` will force color output on all platforms. 54 | Due to a limitation of the `Console` apis this will not work on Windows when output is redirected in environments such as GitHub Actions. 55 | 56 | There is also support for the `NO_COLOR` environment variable. 57 | Setting this to any value will disable color output. 58 | 59 | ### GitHub Actions 60 | 61 | On GitHub Actions you need to set the environment values `DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION` and `TERM`. 62 | `TERM` should be `xterm` or `xterm-256color`. 63 | 64 | ## Configuration 65 | 66 | In your project's `global.json` add a `scripts` object: 67 | 68 | ```jsonc 69 | { 70 | "sdk": { 71 | "version": "8.0.203", 72 | "rollForward": "latestPatch" 73 | }, 74 | "scriptShell": "pwsh", // Optional 75 | "scripts": { 76 | "clean": "dotnet r clean:*", 77 | "clean:artifacts": "dotnet rimraf artifacts", // dotnet tool install rimraf 78 | "clean:bin": "dotnet rimraf **/bin **/obj", 79 | "build": "dotnet build --configuration Release", 80 | "test": "dotnet test --configuration Release", 81 | "ci": "dotnet r build && dotnet r test", 82 | } 83 | } 84 | ``` 85 | 86 | > [!NOTE] 87 | > The shell used depends on the OS. 88 | > On Windows `CMD` is used, on Linux, macOS, and WSL `sh` is used. 89 | > This can be overridden by setting the `scriptShell` property or by passing the `--script-shell` option with the name of the shell to use. 90 | 91 | The `env` command is a special built-in command that lists all available environment variables. 92 | You can override this with your own command if you wish. 93 | 94 | ## Usage 95 | 96 | Use `dotnet r [...] [options]` to run the scripts. 97 | Anything you can run from the command line can be used in a script. 98 | You can also call other scripts to chain them together such as a `ci` script that calls the `build`, `test`, and `package` scripts. 99 | 100 | To help keep your configuration easy to read and maintain `pre` and `post` scripts are supported. 101 | These are run before and after the main script. 102 | 103 | This is an example of a `pre` script that clears the build artifacts folder, and a `post` script that writes to the console saying the command completed. 104 | 105 | ```json 106 | { 107 | "scripts": { 108 | "prepackage": "del /Q ./artifacts", 109 | "package": "dotnet pack --configuration Release --no-build --output ./artifacts", 110 | "postpackage": "echo \"Packaging complete\"" 111 | } 112 | } 113 | ``` 114 | 115 | ### Multiple script execution 116 | 117 | Multiple scripts can be called at the same time like so: 118 | 119 | ```console 120 | dotnet r build test 121 | ``` 122 | 123 | This will run the `build` script and if it returns a `0` exit code it will then run the `test` script. 124 | The `--if-present` option can be used to skip scripts that don't exist. 125 | 126 | ```json 127 | { 128 | "scripts": { 129 | "build": "dotnet build", 130 | "test:unit": "dotnet test", 131 | "package": "dotnet pack" 132 | } 133 | } 134 | ``` 135 | 136 | ```console 137 | dotnet r build test:unit test:integration package --if-present 138 | ``` 139 | 140 | Arguments passed after the double dash are passed through to each executing script. 141 | In this example both the `--configuration` and `--framework` options will be passed to each of the four scripts when running them. 142 | 143 | ```console 144 | dotnet r build test:unit test:integration package -- --configuration Release --framework net8.0 145 | ``` 146 | 147 | ### Globbing or wildcard support 148 | 149 | Multiple scripts can be run at the same time using globbing. 150 | This means `dotnet r test:*` will match `test:unit` and `test:integration` and run them in series in the order they're listed in the `global.json` file. 151 | 152 | Globbing is handled by the [DotNet.Glob](https://github.com/dazinator/DotNet.Glob) library and currently supports all of its patterns and wildcards. 153 | 154 | ### Working directory 155 | 156 | The working directory is set to the root of the project where the `global.json` is located. 157 | If you need to get the folder the command was executed from you can do so using the `INIT_CWD` environment variable. 158 | 159 | ## Common build environments 160 | 161 | When using this tool on a build server, such as GitHub Actions, you might want to use a generic workflow that calls a common set of scripts such as `build`, `test`, and `package`. 162 | These might not be defined in all of your projects and if a script that doesn't exist is called an error is returned. 163 | To work around this you can call them with the `--if-present` flag which will return a `0` exit code for not found scripts. 164 | 165 | Example shared GitHub Actions workflow: 166 | 167 | ```yaml 168 | jobs: 169 | build: 170 | runs-on: ubuntu-latest 171 | steps: 172 | - uses: actions/checkout@v3 173 | 174 | # Always runs 175 | - name: Run build 176 | run: dotnet r build 177 | 178 | # Only runs if `test` script is present 179 | - name: Run test 180 | run: dotnet r test --if-present 181 | 182 | # Only runs if `package` script is present 183 | - name: Run package 184 | run: dotnet r package --if-present 185 | ``` 186 | -------------------------------------------------------------------------------- /src/RunScriptCommand.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.CommandLine.Invocation; 4 | 5 | using DotNet.Globbing; 6 | 7 | using RunScript.Logging; 8 | 9 | internal class RunScriptCommand : RootCommand, ICommandHandler 10 | { 11 | private readonly IEnvironment _environment; 12 | private readonly IFormatProvider _consoleFormatProvider; 13 | private string _workingDirectory; 14 | 15 | internal RunScriptCommand( 16 | IEnvironment environment, 17 | IFormatProvider consoleFormatProvider, 18 | string workingDirectory) 19 | : base("Run arbitrary project scripts") 20 | { 21 | _environment = environment ?? throw new ArgumentNullException(nameof(environment)); 22 | _consoleFormatProvider = consoleFormatProvider ?? throw new ArgumentNullException(nameof(consoleFormatProvider)); 23 | 24 | if (string.IsNullOrEmpty(workingDirectory)) throw new ArgumentException($"'{nameof(workingDirectory)}' cannot be null or empty.", nameof(workingDirectory)); 25 | 26 | _workingDirectory = workingDirectory; 27 | 28 | AddArgument(GlobalArguments.Scripts); 29 | 30 | AddOption(GlobalOptions.IfPresent); 31 | AddOption(GlobalOptions.ScriptShell); 32 | AddOption(GlobalOptions.Verbose); 33 | 34 | Handler = this; 35 | } 36 | 37 | public int Invoke(InvocationContext context) 38 | => throw new NotImplementedException(); 39 | 40 | public async Task InvokeAsync(InvocationContext context) 41 | { 42 | ArgumentNullException.ThrowIfNull(context); 43 | 44 | var ifPresent = context.ParseResult.GetValueForOption(GlobalOptions.IfPresent); 45 | var scriptShell = context.ParseResult.GetValueForOption(GlobalOptions.ScriptShell); 46 | var verbose = context.ParseResult.GetValueForOption(GlobalOptions.Verbose); 47 | var scripts = context.ParseResult.GetValueForArgument(GlobalArguments.Scripts); 48 | 49 | var writer = new ConsoleWriter(context.Console, _consoleFormatProvider, verbose); 50 | 51 | writer.VerboseBanner(); 52 | 53 | Project? project; 54 | try 55 | { 56 | _environment.SetEnvironmentVariable("INIT_CWD", _workingDirectory); 57 | 58 | (project, _workingDirectory) = await ProjectLoader.LoadAsync(_workingDirectory); 59 | } 60 | catch (Exception ex) 61 | { 62 | writer.Error(ex.Message); 63 | 64 | return 1; 65 | } 66 | 67 | var builder = new CommandBuilder( 68 | writer, 69 | _environment, 70 | project, 71 | _workingDirectory, 72 | // For now we just write to the executing shell, later we can opt to write to the log instead 73 | captureOutput: false); 74 | 75 | builder.SetUpEnvironment(scriptShell); 76 | 77 | if (scripts.Length == 0) 78 | { 79 | GlobalCommands.PrintAvailableScripts(writer, project.Scripts!); 80 | 81 | return 0; 82 | } 83 | 84 | var scriptsToRun = FindScripts(project.Scripts!, scripts); 85 | 86 | // When `--if-present` isn't specified and a script wasn't found in the config then we show an error and stop 87 | if (scriptsToRun.Any(s => !s.Exists) && !ifPresent) 88 | { 89 | writer.Error( 90 | "Script not found: {0}", 91 | string.Join( 92 | ", ", 93 | scriptsToRun 94 | .Where(script => !script.Exists) 95 | .Select(script => script.Name))); 96 | 97 | return 1; 98 | } 99 | 100 | var runResults = new List(); 101 | 102 | foreach (var script in scriptsToRun) 103 | { 104 | using (var logGroup = writer.Group(_environment, script.Name)) 105 | { 106 | if (!script.Exists) 107 | { 108 | writer.Banner($"Skipping script {script.Name}"); 109 | 110 | continue; 111 | } 112 | 113 | // UnparsedTokens is backed by string[] so if we cast 114 | // back to that we get a lot better perf down the line. 115 | // Hopefully this doesn't break in the future 🤞 116 | var scriptArgs = (string[])context.ParseResult.UnparsedTokens; 117 | 118 | var scriptRunner = builder.CreateGroupRunner(context.GetCancellationToken()); 119 | 120 | var result = await scriptRunner.RunAsync( 121 | script.Name, 122 | scriptArgs); 123 | 124 | runResults.Add(new(script.Name, result)); 125 | 126 | if (result != 0) 127 | { 128 | break; 129 | } 130 | } 131 | } 132 | 133 | return RunResults(writer, runResults); 134 | } 135 | 136 | internal static List FindScripts( 137 | IDictionary projectScripts, 138 | string[] scripts) 139 | { 140 | var results = new List(); 141 | 142 | foreach (var script in scripts) 143 | { 144 | // The `env` script is special so if it's not explicitly declared we act like it was 145 | if (projectScripts.ContainsKey(script) || string.Equals(script, "env", StringComparison.OrdinalIgnoreCase)) 146 | { 147 | results.Add(new(script, true)); 148 | 149 | continue; 150 | } 151 | 152 | var hadMatch = false; 153 | var matcher = Glob.Parse( 154 | SwapColonAndSlash(script), 155 | new GlobOptions 156 | { 157 | Evaluation = 158 | { 159 | CaseInsensitive = true, 160 | }, 161 | }); 162 | 163 | foreach (var projectScript in projectScripts.Keys) 164 | { 165 | if (matcher.IsMatch(SwapColonAndSlash(projectScript).AsSpan())) 166 | { 167 | hadMatch = true; 168 | 169 | results.Add(new(projectScript, true)); 170 | } 171 | } 172 | 173 | if (!hadMatch) 174 | { 175 | results.Add(new(script, false)); 176 | } 177 | } 178 | 179 | return results; 180 | } 181 | 182 | internal static int RunResults(IConsoleWriter writer, List results) 183 | { 184 | // If only 1 script ran we don't need a report of the results 185 | if (results.Count == 1) 186 | { 187 | return results[0].ExitCode; 188 | } 189 | 190 | var hadError = false; 191 | 192 | foreach (var result in results.Where(r => r.ExitCode != 0)) 193 | { 194 | hadError = true; 195 | 196 | writer.Line( 197 | "ERROR: \"{0}\" exited with {1}", 198 | writer.ColorText(ConsoleColor.Blue, result.Name), 199 | writer.ColorText(ConsoleColor.Green, result.ExitCode)); 200 | } 201 | 202 | return hadError ? 1 : 0; 203 | } 204 | 205 | internal static string SwapColonAndSlash(string scriptName) 206 | { 207 | var result = new char[scriptName.Length]; 208 | 209 | for (var i = 0; i < scriptName.Length; i++) 210 | { 211 | if (scriptName[i] == ':') 212 | { 213 | result[i] = '/'; 214 | } 215 | else if (scriptName[i] == '/') 216 | { 217 | result[i] = ':'; 218 | } 219 | else 220 | { 221 | result[i] = scriptName[i]; 222 | } 223 | } 224 | 225 | return new string(result); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/ArgumentBuilder.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDISP001 // Dispose created 2 | #pragma warning disable IDISP003 // Dispose previous before re-assigning 3 | 4 | namespace RunScript; 5 | 6 | using System.Runtime.CompilerServices; 7 | 8 | /// 9 | /// Copy of the internal sdk class to escape command arguments updated to use ReadOnlySpan. 10 | /// 11 | /// 12 | /// 13 | /// 14 | internal static class ArgumentBuilder 15 | { 16 | private const char Backslash = '\\'; 17 | private const char Caret = '^'; 18 | private const char NewLine = '\n'; 19 | private const char Quote = '"'; 20 | private const char Space = ' '; 21 | private const char Tab = '\t'; 22 | 23 | /// 24 | /// Undo the processing which took place to create string[] args in Main, so that the next process will receive the same string[] args. 25 | /// 26 | /// 27 | /// See here for more info: . 28 | /// 29 | /// The base command. 30 | /// List of arguments to escape. 31 | /// An escaped string of the and . 32 | public static string EscapeAndConcatenateCommandAndArgArrayForProcessStart( 33 | string? command, 34 | string[]? args) 35 | { 36 | var sb = new ValueStringBuilder(stackalloc char[256]); 37 | 38 | sb.Append(command); 39 | 40 | if (args is not null) 41 | { 42 | for (var i = 0; i < args.Length; i++) 43 | { 44 | sb.Append(Space); 45 | 46 | EscapeSingleArg(ref sb, args[i]); 47 | } 48 | } 49 | 50 | return sb.ToString(); 51 | } 52 | 53 | /// 54 | /// Undo the processing which took place to create string[] args in Main, so that the next process will receive the same string[] args. 55 | /// 56 | /// 57 | /// See here for more info: . 58 | /// 59 | /// The base command. 60 | /// List of arguments to escape. 61 | /// An escaped string of the and . 62 | public static string EscapeAndConcatenateCommandAndArgArrayForCmdProcessStart( 63 | string? command, 64 | string[]? args) 65 | { 66 | var sb = new ValueStringBuilder(stackalloc char[256]); 67 | 68 | sb.Append(command); 69 | 70 | if (args is not null) 71 | { 72 | for (var i = 0; i < args.Length; i++) 73 | { 74 | sb.Append(Caret); 75 | sb.Append(Space); 76 | 77 | EscapeArgForCmd(ref sb, args[i]); 78 | } 79 | } 80 | 81 | return sb.ToString(); 82 | } 83 | 84 | /// 85 | /// Concatenates the command and arguments without any escaping. 86 | /// This is meant to be used for display only and not for passing to a new process. 87 | /// 88 | /// The base command. 89 | /// List of optional arguments. 90 | /// A raw concatenation of the and . 91 | public static string ConcatenateCommandAndArgArrayForDisplay( 92 | string? command, 93 | string[]? args) 94 | { 95 | var sb = new ValueStringBuilder(stackalloc char[256]); 96 | 97 | sb.Append(command); 98 | 99 | if (args?.Length > 0) 100 | { 101 | for (var i = 0; i < args.Length; i++) 102 | { 103 | sb.Append(Space); 104 | sb.Append(args[i]); 105 | } 106 | } 107 | 108 | return sb.ToString(); 109 | } 110 | 111 | private static void EscapeSingleArg(ref ValueStringBuilder sb, ReadOnlySpan arg) 112 | { 113 | var isQuoted = IsSurroundedWithQuotes(arg); 114 | 115 | sb.Append(Quote); 116 | 117 | for (var i = 0; i < arg.Length; ++i) 118 | { 119 | var backslashCount = 0; 120 | 121 | // Consume All Backslashes 122 | while (i < arg.Length && arg[i] == Backslash) 123 | { 124 | backslashCount++; 125 | i++; 126 | } 127 | 128 | // Escape any backslashes at the end of the arg 129 | // when the argument is also quoted. 130 | // This ensures the outside quote is interpreted as 131 | // an argument delimiter 132 | if (i == arg.Length && isQuoted) 133 | { 134 | sb.Append(Backslash, 2 * backslashCount); 135 | } 136 | 137 | // At then end of the arg, which isn't quoted, 138 | // just add the backslashes, no need to escape 139 | else if (i == arg.Length) 140 | { 141 | sb.Append(Backslash, backslashCount); 142 | } 143 | 144 | // Escape any preceding backslashes and the quote 145 | else if (arg[i] == Quote) 146 | { 147 | sb.Append(Backslash, (2 * backslashCount) + 1); 148 | sb.Append(Quote); 149 | } 150 | 151 | // Output any consumed backslashes and the character 152 | else 153 | { 154 | sb.Append(Backslash, backslashCount); 155 | sb.Append(arg[i]); 156 | } 157 | } 158 | 159 | sb.Append(Quote); 160 | } 161 | 162 | /// 163 | /// Prepare as single argument to roundtrip properly through cmd. 164 | /// This prefixes every character with the ^ character to force cmd to interpret the argument string literally. 165 | /// 166 | /// 167 | /// See here for more info: . 168 | /// 169 | /// The to append to. 170 | /// The argument to escape. 171 | private static void EscapeArgForCmd(ref ValueStringBuilder sb, ReadOnlySpan argument) 172 | { 173 | var quoted = ArgumentContainsWhitespace(argument); 174 | 175 | if (quoted) 176 | { 177 | sb.Append(Caret); 178 | sb.Append(Quote); 179 | } 180 | 181 | // Prepend every character with ^ 182 | // This is harmless when passing through cmd 183 | // and ensures cmd metacharacters are not interpreted 184 | // as such 185 | for (var i = 0; i < argument.Length; i++) 186 | { 187 | sb.Append(Caret); 188 | sb.Append(argument[i]); 189 | } 190 | 191 | if (quoted) 192 | { 193 | sb.Append(Caret); 194 | sb.Append(Quote); 195 | } 196 | } 197 | 198 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 199 | private static bool IsSurroundedWithQuotes(ReadOnlySpan argument) 200 | => argument[0] == Quote && 201 | argument[^1] == Quote; 202 | 203 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 204 | private static bool ArgumentContainsWhitespace(ReadOnlySpan argument) 205 | => argument.IndexOfAny(Space, Tab, NewLine) >= 0; 206 | } 207 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | 9 | [*.cs] 10 | indent_size = 4 11 | 12 | # Xml project files 13 | [*.csproj] 14 | charset = utf-8 15 | indent_size = 2 16 | 17 | # Xml config files 18 | [*.{props,config}] 19 | indent_size = 2 20 | 21 | [*.json] 22 | indent_size = 2 23 | 24 | # Verify settings 25 | [*.{received,verified}.{txt,xml,json}] 26 | charset = "utf-8-bom" 27 | end_of_line = lf 28 | indent_size = unset 29 | indent_style = unset 30 | insert_final_newline = false 31 | tab_width = unset 32 | trim_trailing_whitespace = false 33 | 34 | [*.yml] 35 | indent_size = 2 36 | 37 | # Dotnet code style settings: 38 | [*.{cs,vb}] 39 | # Add a blank line between using and Import directives based on their name 40 | dotnet_separate_import_directive_groups = true 41 | # Sort using and Import directives with System.* appearing first 42 | dotnet_sort_system_directives_first = true 43 | dotnet_style_coalesce_expression = true:error 44 | dotnet_style_collection_initializer = true:error 45 | dotnet_style_explicit_tuple_names = true:error 46 | dotnet_style_null_propagation = true:error 47 | dotnet_style_object_initializer = true:error 48 | # Prefer parentheses for improved clarity 49 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:error 50 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:error 51 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:error 52 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:error 53 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 54 | dotnet_style_predefined_type_for_member_access = true:error 55 | # Avoid "this." and "Me." if not necessary 56 | dotnet_style_qualification_for_field = false:error 57 | dotnet_style_qualification_for_property = false:error 58 | dotnet_style_qualification_for_method = false:error 59 | dotnet_style_qualification_for_event = false:error 60 | 61 | 62 | ## Naming Conventions 63 | [*.{cs,vb}] 64 | 65 | ## Naming styles 66 | 67 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 68 | dotnet_naming_style.camel_case_style.capitalization = camel_case 69 | 70 | # PascalCase with I prefix 71 | dotnet_naming_style.interface_style.capitalization = pascal_case 72 | dotnet_naming_style.interface_style.required_prefix = I 73 | 74 | # camelCase with _ prefix 75 | dotnet_naming_style._camelCase.capitalization = camel_case 76 | dotnet_naming_style._camelCase.required_prefix = _ 77 | 78 | ## Rules 79 | 80 | # Namespaces 81 | dotnet_naming_rule.namespace_naming.symbols = namespace_symbol 82 | dotnet_naming_rule.namespace_naming.style = pascal_case_style 83 | dotnet_naming_rule.namespace_naming.severity = error 84 | dotnet_naming_symbols.namespace_symbol.applicable_kinds = namespace 85 | dotnet_naming_symbols.namespace_symbol.applicable_accessibilities = * 86 | 87 | # Interfaces 88 | dotnet_naming_rule.interface_naming.symbols = interface_symbol 89 | dotnet_naming_rule.interface_naming.style = interface_style 90 | dotnet_naming_rule.interface_naming.severity = error 91 | dotnet_naming_symbols.interface_symbol.applicable_kinds = interface 92 | dotnet_naming_symbols.interface_symbol.applicable_accessibilities = * 93 | 94 | # Classes, Structs, Enums, Properties, Methods, Events, Type Parameters 95 | dotnet_naming_rule.class_naming.symbols = class_symbol 96 | dotnet_naming_rule.class_naming.style = pascal_case_style 97 | dotnet_naming_rule.class_naming.severity = error 98 | 99 | dotnet_naming_symbols.class_symbol.applicable_kinds = class, struct, enum, property, method, event, type_parameter 100 | dotnet_naming_symbols.class_symbol.applicable_accessibilities = * 101 | 102 | # Const fields 103 | dotnet_naming_rule.const_field_naming.symbols = const_field_symbol 104 | dotnet_naming_rule.const_field_naming.style = pascal_case_style 105 | dotnet_naming_rule.const_field_naming.severity = error 106 | 107 | dotnet_naming_symbols.const_field_symbol.applicable_kinds = field 108 | dotnet_naming_symbols.const_field_symbol.applicable_accessibilities = * 109 | dotnet_naming_symbols.const_field_symbol.required_modifiers = const 110 | 111 | # Public fields 112 | dotnet_naming_rule.public_field_naming.symbols = public_field_symbol 113 | dotnet_naming_rule.public_field_naming.style = pascal_case_style 114 | dotnet_naming_rule.public_field_naming.severity = error 115 | 116 | dotnet_naming_symbols.public_field_symbol.applicable_kinds = field 117 | dotnet_naming_symbols.public_field_symbol.applicable_accessibilities = public, internal 118 | 119 | # Other fields 120 | dotnet_naming_rule.other_field_naming.symbols = other_field_symbol 121 | dotnet_naming_rule.other_field_naming.style = _camelCase 122 | dotnet_naming_rule.other_field_naming.severity = error 123 | 124 | dotnet_naming_symbols.other_field_symbol.applicable_kinds = field 125 | dotnet_naming_symbols.other_field_symbol.applicable_accessibilities = * 126 | 127 | # Everything Else 128 | dotnet_naming_rule.everything_else_naming.symbols = everything_else 129 | dotnet_naming_rule.everything_else_naming.style = camel_case_style 130 | dotnet_naming_rule.everything_else_naming.severity = error 131 | 132 | dotnet_naming_symbols.everything_else.applicable_kinds = * 133 | dotnet_naming_symbols.everything_else.applicable_accessibilities = * 134 | 135 | # CSharp code style settings: 136 | [*.cs] 137 | # Prefer "var" everywhere 138 | csharp_style_var_for_built_in_types = true:error 139 | csharp_style_var_when_type_is_apparent = true:error 140 | csharp_style_var_elsewhere = true:error 141 | 142 | csharp_style_expression_bodied_accessors = true:suggestion 143 | csharp_style_expression_bodied_constructors = true:none 144 | csharp_style_expression_bodied_indexers = true:suggestion 145 | # IDE0022: Use block body for methods 146 | csharp_style_expression_bodied_methods = true:suggestion 147 | csharp_style_expression_bodied_properties = true:suggestion 148 | 149 | csharp_style_inlined_variable_declaration = true:error 150 | 151 | # Require using directives inside a namespace 152 | csharp_using_directive_placement = inside_namespace:error 153 | csharp_indent_case_contents = true 154 | csharp_indent_case_contents_when_block = false 155 | csharp_preserve_single_line_blocks = true 156 | csharp_preserve_single_line_statements = true 157 | csharp_indent_switch_labels = true 158 | 159 | # Formatting - new line options 160 | 161 | # Place catch statements on a new line 162 | csharp_new_line_before_catch = true 163 | # Place else statements on a new line 164 | csharp_new_line_before_else = true 165 | # Require braces to be on a new line for methods, control_blocks, lambdas, types, and object_collection (also known as "Allman" style) 166 | csharp_new_line_before_open_brace = all 167 | # IDE0011: Add braces 168 | csharp_prefer_braces = when_multiline:error 169 | 170 | # Formatting - spacing options 171 | 172 | # Require NO space between a cast and the value 173 | csharp_space_after_cast = false 174 | # Require a space before the colon for bases or interfaces in a type declaration 175 | csharp_space_after_colon_in_inheritance_clause = true 176 | # Require a space after a keyword in a control flow statement such as a for loop 177 | csharp_space_after_keywords_in_control_flow_statements = true 178 | # Require a space before the colon for bases or interfaces in a type declaration 179 | csharp_space_before_colon_in_inheritance_clause = true 180 | # Remove space within empty argument list parentheses 181 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 182 | # Remove space between method call name and opening parenthesis 183 | csharp_space_between_method_call_name_and_opening_parenthesis = false 184 | # Do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 185 | csharp_space_between_method_call_parameter_list_parentheses = false 186 | # Remove space within empty parameter list parentheses for a method declaration 187 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 188 | # Place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 189 | csharp_space_between_method_declaration_parameter_list_parentheses = false 190 | 191 | # IDE0063: Use simple 'using' statement 192 | dotnet_diagnostic.IDE0063.severity = none 193 | -------------------------------------------------------------------------------- /test/RunScriptCommandTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections.Generic; 4 | using System.CommandLine.IO; 5 | using System.CommandLine.Rendering; 6 | 7 | public static class RunScriptCommandTests 8 | { 9 | [Trait("category", "unit")] 10 | public class FindScripts 11 | { 12 | private readonly Dictionary _projectScripts; 13 | 14 | public FindScripts() 15 | { 16 | _projectScripts = new Dictionary(StringComparer.OrdinalIgnoreCase) 17 | { 18 | { "clean", "echo clean" }, 19 | { "prebuild", "echo prebuild" }, 20 | { "build", "echo build" }, 21 | { "test", "echo test" }, 22 | { "posttest", "echo posttest" }, 23 | { "prepack", "echo pack" }, 24 | { "pack", "echo pack" }, 25 | { "postpack", "echo pack" }, 26 | { "foo", "foo" }, 27 | { "foo:foo", "foo:foo" }, 28 | { "foo:bar", "foo:bar" }, 29 | { "foo:baz", "foo:baz" }, 30 | { "foo:foo:foo", "foo:foo:foo" }, 31 | { "foo:foo:bar", "foo:foo:bar" }, 32 | { "foo:foo:baz", "foo:foo:baz" }, 33 | { "foo:bar:foo", "foo:bar:foo" }, 34 | { "foo:bar:bar", "foo:bar:bar" }, 35 | { "foo:bar:baz", "foo:bar:baz" }, 36 | { "foo:baz:foo", "foo:baz:foo" }, 37 | { "foo:baz:bar", "foo:baz:bar" }, 38 | { "foo:baz:baz", "foo:baz:baz" }, 39 | }; 40 | } 41 | 42 | [Fact] 43 | public void Should_match_exact_script_names() 44 | { 45 | // Given 46 | var scripts = new[] 47 | { 48 | "build", 49 | "test", 50 | "magic", 51 | }; 52 | 53 | // When 54 | var result = RunScriptCommand.FindScripts( 55 | _projectScripts, 56 | scripts); 57 | 58 | // Then 59 | result.ShouldBe([ 60 | new("build", true), 61 | new("test", true), 62 | new("magic", false), 63 | ]); 64 | } 65 | 66 | [Fact] 67 | public void Should_match_wildcard_script_names() 68 | { 69 | // Given 70 | var scripts = new[] 71 | { 72 | "foo*", 73 | "pre*", 74 | "magic*", 75 | }; 76 | 77 | // When 78 | var result = RunScriptCommand.FindScripts( 79 | _projectScripts, 80 | scripts); 81 | 82 | // Then 83 | result.ShouldBe([ 84 | new("foo", true), 85 | new("prebuild", true), 86 | new("prepack", true), 87 | new("magic*", false), 88 | ]); 89 | } 90 | 91 | [Fact] 92 | public void Should_match_only_1_segment() 93 | { 94 | // Given 95 | var scripts = new[] 96 | { 97 | "foo:*" 98 | }; 99 | 100 | // When 101 | var result = RunScriptCommand.FindScripts( 102 | _projectScripts, 103 | scripts); 104 | 105 | // Then 106 | result.ShouldBe([ 107 | new("foo:foo", true), 108 | new("foo:bar", true), 109 | new("foo:baz", true), 110 | ]); 111 | } 112 | 113 | [Fact] 114 | public void Should_match_only_1_trailing_segment() 115 | { 116 | // Given 117 | var scripts = new[] 118 | { 119 | "foo:bar:*" 120 | }; 121 | 122 | // When 123 | var result = RunScriptCommand.FindScripts( 124 | _projectScripts, 125 | scripts); 126 | 127 | // Then 128 | result.ShouldBe([ 129 | new("foo:bar:foo", true), 130 | new("foo:bar:bar", true), 131 | new("foo:bar:baz", true), 132 | ]); 133 | } 134 | 135 | [Fact] 136 | public void Should_match_multiple_segments() 137 | { 138 | // Given 139 | var scripts = new[] 140 | { 141 | "foo:**" 142 | }; 143 | 144 | // When 145 | var result = RunScriptCommand.FindScripts( 146 | _projectScripts, 147 | scripts); 148 | 149 | // Then 150 | result.ShouldBe([ 151 | new("foo:foo", true), 152 | new("foo:bar", true), 153 | new("foo:baz", true), 154 | new("foo:foo:foo", true), 155 | new("foo:foo:bar", true), 156 | new("foo:foo:baz", true), 157 | new("foo:bar:foo", true), 158 | new("foo:bar:bar", true), 159 | new("foo:bar:baz", true), 160 | new("foo:baz:foo", true), 161 | new("foo:baz:bar", true), 162 | new("foo:baz:baz", true), 163 | ]); 164 | } 165 | } 166 | 167 | [Trait("category", "unit")] 168 | public class RunResults 169 | { 170 | [Theory] 171 | [InlineData(0)] 172 | [InlineData(1)] 173 | [InlineData(13)] 174 | public async Task Should_handle_single_result(int exitCode) 175 | { 176 | // Given 177 | var (console, writer) = SetUpTest(); 178 | var results = new List 179 | { 180 | new("build", exitCode), 181 | }; 182 | 183 | // When 184 | var result = RunScriptCommand.RunResults( 185 | writer, 186 | results); 187 | 188 | // Then 189 | result.ShouldBe(exitCode); 190 | 191 | await Verify(console).UseParameters(exitCode); 192 | } 193 | 194 | [Fact] 195 | public async Task Should_handle_multiple_success_results() 196 | { 197 | // Given 198 | var (console, writer) = SetUpTest(); 199 | var results = new List 200 | { 201 | new("build", 0), 202 | new("test", 0), 203 | }; 204 | 205 | // When 206 | var result = RunScriptCommand.RunResults( 207 | writer, 208 | results); 209 | 210 | // Then 211 | result.ShouldBe(0); 212 | 213 | await Verify(console); 214 | } 215 | 216 | [Fact] 217 | public async Task Should_handle_multiple_error_results() 218 | { 219 | // Given 220 | var (console, writer) = SetUpTest(); 221 | var results = new List 222 | { 223 | new("build", 13), 224 | new("test", 99), 225 | }; 226 | 227 | // When 228 | var result = RunScriptCommand.RunResults( 229 | writer, 230 | results); 231 | 232 | // Then 233 | result.ShouldBe(1); 234 | 235 | await Verify(console); 236 | } 237 | 238 | [Theory] 239 | [InlineData(1)] 240 | [InlineData(13)] 241 | public async Task Should_handle_any_error_result(int exitCode) 242 | { 243 | // Given 244 | var (console, writer) = SetUpTest(); 245 | var results = new List 246 | { 247 | new("build", 0), 248 | new("test", exitCode), 249 | }; 250 | 251 | // When 252 | var result = RunScriptCommand.RunResults( 253 | writer, 254 | results); 255 | 256 | // Then 257 | result.ShouldBe(1); 258 | 259 | await Verify(console).UseParameters(exitCode); 260 | } 261 | 262 | private static (TestConsole console, IConsoleWriter writer) SetUpTest() 263 | { 264 | var console = new TestConsole(); 265 | var consoleWriter = new ConsoleWriter( 266 | console, 267 | new ConsoleFormatInfo 268 | { 269 | SupportsAnsiCodes = false, 270 | }, 271 | verbose: true); 272 | 273 | return (console, consoleWriter); 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | 400 | # Verify snapshots 401 | *.received.* 402 | -------------------------------------------------------------------------------- /test/CommandGroupRunnerTests.cs: -------------------------------------------------------------------------------- 1 | namespace RunScript; 2 | 3 | using System.Collections.Generic; 4 | using System.CommandLine.IO; 5 | using System.CommandLine.Rendering; 6 | using System.Threading.Tasks; 7 | 8 | [Trait("category", "unit")] 9 | public class CommandGroupRunnerTests 10 | { 11 | [Theory] 12 | [InlineData(true)] 13 | [InlineData(false)] 14 | public async Task Should_run_a_single_script(bool isWindows) 15 | { 16 | // Given 17 | TestCommandRunner[] commandRunners = 18 | { 19 | new(0), 20 | new(999), 21 | }; 22 | 23 | var (_, groupRunner) = SetUpTest(commandRunners, isWindows); 24 | 25 | // When 26 | var result = await groupRunner.RunAsync("clean", null); 27 | 28 | // Then 29 | result.ShouldBe(0); 30 | 31 | A.CallTo(() => groupRunner.BuildCommand()).MustHaveHappenedOnceExactly(); 32 | 33 | await Verify(commandRunners).UseParameters(isWindows); 34 | } 35 | 36 | [Theory] 37 | [InlineData(true)] 38 | [InlineData(false)] 39 | public async Task Should_run_with_a_pre_script(bool isWindows) 40 | { 41 | // Given 42 | TestCommandRunner[] commandRunners = 43 | { 44 | new(0), 45 | new(0), 46 | new(999), 47 | }; 48 | 49 | var (_, groupRunner) = SetUpTest(commandRunners, isWindows); 50 | 51 | // When 52 | var result = await groupRunner.RunAsync("build", null); 53 | 54 | // Then 55 | result.ShouldBe(0); 56 | 57 | A.CallTo(() => groupRunner.BuildCommand()).MustHaveHappenedTwiceExactly(); 58 | 59 | await Verify(commandRunners).UseParameters(isWindows); 60 | } 61 | 62 | [Theory] 63 | [InlineData(true)] 64 | [InlineData(false)] 65 | public async Task Should_run_with_a_post_script(bool isWindows) 66 | { 67 | // Given 68 | TestCommandRunner[] commandRunners = 69 | { 70 | new(0), 71 | new(0), 72 | new(999), 73 | }; 74 | 75 | var (_, groupRunner) = SetUpTest(commandRunners, isWindows); 76 | 77 | // When 78 | var result = await groupRunner.RunAsync("test", null); 79 | 80 | // Then 81 | result.ShouldBe(0); 82 | 83 | A.CallTo(() => groupRunner.BuildCommand()).MustHaveHappenedTwiceExactly(); 84 | 85 | await Verify(commandRunners).UseParameters(isWindows); 86 | } 87 | 88 | [Theory] 89 | [InlineData(true)] 90 | [InlineData(false)] 91 | public async Task Should_run_with_a_pre_and_post_script(bool isWindows) 92 | { 93 | // Given 94 | TestCommandRunner[] commandRunners = 95 | { 96 | new(0), 97 | new(0), 98 | new(0), 99 | new(999), 100 | }; 101 | 102 | var (_, groupRunner) = SetUpTest(commandRunners, isWindows); 103 | 104 | // When 105 | var result = await groupRunner.RunAsync("pack", null); 106 | 107 | // Then 108 | result.ShouldBe(0); 109 | 110 | A.CallTo(() => groupRunner.BuildCommand()).MustHaveHappened(3, Times.Exactly); 111 | 112 | await Verify(commandRunners).UseParameters(isWindows); 113 | } 114 | 115 | [Theory] 116 | [InlineData(true)] 117 | [InlineData(false)] 118 | public async Task Should_emit_environment_variable_list_when_env_script_not_defined(bool isWindows) 119 | { 120 | // Given 121 | TestCommandRunner[] commandRunners = 122 | { 123 | new(999), 124 | }; 125 | 126 | var (console, groupRunner) = SetUpTest(commandRunners, isWindows); 127 | 128 | // When 129 | var result = await groupRunner.RunAsync("env", null); 130 | 131 | // Then 132 | result.ShouldBe(0); 133 | 134 | A.CallTo(() => groupRunner.BuildCommand()).MustNotHaveHappened(); 135 | 136 | await Verify(console).UseParameters(isWindows); 137 | } 138 | 139 | [Theory] 140 | [InlineData(true)] 141 | [InlineData(false)] 142 | public async Task Should_emit_environment_variable_list_and_pre_and_post_scripts_when_env_script_not_defined(bool isWindows) 143 | { 144 | // Given 145 | TestCommandRunner[] commandRunners = 146 | { 147 | new(0), 148 | new(0), 149 | new(999), 150 | }; 151 | 152 | var (console, groupRunner) = SetUpTest( 153 | commandRunners, 154 | isWindows, 155 | scripts => 156 | { 157 | scripts.Add("preenv", "echo preenv"); 158 | scripts.Add("postenv", "echo postenv"); 159 | }); 160 | 161 | // When 162 | var result = await groupRunner.RunAsync("env", null); 163 | 164 | // Then 165 | result.ShouldBe(0); 166 | 167 | A.CallTo(() => groupRunner.BuildCommand()).MustHaveHappenedTwiceExactly(); 168 | 169 | await Verify( 170 | new 171 | { 172 | console, 173 | commandRunners, 174 | }) 175 | .UseParameters(isWindows); 176 | } 177 | 178 | [Theory] 179 | [InlineData(true)] 180 | [InlineData(false)] 181 | public async Task Should_run_env_script_if_defined(bool isWindows) 182 | { 183 | // Given 184 | TestCommandRunner[] commandRunners = 185 | { 186 | new(0), 187 | new(999), 188 | }; 189 | 190 | var (console, groupRunner) = SetUpTest( 191 | commandRunners, 192 | isWindows, 193 | scripts => scripts.Add("env", "echo env")); 194 | 195 | // When 196 | var result = await groupRunner.RunAsync("env", null); 197 | 198 | // Then 199 | result.ShouldBe(0); 200 | 201 | A.CallTo(() => groupRunner.BuildCommand()).MustHaveHappenedOnceExactly(); 202 | 203 | await Verify( 204 | new 205 | { 206 | console, 207 | commandRunners, 208 | }) 209 | .UseParameters(isWindows); 210 | } 211 | 212 | [Theory] 213 | [InlineData(true)] 214 | [InlineData(false)] 215 | public async Task Should_return_first_error_hit(bool isWindows) 216 | { 217 | // Given 218 | TestCommandRunner[] commandRunners = 219 | { 220 | new(0), 221 | new(123), 222 | new(0), 223 | new(999), 224 | }; 225 | 226 | var (_, groupRunner) = SetUpTest(commandRunners, isWindows); 227 | 228 | // When 229 | var result = await groupRunner.RunAsync("pack", null); 230 | 231 | // Then 232 | result.ShouldBe(123); 233 | 234 | A.CallTo(() => groupRunner.BuildCommand()).MustHaveHappenedTwiceExactly(); 235 | 236 | await Verify(commandRunners).UseParameters(isWindows); 237 | } 238 | 239 | private static (TestConsole console, CommandGroupRunner groupRunner) SetUpTest( 240 | TestCommandRunner[] commandRunners, 241 | bool isWindows, 242 | Action>? scriptSetup = null) 243 | { 244 | var console = new TestConsole(); 245 | var consoleFormatProvider = new ConsoleFormatInfo 246 | { 247 | SupportsAnsiCodes = false, 248 | }; 249 | var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: true); 250 | 251 | var scripts = new Dictionary(StringComparer.OrdinalIgnoreCase) 252 | { 253 | // clean 254 | { "clean", "echo clean" }, 255 | 256 | // build 257 | { "prebuild", "echo prebuild" }, 258 | { "build", "echo build" }, 259 | 260 | // test 261 | { "test", "echo test" }, 262 | { "posttest", "echo posttest" }, 263 | 264 | // pack 265 | { "prepack", "echo pack" }, 266 | { "pack", "echo pack" }, 267 | { "postpack", "echo pack" }, 268 | }; 269 | 270 | scriptSetup?.Invoke(scripts); 271 | 272 | var environment = new TestEnvironment( 273 | "/test/path", 274 | isWindows); 275 | 276 | environment.SetEnvironmentVariable("value1", "value 1"); 277 | environment.SetEnvironmentVariable("value2", "value 2"); 278 | environment.SetEnvironmentVariable("value3", "value 3"); 279 | 280 | var context = ProcessContext.Create( 281 | isWindows ? "cmd" : "sh", 282 | isWindows, 283 | "/test/path"); 284 | 285 | var groupRunner = A.Fake( 286 | o => o.WithArgumentsForConstructor( 287 | () => new CommandGroupRunner( 288 | consoleWriter, 289 | environment, 290 | scripts, 291 | context, 292 | true, 293 | default))); 294 | 295 | A.CallTo(() => groupRunner.BuildCommand()).ReturnsNextFromSequence(commandRunners); 296 | 297 | return (console, groupRunner); 298 | } 299 | 300 | private class TestCommandRunner : ICommandRunner 301 | { 302 | private readonly int _result; 303 | 304 | public TestCommandRunner(int result) 305 | => _result = result; 306 | 307 | public string? Name { get; private set; } 308 | 309 | public string? Cmd { get; private set; } 310 | 311 | public IReadOnlyList? Args { get; private set; } 312 | 313 | public Task RunAsync(string name, string cmd, string[]? args) 314 | { 315 | Name = name; 316 | Cmd = cmd; 317 | Args = args; 318 | 319 | return Task.FromResult(_result); 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /analysis/roslynator.editorconfig: -------------------------------------------------------------------------------- 1 | is_global = true 2 | 3 | # 4 | # Options 5 | 6 | roslynator_accessibility_modifiers = explicit 7 | roslynator_accessor_braces_style = single_line_when_expression_is_on_single_line 8 | roslynator_array_creation_type_style = implicit 9 | roslynator_arrow_token_new_line = before 10 | roslynator_binary_operator_new_line = after 11 | roslynator_blank_line_after_file_scoped_namespace_declaration = true 12 | roslynator_blank_line_between_closing_brace_and_switch_section = false 13 | roslynator_blank_line_between_single_line_accessors = false 14 | roslynator_blank_line_between_using_directives = separate_groups 15 | roslynator_block_braces_style = multi_line 16 | roslynator_body_style = expression 17 | roslynator_conditional_operator_condition_parentheses_style = omit 18 | roslynator_conditional_operator_new_line = before 19 | roslynator_configure_await = false 20 | roslynator_doc_comment_summary_style = multi_line 21 | roslynator_empty_string_style = literal 22 | roslynator_enum_flag_value_style = decimal_number 23 | roslynator_enum_has_flag_style = method 24 | roslynator_new_line_before_while_in_do_statement = true 25 | roslynator_infinite_loop_style = while 26 | roslynator_null_check_style = pattern_matching 27 | roslynator_object_creation_parentheses_style = omit 28 | roslynator_suppress_unity_script_methods = false 29 | roslynator_use_var_instead_of_implicit_object_creation = true 30 | 31 | # 32 | # Rules 33 | 34 | # RCS0001: Add blank line after embedded statement 35 | dotnet_diagnostic.RCS0001.severity = error 36 | 37 | # RCS0002: Add blank line after #region 38 | dotnet_diagnostic.RCS0002.severity = error 39 | 40 | # RCS0003: Add blank line after using directive list 41 | dotnet_diagnostic.RCS0003.severity = error 42 | 43 | # RCS0005: Add blank line before #endregion 44 | dotnet_diagnostic.RCS0005.severity = error 45 | 46 | # RCS0006: Add blank line before using directive list 47 | dotnet_diagnostic.RCS0006.severity = none 48 | 49 | # RCS0007: Add blank line between accessors 50 | dotnet_diagnostic.RCS0007.severity = none 51 | 52 | # RCS0008: Add blank line between closing brace and next statement 53 | dotnet_diagnostic.RCS0008.severity = error 54 | 55 | # RCS0009: Add blank line between declaration and documentation comment 56 | dotnet_diagnostic.RCS0009.severity = error 57 | 58 | # RCS0010: Add blank line between declarations 59 | dotnet_diagnostic.RCS0010.severity = error 60 | 61 | # RCS0011: Add/remove blank line between single-line accessors 62 | dotnet_diagnostic.RCS0011.severity = error 63 | 64 | # RCS0012: Add blank line between single-line declarations 65 | dotnet_diagnostic.RCS0012.severity = none 66 | 67 | # RCS0013: Add blank line between single-line declarations of different kind 68 | dotnet_diagnostic.RCS0013.severity = error 69 | 70 | # RCS0014: Add blank line between switch sections 71 | dotnet_diagnostic.RCS0014.severity = error 72 | 73 | # RCS0015: Add/remove blank line between using directives 74 | dotnet_diagnostic.RCS0015.severity = error 75 | 76 | # RCS0016: Put attribute list on its own line 77 | dotnet_diagnostic.RCS0016.severity = error 78 | 79 | # RCS0020: Format accessor's braces on a single line or multiple lines 80 | dotnet_diagnostic.RCS0020.severity = error 81 | 82 | # RCS0021: Format block's braces on a single line or multiple lines 83 | dotnet_diagnostic.RCS0021.severity = error 84 | 85 | # RCS0022: Add new line after opening brace of empty block 86 | dotnet_diagnostic.RCS0022.severity = error 87 | 88 | # RCS0023: Format type declaration's braces 89 | dotnet_diagnostic.RCS0023.severity = error 90 | 91 | # RCS0024: Add new line after switch label 92 | dotnet_diagnostic.RCS0024.severity = error 93 | 94 | # RCS0025: Put full accessor on its own line 95 | dotnet_diagnostic.RCS0025.severity = error 96 | 97 | # RCS0027: Place new line after/before binary operator 98 | dotnet_diagnostic.RCS0053.severity = error 99 | 100 | # RCS0028: Place new line after/before '?:' operator 101 | dotnet_diagnostic.RCS0053.severity = error 102 | 103 | # RCS0029: Put constructor initializer on its own line 104 | dotnet_diagnostic.RCS0029.severity = error 105 | 106 | # RCS0030: Add new line before embedded statement 107 | dotnet_diagnostic.RCS0030.severity = none 108 | 109 | # RCS0031: Put enum member on its own line 110 | dotnet_diagnostic.RCS0031.severity = error 111 | 112 | # RCS0032: Place new line after/before arrow token 113 | dotnet_diagnostic.RCS0032.severity = error 114 | 115 | # RCS0033: Add new line before statement 116 | dotnet_diagnostic.RCS0033.severity = error 117 | 118 | # RCS0034: Put type parameter constraint on its own line 119 | dotnet_diagnostic.RCS0034.severity = error 120 | 121 | # RCS0036: Remove blank line between single-line declarations of same kind 122 | dotnet_diagnostic.RCS0036.severity = none 123 | 124 | # RCS0038: Remove blank line between using directives with same root namespace 125 | dotnet_diagnostic.RCS0038.severity = error 126 | 127 | # RCS0039: Remove new line before base list 128 | dotnet_diagnostic.RCS0039.severity = error 129 | 130 | # RCS0041: Remove new line between 'if' keyword and 'else' keyword 131 | dotnet_diagnostic.RCS0041.severity = error 132 | 133 | # RCS0042: Put auto-accessors on a single line 134 | dotnet_diagnostic.RCS0042.severity = error 135 | 136 | # RCS0043: Format accessor's braces on a single line when expression is on single line 137 | dotnet_diagnostic.RCS0043.severity = error 138 | 139 | # RCS0044: Use carriage return + linefeed as new line 140 | dotnet_diagnostic.RCS0044.severity = none 141 | 142 | # RCS0045: Use linefeed as new line 143 | dotnet_diagnostic.RCS0045.severity = none 144 | 145 | # RCS0046: Use spaces instead of tab 146 | dotnet_diagnostic.RCS0046.severity = error 147 | 148 | # RCS0048: Put initializer on a single line 149 | dotnet_diagnostic.RCS0048.severity = none 150 | 151 | # RCS0049: Add blank line after top comment 152 | dotnet_diagnostic.RCS0049.severity = error 153 | 154 | # RCS0050: Add blank line before top declaration 155 | dotnet_diagnostic.RCS0050.severity = error 156 | 157 | # RCS0051: Add/remove new line before 'while' in 'do' statement 158 | dotnet_diagnostic.RCS0051.severity = error 159 | 160 | # RCS0052: Place new line after/before equals token 161 | dotnet_diagnostic.RCS0052.severity = none 162 | 163 | # RCS0053: Fix formatting of a list 164 | dotnet_diagnostic.RCS0053.severity = error 165 | 166 | # RCS0054: Fix formatting of a call chain 167 | dotnet_diagnostic.RCS0054.severity = error 168 | 169 | # RCS0055: Fix formatting of a binary expression chain 170 | dotnet_diagnostic.RCS0055.severity = error 171 | 172 | # RCS0056: A line is too long 173 | dotnet_diagnostic.RCS0056.severity = none 174 | 175 | # RCS0060: Add/remove line after file scoped namespace declaration 176 | dotnet_diagnostic.RCS0060.severity = error 177 | 178 | # RCS1001: Add braces (when expression spans over multiple lines) 179 | dotnet_diagnostic.RCS1001.severity = error 180 | 181 | # RCS1002: Remove braces 182 | dotnet_diagnostic.RCS1002.severity = none 183 | 184 | # RCS1003: Add braces to if-else (when expression spans over multiple lines) 185 | dotnet_diagnostic.RCS1003.severity = error 186 | 187 | # RCS1004: Remove braces from if-else 188 | dotnet_diagnostic.RCS1004.severity = none 189 | 190 | # RCS1005: Simplify nested using statement 191 | dotnet_diagnostic.RCS1005.severity = error 192 | 193 | # RCS1006: Merge 'else' with nested 'if' 194 | dotnet_diagnostic.RCS1006.severity = error 195 | 196 | # RCS1007: Add braces 197 | dotnet_diagnostic.RCS1007.severity = none 198 | 199 | # RCS1008: Use explicit type instead of 'var' (when the type is not obvious) 200 | dotnet_diagnostic.RCS1008.severity = none 201 | 202 | # RCS1009: Use explicit type instead of 'var' (foreach variable) 203 | dotnet_diagnostic.RCS1009.severity = none 204 | 205 | # RCS1010: Use 'var' instead of explicit type (when the type is obvious) 206 | dotnet_diagnostic.RCS1010.severity = error 207 | 208 | # RCS1012: Use explicit type instead of 'var' (when the type is obvious) 209 | dotnet_diagnostic.RCS1012.severity = none 210 | 211 | # RCS1013: Use predefined type 212 | dotnet_diagnostic.RCS1013.severity = error 213 | 214 | # RCS1014: Use explicitly/implicitly typed array 215 | dotnet_diagnostic.RCS1014.severity = error 216 | 217 | # RCS1015: Use nameof operator 218 | dotnet_diagnostic.RCS1015.severity = error 219 | 220 | # RCS1016: Use block body or expression body 221 | dotnet_diagnostic.RCS1016.severity = error 222 | 223 | # RCS1018: Add/remove accessibility modifiers 224 | dotnet_diagnostic.RCS1018.severity = error 225 | 226 | # RCS1019: Order modifiers 227 | dotnet_diagnostic.RCS1019.severity = error 228 | 229 | # RCS1020: Simplify Nullable to T? 230 | dotnet_diagnostic.RCS1020.severity = error 231 | 232 | # RCS1021: Convert lambda expression body to expression body 233 | dotnet_diagnostic.RCS1021.severity = error 234 | 235 | # RCS1031: Remove unnecessary braces 236 | dotnet_diagnostic.RCS1031.severity = error 237 | 238 | # RCS1032: Remove redundant parentheses 239 | dotnet_diagnostic.RCS1032.severity = error 240 | 241 | # RCS1033: Remove redundant boolean literal 242 | dotnet_diagnostic.RCS1033.severity = error 243 | 244 | # RCS1034: Remove redundant 'sealed' modifier 245 | dotnet_diagnostic.RCS1034.severity = error 246 | 247 | # RCS1035: Remove redundant comma in initializer 248 | dotnet_diagnostic.RCS1035.severity = none 249 | 250 | # RCS1036: Remove unnecessary blank line 251 | dotnet_diagnostic.RCS1036.severity = error 252 | 253 | # RCS1037: Remove trailing white-space 254 | dotnet_diagnostic.RCS1037.severity = error 255 | 256 | # RCS1038: Remove empty statement 257 | dotnet_diagnostic.RCS1038.severity = error 258 | 259 | # RCS1039: Remove argument list from attribute 260 | dotnet_diagnostic.RCS1039.severity = error 261 | 262 | # RCS1040: Remove empty 'else' clause 263 | dotnet_diagnostic.RCS1040.severity = error 264 | 265 | # RCS1041: Remove empty initializer 266 | dotnet_diagnostic.RCS1041.severity = error 267 | 268 | # RCS1042: Remove enum default underlying type 269 | dotnet_diagnostic.RCS1042.severity = error 270 | 271 | # RCS1043: Remove 'partial' modifier from type with a single part 272 | dotnet_diagnostic.RCS1043.severity = error 273 | 274 | # RCS1044: Remove original exception from throw statement 275 | dotnet_diagnostic.RCS1044.severity = error 276 | 277 | # RCS1046: Asynchronous method name should end with 'Async' 278 | dotnet_diagnostic.RCS1046.severity = error 279 | 280 | # RCS1047: Non-asynchronous method name should not end with 'Async' 281 | dotnet_diagnostic.RCS1047.severity = error 282 | 283 | # RCS1048: Use lambda expression instead of anonymous method 284 | dotnet_diagnostic.RCS1048.severity = error 285 | 286 | # RCS1049: Simplify boolean comparison 287 | dotnet_diagnostic.RCS1049.severity = error 288 | 289 | # RCS1050: Include/omit parentheses when creating new object 290 | dotnet_diagnostic.RCS1050.severity = error 291 | 292 | # RCS1051: Add/remove parentheses from condition in conditional operator 293 | dotnet_diagnostic.RCS1051.severity = error 294 | 295 | # RCS1052: Declare each attribute separately 296 | dotnet_diagnostic.RCS1052.severity = error 297 | 298 | # RCS1055: Avoid semicolon at the end of declaration 299 | dotnet_diagnostic.RCS1055.severity = error 300 | 301 | # RCS1056: Avoid usage of using alias directive 302 | dotnet_diagnostic.RCS1056.severity = none 303 | 304 | # RCS1058: Use compound assignment 305 | dotnet_diagnostic.RCS1058.severity = error 306 | 307 | # RCS1059: Avoid locking on publicly accessible instance 308 | dotnet_diagnostic.RCS1059.severity = error 309 | 310 | # RCS1060: Declare each type in separate file 311 | dotnet_diagnostic.RCS1060.severity = error 312 | 313 | # RCS1061: Merge 'if' with nested 'if' 314 | dotnet_diagnostic.RCS1061.severity = error 315 | 316 | # RCS1063: Avoid usage of do statement to create an infinite loop 317 | dotnet_diagnostic.RCS1063.severity = error 318 | 319 | # RCS1064: Avoid usage of for statement to create an infinite loop 320 | dotnet_diagnostic.RCS1064.severity = error 321 | 322 | # RCS1065: Avoid usage of while statement to create an infinite loop 323 | dotnet_diagnostic.RCS1065.severity = error 324 | 325 | # RCS1066: Remove empty 'finally' clause 326 | dotnet_diagnostic.RCS1066.severity = error 327 | 328 | # RCS1068: Simplify logical negation 329 | dotnet_diagnostic.RCS1068.severity = error 330 | 331 | # RCS1069: Remove unnecessary case label 332 | dotnet_diagnostic.RCS.severity = error 333 | 334 | # RCS1070: Remove redundant default switch section 335 | dotnet_diagnostic.RCS1070.severity = error 336 | 337 | # RCS1071: Remove redundant base constructor call 338 | dotnet_diagnostic.RCS1071.severity = error 339 | 340 | # RCS1072: Remove empty namespace declaration 341 | dotnet_diagnostic.RCS1072.severity = error 342 | 343 | # RCS1073: Convert 'if' to 'return' statement 344 | dotnet_diagnostic.RCS1073.severity = error 345 | 346 | # RCS1074: Remove redundant constructor 347 | dotnet_diagnostic.RCS1074.severity = error 348 | 349 | # RCS1075: Avoid empty catch clause that catches System.Exception 350 | dotnet_diagnostic.RCS1075.severity = error 351 | 352 | # RCS1077: Optimize LINQ method call 353 | dotnet_diagnostic.RCS1077.severity = error 354 | 355 | # RCS1078: Use "" or 'string.Empty' 356 | dotnet_diagnostic.RCS.severity = error 357 | 358 | # RCS1079: Throwing of new NotImplementedException 359 | dotnet_diagnostic.RCS.severity = warning 360 | 361 | # RCS1080: Use 'Count/Length' property instead of 'Any' method 362 | dotnet_diagnostic.RCS1080.severity = error 363 | 364 | # RCS1081: Split variable declaration 365 | dotnet_diagnostic.RCS1081.severity = error 366 | 367 | # RCS1084: Use coalesce expression instead of conditional expression 368 | dotnet_diagnostic.RCS1084.severity = error 369 | 370 | # RCS1085: Use auto-implemented property 371 | dotnet_diagnostic.RCS1085.severity = error 372 | 373 | # RCS1089: Use --/++ operator instead of assignment 374 | dotnet_diagnostic.RCS1089.severity = error 375 | 376 | # RCS1090: Add/remove 'ConfigureAwait(false)' call 377 | dotnet_diagnostic.RCS1090.severity = error 378 | 379 | # RCS1091: Remove empty region 380 | dotnet_diagnostic.RCS1091.severity = error 381 | 382 | # RCS1093: Remove file with no code 383 | dotnet_diagnostic.RCS1093.severity = error 384 | 385 | # RCS1094: Declare using directive on top level 386 | dotnet_diagnostic.RCS1094.severity = none 387 | 388 | # RCS1096: Use 'HasFlag' method or bitwise operator 389 | dotnet_diagnostic.RCS1096.severity = error 390 | 391 | # RCS1097: Remove redundant 'ToString' call 392 | dotnet_diagnostic.RCS1097.severity = error 393 | 394 | # RCS1098: Constant values should be placed on right side of comparisons 395 | dotnet_diagnostic.RCS1098.severity = error 396 | 397 | # RCS1099: Default label should be the last label in a switch section 398 | dotnet_diagnostic.RCS1099.severity = error 399 | 400 | # RCS1102: Make class static 401 | dotnet_diagnostic.RCS1102.severity = error 402 | 403 | # RCS1103: Convert 'if' to assignment 404 | dotnet_diagnostic.RCS1103.severity = error 405 | 406 | # RCS1104: Simplify conditional expression 407 | dotnet_diagnostic.RCS1104.severity = error 408 | 409 | # RCS1105: Unnecessary interpolation 410 | dotnet_diagnostic.RCS1105.severity = error 411 | 412 | # RCS1106: Remove empty destructor 413 | dotnet_diagnostic.RCS1106.severity = error 414 | 415 | # RCS1107: Remove redundant 'ToCharArray' call 416 | dotnet_diagnostic.RCS1107.severity = error 417 | 418 | # RCS1108: Add 'static' modifier to all partial class declarations 419 | dotnet_diagnostic.RCS1108.severity = error 420 | 421 | # RCS1110: Declare type inside namespace 422 | dotnet_diagnostic.RCS1110.severity = error 423 | 424 | # RCS1111: Add braces to switch section with multiple statements 425 | dotnet_diagnostic.RCS1111.severity = error 426 | 427 | # RCS1112: Combine 'Enumerable.Where' method chain 428 | dotnet_diagnostic.RCS.severity = suggestion 429 | 430 | # RCS1113: Use 'string.IsNullOrEmpty' method 431 | dotnet_diagnostic.RCS1113.severity = error 432 | 433 | # RCS1114: Remove redundant delegate creation 434 | dotnet_diagnostic.RCS1114.severity = error 435 | 436 | # RCS1118: Mark local variable as const 437 | dotnet_diagnostic.RCS1118.severity = error 438 | 439 | # RCS1123: Add parentheses when necessary 440 | dotnet_diagnostic.RCS1123.severity = error 441 | 442 | # RCS1124: Inline local variable 443 | dotnet_diagnostic.RCS1124.severity = error 444 | 445 | # RCS1126: Add braces to if-else 446 | dotnet_diagnostic.RCS1126.severity = error 447 | 448 | # RCS1128: Use coalesce expression 449 | dotnet_diagnostic.RCS1128.severity = error 450 | 451 | # RCS1129: Remove redundant field initialization 452 | dotnet_diagnostic.RCS1129.severity = error 453 | 454 | # RCS1130: Bitwise operation on enum without Flags attribute 455 | dotnet_diagnostic.RCS1130.severity = error 456 | 457 | # RCS1132: Remove redundant overriding member 458 | dotnet_diagnostic.RCS1132.severity = error 459 | 460 | # RCS1133: Remove redundant Dispose/Close call 461 | dotnet_diagnostic.RCS1133.severity = error 462 | 463 | # RCS1134: Remove redundant statement 464 | dotnet_diagnostic.RCS1134.severity = error 465 | 466 | # RCS1135: Declare enum member with zero value (when enum has FlagsAttribute) 467 | dotnet_diagnostic.RCS1135.severity = error 468 | 469 | # RCS1136: Merge switch sections with equivalent content 470 | dotnet_diagnostic.RCS1136.severity = error 471 | 472 | # RCS1138: Add summary to documentation comment 473 | dotnet_diagnostic.RCS1138.severity = none 474 | 475 | # RCS1139: Add summary element to documentation comment 476 | dotnet_diagnostic.RCS1139.severity = error 477 | 478 | # RCS1140: Add exception to documentation comment 479 | dotnet_diagnostic.RCS1140.severity = error 480 | 481 | # RCS1141: Add 'param' element to documentation comment 482 | dotnet_diagnostic.RCS1141.severity = error 483 | 484 | # RCS1142: Add 'typeparam' element to documentation comment 485 | dotnet_diagnostic.RCS1142.severity = error 486 | 487 | # RCS1143: Simplify coalesce expression 488 | dotnet_diagnostic.RCS1143.severity = error 489 | 490 | # RCS1145: Remove redundant 'as' operator 491 | dotnet_diagnostic.RCS1145.severity = error 492 | 493 | # RCS1146: Use conditional access 494 | dotnet_diagnostic.RCS1146.severity = error 495 | 496 | # RCS1151: Remove redundant cast 497 | dotnet_diagnostic.RCS1151.severity = error 498 | 499 | # RCS1154: Sort enum members 500 | dotnet_diagnostic.RCS1154.severity = error 501 | 502 | # RCS1155: Use StringComparison when comparing strings 503 | dotnet_diagnostic.RCS1155.severity = error 504 | 505 | # RCS1156: Use string.Length instead of comparison with empty string 506 | dotnet_diagnostic.RCS1156.severity = error 507 | 508 | # RCS1157: Composite enum value contains undefined flag 509 | dotnet_diagnostic.RCS1157.severity = error 510 | 511 | # RCS1158: Static member in generic type should use a type parameter 512 | dotnet_diagnostic.RCS1158.severity = error 513 | 514 | # RCS1159: Use EventHandler 515 | dotnet_diagnostic.RCS1159.severity = error 516 | 517 | # RCS1160: Abstract type should not have public constructors 518 | dotnet_diagnostic.RCS1160.severity = error 519 | 520 | # RCS1161: Enum should declare explicit values 521 | dotnet_diagnostic.RCS1161.severity = error 522 | 523 | # RCS1162: Avoid chain of assignments 524 | dotnet_diagnostic.RCS1162.severity = error 525 | 526 | # RCS1163: Unused parameter 527 | dotnet_diagnostic.RCS1163.severity = error 528 | 529 | # RCS1164: Unused type parameter 530 | dotnet_diagnostic.RCS1164.severity = error 531 | 532 | # RCS1165: Unconstrained type parameter checked for null 533 | dotnet_diagnostic.RCS1165.severity = error 534 | 535 | # RCS1166: Value type object is never equal to null 536 | dotnet_diagnostic.RCS1166.severity = error 537 | 538 | # RCS1168: Parameter name differs from base name 539 | dotnet_diagnostic.RCS1168.severity = error 540 | 541 | # RCS1169: Make field read-only 542 | dotnet_diagnostic.RCS1169.severity = error 543 | 544 | # RCS1170: Use read-only auto-implemented property 545 | dotnet_diagnostic.RCS1170.severity = error 546 | 547 | # RCS1171: Simplify lazy initialization 548 | dotnet_diagnostic.RCS1171.severity = error 549 | 550 | # RCS1172: Use 'is' operator instead of 'as' operator 551 | dotnet_diagnostic.RCS1172.severity = error 552 | 553 | # RCS1173: Use coalesce expression instead of 'if' 554 | dotnet_diagnostic.RCS1173.severity = error 555 | 556 | # RCS1174: Remove redundant async/await 557 | dotnet_diagnostic.RCS1174.severity = error 558 | 559 | # RCS1175: Unused 'this' parameter 560 | dotnet_diagnostic.RCS1175.severity = error 561 | 562 | # RCS1177: Use 'var' instead of explicit type (in foreach) 563 | dotnet_diagnostic.RCS1177.severity = error 564 | 565 | # RCS1179: Unnecessary assignment 566 | dotnet_diagnostic.RCS1179.severity = error 567 | 568 | # RCS1180: Inline lazy initialization 569 | dotnet_diagnostic.RCS1180.severity = error 570 | 571 | # RCS1181: Convert comment to documentation comment 572 | dotnet_diagnostic.RCS1181.severity = none 573 | 574 | # RCS1182: Remove redundant base interface 575 | dotnet_diagnostic.RCS1182.severity = error 576 | 577 | # RCS1186: Use Regex instance instead of static method 578 | dotnet_diagnostic.RCS1186.severity = error 579 | 580 | # RCS1187: Use constant instead of field 581 | dotnet_diagnostic.RCS1187.severity = error 582 | 583 | # RCS1188: Remove redundant auto-property initialization 584 | dotnet_diagnostic.RCS1188.severity = error 585 | 586 | # RCS1189: Add or remove region name 587 | dotnet_diagnostic.RCS1189.severity = none 588 | 589 | # RCS1190: Join string expressions 590 | dotnet_diagnostic.RCS1190.severity = error 591 | 592 | # RCS1191: Declare enum value as combination of names 593 | dotnet_diagnostic.RCS1191.severity = error 594 | 595 | # RCS1192: Unnecessary usage of verbatim string literal 596 | dotnet_diagnostic.RCS1192.severity = error 597 | 598 | # RCS1193: Overriding member should not change 'params' modifier 599 | dotnet_diagnostic.RCS1193.severity = error 600 | 601 | # RCS1194: Implement exception constructors 602 | dotnet_diagnostic.RCS1194.severity = error 603 | 604 | # RCS1195: Use ^ operator 605 | dotnet_diagnostic.RCS1195.severity = error 606 | 607 | # RCS1196: Call extension method as instance method 608 | dotnet_diagnostic.RCS1196.severity = error 609 | 610 | # RCS1197: Optimize StringBuilder.Append/AppendLine call 611 | dotnet_diagnostic.RCS1197.severity = error 612 | 613 | # RCS1198: Avoid unnecessary boxing of value type 614 | dotnet_diagnostic.RCS1198.severity = none 615 | 616 | # RCS1199: Unnecessary null check 617 | dotnet_diagnostic.RCS1199.severity = error 618 | 619 | # RCS1200: Call 'Enumerable.ThenBy' instead of 'Enumerable.OrderBy' 620 | dotnet_diagnostic.RCS1200.severity = error 621 | 622 | # RCS1201: Use method chaining 623 | dotnet_diagnostic.RCS.severity = suggestion 624 | 625 | # RCS1202: Avoid NullReferenceException 626 | dotnet_diagnostic.RCS1202.severity = error 627 | 628 | # RCS1203: Use AttributeUsageAttribute 629 | dotnet_diagnostic.RCS1203.severity = error 630 | 631 | # RCS1204: Use EventArgs.Empty 632 | dotnet_diagnostic.RCS1204.severity = error 633 | 634 | # RCS1205: Order named arguments according to the order of parameters 635 | dotnet_diagnostic.RCS1205.severity = error 636 | 637 | # RCS1206: Use conditional access instead of conditional expression 638 | dotnet_diagnostic.RCS1206.severity = error 639 | 640 | # RCS1207: Use anonymous function or method group 641 | dotnet_diagnostic.RCS1207.severity = error 642 | 643 | # RCS1208: Reduce 'if' nesting 644 | dotnet_diagnostic.RCS1208.severity = suggestion 645 | 646 | # RCS1209: Order type parameter constraints 647 | dotnet_diagnostic.RCS1209.severity = error 648 | 649 | # RCS1210: Return completed task instead of returning null 650 | dotnet_diagnostic.RCS1210.severity = none 651 | 652 | # RCS1211: Remove unnecessary 'else' 653 | dotnet_diagnostic.RCS1211.severity = error 654 | 655 | # RCS1212: Remove redundant assignment 656 | dotnet_diagnostic.RCS1212.severity = error 657 | 658 | # RCS1213: Remove unused member declaration 659 | dotnet_diagnostic.RCS1213.severity = error 660 | 661 | # RCS1214: Unnecessary interpolated string 662 | dotnet_diagnostic.RCS1214.severity = error 663 | 664 | # RCS1215: Expression is always equal to true/false 665 | dotnet_diagnostic.RCS1215.severity = error 666 | 667 | # RCS1216: Unnecessary unsafe context 668 | dotnet_diagnostic.RCS1216.severity = error 669 | 670 | # RCS1217: Convert interpolated string to concatenation 671 | dotnet_diagnostic.RCS1217.severity = none 672 | 673 | # RCS1218: Simplify code branching 674 | dotnet_diagnostic.RCS1218.severity = error 675 | 676 | # RCS1220: Use pattern matching instead of combination of 'is' operator and cast operator 677 | dotnet_diagnostic.RCS1220.severity = error 678 | 679 | # RCS1221: Use pattern matching instead of combination of 'as' operator and null check 680 | dotnet_diagnostic.RCS1221.severity = error 681 | 682 | # RCS1222: Merge preprocessor directives 683 | dotnet_diagnostic.RCS1222.severity = error 684 | 685 | # RCS1223: Mark publicly visible type with DebuggerDisplay attribute 686 | dotnet_diagnostic.RCS1223.severity = none 687 | 688 | # RCS1224: Make method an extension method 689 | dotnet_diagnostic.RCS1224.severity = suggestion 690 | 691 | # RCS1225: Make class sealed 692 | dotnet_diagnostic.RCS1225.severity = error 693 | 694 | # RCS1226: Add paragraph to documentation comment 695 | dotnet_diagnostic.RCS1226.severity = error 696 | 697 | # RCS1227: Validate arguments correctly 698 | dotnet_diagnostic.RCS1227.severity = error 699 | 700 | # RCS1228: Unused element in documentation comment 701 | dotnet_diagnostic.RCS1228.severity = error 702 | 703 | # RCS1229: Use async/await when necessary 704 | dotnet_diagnostic.RCS1229.severity = error 705 | 706 | # RCS1230: Unnecessary explicit use of enumerator 707 | dotnet_diagnostic.RCS1230.severity = error 708 | 709 | # RCS1231: Make parameter ref read-only 710 | dotnet_diagnostic.RCS1231.severity = none 711 | 712 | # RCS1232: Order elements in documentation comment 713 | dotnet_diagnostic.RCS1232.severity = error 714 | 715 | # RCS1233: Use short-circuiting operator 716 | dotnet_diagnostic.RCS1233.severity = error 717 | 718 | # RCS1234: Duplicate enum value 719 | dotnet_diagnostic.RCS1234.severity = error 720 | 721 | # RCS1235: Optimize method call 722 | dotnet_diagnostic.RCS1235.severity = error 723 | 724 | # RCS1236: Use exception filter 725 | dotnet_diagnostic.RCS1236.severity = error 726 | 727 | # RCS1237: Use bit shift operator 728 | dotnet_diagnostic.RCS1237.severity = none 729 | 730 | # RCS1238: Avoid nested ?: operators 731 | dotnet_diagnostic.RCS1238.severity = error 732 | 733 | # RCS1239: Use 'for' statement instead of 'while' statement 734 | dotnet_diagnostic.RCS1239.severity = error 735 | 736 | # RCS1240: Operator is unnecessary 737 | dotnet_diagnostic.RCS1240.severity = error 738 | 739 | # RCS1241: Implement non-generic counterpart 740 | dotnet_diagnostic.RCS1241.severity = error 741 | 742 | # RCS1242: Do not pass non-read-only struct by read-only reference 743 | dotnet_diagnostic.RCS1242.severity = error 744 | 745 | # RCS1243: Duplicate word in a comment 746 | dotnet_diagnostic.RCS1243.severity = error 747 | 748 | # RCS1244: Simplify 'default' expression 749 | dotnet_diagnostic.RCS1244.severity = error 750 | 751 | # RCS1246: Use element access 752 | dotnet_diagnostic.RCS1246.severity = error 753 | 754 | # RCS1247: Fix documentation comment tag 755 | dotnet_diagnostic.RCS1247.severity = error 756 | 757 | # RCS1248: Normalize null check 758 | dotnet_diagnostic.RCS1248.severity = error 759 | 760 | # RCS1249: Unnecessary null-forgiving operator 761 | dotnet_diagnostic.RCS1249.severity = error 762 | 763 | # RCS1250: Use implicit/explicit object creation 764 | dotnet_diagnostic.RCS1250.severity = error 765 | 766 | # RCS1251: Remove unnecessary braces from record declaration 767 | dotnet_diagnostic.RCS1251.severity = error 768 | 769 | # RCS1252: Normalize usage of infinite loop 770 | dotnet_diagnostic.RCS1252.severity = error 771 | 772 | # RCS1253: Normalize format of enum flag value 773 | dotnet_diagnostic.RCS1253.severity = error 774 | 775 | # RCS1254: Normalize format of enum flag value 776 | dotnet_diagnostic.RCS1254.severity = error 777 | 778 | # RCS1264: Use 'var' or explicit type 779 | dotnet_diagnostic.RCS1264.severity = error 780 | 781 | # RCS9001: Use pattern matching 782 | dotnet_diagnostic.RCS9001.severity = error 783 | 784 | # RCS9002: Use property SyntaxNode.SpanStart 785 | dotnet_diagnostic.RCS9002.severity = error 786 | 787 | # RCS9003: Unnecessary conditional access 788 | dotnet_diagnostic.RCS9003.severity = error 789 | 790 | # RCS9004: Call 'Any' instead of accessing 'Count' 791 | dotnet_diagnostic.RCS9004.severity = error 792 | 793 | # RCS9005: Unnecessary null check 794 | dotnet_diagnostic.RCS9005.severity = error 795 | 796 | # RCS9006: Use element access 797 | dotnet_diagnostic.RCS9006.severity = error 798 | 799 | # RCS9007: Use return value 800 | dotnet_diagnostic.RCS9007.severity = error 801 | 802 | # RCS9008: Call 'Last' instead of using [] 803 | dotnet_diagnostic.RCS9008.severity = error 804 | 805 | # RCS9009: Unknown language name 806 | dotnet_diagnostic.RCS9009.severity = error 807 | 808 | # RCS9010: Specify ExportCodeRefactoringProviderAttribute.Name 809 | dotnet_diagnostic.RCS9010.severity = error 810 | 811 | # RCS9011: Specify ExportCodeFixProviderAttribute.Name 812 | dotnet_diagnostic.RCS9011.severity = error 813 | 814 | # 815 | # Refactorings 816 | 817 | # Enable all refactorings 818 | roslynator_refactorings.enabled = true 819 | 820 | # 821 | # Compiler diagnostic fixes 822 | 823 | # Enable all compiler diagnostic fixes 824 | roslynator_compiler_diagnostic_fixes.enabled = true 825 | --------------------------------------------------------------------------------