2

let's say I have a directory with the following files/directories:

google
apple
mozilla foundation   # a file with spaces
browsers

So I would like to move files into browsers directory. Here is a script I wrote:

for d in $(ls); do
    if ! [ "$d" == "browsers" ]; then
        mv "$d" "browsers"
    fi
done

Then google and apple went to browsers but I got:

mv: cannot stat 'mozilla': No such file or directory
mv: cannot stat 'foundation': No such file or directory

Obviously, the problem is spaces in a variable. What's the proper way to deal with this?

Of course I believe there should be a more elegant one-line command to do this, but I would like to know how I should use variables with spaces.

  • Set IFS='\n' before doing your command, but reset it to default value afterwards. – Philippos Aug 11 '17 at 05:57
  • 2
    And https://unix.stackexchange.com/questions/131766/why-does-my-shell-script-choke-on-whitespace-or-other-special-characters – muru Aug 11 '17 at 06:03

2 Answers2

7

Do not ever use:

for d in $(ls)

Use this instead:

for d in *

Two problems, among others, with the use of $(ls) are that the shell subjects the results of $(ls) to word splitting and pathname expansion. In your case, it was word splitting that caused mozilla foundation to turn into mozilla and foundation.

For a lengthier discussion of the reason not to use $(ls), see "What does $(ls *.txt) do?"

If you need to account for the case where there is no non-hidden file in the directory (the case where for i in $(ls) may look better as it doesn't do any pass in the loop as opposed to one pass in the loop with * as $i in for i in * (except in zsh)), you'd want to tell the shell to not expand to anything for non-matching globs:

  • zsh: for i in *(N)
  • ksh93: for i in ~(N)*
  • bash4.4+: f() { local -; shopt -s nullglob; for i in *; ...; done; }; f (the point being to use the nullglob option locally (see also the failglob option for a behaviour similar to zsh's default one)).
  • yash: set -o nullglob (and reset afterwards, there's no local scope for options in yash as far as I know)
  • in other shells, you can always add a [ -e "$i" ] || [ -L "$i" ] || continue in the loop to check for the file's existence.
John1024
  • 74,655
  • Oh, I didn't think that way. Thank you. So is there no way to change mozilla foundation to mozilla\ foundation or "mozilla foundation"? – Bumsik Kim Aug 11 '17 at 05:59
  • 1
    @BumsikKim there might be, but why break and reassemble when you can get it unbroken? – muru Aug 11 '17 at 06:01
  • You're welcome. And, yes, there are many ways to manipulate strings like mozilla foundation but, if you use the above for f in *, you won't need to manipulate it: it will just work. Or, were you looking for some other use? – John1024 Aug 11 '17 at 06:01
  • @John1024 I think that's enough. I was thinking the solution I was looking for might be useful in other potential cases but it was just a wild thought – Bumsik Kim Aug 11 '17 at 06:07
  • @BumsikKim Very good. Generally, the key is to avoid word splitting in the first place. (Once a file name is split, you'll never really know how to reassemble --- one space or two or tabs or ...) Fortunately, there are a variety of good techniques that avoid those pitfalls. – John1024 Aug 11 '17 at 06:29
  • @StéphaneChazelas As always, thanks for the edit. – John1024 Aug 11 '17 at 06:30
1

Do not parse ls result, and beside of John1024's answer you could use find.

LC_ALL=C find . ! -name . -prune ! -name '.*' -type f -exec mv -t /path/to/dest {} +

(here assuming GNU find for the -t option, skipping hidden files like ls or a * glob would and excluding non-regular files. Note that the list is not sorted (contrary to with ls or *). LC_ALL=C is needed to properly skip any file whose name starts with . even those that don't contain valid characters in the user's locale. That affects the language of error messages though).

αғsнιη
  • 41,407