Bash while read loop: output never changes; being cached? - linux

***FINAL ANSWER:
dbus-monitor --profile "interface='net.sacredchao.QuodLibet',member='SongStarted'" |
while read -r line; do
sleep 1
echo -e "$(grep '~filename=' $HOME/.quodlibet/current | sed 's/~filename=//')\n$(python2 -c "import sys, urllib as ul; print ul.unquote_plus(sys.argv[1])" "$(quodlibet --print-queue | sed 's|file://||g')")" > $HOME/Dropbox/Playlists/queue
done
There were issues with race conditions and clobbering a file with more than one write, and I also replaced the echo -e with a python command since, quodlibet being made with python, it was probably parsed into the parsing I don't want with python in the first place. It all works now. I've finished giving quodlibet functionality that most music players already have: a queue that actually survives the program crashing and doesn't require constant tinkering and babysitting to just play each song exactly once. sigh
------------------------Original Question:
I'm using Arch Linux, and I've made a script that's supposed to save quodlibet's queue every time a song finishes/starts playing. Everything works EXCEPT for one thing (so you don't have to check all those sed commands for errors or anything). If I run all this stuff manually in a terminal, without any while loop or DBus--just manually writing to the files every time I notice a song ending--it works fine. Once I put it in the while loop, it still writes to the file every time a song starts, but it writes the exact same file every single time until I restart the script. I think it's caching the outputs of these commands and not ever updating that cache.
So, let's spell this out: quodlibet has its currently playing song and the queue of what to play next. Here's the script:
while read -r line; do
grep '~filename=' $HOME/.quodlibet/current \
| sed 's/~filename=//' > $HOME/Dropbox/Playlists/queue
echo -e "$(quodlibet --print-queue | sed 's|%|\\\x|g' | sed 's|file://||g')" \
>> $HOME/Dropbox/Playlists/queue
done <<< "$(dbus-monitor --profile "interface='net.sacredchao.QuodLibet',member='SongStarted'")"
Every time a song changes, what's supposed to happen is those two commands write the now-playing song and the queue to a file. What happens instead is every time a song changes, those two commands write what WAS the now-playing song and queue the FIRST time that while loop wrote to the file.
So let's say the now-playing is track 1 and tracks 2 thru 10 are in the queue. The script writes that to the file.
Then, track 1 finishes. Now, now-playing is track 2 and the queue is tracks 3 thru 10. However, even though the loop notices the song change and writes to the file, what it writes instead is track 1 as now-playing and tracks 2 thru 10 as the queue. Again. And on and on, and before you know it, it's playing track 10 and the queue is empty, but the file still has all 10 tracks in it.
I tried running the exact same commands inside the loop manually, myself, in a terminal outside the loop, immediately after a song change, while the loop script was running. The file would reflect what it was supposed to. But then when the song changed again, the loop would catch that and rewrite all 10 tracks, again. In other words, these exact commands only don't do what I want when they're inside the loop. SOMEthing is definitely being cached and never updated, here.
EDIT: It looks like I need to clarify some things.
1) Despite how this shouldn't be, my script behaves the exact same way whether it's:
while read -r line; do
grep '~filename=' $HOME/.quodlibet/current \
| sed 's/~filename=//' > $HOME/Dropbox/Playlists/queue
echo -e "$(quodlibet --print-queue | sed 's|%|\\\x|g' | sed 's|file://||g')" \
>> $HOME/Dropbox/Playlists/queue
done <<< "$(dbus-monitor --profile "interface='net.sacredchao.QuodLibet',member='SongStarted'")"
Or:
dbus-monitor --profile "interface='net.sacredchao.QuodLibet',member='SongStarted'" |
while read -r line; do
grep '~filename=' $HOME/.quodlibet/current \
| sed 's/~filename=//' > $HOME/Dropbox/Playlists/queue
echo -e "$(quodlibet --print-queue | sed 's|%|\\\x|g' | sed 's|file://||g')" \
>> $HOME/Dropbox/Playlists/queue
done
2) I tested a simpler version out before bringing the more complex stuff in. Here's a script I made:
while true; do
echo "$(cat /tmp/foo | sed 's/b/n/g')"
done
I tried changing the file /tmp/foo in the middle of that loop running, just as the output of the quodlibet commands changes on its own, and it updated just fine. The output changed like it should.
This wasn't what I started with, but it's the last one I made before moving on to the actual script. You'll see it incorporates everything I'm doing except for 4 things: quodlibet, the dbus command, >saving or >>appending to a file, and the while-read combo. One of those is making the output constantly the same no matter what changes in the environment, and I think we can rule out quodlibet, since, as I said before, running those commands manually works fine.
EDIT 2: Welp, I haven't been scrolling down on the file, but I did just now. This issue just got more complicated but probably easier to solve. It's not writing the exact same output every time at all. It's somehow skipping the line that overwrites the file--the one that starts with grep--and JUST appending the output of the second line, the echo -e.
EDIT 3: And now I'm stumped again. When I copy and paste the exact two lines right out of the while loop and into a bash Terminal, they do what I want. But in the while-loop, the first grep command never actually writes to that file. I thought maybe it was inexplicably eating the first command, so I tried adding an empty echo beforehand:
dbus-monitor --profile "interface='net.sacredchao.QuodLibet',member='SongStarted'" |
while read -r line; do
echo
grep '~filename=' $HOME/.quodlibet/current \
| sed 's/~filename=//' > $HOME/Dropbox/Playlists/queue
echo -e "$(quodlibet --print-queue | sed 's|%|\\\x|g' | sed 's|file://||g')" \
>> $HOME/Dropbox/Playlists/queue
done
but it's still eating that first grep. Why won't it save the file?
EDIT 4: I've confirmed that the grep command is definitely outputting the correct output, but just won't write it to the file.

In an effort to decouple the specific thing you're trying to do from the problem you perceive, I'm going to suggest trying out some independent commands before trying to use anything fancy.
while read -r line ; do
echo $line
done <<< $(yes)
Note that this will never output anything. The input provided by $(yes) never completes so the outer loop will never print anything. This was pointed out by at least one of the comments to your main post.
Instead of using a capture variable, try piping the output directly to your loop. I can't say this will work for you, but it at least fixes one of the obvious problems of streaming output to a while loop.
yes | while read -r line ; do
echo $line
done
Based on the other debugging efforts here, the problem appears to be a race condition between the signal to dbus about the song transition and the writing of the new "current" song to disk. In an effort to provide a more complete solution (untested of course), here is what I might start with:
function urldecode() {
python -c "import sys, urllib as ul; print ul.unquote_plus(sys.argv[1])" "$#"
}
dbus-monitor --profile "interface='net.sacredchao.QuodLibet',member='SongStarted'" | \
while read -r line; do
sleep 1 # Adjust as necessary to preventing race conditions
$CurrentSongFile=$(grep '~filename=' $HOME/.quodlibet/current | sed 's/~filename=//')
$SongFileQueue=$(urldecode "$(quodlibet --print-queue)") | sed 's_file://__g')
printf "$CurrentSongFile\n$SongFileQueue" > $HOME/Dropbox/Playlists/queue
done

Related

Read stdin to run a script on certain keystroke without interfering in programs/games

My goal is to run a script every time I push the R key on my keyboard:
#!/bin/bash
tail -50 .local/share/binding\ of\ isaac\ afterbirth+/log.txt | grep "on seed" | awk '{print $(NF-1) $NF}' > /home/fawk/seed
I already tried to do this with a program called xbindkeys but it would interfere and pause the game.
Then I tried it with another bash script using the read command to execute the first script but my knowledge isn't that great so far. Reading and experimenting for hours didn't lead anywhere.
The purpose is to feed a game-seed to OBS. The easiest way seems to create a (text-)file containing the seed for OBS to pick up. All I want is that seed (1st script) to be written into a file.
I tried
#!/bin/bash
while :
do
read -r 1 k <&1
if [[ $k = r ]] ; then
exec /home/fawk/seedobs.sh
fi
done
and many other variations but didn't get closer to a solution.

referencing stdout in a command that has been piped into

I want to make a simple dmenu command that reads a file of commands and names. Then takes the names and displays them using dmenu then takes dmenu's output and runs the associated command using the file again.
I got to the point where dmenu displays the names, but I don't really know where to go from there. Learning bash is a really daunting task to me and I don't really know where to start with this seemingly simple script/command.
here is the file:
Pushbullet
google-chrome-stable --app=https://www.pushbullet.com
Steam
steam
Chrome
google-chrome-stable
Libre Office
libreoffice
Transmission
transmission-qt
Audio Control Panel
sudo pavucontrol & bluberry
and here is what I have so far for my command:
awk 'NR % 2 != 0' /home/rocco/programlist | dmenu | ??(grep -l "stdout" /home/rocco/programlist....)
It was my thinking that I could somehow pipe into grep or awk with the name of the application then get the line number then add one and pipe that into sh.
Thanks
I have no experience with dmenu but if I understand how it works correctly, this should do what you want. Wrapping a command in $(…) returns the output as a variable, which we can pass on to another command.
#!/bin/bash
plist="/home/rocco/programlist"
# pipe every second line to dmenu
selected=$(awk 'NR % 2 != 0' "$plist" | dmenu)
# search for the selected item, get the command after it
cmd=$(grep -A1 "$selected" "$plist" | tail -n 1)
# run the command
$cmd
Worth mentioning a mistake in your question. dmenu sends to stdout, or standard output, but the next program in line would be reading stdin, or standard input. In any case, grep can't take patterns on standard input, which is why I've saved to a variable instead of trying to pipe it somewhere.
Assuming you have programlist.txt in the working directory you can use:
awk 'NR%2 !=0' programlist.txt |dmenu |awk '{system("grep --no-group-separator -A 1 '"'"'"$0"'"'"' programlist.txt");}' |awk '{if(NR==2){system($0);}}'
Note the quoting of the $0 in the first awk envocation. This is necessary to get names with spaces in them like "Libre Office"

Redirecting linux cout to a variable and the screen in a script

I am currently trying to make a script file that runs multiple other script files on a server. I would like to display the output of these script to the screen IN ADDITION to passing it into grep so I can do error testing. currently I have written this:
status=$(SOMEPROCESS | grep -i "SOMEPROCESS started completed correctly")
I do further error handling below this using the variable status, so I would like to display SOMEPROCESS's output to the screen for error reference. This is a read only server and I can not save the output to a log file.
You need to use the tee command. It will be slightly fiddly, since tee outputs to a file handle. However you could create a file descriptor using pipe.
Or (simpler) for your use case.
Start the script without grep and pipe it through tee SOMEPROCESS | tee /my/safely/generated/filename. Then use tail -f /my/safely/generated/filename | grep -i "my grep pattern separately.
You can use process substituion together with tee:
SOMEPROCESS | tee >(grep ...)
This will use an anonymous pipe and pass /dev/fd/... as file name to tee (or a named pipe on platforms that don't support /dev/fd/...).
Because SOMEPROCESS is likely to buffer its output when not talking to a terminal, you might see significant lag in screen output.
I'm not sure whether I understood your question exactly.
I think you want to get the output of SOMEPROCESS, test it, print it out when there are errors. If it is, I think the code bellow may help you:
s=$(SOMEPROCESS)
grep -q 'SOMEPROCESS started completed correctly' <<< $s
if [[ $? -ne 0 ]];then
# specified string not found in the output, it means SOMEPROCESS started failed
echo $s
fi
But in this code, it will store the all output in the memory, if the output is big enough, there will be a OOM risk.

How can you read the most recent line from the linux program screen?

I use screen to run a minecraft server .jar, and I would like to write a bash script to see if the most recent line has changed every five minutes or so. If it has, then the script would start from the beginning and make the check again in another five minutes. If not, it should kill the java process.
How would I go about getting the last line of text from a screen via a bash script?
If I have understand, you can redirect the output of your program in a file and work on it, with the operator >.
Try to run :
ls -l > myoutput.txt
and open the file created.
You want to use the tail command. tail -n 1 will give you the last line of the file or redirected standard output, while tail -f will keep the tail program going until you cancel it yourself.
For example:
echo -e "Jello\nPudding\nSkittles" | tail -n 1 | if grep -q Skittles ; then echo yes; fi
The first section simply prints three lines of text:
Jello
Pudding
Skittles
The tail -n 1 finds the last line of text ("Skittles") and passes that to the next section.
grep -q simply returns TRUE if your pattern was found or FALSE if not, without actually dumping or outputting anything to screen.
So the if grep -q Skittles section will check the result of that grep -q Skittles pattern and, if it found Skittles, prints 'yes' to the screen. If not, nothing gets printed (try replacing Skittles with Pudding, and even though it was in the original input, it never made it out the other end of the tail -n 1 call).
Maybe you can use that logic and output your .jar to standard output, then search that output every 5 minutes?

Grep filtering output from a process after it has already started?

Normally when one wants to look at specific output lines from running something, one can do something like:
./a.out | grep IHaveThisString
but what if IHaveThisString is something which changes every time so you need to first run it, watch the output to catch what IHaveThisString is on that particular run, and then grep it out? I can just dump to file and later grep but is it possible to do something like background it and then bring it to foreground and bringing it back but now piped to some grep? Something akin to:
./a.out
Ctrl-Z
fg | grep NowIKnowThisString
just wondering..
No, it is only in your screen buffer if you didn't save it in some other way.
Short form: You can do this, but you need to know that you need to do it ahead-of-time; it's not something that can be put into place interactively after-the-fact.
Write your script to determine what the string is. We'd need a more detailed example of the output format to give a better example of usage, but here's one for the trivial case where the entire first line is the filter target:
run_my_command | { read string_to_filter_for; fgrep -e "$string_to_filter_for" }
Replace the read string_to_filter_for with as many commands as necessary to read enough input to determine what the target string is; this could be a loop if necessary.
For instance, let's say that the output contains the following:
Session id: foobar
and thereafter, you want to grep for lines containing foobar.
...then you can pipe through the following script:
re='Session id: (.*)'
while read; do
if [[ $REPLY =~ $re ]] ; then
target=${BASH_REMATCH[1]}
break
else
# if you want to print the preamble; leave this out otherwise
printf '%s\n' "$REPLY"
fi
done
[[ $target ]] && grep -F -e "$target"
If you want to manually specify the filter target, this can be done by having the loop check for a file being created with filter contents, and using that when starting up grep afterwards.
That is a little bit strange what you need, but you can do it tis way:
you must go into script session first;
then you use shell how usually;
then you start and interrupt you program;
then run grep over typescript file.
Example:
$ script
$ ./a.out
Ctrl-Z
$ fg
$ grep NowIKnowThisString typescript
You could use a stream editor such as sed instead of grep. Here's an example of what I mean:
$ cat list
Name to look for: Mike
Dora 1
John 2
Mike 3
Helen 4
Here we find the name to look for in the fist line and want to grep for it. Now piping the command to sed:
$ cat list | sed -ne '1{s/Name to look for: //;h}' \
> -e ':r;n;G;/^.*\(.\+\).*\n\1$/P;s/\n.*//;br'
Mike 3
Note: sed itself can take file as a parameter, but you're not working with text files, so that's how you'd use it.
Of course, you'd need to modify the command for your case.

Resources