Working with multi-line

About writing shell scripts and making the most of your shell
Forum rules
Topics in this forum are automatically closed 6 months after creation.
Locked
FreedomTruth
Level 4
Level 4
Posts: 443
Joined: Fri Sep 23, 2016 10:19 am

Working with multi-line

Post by FreedomTruth »

I have this somewhat solved by writing to a temporary file (but would like to skip this step if possible)
The general concept I want help with is, I run a command (a) which returns multiple lines of output; I want to run another command (b) on part of each of the returned lines from (a). I have gotten around this by redirecting output from (a) to a file, then using exec and read with a new file descriptor to process each line individually.
Specifically, I'm using wmctrl to get a list of windows (and grep to narrow it down to specific ones), and another wmctrl command to modify properties of the ones that are grepped.
Last edited by LockBot on Wed Dec 28, 2022 7:16 am, edited 1 time in total.
Reason: Topic automatically closed 6 months after creation. New replies are no longer allowed.
User avatar
Termy
Level 12
Level 12
Posts: 4254
Joined: Mon Sep 04, 2017 8:49 pm
Location: UK
Contact:

Re: Working with multi-line

Post by Termy »

If you tell me what it is you want to do, exactly, I can write you up something which will do it. At the moment, I'm not yet quite sure what you're wanting to do, so there's not much I can suggest. It sounds like what you're wanting can be done by saving the output of the wmctl command you're using to a variable, then process that output separately in the rest of the script/line. Or you could use command substitution. (see man bash)

I need the command you're parsing (the initial wmctl command), and I need to know what it is you want to do with that output. Is it just to get lines based on a match, or is there something else?

Also, which OS and desktop environment are you on? I don't have wmctl so I'd need to load up a virtual machine (I don't mind) but need to know which one you're using. I have a Mint virtual machine already set up with Cinnamon, if that's any use. It has the wmctl command.
I'm also Terminalforlife on GitHub.
FreedomTruth
Level 4
Level 4
Posts: 443
Joined: Fri Sep 23, 2016 10:19 am

Re: Working with multi-line

Post by FreedomTruth »

Simple example of what I'm trying is to remove all VirtualBox windows from the taskbar:

Code: Select all

#!/bin/bash
wmctrl -l|grep 'VirtualBox'>/tmp/windowlist
exec 3</tmp/windowlist
read -u 3 currentline
while [ ! -z "$currentline" ]; do
  windowid=$(echo $currentline|cut --delimiter=\  --fields=1)
  wmctrl -i -r $windowid -b add,skip_taskbar
  read -u 3 currentline
done
rm /tmp/windowlist
Not that big a deal to write to file, just wanted a "cleaner" way of accomplishing this?
I guess one option would be to change the internal field separator, but I generally don't like doing that for some reason lol ...

(edit) forgot to tell you, I'm running LM 18.3 XFCE.
User avatar
Termy
Level 12
Level 12
Posts: 4254
Joined: Mon Sep 04, 2017 8:49 pm
Location: UK
Contact:

Re: Working with multi-line

Post by Termy »

Code: Select all

while read -a X; do [[ "${X[3]}" == *VirtualBox* ]] && wmctrl -i -r "${X[0]}" -b add,skip_taskbar; done <<< "$(wmctrl -l)"
Or for a more spaced-out version:

Code: Select all

while read -a X; do
	[[ "${X[3]}" == *VirtualBox* ]] && wmctrl -i -r "${X[0]}" -b add,skip_taskbar
done <<< "$(wmctrl -l)"
Tested and works on my end, when I tested with various windows of Nemo which had the title "Home".

How does it work? I'll break it down:

The while loop uses read to assign each line into an array consisting of each field in the output of the wmctrl command, which is processed thanks to command substitution (the $() part). The lines are processed, line-by-line, until there are no more lines to process. For every line processed, a check (test) is done (all "pure-shell", aside from of course wmctrl) on the fourth index in the array (index 3, as 0 is the first) to further process only lines with the glob match *VirtualBox*; this is the equivalent to your grep's regex match. Once a match has been found, the other wmctrl command is used to close those windows, per the 0 index which holds the window ID for that line/match.

Where this will fall flat is if you are wanting also to match lines which have multiple space-separated fields which contain the match *VirtualBox*, because it's only checking the fourth array index. An example is if your window title (or are we dealing with classes here? I'm not familiar with wmctrl) contains the string "A Random VirtualBox Window", which would be ignored, as the fourth index would simply be "A". If this is an issue for you, it can be worked around.

This can be further simplified, if you're not comfortable with my more pure-shell approach:

Code: Select all

for ID in `wmctrl -l | grep "VirtualBox" | cut -d " " -f 1`; { wmctrl -i -r "$ID" -b add,skip_taskbar; }
And if a one-liner isn't your thing, here's the same but spaced out:

Code: Select all

for ID in `wmctrl -l | grep "VirtualBox" | cut -d " " -f 1`; {
	wmctrl -i -r "$ID" -b add,skip_taskbar
}
This one doesn't suffer from the drawback mentioned for the other. That works by using command substitution (what is within ``) with a for loop, to iterate over each field (the relevant window IDs) and execute the 2nd wmctrl command on each ID. This is the fastest and simplest method that I've shown you.

By the way, you can change the IFS for a specific command, including read, like so: IFS=":" read It won't affect other commands, only the command you've changed the IFS for. This works because any variable(s) can be set for a specific command. This can be useful if you want to pass specific environment variables to a script. To see this in action yourself, create a script with echo "$HOME" in it, then run HOME="Test" bash script_name.
I'm also Terminalforlife on GitHub.
FreedomTruth
Level 4
Level 4
Posts: 443
Joined: Fri Sep 23, 2016 10:19 am

Re: Working with multi-line

Post by FreedomTruth »

I like your first solution... however
Termy wrote:Where this will fall flat is if you are wanting also to match lines which have multiple space-separated fields which contain the match *VirtualBox*, because it's only checking the fourth array index. An example is if your window title (or are we dealing with classes here? I'm not familiar with wmctrl) contains the string "A Random VirtualBox Window", which would be ignored, as the fourth index would simply be "A". If this is an issue for you, it can be worked around.
Indeed, the 4th column output of wmctrl -l is the window title; a running example right now is 0x0340000d 0 N/A Linux Mint [Running] - Oracle VM VirtualBox
Your second solution is working well. Thanks for the detailed explanation.


Actually, I guess I got the solution I was maybe looking for by looking closer at your first answer... I can pipe the output of wmctrl to grep, and pipe that to a while read command. The array works nicely for this too, no need for cutting.

Code: Select all

wmctrl -l|grep 'VirtualBox'| while read -a wmline; do wmctrl -i -r ${wmline[0]} -b add,skip_taskbar; done
User avatar
Termy
Level 12
Level 12
Posts: 4254
Joined: Mon Sep 04, 2017 8:49 pm
Location: UK
Contact:

Re: Working with multi-line

Post by Termy »

Glad it helped. You can do away with grep too, since the while read loop can do that for you. Here's a modification of the first solution, to mitigate that issue:

Code: Select all

while read -a X; do [[ "${X[*]}" == *VirtualBox* ]] && wmctrl -i -r "${X[0]}" -b add,skip_taskbar; done <<< "$(wmctrl -l)"
Damn near the same thing, but it should work. It must have previously slipped my mind to more closely mimic how you were using grep, by using the full variable (all its indices) in the [[ test.
I'm also Terminalforlife on GitHub.
lmuserx4849

Re: Working with multi-line

Post by lmuserx4849 »

FreedomTruth wrote: ...
The general concept..., I run a command (a) which returns multiple lines of output; I want to run another command (b) on part of each of the returned lines from (a).
...
General example:

Code: Select all

#!/bin/bash
# use an array to hold cmd_b's parameters
declare -a cmd_b_params=()

while read -re line; do

  # parse "${line}" using parameter expansion (## %%) or 
  # regular expression (=~ BASH_REMATCH[]) or command substitution var=$(cut -d' ' -f1 <<<"$line")
    
  # don't like a line get next
  [[ on some condition]] && continue

  # build your command
  cmd_b_params+=('-option1')
  cmd_b_params+=('--option2=')
  cmd_b_params+=("${var}")

  # execute second command.  bash will parse the options correctly as words.
  command_b "${cmd_b_params[@]}"

  # cleanup for next
  unset cmd_b_params
  
done < <(run_command_a)  # process substitution
Last edited by lmuserx4849 on Sat Jan 13, 2018 2:48 am, edited 1 time in total.
User avatar
Termy
Level 12
Level 12
Posts: 4254
Joined: Mon Sep 04, 2017 8:49 pm
Location: UK
Contact:

Re: Working with multi-line

Post by Termy »

lmuserx4849 wrote:---snip---
Unless I'm being blonde, I think there's a typo there or something, because that loop will do nothing; the first action is to continue (thus skipping the remaining code).
I'm also Terminalforlife on GitHub.
lmuserx4849

Re: Working with multi-line

Post by lmuserx4849 »

Termy wrote:
lmuserx4849 wrote:---snip---
Unless I'm being blonde, I think there's a typo there or something, because that loop will do nothing; the first action is to continue (thus skipping the remaining code).
I updated the example: [[on some condition ]] && continue.

I use this all the time to build a command from another command. You don't need exec.
User avatar
Termy
Level 12
Level 12
Posts: 4254
Joined: Mon Sep 04, 2017 8:49 pm
Location: UK
Contact:

Re: Working with multi-line

Post by Termy »

Oooh, this is about command line arguments (positional parameters)? I can't see a reason to add any, as the task is already achieved, but a common approach, and the one I tend to use, is:

Code: Select all

while [ "$1" ]; do
	case "$1" in
		--example|-e)
			echo "Commands would go here. I usually set variables here to indicate the arg is set." ;;
		--some-other-thing|-S)
			echo "More stuff." ;;
		*)
			If an incorrect/non-valid argument is given. ;;
	esac

	shift
done
A lot can be done with it, but that's the general gist. There's also getopts, but I've never really thought much of it; I feel I have more flexibility with the while loop.

The above can be seen working in just about all of my shell programs: https://github.com/terminalforlife
I'm also Terminalforlife on GitHub.
Locked

Return to “Scripts & Bash”