22

I seem to have failed at something pretty simple, in bash. I have a string variable that holds the full path to a directory. I'd like to assign the last two directories in it to another string. For example, if I have:

DIRNAME = /a/b/c/d/e

I'd like:

DIRNAME2 = d/e

I'm sure there's a simple bash construct or sed command that will do it, but it's escaping me. I'd sort of like a generalized version of basename or dirname that doesn't just return the extreme parts of a name.

Thanks!

Davide Fiocco
  • 5,350
  • 5
  • 35
  • 72
user1059256
  • 223
  • 1
  • 2
  • 4

8 Answers8

23
DIRNAME="/a/b/c/d/e"
D2=$(dirname "$DIRNAME")
DIRNAME2=$(basename "$D2")/$(basename "$DIRNAME")

Or, in one line (but be careful with all the double quotes — it is easier when it is split up):

DIRNAME2=$(basename "$(dirname "$DIRNAME")")/$(basename "$DIRNAME")

Don't try that game with back-quotes unless you're heavily into masochism. And if there might be spaces in the paths, use double quotes around the variable names.

This will work with almost any shell Korn Shell as well as Bash. In bash, there are other mechanisms available - other answers illustrate some of the many options, though expr is also an old-school solution (it was present in 7th Edition Unix too). This code using back-quotes works in Bash and Korn shell too — but not in Heirloom Shell (which is similar to a Unix System V Release 2/3/4 shell, IIRC).

DIRNAME2=`basename "\`dirname \\"$DIRNAME\\"\`"`/`basename "$DIRNAME"`

(Two levels of nesting is not too awful, but it is pretty bad; three gets really tricky!)

Testing

When testing path name manipulation that should survive spaces in the path name, it is worth testing with a name containing double-spaces (rather than, or as well as, single spaces). For example:

DIRNAME="/a b/ c d /  ee  ff  /  gg  hh  "
echo "DIRNAME=[[$DIRNAME]]"
echo "basename1=[[$(basename "$DIRNAME")]]"
echo "basename2=[[`basename \"$DIRNAME\"`]]"
echo
D2=$(dirname "$DIRNAME")
echo "D2=[[$D2]]"
DIRNAME2=$(basename "$D2")/$(basename "$DIRNAME")
echo "DIRNAME2=[[$DIRNAME2]]"
echo
DIRNAME3=$(basename "$(dirname "$DIRNAME")")/$(basename "$DIRNAME")
echo "DIRNAME3=[[$DIRNAME3]]"
DIRNAME4=`basename "\`dirname \\"$DIRNAME\\"\`"`/`basename "$DIRNAME"`
echo "DIRNAME4=[[$DIRNAME2]]"

The output from that is:

DIRNAME=[[/a b/ c d /  ee  ff  /  gg  hh  ]]
basename1=[[  gg  hh  ]]
basename2=[[  gg  hh  ]]

D2=[[/a b/ c d /  ee  ff  ]]
DIRNAME2=[[  ee  ff  /  gg  hh  ]]

DIRNAME3=[[  ee  ff  /  gg  hh  ]]
DIRNAME4=[[  ee  ff  /  gg  hh  ]]
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Wow, this is my first post on stackoverflow, and I'm extremely happy with the responses! I went w/ your one-line basename-dirname answer, since this is just a script I'll run as part of submitting numerical jobs. But I'll save this thread for lots of slick bash string tricks! – user1059256 Nov 22 '11 at 14:32
14

I prefer to use the builtins as much as I can, to avoid create unnecessary processes. Because your script may be run under Cygwin or other OS whose process creation are very expensive.

I think it's not so lengthy if you just want to extract two dirs:

base1="${DIRNAME##*/}"
dir1="${DIRNAME%/*}"
DIRNAME2="${dir1##*/}/$base1"

This can also avoid special char problems involved in executing another commands.

Lenik
  • 13,946
  • 17
  • 75
  • 103
5

I don't know of a method specifically for trimming paths, but you can certainly do it with bash's regular expression matching:

DIRNAME=/a/b/c/d/e
if [[ "$DIRNAME" =~ ([^/]+/+[^/]+)/*$ ]]; then
    echo "Last two: ${BASH_REMATCH[1]}"
else
    echo "No match"
fi

Note: I've made the pattern here a little more complex than you might expect, in order to handle some allowed-but-not-common things in the path: it trims trailing slashes, and tolerates multiple (redundant) slashes between the last two names. For example, running it on "/a/b/c//d//" will match "c//d".

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
4
DIRNAME="a/b/c/d/e"
DIRNAME2=`echo $DIRNAME | awk -F/ '{print $(NF-1)"_"$(NF)}'`

DIRNAME2 then has the value d_e

Change the underscore to whatever you wish.

coelmay
  • 51
  • 4
3
dirname=/a/b/c/d/e
IFS=/ read -a dirs <<< "$dirname"
printf "%s/%s\n" "${dirs[-2]}" "${dirs[-1]}"
  • best answer imo. Next up would be the use of built-ins by @Xiè Jìléi but that one doesn't interpolate properly for me. – Jim Apr 13 '21 at 02:57
2

I think there's a shorter way using globs, but:

$ DIRNAME='a/b/c/d/e'
$ LAST_TWO=$(expr "$DIRNAME" : '.*/\([^/]*/[^/]*\)$')
$ echo $LAST_TWO
d/e
harpo
  • 41,820
  • 13
  • 96
  • 131
1

this gets it done with pure bash:

p=/a/b/c/d/e ; echo ${p#/*/*/*/*}

d/e

but how to build the matching prefix pattern #word} part dynamically?

this produces the total number of slashes

echo ${p//[!\/]}

/////

and you can build the prefix pattern knowing that?

-1
function getDir() {
echo $1 | awk -F/ '
{
    n=NF-'$2'+1;
    if(n<1)exit;
    for (i=n;i<=NF;i++) {
        printf("/%s",$i);
    }
}'
}

dir="/a/b/c/d/e"
dir2=`getDir $dir 1`
echo $dir2

You can get any number of directories from the last from this function. For your case run it as,

dir2=`getDir $dir 2`;

Output : /d/e