4

I try to copy 2 or more files from one directory to another with cp using an array.
I executed:

files=(
    LocalSettings.php
    robots.txt
    .htaccess
    ${domain}.png
    googlec69e044fede13fdc.htm
)

filenames indented with tabulations;

I aim to execute afterwards:

cp -a "source_path/${files[@]}" "/destanation_path"

my problem is that while testing the variable itself,
echo $files returned only the first filename LocalSettings.php and not the full list of files.
How would you explain this?
Related: cp in multi line fashion;

  • Note how I, in my answer to the related question, use cd to change to the source directory before copying. If I didn't do that, I would have to specify the full path to the files in the list, e.g. source_path/LocalSottings.php source_path/robots.txt etc. Also $files would always be the same as ${files[0]}. – Kusalananda Aug 18 '19 at 11:34
7

It's Bash feature described in man bash:

Referencing an array variable without a subscript is equivalent to referencing the array with a subscript of 0.

If you want to print all members of files array:

echo "${files[@]}"

Also described in man bash:

${name[@]} expands each element of name to a separate word.

3

$files and ${files[0]} is equivalent when files is a list such as the one you have in your question.

Note that "source_path/${files[@]}" only puts source_path/ before the first element of the list. To modify the list in such a way that each element is prefixed by some path, you can do

files=( ... your list of files ... )

for element in "${files[@]}"; do
    files=( "${files[@]:1}" "source_path/$element" )
done

cp "${files[@]}" destanation_path

or, you could just cd to source_path before doing the cp, or add the path to the actual names at the same time as you assign the values in the list from the start.

3

As others have pointed out, $files only expands to the first element of the array, and "source_path/${files[@]}" only attaches "source_path/" to the first element. But there's a relatively simple way to get all elements, with a path prepended to each:

cp -a "${files[@]/#/source_path/}" "/destanation_path"

This combines the all-elements expansion ([@]) with a substitution. /# means "replace at beginning of string", then the empty string to replace, then / to delimit the replacement, then "source_path/" as the thing to replace (/add). This attaches the source path to each element, and doesn't get confused by funny characters in the elements like some versions do.

Note that in the above example, "source_path/" has a slash at the end but not the beginning; the "/" right before it is a delimiter. If it started with a slash and contained more slashes, like "/source/path/", it'd still work:

cp -a "${files[@]/#//source/path/}" "/destanation_path"
-1

You can also try the following snippet:

IFS=$'\n'
cp -a $( printf "source_path/%s\n" "${files[@]}" ) /destination_path/

It should also work with filenames with spaces.

  • It works with file names containing spaces, but not with file names containing newlines or wildcards. It's also completely pointless, since cp -a "${files[@]}" /destination_path/ works strictly better. – Gilles 'SO- stop being evil' Aug 18 '19 at 15:19
  • 1
    @Gilles, it's not completely pointless, the printf adds the source_path/ prefix which wasn't there in the array entries. – ilkkachu Aug 19 '19 at 8:06

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.