'Parse filename string and extract parent at specific level using shell

I have a filename as a string, say filname="a/b/c/d.png".

Is there a general method to extract the parent directory at a given level using ONLY shell parameter expansion?

I.e. I would like to extract "level 1" and return c or "level 2" and return b.

Explicitly, I DO NOT want to get the entire parent path (i.e. a/b/c/, which is the result of ${filename%/*}).



Solution 1:[1]

Using just shell parameter expansion, assuming bash, you can first transform the path into an array (splitting on /) and then ask for specific array indexes:

filename=a/b/c/d.png
IFS=/
filename_array=( $filename )
unset IFS

echo "0 = ${filename_array[0]}"
echo "1 = ${filename_array[1]}"
echo "2 = ${filename_array[2]}"
echo "3 = ${filename_array[3]}"

Running the above produces:

0 = a
1 = b
2 = c
3 = d.png

These indexes are the reverse of what you want, but a little arithmetic should fix that.

Solution 2:[2]

Using zsh, the :h modifier trims the final component off a path in variable expansion.

The (s:...:) parameter expansion flag can be used to split the contents of a variable. Combine those with normal array indexing where a negative index goes from the end of the array, and...

$ filename=a/b/c/d.png
$ print $filename:h
a/b/c
$ level=1
$ print ${${(s:/:)filename:h}[-level]}
c
$ level=2
$ print ${${(s:/:)filename:h}[-level]}
b

You could also use array subscript flags instead to avoid the nested expansion:

$ level=1
$ print ${filename[(ws:/:)-level-1]}
c
$ level=2
$ print ${filename[(ws:/:)-level-1]}
b

w makes the index of a scalar split on words instead of by character, and s:...: has the same meaning, to say what to split on. Have to subtract one from the level to skip over the trailing d.png, since it's not stripped off already like the first way.

Solution 3:[3]

The :h (head) and :t (tail) expansion modifiers in zsh accept digits to specify a level; they can be combined to get a subset of the path:

> filname="a/b/c/d.png"
> print ${filname:t2}
c/d.png
> print ${filname:t2:h1}
c
> print ${filname:t3:h1}
b

If the level is in a variable, then the F modifier can be used to repeat the h modifier a specific number of times:

> for i in 1 2 3; printf '%s: %s\n' $i ${filname:F(i)h:t}
1: c
2: b
3: a

Solution 4:[4]

If using printf (a shell builtin) is allowed then this will do the trick in bash:

filename='a/b/c/d.png'
level=2

printf -v spaces '%*s' $level
pattern=${spaces//?/'/*'}
component=${filename%$pattern}
component=${component##*/}
echo $component

prints out

b

You can assign different values to the variable level.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4