Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

Is there a way to write a bash script with the following functionalities?

  1. Be launched when I press some key or key combination. (not so important requirement)
  2. Identify the 7 most visited directories in last 3 hours.
  3. Offer me the list of this 7 directories so I can cycle through them with tab and shift-tab (backward). Then press enter would mean cd to the selected directory.

thank you

share|improve this question
1  
what do you mean by "most visited directory"? visited by what? –  Mat Apr 14 '11 at 8:01
    
my definition: Let's say that during 3 hours you call the cd command with 10 arguments (a1..a10) The directory ax is most visited if the command cd ax is called more often then cd ai, for each i in {a1, a10} where ax <> ai. –  xralf Apr 14 '11 at 8:50
add comment

2 Answers

up vote 6 down vote accepted

Add the following to your ~/.bashrc file:

#### cd history mechanism ##############################################

export CDHISTFILE=~/.cdhistory
if [ -e "$CDHISTFILE" ]
then
    cdht=`mktemp`
    tail -500 "$CDHISTFILE" > $cdht
    mv "$cdht" "$CDHISTFILE"
fi

function keep_cd_history() {
    if [ -z "$1" ] ; then d="$HOME" ; else d="$1" ; fi
    cdhcan=`readlink -f "$d"`
    if 'cd' "$d"
    then
        echo -e `date +%s`"\t"$cdhcan >> $CDHISTFILE
    fi
}
function pick_cwd_from_history() {
    f=~/.cdhistgo
    cdhistpick "$f"
    if [ -r "$f" ] ; then cd "`head -1 $f`" ; fi
}

alias cd=keep_cd_history
alias cdh=pick_cwd_from_history

########################################################################

The first section truncates the cd history mechanism's custom history file if it's gotten bigger than 500 lines since the last time we looked at it. We can't use Bash's built-in history because it doesn't include timestamps, which you need in order to get the "in the last 3 hours" behavior.

The two Bash functions do things we cannot do in the Perl code below, which otherwise does all the heavy lifting. The only tricky bit here is the readlink call, which canonicalizes the paths you use. We have to do that so that cd $HOME ; cd ; cd ~ ; cd ../$USER results in 4 instances of the same path in the cd history, not four different entries.

The aliases are just convenience wrappers for the functions.

Now the really tricky bit:

#!/usr/bin/perl -w
use strict;
use List::Util qw(min);

#### Configurables #####################################################

# Number of seconds back in time to look for candidate directories
my $history_seconds_threshold = 3 * 60 * 60;

# Ignore directories we have gone to less than this many times
my $access_count_threshold = 1;

# Number of directory options to give in pick list
my $max_choices = 7;


#### DO NOT OPEN. NO USER-SERVICEABLE PARTS INSIDE. ####################

# Get file name our caller wants the cd choice to be sent to
die "usage: $0 <choice_file>\n" unless $#ARGV == 0;
my $cdhg_file = $ARGV[0];
unlink $cdhg_file;      # don't care if it fails

# Build summary stats from history file to find recent most-accessed
my $oldest_interesting = time - $history_seconds_threshold;
my %stats;
open my $cdh, '<', "$ENV{HOME}/.cdhistory" or die "No cd history yet!\n";
while (<$cdh>) {
    chomp;
    my ($secs, $dir) = split /\t/;
    next unless $secs and $secs >= $oldest_interesting;
    ++$stats{$dir};
}

# Assemble directory pick list
my @counts = sort values %stats;
$access_count_threshold = $counts[$max_choices - 1] - 1
        if @counts > $max_choices;
my @dirs = grep { $stats{$_} > $access_count_threshold } keys %stats;
$max_choices = min($max_choices, scalar @dirs);

# Show pick list, and save response to the file pick_cwd_from_history()
# expects.  Why a file?  The shell must call chdir(2), not us, because
# if we do, we change only our CWD.  Can't use stdio; already in use.
my $choice;
if ($max_choices > 1) {
    for (my $i = 0; $i < $max_choices; ++$i) {
        print $i + 1, '. ', $dirs[$i], "\n";
    }
    print "\nYour choice, O splendid one? [1-$max_choices]: ";
    $choice = <STDIN>;
    chomp $choice;
    exit 0 unless $choice =~ /^[0-9]+$/ && $choice <= $max_choices;
}
elsif ($max_choices == 1) {
    print "Would you like to go to $dirs[0]? [y/n]: ";
    $choice = 1 if uc(<STDIN>) =~ /^Y/;
}
else {
    die "Not enough cd history to give choices!\n";
}
if ($choice) {
    open my $cdhg, '>', $cdhg_file or
            die "Can't write to $cdhg_file: $!\n";
    print $cdhg $dirs[$choice - 1], "\n";
}

Save this to a file called cdhistpick, make it executable, and put it somewhere in your PATH. You won't execute it directly. Use the cdh alias for that, as it passes in a necessary argument via pick_cwd_from_history().

How does it work? Ummmm, exercise for the reader? :)

To get your first requirement, the hotkey, you can use any macro recording program you like for your OS of choice. Just have it type cdh and press Enter for you. Or, you can run cdh yourself, since it's easy to type.

If you want a simpler but less functional alternative that will work everywhere, get into the habit of using Bash's reverse incremental search feature, Ctrl-R. Press that, then type "cd " (without the quotes, but with the trailing space) to be taken back to the previous cd command. Then each time you hit Ctrl-R, it takes you back to the cd command prior to that. In this way, you can walk backwards through all the cd commands you've given, within the limits of Bash's history feature.

Say:

$ echo $HISTFILESIZE

to see how many command lines Bash history will store for you. You might need to increase this to hold 3 hours worth of command history.

To search forward through your command history after having stepped backwards through it, press Ctrl-S.

If that doesn't work on your system, it is likely due to a conflict with software flow control. You can fix it with this command:

$ stty stop undef

That prevents Ctrl-S from being interpreted as the XOFF character.

The consequence of this is that you can then no longer press Ctrl-S to temporarily pause terminal output. Personally, the last time I used that on purpose was back in the days of slow modems. These days with fast scrolling and big scroll-back buffers, I only use this feature by accident, then have to spend a second remembering to press Ctrl-Q to get the terminal un-stuck. :)

share|improve this answer
    
Thank you. I didn't know this great use of history command. I will use it and if that's insufficient I will try to implement the timestamps. –  xralf Apr 14 '11 at 12:58
    
Can I go to forward too in history? –  xralf Apr 14 '11 at 12:59
    
This would be good for the following situation: I press Ctrl+R, then cd, then Ctrl+R 10 times (I'm doing it quickly) and I will note that I was too quick and want to go one step back. –  xralf Apr 14 '11 at 14:14
    
It does for me (bash 4.1.5 on Debian squeeze or 3.2.39 on Debian lenny or 4.1.7 on OpenBSD, started with HOME=/none bash). –  Gilles Apr 14 '11 at 19:09
    
Found the problem and fix, so added it to above and deleted my comments. –  Warren Young Apr 14 '11 at 23:34
show 1 more comment

Use this script. You need to install sqlite3 for using it. It launches sqlite3 in subshell to update the database (so that changing to directory is as fast as with normal cd). Download this script is source it in your .bashrc. Use command c to change to directories. If no argument is given to c then a sorted list of directories most accessed in last two days will be displayed on bash. You can pick one.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.