diff --git a/FindExecutable/ComponentSource.json b/FindExecutable/ComponentSource.json index 5e73300..aaba352 100644 --- a/FindExecutable/ComponentSource.json +++ b/FindExecutable/ComponentSource.json @@ -8,7 +8,7 @@ { "type": "git", "url": "https://www.github.com/sgrottel/FindExecutable", - "hash": "07160e7362cb7797f962ff98a707d6064d719ef3", + "hash": "4470fc9cea00acf2bed380e4cefe62137acd0ef7", "path": "FindExecutable" } ] diff --git a/FindExecutable/FindExecutable.cs b/FindExecutable/FindExecutable.cs index 006d9f3..2c9660c 100644 --- a/FindExecutable/FindExecutable.cs +++ b/FindExecutable/FindExecutable.cs @@ -1,202 +1,237 @@ -// -// FindExecutable.cs -// Copyright, by SGrottel.de https://www.github.com/sgrottel/FindExecutable -// Open Source under the `MIT license` -// -// Copyright(c) 2023-2024 Sebastian Grottel -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; - -namespace FindExecutable -{ - - /// - /// Utility class to help finding the full file system path from an executable name - /// - public static class FindExecutable - { - - /// The name of the executable to find. - /// If set to true, the current execution directory is included in the list of search paths - /// If set to true, the application domain base directory is included in the list of search paths - /// If set to something other then null, the paths iterated within are included in the list of search paths - /// The full file system path to the executable file requested, or null if the file is not found. - /// - /// The function searches in all paths specified by the `PATH` environment variable. - /// - /// Depending on the platform the code is run on, `executable` might be case sensitive. - /// - /// When searching on Windows and the name does not specify a file name extension, - /// `.exe` is assumed, if no executable file without file name extension is found. - /// - /// When searching on Linux and the n ame does contain a file name extension, - /// the extension is removed, if no executable file is found with it. - /// - public static string? FullPath( - string executable, - bool includeCurrentDirectory = false, - bool includeBaseDirectory = false, - IEnumerable? additionalPaths = null) - { - IEnumerable paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty(); - if (includeCurrentDirectory) - paths = paths.Concat(new string[] { Environment.CurrentDirectory }); - if (includeBaseDirectory) - paths = paths.Concat(new string[] { AppDomain.CurrentDomain.BaseDirectory }); - if (additionalPaths != null) - paths = paths.Concat(additionalPaths); - - while (!string.IsNullOrEmpty(executable)) - { - foreach (string path in paths) - { - if (string.IsNullOrWhiteSpace(path)) continue; - if (!Directory.Exists(path)) continue; - string p = Path.GetFullPath(path); - if (!Directory.Exists(p)) continue; - - string f = Path.Combine(p, executable); - if (File.Exists(f)) - { - if (IsExecutable(f)) - { - return f; - } - } - } - - // Not found in first go. Maybe try fallback - if (OperatingSystem.IsWindows()) - { - if (string.IsNullOrEmpty(Path.GetExtension(executable))) - { - executable = Path.ChangeExtension(executable, ".exe"); - } - else break; - } - else - { - if (!string.IsNullOrEmpty(Path.GetExtension(executable))) - { - executable = Path.GetFileNameWithoutExtension(executable); - } - else break; - } - } - - return null; - } - - #region Test if File is Executable - - #region Windows P/Invoke - [DllImport("shell32.dll")] - private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, IntPtr psfi, uint cbSizeFileInfo, uint uFlags); - private const uint SHGFI_EXETYPE = 0x000002000; - #endregion - - [SupportedOSPlatform("Windows")] - private static bool IsExecutableOnWindow(string path) - { - try - { - // Ask the windows shell what kind of file `path` points to. - IntPtr exeType = SHGetFileInfo(path, 128, IntPtr.Zero, 0, SHGFI_EXETYPE); - long wparam = exeType.ToInt64(); - int loWord = (int)(wparam & 0xffff); - int hiWord = (int)(wparam >> 16); - - if (wparam != 0) - { - if (hiWord == 0x0000 && loWord == 0x5a4d) - { - return true; // Dos - } - else if (hiWord == 0x0000 && loWord == 0x4550) - { - return true; // Console - } - else if ((hiWord != 0x0000) && (loWord == 0x454E || loWord == 0x4550 || loWord == 0x454C)) - { - return true; // Windows - } - } - return false; // Very likely not an executable - } - catch { } - - // it exists, but shell does not answer. So, in doubt, give it a try... might work. - return true; - } - - #region Linux P/Invoke - [DllImport("libc", SetLastError = true)] - private static extern int access(string pathname, int mode); - // https://codebrowser.dev/glibc/glibc/posix/unistd.h.html#283 - private const int X_OK = 1; - #endregion - - [UnsupportedOSPlatform("Windows")] - private static bool IsExecutableOnLinux(string path) - { - try - { - // access with X_OK to test if the user of the current process - // should be able to execute the specified file. - if (access(path, X_OK) == 0) - { - return true; - } - } - catch { } - return false; - } - - /// - /// Tests if a file is (likely) executable - /// - /// The path to the file to test - /// True if the file is executable, false otherwise. - /// If `path` does not point to an existing file, the function returns false. - public static bool IsExecutable(string path) - { - if (!File.Exists(path)) return false; - if (OperatingSystem.IsWindows()) - { - return IsExecutableOnWindow(path); - } - else - { - return IsExecutableOnLinux(path); - } - } - - #endregion - - } - -} +// +// FindExecutable.cs +// Copyright, by SGrottel.de https://www.github.com/sgrottel/FindExecutable +// Open Source under the `MIT license` +// +// Copyright(c) 2023-2025 Sebastian Grottel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Version history: +// +// v1.0 -- 2025-05-19 +// v0.4 -- 2024-05-20 +// v0.1 -- 2023-11-26 +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace FindExecutable +{ + + /// + /// Utility class to help finding the full file system path from an executable name + /// + public static class FindExecutable + { + + /// The name of the executable to find. + /// If set to true, the current execution directory is included in the list of search paths + /// If set to true, the application domain base directory is included in the list of search paths + /// If set to something other then null, the paths iterated within are included in the list of search paths + /// The full file system path to the executable file requested, or null if the file is not found. + /// + /// The function searches in all paths specified by the `PATH` environment variable. + /// + /// Depending on the platform the code is run on, `executable` might be case sensitive. + /// + /// When searching on Windows and the name does not specify a file name extension, + /// `.exe` is assumed, if no executable file without file name extension is found. + /// + /// When searching on Linux and the n ame does contain a file name extension, + /// the extension is removed, if no executable file is found with it. + /// + public static string? FullPath( + string executable, + bool includeCurrentDirectory = false, + bool includeBaseDirectory = false, + IEnumerable? additionalPaths = null) + { + IEnumerable paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty(); + if (includeCurrentDirectory) + { + paths = paths.Concat(new string[] { Environment.CurrentDirectory }); + } + + if (includeBaseDirectory) + { + paths = paths.Concat(new string[] { AppDomain.CurrentDomain.BaseDirectory }); + } + + if (additionalPaths != null) + { + paths = paths.Concat(additionalPaths); + } + + while (!string.IsNullOrEmpty(executable)) + { + foreach (string path in paths) + { + if (string.IsNullOrWhiteSpace(path)) + { + continue; + } + + if (!Directory.Exists(path)) + { + continue; + } + + string p = Path.GetFullPath(path); + if (!Directory.Exists(p)) + { + continue; + } + + string f = Path.Combine(p, executable); + if (File.Exists(f)) + { + if (IsExecutable(f)) + { + return f; + } + } + } + + // Not found in first go. Maybe try fallback + if (OperatingSystem.IsWindows()) + { + if (string.IsNullOrEmpty(Path.GetExtension(executable))) + { + executable = Path.ChangeExtension(executable, ".exe"); + } + else + { + break; + } + } + else + { + if (!string.IsNullOrEmpty(Path.GetExtension(executable))) + { + executable = Path.GetFileNameWithoutExtension(executable); + } + else + { + break; + } + } + } + + return null; + } + + #region Test if File is Executable + + #region Windows P/Invoke + [DllImport("shell32.dll")] + private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, IntPtr psfi, uint cbSizeFileInfo, uint uFlags); + private const uint SHGFI_EXETYPE = 0x000002000; + #endregion + + [SupportedOSPlatform("Windows")] + private static bool IsExecutableOnWindow(string path) + { + try + { + // Ask the windows shell what kind of file `path` points to. + IntPtr exeType = SHGetFileInfo(path, 128, IntPtr.Zero, 0, SHGFI_EXETYPE); + long wparam = exeType.ToInt64(); + int loWord = (int)(wparam & 0xffff); + int hiWord = (int)(wparam >> 16); + + if (wparam != 0) + { + if (hiWord == 0x0000 && loWord == 0x5a4d) + { + return true; // Dos + } + else if (hiWord == 0x0000 && loWord == 0x4550) + { + return true; // Console + } + else if ((hiWord != 0x0000) && (loWord == 0x454E || loWord == 0x4550 || loWord == 0x454C)) + { + return true; // Windows + } + } + return false; // Very likely not an executable + } + catch { } + + // it exists, but shell does not answer. So, in doubt, give it a try... might work. + return true; + } + + #region Linux P/Invoke + [DllImport("libc", SetLastError = true)] + private static extern int access(string pathname, int mode); + // https://codebrowser.dev/glibc/glibc/posix/unistd.h.html#283 + private const int X_OK = 1; + #endregion + + [UnsupportedOSPlatform("Windows")] + private static bool IsExecutableOnLinux(string path) + { + try + { + // access with X_OK to test if the user of the current process + // should be able to execute the specified file. + if (access(path, X_OK) == 0) + { + return true; + } + } + catch { } + return false; + } + + /// + /// Tests if a file is (likely) executable + /// + /// The path to the file to test + /// True if the file is executable, false otherwise. + /// If `path` does not point to an existing file, the function returns false. + public static bool IsExecutable(string path) + { + if (!File.Exists(path)) + { + return false; + } + + if (OperatingSystem.IsWindows()) + { + return IsExecutableOnWindow(path); + } + else + { + return IsExecutableOnLinux(path); + } + } + + #endregion + + } + +} diff --git a/LICENSE b/LICENSE index 37ff024..560bf75 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023-2024 Sebastian Grottel +Copyright (c) 2023-2025 Sebastian Grottel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 72db123..5066a9c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # FindExecutable + C# code to find the full path of an executable file by searching the system's search paths. It is similar to `locate` or `get-command`. @@ -15,11 +16,16 @@ Nuget: The code was previously part of [SGrottel's Tiny Tools Collection (https://github.com/sgrottel/tiny-tools-collection)](https://github.com/sgrottel/tiny-tools-collection). + ## Code + The code is located in the `FindExecutable` subdirectiory. + ### Example + `FindExecutable` is a static class with one public static method: + ```cs public static string? FullPath( string executable, @@ -37,7 +43,9 @@ The function will return the full file system path to the executable file reques A description of how the function performs it's search can be found in the function comment in the source code. + ### How to Add to Your Project + You can simple copy this directory and all it's files to your project. Or, you can use the `SGrottel.FindExecutable.nuget` to import the files into your project. @@ -45,16 +53,21 @@ This way, if you manage your dependencies with tooling, you can be automatically [SGrottel.FindExecutable (https://www.nuget.org/packages/SGrottel.FindExecutable)](https://www.nuget.org/packages/SGrottel.FindExecutable) + ## Contributions + Contributions to this project are welcome! + * Open [Issues](https://github.com/sgrottel/FindExecutable/issues) here on Github * Create Pull Requests with Improvements (I recommend you talk to me first, e.g. via e-mail or issues) * Reach out to me, e.g. via [the contact form on my website (https://www.sgrottel.de/about)](https://www.sgrottel.de/about). + ## License + This project is freely available under the terms of the [MIT License](./LICENSE) - Copyright (c) 2023-2024 Sebastian Grottel + Copyright (c) 2023-2025 Sebastian Grottel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/SGrottel.FindExecutable.nuspec b/SGrottel.FindExecutable.nuspec index 562130e..9c68148 100644 --- a/SGrottel.FindExecutable.nuspec +++ b/SGrottel.FindExecutable.nuspec @@ -6,7 +6,7 @@ SGrottel.FindExecutable - 0.4.0.5 + 1.0.0 SGrottel.FindExecutable SGrottel SGrottel @@ -14,7 +14,7 @@ https://github.com/sgrottel/FindExecutable false C# code to find the full path of an executable file by searching the system's search paths. - Copyright 2023-2024 + Copyright 2023-2025 FindExecutable docs/README.md diff --git a/package_readme.md b/package_readme.md index 3e07f35..1729cb5 100644 --- a/package_readme.md +++ b/package_readme.md @@ -23,7 +23,7 @@ The function will return the full file system path to the executable file reques ## License This project is freely available under the terms of the [MIT License](./LICENSE) - Copyright (c) 2023-2024 Sebastian Grottel + Copyright (c) 2023-2025 Sebastian Grottel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal