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"
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
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
Method 2
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.
Method 3
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 "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d4f094">[email protected]</a>"
# 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.
Method 4
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
realpathsystem 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).
Method 5
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%/*}
Method 6
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
Method 7
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"
Method 8
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 "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f9ddb9">[email protected]</a>"
cd ..
done
Method 9
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"
}
Method 10
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.
Method 11
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" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a185e1">[email protected]</a>" -print || true
# Equivalent, but -mindepth and -maxdepth aren't POSIX.
# find "$directory" -mindepth 1 -maxdepth 1 "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="eaceaa">[email protected]</a>"
if [ "$directory" = '/' ]; then
break # End do while loop
else
directory=$(dirname "$directory")
fi
done
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0