1

I'm trying to run the find command on a directory and generate an output file based on the found items.

Here is what I have so far:

docker2:~/src/docker# cat test.sh
#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 \( ! -name "." \) -exec sh -c "\
     echo \"Full path: {}\"; echo \"Basename: `basename {}`\"
" \;

However, this command outputs something like this:

Full path: /data/nc
Basename: /data/nc
Full path: /data/src
Basename: /data/src
Full path: /data/sql0
Basename: /data/sql0
Full path: /data/proxy
Basename: /data/proxy

I know that the backticks should be working:

docker2:~/src/docker# cat test.sh
#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 \( ! -name "." \) -exec sh -c "\
     echo \"Date: `date`\"; echo \"Full path: {}\"; echo \"Basename: `basename {}`\"
" \;

Gives:

Date: Wed Dec 18 22:56:32 UTC 2019
Full path: /data/nc
Basename: /data/nc
Date: Wed Dec 18 22:56:32 UTC 2019
Full path: /data/src
Basename: /data/src
...

And I'm also having trouble with variables:

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 \( ! -name "." \) -exec sh -c "\
     export THIS_DIR={}; THIS_DIRECTORY={}; echo \"$THIS_DIR $THIS_DIRECTORY\";
" \;

The output of this command is empty, with one blank line per directory in /data. This means variables are not being stored even with export.

What am I doing wrong? I'm guessing this has to do with some sort of weird nesting or order-of-operations or some weird shell code thing.

fdmillion
  • 2,828

1 Answers1

1

This is expected as your `basename {}` is in double quotes. This would make it execute before find is even invoked, which would replace the command substitution by the string {}. This is also an issue in your later examples, where the variables gets expanded before they are actually set.

When you use sh -c, the script that it takes as an argument to -c should really always be single quoted as this makes quoting inside the script simpler. Any arguments of the inline script should be passed on its command line, not as text injected into the code (see "Is it possible to use `find -exec sh -c` safely?" for reasons why). In your case:

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 -exec sh -c '
    echo "Full path: $1"
    echo "Basename: $(basename "$1")"' sh {} \;

Or, with a couple of variables (and switching over to using printf):

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 -exec sh -c '
    pathname=$1
    filename=$(basename "$pathname")
    printf "Full path: %s\n" "$pathname"
    printf "Basename: %s\n" "$filename"' sh {} \;

When you run an inline script with sh -c, the arguments passed will be placed in $0, $1, $2 etc. Since $0 is usually the name of the shell or script, we pass the string sh for this, and then the found pathname as $1.

Or, more efficiently, using as few invocations of sh -c as possible, and using parameter substitutions:

#!/bin/sh

find /data -type d -mindepth 1 -maxdepth 1 -exec sh -c '
    for pathname do
        printf "Full path: %s\n" "$pathname"
        printf "Basename: %s\n" "${pathname##*/}"
    done' sh {} +

Here, find arranges for the inline script to be called with as many arguments as possible (as few times as possible). In each invocation, $0 gets set to sh as before, while the other positional parameters takes their values from the found pathnames. The loop iterates over the passed pathnames ($0 is not a positional parameter, so that won't be iterated over).

I'm assuming that your ! -name . test is to avoid finding the /data directory itself. This will not be necessary as it's already skipped by -mindepth 1 (the /data path is at "depth 0" since it's the top of a search path). Also, find won't return the . entry from directories, so the test would not remove /data had you not used -mindepth 1. In that case, you could instead have used ! -path /data.

Since you're just iterating over the directories in a single directory, and since you have used the tag, and since you don't use find to do any fancy tests on e.g. timestamp etc., you could instead do a basic shell loop:

#!/bin/bash

shopt -s dotglob nullglob

for pathname in /data/*; do
    if [[ -d $pathname ]] && [[ ! -L $pathname ]]; then
        printf 'Full path: %s\n' "$pathname"
        printf 'Basename: %s\n' "${pathname##*/}"
    fi
done

Here, the names in /data are tested explicitly for whether they are directories, before information about their pathname and name is printed. The dotglob shell option in bash allows the * globbing pattern to match hidden names, and nullglob prevents the loop form running at all if /data is empty or if the pattern otherwise does not match anything.

See "Understanding the -exec option of `find`" for general information about using find with -exec.

Kusalananda
  • 333,661