11using System ;
22using System . Collections . Generic ;
33using System . Diagnostics ;
4+ using System . IO ;
45using System . Linq ;
56using System . Reflection ;
67using 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