Search code examples
bashshellparameter-expansion

Bash Parameter Expansion - get immediate parent directory of file


I'd like to get the name of the immediate parent directory of a given file, e.g. foo given /home/blah/foo/bar.txt, using a parameter expansion. Right now I can do it in two lines:

f="/home/blah/foo/bar.txt"
dir_name="${f%/*}"
immediate_parent="${dir_name##*/}"

But I'm very new to parameter expansions, so I assume this could be optimized. Is there a way to do it in only one line?


Solution

  • You can't do it with a single parameter expansion, but you can use =~, Bash's regex-matching operator:

    [[ $f =~ ^.*/(.+)/.+$ ]] && immediate_parent=${BASH_REMATCH[1]}
    

    Note: Assumes an absolute path with at least 2 components.

    If calling an external utility is acceptable, awk offers a potentially simpler alternative:

    immediate_parent=$(awk -F/ '{ print $(NF-1) }' <<<"$f")
    

    As for why it can't be done with a single parameter expansion:

    • Parameter expansion allows for stripping either a prefix (# / ##) or a suffix (% / %%) from a variable value, but not both.

      • Nesting prefix- and suffix-trimming expansions, while supported in principle, does not help, because you'd need iterative modification of values, whereas an expansion only ever returns the modified string; in other words: the effective overall expansion still only performs a single expansion operation, and you're again stuck with either a prefix or a suffix operation.
    • Using a single parameter expansion, extracting an inner substring can only be done by character position and length; e.g., a hard-coded solution based on the sample input would be: immediate_parent=${f:11:3}

      • You can use arithmetic expressions and even command substitutions as parameter expansion arguments, but the pointlessness of this approach - at least in this scenario - becomes obvious if we try it; note the embedded command substitutions - the first to calculate the character position, the second to calculate the length:

        immediate_parent=${f:$(d=${f%/*/*}; printf %s ${#d})+1:$(awk -F/ '{ print length($(NF-1)) }' <<<"$f")}