0

Given a file with path

/foo/bar/a.b.c.d.x

I would like to be able to find all files matching /foo/bar/a.b.c.d.* with any final extension

This is what I have been using thus far

findSiblingFiles () {
  local filePath="$1"
  find "$(dirname "$filePath")" -name "$(basename "${filePath%.*}").*"
}

This works most of the time, but it breaks if the file contains square brackets (eg "a.b.c.[helloworld].x"). I can get it to work if I backslash escape the square brackets, but to do that in a function I would have to add the backslashes with regexes, which is getting into messy territory and I wonder if I'm missing a more simple way to do this.

Ideally it should find files with any characters in the file name (brackets, braces, parens, etc), and time efficiency is a consideration.

3
  • filePath='a.b.c.[helloworld].x' && basename "${filePath%.*}" outputs a.b.c.[helloworld] - so if it's wrong then what did you expect? Oct 10, 2021 at 19:05
  • Ah, you mean you want to escape pattern special characters in find? Oct 10, 2021 at 19:06
  • I don't specifically want to use find although that's my current approach. I'm looking for a general (and not overly convoluted) way to find files in the same folder with alternate extensions, that doesn't choke on square brackets (or other characters) Oct 10, 2021 at 19:15

2 Answers 2

3

To see all files that matches the pattern /foo/bar/a.b.c.d.*, use

printf '%s\n' /foo/bar/a.b.c.d.*

To see all files that matches the pattern /foo/bar/a.b.c.[helloworld].*, where [hellowerld] is literal, use

printf '%s\n' /foo/bar/a.b.c.'[helloworld]'.*

I.e., quote the parts of the pattern that need to be literal.

If you get the string a.b.c.[helloworld].x and you want to see all files under /foo/bar that matches the pattern that you get if you remove the last .x and replace it with .*, use

string='a.b.c.[helloworld].x'
printf '%s\n' /foo/bar/"${string%.*}".*

The only thing you need to think about here is to quote the expansion of the variable substitution.

Would you want to do this in the bash shell, recursively, and include hidden names, then use

shopt -s globstar dotglob
string='a.b.c.[helloworld].x'
printf '%s\n' /foo/bar/**/"${string%.*}".*

Your function could probably be written (without searching in subdirectories) as

findSiblingFiles () {
    printf '%s\n' "${1%.*}".*
}

or, to have it output nothing, not even a blank line, if the pattern does not match,

findSiblingFiles () (
    shopt -s nullglob
    set -- "${1%.*}".*
    [ "$#" -gt 0 ] && printf '%s\n' "$@"
)

Note that since we are only modifying the first argument's "tail", we don't need to separate the filename from the directory path, unless we want to verify that there's actually a dot in the given filename, which the following code does.

findSiblingFiles () (
    shopt -s nullglob

    if [[ ${1##*/} != *.* ]]; then
        echo 'Filename has no suffix' >&2
        return 1
    fi

    set -- "${1%.*}".*

    [ "$#" -gt 0 ] && printf '%s\n' "$@"
)

Testing:

$ findSiblingFiles ~myself/local/src/project/doc/document.txt
/home/myself/local/src/project/doc/document.man
/home/myself/local/src/project/doc/document.mdoc
/home/myself/local/src/project/doc/document.txt
5
  • Very clean, thank you. For the record one tweak I will make is to handle the case of inputting a non existent file path (where the literal .* would come out after the input) by catching the result and checking it via [[ "${result: -2}" == $'.*' ]] && return to output nothing, and only print the result after that check. Oct 10, 2021 at 19:56
  • 1
    @user1169420 See updated answer with extra function added.
    – they
    Oct 10, 2021 at 21:23
  • Or with zsh: findSiblingFiles() print -rC1 -- $1:r.*(N). Or findSiblingFiles() { set -o localoptions -o extendedglob; print -rC1 -- $1:r.[^.]##~$1(N); } to exclude the file itself and exclude <rootname>.foo.bar Oct 11, 2021 at 7:45
  • 1
    Note that difference in behaviour with csh / zsh's :r (root name) compared to ${1%.*} when $1 is foo.d/bar for instance. Oct 11, 2021 at 7:46
  • @StéphaneChazelas Thanks for the heads-up about that. This was now fixed by parsing out the filename and checking it for dots first.
    – they
    Oct 11, 2021 at 8:51
1

With zsh:

filesWithSameRootName() {
  set -o localoptions -o extendedglob
  local filepath=${1%%/#}
  print -rC1 -- $filepath:h/**/$filepath:r(|[^.]#)(ND)
}

$filepath:r like in csh or vim (or bash but only for history expansion) returns the root of a path, that is the filepath stripped of its extension. For dir.d/file.foo.bar, that's dir.d/file.foo and for dir.d/file, that's dir.d/file (and dir.d/ for dir.d/, hence the stripping of trailing /s first above).

So to find the files with the same root, we find (recursively with **/) the files with an optional .<zero-or-more-non-dots> appended to them.

All the name-matching predicates in find (-name, -path, -regex...) take a pattern as argument (either shell wildcard or regex), so for a substring match, you'd need to escape the regexp operators in there. Or you could post-process the find -print0 (or POSIXly: find -exec printf '%s\0' {} +) output with something that can do basic string comparison, like using POSIX sh syntax and perl:

filesWithSameRootName() {
  find "$(dirname -- "$1")" -exec printf '%s\0' {} + |
    FILE="$1" perl -l -0ne '
      BEGIN{
        $root = $ENV{FILE};
        $root =~ s{.*/}{};
        $root =~ s{\.[^./]*\Z}{};
      }
      print if m{/\Q$root\E(\.[^.]*)?\Z}'
}

(though without the sorting done by zsh globs, and assuming the dirname doesn't end in newline characters and doesn't start with - and is not ! / (, )...)

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.