// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Win32;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public static class ShellHelpers
{
///
/// These are the executable file extensions that Windows Shell recognizes. Unlike CMD/PowerShell,
/// Shell does not use PATHEXT, but has a magic fixed list.
///
public static string[] ExecutableExtensions { get; } = [".PIF", ".COM", ".EXE", ".BAT", ".CMD"];
///
/// Determines whether the specified file name represents an executable file
/// by examining its extension against the known list of Windows Shell
/// executable extensions (a fixed list that does not honor PATHEXT).
///
/// The file name (with or without path) whose extension will be evaluated.
///
/// True if the file name has an extension that matches one of the recognized executable
/// extensions; otherwise, false. Returns false for null, empty, or whitespace input.
///
public static bool IsExecutableFile(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
{
return false;
}
var fileExtension = Path.GetExtension(fileName);
return IsExecutableExtension(fileExtension);
}
///
/// Determines whether the provided file extension (including the leading dot)
/// is one of the Windows Shell recognized executable extensions.
///
/// The file extension to test. Should include the leading dot (e.g. ".exe").
///
/// True if the extension matches (case-insensitive) one of the known executable
/// extensions; false if it does not match or if the input is null/whitespace.
///
public static bool IsExecutableExtension(string fileExtension)
{
if (string.IsNullOrWhiteSpace(fileExtension))
{
// Shell won't execute app with a filename without an extension
return false;
}
foreach (var extension in ExecutableExtensions)
{
if (string.Equals(fileExtension, extension, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public static bool OpenCommandInShell(string? path, string? pattern, string? arguments, string? workingDir = null, ShellRunAsType runAs = ShellRunAsType.None, bool runWithHiddenWindow = false)
{
if (string.IsNullOrEmpty(pattern))
{
// Log.Warn($"Trying to run OpenCommandInShell with an empty pattern. The default browser definition might have issues. Path: '${path ?? string.Empty}' ; Arguments: '${arguments ?? string.Empty}' ; Working Directory: '${workingDir ?? string.Empty}'", typeof(ShellHelpers));
}
else if (pattern.Contains("%1", StringComparison.Ordinal))
{
arguments = pattern.Replace("%1", arguments);
}
return OpenInShell(path, arguments, workingDir, runAs, runWithHiddenWindow);
}
public static bool OpenInShell(string? path, string? arguments = null, string? workingDir = null, ShellRunAsType runAs = ShellRunAsType.None, bool runWithHiddenWindow = false)
{
using var process = new Process();
process.StartInfo.FileName = path;
process.StartInfo.WorkingDirectory = string.IsNullOrWhiteSpace(workingDir) ? string.Empty : workingDir;
process.StartInfo.Arguments = string.IsNullOrWhiteSpace(arguments) ? string.Empty : arguments;
process.StartInfo.WindowStyle = runWithHiddenWindow ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal;
process.StartInfo.UseShellExecute = true;
if (runAs == ShellRunAsType.Administrator)
{
process.StartInfo.Verb = "RunAs";
}
else if (runAs == ShellRunAsType.OtherUser)
{
process.StartInfo.Verb = "RunAsUser";
}
try
{
process.Start();
return true;
}
catch (Win32Exception)
{
// Log.Exception($"Unable to open {path}: {ex.Message}", ex, MethodBase.GetCurrentMethod().DeclaringType);
return false;
}
}
public enum ShellRunAsType
{
None,
Administrator,
OtherUser,
}
///
/// Parses the input string to extract the executable and its arguments.
///
public static void ParseExecutableAndArgs(string input, out string executable, out string arguments)
{
input = input.Trim();
executable = string.Empty;
arguments = string.Empty;
if (string.IsNullOrEmpty(input))
{
return;
}
if (input.StartsWith("\"", System.StringComparison.InvariantCultureIgnoreCase))
{
// Find the closing quote
var closingQuoteIndex = input.IndexOf('\"', 1);
if (closingQuoteIndex > 0)
{
executable = input.Substring(1, closingQuoteIndex - 1);
if (closingQuoteIndex + 1 < input.Length)
{
arguments = input.Substring(closingQuoteIndex + 1).TrimStart();
}
}
}
else
{
// Executable ends at first space
var firstSpaceIndex = input.IndexOf(' ');
if (firstSpaceIndex > 0)
{
executable = input.Substring(0, firstSpaceIndex);
arguments = input[(firstSpaceIndex + 1)..].TrimStart();
}
else
{
executable = input;
}
}
}
///
/// Checks if a file exists somewhere in the PATH.
/// If it exists, returns the full path to the file in the out parameter.
/// If it does not exist, returns false and the out parameter is set to an empty string.
/// The name of the file to check.
/// The full path to the file if it exists; otherwise an empty string.
/// An optional cancellation token to cancel the operation.
/// True if the file exists in the PATH; otherwise false.
///
public static bool FileExistInPath(string filename, out string fullPath, CancellationToken? token = null)
{
fullPath = string.Empty;
if (File.Exists(filename))
{
token?.ThrowIfCancellationRequested();
fullPath = Path.GetFullPath(filename);
return true;
}
else
{
var values = Environment.GetEnvironmentVariable("PATH");
if (values is not null)
{
foreach (var path in values.Split(Path.PathSeparator))
{
var path1 = Path.Combine(path, filename);
if (File.Exists(path1))
{
fullPath = Path.GetFullPath(path1);
return true;
}
token?.ThrowIfCancellationRequested();
var path2 = Path.Combine(path, filename + ".exe");
if (File.Exists(path2))
{
fullPath = Path.GetFullPath(path2);
return true;
}
token?.ThrowIfCancellationRequested();
}
}
return false;
}
}
private static bool TryResolveFromAppPaths(string name, [NotNullWhen(true)] out string? fullPath)
{
try
{
fullPath = TryHiveView(RegistryHive.CurrentUser, RegistryView.Registry64) ??
TryHiveView(RegistryHive.CurrentUser, RegistryView.Registry32) ??
TryHiveView(RegistryHive.LocalMachine, RegistryView.Registry64) ??
TryHiveView(RegistryHive.LocalMachine, RegistryView.Registry32) ?? string.Empty;
return !string.IsNullOrEmpty(fullPath);
string? TryHiveView(RegistryHive hive, RegistryView view)
{
using var baseKey = RegistryKey.OpenBaseKey(hive, view);
using var k1 = baseKey.OpenSubKey($@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{name}.exe");
var val = (k1?.GetValue(null) as string)?.Trim('"');
if (!string.IsNullOrEmpty(val))
{
return val;
}
// Some vendors create keys without .exe in the subkey name; check that too.
using var k2 = baseKey.OpenSubKey($@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{name}");
return (k2?.GetValue(null) as string)?.Trim('"');
}
}
catch (Exception)
{
fullPath = null;
return false;
}
}
///
/// Mimics Windows Shell behavior to resolve an executable name to a full path.
///
///
///
///
public static bool TryResolveExecutableAsShell(string name, out string fullPath)
{
// First check if we can find the file in the registry
if (TryResolveFromAppPaths(name, out var path))
{
fullPath = path;
return true;
}
// If the name does not have an extension, try adding common executable extensions
// this order mimics Windows Shell behavior
// Note: HasExtension check follows Shell behavior, but differs from the
// Start Menu search results, which will offer file name with extensions + ".exe"
var nameHasExtension = Path.HasExtension(name);
if (!nameHasExtension)
{
foreach (var ext in ExecutableExtensions)
{
var nameWithExt = name + ext;
if (FileExistInPath(nameWithExt, out fullPath))
{
return true;
}
}
}
fullPath = string.Empty;
return false;
}
}