diff --git a/src/normalize.ts b/src/normalize.ts index 2931f29..ab9a618 100644 --- a/src/normalize.ts +++ b/src/normalize.ts @@ -16,6 +16,7 @@ const isWindowsExecutableRegExp = /\.(?:com|exe)$/i; const isNodeModulesCmdRegExp = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i; const isWindows = process.platform === 'win32'; const defaultPathExt = ['.EXE', '.CMD', '.BAT', '.COM']; +const noPathExt = ['']; interface NormalizedSpawnCommand { command: string; @@ -148,30 +149,31 @@ function resolveCommand(command: string, options: SpawnOptions): string | null { command.includes('/') || command.includes('\\') ? [''] : [cwd, ...PATH.split(pathDelimiter)]; - const pathExt = env.PATHEXT - ? env.PATHEXT.split(pathDelimiter) - : defaultPathExt; + let pathExt = env.PATHEXT ? env.PATHEXT.split(pathDelimiter) : defaultPathExt; if (command.includes('.') && pathExt[0] !== '') { - pathExt.unshift(''); + pathExt = ['', ...pathExt]; } - for (const path of pathEnv) { - const unquoted = - path.startsWith('"') && path.endsWith('"') && path.length > 1 - ? path.slice(1, -1) - : path; - const dest = resolvePath(cwd, unquoted, command); - - for (const ext of pathExt) { - const destWithExt = dest + ext; - - try { - if (statSync(destWithExt).isFile()) { - return destWithExt; + // The second pass tries to resolve the command with no extension + for (const extensions of [pathExt, noPathExt]) { + for (const path of pathEnv) { + const unquoted = + path.startsWith('"') && path.endsWith('"') && path.length > 1 + ? path.slice(1, -1) + : path; + const dest = resolvePath(cwd, unquoted, command); + + for (const ext of extensions) { + const destWithExt = dest + ext; + + try { + if (statSync(destWithExt).isFile()) { + return destWithExt; + } + } catch { + // do nothing, it didn't exist } - } catch { - // do nothing, it didn't exist } } } diff --git a/src/test/normalize_test.ts b/src/test/normalize_test.ts index cd52bbc..fdd12ca 100644 --- a/src/test/normalize_test.ts +++ b/src/test/normalize_test.ts @@ -71,6 +71,14 @@ describe('normalizeSpawnCommand', () => { expect(normalized.args).toEqual([scriptPath]); }); + test('detects shebang of a command without extension', () => { + const scriptPath = path.join(fixturesPath, 'shebang_script_noext'); + const normalized = normalizeSpawnCommand(scriptPath, []); + + expect(normalized.command).toBe('where'); + expect(normalized.args).toEqual([scriptPath]); + }); + test('handles relative commands without extension', () => { const relativePath = path.relative( cwd, diff --git a/test/fixtures/shebang_script_noext b/test/fixtures/shebang_script_noext new file mode 100644 index 0000000..a3763eb --- /dev/null +++ b/test/fixtures/shebang_script_noext @@ -0,0 +1,2 @@ +#!where +not a real script, just used to verify shebang parsing