Is a for loop using arrays better than using field splitting on a simple variable?

In your script $list is the same as ${list[@]}.

The latter is array syntax, but in your script it is a normal variable.


As you have no whitespace in your wmctl output items, you don't need an array, and using $list is perfectly fine.


If it was an array, $list would only be the first item of the array (=> item1) and ${list[@]} would extend to all items (=> item1 item2 item3).

But what you really wanted if it actually was an array is "${list[@]}" (with quotes) that extends to "item1" "item2" "item3", so it would not choke on whitespace.


(Read)


A while loop is often a better fit than a for loop for processing command output, allowing you to process lines directly rather than storing them in a list or array.

In this case, it allows you to avoid the awk command altogether:

wmctrl -l | while read -r id dt stuff; do 
  case $dt in 
    -1) continue
        ;; 
     *) echo wmctrl -i -a "$id"
        echo wmctrl -i -c "$id"
        ;; 
  esac
done

Remove the echos once you are happy that it is doing the right thing.

As noted in comments, xargs is another option - but it gets tricky when you want to do more than one thing with each arg.


Answer to original title

The original title asked "what type of for loop is better".

For myself, the best method is the fastest one. To find out prepend the time command to your script or function. Some examples:

$ time du -s

real    0m0.002s
user    0m0.003s
sys     0m0.000s

$ time ls

real    0m0.004s
user    0m0.000s
sys     0m0.004s

It is important to flush cached buffers in-between tests though:

  • Which is the right way to drop caches in Lubuntu?

If two loops are about the same in speed, I'll pick the one with best readability.

The scope of this question is makes speed irrelevant though because most of the time is spent waiting for user input and there are only a maximum of 10 windows open for most people.


Answer to body of question

Other answers focus on rewriting the script so I'll give my two cents worth too.

The line:

list=$(wmctrl -l | awk ' !/-1/ { print $1 } ')
  • is malformed if intent is to be an array
  • list is generic and not descriptive

So I would use:

Windows=( $(wmctrl -l | awk ' !/-1/ { print $1 } ') )
  • The outer set of () tells bash/shell everything inside is an array element delineated by spaces.
  • Windows are what we are talking about so it is a descriptive array name.
  • Windows is plural so naming convention helps identify it's an array.

The line:

wmctrl -i -a $i
  • -i and -a can be combined into -ia.
  • $i is non-descriptive I would use $Window instead.

There are two ways of writing a shorter more readable script, first with an array:

#!/bin/bash
Windows=( $(wmctrl -l | awk ' !/-1/ { print $1 } ' ) )
for Window in "${Windows[@]}" ; do wmctrl -ia $Window -c $Window ; done

second without an array:

#!/bin/bash
Windows=$(wmctrl -l | awk ' !/-1/ { print $1 } ' )
for Window in $Windows ; do wmctrl -ia $Window -c $Window ; done

I prefer the array method because I'm trying to learn more about them and want to use them as much as possible. The choice is yours however.


You can manage without an array. Setting IFS to newline will allow for to loop lines, you can then unset the IFS inside the loop without affecting the loop itself.

#!/bin/bash

IFS=$'\n'
for i in $(wmctrl -l); do
    unset IFS
    set -- $i
    (($2 > -1)) && wmctrl -i -a $1 -c $1
done

(resetting the positional parameters is a neat trick to split a line in to fields).

if you need to use an array you can use mapfile and take advantage of the callback function to create something similar to a loop. For a small set of iterations it can be an advantage to use the simpler function call.

mapfile -c 1 -C 'f(){ set -- $@; (($3 >= 0)) && wmctrl -i -a $2 -c $2; }; f' -t < <(wmctrl -l)

(long version):

#!/bin/bash

f(){
    set -- $@
    if (($3 > -1)); then
        wmctrl -i -a $2 -c $2
    fi
}
mapfile -c 1 -C f -t < <(wmctrl -l)