Skip to content

Instantly share code, notes, and snippets.

@cjtheham
Last active December 31, 2025 20:58
Show Gist options
  • Select an option

  • Save cjtheham/3e1ef3534d4b45c272d1846b06adc5df to your computer and use it in GitHub Desktop.

Select an option

Save cjtheham/3e1ef3534d4b45c272d1846b06adc5df to your computer and use it in GitHub Desktop.
A bash script for tracking my working hours for contract employment.
#!/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