5

Here is a simplified code that prints the name of Directory if it contains a Filename with same name as the parent directory and .md extension.

FIND(){
    find . -type d -exec sh -c '
        for d do
            [ -f "${d}/${d##*/}.md" ] && printf "%s\n" "$d"
        done' find-sh {} +
}


FIND

To generalize I want to send the Search term ${d}/${d##*/}.md as an argument to the FIND function, but unfortunately this does not outputs anything:

FIND(){
    local SearchTerm="${1}"
    find . -type d -exec sh -c '
        for d do
            [ -f "${SearchTerm}" ] && printf "%s\n" "$d"
        done' find-sh {} +
}

FIND '${d}/${d##*/}.md'

I am sure there is some issue with the quotation of the SearchTerm. Any hints?


I tried: FIND '\${d}/\${d##*/}.md' but has no output

5
  • you should export that SearchTerm variable, if you want to pass it to child process. Instead of local SearchTerm=...;, write SearchTerm=$1 find ....
    – user313992
    Commented Jan 22, 2020 at 0:17
  • notice that you do not need to quote the $1 in things like func(){ VAR=$1 find ... ; }
    – user313992
    Commented Jan 22, 2020 at 0:20
  • And even if you pass it to the subshell, passing ${d}/$... still won't work, because the shell does not expand variables recursively.
    – user313992
    Commented Jan 22, 2020 at 0:28
  • @mosvy Yes exporting doesn't help. How can we make this work? I want to generalize this function by passing different SearchTerm while invocation of the function. Also, may I ask you why we don't need to quote $1 as you wrote in the previous comment.
    – Porcupine
    Commented Jan 22, 2020 at 8:01
  • You don't need to quote it because split+glob doesn't happen on the right side of a variable assignment; example: f(){ v=$1 printenv v; }; f '* *'. I think that @Philippes' solution, which could be simplified to just FIND(){ find ... sh -c '... [ -f "'"$1"'" ] && ...' ...; } is fine for what you want to achieve, and trying to "improve" it (with eg. backquoting "s in the interpolated variable) will only break it.
    – user313992
    Commented Jan 22, 2020 at 14:17

3 Answers 3

5

"${SearchTerm}" needs to be outside of the sh -c ' to get extrapolated, like follows:

FIND(){
    local SearchTerm="${1}"
    find . -type d -exec sh -c '
        for d do
            [ -f "'"${SearchTerm}"'" ] && printf "%s\n" "$d"
        done' find-sh {} +
}

FIND '${d}/${d##*/}.md'

To explain a little more, we can run a small test :

SearchTerm='${d}/${d##*/}.md'
echo 'Before "$SearchTerm" After'
# output : Before "$SearchTerm" After
echo 'Before "'"$SearchTerm"'" After'
# output : Before "${d}/${d##*/}.md" After

'Before "$SearchTerm" After' is a single string without any parameter expansion because it's all in single quote.

'Before "'"$SearchTerm"'" After' is actually three strings joining together :

'Before "' # at the end, it's a double quote followed by a single quote

"$SearchTerm" # parameter expansion happens here.

'" After' # at the begining, it's a single quote followed by a double quote

Hope it's clearer.

9
  • @Philippe Could you please explain a bit more about how quoting "'"${SearchTerm}"'" helps in keeping it outside sh -c?
    – Porcupine
    Commented Jan 22, 2020 at 8:08
  • 1
    This introduces a code injection vulnerability as you are modifying the actual script that you're later running.
    – Kusalananda
    Commented Jan 22, 2020 at 9:06
  • 1
    I do agree. The solution I gave is what I think he wanted to do, and I assume he has total control of $SearchTerm.
    – Philippe
    Commented Jan 22, 2020 at 9:30
  • @Kusalananda, that's clearly what the OP wants, they want to run arbitrary code (at least arbitrary expansions) for each of the selected directories. Commented Jan 22, 2020 at 10:17
  • @GordonDavisson, single quotes are not going to be much of a problem there. The user only needs to bear in mind that the argument to FIND has to be shell code that is valid within double quotes. FIND "\$d/it's cool" would happily return the searchable directories that contain a regular file called it's cool. But you'd need FIND '$d/it\"s cool' to find directories that contain a it"s cool file. Commented Jan 22, 2020 at 10:22
2

The in-line script that you call is single-quoted (as it should be). This means that the sh -c shell will get a script where "${SearchTerm}" is unexpanded. Since that shell does not have a SearchTerm variable, its value will be empty.

Since you tagged your question with , you can pass the name of an exported function:

# Our find function.
# Takes the name of a test function that will be called
# with the pathname of a directory.
myfind () {
    local thetest="$1"

    # Run find, passing the name of the function into the in-line script.
    find . -type d -exec bash -c '
        testfunc=${1:-:}; shift
        for dirpath do
            "$testfunc" "$dirpath" && printf "%s\n" "$dirpath"
        done' bash "$thetest" {} +
}

# Our test function.
test_md_file () {
    [ -f "$1/${1##*/}.md" ]
}
export -f test_md_file

# Run the thing.
myfind test_md_file

The testfunc=${1:-:} in the code will assign $1 to testfunc if it's available and not empty, otherwise, it will use : as the test (a no-op utility that returns true).

2
  • Could you please explain how this solves the code injection vulnerability?
    – Porcupine
    Commented Jan 22, 2020 at 11:52
  • 1
    @Nikhil Instead of allowing the caller to modify the code by passing strings like . -o "x" ] && sudo reboot || rm files, this restricts the caller to a simple command with a single parameter.
    – Kusalananda
    Commented Jan 22, 2020 at 11:59
2

Some alternative approaches to be able to run arbitrary shell code instead of just [ -f "$d/${d##*/}.md" ]:

Pass the shell code as the first argument and use eval to interpret it:

FIND(){
    find . -type d -exec sh -c '
        code=$1; shift
        for d do
            eval "$code" && printf "%s\n" "$d"
        done' find-sh "$1" {} +
}
FIND '[ -f "$d/${d##*/}.doc" ]'

Same with an environment variable

FIND(){
    CODE=$1 find . -type d -exec sh -c '
        for d do
            eval "$CODE" && printf "%s\n" "$d"
        done' find-sh "$1" {} +
}
FIND 'base=${d##*/}; [ -f "$d/$base.md" ] && [ -f "$d/$base.doc" ]'

Of if the shell code to interpret is always going to be [ -f "something" ]:

FIND(){
    FILE_TEST=$1 find . -type d -exec sh -c '
        for d do
            eval "[ -f \"$FILE_TEST\" ]" && printf "%s\n" "$d"
        done' find-sh "$1" {} +
}
FIND '$d/${d##*/}.doc'

Note that if using zsh, you can just use its e glob qualifier to further qualify the directories (/) with an arbitrary shell expression:

print -rC1 ./**/*(ND/e['[ -f $REPLY/$REPLY:t.md ]'])

Or with a function:

has_md() [ -f $REPLY/$REPLY:t.md ]
print -rC1 ./**/*(ND/+has_md)

(N for nullglob, D for dotglob to not ignore hidden files and dirs)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .