1

I've found bits and pieces of each of the things I want to do across the Web, but nothing exactly fits my use case.

I'm trying to write a script that copies multiple specific files to a directory on the Desktop, as well as files with a certain file extension. It also has to run on bash 3.2.

This line is the one that trips me up. I realize it probably has to do something with pattern matching, order, expansion and globbing.

   #!/usr/bin/env bash

  declare -a backup_files=(*.{id,nsf}, "desktop8.ndk", "archive", "user.dic");
  declare -a notes_data_directory=~/Library/Application\ Support/IBM\ Notes\ Data/;
  declare -a notes_backup_directory=~/Desktop/Notes\ Backup;

  mkdir "$notes_backup_directory";
  cd "$notes_data_directory";

  for i in "${backup_files[@]}"
  do
    cp -nipvR "$notes_data_directory$i" "$notes_backup_directory";

    # Also tried this
    # find "$notes_data_directory""$i" -exec cp -nipvR {} "$notes_backup_directory" \;
  done

  cd -

When I echo "$backup_files" this is what I get:

*.{id,nsf}, desktop8.ndk, user.dic

I'm sure it has to do with syntax. I just don't know where.

  • Remove double quotes from first element *.{id,nsf}. By putting quotes around this pattern you do not ask shell for globbing, while this is your intention (if I correctly understood the question). Secondly, remove delimeter , unless you want it as a part of array elements. – jimmij Jul 19 '15 at 19:00
  • I'll add, if you wish to see the contents of the array then you should try echo ${backup_files[@]} – fd0 Jul 19 '15 at 20:47
1

Putting quotes around some text disables expansions such as wildcards expansion and brace expansion. A logical way to remember this is that they produce a list of words, but contexts such as quotes ensure that the content is a single word.

Furthermore you have spurious commas. In a shell script, words are separated by spaces.

declare -a backup_files=(*.{id,nsf} "desktop8.ndk" "user.dic")

You can write this as

backup_files=(*.{id,nsf} "desktop8.ndk" "user.dic")

unless you also wanted to declare backup_files as a local variable in a function.

Note that if one of the patterns doesn't match any file, it will be left unexpanded. For example, in an empty directory, you'll end up with the four elements *.id, *.nsf, desktop8.ndk and user.dic. If you're only going to pass these to rm -f, it doesn't matter. More generally, you can cope with that by checking whether each element is an existing file when processing, e.g.

for x in "${backup_files[@]}"; do
  if [[ ! -e "$x" ]]; then continue; fi
  …
done

(Incidentally, note that accessing the elements of an array is an exception to the rule that a quoted string results in a word rather than a list of words. With array expansion, the double quotes ensure that each elements ends up as a single word, rather than undergoing pattern matching and word splitting. This, including variants such as "$@", is the only exception.)

Alternatively, you can set the nullglob option to make pattern matching result in an empty list when it matches no files.

shopt -s nullglob
backup_files=(*.{id,nsf} "desktop8.ndk" "archive" "user.dic")

Then the array will only contain existing .id and .nsf files, but it will contain desktop8.ndk and user.dic no matter what. You can arrange to exclude these files if they don't exist by making them a pattern as well.

shopt -s nullglob
backup_files=(*.{id,nsf} desktop8.nd[k] archiv[e] user.di[c])

Note that the braces and wildcard are expanded when the array is defined. Thus the patterns match files in the current directory. Your script is doing something strange and probably unintended: you're gathering names of files in the current directory, and copying identically-named files from a different directory $notes_data_directory. If you want to match files in a different directory, you need to say so.

notes_data_directory=~/Library/Application\ Support/IBM\ Notes\ Data/
notes_backup_directory=~/Desktop/Notes\ Backup
backup_files=("$notes_data_directory/"*.{id,nsf} "$notes_data_directory/desktop8.ndk" "$notes_data_directory/archive" "$notes_data_directory/user.dic")
for i in "${backup_files[@]}"
do
  cp -nipvR "$i" "$notes_backup_directory"
done

You can use brace expansion to shorten the definition of backup_files:

backup_files=("$notes_data_directory/"{*.{id,nsf},desktop8.ndk,archive,user.dic})

Alternatively, switch to the directory before copying.

notes_data_directory=~/Library/Application\ Support/IBM\ Notes\ Data/
notes_backup_directory=~/Desktop/Notes\ Backup
cd "$notes_data_directory"
shopt -s nullglob
backup_files=(*.{id,nsf} desktop8.nd[k] archiv[e] user.di[c])
for i in "${backup_files[@]}"
do
  cp -nipvR "$i" "$notes_backup_directory"
done

Yet another approach is to store a list of patterns in the array, and use the variable unquoted in order to expand the patterns. Note that the brace expansion needs to take place at the time the source code line is parsed, whereas the wildcards need to be stored unexpanded in the array. If there's any whitespace in the pattern, it needs to be quoted; getting this right is slightly tricky. I don't recommend this approach, it's hard to follow. Since a pattern may or may not end up matching a file, but cp requires at least one source file, you need to handle the case where the pattern matches zero file separately.

notes_data_directory=~/Library/Application\ Support/IBM\ Notes\ Data/
notes_backup_directory=~/Desktop/Notes\ Backup
backup_patterns=(\*.{id,nsf} "desktop8.nd[k]" "archiv[e]" "user.di[c]")
# equivalent to backup_patterns=("*.id" "*.nsf" "desktop8.nd[k]" "archiv[e]" "user.di[c]")
shopt -s nullglob
for i in "${backup_patterns[@]}"
do
  files=("$notes_data_directory/"$i)
  if [[ ${#files[@]} -ne 0 ]]; then
    cp -nipvR "${files[@]}" "$notes_backup_directory"
  fi
done
  • Fascinating. Everyone so far agrees to remove the double quotes and commas. The difference between your answer and @jimmij's is the declare -a statement. While neither one fixes the issue, the presence of declare -a affects the output. script declare -a backup_files=(*.{id,nsf} "desktop8.ndk" "user.dic"); echo $backup_files shell $ . ./notes-backup.sh *.id $ . ./notes-backup.sh *.nsf – Kevin Suttle Jul 20 '15 at 1:58
  • @KevinSuttle I still don't know what you're trying to say here. – Gilles Jul 20 '15 at 2:09
  • TL;DR: while declare -a changes the output, neither answer actually works. One echoes *.id, the other *.nsf. – Kevin Suttle Jul 20 '15 at 2:22
  • @KevinSuttle What you posted in your comment isn't complete code, is broken because it lacks double quotes when you use backup_files (it should be echo "${backup_files[@]}"), and you aren't saying whether there are files matching the patterns. You also didn't provide the rest of the code. Nothing that you show uses any script argument, so if passing different arguments produces different results, it's obviously due to something you didn't show. Edit your question to post your whole script — but you may have a different problem requiring a new question. – Gilles Jul 20 '15 at 2:35
  • No, I posted what code I had, and all of the output. SO doesn't support carriage returns, and by extension, code blocks in comments, so it may have been harder to read. – Kevin Suttle Jul 20 '15 at 3:05

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.