Bash Pwd Shortening

Bash PWD Shortening

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

How to shorten path parts between // in bash

If you want to unconditionally shorten all path components, you can do it quite easily with sed:

sed 's:\([^/]\)[^/]*/:\1/:g'

If you want to also insert ~ at the beginning of paths which start with $HOME, you can add that to the sed command (although this naive version assumes that $HOME does not include a colon).

sed 's:^'"$HOME"':~:/;s:\([^/]\)[^/]*/:\1/:g'

A better solution is to use bash substitution:

short_pwd() {
local pwd=$(pwd)
pwd=${pwd/#$HOME/\~}
sed 's:\([^/]\)[^/]*/:\1/:g' <<<"$pwd"
}

With that bash function, you can then "call" it from your PS1 string:

$ PS1='$(short_pwd)\$ '
~/s/tmp$ PS1='\$ '
$

Bash prompt shortening

You could try this:

if ((start > 1)); then
name=$(IFS=/; echo .../"${elts[*]:start}")
# If your terminal is correctly set up for unicode, you can save two character positions:
# name=$(IFS=/; echo …/"${elts[*]:start}")
fi

Note that in bash, in an arithmetic context, which includes the inside of ((...)) and array subscripts, you can write just the name of the variable; no need for sigils.

Another way to do it would be

if ((start > 1)); then
printf -v name "/%s" "${elts[@]:start}"
name=...$name
fi

Yet another solution, using regex captures in the BASH_REMATCH array rather than splitting and rejoining the string:

dirtrim () { 
local path="$1";
[[ $path =~ ^"$HOME"(/.*)? ]] && path=~${BASH_REMATCH[1]};
((PROMPT_DIRTRIM)) &&
[[ $path =~ ...*((/[^/]*){$PROMPT_DIRTRIM}) ]] &&
path=…${BASH_REMATCH[1]};
echo "$path"
}

The ((PROMPT_DIRTRIM)) test is not completely robust because of the peculiarities of bash evaluation in an arithmetic context. For distribution you might prefer something like [[ $PROMPT_DIRTRIM =~ ^[1-9][0-9]*$ ]]

How can I shortern my command line prompt's current directory?

Consider this script using awk instead of sed for your case:

pwd_length=14
pwd_symbol="..."
newPWD="${PWD/#$HOME/~}"
if [ $(echo -n $newPWD | wc -c | tr -d " ") -gt $pwd_length ]
then
newPWD=$(echo -n $newPWD | awk -F '/' '{
print $1 "/" $2 "/.../" $(NF-1) "/" $(NF)}')
fi
PS1='${newPWD}$ '

For your example of directory ~/workspace/projects/project1/folder1/test it makes PS1 as: ~/workspace/.../folder1/test

UPDATE

Above solution will set your prompt but as you noted in your comment that it will NOT change PS1 dynamically when you change directory. So here is the solution that will dynamically set PS1 when you change directories around.

Put these 2 lines in your .bashrc file:

export MYPS='$(echo -n "${PWD/#$HOME/~}" | awk -F "/" '"'"'{
if (length($0) > 14) { if (NF>4) print $1 "/" $2 "/.../" $(NF-1) "/" $NF;
else if (NF>3) print $1 "/" $2 "/.../" $NF;
else print $1 "/.../" $NF; }
else print $0;}'"'"')'
PS1='$(eval "echo ${MYPS}")$ '

if (NF > 4 && length($0) > 14) condition in awk will only apply special handling when your current directory is more than 3 directories deep AND if length of $PWD is more than 14 characters otherwise and it will keep PS1 as $PWD.

eg: if current directory is ~/workspace/projects/project1$ then PS1 will be ~/workspace/projects/project1$

Effect of above in .bashrc will be as follows on your PS1:

~$ cd ~/workspace/projects/project1/folder1/test
~/workspace/.../folder1/test$ cd ..
~/workspace/.../project1/folder1$ cd ..
~/workspace/.../project1$ cd ..
~/.../projects$ cd ..
~/workspace$ cd ..
~$

Notice how prompt is changing when I change directories. Let me know if this is not what you wanted.

Get current directory or folder name (without the full path)

No need for basename, and especially no need for a subshell running pwd (which adds an extra, and expensive, fork operation); the shell can do this internally using parameter expansion:

result=${PWD##*/}          # to assign to a variable
result=${result:-/} # to correct for the case where PWD=/

printf '%s\n' "${PWD##*/}" # to print to stdout
# ...more robust than echo for unusual names
# (consider a directory named -e or -n)

printf '%q\n' "${PWD##*/}" # to print to stdout, quoted for use as shell input
# ...useful to make hidden characters readable.

Note that if you're applying this technique in other circumstances (not PWD, but some other variable holding a directory name), you might need to trim any trailing slashes. The below uses bash's extglob support to work even with multiple trailing slashes:

dirname=/path/to/somewhere//
shopt -s extglob # enable +(...) glob syntax
result=${dirname%%+(/)} # trim however many trailing slashes exist
result=${result##*/} # remove everything before the last / that still remains
result=${result:-/} # correct for dirname=/ case
printf '%s\n' "$result"

Alternatively, without extglob:

dirname="/path/to/somewhere//"
result="${dirname%"${dirname##*[!/]}"}" # extglob-free multi-trailing-/ trim
result="${result##*/}" # remove everything before the last /
result=${result:-/} # correct for dirname=/ case

Bash: How to shorten long lines of a log file whilst keeping a fixed number of characters from beginning and end of each line?

You can do this with awk

awk '
(length > 100) {
l=length
$0 = substr($0,0,40) "..."l-80" Characters Removed..." substr($0,l-39)
}1' ./infile

Proof of Concept

$ cat ./infile
|- These are the first 40 characters --|0123456789012345678901234567890|-- These are the last 40 characters --|
12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
9012345678901234567890
2345678901234567890
12345678901234567asdfasd9as98jf-a9jfa9uhf0sd9uhfas0dfadfa890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
123456789012345678901234567890123456789012345678901234567aisfjds9dafa908sfj9asdjf9asdf89012345678901234567890123456789012345678901234567890
12345678901234567890123456789012345678901234567890123456789012345678901234567890123456asf9jasf-asjf0as8789012345678901234567890

$ awk '
(length > 100) {
l=length
$0 = substr($0,0,40) "..."l-80" Characters Removed..." substr($0,l-39)
}1' ./infile
|- These are the first 40 characters --|...31 Characters Removed...|-- These are the last 40 characters --|
1234567890123456789012345678901234567890...30 Characters Removed...1234567890123456789012345678901234567890
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
9012345678901234567890
2345678901234567890
12345678901234567asdfasd9as98jf-a9jfa9uh...70 Characters Removed...1234567890123456789012345678901234567890
1234567890123456789012345678901234567890...59 Characters Removed...1234567890123456789012345678901234567890
1234567890123456789012345678901234567890...47 Characters Removed...sf9jasf-asjf0as8789012345678901234567890

Setting value of command prompt ( PS1) based on present directory string length

A one-liner is basically always the wrong choice. Write code to be robust, readable and maintainable (and, for something that's called frequently or in a tight loop, to be efficient) -- not to be terse.

Assuming availability of bash 4.3 or newer:

# Given a string, a separator, and a max length, shorten any segments that are
# longer than the max length.
shortenLongSegments() {
local -n destVar=$1; shift # arg1: where do we write our result?
local maxLength=$1; shift # arg2: what's the maximum length?
local IFS=$1; shift # arg3: what character do we split into segments on?
read -r -a allSegments <<<"$1"; shift # arg4: break into an array

for segmentIdx in "${!allSegments[@]}"; do # iterate over array indices
segment=${allSegments[$segmentIdx]} # look up value for index
if (( ${#segment} > maxLength )); then # value over maxLength chars?
segment="${segment:0:3}*${segment:${#segment}-3:3}" # build a short version
allSegments[$segmentIdx]=$segment # store shortened version in array
fi
done
printf -v destVar '%s\n' "${allSegments[*]}" # build result string from array
}

# function to call from PROMPT_COMMAND to actually build a new PS1
buildNewPs1() {
# declare our locals to avoid polluting global namespace
local shorterPath
# but to cache where we last ran, we need a global; be explicit.
declare -g buildNewPs1_lastDir
# do nothing if the directory hasn't changed
[[ $PWD = "$buildNewPs1_lastDir" ]] && return 0
shortenLongSegments shorterPath 10 / "$PWD"
PS1="${shorterPath}\$"
# update the global tracking where we last ran this code
buildNewPs1_lastDir=$PWD
}

PROMPT_COMMAND=buildNewPs1 # call buildNewPs1 before rendering the prompt

Note that printf -v destVar %s "valueToStore" is used to write to variables in-place, to avoid the performance overhead of var=$(someFunction). Similarly, we're using the bash 4.3 feature namevars -- accessible with local -n or declare -n -- to allow destination variable names to be parameterized without the security risk of eval.


If you really want to make this logic only apply to the last two directory names (though I don't see why that would be better than applying it to all of them), you can do that easily enough:

buildNewPs1() {
local pathPrefix pathFinalSegments
pathPrefix=${PWD%/*/*} # everything but the last 2 segments
pathSuffix=${PWD#"$pathPrefix"} # only the last 2 segments

# shorten the last 2 segments, store in a separate variable
shortenLongSegments pathSuffixShortened 10 / "$pathSuffix"

# combine the unshortened prefix with the shortened suffix
PS1="${pathPrefix}${pathSuffixShortened}\$"
}

...adding the performance optimization that only rebuilds PS1 when the directory changed to this version is left as an exercise to the reader.

How to get a vim-tab-like short path in bash script?

Using sed is easiest because of regex backreference support, but for fun and profit a pure bash solution:

path="$(while read -rd/; do echo -n ${REPLY::1}/; done <<< "$PWD"; echo "${PWD##*/}")"

The value of $PWD is fed into the while loop via the herestring syntax <<<, then split on slashes by read -rd/. Conveniently, the last component is ignored because it doesn't end in a slash, so read exits with a nonzero status and terminates the loop.

Inside the loop, ${REPLY::1} takes only the first character of the path component, and echo -n prints it without a newline.

Finally, we print the last pathname component in full using ${PWD##*/}, which strips the longest prefix that matches */.

Shortening my prompt in Zsh

Old question, I know, but as an alternative solution I just discovered powerlevel9k, an extension of agnoster (they appear practically identical bar a fair few tweaks), which has this functionality built in.

Just set it as your zsh theme, then in .zshrc set

POWERLEVEL9K_SHORTEN_DIR_LENGTH=2

which ensures that only two directories are listed.

Alternate options are outlined in the readme.



Related Topics



Leave a reply



Submit