Skip to content

Commit 38ec478

Browse files
authored
Merge pull request #438 from adamralph/windows
Automatically resolve .cmd and .bat paths on Windows
2 parents 46eed86 + 49303eb commit 38ec478

5 files changed

Lines changed: 113 additions & 61 deletions

File tree

‎README.md‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,6 @@ var extraFilesOrDirectoriesDetected = exitCode & 2;
107107
var misMatchedFilesOrDirectoriesDetected = exitCode & 4;
108108
```
109109

110-
### Windows
111-
112-
🙄
113-
114-
Sometimes, for whatever wonderful reasons, it's necessary to run a different command on Windows. For example, when running [Yarn](https://yarnpkg.com), some combination of mysterious factors may require that you explicitly run `cmd.exe` with Yarn as an argument, rather than running Yarn directly. The optional `windowsNames` and `windowsArgs` parameters may be used to specify a different command name and arguments for Windows:
115-
116-
```c#
117-
Run("yarn", windowsName: "cmd", windowsArgs: "/c yarn");
118-
```
119-
120110
---
121111

122112
<sub>[Run](https://thenounproject.com/term/target/975371) by [Gregor Cresnar](https://thenounproject.com/grega.cresnar/) from [the Noun Project](https://thenounproject.com/).</sub>

‎SimpleExec/Command.cs‎

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.IO;
45
using System.Linq;
56
using System.Reflection;
67
using System.Runtime.InteropServices;
@@ -15,6 +16,9 @@ namespace SimpleExec
1516
/// </summary>
1617
public static class Command
1718
{
19+
// an experiment in Windows revealed this order of precedence
20+
private static readonly List<string> windowsExecutableExtensions = new List<string> { "exe", "bat", "cmd" };
21+
1822
private static readonly Action<IDictionary<string, string?>> defaultAction = _ => { };
1923
private static readonly string defaultEchoPrefix = Assembly.GetEntryAssembly()?.GetName().Name ?? "SimpleExec";
2024

@@ -26,8 +30,6 @@ public static class Command
2630
/// <param name="args">The arguments to pass to the command.</param>
2731
/// <param name="workingDirectory">The working directory in which to run the command.</param>
2832
/// <param name="noEcho">Whether or not to echo the resulting command line and working directory (if specified) to standard output (stdout).</param>
29-
/// <param name="windowsName">The name of the command to use on Windows only.</param>
30-
/// <param name="windowsArgs">The arguments to pass to the command on Windows only.</param>
3133
/// <param name="echoPrefix">The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout).</param>
3234
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
3335
/// <param name="createNoWindow">Whether to run the command in a new window.</param>
@@ -47,17 +49,15 @@ public static void Run(
4749
string args = "",
4850
string workingDirectory = "",
4951
bool noEcho = false,
50-
string? windowsName = null,
51-
string? windowsArgs = null,
5252
string? echoPrefix = null,
5353
Action<IDictionary<string, string?>>? configureEnvironment = null,
5454
bool createNoWindow = false,
5555
Func<int, bool>? handleExitCode = null,
5656
CancellationToken cancellationToken = default) =>
5757
ProcessStartInfo
5858
.Create(
59-
Validate(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name),
60-
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args,
59+
Resolve(Validate(name)),
60+
args,
6161
Enumerable.Empty<string>(),
6262
workingDirectory,
6363
false,
@@ -76,11 +76,6 @@ public static void Run(
7676
/// </param>
7777
/// <param name="workingDirectory">The working directory in which to run the command.</param>
7878
/// <param name="noEcho">Whether or not to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout).</param>
79-
/// <param name="windowsName">The name of the command to use on Windows only.</param>
80-
/// <param name="windowsArgs">
81-
/// The arguments to pass to the command on Windows only.
82-
/// As with <see cref="System.Diagnostics.ProcessStartInfo.ArgumentList"/>, the strings don't need to be escaped.
83-
/// </param>
8479
/// <param name="echoPrefix">The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout).</param>
8580
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
8681
/// <param name="createNoWindow">Whether to run the command in a new window.</param>
@@ -96,18 +91,16 @@ public static void Run(
9691
IEnumerable<string> args,
9792
string workingDirectory = "",
9893
bool noEcho = false,
99-
string? windowsName = null,
100-
IEnumerable<string>? windowsArgs = null,
10194
string? echoPrefix = null,
10295
Action<IDictionary<string, string?>>? configureEnvironment = null,
10396
bool createNoWindow = false,
10497
Func<int, bool>? handleExitCode = null,
10598
CancellationToken cancellationToken = default) =>
10699
ProcessStartInfo
107100
.Create(
108-
Validate(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name),
101+
Resolve(Validate(name)),
109102
"",
110-
(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args) ?? throw new ArgumentNullException(nameof(args)),
103+
args ?? throw new ArgumentNullException(nameof(args)),
111104
workingDirectory,
112105
false,
113106
configureEnvironment ?? defaultAction,
@@ -140,8 +133,6 @@ private static void Run(
140133
/// <param name="args">The arguments to pass to the command.</param>
141134
/// <param name="workingDirectory">The working directory in which to run the command.</param>
142135
/// <param name="noEcho">Whether or not to echo the resulting command line and working directory (if specified) to standard output (stdout).</param>
143-
/// <param name="windowsName">The name of the command to use on Windows only.</param>
144-
/// <param name="windowsArgs">The arguments to pass to the command on Windows only.</param>
145136
/// <param name="echoPrefix">The prefix to use when echoing the command line and working directory (if specified) to standard output (stdout).</param>
146137
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
147138
/// <param name="createNoWindow">Whether to run the command in a new window.</param>
@@ -162,17 +153,15 @@ public static async Task RunAsync(
162153
string args = "",
163154
string workingDirectory = "",
164155
bool noEcho = false,
165-
string? windowsName = null,
166-
string? windowsArgs = null,
167156
string? echoPrefix = null,
168157
Action<IDictionary<string, string?>>? configureEnvironment = null,
169158
bool createNoWindow = false,
170159
Func<int, bool>? handleExitCode = null,
171160
CancellationToken cancellationToken = default) =>
172161
await ProcessStartInfo
173162
.Create(
174-
Validate(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name),
175-
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args,
163+
Resolve(Validate(name)),
164+
args,
176165
Enumerable.Empty<string>(),
177166
workingDirectory,
178167
false,
@@ -192,11 +181,6 @@ await ProcessStartInfo
192181
/// </param>
193182
/// <param name="workingDirectory">The working directory in which to run the command.</param>
194183
/// <param name="noEcho">Whether or not to echo the resulting command name, arguments, and working directory (if specified) to standard output (stdout).</param>
195-
/// <param name="windowsName">The name of the command to use on Windows only.</param>
196-
/// <param name="windowsArgs">
197-
/// The arguments to pass to the command on Windows only.
198-
/// As with <see cref="System.Diagnostics.ProcessStartInfo.ArgumentList"/>, the strings don't need to be escaped.
199-
/// </param>
200184
/// <param name="echoPrefix">The prefix to use when echoing the command name, arguments, and working directory (if specified) to standard output (stdout).</param>
201185
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
202186
/// <param name="createNoWindow">Whether to run the command in a new window.</param>
@@ -213,18 +197,16 @@ public static async Task RunAsync(
213197
IEnumerable<string> args,
214198
string workingDirectory = "",
215199
bool noEcho = false,
216-
string? windowsName = null,
217-
IEnumerable<string>? windowsArgs = null,
218200
string? echoPrefix = null,
219201
Action<IDictionary<string, string?>>? configureEnvironment = null,
220202
bool createNoWindow = false,
221203
Func<int, bool>? handleExitCode = null,
222204
CancellationToken cancellationToken = default) =>
223205
await ProcessStartInfo
224206
.Create(
225-
Validate(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name),
207+
Resolve(Validate(name)),
226208
"",
227-
(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args) ?? throw new ArgumentNullException(nameof(args)),
209+
args ?? throw new ArgumentNullException(nameof(args)),
228210
workingDirectory,
229211
false,
230212
configureEnvironment ?? defaultAction,
@@ -256,8 +238,6 @@ private static async Task RunAsync(
256238
/// <param name="name">The name of the command. This can be a path to an executable file.</param>
257239
/// <param name="args">The arguments to pass to the command.</param>
258240
/// <param name="workingDirectory">The working directory in which to run the command.</param>
259-
/// <param name="windowsName">The name of the command to use on Windows only.</param>
260-
/// <param name="windowsArgs">The arguments to pass to the command on Windows only.</param>
261241
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
262242
/// <param name="encoding">The preferred <see cref="Encoding"/> for standard output (stdout) and standard output (stdout).</param>
263243
/// <param name="handleExitCode">
@@ -278,17 +258,15 @@ private static async Task RunAsync(
278258
string name,
279259
string args = "",
280260
string workingDirectory = "",
281-
string? windowsName = null,
282-
string? windowsArgs = null,
283261
Action<IDictionary<string, string?>>? configureEnvironment = null,
284262
Encoding? encoding = null,
285263
Func<int, bool>? handleExitCode = null,
286264
string? standardInput = null,
287265
CancellationToken cancellationToken = default) =>
288266
await ProcessStartInfo
289267
.Create(
290-
Validate(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name),
291-
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args,
268+
Resolve(Validate(name)),
269+
args,
292270
Enumerable.Empty<string>(),
293271
workingDirectory,
294272
true,
@@ -310,11 +288,6 @@ await ProcessStartInfo
310288
/// As with <see cref="System.Diagnostics.ProcessStartInfo.ArgumentList"/>, the strings don't need to be escaped.
311289
/// </param>
312290
/// <param name="workingDirectory">The working directory in which to run the command.</param>
313-
/// <param name="windowsName">The name of the command to use on Windows only.</param>
314-
/// <param name="windowsArgs">
315-
/// The arguments to pass to the command on Windows only.
316-
/// As with <see cref="System.Diagnostics.ProcessStartInfo.ArgumentList"/>, the strings don't need to be escaped.
317-
/// </param>
318291
/// <param name="configureEnvironment">An action which configures environment variables for the command.</param>
319292
/// <param name="encoding">The preferred <see cref="Encoding"/> for standard output (stdout) and standard error (stderr).</param>
320293
/// <param name="handleExitCode">
@@ -335,18 +308,16 @@ await ProcessStartInfo
335308
string name,
336309
IEnumerable<string> args,
337310
string workingDirectory = "",
338-
string? windowsName = null,
339-
IEnumerable<string>? windowsArgs = null,
340311
Action<IDictionary<string, string?>>? configureEnvironment = null,
341312
Encoding? encoding = null,
342313
Func<int, bool>? handleExitCode = null,
343314
string? standardInput = null,
344315
CancellationToken cancellationToken = default) =>
345316
await ProcessStartInfo
346317
.Create(
347-
Validate(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name),
318+
Resolve(Validate(name)),
348319
"",
349-
(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args) ?? throw new ArgumentNullException(nameof(args)),
320+
args ?? throw new ArgumentNullException(nameof(args)),
350321
workingDirectory,
351322
true,
352323
configureEnvironment ?? defaultAction,
@@ -400,5 +371,46 @@ await ProcessStartInfo
400371

401372
private static string Validate(string name) =>
402373
string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("The command name is missing.", nameof(name)) : name;
374+
375+
private static string Resolve(string name)
376+
{
377+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
378+
Path.IsPathRooted(name) ||
379+
!string.IsNullOrEmpty(Path.GetExtension(name)))
380+
{
381+
return name;
382+
}
383+
384+
var fileNames = windowsExecutableExtensions.Select(extension => Path.ChangeExtension(name, extension)).ToList();
385+
386+
return GetSearchDirectories().SelectMany(_ => fileNames, Path.Combine).FirstOrDefault(File.Exists) ?? name;
387+
}
388+
389+
// see https://github.com/dotnet/runtime/blob/14304eb31eea134db58870a6d87312231b1e02b6/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs#L703-L726
390+
private static IEnumerable<string> GetSearchDirectories()
391+
{
392+
var currentProcessPath = Process.GetCurrentProcess().MainModule?.FileName;
393+
if (!string.IsNullOrEmpty(currentProcessPath))
394+
{
395+
var currentProcessDirectory = Path.GetDirectoryName(currentProcessPath);
396+
if (!string.IsNullOrEmpty(currentProcessDirectory))
397+
{
398+
yield return currentProcessDirectory;
399+
}
400+
}
401+
402+
yield return Directory.GetCurrentDirectory();
403+
404+
var path = Environment.GetEnvironmentVariable("PATH");
405+
if (string.IsNullOrEmpty(path))
406+
{
407+
yield break;
408+
}
409+
410+
foreach (var directory in path.Split(Path.PathSeparator))
411+
{
412+
yield return directory;
413+
}
414+
}
403415
}
404416
}

0 commit comments

Comments
 (0)