7

I have defined a function in bash, which checks if two files exists, compare if they are equal and delete one of them.

function remodup {
    F=$1
    G=${F/.mod/}
    if [ -f "$F" ] && [ -f "$G" ]
    then
        cmp --silent "$F" "$G" && rm "$F" || echo "$G was modified"
    fi
}

Then I want to call this function from a find command:

find $DIR -name "*.mod" -type f -exec remodup {} \;

I have also tried | xargs syntax. Both find and xargs tell that ``remodup` does not exist.

I can move the function into a separate bash script and call the script, but I don't want to copy that function into a path directory (yet), so I would either need to call the function script with an absolute path or allways call the calling script from the same location.

(I probably can use fdupes for this particular task, but I would like to find a way to either

  1. call a function from find command;
  2. call one script from a relative path of another script; or
  3. Use a ${F/.mod/} syntax (or other bash variable manipulation) for files found with a find command.)

4 Answers4

7

You need to export the function first using:

export -f remodup

then use it as:

find $DIR -name "*.mod" -type f -exec bash -c 'remodup "$1"' - {} \;
anubhava
  • 761,203
  • 64
  • 569
  • 643
5

You could manually loop over find's results.

while IFS= read -rd $'\0' file; do
    remodup "$file"
done < <(find "$dir" -name "*.mod" -type f -print0)

-print0 and -d $'\0' use NUL as the delimiter, allowing for newlines in the file names. IFS= ensures spaces as the beginning of file names aren't stripped. -r disables backslash escapes. The sum total of all of these options is to allow as many special characters as possible in file names without mangling.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • I'd write that as `-d ''`; writing `-d $'\0'` implies that the shell can actually represent a NUL inside a string (except as a terminator), which it doesn't and can't. – Charles Duffy Jun 16 '15 at 22:16
  • That said -- I tend to much prefer this option, which is more efficient than launching a `bash` instance per file found, as the other currently proposed answer does. – Charles Duffy Jun 16 '15 at 22:17
  • In testing, I found a for loop much easier to read. For example: ,, `for result in \`find "base" -name "keyword"\`; do echo $result; done` – NightFlight Jun 27 '17 at 15:40
  • 1
    That method is not whitespace safe. It will fail on file names with spaces. There's a reason for everything I did up above, as the answer explains, in detail. – John Kugelman Jun 27 '17 at 16:30
  • 1
    `while … ; do … ; done < <( find … ) ` pattern is recommended. [here](https://stackoverflow.com/a/37210472/766330) is why. – plhn Oct 10 '19 at 14:48
3

Given that you aren't using many features of find, you can use a pure bash solution instead to iterate over the desired files.

shopt -s globstar nullglob
for fname in ./"$DIR"/**/*.mod; do
    [[ -f $fname ]] || continue
    f=${fname##*/}
    remodup "$f"
done
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Nice, but unfortunately it doesn’t work with the macOS-builtin bash (see https://apple.stackexchange.com/questions/291287/globstar-invalid-shell-option-name-on-macos-even-with-bash-4-x). – aaronk6 Sep 26 '22 at 19:32
  • 1
    You should almost certainly not be relying on macOS's built-in `bash` to run `bash` scripts. Install something updated in the last 15 years, or target `zsh` instead. – chepner Sep 26 '22 at 19:35
  • That’s fair :-) – aaronk6 Sep 26 '22 at 19:37
1

To throw in a third option:

find "$dir" -name "*.mod" -type f \
  -exec bash -s -c "$(declare -f remodup)"$'\n'' for arg; do remodup "$arg"; done' _ {} +

This passes the function through the argv, as opposed to through the environment, and (by virtue of using {} + rather than {} ;) uses as few shell instances as possible.


I would use John Kugelman's answer as my first choice, and this as my second.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Why a nested `declare -f` instead of `export -f`? – John Kugelman Jun 16 '15 at 22:19
  • 1
    @JohnKugelman, because exported functions aren't compatible between all versions of bash due to shellshock patching. If the shell you invoke isn't the same version as the shell you're in, there's no guarantee it will actually honor your exported function. – Charles Duffy Jun 16 '15 at 22:20