Simple program: Terminal time sheet

An old pocket watch
Published: Sep 1, 2023
Last edit: Sep 1, 2023

Today we want to write a very simple tool using only a few lines of Bash and Python.

With this, I want to show you how in the command line (or terminal), you can do a lot with a little.

Simplicity is king

If you need to give every tool you write a graphical interface, your coding will be time-consuming, and your programs will not be very flexible. When you become comfortable with using the command line for small everyday tasks, your overhead becomes much smaller. The best thing is, for small utilities that just do one task, you don’t even need to write a lot of code!

The task: Keep track of your time

Here’s a simple task for you: Build a small utility that helps you keep track of your time.

More specifically, we want:

  • a one-line way to record entries (each entry consist of date, duration, and action) on the command line
  • a tool that aggregates entries representing the same action for a day
  • a tool to visualize/summarize past entries

Try coming up with a solution yourself! You will learn more than by following mine.

A simple solution

Storage

I opted to keep all the information in a single CSV file. Of course, you could split over multiple files or use a database, but we want to stay as simple as possible. So first, create an empty file timesheet in the home directory.

touch timesheet

For each record we will add later, we want three things: date, period, and action. So we go on by adding a header to our CSV file like:

echo "date,period,action" >> timesheet

Good, we have our central file where we will record all information. Now we want a simple way to do that. It is fair to assume that date will usually be the current date, so we don’t want to write that every time. Instead, we want to add records from any terminal window by just typing something like:

rectime 1:20 reading

to indicate that we have just spent 1 hour and 20 minutes with reading.

To do that, we go to directory $HOME/bin (where I like to keep my small scripts and utilities), and open a new file rectime in our favorite terminal text editor (vim for me, but maybe nano or something else for you).

cd bin
vi  # or nano, or others

Now in the text editor, we add the following to our script:

#!/usr/bin/env bash

echo $(date +%F),$1,${*:2} >> ~/timesheet

What does it do?

The first line (the shebang) says which interpreter to use when running this script. This version should find bash on MacOS and most Linux distros, but you can always adjust it for your system if it doesn’t work.

The other line is were the meat is. The script puts together a string like <current date>,<first argument>,<second argument and all thereafter>. So we if we would run rectime 1:20 read a book on the 1st of September 2023, then the string “2023-09-01,1:20,read a book” would be appended to the file timesheet.

To be able to execute the script, we need to set its permissions. Running sudo chmod 744 rectime in the directory where the script lies, sets read, write, and execution permissions for the owner (you), and read permissions for everyone else on the machine.

We also want to be able to run the script from any directory, so we add the $HOME/bin/ directory to PATH. If you are unsure what that means, check out this introduction to PATH.

And the first part is done! We can record timings from any terminal window on our machine with a dead simple Bash script.

Consolidate records

Now for the second part, consolidating records on a daily basis, we will use Python and the Pandas library. So for example if we have the activity “breakfast” twice on the same day (because we are not on a mission to destroy the One Ring and can actually take time for that), we would want to run a consolidate_timesheet.py script that adds up the durations of 1st and 2nd breakfast to merge them in a single entry.

Again, try to come up with your own solution!

Here’s mine:

#!/Users/julian/miniconda3/envs/pandas/bin/python

import datetime
import sys

import pandas as pd


# parse argument
try:
    # if a valid date is given, use that
    date = datetime.datetime.strptime(sys.argv[1], "%Y-%m-%d")
except IndexError:
    # if no argument is given, use yesterday's date
    date = datetime.datetime.today() - datetime.timedelta(days=1)
except ValueError:
    # if a wrong format is given, exit
    print("Date must be in the format YYYY-MM-DD")
    sys.exit(1)

# Read in the data
df = pd.read_csv("~/timesheet")
# Convert the period column to minutes
df["period"] = df["period"].apply(lambda x: int(x.split(":")[0]) * 60 + int(x.split(":")[1]))
# Convert the date column to datetime
df["date"] = pd.to_datetime(df["date"])
# select only the date we asked for
df_cutout = df.loc[df["date"] == date]
# aggregate duplicates and sum over periods
temp = df_cutout.groupby(["date", "action"]).sum(numeric_only=True).reset_index()
# put that chunk of data back into the original dataframe
df_out = pd.concat([df.loc[df.date < date], temp, df.loc[df.date > date]])
# convert the period column back to a string
df_out["period"] = df_out["period"].apply(lambda x: str(int(x / 60)) + ":" + str(x % 60).zfill(2))
# convert the date column back to a string
df_out["date"] = df_out["date"].dt.strftime("%Y-%m-%d")
# write the output
df_out.to_csv("~/timesheet", index=False)

print("Consolidated timesheet for " + date.strftime("%Y-%m-%d"))

This task is pretty simple, too, the only small difficulty being that we have to deal with the H:MM format that we used to record time. At the top of the script we again find a shebang, but now pointing at a Python environment that contains Pandas. The script itself should be easy to understand with the comments.

Interpret the data

Our last task would be to do something with that gathered data. Now we might go fancy and use Python to create some plots out of that, but I want to stick to something more basic here. Let’s try to come up with a way to display all dates in a month when we did a certain activity - let’s say “meeting” - and also display the time we spent on that. We could go write a script in Python or Bash again, but I want to take this chance to illustrate the power of the terminal and pack all into a one-liner.

Much of the power of the terminal comes from chaining multiple simple commands into a complex pipeline. We will use two different utilities: grep and awk. grep searches an input line by line for matches to a given string and returns matching lines. awk is actually an entire programming language to work on files, but its most common use is probably dealing with data that is organized in columns. To connect the tools, we use a pipe |. This operator passes the output from the previous command as an input to the next command.

Our assembled one-liner is:

grep 2023-08 timesheet | grep meeting | awk -F ',' '{print $1,$2}'

What are we doing here? We take the file timesheet and filter for lines containing “2023-08” for August 2023. Then we filter again, looking for the term “meeting”, and finally, we print only the first two columns of the remaining lines, containing the date and duration of all meetings we recorded in August 2023.

And that’s it! With simple tools we have built a flexible utility we can use and adapt to our needs.

Key points

  • The command line may seem scary in the beginning, but is worth getting to know
  • Even with simple tools, we can build our own customized utilities, exactly how we need them
  • Atomic commands can be chained using operators like the pipe | to build complex programs in a single line

Found that interesting?

Share your experiences with me!

Contact