0

I'm trying to write a bash script that read user's input (some files so user can use TAB completion) and copy them into a specific folder.

#/bin/bash read -e files for file in $files do echo $file cp "$file" folder/"$file" done 

It's ok for: file1 file2 ...

Or with : file* (even if there is a filename with space in the folder).

But it's not working for filenames with space escaped with backslash \ like : file\ with\ space escaped spaces are ignored and string is split on each spaces, even escaped.

I saw information on quoting, printf, IFS, read and while... I think it's very basic bash script but I can't find a good solution. Can you help me?

10
  • Aside: The unix spelling is "directory", not "folder". Commented May 25, 2016 at 0:42
  • Why not just quote $files (eg. for file in "$files")? Commented May 25, 2016 at 0:57
  • @l'L'l, because that would disable globbing, which the OP explicitly wants. Commented May 25, 2016 at 0:58
  • @l'L'l, ...and doing it at the for file in "$files" point makes the loop useless -- it'll only iterate once, over the exact and entire content of $files; might as well not have a loop at all in that case. Commented May 25, 2016 at 0:58
  • 1
    @Lucien, ...btw, in any kind of more real-life scripting case, the preferred approach is to take input on the command line rather than prompting on stdin. If the user specifies a list of globs on the command line, they all get expanded there, so you can just expand "$@" to get the exact names and not deal with any of this mess. Easier to automate when calling from other scripts that way, as well. Commented May 25, 2016 at 1:00

4 Answers 4

4

Clearing IFS prior to your unquoted expansion will allow globbing to proceed while preventing string-splitting:

IFS=$' \t\n' read -e -a globs # read glob expressions into an array IFS='' for glob in "${globs[@]}"; do # these aren't filenames; don't claim that they are. files=( $glob ) # expand the glob into filenames # detect the case where no files matched by checking whether the first result exists # these *would* need to be quoted, but [[ ]] turns off string-splitting and globbing [[ -e $files || -L $files ]] || { printf 'ERROR: Glob expression %q did not match any files!\n' "$glob" >&2 continue } printf '%q\n' "${files[@]}" # print one line per file matching cp -- "${files[@]}" folder/ # copy those files to the target done 

Note that we're enforcing the default IFS=$' \t\n' during the read operation, which ensures that unquoted whitespace is treated as a separator between array elements at that stage. Later, with files=( $glob ), by contrast, we have IFS='', so whitespace no longer can break individual names apart.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for this answer it works exactly as I wanted and for the comments they help me to understand the process.
2

Note that the answers of both Charles Duffy and user2350426 do not preserve escaped *s; they will expand them, too.

Benjamin's approach, however, won't do globbing at all. He is mistaken in that you can first put your globs in a string and then load them into an array.

Then it will work as desired:

globs='file1 file\ 2 file-* file\* file\"\"' # or read -re here # Do splitting and globbing: shopt -s nullglob eval "files=( $globs )" shopt -u nullglob # Now we can use ${files[@]}: for file in "${files[@]}"; do printf "%s\n" "$file" done 

Also note the use of nullglob to ignore non-expandable globs. You may also want to use failglob or, for more fine-grained control, code like in the aforementioned answers.

Inside functions, you probably want to declare variables, so they stay local.

Comments

1

You can read the filenames into an array, then loop over the array elements:

read -e -a files for file in "${files[@]}"; do echo "$file" cp "$file" folder/"$file" done 

Reading into a single string won't work no matter how you quote: the string will either be split up at each space (when unquoted) or not at all (when quoted). See this canonical Q&A for details (your case is the last item in the list).

This prevents globbing, i.e., file* is not expanded. For a solution that takes this into account, see Charles' answer.

1 Comment

Thank you for your answer! But file* is not working anymore. It does not loop on all the files beginning with file just on the file named file*. I would like to allow this kind of input
1

There is a fully functional solution for files and globs.

With the help of using xargs (which is able to preserve quoted strings). But you need to write files with spaces inside quotes:

"file with spaces" 

When you use the script: Unquote the read and quote the assignment for listOfFiles.

I am also taking advantage of some ideas on the post of @CharlesDuffy (thanks Charles).

#!/bin/bash # read -e listOfFiles listOfFiles='file1 file* "file with spaces"' IFS='' while IFS='' read glob; do # read each file expressions into an array files=( $glob ) # try to expand the glob into filenames # If no file match the split glob # Then assume that the glob is a file and test its existence [[ -e $files || -L $files ]] || { files="$glob" [[ -e $files || -L $files ]] || { printf 'ERROR: Glob "%q" did not match any file!\n' "$glob" >&2 continue } } printf '%q\n' "${files[@]}" # print one line per file matching cp -- "${files[@]}" folder/ # copy those files to the target done < <(xargs -n1 <<<"$listOfFiles") 

4 Comments

I'm not sure that the extra code accomplishes anything when the shell is at default settings -- a glob that doesn't match evaluates to itself, absent nullglob being active. Can you provide an example case where this fixes a bug present in the preceding answer?
@CharlesDuffy Because of the last line: xargs -n1 <<<"$listOfFiles" the part inside double quotes "file with spaces" appear as one line to the shell (not broken into parts by the spaces). Once that is a single line, we can test if a line is a glob, or in failing, if it is a file, which "file with spaces" successfully is (if it exist). Your answer does not allow nor process the case in which there is a "file with spaces" in the given list (which is written in code in the script here). I hope that I was able to explain the difference. Not a bug: I hope that it is an improvement.
Ahh, got it. Yup, I only support file\ with\ spaces, so this is indeed an improvement.
...the place where I got held up, by the way, was trying to figure out in what scenario files="$glob" will result in a match, when files=( $glob ) didn't.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.