22

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
    Commented 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
    Commented Jul 31, 2012 at 12:50
  • Ah. Actually, I would think the best forum would be SO, wouldn't it?
    – Matt
    Commented Jul 31, 2012 at 12:52
  • Possible same on unix SE: unix.stackexchange.com/questions/6463/… Commented Mar 30, 2015 at 10:26

6 Answers 6

3

Existing answers unsufficient (details below).

Put this into a script named upfind somewhere in your $PATH, and chmod +x /the/path/upfind:

#!/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
    Commented Nov 12, 2022 at 9:32
24

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*'
9
  • Without having tested it, doesn't this print an incrementally growing result list with duplicates because the loop terminates when root is reached? Commented Jun 20, 2021 at 14:33
  • Why the loop termination at the root should result in duplicates?
    – choroba
    Commented 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. Commented 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
    Commented Nov 12, 2022 at 9:43
  • @xtofl: Updated.
    – choroba
    Commented 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
2

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
  • 2
    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
    Commented Jan 10, 2023 at 12:35
  • 2
    Replacing the else-branch with ([ "$(realpath $(dirname $1))" == "/" ] || upfind ../$1;) does the trick.
    – xtofl
    Commented Jan 10, 2023 at 12:39
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.

0

Bash function.

  • doesn't change directory
  • doesn't use subshells
  • doesn't use ls
  • doesn't use find
  • sets the exit code if the file is not found.
upfind()
{
# work with full path for search
local search ; [[ ${1:0:1} == "/" ]] && search="$1" || search="$PWD/$1" ;

# does the searched file exist
stat "$search" &>/dev/null && echo "$search" ||
    {
    # file not found, try parent's directory
 
    # split the search in its components 
    local path="${search%/*}" file="${search##*/}" parent_path="${path%/*}"

    # if there's a parent, try to find the searched file there
    [[ "$parent_path" ]] && upfind "$parent_path/$file"
    }
}

1
  • 2
    Code without any explanation is useless. Can you elaborate on this a little more?
    – Toto
    Commented Jul 8, 2023 at 12:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .