Need help with nested quotes in bash function

I am using some custom functions to batch transcode videos in a folder. Purpose is to reduce size. I am having trouble implementing some new “filter” to ffmpeg, to reduce video resolution when it is 4k. I looked up an answer from Stack overflow and my function currently looks like this:

lowmp4() { find . -type f -iname '*.mp4' -exec sh -c 'SCALE="\"scale=1920:1080\""; for f; do ffmpeg -i "$f" -filter_complex $SCALE -vcodec libx265 -crf 28 "${f%.mp4}low_q.mp4" && rm "$f" && mv "${f%.mp4}low_q.mp4" "$f"; done' sh {} + ; }

SCALE needs to be "scale='if(gt(iw,1920),1920,-1)':'if(gt(ih,1080),1080,-1)':force_original_aspect_ratio=decrease", but even the above doesn’t work. Can you spot the issue?

I don’t think -filter_complex actually needs a quote, it just needs a single argument. Quote is used to make sure a string doesn’t split into multiple arguments when it contains spaces, and () doesn’t spawn a subshell, etc. Same reason as why "$f" is used.

So in your example, you’re passing a literal quote to the command, e.g.

argv[0] = ffmpeg
argv[1] = -i
argv[2] = filename with space.mp4
argv[3] = -filter_complex
argv[4] = "scale=1920:1080"
# ...

…when what you want is to have argv[4] to be scale=1920:1080 (without quote).

In this case, remove the \" from SCALE (e.g., SCALE="scale=1920:1080") and use quote to prevent argument from breaking in ffmpeg command instead (e.g., -filter_complex "$SCALE"):

lowmp4() { find . -type f -iname '*.mp4' -exec sh -c 'SCALE="scale=1920:1080"; for f; do ffmpeg -i "$f" -filter_complex "$SCALE" -vcodec libx265 -crf 28 "${f%.mp4}low_q.mp4" && rm "$f" && mv "${f%.mp4}low_q.mp4" "$f"; done' sh {} + ; }

Further suggestion: instead of using -exec and piping into sh, using the output of find in a loop is a likely better option (to avoid spawning sh and doing a single-run loop for every file). Since SCALE doesn’t change between runs, it can also be moved out of the loop altogether.

So, possibly, something like this:

lowmp4() {
    SCALE="scale='if(gt(iw,1920),1920,-1)':'if(gt(ih,1080),1080,-1)':force_original_aspect_ratio=decrease"

    find . -type f -iname '*.mp4' | while read -r f; do
        ffmpeg -nostdin -i "$f" -filter_complex "$SCALE" -vcodec libx265 -crf 28 "${f%.mp4}low_q.mp4"
        rm "$f"
        mv "${f%.mp4}low_q.mp4" "$f"
    done
}

-nostdin is needed here so ffmpeg doesn’t swallow stdin and result in while read to run only once. You can make this into a single line by joining them with ;.

1 Like

I have to thank you for taking the time to read my garbage script.

Taking this:
lowmp4() { find . -type f -iname '*.mp4' -exec sh -c 'SCALE="scale=1920:1080"; for f; do ffmpeg -i "$f" -filter_complex "$SCALE" -vcodec libx265 -crf 28 "${f%.mp4}low_q.mp4" && rm "$f" && mv "${f%.mp4}low_q.mp4" "$f"; done' sh {} + ; }

And substituting the below for the value of SCALE:
SCALE="scale='if(gt(iw,1920),1920,-1)':'if(gt(ih,1080),1080,-1)':force_original_aspect_ratio=decrease"

Gives error:
bash: syntax error near unexpected token ``('

But with only a single ` above (couldn’t do that in preformatted text)

I really don’t know much to fix it

This one is likely because you have ' inside ' (part of -exec sh -c '...') so that turns the opening ' into an ending ' for the sh command. The easiest way to fix this is to exit the opening ' and switch to ", put the command there, and switch " back to ' (sorry if this sounds confusing).

So for your command:

lowmp4() { find . -type f -iname '*.mp4' -exec sh -c 'SCALE="'"scale='if(gt(iw,1920),1920,-1)':'if(gt(ih,1080),1080,-1)':force_original_aspect_ratio=decrease"'"; for f; do ffmpeg -i "$f" -filter_complex "$SCALE" -vcodec libx265 -crf 28 "${f%.mp4}low_q.mp4" && rm "$f" && mv "${f%.mp4}low_q.mp4" "$f"; done' sh {} + ; }

This basically turns the string into three parts:

  • SCALE="
  • scale='if(gt(iw,1920),1920,-1)':'if(gt(ih,1080),1080,-1)':force_original_aspect_ratio=decrease
  • "; to the end of the command
2 Likes

I think I got it. It seems that I can pass multiple consecutive “strings” as arguments to sh -c, and those strings can be defined either by double or single quotation marks. This is defined in the beginning of the string, by using either double or single quotes, and the string is terminated at the first occurrence of the same character. I can freely use either of these 2 choices, and mix them up as more strings are used.

1 Like