6

I have a Bash script that checks my laptop battery and sends me an Android notification (via shuttle, a Bash script I wrote that serves as a cli interface for the Pushbullet API). It works nicely, but it will repeatedly notify me to unplug my laptop when the battery reach 100%. Instead, I'd rather it just notify me of 100% charge once, and then that's it (i.e. continue checking but do not notify). I do, however, want it to repeatedly notify me when the battery is low, so that I remember to plug in the laptop (which is currently what it does).

I was thinking that this is possible using continue and break commands inside the loop, but I'm not sure if that's what I want (I'm not very familiar with those).

Anyway, I would appreciate suggestions as to how to best implement this using Bash. I suspect this is very simple, but I'm just not getting it for some reason.

Here is my script:

#! /bin/bash

while true;
do

percent=$(acpi | awk '{ print $4}' | sed -e 's/%//g' |  sed -e 's/,//g')

 if [ "$percent" -le "20" ];
then
    shuttle push note Chrome "Aurora: Plug in now" "Battery is at $percent percent"
fi  
if [ "$percent" -eq "100" ];
then
    shuttle push note Chrome "Aurora: Battery charged" "Battery is at $percent percent"
fi

sleep 7m
done
1
  • for the general case of "dobthe loop but dobsomething in it only once": inside the loop: [[ "IDidIT" = "Yes" ]] || { do it here... ; IDitIt="Yes" ; } Jul 15, 2015 at 0:29

3 Answers 3

6
push(){
    shuttle push note Chrome \
        "Aurora: $1" \
        "Battery is at $percent percent"
}

full=0
while    percent=$(acpi | awk '{ print $4}' | sed 's/[,%]//g')
do       case $percent:$full in 
         (100:1) ;; (100:0)
              full=1
              push 'Battery charged';;
         (?:*|1?:*|20:*)
              full=0
              push 'Plug in Now';;
         (*1) full=0
         esac
done

The shell's case statement allows you to execute an arbitrary code block based on whether or not a shell pattern can be matched against the value of a shell expansion. In this way you can very simply handle multiple possible outcomes of the very same test.

Above I concatenate the values of $percent and $full on a : colon delimiter, which is a technique I originally picked up from Stephane Chazelas even if I have become fairly good with it on my own right since. Because the code executed needs to depend on both values, testing both simultaneously is the most simple solution.

case pattern blocks are evaluated in order from first to last. As soon as a pattern is matched the associated code is executed and case returns. The first matched pattern is also the last pattern evaluated.

And so if the values of $percent and $full are respectively:

  1. 100 and 1

    • The first pattern is matched, which is associated with an empty code block. No action is taken, and case returns to while.
  2. 100 and 0

    • The second pattern is matched, which sets $full to 1 and calls the shell function push w/ the argument Battery Charged
    • push() is only defined here to organize the code according to purpose.
  3. <=20 and anything at all

    • The third pattern is matched, $full is set to 0, and push is called with the arg Plug in Now.
  4. anything at all and 1

    • The last pattern is matched and $full is set to 0.
  5. Anything else

    • No pattern is matched and no action is taken.

Taken altogether, this means that $full is only set when necessary, push() is only called when $full is 0 and $percent is 100 or $full is anything and $percent is <=20.

All of that would be something of an improvement on what you have already. But the real problem there is:

percent=$(acpi | awk '{ print $4}' | sed 's/[,%]//g')

Forking multiple times like that in a while true is incredibly wasteful. You've gotta come up with another way to do that.

7
  • If that's wasteful then you'll probably die if you see my Pushbullet script. Many, many command substitutions with multiple forks using awk and sed. I admit, it's a mess, but it works. I suppose if I had more sense, I would have just used Python, but that would take out all of the fun of the doing it in bash! At any rate, I suppose I need to brush up on my awk and sed skills and just stick to one of them, rather than both. Thank you for your helpful suggestions. Jul 14, 2015 at 3:13
  • 1
    @Sophiejane - no, it's a perfectly sensible assumption. It's just that the shell works best as a data conductor rather than the terminal, if you know what I mean. Sorry if that's vague, but these things are usually best accomplished with a single pipeline per job as opposed to a single pipeline per test. Plus, I hate Python. In fact, though, I was just looking at acpid and it appears to be the way to go - it opens a listening socket (and so achieves the single pipeline) on kernel acpi events and dispatches events by name to scripts you write.
    – mikeserv
    Jul 14, 2015 at 3:20
  • Oh, and command substitutions are fine, but they should be used to gather data needed to prime a loop so you can kick it off without needing any within it. It's not the fork()s that are problematic - it's looping on fork()s which you should try to avoid.
    – mikeserv
    Jul 14, 2015 at 3:24
  • I don't think I've ever seen a case statement used like that before, with the values of variables concatenated using a colon delimiter (or if I had, I didn't understand it). While that way is not as immediately human-readable, your explanation was very helpful, and I can immediately see the advantages to doing it this way. I'll test it out tonight while I mess around with sed to try and reduce the pipes in $percent. Jul 14, 2015 at 4:48
  • @Sophiejane - if you don't like the colon, maybe use a + instead?
    – mikeserv
    Jul 14, 2015 at 4:57
3

How about:

if [ "$percent" -eq 100 ] && [ "$full_flag" -eq 0 ];
then
    shuttle push note Chrome "Aurora: Battery charged" "Battery is at $percent percent"
    full_flag=1
fi
if [ "$percent" -lt 100 ];
then
    full_flag=0
fi
5
  • 1
    Please update each stanza to include full_flag, ie if $percent -lt 20, full_flag=0
    – eyoung100
    Jul 14, 2015 at 1:50
  • 1
    ...combined with a test for $percent being less than 100% to reset full_flag to 0...
    – Jeff Schaller
    Jul 14, 2015 at 1:51
  • Should the flag be set to = 0 outside the loop or in the beginning of it? I had implemented a flag set to = 0 outside the loop and set to = 1 after the if... then, but it did not work. (I had the if.. then check for it in addition to the battery level, of course.). Perhaps the missing element was a test for the battery being < 100%. Jul 14, 2015 at 2:29
  • @Sophiejane - if...fi doesn't suit a multiple possibility branch tree well. That's what the case...esac statement is for.
    – mikeserv
    Jul 14, 2015 at 2:35
  • OK, the suggestions above worked. I think the key was the check for the battery being less than 100 (and if so, then reset the flag). Without that, the flag is useless. Thank you everyone for your input :-) Jul 14, 2015 at 3:11
1

continue jumps to the next iteration of the loop. break exits the loop altogether. Neither is useful if you want to have some effect on future iterations. For that, you need to remember the information that something has to behave differently. The way to remember some information is to store it in a variable.

One way to do what you want is to store the previous value of percent in a variable previous. If percent and previous are both 100, don't print a message.

Given that you're running this in the background to monitor the battery, you should strive not to use too much power. Minimize the number of processes.

Instead of looping on true, loop on the sleep command. This way, you can kill the script by killing the sleep command, when it's running, which is most of the time.

Indent your code. Anything inside a block (do, then, etc.) should have more whitespace on its left, by a consistent number.

previous=
while sleep 7m
do
  percent=$(acpi | awk '{ gsub(/[^ 0-9]/, ""); print $4 }')
  if [ "$percent" -le "20" ]; then
    shuttle push note Chrome "Aurora: Plug in now" "Battery is at $percent percent"
  elif [ "$percent" -eq "100" ] && [ "$previous" -ne "100" ]; then
    shuttle push note Chrome "Aurora: Battery charged" "Battery is at $percent percent"
  fi
done
0

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.