11

I have several applications open. Running wmctrl and piping the output to awk lists the window IDs (excluding "sticky" windows) like this:

$ wmctrl -l | awk ' !/-1/ { print $1 } '
0x00a00018
0x04800005
0x04e00005
0x04400003
0x05000003
0x0540002b
0x05a00012
0x05800002
0x05c00003
$ 

I can send this output to wmctrl to close all these windows:

  • windows without content that needs to be saved and windows that don't need a response will be closed without asking me but

  • windows such as those of editors with unsaved content or terminals running a process will be closed "gracefully": the respective application will present a window allowing me to save changes or discard changes or to inform me of a process that's still running.

The following script, assigned to a suitable shortcut, works:

#!/bin/bash

list=$(wmctrl -l | awk ' !/-1/ { print $1 } ')

for i in ${list[@]}
do
    wmctrl -i -a $i
    wmctrl -i -c $i
done

I found that the simpler (to me) for i in $list also works.

Is there any reason to prefer one over the other?


"sticky" and "gracefully" are terms from man wmctrl.

  • 2
    you're using list like it was an array, but it is a normal variable. – pLumo Oct 17 at 11:46
  • 4
    I would use the simpler expression, because it is simpler and works. No reason to use the array syntax in this case. – sudodus Oct 17 at 11:49
  • Why not also consider xargs – Stephen Boston Oct 17 at 12:07
  • @StephenBoston because I'm not really well-versed in these things! I find code somewhere that seems useful and then spend days trying to understand how it does what it does. – Justice for Monica Oct 17 at 12:10
  • @DKBose Ah well. I find you very well versed in almost everything DK! I wonder though if there might be another reason to not use xargs. I have a thing about it, is why. – Stephen Boston Oct 17 at 12:14
13

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)

  • To use an array isn't a bad habit, although you don't have to. To use an array you would do something like: declare -a list; list=($(wmctrl -l | awk ' !/-1/ { print $1 } '|tr '\n' ' ')); – A.Danischewski Oct 19 at 22:30
8

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.

5

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:

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.

  • For very short operations, would time some_command be conclusive? And I'm guessing a statistically significant number of runs would be needed? – Justice for Monica Oct 17 at 11:58
  • 2
    @DKBose, I think in the case of this question, the time is the same for both alternatives (within the measuring accuracy), so the conclusion would be to pick the one with best readability. – sudodus Oct 17 at 12:04
  • @DKBose yes it is maddening running time many times when job is hundredths of second, wild fluctuations. I've added new section to answer to rewrite your script like the other posters did. – WinEunuuchs2Unix Oct 17 at 23:12
  • maybe even wmctrl -ia $Window -c $Window ? – bac0n Oct 18 at 5:51
  • @bac0n Good suggestion! I tested it and it works. – WinEunuuchs2Unix Oct 18 at 10:37
5

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)
  • Re. ` ... should skip '-1' windows`, it does indeed skip the -1 windows. Thanks! Could you please add a little description of how the job's being done? – Justice for Monica Oct 18 at 13:14
  • Clever - nice to see a real-world example of the mapfile callback feature – steeldriver Oct 18 at 18:23

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.