In Unix-based operating systems, the which
command is a very handy tool that helps users locate the executable file associated with a command. When you run a command in the terminal, the system looks through directories listed in the PATH
environment variable to find the corresponding executable.
This blog post will dive deep into how you can implement a similar functionality in Go, and will explore how the code works and how it can be extended for more customization.
Introduction
The which
command essentially checks whether a specific executable exists in the directories defined in the PATH
environment variable. It checks each directory in PATH
to see if it contains a file matching the name of the executable. If it finds a match, it returns the full path to that executable; otherwise, it informs the user that the executable doesn’t exist.
Here’s how we can write a Go program that mimics the which
command with added verbosity and extension customization.
Code Walkthrough
1. Setting Up the Command-Line Flags
We start by importing the necessary packages. The flag
package is used to handle command-line arguments, and os
, path/filepath
, and strings
are used to interact with the operating system and file paths.
verbose := flag.Bool("v", false, "Enable Verbose output")
flag.Parse()
We define a verbose
flag that allows the user to toggle verbose output. By default, it is set to false
, but the user can pass the -v
flag when running the program to enable verbose logging.
2. Handling Arguments
We then check if the user has provided any arguments. If the user doesn’t pass the executable name, the program prints the usage instructions and exits.
arguments := os.Args
if len(arguments) == 0 {
fmt.Println("Usage: which [-v] <executable_name>")
return
}
If the user provides the name of the executable, the program continues.
3. Retrieving the PATH Environment Variable
Next, we retrieve the directories listed in the PATH
environment variable, which are where executables are stored. We split the PATH
string by the system’s file separator (:
on Unix-like systems or ;
on Windows).
path := os.Getenv("PATH")
pathSplit := filepath.SplitList(path)
4. Customizing Extensions
By default, executables on Windows might have extensions like .exe
, .cmd
, or .bat
, while on Linux or macOS, they usually don’t have extensions. To handle this, we create a list of extensions.
exeExtension := []string{".exe", ".cmd", ".bat"}
customizeExtension := os.Getenv("CUSTOMIZE_EXTENSION")
if customizeExtension != "" {
exeExtension = append(exeExtension, strings.Split(customizeExtension, ",")...)
}
The program checks for the CUSTOMIZE_EXTENSION
environment variable, which allows users to define custom executable extensions. For example, users could add .sh
or .bin
as executable extensions on their systems.
If the variable is not set, the program uses default extensions for Windows executables.
5. Verbose Mode and Searching Directories
When the verbose flag is enabled, the program outputs the directories it is searching through and whether the executable was found in each directory.
if *verbose {
fmt.Printf("Searching %s in PATH directories... ", file)
}
The program then iterates through each directory in PATH
, checking if the file exists with each of the extensions defined earlier.
for _, directory := range pathSplit {
fullpath := filepath.Join(directory, file)
for _, extension := range exeExtension {
fullpathextension := fullpath + extension
if fileExists(fullpathextension) {
fmt.Println("Found match! ", fullpath)
return
}
if *verbose {
fmt.Printf("checked: %s (not found)\n ", fullpath)
}
}
}
For each directory, it constructs a full path for the executable with every possible extension, and checks if the file exists using the fileExists()
helper function. If the file is found, it prints the path; if not, it continues to the next directory.
6. File Existence Check
The fileExists()
function checks if a file exists at the given path:
func fileExists(filename string) bool {
fileInfo, err := os.Stat(filename)
return err == nil && !fileInfo.IsDir()
}
This function attempts to get the file’s metadata. If the file exists and isn’t a directory, it returns true
; otherwise, it returns false
.
7. Handling Missing Executables
If no executable matching the provided name is found, the program outputs:
fmt.Println("Executable not found")
This message indicates that the file doesn’t exist in any of the directories listed in PATH
.
The Go implementation of the which
command is a simple yet powerful tool to locate executables within the directories listed in the PATH
environment variable. By adding verbosity and support for customizable extensions, this program offers more flexibility than the typical which
command. Additionally, its design can be extended with more advanced features, making it a great starting point for those looking to learn how to interact with the file system and the environment in Go.
https://github.com/vickychhetri/which