6

I am attempting to write a BASH script that creates a timestamp (to be written to file) every time a certain string is found within the output of dbus-monitor (parameters specified later in post). The main purpose of my script is to save the time (including milliseconds) and date whenever a song starts playing on Spotify, as it utilizes notifications.

The following command outputs string "Spotify" whenever a song beings playing.

dbus-monitor --session interface='org.freedesktop.Notifications',member='Notify' | grep 'string "Spotify"'

My attempt:

search='string "Spotify"'
found=$(dbus-monitor --session interface='org.freedesktop.Notifications',member='Notify' | grep 'string "Spotify"')

while [ ${search} == ${found} ]; do
    date -u +%Y%M%d-%H%M%S.%N >> timestamp.txt
done

I am assuming the reason of my code's dysfunction is that dbus-monitor continuously runs, therefore preventing the while loop from executing.

3
  • Read man dbus-monitor dbus. Commented Nov 14, 2021 at 2:51
  • Read man grep, especially about the --line-buffered option. Commented Nov 14, 2021 at 4:32
  • 1
    line-buffered doesn't help either. grep don't give return code per line (never solved) stackoverflow.com/q/56675613 Commented Nov 14, 2021 at 8:50

4 Answers 4

9

Use awk instead of grep - something like:

dbus-monitor ... | awk '/Spotify/ {
    system("date -u +%Y%m%d-%H%M%S.%N >> timestamp.txt")
  }'

(note the use of %Y%m%d instead of %Y%M%D - capital-M is minutes, not months. And capital-D is equivalent to %m/%d/%y)

This will use awk's system() function to run the date command in a sub-shell whenever it sees "Spotify" in the input. Alternatively, use awk's built-in date-formatting and redirection:

dbus-monitor ... | awk '/Spotify/ {
    print strftime("%Y%m%d-%H%M%S") >> "timestamp.txt"
  }'

This version won't print nanoseconds in the timestamp because strftime() doesn't support %N.

Alternatively, use perl instead of awk. That would allow you to use perl's Desktop::Notify module to get the notifications or Net::DBus to communicate directly with dbus.

9

Since you have GNU utilities, you could do something like:

dbus-monitor --session interface='org.freedesktop.Notifications',member='Notify' |
  sed -un 's/^.*string "Spotify".*$/now/p' |
  stdbuf -oL date -uf - +%Y%m%d-%H%M%S.%N >> timestamp.txt

dbus-monitor already disables buffering, so stdbuf -oL is not necessary there.

The -u option of GNU sed disables output buffering and also makes it read its input one byte at a time when it's not seekable here. We don't need the latter but we need the former so that it outputs a line as soon as it reads it.

Here, we get sed to output now each time it finds a line that contains string "Spotify".

That now is fed to date. With -f -, date reads the date to print from stdin. For each now it reads, it prints the current time in the format specified. With stdbuf -oL we make sure that output makes it to the timestamp.txt file straight away instead of in chunks.

If you do want to run any arbitrary command, instead of just outputting the current time, with, zsh/bash/ksh93, you could do:

while IFS= read -ru3 line || [ -n "$line" ]; do
  any arbitrary command
done 3< <(
  dbus-monitor --session interface='org.freedesktop.Notifications',member='Notify' |
  grep --line-buffered 'string "Spotify"'
)
2
  • 1
    Since we're using GNU sed, we can even have it execute the command, with a s///e substitution. Commented Nov 15, 2021 at 14:10
  • What is the reason for the usage of file descriptor 3, occurring in - ru3 and 3<? Commented Nov 23, 2021 at 21:10
3

This should work:

stdbuf -oL dbus-monitor --session interface='org.freedesktop.Notifications',member='Notify' |
while grep -q 'string "Spotify"'; do
    date -u +%Y%M%d-%H%M%S.%N >> timestamp.txt
done

EDIT after @StéphaneChazelas comments:

stdbuf -oL dbus-monitor --session interface='org.freedesktop.Notifications',member='Notify' |
grep --line-buffered 'string "Spotify"' |
while read trash; do
    stdbuf -oL date -u +%Y%M%d-%H%M%S.%N >> timestamp.txt
done

+1 other answers but I keep this for completeness

5
  • grep -q reads its input in large blocks though. If in that block there are several matches, only the first one will cause a timestamp to be added to timestamp.txt Commented Nov 14, 2021 at 11:24
  • @StéphaneChazelas Right, just edited. I think this forces grep to read line by line. Commented Nov 14, 2021 at 11:35
  • stdbuf -oL forces dbus-monitor to buffer its output by line. But dbus-monitor already doesn't buffer its output. Still, if dbus-monitor outputs more than one match into the pipe by the time grep reads that output from the other end, grep will read both and the second will be dropped. Commented Nov 14, 2021 at 11:58
  • I think I disagree with the first part. the output is buffered because dbus-monitor is connected to a pipe. anyway, I agree about the second one. Commented Nov 14, 2021 at 12:29
  • 1
    Applications using stdio for I/O would start buffering their output by block instead of by line when it doesn't go to a tty device (and a pipe or regular file is not a tty device). dbus-monitor does use stdio, but the first thing it does upon starting is setvbuf (stdout, NULL, _IOLBF, 0) to disable buffering regardless of whether stdout is a tty or not. Commented Nov 14, 2021 at 17:21
2

In this case, it looks like dbus-monitor already provides timestamps (in epoch time + microseconds). So you may not need to execute date for each match. Perhaps the match expression can narrowed down with, for example, arg0='Spotify'.

Check the output from this:

dbus-monitor "
  type='method_call',
  interface='org.freedesktop.Notifications',
  member='Notify',
  arg0='Spotify'"

Hopefully you will only see dbus messages relating to notifications from Spotify (unable to test this - it's just a guess from looking at the dbus spec). If this works, then the following may be suitable:

dbus-monitor --profile "type='method_call',
  interface='org.freedesktop.Notifications', member='Notify', arg0='Spotify'" |
gawk -F '\t' '
  $NF=="Notify" {
    secs = usecs = $2
    sub(/^[^.]+/,"",usecs)
    print strftime("%Y%m%d-%H%M%S",int(secs),"UTC") usecs
    fflush()
  }' > timestamp.log

Using --profile for the output format as it seems simpler to parse than the default --monitor output. Piping to GNU awk to extract and format the timestamp.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.