M2bandit
M2bandit

Reputation: 99

How do you get the outermost parent directory in a file path in bash?

I tried using a while loop but I messed that up.

#!/bin/bash
parent=$(dirname "$1")
while ["$parent" -ne "/"]
do
    parent=$(dirname "$parent")
done  
echo "$parent"

the path can be any length and is passed in via console.

./script.bash /file/path/can/be/huge.bash the desired output is file

Upvotes: 0

Views: 2108

Answers (4)

Mort
Mort

Reputation: 3549

I'm a bit confused, I think this is all you want.

#!/bin/bash
echo "$1" | sed -e 's=^/==' -e 's=/.*=='

Or

#!/bin/bash
path=${1#/}
echo "${path%%/*}"

With the first case, since it is a one-liner, you really probably don't need an external script.

If the path can have a leading . (e.g. ./file/path/can/be/huge.bash), this will need to be slightly modified.

If the path can be huge, the last thing you surely want is to loop.

Upvotes: 3

David C. Rankin
David C. Rankin

Reputation: 84561

Regardless whether you use dirname or use parameter expansion with substring removal (e.g. parent="${1%/*}") to incrementally lop off the final pathspec, you need to test for several conditions to handle both absolute and relative pathnames. Specifically you need to determine if:

  • the path name is absolute, e.g. "/path/to/file"
  • the path name is relative, e.g. "path/to/file"
  • the path name references the parent dir using ".."
  • the path name references the current dir using "."

To do that you can use compound statements e.g. [ ... ] && [ ... ] ... (or the older, less favored [ ... -a .... -a .... ]). You also need to consider whether to use bashisms (non-POSIX, bash only functions and test clauses, like [[ ... ]]), or to make your script portable limiting your syntax to POSIX compatible elements. (parameter expansion is POSIX compliant)

Putting all those pieces together, you can do something similar to the following:

#!/bin/bash

parent="$1"
tmp="$(dirname "$parent")"

## if only filename given, parent is "./"
if [ "$tmp" = "." ]; then
    parent="./"
else
    ## find parent for absolute and relative paths
    while [ "$parent" != "$tmp" ] && [ "$tmp" != "/" ] && 
          [ "$tmp" != "." ] && [ "$tmp" != ".." ]; do
        parent="$tmp"
        tmp="$(dirname "$parent")"
    done
fi

printf "%s\n%s\n" "$1" "$parent"

Example Use/Output

$ bash parent.sh ../a/b/c.d
../a/b/c.d
../a

$ bash parent.sh ./a/b/c.d
./a/b/c.d
./a

$ bash parent.sh /a/b/c.d
/a/b/c.d
/a

$ bash parent.sh a/b/c.d
a/b/c.d
a

$ bash parent.sh c.d
c.d
./

note: you can use tmp="${parent%/*}" in place of tmp="$(dirname "$parent")", it is up to you. (you just need to adjust the first test for filename only to [ "$parent" = "$tmp" ] and replace "$tmp" != "/" with "$tmp" != "")

You can be as creative as you like isolating the parent. There isn't one right way and all others are wrong. Just try and use good script tests and validations to cover as many corner cases as you are likely to encounter.

If you wanted to use parameter expansion and use the old conditional -a operator, you could do:

#!/bin/bash

parent="$1"
tmp="${parent%/*}"

## if only filename given, parent is "./"
if [ "$parent" = "$tmp" ]; then
    parent="./"
else
    ## find parent for absolute and relative paths
    while [ "$parent" != "$tmp" -a "$tmp" != "" -a \
            "$tmp" != "." -a "$tmp" != ".." ]; do
        parent="$tmp"
        tmp="${parent%/*}"
    done
fi

printf "%s\n%s\n" "$1" "$parent"

Look things over and let me know if you have further questions.

Upvotes: 0

grail
grail

Reputation: 930

Just using bash I would have started where HTNW finished :)

parent=${1#/}        # strip leading slash, if there is one
echo "${parent%%/*}" # strip everything from slash closest to the start til end

Upvotes: 0

HTNW
HTNW

Reputation: 29193

You almost had it:

#!/bin/bash
parent=$(dirname "$1")
# Use until instead of while (just a style thing to avoid having negated logic)
# Use == for string comparison
# Use [[ ]] because it's a shell builtin and generally is nicer to use
# Use $(dirname "$parent") instead of $parent. Without it, the loop runs until
# parent=/, and that destroys any useful information
# Now, it runs until the parent of $parent is /, which is what you wanted
until [[ $(dirname "$parent") == "/" ]]; do
    parent=$(dirname "$parent")
done
# Strip off the leading /
# ${var#pat} means "strip off the shortest prefix of $var that matches pat"
parent=${parent#/}
echo "$parent"

Do note that this will fall into an infinite loop on the input .. You should check for an absolute path in the beginning.

Upvotes: 1

Related Questions