0

I'm looking for a tool that I can use from a bash script that can give me the list of parent directories for a given path.

e.g. given the input foo/bar/moocow I would like to get out:

foo
foo/bar
foo/bar/moocow

It would also be great if I could pipe in multiple paths, and get unique results back, e.g.:

toolimlookingfor << EOF
dir1/file1
dir1/file2
foo/bar/moocow
EOF

Output:

dir1
dir1/file1
dir1/file2
foo
foo/bar
foo/bar/moocow

dirname is close to what I'm looking for, but it only gives the immediate parent. I'm looking for the path it's self and all parents.

  • Is it correct to say that you want to treat those paths as text, i.e. without canonicalizing them (think a/../b/cb/c) nor resolving symlinks? – fra-san Aug 1 at 11:25
  • For my use case, my input paths are from another tool (git ls-files) and so are already canonical, so yes, treating them as text is ok. – Gary van der Merwe Aug 1 at 11:29
2

A Python script using the standard pathlib module, working over arguments instead of input:

#! /usr/bin/env python3

import sys

from collections import OrderedDict
from pathlib import Path

paths = OrderedDict()  # to maintain ordering and skip duplicates

for arg in sys.argv[1:]:
    path = Path(arg)
    for subpath in reversed(path.parents):
        # add the parents
        paths[subpath.as_posix()] = subpath
    # add the path itself
    paths[path.as_posix()] = path

# we don't need '.' in the output
if '.' in paths:
    paths.pop('.')

print('\n'.join(paths.keys()))

(One could easily modify it to use stdin, by looping over sys.stdin instead.)

Example:

% ./foo.py dir1/file1 dir1/file2 foo/bar/moocow
dir1
dir1/file1
dir1/file2
foo
foo/bar
foo/bar/moocow
| improve this answer | |
1

If you are okay with creating new directory in /tmp folder then, you can use find command, this method will also take care of print only unique directory names.

#!/bin/bash
if [[ $# -eq 0 ]]; then exit; fi
mkdir -p /tmp/printnames
for i
do
  mkdir -p /tmp/printnames/"$i"
done
cd /tmp/printnames
find *  -type d -print
rm -r /tmp/printnames

So you can create a temporary directory structure and then traverse though it using find.

| improve this answer | |
  • find * will fail if a path starts with a -, e.g. mkdir -- -foo; find . -type d. A possible workaround, using GNU find, is find . -type d ! -name . -printf '%P\n' – fra-san Aug 1 at 13:54
1

This code will do it. It uses no temporary files. It works with white space, however newlines are ambiguous( change \n to \0 to fix). You should use \0 if it is to be used in automated processes. \0 is the only character that is guaranteed not to be in a Unix file name.

It just recursively uses dirnames. You can tweak it by removing/enabling some of the printfs. To make it ascend or descend, or to not show the root node (. or /). The showing of the root node was not in your example, but I think it is needed. Without it /a/b/c gives the same output as a/b/c.

#!/bin/bash

function dirnames {
    local input
    local parent
    input="$1"
    if [[ "$input" == "" ]]
    then
        true
    elif [[ "$input" == "." ||  "$input" == "/" ]]
    then
        printf '%s\n' "$input" #print the root node
    else
        #printf '%s\n' "$input"  #print node (descending)
        parent="$(dirname "$input")"
        dirnames "$parent"
        printf '%s\n' "$input" #print node (ascending)
    fi
}

for d in "$@"
do
    dirnames "$d"
done

Example usage

%↳ ./dirnames a/b/c
.
a
a/b
a/b/c
%↳ ./dirnames /a/b/c
/
/a
/a/b
/a/b/c
%↳

To get it to work like this

%↳ ./dirnames a/b/c
.
a
b
c
%↳ ./dirnames /a/b/c
/
a
b
c
%↳

then save file as dirnames then run sed -r -i '17 s/("[$]input")/"$(basename \1)"/' dirnames. Now you will have a script that only outputs the leafs at each level.

| improve this answer | |
  • How will it print only unique path names? – Prvt_Yadav Aug 1 at 11:44
  • @Prvt_Yadav please explain your query. – ctrl-alt-delor Aug 1 at 12:46
  • Assume two directories a/b and a/c. It will print directory a twice. – Prvt_Yadav Aug 1 at 12:59
  • @Prvt_Yadav I have added an example of its use. It does what was asked for. You can modify it by putting output through basename before printing. I also added a mod, to do what (I think) you asked for. – ctrl-alt-delor Aug 1 at 18:42
1

Given that you are going to treat paths as text, with the implicit assumptions that they won't contain newline characters and that they don't need to be canonicalized, you may pipe them to this script:

#!/bin/awk -f
BEGIN {
  FS="/"
}
{
  f = ""
  for (i=1; i<=NF; i++) {
    f = f (i>1 ? FS : "") $i
    if (! p[f]++ && f)
      print f
  }
}

Every input line is split on /. Fields are then concatenated, first to last, separated by a /, and each intermediate result (fragment) is saved as a value of the index of an associative array (paths). Each non-empty1 fragment that has not been encountered yet is printed.

Note that this will consider foo, /foo, foo/, /foo/ and even /foo// to be distinct paths and it will print out all of them separately.

1 The first one in absolute paths, which in AWK have an empty field before the first /

| improve this answer | |
0

If you don't mind a sorted output, you could obtain a good result with a pipeline using a sed script plus sort like this:

$ sed -nE 's%/+(/[^/]|$)%\1%g;:loop;\%(^[^/]+|/[^/]*)$%{p;s///};tloop' | LC_ALL=C sort -u

The above Sed script goes to the extent of coalescing adjacent / characters (the very first s command up to the first ; does that) but apart from that it has no knowledge of relative paths etc.

Note that the script is POSIX compliant although on some systems you might need to type it in multiline, for instance like this:

$ sed -nE 's%/+(/[^/]|$)%\1%g
:loop
\%(^[^/]+|/[^/]*)$%{
p;s///
}
tloop
' | LC_ALL=C sort -u

Note also that it cannot handle newlines in filenames. However, on enhanced tools that support -z option you can just add it to both sed and sort to obtain nul-delimited I/O.

I also had fun in making a script capable of full canonicalization, including any arbitrary . and .. occurrences as well as unqualified paths like dir1/file1 prepended by a leading ./. However, it does not resolve symlinks as it does not access the filesystem at all.

I used Awk for its useful builtin machineries to split and rebuild strings.

#!/usr/bin/awk -f

BEGIN { FS="/+"; OFS="/" }
{
    for (nfields=dotdot=0; NF; NF--) {
        if (path[nfields]=$NF) {
            if ($NF=="..") dotdot++
            else if ($NF!=".") {
                if (dotdot) dotdot--
                else nfields++
            }
        }
    }
    if (nfields in path && !path[nfields])
        $1=""
    else {
        while (dotdot--) path[nfields++]=".."
        path[nfields++]="."
    }
    while (nfields--) {
        $(NF+1) = path[nfields]
        if (!seen[$0]++) print $0
    }
}

You may handle newlines in filenames by invoking it with -v RS='\0' -v ORS='\0' on Awk implementations (such as GNU Awk) that support a nul RS.

HTH

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.