gpx2tcx Shell Script screenshot
Following on from my gpx2tcx AWK script and Windows batch file, I now present what will probably the last separate component in the series... my Bash shell script.

This script is similar in nature to the Windows equivalent presented earlier, however it is considerably more flexible. Unlike the Windows batch file, this Bash script supports a number of command line arguments that allow its operation to be tweaked more easily.

The script's basic help message is as follows:

Usage: gpx2tcx [options] [basefilename[.gpx|hrm]] ... [basefilename[.gpx|hrm]]

Allowed options: -a meters user meters as the starting altitude if no HRM altitude data present -f overwrite destination file if any exists -h show this help text and exit -s sport set TCX activity type to sport; should be one of "Biking", "Running", or "Other"; if not set, will use "Biking" if cadence data is present, otherwise "Running" -v report this script's version and exit -z offset replace UTZ "Z" timezone suffixes with offset (eg "+11:00")

If called with no arguments, this script will perform essentially the same as the Windows batch file - ie will attempt to convert all GPX files in the current directory that do not yet have corresponding TCX files. However, as you can see, the script allows you to set explicit GPX / HRM files to convert on the command line.

I dare say the -f, -h and -v options are self-explanatory, but I'll take just a second to explain the other two options.

The -s option allows you to specify the sport or TCX activity type. The TCX format includes the concept of an "sport type", which of course, specifies that type of sport the TCX file has recorded data for. According to the TCX schema, valid sport types are: Running, Biking, and Other, so you should probably only use one of those.

As noted in the help message above, if -s not specified, the following default behaviour is used: if a corresponding HRM file is being used, and that HRM file includes cadence information, then the default sport will be Biking, otherwise Running will be used instead. Note, this default behaviour is actually implemented in the AWK script (so exists for the Windows batch file too), this Bash script merely provides an easy way to override that behaviour.

Finally, the -z option may be used to specify the correct timezone to used in place of any UTC timezones found in the GPX / TCX files. This is because although the Polar RCX5 exports data with local timestamps, those timestamps all purport to be UTC instead - which is wrong, obviously. So specifying a timezone is an easy way to have those erroneous UTC timestamps corrected (the Windows batch file has equivalent functionality, but its not quite so easy to use).

So, here's the script:

#!/bin/bash by Paul Colby (, no rights reserved ;)

$Id: 263 2012-02-11 03:16:18Z paul $

function showUsage { SCRIPTNAME=basename $0 .sh echo -e "\nUsage: $SCRIPTNAME [options] [basefilename[.gpx|hrm]] ... [basefilename[.gpx|hrm]]\n" echo 'Allowed options:' echo ' -a meters user meters as the starting altitude if no HRM altitude data present' echo ' -f overwrite destination file if any exists' echo ' -h show this help text and exit' echo ' -s sport set TCX activity type to sport; should be one of "Biking", "Running", or "Other";' echo ' if not set, will use "Biking" if cadence data is present, otherwise "Running"' echo " -v report this script's version and exit" echo ' -z offset replace UTZ "Z" timezone suffixes with offset (eg "+11:00")' echo }

function showVersion { # Note, Subversion will automatically update the following line. SCRIPTVERSION=( 1 0 0 echo '$Revision: 263 $' | sed -e 's/.*: *\([0-9]\+\).*$/\1/' ) echo "${SCRIPTVERSION[0]}.${SCRIPTVERSION[1]}.${SCRIPTVERSION[2]}.${SCRIPT_VERSION[3]}" }

function parseCommandLine { unset ALTITUDE unset FORCE unset SPORT unset TIMEZONE if [ -r ~/".basename $0 .sh" ]; then . ~/".basename $0 .sh"; fi local -a ERRORS while getopts ':a:fhs:vz:' OPTION; do case $OPTION in a) ALTITUDE="$OPTARG" ;; f) FORCE=Y ;; h) showUsage ; exit 0 ;; s) SPORT="$OPTARG" ;; v) showVersion ; exit 0 ;; z) TIMEZONE="$OPTARG" ;; :) ERRORS[${#ERRORS[@]}]="Option -$OPTARG requires an argument." ;; \?) ERRORS[${#ERRORS[@]}]="Option -$OPTARG not known." ;; *) ERRORS[${#ERRORS[@]}]="Option -$OPTION not implemented yet." ;; esac done shift $(( $OPTIND - 1))

if [ ${#ERRORS[@]} -gt 0 ]; then for ERROR in "${ERRORS[@]}"; do echo "$ERROR" >&2 done showUsage ; exit 1 fi

# All positional arguments are [base] filenames. unset $FILENAMES for POSITIONALARGUMENT; do FILENAMES[${#FILENAMES[@]}]=$POSITIONALARGUMENT done

# If no filenames where specified, look for GPX/HRM files in the current directory. if [ ${#FILENAMES[@]} -eq 0 ]; then local PREVIOUSIFS="$IFS" IFS=$'\n' FILENAMES=( find . -name '*.gpx' | sed -e 's/\.[^.]*$//' | sort | uniq ) if [ -z "$FORCE" ]; then # Not forcing output, so ignore any input files that have already have corresponding TCX files. FILENAMES=( $( for NAME in "${FILENAMES[@]}"; do if [ ! -e "$NAME.tcx" ]; then echo "$NAME"; fi; done ) ) fi IFS="$PREVIOUSIFS"

# See if we actually found any files to process.
if [ ${#FILENAMES[@]} -eq 0 ]; then
  echo "Found no files to process."
  exit 0

else # Remove any .gpx/.hrm suffixes from the user-supplied list, and uniquify. local PREVIOUSIFS="$IFS" IFS=$'\n' FILENAMES=( $( for NAME in "${FILENAMES[@]}"; do echo $NAME; done | sed -e 's/.(hrm|gpx)$//' | sort -u ) ) IFS="$PREVIOUSIFS" fi }

function merge { if [ -z "$FORCE" ] && [ -e "$1.tcx" ]; then echo "Skipping \"$1\" - output file (\"$1.tcx\") already exists." >&2 return 0 fi

echo "Processing $1" awk -f "dirname $0/basename $0 shawk" -v ALTITUDE="$ALTITUDE" -v HRMFILE="$1.hrm" -v SPORT="$SPORT" "$1.gpx" > "$1.tcx"

if [ -n "$TIMEZONE" ]; then sed -i -re "s/([>\"][0-9]{4}(-[0-9]{2}){2}T([0-9]{2}:){2}[0-9]{2})Z([<\"])/\1$TIMEZONE\4/g" "$1.tcx" fi }

function mergeAll { for FILENAME in "${FILENAMES[@]}"; do merge "$FILENAME" done }

parseCommandLine "$@" mergeAll

Finally, here's a quick summary of some relevant links:


Just updated the script to include support for the ALTITUDE variable just added to the AWK script.


comments powered by Disqus