0

I'm trying to write a Bash shell script that copies all the files in the current directory, minus a few exceptions, into another directory. The script builds a command, stores it in a variable, and runs it. Here's the baffling part: when the command it builds is run in a shell script, it fails with the message find: paths must precede expression: `|xargs' If I remove the space before the |, I instead see find: unknown predicate `-i' . But when I echo the command it builds and run it, it works fine! To make it even weirder, echo "$cmd"|bash fails with the same error message!

The script (synchronize.sh):

#! /bin/bash

EXCLUDED_FILES=(folder1 file.txt synchronize.sh)
FLAGS='-r'
PROJECT_NAME=project

TARGET_DIR=$HOME/Documents/IDE/$PROJECT_NAME/assets
cmd='find ./* -maxdepth 0'
cmd_end="|xargs -i cp -r {} -t $TARGET_DIR"

for file in ${EXCLUDED_FILES[@]}
do
    cmd+=" ! -name \"$file\""
done

cmd+=$cmd_end

$cmd               #Running the command directly fails
#echo  $cmd        #Yet copying and pasting the output of this command will work just fine
#echo "$cmd"|bash  #Though this inexplicably fails, with the same message as just $cmd

None of the files or folders have spaces in their name, or any special characters except underscores and periods.

3
  • 2
    (1) Your code does not depend on any input not known in advance, right? It could be a plain find … | xargs … line without this cmd contraption. But if you want it then: How can we run a command stored in a variable? (2) "None of the files or folders have spaces in their name, or any special characters except underscores and periods." – Don't let it be your excuse. It's a virtue to do things in the right way anyway. Why does my shell script choke on whitespace or other special characters? Commented May 3, 2022 at 5:36
  • Thank you SO much for that first linked answer! I hardly ever write any shell scripts, and all the idiosyncrasies and minor distinctions are really confusing to someone who doesn't write much Bash and only uses the terminal for simple tasks like running Make, moving files, wget'ing a bunch of images, and opening a file browser to a deep subfolder.
    – VHS
    Commented May 3, 2022 at 7:10
  • This question is deceivingly tricky. I ended up spending some time poking through this after I posted a pretty lousy response. I assume you came to the same conclusion that I did -- concatenating variables in the find command is...not ideal. I ended up rewriting your script using an array comparison just because I was curious how the workflow looked. Since it's off-topic to the subject of this question I'm just adding a gist. gist.github.com/iamwpj/12cb157d82578a2383b28ec1fa259c3e
    – iamwpj
    Commented May 6, 2022 at 8:00

2 Answers 2

-1

So, in the answer linked by @KamilMaciorowski , I learned that Bash treats pipes differently in strings that run commands. I solved the problem by changing the line $cmd to eval "$cmd" and it works great now!

Be warned, eval can lead to code injection issues, but that's not relevant for this particular script.

2
  • 2
    No, that's the wrong solution. The correct one is to use cmd=( find ./* -maxdepth 0 ) then to extend that array with e.g. cmd+=( ! -name "$file" ) etc. and finally to run using "${cmd[@]}". Note that it makes no sense to store the xargs command in a variable. Just use cmd+=( -exec cp -t "$TARGET_DIR" -r {} + ) to avoid piping to xargs at all.
    – Kusalananda
    Commented May 3, 2022 at 7:03
  • 1
    Note that if you build cmd with cmd+=" ! -name \"$file\"" as in the question, you'll end up with a string like ! -name "filename1" ! -name "filename2 ..., which will break apart if the filenames contain double quotes, dollar signs or backslashes. If you know they don't, it should work, though.
    – ilkkachu
    Commented May 3, 2022 at 7:37
-2

You have a good idea about loading a list to a command, but there are better ways to combine variables and commands.

@KamilMaciorowski provided some good context here, but you're probably still hunting around for a more definitive answer. You can see a good example for using printf to format your array into the find command by reading more here.

What this will look like in your case would be something like this:

find ./* -maxdepth 0 $(printf "! -name %s " $EXCLUDED_FILES) -exec cp -r {} $TARGET_DIR \;
1
  • if EXCLUDED_FILES is an array, like in the code in the question, $EXCLUDED_FILES will only take the first element of the array, and we could just have ! -name "$EXCLUDED_FILES" without the printf and command substitution. If you make $EXCLUDED_FILES a whitespace-separated list of filenames, it would basically work (as long as the filenames don't contain glob characters either. Just that it would be impossible to have filenames that contain whitespace themselves in the list.
    – ilkkachu
    Commented May 3, 2022 at 7:35

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.