Last active
December 31, 2025 20:58
-
-
Save cjtheham/3e1ef3534d4b45c272d1846b06adc5df to your computer and use it in GitHub Desktop.
A bash script for tracking my working hours for contract employment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Check if timew is installed, otherwise rest of program fails. | |
| # from: https://stackoverflow.com/questions/592620/how-can-i-check-if-a-program-exists-from-a-bash-script | |
| if ! [ -x "$(command -v timew)" ]; then | |
| echo 'Error: timewarrior (timew) is not installed.' | |
| echo 'Go to https://timewarrior.net/releases/ to install' | |
| exit 1 | |
| fi | |
| usage="Usage: | |
| timesheet [-D] [-r] [-e csv,txt] <tag> <cost> [start_date] [end_date] | |
| Options: | |
| -D Daily mode, separate total times by date. | |
| -e Export mode: CSV, or text to stdout. | |
| -r Round values to the nearest half hour (0.5/1)." | |
| text_color='\033[1;32m' # Bolded green by default | |
| no_color='\033[0m' | |
| daily=false | |
| rounding=false | |
| export_type="txt" | |
| output_file="$(date +%Y-%m-%d).csv" | |
| # Get script arguments with options and positional parameters. | |
| # from: https://stackoverflow.com/a/63421397 | |
| script_args=() | |
| while [ $OPTIND -le "$#" ] | |
| do | |
| if getopts e:f:rD option | |
| then | |
| case $option | |
| in | |
| e) export_type="$OPTARG";; | |
| f) output_file="$OPTARG";; | |
| r) rounding=true;; | |
| D) daily=true;; | |
| esac | |
| else | |
| script_args+=("${!OPTIND}") | |
| ((OPTIND++)) | |
| fi | |
| done | |
| if [ ${#script_args[@]} -lt 2 ]; then | |
| echo "$usage" | |
| else | |
| # Get hours and cost as arguments. | |
| tag=${script_args[0]} | |
| cost=${script_args[1]} | |
| start=$(if ! [ -z "${script_args[2]}" ]; then echo ${script_args[2]}; else echo $(date -d'last monday' +%Y-%m-%dT00:00:00); fi) | |
| end=$(if ! [ -z "${script_args[3]}" ]; then echo ${script_args[3]}; else echo $(date +%Y-%m-%dT%H:%M:%S); fi) | |
| echo "$tag hours from $start to $end" | |
| if [ $daily = true ]; then # Get hours for one job and separate by day. | |
| # Lot of this work came from Juhis (@hamatti@mastodon.world), re-adapted for this workflow. | |
| printf "Daily mode active - getting day by day summary.\n\n" | |
| summary=`timew summary $start - $end $tag` | |
| date=0 | |
| idx=0 | |
| declare -a report | |
| while read -r line; do | |
| timestamps=$(awk -- 'BEGIN{print gsub(ARGV[2], "&", ARGV[1])}' "$line" '[0-9]+:[0-9]+:[0-9]+') | |
| if [[ $(echo $line | cut -d' ' -f 2 ) =~ [0-9]{4}-[0-9]{2}-[0-9]{2} ]]; then | |
| date=$(echo $line | cut -d' ' -f 2 ) | |
| fi | |
| if [[ $timestamps -eq 4 ]]; then | |
| daily_total=$(echo $line | rev | cut -d' ' -f 1 | rev) | |
| report[$idx]=$date | |
| idx=$((idx+1)) | |
| report[$idx]=$daily_total | |
| idx=$((idx+1)) | |
| fi | |
| done <<< "$summary" | |
| idx=0 | |
| declare -a formatted | |
| for key in "${!report[@]}" | |
| do | |
| if [[ $(( $key % 2)) -eq 0 ]]; then | |
| formatted[$idx]="${report[$key]}" | |
| else | |
| minutes=$(echo "${report[$key]}" | cut -d':' -f2) | |
| hours=$(echo "${report[$key]}" | cut -d':' -f1) | |
| if [[ $rounding = true ]]; then | |
| if [[ $minutes -lt 15 ]]; then | |
| formatted[$idx]="$hours.0" | |
| elif [[ $minutes -lt 45 ]]; then | |
| formatted[$idx]="$hours.5" | |
| else | |
| formatted[$idx]="$((hours+1)).0" | |
| fi | |
| else | |
| formatted[$idx]=$(bc <<< "scale=1; $hours + ($minutes/60)") | |
| fi | |
| fi | |
| idx=$((idx+1)) | |
| done | |
| idx=0 | |
| total=0 | |
| total_cost=0 | |
| case "$export_type" in | |
| txt) | |
| # This should be a markdown table style format if in txt. | |
| printf "| Date | Hours | Cost |\n" | |
| echo "|------|-------|------|" | |
| for key in "${!formatted[@]}" | |
| do | |
| if [[ $(( $key % 2)) -eq 0 ]]; then | |
| echo -n "| ${formatted[$key]} |" | |
| else | |
| daily_cost=$(bc <<< "scale=2;${formatted[$key]} * $cost") | |
| printf " %.1f | $%.2f |\n" ${formatted[$key]} $daily_cost | |
| total=$(bc <<< "scale=1;$total+${formatted[$key]}") | |
| total_cost=$(bc <<< "scale=1;$total_cost+$daily_cost") | |
| fi | |
| done | |
| printf "| Total | %.1f | $%.2f |\n" $total $total_cost | |
| ;; | |
| csv) | |
| # Write to a CSV file if set to CSV. | |
| printf "Date,Hours,Cost\n" >> $output_file | |
| for key in "${!formatted[@]}" | |
| do | |
| if [[ $(( $key % 2)) -eq 0 ]]; then | |
| printf "${formatted[$key]}," >> $output_file | |
| else | |
| daily_cost=$(bc <<< "scale=2;${formatted[$key]} * $cost") | |
| printf "%.1f,$%.2f\n" ${formatted[$key]} $daily_cost >> $output_file | |
| total=$(bc <<< "scale=1;$total+${formatted[$key]}") | |
| fi | |
| done | |
| total_cost=$(bc <<< "scale=2;$total * $cost") | |
| printf "Total,%.1f,$%.2f\n" $total $total_cost >> $output_file | |
| echo "Exported to $output_file" | |
| ;; | |
| esac | |
| else # Get hours for one job, and output with cost for current week. | |
| # Output time for past week. | |
| # $start - $end by default replicates the behavior of `:week` which gets the latest Monday. | |
| time=$(timew summary $start - $end $tag | grep "." | tail -1) | |
| time="${time// /}" | |
| # Validate that time was a proper output - this will fail if there is no time, if a bad tag is given, etc. | |
| if ! [[ $time =~ ^[0-9]{1,2}:[0-9]{2}:[0-9]{2}$ ]]; then | |
| echo "No data found for tag $tag within the given dates." | |
| exit 1 | |
| fi | |
| # Split into hours, minutes, seconds (0, 1, 2). | |
| # https://www.geeksforgeeks.org/linux-unix/shell-script-to-split-a-string/ | |
| IFS=':' | |
| read -ra timearr <<< "$time" | |
| hours=${timearr[0]} | |
| minutes=${timearr[1]} | |
| # seconds=${timearr[2]} # For my current purposes, I don't care about seconds... | |
| # Now, I want to convert hours and minutes into one value. | |
| final_time=0 | |
| if [[ $rounding = true ]]; then | |
| # This rounding function is a nightmare but it works so I'm leaving it for now... | |
| final_time=$(bc <<< "scale=1; a=$minutes/60; if(a >= 0.5 && a < 0.8) { b=0.5 } else { if(a >= 0.8) {b = 1} else {if (a >= 0.25 && a < 0.5) {b = 0.5} else {b = 0}}}; $hours + b") | |
| else | |
| final_time=$(bc <<< "scale=1; $hours + ($minutes/60)") | |
| fi | |
| # Lastly, calculate cost for the time period. | |
| final_cost=$(bc <<< "scale=3; $final_time * $cost") | |
| printf "Hours worked: %.1f\n" $final_time | |
| printf "Final cost: $%.2f x %.1f = ${text_color}$%.2f${no_color} \n" $cost $final_time $final_cost | |
| fi | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment