Manipulate %PATH% in the current Windows cmd.exe window with help from Perl

I often find myself working in one ConEmu window for extended periods of time. Sometimes, I end up adding a bunch of directories to the %PATH% during a session. And, more often than not, I end up wanting to just remove one or two directories from the %PATH% and continue working. For example, I might want to switch to using Cygwin’s git instead of the MinGW git installed alongside Visual Studio 2015.

So, I wrote a quick Perl script to do that. It outputs a string so I can grab that and set the path in the current shell.

But, of course, that felt incomplete. I thought I should also be able to insert a directory before or after another directory in the %PATH%. So, I added that. The script below manipulates the %PATH% and writes a replacement string to STDOUT. It is intended to be called from a batch file to set %PATH% in the current command line window. It includes some rudimentary tests to help me avoid obvious mistakes.

#!/usr/bin/env perl

use 5.024; # why not?!
use warnings;

my $SEP;
my %EXIT = (
    SUCCESS => 0,
    HELP_REQUESTED => 1,
    INSERT_POSITION_NOT_FOUND => 2,
    INSERT_LOCATION_UNKNOWN => 255,
);

BEGIN {
    if ( eq 'MSWin32') {
        $SEP = ';';
    }
    else {
        $SEP = ':';
    }
}

run();

sub run {
    my ($cmd, @args) = @_;

    my %dispatch = (
        ia => sub { insert_dir(after => @_) },
        ib => sub { insert_dir(before => @_) },
        help => \&help,
        rm => \&remove,
        test => \&test,
    );

    unless (defined($cmd) and exists $dispatch{$cmd}) {
        $cmd = 'help';
    }

    my $handler = $dispatch{$cmd};
    my $output = $handler->({PATH}, @args);
    say $output;
    exit $EXIT{SUCCESS};
}

sub insert_dir {
    my ($where, $path, $pattern, $dir) = @_;

    validate_pattern($pattern);
    validate_dir($dir);

    my ($before, $pivot, $after) = bisect_path($path, $pattern)->@

    if (not defined($pivot)) {
        exit $EXIT{INSERT_POSITION_NOT_FOUND};
    }
    elsif ($where eq 'after') {
        return join($SEP => $before->@ $pivot, $dir, $after->@*);
    }
    elsif ($where eq 'before') {
        return join($SEP => $before->@ $dir, $pivot, $after->@*);
    }
    else {
        warn "Unknown position '$where'\n";
        exit $EXIT{INSERT_LOCATION_UNKNOWN};
    }
}

sub remove {
    my ($path, $pattern) = @_;

    validate_pattern($pattern);

    join(';', grep ! /$pattern/i, split /\Q$SEP/, $path);
}

sub bisect_path {
    my ($path, $pattern) = @_;
    my @dirs = split /\Q$SEP/, $path;

    my (@before, $pivot);
    while ($pivot = shift @dirs) {
        last if $pivot =~ /$pattern/i;
        push @before, $pivot;
    }

    return [\@before, $pivot, \@dirs];
}

sub validate_dir {
    defined([0]) or die "Need a directory\n";
}

sub validate_pattern {
    defined([0]) or die "Need a pattern\n";
}

sub help {
    print STDERR <<EO_HELP;
Remove directories matching pattern from \$PATH
    pathmanip rm /pattern/
Insert directory before entry matching pattern in \$PATH
    pathmanip ib /pattern/ dir
Insert directory after entry matching pattern in \$PATH
    pathmanip ia /pattern/ dir
EO_HELP
    exit $EXIT{HELP_REQUESTED};
}

sub test {
    my $path = 'ab;bc;cd';

    my @cases = (
        [ remove($path, 'b'), 'cd' ],
        [ remove($path, 'z'), $path ],
        [ insert_dir(after => $path, '^a', 'z'), 'ab;z;bc;cd' ],
        [ insert_dir(after => $path, 'c', 'z'), 'ab;bc;z;cd' ],
        [ insert_dir(before => $path, 'c\z', 'z'), 'ab;z;bc;cd' ],
        [ insert_dir(before => $path, '^c', 'z'), 'ab;bc;z;cd' ],
    );
    require Test::More;
    Test::More->import(tests => scalar @cases);

    exit grep !, map is(->@*), @cases;
}

Here is the batch file that drives it:

@echo off
set sinan_savedpath=%path%

if /i "%1" EQU "test" goto TEST

for /f "usebackq delims=|" %%f in (`perl c:\opt\bin\pathmanip.pl "%1" "%2" "%3"`) do (
    if %ERRORLEVEL% NEQ 0 goto END
    path=%%f
)

goto END

:TEST
perl c:\opt\bin\pathmanip.pl test

:END

I called the batch file cpath.bat. I am a wuss, so I save the current path in case I want to quickly restore it after an unintentional change.

Can I do this using cmd.exe builtins only?

Originally, I had wanted to this using only built in cmd.exe facilities, but I haven’t been able to figure out how to get findstr to terminate in the following:

for /f "usebackq delims=|" %%I in (`findstr /r/i "%pattern%" ^| echo "%adir%"`) do (
    ...
)

Or, similarly, from the command line, I would be grateful if there is a way to have

findstr test | echo test

terminate without user input. I don’t want to create temporary files.

PS: I am sure you can do this using some Powershell feature, but that’s not where I spend most of my time.

PPS: You can discuss this post on r/perl.

PPPS: Sure, this would probably also work in *nix shells, but I don’t have to fiddle with paths as much there and I haven’t tried it.