Wednesday, February 18, 2009

Bash prompt trick: cheap emulation of tcsh's pwd trailing component

I started Unix with Linux where Bash has always been the default shell, at least in my own reckoning. Hence I've always been using Bash as I never found the necessity nor the motivation to really switch to another shell.

When FreeBSD turned to be my favorite operating system, I had a chance to fiddle with tcsh. One thing I really liked was the "%c" prompt sequence, a.k.a. "trailing component of the current working directory". Basically, "%c02" in "/home/jlh/a/b" expands to something like "/<2>/a/b". Since then, I've always missed this sequence in Bash, as "\w" often expands to something far too long and "\W" is often not enough.

Therefore I implemented a function to mimic this behaviour in tcsh. I devised it to only use builtins so as to be cheap (read inexpensive, not lousy). Indeed, I think it would be really stupid to spawn many processes each time Enter is hit in my shell. Here it is:

traildir() {
local n=$1 dir=$2
local sl tildedir homelen shifted traildir

sl=/
tildedir=${dir#$HOME}
if ! [[ "$tildedir" == "$dir" ]]; then
tildedir="~$tildedir"
sl=
fi

set -- ${HOME//\// }
homelen=$#

shifted=0
set -- ${tildedir//\// }
if [[ $# -gt $n ]]; then
shifted=$(($# - $n))
shift $shifted
[[ -z "$sl" ]] && shifted=$(($shifted + $homelen - 1))
traildir="<$shifted>/$@"
else
traildir="$sl$@"
fi

echo ${traildir// /\/}
}


Then, you just have to set, for example:

TRAILPWD=2
export PS1='\u@\h:$(traildir $TRAILPWD $PWD) \#\$'


And that's it! Ok it still forks a process each time you hit enter, but it's not possible to achieve it less expensively with Bash anyway. Note that the example above uses a neat trick: if you need more trailing components in your prompt for your current task, just set TRAILPWD to the desired value.


Addendum on 2009/06/22


A smaller but less resilient version of this prompt is:

export PS1='\u@\h:$(set -- ${PWD//\// }; n=$(($# - $TRAILPWD)); s=; [[ $n -le 0 ]] && s=/ || shift $n; d="$@"; echo $s${d// /\/}) \#\$'


One advantage of this one is that is only relies on an environment variable, thus is inherited across forks. This is useful for instance if you call bash through sudo(8) and you wish to use the same prompt. This is impossible with the function-based prompt.

2 comments:

Unknown said...

Dear tataz,

There's a new feature in the freshly released bash4 which you may find interesting:

$ export PS1="\u@xxx:\w\$ "
bana@xxx:~$ cd lala/lili/lolo/lulu/
bana@xxx:~/lala/lili/lolo/lulu$ export PROMPT_DIRTRIM=2
bana@xxx:~/.../lolo/lulu$ export PROMPT_DIRTRIM=1
bana@xxx:~/.../lulu$ unset PROMPT_DIRTRIM
bana@xxx:~/lala/lili/lolo/lulu$

:*

Mike Heffner said...

Thanks for the code!

FWIW, here's a version that keeps the prefix (~ or /) similar to how it works in tcsh:

function traildir()
{
local n=$1 dir=$2
local sl tildedir homelen shifted traildir
local oldifs=$IFS

tildedir=${dir#$HOME}
if ! [[ "$tildedir" == "$dir" ]]; then
# Special break out case
[[ -z "$tildedir" ]] && { echo "~"; return 0; }

sl="~/"
else
sl="/"
fi

set -- ${HOME//\// }
homelen=$#

shifted=0
IFS="/"
set -- ${tildedir/\//}
if [[ $# -gt $n ]]; then
shifted=$(($# - $n))
shift $shifted
IFS="/"
traildir="$sl<$shifted>/$*"
else
IFS="/"
traildir="$sl$*"
fi

IFS=$oldifs
echo $traildir
}