Bash PWD Shortening - linux

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.

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.

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

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.

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') "
}

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

Here's another spin on Evan's answer:
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 http://stackoverflow.com/a/1617048/359287
# 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)\$ "

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++ &lt #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

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.

Related

Count number of specific characters in input string

Im trying to count the number of letters, numbers and special characters in an input string
The user would type the string and then finish with a * to finish the program should then display a count for the number of letters numbers and special characters
So far i have this but i get errors on line 21 which i think is the else statement
The exact error message i get is "./masher3: line 21: 0: command not found"
#!/bin/bash
numcount=0
charcount=0
othercount=0
echo "Input string"
for char in $#
do
if [[ $char == "*" ]]
then
break
elif [[ $char == '0-9' ]]
then
$numcount = $numcount + 1
elif [[ $char == 'A-Z' ]]
then
$charcount = $charcount + 1
else
$othercount = $othercount + 1 <----- Error on this line
fi
done
echo $charcount
This program is written in pure bash (without calling any external programs).
Also look below the code – I added some more information.
#!/bin/bash
# Print the message without going to next line (-n)
echo -n "Your input string: "
# Read text from standard input. ‘-d '*'’ stops
# reading at first ‘*’ character. Remove it to
# terminate on press of key Enter.
# Result is stored in variable $input.
#
# -e enables backspace and other keys.
read -ed '*' input
# Jump to next line
echo
# Fill all counters with zeros
letters=0
digits=0
spaces=0
others=0
# While $input contains some text…
while [[ -n "$input" ]]
do
# Get the first character
char="${input:0:1}"
# Take everything from $input except
# the first character and store it again
# in $input
input="${input:1}"
# Is the character space?
if [[ "$char" == " " ]]
then
# Increase the $spaces variable by one
((spaces++))
# Else: If the $char after removal of all
# letters in (english) alphabet is empty string?
# That will be true when the $char is letter.
elif [[ -z "${char//[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]/}" ]]
then
# Increase $letters
((letters++))
# Else: If the $char …
# Just the same for digits
elif [[ -z "${char//[0123456789]/}" ]]
then
((digits++))
# Else increase the $others variable
else
((others++))
fi
done
# Show values
echo "Letters: $letters"
echo "Digits: $digits"
echo "Spaces: $spaces"
echo "Other characters: $others"
Also open/download the Bash Reference Manual (available as single page, plaintext, PDF). You probably have one copy already installed if you use Linux. Try commands info bash (usually shows hypertext browser if installed) or man bash (single page documentation but usually the same). It is sometimes hard to understand for beginners but you will learn more information about this programming language.
Bash has many builtin commands (such as read, [[, echo, printf etc.) that work like ordinary commands. Their help is in the Reference Manual or can be shown by typing help command_name in your bash shell.
See my other answer for solution.
Your program is quite weird.
Assignment into variable looks like variable=42 (not $variable = 42)
You have to use $((…)) syntax to perform calculations
[[ $char == '0-9' ]] means “When the character is exactly 0-9
$char contains separate arguments of the program, not characters in input.
$othercount = … means “run command with name specified in variable $othercount with arguments = and ….
Assigning into variable in bash
You have to NOT write $ before variable name when you want to assign to it and you have to have NO whitespace before =:
my_variable=42
variable_2=$(($my_variable + 8))
echo $my_variable # Prints “50”

Search for multiple patterns in multiple files

This is related to Function to search of multiple patterns using grep
I want to search multiple files with multiple patterns using command such as follows:
myscript *.txt pattern1 pattern2 pattern3
I tried implementing the codes in the previous question but they do not work with wildcards. For example, following does not work:
#!/bin/bash
ARGS=$#
if [ $ARGS -lt 2 ]
then
echo "You entered only $ARGS arguments- at least 2 are needed."
exit
fi
search() {
if [ $# -gt 0 ]
then
local pat=$1
shift
grep -i "$pat" | search "$#"
else
cat
fi
}
for VAR in $1
do
file=$VAR
shift
cat "$file" | search "$#"
done
How can I create a script which can search for multiple files (taking it from first argument) to search multiple patterns (from rest of arguments)?
Did you try to use find and sed?
find . -name *.txt -exec sed -n -e '/pattern1/p' -e '/pattern2/' '{}' ';'
The -n option will make sure sed does not print all the file, and the p command prints the matching lines. Finaly, find will get all the files you need.
EDIT:
If you want to put that in a script to generate the sed command, you can use this trick.
EDIT 2:
As #shellter said, it is usually better to use options, and as your script is written, *.txt will be expanded by bash. To avoid that, you'll need to quote the first argument.
As usual, there is several solutions to your problem:
Solution 1 (Using bash built-in):
#! /usr/bin/env bash
set -o nounset # Throw error if variable not set
set -o errexit # Exit if error is thrown
work_dir=$PWD # directory to search from
# Reading the command line
files_pattern=${1:-}; # Save first argument as files pattern.
shift 1; # Move $1 to next argument (and propagate such as $n gets $n+1)
echo "==> Files to search follow pattern: ${files_pattern}"
_len=$#; #save the number of arguments.
for (( i=0; i<$_len; i=$i+1 )); # Go through the search patterns.
do
search_patterns[$i]=$1; # store the next search pattern
shift 1; # move $1 to next patern.
echo "==> New search pattern #$i: ${search_patterns[$i]}"
done
while read -r file; # Go through all the matching files
do
echo "==> In file: ${file}"
while read -r line; # Go though all the lines in the file
do
for regex in "${search_patterns[#]}"; # iterate trough patterns
do
[[ "${line}" =~ $regex ]] && echo "${line}";
done
done < ${file}
done < <(find $work_dir -iname $files_pattern -print) # find all the files matching file_pattern
Solution 2 (using grep):
#! /usr/bin/env bash
set -o nounset # Throw error if variable not set
set -o errexit # Exit if error is thrown
work_dir=$PWD # directory to search from
# Reading the command line
files_pattern=${1:-}; # Save first argument as files pattern.
shift 1; # Move $1 to next argument (and propagate such as $n gets $n+1)
echo "==> Files to search follow pattern: ${files_pattern}"
while [ $# -gt 0 ]; # Go through the search patterns.
do
search_patterns+="$1"; # store the next search pattern
shift 1; # move $1 to next patern.
[ $# -gt 0 ] && search_patterns+="|" #Add or option
done
echo "==> Search patterns: ${search_patterns}"
cd ${work_dir} && egrep -iR '('"${search_patterns}"')' && cd -;
Solution 3 (Using sed):
#! /usr/bin/env bash
set -o nounset # Throw error if variable not set
set -o errexit # Exit if error is thrown
work_dir=$PWD # directory to search from
# Reading the command line
files_pattern=${1:-}; # Save first argument as files pattern.
shift 1; # Move $1 to next argument (and propagate such as $n gets $n+1)
echo "==> Files to search follow pattern: ${files_pattern}"
while [ $# -gt 0 ]; # Go through the search patterns.
do
search_patterns+="/$1/p;"; # store the next search pattern
shift 1; # move $1 to next patern.
[ $# -gt 0 ] && search_patterns+=" " #Add or option
done
echo "==> Search patterns: ${search_patterns}"
# Will print file names, and then matching lines
find "$work_dir" -iname "$files_pattern" -print -exec sed -n "${search_patterns}" '{}' ';'
I am sure there is plenty other ways to tweak or solve this problem, but this should get you started.
Good Luck!

How to show the file with most hard links in a directory in bash

Does anyone know the specific command for how to show the file with most hard links in a directory on terminal on unix?
This solution works for all filenames (including ones with newlines in them), skips non-files, and prints the paths of all files that have the maximum number of links:
dir=$1
# Support empty directories and directories containing files whose
# names begin with '.'
shopt -s nullglob dotglob
declare -i maxlinks=0 numlinks
maxlinks_paths=()
for path in "$dir"/* ; do
[[ -f $path ]] || continue # skip non-files
numlinks=$(stat --format '%h' -- "$path")
if (( numlinks > maxlinks )) ; then
maxlinks=numlinks
maxlinks_paths=( "$path" )
elif (( numlinks == maxlinks )) ; then
maxlinks_paths+=( "$path" )
fi
done
# Print results with printf and '%q' to quote unusual filenames
(( ${#maxlinks_paths[*]} > 0 )) && printf '%q\n' "${maxlinks_paths[#]}"

custom bash completion with whitespace and paths

I can't figure out what I am doing wrong. I have my bash_completion file setup as such:
_bcd()
{
local cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=( $(compgen -W "$(back_directory.pl --complete $cur)" -- $cur) )
}
complete -o filenames -o nospace -F _bcd bcd
back_directory.pl is a program that will return directories paths up the tree: back_directory.pl --complete Th produces: This\ test/
But:
22:50:24-Josh#Joshuas-MacBook-Air:~/Desktop/bcd/This test/more white/t$ bcd Th<TAB><TAB>
This test/
As shown above, it doesn't auto complete for directories with whitespace in them (but it shows the completion option).
It should look like this: bcd This\ test/
I thought -o filenames should add the backslashes to escape the whitespace. Thanks for any help :)
Your single call to compgen produces a single word (containing embdedded newlines), so you are only adding a single possible completion to COMPREPLY. Instead, you need to process the output of back_directory.pl one item at a time. Each item is tested as a possible match, and if compgen returns a non-empty string, add that to COMPREPLY.
_bcd() {
local cur=${COMP_WORDS[COMP_CWORD]}
IFS=: read -a matches < <(back_directory.pl --complete "$cur")
for match in "${matches[#]}"; do
possible=$(IFS= compgen -W "$match" -- "$cur")
[[ $possible ] && COMPREPLY+=( "$possible" )
done
}
(Note: I'm assuming back_directory.pl will produce a single line of output similar to
directory1:directory two:directory three:directory4
)
For the sake of completion, this is the final file:
_bcd()
{
local cur=${COMP_WORDS[COMP_CWORD]}
IFS=: read -a matches < <(back_directory.pl --complete "$cur")
for match in "${matches[#]}"; do
possible=$(IFS= compgen -W "$match" -- "${cur#/}")
[[ $possible ]] && COMPREPLY+=( "$possible" )
done
longest=""
for e in "${COMPREPLY[#]}"; do
if [[ "$longest" == "" ]]; then
longest="$e"
fi
while [[ ! "$e" =~ ^$longest ]]; do
longest=${longest%?}
done
done
if [[ $longest != "$input" && "$cur" =~ ^/ ]]; then
for ((i=0; i<${#COMPREPLY[#]}; i++))
do
COMPREPLY[$i]="/${COMPREPLY[$i]}"
done
fi
}
complete -o filenames -o nospace -F _bcd bcd
The script back_directory.pl --complete will return a single line of paths delimited by colons.
My solution seems to be pretty terrible but it works.
Basically it removes a beginning slash from the current word, creates all the matches (containing no beginning slash) and then checks to see whether $longest is different from $input, which would mean bash would change your current word to something different--in which case we add back a beginning slash.

List only common parent directories for files

I am searching for one file, say "file1.txt", and output of find command is like below.
/home/nicool/Desktop/file1.txt
/home/nicool/Desktop/dir1/file1.txt
/home/nicool/Desktop/dir1/dir2/file1.txt
In above cases I want only common parent directory, which is "/home/nicool/Desktop" in above case. How it can be achieved using bash? Please help to find general solution for such problem.
This script reads lines and stores the common prefix in each iteration:
# read a line into the variable "prefix", split at slashes
IFS=/ read -a prefix
# while there are more lines, one after another read them into "next",
# also split at slashes
while IFS=/ read -a next; do
new_prefix=()
# for all indexes in prefix
for ((i=0; i < "${#prefix[#]}"; ++i)); do
# if the word in the new line matches the old one
if [[ "${prefix[i]}" == "${next[i]}" ]]; then
# then append to the new prefix
new_prefix+=("${prefix[i]}")
else
# otherwise break out of the loop
break
fi
done
prefix=("${new_prefix[#]}")
done
# join an array
function join {
# copied from: http://stackoverflow.com/a/17841619/416224
local IFS="$1"
shift
echo "$*"
}
# join the common prefix array using slashes
join / "${prefix[#]}"
Example:
$ ./x.sh <<eof
/home/nicool/Desktop1/file1.txt
/home/nicool/Desktop2/dir1/file1.txt
/home/nicool/Desktop3/dir1/dir2/file1.txt
eof
/home/nicool
I don't think there's a bash builtin for this, but you can use this script, and pipe your find into it.
read -r FIRSTLINE
DIR=$(dirname "$FIRSTLINE")
while read -r NEXTLINE; do
until [[ "${NEXTLINE:0:${#DIR}}" = "$DIR" || "$DIR" = "/" ]]; do
DIR=$(dirname "$DIR")
done
done
echo $DIR
For added safety, use -print0 on your find, and adjust your read statements to have -d '\0'. This will work with filenames that have newlines.
lcp() {
local prefix path
read prefix
while read path; do
while ! [[ $path =~ ^"$prefix" ]]; do
[[ $prefix == $(dirname "$prefix") ]] && return 1
prefix=$(dirname "$prefix")
done
done
printf '%s\n' "$prefix"
return 0
}
This finds the longest common prefix of all of the lines of standard input.
$ find / -name file1.txt | lcp
/home/nicool/Desktop

Resources