tags:

views:

767

answers:

5

I'm looking for a bash function that will shorten long path names to keep my PS1 variable from getting excessively long. Something along the lines of:

/this/is/the/path/to/a/really/long/directory/i/would/like/shortened

might end up as:

/t../i../t../p../to/a/r../l../d../i/w../like/shortened

something that the took the path and a maximum acceptable number of characters to shorten to would be perfect for my .bashrc file.

+5  A: 

How about a Python script? This shortens the longest directory names first, one character at a time until it meets its length goal or cannot get the path any shorter. It does not shorten the last directory in the path.

(I started writing this in plain shell script but man, bash stinks at string manipulation.)

!/usr/bin/env python
import sys

try:
    path   = sys.argv[1]
    length = int(sys.argv[2])
except:
    print >>sys.stderr, "Usage: $0 <path> <length>"
    sys.exit(1)

while len(path) > length:
    dirs = path.split("/");

    # Find the longest directory in the path.
    max_index  = -1
    max_length = 3

    for i in range(len(dirs) - 1):
        if len(dirs[i]) > max_length:
            max_index  = i
            max_length = len(dirs[i])

    # Shorten it by one character.    
    if max_index >= 0:
        dirs[max_index] = dirs[max_index][:max_length-3] + ".."
        path = "/".join(dirs)

    # Didn't find anything to shorten. This is as good as it gets.
    else:
        break

print path

Example output:

$ echo $DIR
/this/is/the/path/to/a/really/long/directory/i/would/like/shortened
$ ./shorten.py $DIR 70
/this/is/the/path/to/a/really/long/directory/i/would/like/shortened 
$ ./shorten.py $DIR 65
/this/is/the/path/to/a/really/long/direc../i/would/like/shortened
$ ./shorten.py $DIR 60
/this/is/the/path/to/a/re../long/di../i/would/like/shortened
$ ./shorten.py $DIR 55
/t../is/the/p../to/a/r../l../di../i/wo../like/shortened
$ ./shorten.py $DIR 50
/t../is/the/p../to/a/r../l../d../i/w../l../shortened
John Kugelman
I was just writing a python script that's quite similar to this one. Mine does a bit more recursion, is slightly more efficient, and utterly fails to stop truncating when the desired length is reached. Therefore, I'm not going to bother finishing and posting it unless somebody cares. :-/
Benson
Nice. My only concern is the cost of executing a python script on every shell execution. I'll give it a try and let you know.
dlibby00
If it's too slow let me know, it can certainly be made faster if needed.
John Kugelman
Works just great. Here's my whole PS1 definition form .bashrc: PS1="\[\e[31m\]\H:\[\e[32m\]"'`path-shorten $PWD 50`'"\[\e[31m\] -->\[\e[0m\] "
dlibby00
+7  A: 

Here's a bash-only solution that you might like. This shortens each part of the path down to the shortest prefix that can still be tab-completed, and uses * instead of .. as the filler.

#!/bin/bash

begin="" # The unshortened beginning of the path.
shortbegin="" # The shortened beginning of the path.
current="" # The section of the path we're currently working on.
end="${2:-$(pwd)}/" # The unmodified rest of the path.
end="${end#/}" # Strip the first /
shortenedpath="$end" # The whole path, to check the length.
maxlength="${1:-0}"

shopt -q nullglob && NGV="-s" || NGV="-u" # Store the value for later.
shopt -s nullglob    # Without this, anything that doesn't exist in the filesystem turns into */*/*/...

while [[ "$end" ]] && (( ${#shortenedpath} > maxlength ))
do
  current="${end%%/*}" # everything before the first /
  end="${end#*/}"    # everything after the first /

  shortcur="$current"
  shortcurstar="$current" # No star if we don't shorten it.

  for ((i=${#current}-2; i>=0; i--))
  do
    subcurrent="${current:0:i}"
    matching=("$begin/$subcurrent"*) # Array of all files that start with $subcurrent. 
    (( ${#matching[*]} != 1 )) && break # Stop shortening if more than one file matches.
    shortcur="$subcurrent"
    shortcurstar="$subcurrent*"
  done

  begin="$begin/$current"
  shortbegin="$shortbegin/$shortcurstar"
  shortenedpath="$shortbegin/$end"
done

shortenedpath="${shortenedpath%/}" # strip trailing /
shortenedpath="${shortenedpath#/}" # strip leading /

echo "/$shortenedpath" # Make sure it starts with /

shopt "$NGV" nullglob # Reset nullglob in case this is being used as a function.

Give it the length as the first argument, and the path as the optional second argument. If no second argument is given, it uses the current working directory.

This will try to shorten to under the length given. If that's not possible, it just gives the shortest path it can give.

Algorithmically speaking, this is probably horrible, but it ends up being pretty fast. (The key to quick shell scripts is avoiding subshells and external commands, especially in inner loops.)

By design, it only shortens by 2 or more characters ('hom*' is just as many characters as 'home').

It's not perfect. There are some situations where it won't shorten as much as is possible, like if there are several files whose filenames share a prefix (If foobar1 and foobar2 exist, foobar3 won't be shortened.)

Evan Krall
I like the idea of displaying the unique prefix that is tab-completable.
Barry Brown
+1  A: 
William Pursell
Thanks for this. I borrowed it to suggest an answer to (pretty much) the same question on Super User. http://superuser.com/questions/180257/bash-prompt-how-to-have-the-initials-of-directory-path
Telemachus
+5  A: 

Doesn't give the same result, but my ~/.bashrc contains

_PS1 ()
{
    local PRE= NAME="$1" LENGTH="$2";
    [[ "$NAME" != "${NAME#$HOME/}" || -z "${NAME#$HOME}" ]] &&
        PRE+='~' NAME="${NAME#$HOME}" LENGTH=$[LENGTH-1];
    ((${#NAME}>$LENGTH)) && NAME="/...${NAME:$[${#NAME}-LENGTH+4]}";
    echo "$PRE$NAME"
}
PS1='\u@\h:$(_PS1 "$PWD" 20)\$ '

which limits the path shown to 20 characters max. If the path is over 20 characters, it will be shown like /...d/like/shortened or ~/.../like/shortened.

ephemient
This is great. Simple and effective.
Somebody still uses you MS-DOS
+6  A: 

I made some improvements to Evan Krall's code. It now checks to see if your path starts in $HOME and begins the shortened variety with ~/ instead of /h*/u*/

#!/bin/bash

begin="" # The unshortened beginning of the path.
shortbegin="" # The shortened beginning of the path.
current="" # The section of the path we're currently working on.
end="${2:-$(pwd)}/" # The unmodified rest of the path.

if [[ "$end" =~ "$HOME" ]]; then
    INHOME=1
    end="${end#$HOME}" #strip /home/username from start of string
    begin="$HOME"      #start expansion from the right spot
else
    INHOME=0
fi

end="${end#/}" # Strip the first /
shortenedpath="$end" # The whole path, to check the length.
maxlength="${1:-0}"

shopt -q nullglob && NGV="-s" || NGV="-u" # Store the value for later.
shopt -s nullglob    # Without this, anything that doesn't exist in the filesystem turns into */*/*/...

while [[ "$end" ]] && (( ${#shortenedpath} > maxlength ))
do
  current="${end%%/*}" # everything before the first /
  end="${end#*/}"    # everything after the first /

  shortcur="$current"
  shortcurstar="$current" # No star if we don't shorten it.

  for ((i=${#current}-2; i>=0; i--)); do
    subcurrent="${current:0:i}"
    matching=("$begin/$subcurrent"*) # Array of all files that start with $subcurrent. 
    (( ${#matching[*]} != 1 )) && break # Stop shortening if more than one file matches.
    shortcur="$subcurrent"
    shortcurstar="$subcurrent*"
  done

  #advance
  begin="$begin/$current"
  shortbegin="$shortbegin/$shortcurstar"
  shortenedpath="$shortbegin/$end"
done

shortenedpath="${shortenedpath%/}" # strip trailing /
shortenedpath="${shortenedpath#/}" # strip leading /

if [ $INHOME -eq 1 ]; then
  echo "~/$shortenedpath" #make sure it starts with ~/
else
  echo "/$shortenedpath" # Make sure it starts with /
fi

shopt "$NGV" nullglob # Reset nullglob in case this is being used as a function.

Also, here are some functions I put in my .bashrc file to shrink the path shown by the shell. I'm not sure if editing $PWD like this is completely safe as some scripts might depend on a valid $PWD string, but so far I haven't had problems with occasional use. Note that I saved the above script as "shortdir" and put it in my PATH.

function tinypwd(){
    PWD=`shortdir`
}

function hugepwd(){
    PWD=`pwd`
}

EDIT Oct 19 2010

The proper way to do the aliases in bash is by modifying the $PS1 variable; this is how the prompt is parsed. In MOST cases (99% of the time) the current path is in the prompt string as "\w". We can use sed to replace this with shortdir, like so:

#NOTE: trailing space before the closing double-quote (") is a must!!
function tinypwd(){                                                             
    PS1="$(echo $PS1 | sed 's/\\w/\`shortdir\`/g') "
}                                                                               

function hugepwd(){                                                             
    PS1="$(echo $PS1 | sed 's/[`]shortdir[`]/\\w/g') "                            
} 
bobpaul