@@ -169,11 +169,6 @@ private async Task RunLoopAsync(StdioProcessInfo stdio, CancellationToken cancel
169169 } ;
170170 }
171171
172- /// <summary>
173- /// 跨平台解析命令路径。<br/>
174- /// 如果命令是完整路径或相对路径,直接返回;<br/>
175- /// 否则在 PATH 环境变量中查找命令。
176- /// </summary>
177172 static string ? ResolveCommandPath ( string command )
178173 {
179174 // 如果命令包含路径分隔符,说明是路径而非命令名
@@ -189,48 +184,40 @@ private async Task RunLoopAsync(StdioProcessInfo stdio, CancellationToken cancel
189184 return null ;
190185 }
191186
187+ // 尝试提取命令里原本的扩展名(例如用户可能指定的是 npx/dnx 也可能指定的是 npx.cmd/dnx.exe。
188+ var extensionInCommand = Path . GetExtension ( command ) ;
192189 var paths = pathEnv . Split ( Path . PathSeparator , StringSplitOptions . RemoveEmptyEntries ) ;
193- var extensions = GetExecutableExtensions ( ) ;
190+ var extensions = ExecutableExtensionsLazy . Value ;
194191
195192 foreach ( var path in paths )
196193 {
197- // 在 Windows 上,尝试命令名 + 各种扩展名
198- foreach ( var extension in extensions )
194+ if ( extensions . Count is 0 )
199195 {
200- var fullPath = Path . Combine ( path , command + extension ) ;
196+ // Linux 等无扩展名的可执行程序。
197+ var fullPath = Path . Join ( path , command ) ;
201198 if ( File . Exists ( fullPath ) )
202199 {
203200 return fullPath ;
204201 }
202+ continue ;
205203 }
206- }
207-
208- return null ;
209- }
210204
211- /// <summary>
212- /// 获取可执行文件扩展名列表(跨平台)。
213- /// </summary>
214- static string [ ] GetExecutableExtensions ( )
215- {
216- if ( OperatingSystem . IsWindows ( ) )
217- {
218- // Windows 上从 PATHEXT 环境变量获取可执行扩展名
219- var pathExt = Environment . GetEnvironmentVariable ( "PATHEXT" ) ;
220- if ( ! string . IsNullOrEmpty ( pathExt ) )
205+ // Windows 等带扩展名的可执行程序。
206+ foreach ( var extension in extensions )
221207 {
222- var extensions = pathExt . Split ( Path . PathSeparator , StringSplitOptions . RemoveEmptyEntries ) ;
223- // 确保包含空扩展名(用于无扩展名的可执行文件)
224- return [ .. extensions ] ;
208+ var fullPath = extensionInCommand ? . Equals ( extension , StringComparison . OrdinalIgnoreCase ) is true
209+ // 如果命令自带的扩展名正好与环境变量里的相同,说明命令本身确实带的是扩展名。
210+ ? Path . Join ( path , command )
211+ // 否则,叠加环境变量里的扩展名。
212+ : Path . Join ( path , command + extension ) ;
213+ if ( File . Exists ( fullPath ) )
214+ {
215+ return fullPath ;
216+ }
225217 }
226- // 默认 Windows 可执行扩展名
227- return [ ".exe" , ".cmd" , ".bat" , ".com" ] ;
228- }
229- else
230- {
231- // Unix 系统上可执行文件通常没有扩展名
232- return [ "" ] ;
233218 }
219+
220+ return null ;
234221 }
235222 }
236223
@@ -249,6 +236,25 @@ await Task.Run(() =>
249236 } ) ;
250237 }
251238
239+ private static readonly Lazy < IReadOnlyList < string > > ExecutableExtensionsLazy = new Lazy < IReadOnlyList < string > > ( ( ) =>
240+ {
241+ if ( ! OperatingSystem . IsWindows ( ) )
242+ {
243+ // Unix 系统上可执行文件通常没有扩展名
244+ return [ "" ] ;
245+ }
246+ // Windows 上从 PATHEXT 环境变量获取可执行扩展名
247+ var pathExt = Environment . GetEnvironmentVariable ( "PATHEXT" ) ;
248+ if ( string . IsNullOrEmpty ( pathExt ) )
249+ {
250+ return [ ".exe" , ".cmd" , ".bat" , ".com" ] ;
251+ }
252+ var extensions = pathExt . Split ( Path . PathSeparator , StringSplitOptions . RemoveEmptyEntries ) ;
253+ // 确保包含空扩展名(用于无扩展名的可执行文件)
254+ return extensions ;
255+ // 默认 Windows 可执行扩展名
256+ } , LazyThreadSafetyMode . None ) ;
257+
252258 private readonly record struct StdioProcessInfo
253259 {
254260 public required Process Process { get ; init ; }
0 commit comments