Thursday, May 24, 2018

Interactive Command Line Directory Picker

Like most developers, I work on multiple projects, and I'm lazy so I don't like to constantly typing in directory paths to change my location in terminal. I used to use a custom bash script to make this task easier but it proved to be hard to maintain and not flexible.

Since I am primarily a PHP developer, I wanted to make a script in PHP to handle the interactive logic bits of determining what project I am trying to navigate to. As an added feature, I wanted to not have to memorize all the possible paths that I've setup in the chooser. To this end, I can enter as little as the function name or the beginning portion of the option or the entire path option:

  • chdir
  • chdir juno
  • chdir juno.api
  • chdir juno.api.logs
If the path entered is the final destination (resulting in a string) then you're redirected to that location. If it's a option (resulting in an array) you're given a choice of where to go.

A simple bash function is created in my ~/.bash_profile. The function takes the first argument it receives and passes it directly to the PHP script. The PHP script then puts the resulting path choice into a temporary file. Then the bash script reads the file. With some additional time spent, I probably could have avoided the temporary file but I didn't feel it was worth it. The PHP script validates the path, so as long as the result in the temporary file is not empty, the bash function changes the current working directory with the cd command.

chdir() {
    echo '' >> /tmp/chdir
    php /Users/david/dev/directory_chooser $1
    route=$(</tmp/chdir)
    rm /tmp/chdir
    if [ "${route}" != '' ]
    then
        cd $route
    fi
}

Because this function is placed into my bash profile script, there is no need to prepend the call with a period so it will result in changing the current working directory. The custom bash script I used previously had to be executed with a dot space prepending it to actually change my working directory.

The bulk of the work happens in a PHP script called directory_chooser. The possible path options are coded into the $routes array. The name displayed is the key. If the value is a string, then it's the destination for that option. If the value of is an array, then it's a deeper set of options. The special case is when an option has no key (index of zero), it's presented as the Root. So choosing juno.api will result in a choice between the Root and logs.

// Command line colors
$colorReset="\033[0m";           # Text Reset
$colorCyan="\033[0;36m";         # Cyan
$colorBRed="\033[1;31m";         # Bold Red
$colorBGreen="\033[1;32m";       # Bold Green
$colorBCyan="\033[1;36m";        # Bold Cyan

// Available path options
$routes = [
    'juno' => [
        'home'   => '/Users/david/dev/juno-home/Juno-Home',
        'api'    => [
                      '/Users/david/dev/juno-api/Juno-API',
            'logs' => '/Users/david/dev/juno-api/Juno-API/storage/logs'
        ],
        'app'    => '/Users/david/dev/juno-app/Juno-App',
        'mobile' => '/Users/david/dev/juno-mobile/Juno-Mobile'
    ],
    'example' => '/Users/david/dev/example.com'
];

$input = (isset($argv[1])) ? $argv[1] : null;
$route = null;

// If the input is a dot notation value, get the path option desired
if($input !== null && strpos($input, '.') !== false) {
    $parts = explode('.', $input);

    $route = $routes;
    foreach($parts as $part) {
        if(is_array($route) && isset($route[$part])) {
            $route = $route[$part];
        }
    }
}
// If the input is provided but is not a dot notation
elseif($input !== null && isset($routes[$input])) {
    $route = $routes[$input];
} else {
    $route = $routes;
}

// Drill down in the path options until we get to a string path
while(!is_string($route)) {
    $names = array_keys($route);

    if(count($names) == 1) {
        $route = $route[$names[0]];
        continue;
    }

    foreach($names as $index => $name) {
        if($name === 0) {
            echo "{$colorCyan}\t{$index} - Root{$colorReset}\n";
        } else {
            echo "{$colorCyan}\t{$index} - {$name}{$colorReset}\n";
        }
    }

    echo "{$colorBCyan}Which path (numeric index):{$colorReset}\n";
    $handle = fopen('php://stdin', 'r');
    $line = fgets($handle);
    if(isset($names[(int)$line]) && $route[$names[(int)$line]]) {
        $route = $route[$names[(int)$line]];
    }
}

// Make sure path actually exists
// On success, write the path to a temporary file so the bash script can get it
if(file_exists($route)) {
    echo "{$colorBGreen}Changing location to {$route}{$colorReset}\n";
    file_put_contents('/tmp/chdir', $route);
} else {
    echo "{$colorBRed}ERROR: The destination does not exist - $route\n";
}

The while loop makes it so that you can have path options as deep or as shallow as we want.

Because I like a pretty command line tools, I'm colorizing the PHP output with the $colorCyan and similar values. There are libraries to make command line choices like this easy, but I didn't want the overhead of maintaining links to a library, that's why I'm using the basic PHP command line input method of stdin.

The names have been changed to protect the innocent.