67

I'd like to have a desktop notification whenever a command that has run for more than, say 15 seconds, finishes in an interactive shell.

In other words, I would like all commands to be wrapped in something like this

start=$(date +%s);
ORIGINAL_COMMAND;
[ $(($(date +%s) - start)) -le 15 ] || notify-send "Long running command finished"

What's the best way to accomplish this in bash?

4
  • 1
    Possible same on Superuser: superuser.com/questions/31917/… Jan 27, 2015 at 14:08
  • Why did you add "in interactive shell" just now ? It might invalidate some of the existing answers which existed in context of your original post for over a year. If you need a solution specifically for interactive shell, consider asking separate question referencing this one. May 5, 2017 at 4:15
  • The "interactive shell" part was clarified in the first comment before the edit. I simply edited the question and removed the comment. Besides, the 3 lines posted in the questions works well for a script.
    – aioobe
    May 5, 2017 at 12:55

9 Answers 9

36

As far as I understood you want a wrapper. And you want to use a command through it so that it will give you desired notification if running time of your command is more than 15 sec. So here is it.

wrapper(){
    start=$(date +%s)
    "$@"
    [ $(($(date +%s) - start)) -le 15 ] || notify-send "Notification" "Long\
 running command \"$(echo $@)\" took $(($(date +%s) - start)) seconds to finish"
}

Copy this function in your ~/.bashrc and source ~/.bashrc as,

. ~/.bashrc

Useage

wrapper <your_command>

If it takes more than 15 sec you will get the desktop-notification describing the command and its time of execution.

Example

wrapper sudo apt-get update

screenshot of desktop-nitification

7
  • 1
    "$@", not $@. Big difference.
    – geirha
    Jan 23, 2014 at 19:31
  • 4
    I'd like to avoid writing wrapper in front of every command I type though.
    – aioobe
    Jan 23, 2014 at 19:58
  • 2
    @aioobe you can use a shorter function name even may be a letter. but a wrapper works in this manner.
    – sourav c.
    Jan 23, 2014 at 21:05
  • 3
    command; alert is in fact shorter than wrapper command :-)
    – aioobe
    May 8, 2015 at 15:19
  • 1
    If you don't want the notify-send to expire for a long time then give notify-send a custom timeout (in milliseconds). e.g. notify-send --expire-time 999999999 ... Apr 4, 2017 at 23:49
32

In ~/.bashrc there is an alias alert defined as:

alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'

which can be used to notify the completion of command execution.

Usage:

$ the_command; alert

e.g.

$ sudo apt-get update; alert

snap1

You may customize the alias as per your need and desire.

2
  • 3
    That's nice to know. Is there a way to append alert after every command I type (and also have it alert only if the command took more than 15 seconds)?
    – aioobe
    Jan 23, 2014 at 19:59
  • strange thing it is already added to my .bashrc, is it default in ubuntu?
    – Vignesh
    Dec 5, 2017 at 9:47
30
+100

You want https://launchpad.net/undistract-me (installable from the Ubuntu archives with sudo apt-get install undistract-me) which does precisely what you're asking for, including working automatically (that is, without having to remember to add something extra to potentially long-running commands).

2
  • 1
    It doesn't seem to be working. I installed it, restarted my system and tested with sleep 30, but no notification. May 3, 2015 at 16:08
  • 5
    You need to add the following two lines to .bashrc: . /usr/share/undistract-me/long-running.bash notify_when_long_running_commands_finish_install. It seems to be a bug: github.com/jml/undistract-me/issues/23
    – Ludenticus
    Nov 27, 2016 at 2:21
13

Apart from a wrapper like souravc suggested, there isn't really any good way to do this in bash. You can hack your way around it with a DEBUG trap and a PROMPT_COMMAND. A DEBUG trap is triggered whenever you run a command, and PROMPT_COMMAND is run just before the prompt is written.

So stuff for ~/.bashrc becomes something like

trap '_start=$SECONDS' DEBUG
PROMPT_COMMAND='(if (( SECONDS - _start > 15 )); then notify-send "Long running command ended"; fi)'

This is a hack, so don't be surprised if you encounter odd side-effects with this.

5

EDIT

TL;DR: create autocompletion shortcut in .inputrc and function in .bashrc . Run command as usual, type in, but instead of ENTER, press the shortcut that you specified in .inputrc

The person who placed bounty on this question said:

"All of the existing answers require typing an additional command after the command. I want an answer that does this automatically."

While researching the solutions to this problem I've stumbled upon this question from stackexchange, which allows binding CtrlJ to a sequence of commands: Ctrla (move to beginning of line), place "mesure" string in front of the command you entered, Ctrlm (execute)

Thus you get functionality of auto-completion and separate ENTER command for measuring time, while perserving original purpose of the second function i posted bellow.

As of now, here are the contents of my ~/.inputrc file:

"\C-j": "\C-a measure \C-m"

And here are the contents of .bashrc (note , I haven't been using bash in forever - I use mksh as my shell , hence that's what you see in the original post. Functionality is still the same)

PS1=' serg@ubuntu [$(pwd)]
================================
$ '
function measure () 
{

/usr/bin/time --output="/home/xieerqi/.timefile" -f "%e" $@ 

if [ $( cat ~/.timefile| cut -d'.' -f1 ) -gt 15 ]; then

    notify-send "Hi , $@ is done !"

fi


}

Original Post

Here's my idea - use a function in .bashrc. Basic principle - use /usr/bin/time to measure the time it takes for command to complete, and if it is over 15 seconds, send notification.

function measure () 
{

if [ $( /usr/bin/time -f "%e" $@ 2>&1 >/dev/null ) -gt 15 ]; then

    notify-send "Hi , $@ is done !"

fi


}

Here I am redirecting output to /dev/null but to view output, redirecting to file can also be done.

A much better approach, IMHO, is to send output of time to some file in your home folder (just so you don't pollute your system with timefiles, and always know where to look). Here's that second version

function measure () 
{

/usr/bin/time --output=~/.timefile -f "%e" $@ 

if [ $( cat ~/.timefile | cut -d'.' -f1 ) -gt 15 ]; then

    notify-send "Hi , $@ is done !"

fi


}

And here's the screenshots of first and second version, in that order

First version, no output enter image description here

Second version, with output enter image description here

1
  • By the way, works with sudo commands,too. I've tested it with sudo lsof and mostly with ping -c20 google.com. May 4, 2015 at 2:19
4

I've recently built a tool that serves this purpose. It can either be run as a wrapper or automatically with shell integration. Check it out here: http://ntfy.rtfd.io

To install it:

sudo pip install ntfy

To use it as a wrapper:

ntfy done sleep 3

To get notifications automatically, add this to your .bashrc or .zshrc:

eval "$(ntfy shell-integration)"
1

Your script works quite well, just make sure you include the 'shebang' (#!/bin/bash) line. Other ones to #!/bin/bash are mentioned here, but most of the time, #!/bin/bash works fine for Unix bash scripts. It is required by the script interpreter so it know what type of script it is.

This seems to work as a test script:

#!/bin/bash
start=$(date +%s);
   echo Started
   sleep 20;
   echo Finished!
[ $(($(date +%s) - start)) -le 15 ] || notify-send -i dialog-warning-symbolic "Header" "Message"

To modify this script, put the command(s) where the echo and sleep lines are.

Note with notify-send, you can use -i to specify an icon as well :-)

Also, make sure it is executable by running chmod +x /PATH/TO/FILE on it first.

1

You can modify Bash to implement the wrapper functionality you want.

If you're inclined to make many such modifications to your interactive shell, you might consider switching to an intrinsically hackable shell like eshell, which is built using Emacs elisp and has all its storied customizability:

http://www.masteringemacs.org/articles/2010/12/13/complete-guide-mastering-eshell/

-2

You can also do this with a simple if statement like this

#!/bin/bash

START=$(date +%s)

TIME=$(($START+15))

ORIGINAL_COMMAND;

END=$(date +%s)

if [ $END -gt $TIME ]
then
    notify-send "Task Completed"
fi

You can use your own variable names.

I can confirm that this works, I tested with a command that takes a long time to finish and one that does not and the notification comes for the one that takes a long time

You can also do this with any command by replacing ORIGINAL_COMMAND with $@ and running the script as ./script command.

3
  • Why was my answer downvoted? I want to know where i went wrong in the answer
    – Rumesh
    May 3, 2015 at 16:46
  • I didn't downvote it, but I'll tell you what's wrong with it: First of all, it requires typing an additional command. I specifically stated in my bounty I do not want to do that. Second of all, why calc? $((2+2)) is a bash builtin, doesn't require any additional programs. Third: it doesn't work with commands that have spaces. tl;dr: it's worse than the other 1 year old answers May 3, 2015 at 18:44
  • Well..... start and end time alway will be different, right ? Even for a short command that is 1second long Mar 4, 2016 at 23:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .