Bash PWD Shortening
Asked Answered
S

8

23

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.

Scissor answered 24/10, 2009 at 1:23 Comment(3)
Personally, I have the prompt just be the top two directories, so the above would become like/shortened. I use ZSH, though, so I don't know how you'd do it in bash.Hippocrene
@Hippocrene pwd | sed -e "s|.*/\(.*/.*\)|\1|"Brightwork
And for use in PS1, one might: function pwd_depth_limit_2 { if [ "$PWD" = "$HOME" ] then echo -n "~" else pwd | sed -e "s|.*/(.*/.*)|\1|" fi }Brightwork
B
5

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
Boser answered 24/10, 2009 at 2:51 Comment(5)
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. :-/Sepulture
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.Scissor
If it's too slow let me know, it can certainly be made faster if needed.Boser
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] "Scissor
It doesn't update, when I change the current path (cd somepath). Any suggestions?Spindlelegs
C
34

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.

Corron answered 24/10, 2009 at 17:46 Comment(1)
This is great. Simple and effective.Tavares
E
18

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.)

Ethanol answered 24/10, 2009 at 5:22 Comment(1)
I like the idea of displaying the unique prefix that is tab-completable.Soulful
I
15

FYI, there is a built-in \w "shortener" in Bash 4+:

PROMPT_DIRTRIM=3

will shorten /var/lib/whatever/foo/bar/baz to .../foo/bar/baz.

Irrepealable answered 16/4, 2013 at 14:51 Comment(3)
Great; just to clarify: requires bash v4+ (OS X 10.8, for instance, comes with bash 3.2.48).Washroom
Just brew install bash to get the latest version on mac.Hyden
And set the new bash as default shell: sudo -s; echo /usr/local/bin/bash >> /etc/shells; chsh -s /usr/local/bin/bashHyden
R
11

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') "                            
} 
Roter answered 1/6, 2010 at 16:58 Comment(0)
B
5

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
Boser answered 24/10, 2009 at 2:51 Comment(5)
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. :-/Sepulture
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.Scissor
If it's too slow let me know, it can certainly be made faster if needed.Boser
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] "Scissor
It doesn't update, when I change the current path (cd somepath). Any suggestions?Spindlelegs
O
3

Here's another spin on Evan's answer:

enter image description here

This one uses plus (+) instead of an asterisk (*) for truncated paths. It replaces the HOME path with ~, and it leaves the final directory segment intact. If the final segment is over 20 characters, it shortens it to the tab-completable bit and adds an ellipses (...).

#!/bin/bash
# Modified from https://mcmap.net/q/554254/-bash-pwd-shortening
# By Alan Christopher Thomas (http://alanct.com)

__pwd_ps1 ()
{
    begin=""
    homebegin=""
    shortbegin=""
    current=""
    end="${2:-$(pwd)}/" # The unmodified rest of the path.
    end="${end#/}" # Strip the first /
    shortenedpath="$end"

    shopt -q nullglob && NGV="-s" || NGV="-u"
    shopt -s nullglob

    while [[ "$end" ]]
    do
      current="${end%%/*}" # Everything before the first /
      end="${end#*/}" # Everything after the first /

      shortcur="$current"
      for ((i=${#current}-2; i>=0; i--))
      do
        [[ ${#current} -le 20 ]] && [[ -z "$end" ]] && break
        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
        [[ -z "$end" ]] && shortcur="$subcurrent..." # Add character filler at the end of this string
        [[ -n "$end" ]] && shortcur="$subcurrent+" # Add character filler at the end of this string
      done

      begin="$begin/$current"
      homebegin="$homebegin/$current"
      [[ "$homebegin" =~ ^"$HOME"(/|$) ]] && homebegin="~${homebegin#$HOME}" # Convert HOME to ~
      shortbegin="$shortbegin/$shortcur"
      [[ "$homebegin" == "~" ]] && shortbegin="~" # Use ~ for home
      shortenedpath="$shortbegin/$end"
    done

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

    [[ ! "$shortenedpath" =~ ^"~" ]] && printf "/$shortenedpath" # Make sure it starts with /
    [[ "$shortenedpath" =~ ^"~" ]] && printf "$shortenedpath" # Don't use / for home dir

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

Download the script here and include it in your .bashrc:

https://raw.github.com/alanctkc/dotfiles/master/.bash_scripts/pwd-prompt.bash

. ~/.bash_scripts/pwd-prompt.bash

Add the directory to your PS1 like this:

export PS1="[other stuff...] \$(__pwd_ps1)\$ "
Obstreperous answered 24/10, 2013 at 15:15 Comment(0)
S
2

Here's a relatively easy perl solution. This is short enough that you could embed it directly in PS1 rather than invoking a script. It gives all the characters of the truncated names rather than replacing with '.'


$ echo '/this/is/a/realy/long/path/id/like/shortened' |
 perl -F/ -ane 'print join( "/", map { $i++ < @F - 2 ?
 substr $_,0,3 : $_ } @F)'
/thi/is/a/rea/lon/pat/id/like/shortened

I'm not immediately seeing a nice way to replace characters with '.', but here's an ugly way:


echo '/this/is/a/realy/long/path/id/like/shortened' |
 perl -F/ -ane 'print join( "/", map { m/(.)(.*)/;
 $_ = $1 . "." x (length $2 > 2 ? 2 : length $2 ) if $i++ < @F - 2; $_ } @F)'
/t../i./a/r../l../p../i./like/shortened
Songful answered 24/10, 2009 at 8:48 Comment(1)
Thanks for this. I borrowed it to suggest an answer to (pretty much) the same question on Super User. superuser.com/questions/180257/…Massasauga
T
0

Try this:

PS1='$(pp="$PWD/" q=${pp/#"$HOME/"/} p=${q%?};((${#p}>19))&&echo "${p::9}…${p:(-9)}"||echo "$p") \$'

It transforms

~/.vim/bundle/ack.vim/plugin

to

.vim/bund…im/plugin

transfrom

/usr/share/doc/xorg-x11-font-utils-7.5/

to

/usr/shar…utils-7.5

And when $PWD same as $HOME, show nothing.

Bonus: you could modify number of length to fit you need.

Topography answered 26/5, 2018 at 13:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.