Showing posts with label bash. Show all posts
Showing posts with label bash. Show all posts

Sunday, April 27, 2014

Generating and learning strong passwords

In the wake of the recent catastrophic security vulnerability known as “Heartbleed”, many people have been tasked with thinking of new strong passwords for their online accounts and learning them. I’m not writing about Heartbleed, per se, but suffice it to say that you need to change passwords for any affected sites (after the vulnerability has been patched) and any sites you may have reused those passwords on. What I’m sharing here is an approach to generating and learning strong passwords. There are many approaches to password security, including password managers and using long passphrases instead of simple passwords, but I’m just sharing one approach here.

Humans are not very good at generating random passwords, so it can be helpful to use a proven computer algorithm and then simply work to memorize the password that was generated. We are going to use a bit of Bash scripting with the OS pseudo-random number generator and some basic Unix utilities for this. One of the best ways to memorize a strong, random password is to practice typing it. To help with this, we are also going to use a bit of Bash scripting that will let us type the password repeatedly and check if it is correct.

We could create three different shell scripts for this and keep them somewhere like /usr/local/bin (for system-wide use) or somewhere in your home directory (for personal use). Or, we could define them as functions in /etc/bash.bashrc (for system-wide use) or ~/.bashrc (for personal use). I’ll present them both ways, first as individual scripts and at the end as a series of functions. Putting them in separate script files would make them available from other shells, etc. (If you login to csh and invoke one of the scripts it will simply call Bash to run it. If you defined them as functions they would be unavailable in csh.)

First, here is a one-liner to generate a random password:

cat /dev/urandom | tr -cd "[:graph:]" | head -c 13 && echo

The first part reads from the pseudorandom number generator and passes it to the next part, the tr command removes all characters that are not printable ascii (you could also use "[:alnum:]" to generate an alphanumeric password), head -c takes only the specified number of characters and then terminates the pipeline, and the echo command simply outputs a newline, so that we don't end up with the command prompt being printed on the same line at the end of the password.

Now, we’ll look at a more complete example that takes command line options instead of manually editing our command:

#!/bin/bash
chars="[:graph:]"
length="13"
forbidden=""
for i in $(seq 1 $#); do
    if [[ "${!i}" == "--alnum" ]]; then
        chars="[:alnum:]"
    elif [[ "${!i}" == "--length" ]]; then
        ((n=$i+1))
        length="${!n}"
    elif [[ "${!i}" == "--forbidden" ]]; then
        ((n=$i+1))
        forbidden="${!n}"
    fi
done
cat /dev/urandom | tr -cd "$chars" | tr -d "$forbidden" | head -c "$length"
echo


This script takes several arguments. The --alnum argument limits the password to alphanumeric characters rather than printable ascii. The --length option is followed by the number of characters to generate and --forbidden is an additional list of forbidden characters (useful for sites that accept special characters with a few stated exceptions). The default is 13 characters consisting of printable ascii characters. 13 random ascii characters meets the NIST recommendation for 80 bits of entropy for a strong password (learn more about password strength on Wikipedia).

The for loop here counts the number of arguments passed to the script (stored in $#) and loops over them. The ${!var} notation treats $var as the name of another variable. In other words, if $i is 1, then ${!i} is the same as $1 which is the first argument that was passed to the script. The double parentheses are used to evaluate a mathematical expression. After evaluating the command line arguments, we have essentially the same pipeline we used before. The -d option for tr deletes characters from the input, while -c means to delete everything but the specified characters (the “complement” of the specified character set). So the first tr command removes all of the characters except for printable ascii (or alphanumeric, if specified), the second removes additional characters specified with --forbidden.

Now, on to our password practicing tools. First, we need a way to set the password:

#!/bin/bash
name=${1:-default}
read pw
echo -ne "\033[1A\033[0K"
echo -n $pw | sha512sum | tr -d ' -' > ~/.${name}pwhash


The read command takes input from the user and stores it in a variable named pw. By default, read prints what you are typing to the terminal. We allow it to do so here, so that you can make certain you are typing the password correctly the first time. However, as soon as we have finished typing and hit “enter”, we clear that line so the password is no longer visible. The -n option tells echo not to automatically output a newline at the end, and the -e tells it to interpret escape sequences. The sequence \033[1A moves the cursor up one line, and \033[0K deletes the current line. Rather than storing the password itself, we store a hash of the password for a bit of extra security (hopefully, of course, the machine we are doing this on is already secure, but this is a simple precaution to take). The sha512sum prints a couple of spaces and a hyphen at the end; the tr -d ' -' removes these. This script optionally takes one argument, a name so that you set and practice multiple passwords. The notation ${1:-default} is equivalent to $1 if it is set, otherwise it defaults to default.

Now, we need a way to practice typing the password we set:

#!/bin/bash
name=${1:-default}
read -s pw
userhash=$(echo -n $pw | sha512sum | tr -d ' -')
storedhash=$(cat ~/.${name}pwhash)
if [[ $userhash == $storedhash ]]; then
    echo "Correct"
else
    echo -e "Wrong\a"
fi


This time we used the -s option so that read does not print what you are typing to the terminal. Similar to the first script, this one optionally allows you to specify a name and then compares the hash of the password you type to the one that was previously stored. If they match, it informs you that you have typed the password correctly; if not, it let’s you know it was wrong. The \a is the bell character; it may give an audible alert, or in some cases a visual alert or nothing at all, but it is a nice touch to get your attention when the password is typed incorrectly.

Putting them all into functions is quite simple:

function genpw() {
    chars="[:graph:]"
    length="13"
    forbidden=""
    for i in $(seq 1 $#); do
        if [[ "${!i}" == "--alnum" ]]; then
            chars="[:alnum:]"
        elif [[ "${!i}" == "--length" ]]; then
            ((n=$i+1))
            length="${!n}"
        elif [[ "${!i}" == "--forbidden" ]]; then
            ((n=$i+1))
            forbidden="${!n}"
        fi
    done
    cat /dev/urandom | tr -cd "$chars" | tr -d "$forbidden" | head -c "$length"
    echo
}
function setpw() {
    name=${1:-default}
    read pw
    echo -ne "\033[1A\033[0K"
    echo -n $pw | sha512sum > ~/.${name}pwhash
}
function ppw() {
    name=${1:-default}
    read -s pw
    userhash=$(echo -n $pw | sha512sum)
    storedhash=$(cat ~/.${name}pwhash)
    if [[ $userhash == $storedhash ]]; then
        echo "Correct"
    else
        echo -e "Wrong\a"
    fi
}
function unsetpw() {
    name={1:-default}
    shred -uxn1 ~/.${name}pwhash
}


I added an extra one here to unset the password by removing the hash from your system, although this one is fairly trivial. In addition to learning one handy way to generate and learn strong random passwords, hopefully this little exercise has also given us a look at some handy Unix tools and Bash scripting features. For comparison, I’ve also written a Python version of these scripts.

Friday, March 7, 2014

Batch file processing from the command line (photo-editing, renaming, etc)

One of the convenient things about the command line is being able to easily perform batch operations on a large number of files, things that are systemically applied to each file in the same way, such as replacing all of the spaces in file names with underscores or hyphens, resizing images or converting from .png to .jpg, etc. Here I'm going to present some of the basic tools for performing these kind of operations and you can then put them together in various ways to perform any number of customized tasks. Note that I'm using Bash for the interactive shell (or interpreter) here, and the parameter expansions presented here, although useful, are not portable.

Looping

The most common idiom you will see for iterating over a set of files is using the glob to match a set of files:

for f in /path/*

The * matches any file in /path, so it becomes the list of files in that directory. The variable f becomes each file (or rather, the full path to each file) in turn, and we can use that in the code that follows. The command following the for should begin with do and the loop will continue until it reaches the done command and then repeat for each file. The glob can be used to match part of a file, so we could operate on only the .png files in /path like this:

for img in /path/*.png

In addition to the glob, you can also use other things like brace expansion. For instance, /path/*.{png,jpg,gif} will give you all of the files ending in .png, .jpg, or .gif. If you have a set of pictures with names like DSC_nnnn.JPG, but you only want to work on images 5 through 22, /path/DSC_{0005..0022}.JPG would let you do this. You can also use command substitution to loop over the output of a command by wrapping the command with $() or ``.[1]

Parameter Expansion

Bash parameter expansion provides a really handy way of manipulating file names. Here we will see how to separately get the filename, path, and extension, and substitute one character for another.

Get the base name of a file:

base=${file##*/}

Get the path to a file:

path=${file%/*}

Strip the last extension from a filename:

new=${file%.*}

Strip all extensions from a filename:

new=${file%%.*}

These work by stripping a prefix (# and ##) or suffix (% and %%) from $file. In the first example, the * comes before the / because we are stripping a forward slash and everything that comes before it, while in the latter two examples the * comes after the . because we are stripping a dot and everything that comes after it. The double forms strip the longest match, while the single forms strip the shortest match. But with the latter two examples, beware files and directories with unexpected dots! For instance:

file="/home/user/my.pics/selfie.jpg"
echo ${file%%.*}


will print "/home/user/my",

file="/home/user/web2.0essay"
echo ${file%.*}


will print "/home/user/web2". Errors like these are easier to avoid when you are typing a quick command and know what kind of files you are dealing with. If you are writing a script that might later be used in different context, you must be extra careful that it doesn't break when file and directory names don't conform to your initial expectations.

Replacing spaces in a filename with hyphens:

new=${file// /-}

This form actually does pattern matching. You can also require it to match at the beginning or end by replacing the second forward slash with % or #, respectively. If you leave out the last part, whatever matches the pattern will be replaced by nothing, that is, it will be deleted. In this case, you can also omit the final forward slash. So you could remove only a three character file extension with:

new=${file/%.???}

Or you could remove a two digit prefix with:

new=${file/#[0-9][0-9]}

Resizing Images

For command line image processing, we will be using convert from the imagemagick package. The basic command for resizing an image is:

convert picture.jpg -resize 1232x816 smallerpicture.jpg

Note that this will not necessarily make the image exactly 1232x816, it will make it fit inside a box that is 1232x816; it will not squeeze, stretch, or crop the image to fit that exact size. If you want to distort the image to fit:

convert picture.jpg -resize 1232x816\! smallerpicture.jpg

Distortion, however, is often undesirable and it may be better to crop the image. Suppose you wanted to create a series of 64x64 thumbnails without distorting the images. You could crop them with:

convert picture.jpg -resize 64x64^ -gravity center -extent 64x64 thumbnail.jpg

The ^ means to make the image fill, rather than fit into, the 64x64 box. The extent crops the image to 64x64, and the -gravity center means to center the image when cropping so the thumbnail comes from the center of the original image.

Now suppose that you wanted to shrink a series of larger images, but you don't want to enlarge any that are smaller. You can do that like so:

convert picture.jpg -resize 64x64\> resized.jpg

You will notice that all of the above examples require a file name to be supplied for the output, so if we actually want to use them on a batch of files, we will need to combine them with a loop, like this:

mkdir /path/images/thumbnails
for img in /path/images/*.{jpg,gif,png}
do basename=${img##*/}
name=${basename%.*}
convert "$img" -resize 64x64^ -gravity center -extent 64x64 "/path/images/thumbnails/${name}-thumb.jpg"
done


For each image.jpg this will produce an image-thumb.jpg in /path/images/thumbnails. Note that this will create jpg thumbnails for gif and png images, too. You may also note that I didn't bother to write out a script for this, these commands can easily enough be typed in on the command line for a simple task, as explained in Protip #2. Of course, you could write a script if you will be doing the exact same operation frequently.

Convert Image Types

You will notice that our thumbnail example actually converted any gif or png images to jpg when generating thumbnails. Similarly, it is very simple to convert one image type to another, such as if you wanted to use a series of jpg images to make an animated gif or png (similar to this), you just leave out the resize part:

for img in /path/*.jpg
do convert "$img" "${img%.*}.png"
done

Renaming Files

We saw the basics for this when we introduced parameter expansion, but I'm going to go ahead and show a full example. Suppose that we have some files with spaces in their names and find this annoying when manipulating them from the command line, so we decide to replace the spaces with underscores. But some of the files have hyphens and the resulting "_-_" just doesn't look right, so you want to collapse the spaces surrounding a hyphen. Easy enough, we'll just use two steps:

for f in /path/*
do f1=${f// - /-}
new=${f1// /_}
mv "$f" "$new"

done

But what if the path contains spaces? That would be a problem here, but it's not too hard to get around:

for f in /path/*
do basename=${f##*/}
f1=${basename// - /-}
new=${f1// /_}
mv "$f" "${f%/*}$new"


We simply removed the path first, and added it back at the end.

And More!

You can do many more interesting things on the command line with Bash, and ImageMagick offers a huge selection of image editing features not mentioned here. If you want to learn more, you may want to check out the Bash Reference Manual and Examples of ImageMagick Usage. I also recommend GreyCat's Wiki, which has several resources on Bash, including an introductory guide, pitfalls, FAQ, and quick reference sheet.

Tuesday, February 25, 2014

Protip #2: Using loops from the commandline

Remember that the same things you can do in a shell script, you can also do directly from the command line. This includes looping constructs like for, while, and until. In a shell script you might write:

#!/bin/bash
for f in /home/user/Documents/*; do
    new=${f// /-}
    mv "$f" "$new"
done


to rename all of the files in your Documents folder, replacing spaces with hyphens (note that the parameter expansion, ${f// /-}, is not portable). But for something this simple, you don't need to write a script, you can just type this in on the command line, like this:

for f in /home/user/Documents/*
do new=${f// /-}
mv "$f" "$new"
done


or on one line:

for f in /home/user/Documents/*; do new=${f// /-}; mv "$f" "$new"; done

The indentation is irrelevant to the interpreter, we just use indentation in shell scripts for human readability. Those of you who have worked with a programming language with an interactive toplevel, such as Lisp or Python, may recognize how handy this is. Of course, many of you already know this and probably just went, "Duh!", but someone out there either didn't realize they could use loops from the command line, or just didn't really think of it because we usually see them in scripts. That person, upon reading this, was enlightened.1

Thursday, February 6, 2014

Pop-up and email reminders

Previously, we used remind to calculate the next Thanksgivukkah (co-occurrence of Thanksgiving and Hanukkah). Today, we will learn to use remind for a much more typical use case, birthdays and anniversaries. But we are going to take it a little bit further and write some useful scripts using remind to send us reminders by email and as a pop-up on your desktop. I'm doing this a bit differently here, stringing several things together, so you can look through for the things you want, or you can follow through from beginning to end to get a tutorial that will introduce you not only to the specific tools used here, but also to programming via shell scripts. This tutorial should be simple enough to follow even if you don't have programming experience.

So, what can you find here?
  1. Basic usage of remind for birthdays and anniversaries
  2. Using kdialog, gdialog, and zenity for pop-ups
  3. Sending email from the command line with mail
  4. Checking local mail with Mutt or Thunderbird
  5. Scheduling tasks with cron
  6. Writing simple shell scripts with Bash
Below you will find sections dedicated to remind, the pop-up script, and then the mail script. We will be learning shell scripting through the pop-up and mail script sections. At the end of each, we will look at using cron to run the script automatically on a schedule. Then we'll see how to check local mail with Mutt or Thunderbird, and to forward system messages to your user. Finally, I'll present the complete scripts for review (or for those who just want to skip to the code). Let's dive in and have some fun!

Remind

Creating reminders for birthdays and anniversaries

The first thing we need to do is create a reminder file containing the birthdays and anniversaries. This is plain text file, so use any text editor you are comfortable with. Let us suppose that Justin Smith was born January 31, 1987. To create a reminder for his birthday, we write a line like this:

REM 31 JAN MSG Justin Smith's [ord(year(trigdate())-1987)] Birthday

Whoa! That looks a bit complicated; let's break it down. All reminders start with REM and this one occurs every 31st of January. The MSG means that when the reminder is triggered (on the 31st of January), it will produce a message. You can probably guess what the part in brackets should produce in the final message, but it may still not be quite clear what is happening. Since this section is nested, we will work from the innermost pieces outward. The trigdate() function gives the date on which the reminder was triggered. In 2014, that will be January 31, 2014. The year() function takes just the year portion out of a date, so year(trigdate()) is 2014. Subtracting Justin's birth year gives us 27, which is of course how old he is this year. The ord() function turns a number into an ordinal, that is, 1 becomes 1st, 2 becomes 2nd, 3 becomes 3rd, 27 becomes 27th, etc. Ordinarily, remind is expecting the message to consist simply of words to be printed, the square brackets tell remind that what is inside is code to be evaluated and the result is to be pasted into the message before it's printed.

And so we write a line the same way for each birthday. Anniversaries are, of course, quite similar. This file doesn't have to be in a particular place or have a particular name since we pass it to remind on the command line, so we will just save it somewhere convenient. For our scripts, that file name will be included in our code when we run the remind command.

Getting the reminders for the day, week, or month

If all you give the remind command is the file with your reminders, it simply prints the reminders for today:

remind /path/to/reminders.txt

We can also print a calendar with reminders on it. To print the calendar for the next month, all we need is:

remind -c /path/to/reminders.txt

To print more than one month, just put the desired number after the -c, so to print a calendar for the next 12 month we use:

remind -c12 /path/to/reminders.txt

Now, if we only want a week (or number of weeks) we use a + after -c (but before any number):

remind -c+ /path/to/reminders.txt

These calendars all have "lines" drawn with ascii characters (like hyphens, vertical bars, and plus symbols). We can make them look nicer with smooth, unbroken lines using unicode by adding a u after the -c. So to print a nicer version of the current week reminders:

remind -cu+ /path/to/reminders.txt

To learn more about remind, read the manual with:

man remind

Popups

There are a number of convenient programs that allow you to use graphical dialogs to interact with shell scripts. In this case, we simply want to generate an informational pop-up to remind us of the birthdays and anniversaries today.

I'm using kdialog which comes with KDE because that is the desktop environment I'm currently using. If you are using Gnome or another GTK desktop environment, and your system has gdialog available, the same syntax will work with it. If your system has the newer zenity instead of gdialog, the syntax is slightly different so I'll show that as well.

kdialog --title "Today's Reminders" --msgbox "Justin Smith's 27th birthday"

This example is pretty simple, the string after --title sets the title of the pop-up window, and the string after --msgbox is the message in the pop-up. Of course, when we put this into a script, we'll actually get the reminders from remind, and we will see how to pass that to kdialog (or gdialog or zenity) later.

As noted, gdialog would be the same, but zenity is slightly different:

zenity --title="Today's Reminders" --info --text="Justin Smith's 27th birthday"

Putting it together in a shell script

In this step we are actually writing our own computer program! It's not that scary, though. These will be very short and simple. Shell scripts are essentially a series of commands that you could have typed in at the command line, but they are written in a file that can be run as a program. The first thing we need is a line that tells the system how to run our program, that is, what other program is needed to interpret our commands:

#!/usr/bin/bash

Those first two characters are magic. No really, they represent what is called a magic number which indicates to Unix-like systems that the file contains a script and the path to the interpreter follows on the same line. Whenever the system sees a file starting with #! the next thing it looks for is a path to the program it needs to run the commands in the rest of the file. In this case, we are using Bash which is usually located at /usr/bin/bash.

Now we need to get the reminders for today from remind, and pass them to kdialog (or gdialog or zenity). For convenience, we will store the reminders we get from remind in a variable.

MSG=$(remind /path/to/reminders.txt)

There are two things going on here. The equal sign allows us to use the string on the left later to represent the thing to the right. In Bash, when we use that string later as a reference, we will put a $ in front of it to indicate we are using it as a variable to represent something else.

The next thing happening here is that the $() indicate that we want to run the command inside and substitute the output of the command here. This is called command substitution, because we are substituting the output of the command for the command itself. So later when we reference $MSG, it won't mean "remind /path/to/reminders.txt", it will mean "Justin Smith's 27th birthday". Now our kdialog command looks like:

kdialog --title "Today's Reminders" --msgbox "$MSG"

Now, if it's not anyone's birthday or anniversary, this will produce a pop-up that says, "No reminders." We probably don't want this, so let's add a condition so that it will only create a pop-up if we actually have a reminder. Here is how we do that in Bash:

if [[ "$MSG" != "No reminders." ]]; then
    kdialog --title "Today's Reminders" --msgbox "$MSG"
fi

If you are used to other programming languages like C, C++, JavaScript, Perl, etc, this may look a little strange; if you don't have any programming experience, it probably looks rather complicated. I won't go into all of the details, but let's break down the basics. The if-then part of the construct mirrors standard English usage. The double square brackets indicate that what is inside is some kind of test. As in many programming languages, == is used to test whether two things are equal. In this case, != tests whether two things are not equal. We will only do the things that come after then if the message in $MSG is not "No reminders." If it is, then we will not produce a pop-up. Note that the spacing is important, there are spaces between the brackets and what is inside, and between the != and the two things it is comparing. We end the conditional part of our code with fi (the opposite of if), so if there was any code after that, it would be run regardless of what $MSG turned out to be.

So far we have:

#!/usr/bin/bash
MSG=$(remind /path/to/reminders.txt)
if [[ "$MSG" != "No reminders." ]]; then
    kdialog --title "Today's Reminders" --msgbox "$MSG"
fi

Now, if you run this script from a terminal in your graphical desktop environment, it will work fine. But if you go to a virtual terminal (CTRL + ALT + F1, etc) and run it from there, you will get an error, "cannot connect to X server". The X server is what is responsible for drawing all of those windows, icons, etc that make up your graphical interface. And on a multi-user system, there could be several different displays at once, so it isn't sure what to connect to unless you tell it. Now if there are several users who could be logged into a graphical desktop and the same time using a switch user feature, which display is yours may depend on who logged in first. Since we don't know in advance what it will be, we need a way to find what it is.

There is a useful tool for obtaining the information we need, simply called w. It shows us who is logged into the system and what they are doing, including which X server display they are using. The problem is that it gives us more information than we need, so we will have to cut out just the relevant piece. There are a couple of tools we can use to cut out the parts we want, but before I get into those details, let me introduce you to the Unix pipe. What we need to do here is take the output of one program and pass it on to another (and then pass it's output on to yet another program). All Unix-type systems provide a simple way to do this. It is called a pipe because conceptually we are piping the output of one command directly into the input of the next. The syntax is very simple, we simply join the commands with a | symbol.

The first thing we will do is to find a line that has the information we are looking for. It needs to contain the user name and an X display number (if you log in from a virtual terminal or via ssh, there will be no display number associated with that login). First, let's find only the lines that have an X display number. We will use grep, a tool that has powerful pattern matching abilities and works line by line. Each X display number starts with a colon and a digit. But if we look just for a colon and a digit, we could be picking up lines that don't have an X display number but just have a time formatted with a colon. To avoid that, let's look for space in front of the colon instead of a digit. OK, now that we know what we are looking for in our first step, let's see how we do it:

w | grep "\s:[0-9]"

Without going into all of the details of how grep works, I'll just explain the patterns used here. The \s represents some kind of whitespace, such as a space or tab. The : simply represents a literal colon. The [0-9] looks for anything in the range of 0-9, a digit.

Now the next thing we need to find is lines that begin with the correct username. For this, we will use grep a second time. Just using the username for the second pattern would probably work, but we can protect against some odd cases by using a slightly more complicated pattern:

w | grep "\s:[0-9]" | grep -m1 "^username\s"

We already know that the \s represents whitespace, so this prevents us from picking up a longer username that actually begins with your user name (eg- if your username is jon, you don't want the script to try to send your reminders to a user named jonathan). The ^ means the beginning of the line, so if your user name is ash you won't get a line for some other user running bash, etc. The -m1 option returns only the first matching line. Since all of the lines for a user at this point have the same X display number, simply taking the first will do.

Finally, we need to cut just the X display number out of the line we have selected. We will use a very powerful program called awk (it's so powerful you can write whole scripts with it, but we will only look at what we need here):

w | grep "\s:[0-9]" | grep -m1 "^username\s" | awk '{print $3}'

This bit is simple enough, the $3 represents the third field, where fields are separated by whitespace, and this is the part that awk will print to it's output.

Now, the commands that generate pop-ups automatically look for the X display number in a special kind of variable, an environment variable, called DISPLAY. We set that using a command called export, and we will use command substitution the same way we did when storing the output of remind:

export DISPLAY=$(w | grep "\s:[0-9]" | grep -m1 "^username\s" | awk '{print $3}')

Finally, we have all of the pieces, but I'm going to suggest a couple more tweaks to the remind command we are using. It won't make a difference with what we are doing here, but if you have more advanced reminders in use, the -q option tells remind not to queue timed reminders for later execution and the -g option will sort them by date, time, and priority. So here is what we have now:

#!/usr/bin/bash
export DISPLAY=$(w | grep "\s:[0-9]" | grep -m1 "^username\s" | awk '{print $3}')
MSG=$(remind /path/to/reminders.txt)
if [[ "$MSG" != "No reminders." ]]; then
    kdialog --title "Today's Reminders" --msgbox "$MSG"
fi

Making it run automatically

While the script we have written is certainly handy, we don't want to have to run it every day, the point was to make it automatic. So now we are going to learn how to schedule a command to run on a recurring basis. The scheduler on a Unix-type system is called cron and the configuration file that specifies what commands to run and when is called a crontab. To edit your crontab, just run:

crontab -e

The format of the crontab file is a bit obtuse. Each line begins with a series of characters indicating the minute, hour, day of the month, month, and day of the week for the command to be run on. An asterisk indicates that any value is acceptable. So, to run our pop-up script every day we would use:

0 0 * * * /path/to/reminderpopup.sh

This will run the script at midnight. If we wanted to wait and have it run at 8AM, we would use:

0 8 * * * /path/to/reminderpopup.sh

Mail

Now, let's try a script that generates a calendar of upcoming events and mails them to us. I won't get into configuring your system to send email, but your system is probably configured by default to deliver mail locally, to users on the same system. If your system isn't going to be configured to deliver email to the outside world, or hasn't been yet, you can send mail to user@localhost.

We've already seen how to generate a calendar of the reminders for the coming month, or the current week. It turns out, there is a handy command for sending email which is simply called mail. The body of your message is sent to the command's standard input, so we will use a pipe, just like we did earlier. There is a convenient way to combine the output of several commands so that you can pipe them all into the same command by wrapping the commands in curly braces. So, we can send a reminder email with something like:

{
echo "Here are your weekly reminders:";
remind -c+ /path/to/reminders.txt;
} | mail -s "Weekly reminders" user@localhost

The -s option provides the subject for the email. Now, we could make that calendar look nicer with unicode, but standard email is ascii so if we just throw some unicode in there all we will get is garbage. In order to send unicode email, we need to specify the encoding in a header. We can add an arbitrary header to the email with the -a option. Here is what we need to use a unicode calendar in our email:

{
echo "Here are your weekly reminders:";
remind -cu+ /path/to/reminders.txt;
} | mail -a "Content-type: text/plain; charset=UTF-8" -s "Weekly reminders" user@localhost


This works pretty well already, but it would be nice to include which week the reminders are for in the subject. For this, we would like to find the dates of the first and last days of the week. Again, there is a handy command for working with dates which is simply called date. If run without arguments, it simply prints the current date and time. However, we can tell it to find a different date and we can also specify how to format it.

SUN=$(date --date='last Sunday' +'%B %d')
SAT=$(date --date='next Saturday' +'%B %d')

We have already seen how to store something in a variable and how to get the output of a command. The specifications for the dates are fairly obvious. The + indicates a string used to indicate the format we want the date in, %B is the name of the month and %d is the day of the month.

Now there is one catch with the dates here, if it is run on Saturday or Sunday, that date will be off (it will be last Sunday or next Saturday when what we really want is this Sunday or Saturday). We can avoid this by checking if it is Saturday or Sunday and using the current date if it is:

if [[ $(date +'%a') == 'Sun' ]]; then
    SUN=$(date +'%B %d')
else
    SUN=$(date --date='last Sunday' +'%B %d')
fi
if [[ $(date +'%a') == 'Sat' ]]; then
    SAT=$(date +'%B %d')
else
    SAT=$(date --date='next Saturday' +'%B %d')
fi

Here, the +'%a' requests the current date be formatted simply as the three letter abbreviation for the day of the week. So if today is Sunday, we use today's date for the Sunday on which our calendar begins, otherwise we use last Sunday. We do the same thing with Saturday.

Putting it all together we have:

#!/usr/bin/bash
if [[ $(date +'%a') == 'Sun' ]]; then
    SUN=$(date +'%B %d')
else
    SUN=$(date --date='last Sunday' +'%B %d')
fi
if [[ $(date +'%a') == 'Sat' ]]; then
    SAT=$(date +'%B %d')
else
    SAT=$(date --date='next Saturday' +'%B %d')
fi
{
echo "Here are your weekly reminders:";
remind -cu+ /path/to/reminders.txt;
} | mail -a "Content-type: text/plain; charset=UTF-8" -s "Reminders for the week of $SUN through $SAT" user@localhost

Since this script generates a weekly calendar of reminders (exercise for the interested reader: write a version that mails reminders monthly instead of weekly), we want to make it's crontab entry run the script weekly, each Sunday:

0 0 * * 7 /path/to/remindermail.sh

Checking local mail

Since I didn't get into configuring your system to send mail to the outside world, I will at least give you a quick look at how to check your local mail.

Mutt

You can check local mail from the command line with Mutt. Basic usage is pretty simple, it will take you to a list of messages, you can move up and down the list with the arrow keys and read the highlighted message by pressing "Enter", pressing the letter "q" will return to the list of messages or exit the application. For more information, you can read the manual.

Thunderbird

It's also fairly simple to configure Thunderbird to check your local mail, so you can see your local mail right next to your regular email. Under "Edit", select "Account Settings...", in the Account Settings dialog pull down the "Account Actions" drop down menu and select "Add Other Account..." (yes, it's Other Account, not Mail Account), select "Unix Mailspool (Movemail)" and click "Next", make sure the email address is user@localhost (or the domain name assigned to your machine), continue clicking "Next" (the account name is arbitrary, use whatever you like), and then click "Finish". Thunderbird is now configured to check your local mail, but in order for this to work, your user must be in the mail group. If you aren't in it already, you can add yourself by running this command as root:

usermod -aG mail user

Note that if you do this, it moves your mail from your system mailbox, to Thunderbird's. So once you have checked the mail with Thunderbird, it is no longer available to Mutt. Thunderbird seems to ignore the setting to automatically check mail from a Unix mailspool, you must explicitly click "Get Mail".

Getting system mail

Another handy use for checking local mail is to get important messages about the system. Many system utilities will send mail (which is why many systems like Debian automatically have an MTA installed and configured for local mail). This mail typically goes to root, so if your system isn't already configured to forward this mail to your account, you can do so by simply adding this line to the end of you /etc/aliases:

root: user

Summary

reminderpopup.sh

#!/usr/bin/bash
export DISPLAY=$(w | grep "\s:[0-9]" | grep -m1 "^username\s" | awk '{print $3}')
MSG=$(remind /path/to/reminders.txt)
if [[ "$MSG" != "No reminders." ]]; then
    kdialog --title "Today's Reminders" --msgbox "$MSG"
fi

remindermail.sh

#!/usr/bin/bash
if [[ $(date +'%a') == 'Sun' ]]; then
    SUN=$(date +'%B %d')
else
    SUN=$(date --date='last Sunday' +'%B %d')
fi
if [[ $(date +'%a') == 'Sat' ]]; then
    SAT=$(date +'%B %d')
else
    SAT=$(date --date='next Saturday' +'%B %d')
fi
{
echo "Here are your weekly reminders:";
remind -cu+ /path/to/reminders.txt;
} | mail -a "Content-type: text/plain; charset=UTF-8" -s "Reminders for the week of $SUN through $SAT" user@localhost

crontab

0 8 * * * /path/to/reminderpopup.sh
0 0 * * 7 /path/to/remindermail.sh

Notes

Bash versus generic sh

The scripts here are all written specifically for Bash, not portable shell scripting. All Unix-type systems have a shell, /bin/sh, that conforms to certain standards. Generic, portable shell scripts will work with any of them, but the code I have written here use extra features that Bash offers and so these scripts will only work with Bash. You can do the same things with portable shell scripting. So, if you are trying to write portable shell scripts, you will want to note the differences from what you have seen here.

Debian

I wrote and tested most of this on Debian. If you are using a different distribution, there may be some differences. I appreciate feedback if there is a significant difference affecting users of another distribution, etc.

Feedback

Questions, corrections, and feedback are welcome. You can comment on the post or email me directly. I'll try to answer any questions, but I can't guarantee a time frame. 

Update, 2015-01-12:

The previous version of the popup script broke when updating to Debian 8.0 “Jessie” as the output of w has changed. The following version works on the new version:


#!/bin/bash
export DISPLAY=$(w | grep -m1 "^blue\s\+:[0-9]" | awk '{print $2}')
MSG=$(remind -q -g /home/blue/Documents/FamilyBirthdays-remind.txt)
if [[ "$MSG" != "No reminders." ]]; then
    kdialog --title "Today's Reminders" --msgbox "$MSG"
fi