76

I'm nested deep in a file tree, and I'd like to find which parent directory contains a file.

E.g. I'm in a set of nested GIT repositories and want to find the .git directory controlling the files I'm currently at. I'd hope for something like

find -searchup -iname ".git"
1
  • 2
    I wish this was build in!
    – rds
    Jan 1, 2021 at 11:00

12 Answers 12

47
git rev-parse --show-toplevel

will print out the top level directory of the current repository, if you are in one.

Other related options:

# `pwd` is inside a git-controlled repository
git rev-parse --is-inside-work-tree
# `pwd` is inside the .git directory
git rev-parse --is-inside-git-dir

# path to the .git directory (may be relative or absolute)
git rev-parse --git-dir

# inverses of each other:
# `pwd` relative to root of repository
git rev-parse --show-prefix
# root of repository relative to `pwd`
git rev-parse --show-cdup
3
  • 11
    This works only for git itself. The question is more generic.
    – alex
    Jan 26, 2011 at 7:15
  • 1
    This will not work in a bare repo, Sep 1, 2014 at 4:01
  • 1
    thanks great way to find file at root of git repo.e.g. user_file_path=$(realpath $(git rev-parse --show-toplevel)/$user_file)
    – Pieter
    Dec 16, 2020 at 22:08
38

A generalized version of Gilles' answer, first parameter used to find match:

find-up () {
  path=$(pwd)
  while [[ "$path" != "" && ! -e "$path/$1" ]]; do
    path=${path%/*}
  done
  echo "$path"
}

Keeps the use of sym-links.

24

An even more general version that allows using find options:

#!/bin/bash
set -e
path="$1"
shift 1
while [[ $path != / ]];
do
    find "$path" -maxdepth 1 -mindepth 1 "$@"
    # Note: if you want to ignore symlinks, use "$(realpath -s "$path"/..)"
    path="$(readlink -f "$path"/..)"
done

For example (assuming the script is saved as find_up.sh)

find_up.sh some_dir -iname "foo*bar" -execdir pwd \;

...will print the names of all of some_dir's ancestors (including itself) up to / in which a file with the pattern is found.

When using readlink -f the above script will follow symlinks on the way up, as noted in the comments. You can use realpath -s instead, if you want to follow paths up by name ("/foo/bar" will go up to "foo" even if "bar" is a symlink) - however that requires installing realpath which isn't installed by default on most platforms.

5
  • the trouble with these is the way it resolves links. for example if i'm in ~/foo/bar , and bar is a symlink to /some/other/place, then ~/foo and ~/ will never be searched. Jun 5, 2015 at 13:07
  • @ErikAronesty, you're right - updated the answer. You can use realpath -s to get the behaviour you want.
    – sinelaw
    Jun 6, 2015 at 20:53
  • that works when you specify a full path to search. sadly, on centos 5.8, realpath -s <dir> has a bug where if <dir> is relative, then it resolves symlinks anyway... despite it's documentation. Jun 8, 2015 at 20:17
  • readlink is not part of the standard. A portable script could be implemented with only POSIX shell features.
    – schily
    Oct 24, 2018 at 14:22
  • 1
    FYI, this doesn't work in zsh because it redefines $path so the shell can't find find or readlink. I suggest changing it to something like $curpath instead so it doesn't shadow the shell variable.
    – Skyler
    Apr 12, 2019 at 16:03
22

If you're using zsh with extended globbing enabled, you can do it with a oneliner:

(../)#.git(:h)   # relative path to containing directory, eg. '../../..', '.'
(../)#.git(:a)   # absolute path to actual file, eg. '/home/you/src/prj1/.git'
(../)#.git(:a:h) # absolute path to containing directory, eg. '/home/you/src/prj1'

Explanation (quoted from man zshexpn):

Recursive Globbing

A pathname component of the form (foo/)# matches a path consisting of zero or more directories matching the pattern foo. As a shorthand, **/ is equivalent to (*/)#.

Modifiers

After the optional word designator, you can add a sequence of one or more of the following modifiers, each preceded by a ':'. These modifiers also work on the result of filename generation and parameter expansion, except where noted.

  • a
    • Turn a file name into an absolute path: prepends the current directory, if necessary, and resolves any use of '..' and '.'
  • A
    • As 'a', but also resolve use of symbolic links where possible. Note that resolution of '..' occurs before resolution of symbolic links. This call is equivalent to a unless your system has the realpath system call (modern systems do).
  • h
    • Remove a trailing pathname component, leaving the head. This works like 'dirname'.

Credits: Faux on #zsh for the initial suggestion of using (../)#.git(:h).

4
  • This is amazing... If I could only figure out (hint: you should tell me) what (../) is doing... Oh, and who/what is Faux on #zsh?
    – alex gray
    Mar 20, 2015 at 15:05
  • 1
    @alexgray (../) alone doesn't mean much, but (../)# means try to expand the pattern inside the parenthesis 0 or more times, and only use the ones that actually exist on the filesystem. Because we're expanding from 0 to n parent directories, we effectively search upwards to the root of the file system (note: you probably want to add something after the pattern to make it meaningful, but for enlightenment execute print -l (../)#). And #zsh is an IRC channel, Faux is a username on it. Faux suggested the solution, therefore the credit.
    – unthought
    Mar 22, 2015 at 11:03
  • 2
    That will expand all of them. You may want: (../)#.git(/Y1:a:h) to stop at the first found one (with a recent version of zsh) Jun 8, 2015 at 20:43
  • 4
    Very helpful, thanks. To enable extended globbing do setopt extended_glob
    – Shlomi
    Sep 24, 2015 at 8:40
15

Find can't do it. I can't think of anything simpler than a shell loop. (Untested, assumes there is no /.git)

git_root=$(pwd -P 2>/dev/null || command pwd)
while [ ! -e "$git_root/.git" ]; do
  git_root=${git_root%/*}
  if [ "$git_root" = "" ]; then break; fi
done

For the specific case of a git repository, you can let git do the work for you.

git_root=$(GIT_EDITOR=echo git config -e)
git_root=${git_root%/*}
1
  • 1
    Cool, this puts me on a path for a general solution - which I post as another answer here. Oct 7, 2011 at 0:56
4

Recursion can result in a quite concise solution.

#!/usr/bin/env bash

parent-find() {
  local file="$1"
  local dir="$2"

  test -e "$dir/$file" && echo "$dir" && return 0
  [ '/' = "$dir" ] && return 1

  parent-find "$file" "$(dirname "$dir")"
}

# Example
parent-find .bashrc "/home/user/projects/parent-find" 
# should respond with "/home/user"
7
  • This is actually really good! Thanks
    – Tek
    Jul 13, 2021 at 5:59
  • 1
    Similar trick: local file="$1?Usage $0 <target> [start-directory]" will bomb out without at least one arg with a hint to the user. Jan 13, 2022 at 14:20
  • 1
    @DanGarthwaite, I did not know that was possible. Thanks for sharing that!
    – bas080
    Jan 15, 2022 at 12:32
  • 1
    I noticed $0 doesn't work right in a function. So instead use the function name. Jan 16, 2022 at 17:02
  • 1
    This doesn't work when the directory to be searched is . Aug 24, 2023 at 12:02
3

Answer

Since there's a zsh one-liner here's a more general unix oneish-liner to search the 3 parent directories:

$ name=fileOrDirName
$ eval find ./$(printf "{$(echo %{1..4}q,)}" | sed 's/ /\.\.\//g')/ -maxdepth 1 -name $name

Explanation

I'll admit it's not the prettiest command, but it's one line. To search 4 directories up replace echo %{1..4}q, with echo %{1..5}q,. Specify the file or directory's name with the name variable.

In short $(printf ... | sed ...) is substituted for its output using command substitution which is then expanded with find using brace expansion. Broken into two commands it looks like this (with a copy and paste in the middle):

$ printf "{$(echo %{1..4}q,)}" | sed 's/ /\.\.\//g'
{'',../'',../../'',../../../'',}
$ #copy and paste the output

$ find ./{'',../'',../../'',../../../'',} -maxdepth 1 -name $name

You can learn more about shell expansions with man bash or in the resources below on the gnu website

1

This version of findup supports "find" syntax, like @sinelaw's answer, but also support symlinks without needing realpath. It also supports an optional "stop at" feature, so this works: findup .:~ -name foo ... searches for foo without passing the home dir.

#!/bin/bash

set -e

# get optional root dir
IFS=":" && arg=($1)
shift 1
path=${arg[0]}
root=${arg[1]}
[[ $root ]] || root=/
# resolve home dir
eval root=$root

# use "cd" to prevent symlinks from resolving
cd $path
while [[ "$cur" != "$root" && "$cur" != "/" ]];
do
    cur="$(pwd)"
    find  "$cur/"  -maxdepth 1 -mindepth 1 "$@"
    cd ..
done
1

Vincent Scheib's solution doesn't work for files that reside in the root directory.

The following version does, and also lets you pass the starting dir as the 1st argument.

find-up() {
    path="$(realpath -s "$1")"

    while ! [ -e "$path"/"$2" ] && [ -n "$path" ]; do
        path="${path%/*}"
    done

    [ -e "$path"/"$2" ] && echo "$path"/"$2"
}
1

Here's my one-liner tested only on bash. It stops before it looks in the root though, so you'd have to look there manually. But hey, it's rare to put files in the root, right?

f=file_to_find
p=$(pwd)
until [ $p == "/" ]; do
    if [ -f "$p/$f" ]; then
        echo $f is in $p;
        break;
    fi;
    p=$(dirname $p);
done;
1
  • Note that you need dirname "$p" with quotes for the same reasons you have quotes in [ -f "$p/$f" ]. You could also just do while true; do ... if [ "$p" = / ]; then break; fi; done to test the condition in the end. (Also that's rather too complex for one line, esp. since you have to scroll horizontally to view it.)
    – ilkkachu
    Oct 28, 2022 at 16:57
0

I've found that working with symlinks kill some of the other options. Especially the git specific answers. I've halfway created my own favorite out of this original answer that's pretty effecient.

#!/usr/bin/env bash

# usage: upsearch .git

function upsearch () {
    origdir=${2-`pwd`}
    test / == "$PWD" && cd "$origdir" && return || \
    test -e "$1" && echo "$PWD" && cd "$origdir" && return || \
    cd .. && upsearch "$1" "$origdir"
}

I'm using symlinks for my go projects because go wants source code in a certain location and I like to keep my projects under ~/projects. I create the project in $GOPATH/src and symlink them to ~/projects. So running git rev-parse --show-toplevel prints out the $GOPATH directory, not the ~/projects directory. This solution solves that problem.

I realize this is a very specific situation, but I think the solution is valuable.

0

I modified sinelaw's solution to be POSIX. It doesn't follow symlinks and includes searching the root directory by using a do while loop.

find-up:
#!/usr/bin/env sh

set -e # exit on error
# Use cd and pwd to not follow symlinks.
# Unlike find, default to current directory if no arguments given.
directory="$(cd "${1:-.}"; pwd)"

shift 1 # shift off the first argument, so only options remain

while :; do
  # POSIX, but gets "Permission denied" on private directories.
  # Use || true to allow errors without exiting on error.
  find "$directory" -path "${directory%/}/*/*" -prune -o ! -path "$directory" "$@" -print || true

  # Equivalent, but -mindepth and -maxdepth aren't POSIX.
  # find "$directory" -mindepth 1 -maxdepth 1 "$@"

  if [ "$directory" = '/' ]; then
    break # End do while loop
  else
    directory=$(dirname "$directory")
  fi
done

You must log in to answer this question.

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