Skip to content

Commit 58c560e

Browse files
authored
Merge pull request #10 from xt0rted/multiple-script-execution
Add the ability to run multiple scripts in series
2 parents e0ea1d0 + a193c3e commit 58c560e

File tree

70 files changed

+1798
-239
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1798
-239
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ indent_size = 2
2121
[*.json]
2222
indent_size = 2
2323

24+
[*.verified.txt]
25+
insert_final_newline = false
26+
2427
[*.yml]
2528
indent_size = 2
2629

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ jobs:
161161

162162
- run: dotnet r integration:ci --verbose -- --logger "trx;LogFilePrefix=ci test"
163163

164+
- run: dotnet r clean:bin build test --verbose
165+
164166
release:
165167
if: github.event_name == 'push'
166168

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- added: Ability to run multiple scripts in a single call (e.g. `dotnet r build test pack`)
6+
57
## [0.3.0](https://github.com/xt0rted/dotnet-run-script/compare/v0.2.0...v0.3.0) - 2022-04-24
68

79
- fixed: Don't escape the script passed to `cmd.exe`, just the double dash arguments

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ Name | Description
4040
`--version` | Show version information
4141
`--help` | Show help and usage information
4242

43+
Arguments passed after the double dash are passed through to the executing script.
44+
45+
```console
46+
dotnet r build --verbose -- --configuration Release
47+
```
48+
4349
### Color output
4450

4551
This tool supports the `DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION` environment variable.
@@ -82,7 +88,7 @@ You can override this with your own command if you wish.
8288

8389
## Usage
8490

85-
Use `dotnet r <command>` to run the scripts.
91+
Use `dotnet r [<scripts>...] [options]` to run the scripts.
8692
Anything you can run from the command line can be used in a script.
8793
You can also call other scripts to chain them together such as a `ci` script that calls the `build`, `test`, and `package` scripts.
8894

@@ -101,12 +107,44 @@ This is an example of a `pre` script that clears the build artifacts folder, and
101107
}
102108
```
103109

110+
### Multiple script execution
111+
112+
Multiple scripts can be called at the same time like so:
113+
114+
```console
115+
dotnet r build test
116+
```
117+
118+
This will run the `build` script and if it returns a `0` exit code it will then run the `test` script.
119+
The `--if-present` option can be used to skip scripts that don't exist.
120+
121+
```json
122+
{
123+
"scripts": {
124+
"build": "dotnet build",
125+
"test:unit": "dotnet test",
126+
"package": "dotnet pack"
127+
}
128+
}
129+
```
130+
131+
```console
132+
dotnet r build test:unit test:integration package --if-present
133+
```
134+
135+
Arguments passed after the double dash are passed through to each executing script.
136+
In this example both the `--configuration` and `--framework` options will be passed to each of the four scripts when running them.
137+
138+
```console
139+
dotnet r build test:unit test:integration package -- --configuration Release --framework net6.0
140+
```
141+
104142
### Working directory
105143

106144
The working directory is set to the root of the project where the `global.json` is located.
107145
If you need to get the folder the command was executed from you can do so using the `INIT_CWD` environment variable.
108146

109-
### Common build environments
147+
## Common build environments
110148

111149
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`.
112150
These might not be defined in all of your projects and if a script that doesn't exist is called an error is returned.

global.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
"test:31": "dotnet r test -- --framework netcoreapp3.1",
1414
"test:5": "dotnet r test -- --framework net5.0",
1515
"test:6": "dotnet r test -- --framework net6.0",
16+
17+
"testbase": "dotnet test --no-build --logger \"trx;LogFilePrefix=tests\" --results-directory \"./.coverage\"",
18+
"test:unit": "dotnet r testbase -- --filter \"category=unit\"",
19+
"test:int": "dotnet r testbase -- --filter \"category=integration\"",
20+
1621
"pack": "dotnet pack --no-build --output \"./artifacts\"",
1722

1823
"build:release": "dotnet r build -- --configuration Release",

src/CommandBuilder.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace RunScript;
2+
3+
using System.Text.RegularExpressions;
4+
5+
internal class CommandBuilder
6+
{
7+
// This is the same regex used by npm's run-script library
8+
public static readonly Regex IsCmdCheck = new("(?:^|\\\\)cmd(?:\\.exe)?$", RegexOptions.IgnoreCase);
9+
10+
private readonly IConsoleWriter _writer;
11+
private readonly IEnvironment _environment;
12+
private readonly Project _project;
13+
private readonly string _workingDirectory;
14+
private readonly bool _captureOutput;
15+
16+
public CommandBuilder(
17+
IConsoleWriter writer,
18+
IEnvironment environment,
19+
Project project,
20+
string workingDirectory,
21+
bool captureOutput)
22+
{
23+
if (string.IsNullOrEmpty(workingDirectory)) throw new ArgumentException($"'{nameof(workingDirectory)}' cannot be null or empty.", nameof(workingDirectory));
24+
25+
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
26+
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
27+
_project = project ?? throw new ArgumentNullException(nameof(project));
28+
_workingDirectory = workingDirectory;
29+
_captureOutput = captureOutput;
30+
}
31+
32+
public ProcessContext? ProcessContext { get; private set; }
33+
34+
public void SetUpEnvironment(string? scriptShellOverride)
35+
{
36+
var (scriptShell, isCmd) = GetScriptShell(scriptShellOverride ?? _project.ScriptShell);
37+
38+
ProcessContext = ProcessContext.Create(scriptShell, isCmd, _workingDirectory);
39+
40+
_writer.LineVerbose("Using shell: {0}", scriptShell);
41+
_writer.BlankLine();
42+
}
43+
44+
public ICommandGroupRunner CreateGroupRunner(CancellationToken cancellationToken)
45+
=> new CommandGroupRunner(
46+
_writer,
47+
_environment,
48+
_project.Scripts!,
49+
ProcessContext!,
50+
_captureOutput,
51+
cancellationToken);
52+
53+
/// <summary>
54+
/// Gets the script shell to use.
55+
/// </summary>
56+
/// <param name="shell">A optional custom shell to use instead of the system default.</param>
57+
/// <returns>The shell to use and if it's <c>cmd</c> or not.</returns>
58+
private (string shell, bool isCmd) GetScriptShell(string? shell)
59+
{
60+
shell ??= _environment.IsWindows
61+
? _environment.GetEnvironmentVariable("COMSPEC") ?? "cmd"
62+
: "sh";
63+
64+
var isCmd = IsCmdCheck.IsMatch(shell);
65+
66+
return (shell, isCmd);
67+
}
68+
}

src/CommandGroupRunner.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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<string, string?> _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<string, string?> 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<int> RunAsync(string name, string[]? scriptArgs)
38+
{
39+
var scriptNames = ImmutableArray.Create(new[] { "pre" + name, name, "post" + name });
40+
41+
foreach (var subScript in scriptNames.Where(scriptName => _scripts.ContainsKey(scriptName) || scriptName == "env"))
42+
{
43+
// At this point we should have done enough checks to make sure the only not found script is `env`
44+
if (!_scripts.ContainsKey(subScript))
45+
{
46+
GlobalCommands.PrintEnvironmentVariables(_writer, _environment);
47+
48+
continue;
49+
}
50+
51+
var command = BuildCommand();
52+
53+
var args = subScript == name
54+
? scriptArgs
55+
: null;
56+
57+
var result = await command.RunAsync(
58+
subScript,
59+
_scripts[subScript]!,
60+
args);
61+
62+
if (result != 0)
63+
{
64+
return result;
65+
}
66+
}
67+
68+
return 0;
69+
}
70+
}

src/CommandLineBuilderExtensions.cs

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/CommandRunner.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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<int> RunAsync(string name, string cmd, string[]? args)
25+
{
26+
_cancellationToken.ThrowIfCancellationRequested();
27+
28+
_writer.Banner(name, ArgumentBuilder.ConcatinateCommandAndArgArrayForDisplay(cmd, args));
29+
30+
using (var process = new Process())
31+
{
32+
process.StartInfo.WorkingDirectory = _processContext.WorkingDirectory;
33+
process.StartInfo.FileName = _processContext.Shell;
34+
35+
var outStream = new StreamForwarder();
36+
var errStream = new StreamForwarder();
37+
Task? taskOut = null;
38+
Task? taskErr = null;
39+
40+
if (_captureOutput)
41+
{
42+
process.StartInfo.RedirectStandardOutput = true;
43+
process.StartInfo.RedirectStandardError = true;
44+
45+
outStream.Capture();
46+
errStream.Capture();
47+
}
48+
49+
if (_processContext.IsCmd)
50+
{
51+
process.StartInfo.Arguments = string.Concat(
52+
"/d /s /c \"",
53+
ArgumentBuilder.EscapeAndConcatenateCommandAndArgArrayForCmdProcessStart(cmd, args),
54+
"\"");
55+
}
56+
else
57+
{
58+
process.StartInfo.ArgumentList.Add("-c");
59+
process.StartInfo.ArgumentList.Add(ArgumentBuilder.EscapeAndConcatenateCommandAndArgArrayForProcessStart(cmd, args));
60+
}
61+
62+
process.Start();
63+
64+
if (_captureOutput)
65+
{
66+
taskOut = outStream.BeginReadAsync(process.StandardOutput);
67+
taskErr = errStream.BeginReadAsync(process.StandardError);
68+
}
69+
70+
await process.WaitForExitAsync(_cancellationToken);
71+
72+
if (_captureOutput)
73+
{
74+
await taskOut!.WaitAsync(_cancellationToken);
75+
await taskErr!.WaitAsync(_cancellationToken);
76+
77+
_writer.Raw(outStream.CapturedOutput);
78+
_writer.Error(errStream.CapturedOutput);
79+
}
80+
81+
return process.ExitCode;
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)