4
\$\begingroup\$

A Posix function for finding the nearest parent file. For example "what .git/ am I working in?" or "is there an .npmrc affecting npm from this path?" This implementation specifically follows the symbolic links toward root rather than the canonical path.

Looking for improvements and potential edge cases.

lsup() {
  path="$1";
  file="$2";
  if [ -z "${file}" ]; then
    file="${path}";
    path="$(pwd)";
  fi;
  path="$(realpath -es "${path}")"
  [ -n "${SH_DEBUG}" ] && echo "Searching for ${file} from ${path}…";
  while [ "${path}" != "/" ]; do
    [ -e "${path}/${file}" ] && break;
    [ -n "${SH_DEBUG}" ] && echo "Not found in ${path}";
    path="$(realpath -s "${path}/..")";
  done;
  [ -e "${path}/${file}" ] && echo "${path}/${file}" || return 1;
}

https://gist.github.com/psaxton/6937fec3357f8295828d2e3577f9bcb6

Used as:

> lsup .git

> lsup /path/to/check .npmrc

\$\endgroup\$
2
  • \$\begingroup\$ The variables should probably be decared local if your shell supports that, or else changed to have more specific names which are less likely to clash with user variables with the same names. E.g. _lsup_path and _lsup_file would be quite a lot uglier, but correspondingly safer if you don't have local. \$\endgroup\$
    – tripleee
    May 29, 2022 at 16:09
  • \$\begingroup\$ realpath is not in POSIX (detailed link: utilities section). unix.stackexchange.com/questions/101080/… suggests readlink -f \$\endgroup\$
    – tripleee
    May 29, 2022 at 16:29

1 Answer 1

3
\$\begingroup\$

First, dump all those empty statements (; followed immediately by newline).

It might be better to swap the order of arguments, so the optional one is last:

file=$1
path=$(realpath -es "${2:-$PWD}") || return 1

(No need for quotes for RHS of assignment; explicit fail instead of depending on set -e).

The conditional printing gives an error return when $SH_DEBUG is unset; turn it around so that the pipeline is always true. Expand as ${SH_DEBUG-} so that this works when set -u -e. Log output should go to standard error stream, not output.

Since we have an absolute path and we're not expanding symlinks, we can use a simple pattern substitution in the iteration, rather than :

path=${path%/*}

We could turn the loop around, so that it terminates when the file is found, instead of repeating the test.


Modified code

#!/bin/sh

set -eu  # will work with any combination of these flags

lsup() {
    file=${1:-}
    [ "$file" ] || return 1
    path=$(realpath -es "${2:-$PWD}") || return 1

    [ -z "${SH_DEBUG-}" ] || echo >&2 "Searching for ${file} from ${path}…"

    until [ -e "${path}/${file}" ]
    do
        [ -z "${SH_DEBUG-}" ] || echo >&2 "Not found in ${path}/"
        [ -n "$path" ] || return 1
        path=${path%/*}
    done
    echo "${path}/${file}"
}

\$\endgroup\$
3
  • \$\begingroup\$ Very nice review. Thank you for taking the time. I appreciate the simplicity that changing the argument order yields, but feel awkward taking a filename before a path. Your comments regarding variable expansion were enlightening. I will keep those points in mind in the future. Changing to an until loop is really the icing on the cake I was hoping for but just couldn't produce on my own. Cheers. \$\endgroup\$
    – psaxton
    Jan 6, 2022 at 22:40
  • \$\begingroup\$ Also as a small nit if realpath is given . it can only come up with the canonical path. $(pwd) or $PWD must be used to use the current path if its the child of a symlink. \$\endgroup\$
    – psaxton
    Jan 6, 2022 at 22:44
  • 1
    \$\begingroup\$ Good point - I've updated to use $PWD as the default. This variable is specified by POSIX, so it's portable. $(pwd) can be slower, and may yield the canonical path if not provided as a shell built-in. (To demonstrate, move into a directory via symlink and compare results of pwd and /bin/pwd). \$\endgroup\$ Jan 7, 2022 at 8:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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