3

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 .... – mosvy Jan 22 '20 at 0:17
  • notice that you do not need to quote the $1 in things like func(){ VAR=$1 find ... ; } – mosvy Jan 22 '20 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. – mosvy Jan 22 '20 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 Jan 22 '20 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. – mosvy Jan 22 '20 at 14:17
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 Jan 22 '20 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 Jan 22 '20 at 11:59
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 Jan 22 '20 at 8:08
  • 1
    This introduces a code injection vulnerability as you are modifying the actual script that you're later running. – Kusalananda Jan 22 '20 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 Jan 22 '20 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. – Stéphane Chazelas Jan 22 '20 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. – Stéphane Chazelas Jan 22 '20 at 10:22
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)

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.