21

I found I asked this question on the wrong stackexchange site.

To find files starting from a certain path, I can use find <path> .... If I want to find 'upwards', i.e. in the parent directory, and it's parent, and..., is there an equivalent tool?

The use case is knowing the right number of dots (../../x.txt or ../../../x.txt?) to use in e.g. a makefile including some common makefile functions somewhere upstream.

Intended usage for a folder structure like this:

/
/abc
/abc/dce/efg/ghi
/abc/dce/efg2


$ cd /abc/dce/efg/ghi
$ touch ../../x.txt
$ upfind . -name X*
../../x.txt
$ upfind . -name Y* || echo "not found"
not found
$ touch /abc/dce/efg2/x.txt
$ upfind . -name Y* || echo "not found"
not found
$ 

So in short:

  • it should search on this folder, it's parent, it's parent's parent...
  • but not in any of their siblings (like 'find' would)
  • it should report the found file(s) relative to the current path
4
  • It appears from the link that you already wrote a script that solved your problem...
    – Matt
    Jul 31, 2012 at 12:38
  • @Matt: yes, but I'm allways try to find one better answer, and this is a better forum to do so.
    – xtofl
    Jul 31, 2012 at 12:50
  • Ah. Actually, I would think the best forum would be SO, wouldn't it?
    – Matt
    Jul 31, 2012 at 12:52
  • Possible same on unix SE: unix.stackexchange.com/questions/6463/… Mar 30, 2015 at 10:26

5 Answers 5

2

Existing answers unsufficient (details below).

#!/usr/bin/env bash

DIR=$(readlink -f "$1")
# Alternative: use current working dir; but then also replace ${@:2} with $@ on line 8
# and $1 with $PWD when calling realpath
# DIR=$PWD

while
  RESULT=$(find "$DIR"/ -maxdepth 1 "${@:2}")
  # echo "Debugging upfind - search in $DIR gives: $RESULT"
  [[ -z $RESULT ]] && [[ "$DIR" != "/" ]]
do DIR=$(dirname "$DIR"); done

realpath --relative-to="$1" "$RESULT"
# Alternative: output absolute path
# echo "$RESULT"

The two alternatives you might consider can be toggled in the script; using $PWD instead of the first parameter means we need to pass $@ (all parameters) to find, and outputting the full path instead of a relative path by commenting the realpath line and just echoing $RESULT.

Note use of a do-while loop is from this SO answer.

Choroba and Matthew Wolff got the end condition wrong; so they would fail to find a file that is actually in the root of the filesystem. They also both do cd's, which is not something I want bash scripts to do. Peter O's solution looks better, but simply outputs nothing for my constructed testcase, and it's easier to write a new bash script than to debug an existing one.

1
  • Thanks! Indeed, avoiding cd allows one to reuse this snippet inside another bash script (or a function). We have to start providing test suites for our snippets, as a community, I mean. That would have revealed the /needle edge case early.
    – xtofl
    Nov 12, 2022 at 9:32
23

You can use this simple script. It walks the directory tree upwards and searches for the specified files.

#! /bin/bash
prev=.
while [[ $PWD != "$prev" ]] ; do
    find "$PWD" -maxdepth 1 "$@"
    prev=$PWD
    cd ..
done

Usage:

upfind -name 'x*'
5
  • Without having tested it, doesn't this print an incrementally growing result list with duplicates because the loop terminates when root is reached? Jun 20, 2021 at 14:33
  • Why the loop termination at the root should result in duplicates?
    – choroba
    Jun 20, 2021 at 18:14
  • 1
    Hm okay: I was wrong with the duplicates because I didn't consider the -maxdepth flag when I said that. I was also under the impression that we want to stop searching after the first match, and this script doesn't do that – it runs find on each iteration until we reach the root. I treated this problem more like .closest() in jQuery. But no, upfind should behave like you suggested. Never mind me. Jun 20, 2021 at 19:35
  • Grave digging... but @tamaMcGlinn rightfully pointed out that this script doesn't find needles that are in the root directory.
    – xtofl
    Nov 12, 2022 at 9:43
  • @xtofl: Updated.
    – choroba
    Nov 12, 2022 at 10:22
3

You can just split the path into its constituent directory nodes and search each one discreetly. It is a bash script.

IFS=/; dn=($1); ct=${#dn[@]}
for((i=0; i<ct; i++)); do
  subd+=/"${dn[i]}"
  dots=$(for((j=ct-i; j>1; j--)); do printf "../"; done)
  find "$subd" -maxdepth 1 -type f -name "$2" -printf "$dots%f\n"
done

run upfind $HOME/zt" "Y*" ... which produces the following output
when YABBA exists in /, /home/user, /home/user/zt

../../../YABBA
../YABBA
YABBA
1

Expanding on @choroba's answer with my own solution for finding the upward location of a file (by name):

upfind() {
  ORIG_DIR="$PWD"
  while [[ "$PWD" != / ]] ; do
    if find "$PWD"/ -maxdepth 1 -type f -name "$@" | grep -q "$@"; then
      echo "$PWD" && builtin cd "$ORIG_DIR"
      return 0
    else
      builtin cd ..
    fi
  done
  builtin cd "$ORIG_DIR"
  return 1
}

Example:

> upfind packageInfo
/home/matthew/development/packageRoot

It will return with an error code, which is useful if you're using it in a conditional. However, this solution is less of an upward version of find (I assume it won't play nice when you pass in additional parameters) as it is a specific solution to finding a file by name.

1

In case someone is looking for something similar but more concise for interactive shell

Bash

You can define a short recursive function:

upfind () {
  ls -d $1 || upfind ../$1
}

Zsh

A one-liner is possible. Unfortunately, you'll need to manually press Enter a few times.

ls -d ./.git || pushln '^./^./../^'

pushln makes ^./^./../^ be waiting for you in your next prompt which in turn expands into the same command but with added ../

2
  • 1
    Thanks! That's nifty! Redirecting to /dev/null makes it a bit nicer. It needs an end condition, too: the bash version recurses infinitely when needle is not found.
    – xtofl
    Jan 10 at 12:35
  • 1
    Replacing the else-branch with ([ "$(realpath $(dirname $1))" == "/" ] || upfind ../$1;) does the trick.
    – xtofl
    Jan 10 at 12:39

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.