1

I have an idea for a little command-line tool that would hopefully let me navigate directories quicker and I need to know if given a character from < STDIN > it is possible, from within perl, to automatically inject characters into < STDIN > dependent on the context (so it would look like an unsolicited tab-expansion if the circumstances are correct).

For example, supposing I had the following directories in some directory somewhere:

dir1 directory_2 test testing tested experiments

and contained in testing was the directories

fully_tested partially_tested 

then invocation of the script would first list all directories cd-able from where I called and wait for input. If the first character I entered was a 't' I would like to automatically inject 'est' into STDIN (since "test" is in the union of test, testing, and tested), list all directories beginning with "test" and wait for further input.

if the next char was either 'e' or 'i' it would cd automatically into tested or testing respectively (since these directories are then uniquely determined), if it was some yet to be decided "easy to reach key" like * I would cd into test, hitting '.' would do a cd .., and hitting any other alphabetic character would have no effect (and perhaps some other easy to reach key might list everything).

Getting back to the example, typing the chars 'tef' means I would cd into testing/fully_tested (since 'te' uniquely matches testing and the 'f' uniquely matches fully_tested)

As for actually coding this up I would like it to be my own problem (I believe it is possible to process input on a char by char basis) so to reiterate, all I'm asking here is:


Does perl provide some mechanism via which STDIN can be modified without user interaction (such that I can emulate tab-expand on the commandline, but with this expansion happening behind the scenes)? Or in other words, on evaluation of the last input I want to silently do the tab expansion (with it "looking like a tab expansion" wrt to my input stream, as per the commandline) without explicitly hitting a < TAB >


More detail: as far as I know what I am doing presently what I would end up with once coded up is something that would look like the following at runtime (suppose my home directory has the folders Desktop, Documents, Documents_bak, Downloads, Music)

Desktop/ Documents/ Documents_bak/ Downloads/ Music
D
Desktop/ Documents/ Documents_bak/ Downloads
o
Documents/ Documents_bak/ Downloads
c
Documents/ Documents_bak
_
programming/ books/ ...(other directories)

which doesn't look very nice... What I would actually like is something that resembled

Desktop/ Documents/ Documents_bak/ Downloads/ Music
D
Desktop/ Documents/ Documents_bak/ Downloads
Do
Documents/ Documents_bak/ Downloads
Documents
Documents/ Documents_bak
Documents_bak/
programming/ books/ ...(other directories)
Documents_bak/

where the last line "Documents_bak/" is actually the input stream as though I had typed it all (so I could delete some of it if I wanted). It is only how to achieve this little bit that presents a problem to me

EDIT* I am not (as far as I'm aware) trying to reinvent the wheel here - I'm just trying to cut out the < TAB >, < RET >, retype 'cd ' steps when changing directories. I.e. with my scheme it would take just "tef" to cd into testing/fully_tested yet just in bash I need "t< TAB >i< TAB >< RET >cd< SPACE >f< TAB >< RET >" to do the same.

HexedAgain
  • 1,011
  • 10
  • 21
  • so you want to recreate, in perl, what bash and the like already do anyways? – Marc B Dec 03 '13 at 21:01
  • not really... I don't see how I could possibly get this functionality in bash yet.. Indeed counting tab as a key how could I traverse, say 10 directories in just 10 keystrokes (without a load of aliases of course)? – HexedAgain Dec 03 '13 at 21:03
  • given `abc/def/ghi`, `a[tab]/b[tab]/c[tab]` would get what you want, and how will your app be able to differentiate between `abc/` and `abcdef/` for dirs? which one should get chosen? – Marc B Dec 03 '13 at 21:04
  • Supposing 'a' uniquely expanded to a directory I would like to go there immediately without the and steps. On the other hand, regarding your question (and I believe I acknowledged this in my question), if it didn't it would wait for me at abc, if I hit d it cds into abcdef, if I hit, say * it cds into abc – HexedAgain Dec 03 '13 at 21:05
  • Possible duplicate of http://stackoverflow.com/questions/8676026/how-can-i-have-perl-take-input-from-stdin-one-character-at-a-time – Tim Pierce Dec 03 '13 at 21:24
  • I actually googled, then visited that page prior to your link, and it does not answer my question (it suggests how char by char input can be handled only (and I acknowledged the possibility of getting this functionality in my OP) - I care more about whether there is a mechanism for alteringi stdin behind the scenes given that I have entered a char) – HexedAgain Dec 03 '13 at 21:26

1 Answers1

3

Term::ReadKey may be the answer to your question. It is not guaranteed to work on all systems but it gives the control you asked for.

Here's an example that works nicely on my Ubuntu machine.

#!/usr/bin/perl
use strict;
use Term::ReadKey;

#a simple sub to return directories in supplied path 
sub list_dirs {
    my $path = shift;
    my @dirs;
    opendir DIR, $path or die "can't opendir '$path' : $!";
    my @dirs = grep {-d $path.'/'.$_} grep {$_ !~ m/^\.+$/} readdir DIR;
    closedir DIR;
    return @dirs;
}
#my start directory
my $home = '/path/to/startdir'; 
#my current directory, this will change
my $curdir = $home; 
#fetch the first list
my @dirs = list_dirs($curdir);

#read chars without pressing Enter
ReadMode 'cbreak'; 

#since we only need a char at a time
#we should remember our previous input
my $input;
#this var holds the real user input (without the autocomplete)
my $realinput;
#all the processing happens here, in the while loop, char by char
while (my $key = ReadKey(0)) {
    #validate input by allowed chars.
    #special chars like backspace, tab and enter can be handled separately
    if (ord($key) == 127) { #backspace
        chop $realinput;
        $input = $realinput;
        $key = '';
    #other special char checks can be added here
    } elsif ($key =~ m/^[\w\d\.\/\-]$/) { #allowed chars (more should be added)
        $realinput = $realinput.$key;
    } else {
        print "\ninvalid input\n$input";
        next; #ignore this char and ask for another
    }

    my @found = grep {$_ =~ m/^$input$key/} @dirs;
    #if we added a trailing slash or pressed enter, 
    #then limit our search to find one dir that exactly matches
    @found = grep {$_ =~ m/^$input$/} @dirs if ($key eq '/' || ord($key)==10);

    if  (scalar(@found) == 0) {
        #not dir matching, do nothing
        print "\nNo match\n$input";
    } elsif (scalar(@found) == 1) {
        #only one dir found
        #cd and reset input
        $curdir .= '/'.$found[0];
        my @dirs = list_dirs($curdir);
        $input = '';
        $realinput = '';
        print "\ncd to $curdir\n";
    } elsif (scalar(@found) > 1){
        print "\n".join(' ',map {$_.'/'} @found);
        $input .= $key;
        #find least match of matching dirs and autocomplete as much as possible 
        #assign the smallest matching string to $input and go to next loop
        for (my $i = length($input); $i <= length($found[0]); $i++) {
            my $matchcount = 0;
            my $tmpstr = substr($found[0], 0, $i);
            foreach my $d(@found) {
                $matchcount++ if ($d =~ m/^$tmpstr/);
            }
            if ($matchcount == scalar(@found)) {
                $input = $tmpstr;
            } else {
                last;
            }
        }
        print "\n$input";
    }
}

EDIT:

I've updated my script. Added autocomplete support like when pressing TAB in bash, and backspace support and checks for certain characters. Also I've added a trailing slash for listing the directories and another check for exact matches. So if you have directories Documents/ and Documents_bak/, pressing D in the input will autocomplete to Documents and you have to press / or Enter to cd to Documents/.

I think this script is a good way to show that everything is possible. However each goal requires certain amount of work. Replacing bash which is almost 1MB compiled with a perl script, is not a realistic goal. But the behavior you asked for is demonstrated exactly in this script. It can be enriched with more checks, more special keys handling and more code cleaning/optimising.

foibs
  • 3,258
  • 1
  • 19
  • 13
  • Thanks for that, I'll have a look at and study the link. I agree that the implementation should be simple (since I'm just regexing strings and redisplaying a new list, or cd-ing conditional on whether I find a unique match or not) and the only problem I really have (or maybe had) is populating STDIN with stuff in the background (so like with tab-completion it looks and behaves like stuff you actually typed even though you didn't). Once I'm over that the coding should be simple :] – HexedAgain Dec 03 '13 at 23:18
  • Further (noting that I can look at your code and nod my head in most parts), the main thing I'm after is the following behaviour: If on the commandline 'd'< TAB > would expand to 'directory_' I want to get "directory_" in my perlscript as though I had actually typed it in just the same way as it would be with tab-autocompletion; i.e. it looks like I actually typed it even though I didn't. (and of course I wouldn't be pressing a tab here because it would happen in response to STDIN getting a 'd' and my script processing it) – HexedAgain Dec 03 '13 at 23:26
  • @HexedAgain If I understand correctly, you're looking for the `I'm sleepy` part. You don't have to populate the STDIN, you just need to assign the proper value to `$input`. The printing I do in my script is just to help the user understand what's going on. The STDIN is just the $key you press. A single char. For the missing part, I was thinking about a loop which takes `substr($input,length($input),$i++)` and stops until one element of @found doesn't match. I'll edit my answer in the morning to add this part – foibs Dec 03 '13 at 23:46
  • 1
    You can save for me the job of coding up "I'm sleepy". :] The reason I'm wanting to populate STDIN is because that is what I expect bash is doing when you hit a tab (so in response to the events 'k' + < TAB > from the keyboard, say; bash, I expect, does some low level OS stuff and fills up the rest of STDIN behind the scenes (so it might fill populate STDIN with 'eyboard_' if I had a directories keyboard_shorcuts and keyboard_styles for example). It is the same sort of low-level stuff I'm looking to perform from within perl) – HexedAgain Dec 03 '13 at 23:57
  • 1
    ... what I would actually do in terms of saving variables building up a new list of directories or possible completions and printing them out after every character, and the act of cd-ing is as you suggest quite straightforward, it's the "look and feel" of tab-completion I'm hankering for here. – HexedAgain Dec 04 '13 at 00:17
  • ...For more clarification I edited my Question again – HexedAgain Dec 04 '13 at 01:33
  • @HexedAgain Hi, I'm not so sleepy anymore so I updated my answer. I believe it does exactly what you describe. It's not the cleanest script but you get the point. I think you can take it from here :) – foibs Dec 04 '13 at 11:19
  • Hi cheers for that!... I feel a bit guilty in that it was something I was going to code up myself and didn't - though I will spend some time tweaking it (doesn't quite work on my system yet) and optimising it where I can and will post back my results once I get it to a stage where I can call it my own...But good job :] Further, I am quite surprised at the completion actually here - it works as I imagined I'd want it to (I was expecting that unless I went really low level I would have to fake it by printing something to screen that cannot be changed) :] – HexedAgain Dec 04 '13 at 22:16