Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash for loop with spaces

I would like to do something like this:

COMMANDS='"ls /" "df ~" "du -hs ~/Devel/"'
for i in $COMMANDS; do
    echo $i
done

Where the result would be:

ls /
df ~
du -hs ~/Devel/

But I can't find the right syntax for the spaces.

like image 285
dargaud Avatar asked Sep 25 '15 14:09

dargaud


People also ask

What does IFS mean in bash?

The IFS variable is used in shells (Bourne, POSIX, ksh, bash) as the input field separator (or internal field separator). Essentially, it is a string of special characters which are to be treated as delimiters between words/fields when splitting a line of input. The default value of IFS is space, tab, newline.

How do you handle spaces in file names?

There are two main ways to handle such files or directories; one uses escape characters, i.e., backslash (\<space>), and the second is using apostrophes or quotation marks. Using backslash can be confusing; it's easy and better to use quotation marks or apostrophes.


2 Answers

COMMANDS=("ls /" "df ~" "du -hs ~/Devel/")
for i in "${COMMANDS[@]}"; do 
  echo "$i"
done

This uses an array to store the commands. This feature is also available in ksh, zsh, but not in sh.

Arrays behave like the "$@" argument array. Applying a for loop on "${ARRAY_NAME[@]}" (the quotes are important) will give you each item in succession. If you omit the quotes, it'll all get smushed together and split on the separators present in your IFS environment variable ('\t', '\n' and ' ' by default).

like image 62
PSkocik Avatar answered Oct 14 '22 13:10

PSkocik


I'd recommend you not do that at all. First, it's much longer and more complicated than simply writing

ls /
df ~
du -hs ~/Devel/

Second, flat strings are not able to store nested, space-delimited strings. There is no way (and yes, I'm ignoring eval) to differentiate between spaces that separate commands and spaces that separate arguments within a command. You can use an array for simple commands, but you can't nest arrays, so as soon as the arguments for one of your commands contain spaces, you are back to the original problem.

commands=("ls /" "df ~" "du -hs ~/Devel")  # OK, but...
commands=("ls \"foo bar\"" "echo 'hello world'")  # No.

If you want your script to be able to run arbitrary commands specified by a user, have it source files from a known directory instead (that is, implement a plug-in system).

command_dir=~/myscript_plugins
for f in "$command_dir"; do
    source "$f"
done

where $command_dir contains one file for each of the commands you want to run.

Or, define a series of functions, and store their names in an string (function names can't contain spaces, so there's no need for arrays):

lister () { ls /; }
dfer () { df ~; }
duer () { du -hs ~/Devel; }

commands="lister dfer duer"
for command in $commands; do 
    $command
done

or

commands=(lister dfer duer)
for command in "${commands[@]}"; do
    $command
done

Further reading: I'm trying to put a command in a variable, but the complex cases always fail!

like image 40
chepner Avatar answered Oct 14 '22 13:10

chepner