$ git clone http://gcodetool.ion.nu/gcodetool.git
commit bcf7333a0b0f67a0d5d20d17d3f87d27c370c4db
Author: Alicia <...>
Date:   Tue Jun 11 17:54:25 2019 +0000

    Initial commit.

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a85adc3
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+CFLAGS=-Wall -g3
+gcodetool: main.o gcode.o mirror.o move.o info.o
+ $(CC) $^ -lm -o $@
+static: CC+=-static
+static: gcodetool
diff --git a/gcode.c b/gcode.c
new file mode 100644
index 0000000..bf2d6d9
--- /dev/null
+++ b/gcode.c
@@ -0,0 +1,187 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2018-2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <math.h>
+#include "gcode.h"
+#define BLOCK 1024
+
+int getprecision(double v) // For printing doubles without excess zeros, use with format %.*f and macro pd(double)
+{
+  char buf[snprintf(0,0,"%f",v)+1];
+  sprintf(buf,"%f",v);
+  char* dot=strchr(buf,'.');
+  if(!dot){dot=strchr(buf,',');}
+  if(!dot){return 0;}
+  int len=strlen(dot)-1;
+  while(dot[len]=='0'){--len;}
+// printf("Precision of %f: %i\n", v, len);
+  return len;
+}
+
+struct gcode* gcode_read(const char* filename)
+{
+  FILE* in;
+  if(filename && strcmp(filename, "-"))
+  {
+    in=fopen(filename, "r");
+// printf("fopen(%s): %p\n", filename, in);
+  }else{in=stdin;}
+  if(!in){return 0;}
+  struct gcode* gcode=malloc(sizeof(struct gcode));
+  gcode->code=0;
+  gcode->pos=0;
+  gcode->size=0;
+  while(!feof(in))
+  {
+    gcode->size+=1024;
+    gcode->code=realloc(gcode->code, gcode->size);
+    int r;
+    if((r=fread(&gcode->code[gcode->size-1024], 1, 1024, in))<1024)
+    {
+      if(r<1 && !feof(in)){perror("read"); return 0;}
+      gcode->size-=1024-r;
+    }
+  }
+  return gcode;
+}
+
+int gcode_write(struct gcode* gcode, const char* filename);
+
+double gcode_readnumber(struct gcode* gcode)
+{
+  char buf[1024];
+  unsigned int pos=0;
+  while(gcode->pos<gcode->size && pos<1023 && memchr("0123456789.-\r\n\t ", gcode->code[gcode->pos], 16))
+  {
+    if(memchr("\r\n\t ", gcode->code[gcode->pos], 4)){++gcode->pos; continue;}
+    buf[pos++]=gcode->code[gcode->pos++];
+  }
+  buf[pos]=0;
+  return strtod(buf, 0);
+}
+
+int gcode_seekcmd(struct gcode* gcode, char cmd)
+{
+  if(gcode->pos>=gcode->size){return 0;}
+// TODO: skip text commands
+  cmd=toupper(cmd);
+  char comment=0;
+  while(toupper(gcode->code[gcode->pos])!=cmd || comment)
+  {
+    if(gcode->code[gcode->pos]==';'){comment=1;}
+    ++gcode->pos;
+    if(gcode->pos>=gcode->size){return 0;}
+    if(gcode->code[gcode->pos]=='\n'){comment=0;}
+  }
+  return 1;
+}
+
+double gcode_getparam(struct gcode* gcode, char param)
+{
+// TODO: skip text commands
+  param=toupper(param);
+  unsigned int pos=gcode->pos;
+  char comment=0;
+  while(toupper(gcode->code[gcode->pos])!=param || comment)
+  {
+    if(gcode->code[gcode->pos]==';'){comment=1;}
+    ++gcode->pos;
+    if(gcode->pos>=gcode->size){gcode->pos=pos; return nan("");}
+    if(strchr(GCODE_KEYS, toupper(gcode->code[gcode->pos]))){gcode->pos=pos; return nan("");} // Hit next command
+    if(gcode->code[gcode->pos]=='\n'){comment=0;}
+  }
+  ++gcode->pos;
+  double num=gcode_readnumber(gcode);
+  gcode->pos=pos;
+  return num;
+}
+
+void gcode_nextline(struct gcode* gcode)
+{
+  while(gcode->pos<gcode->size && gcode->code[gcode->pos]!='\n'){++gcode->pos;}
+  ++gcode->pos;
+}
+
+const struct gcommand* gcode_readcommand(struct gcode* gcode)
+{
+  // Find command
+  while(gcode->pos<gcode->size && !strchr(GCODE_KEYS, toupper(gcode->code[gcode->pos])))
+  {
+    if(gcode->code[gcode->pos]==';'){gcode_nextline(gcode); continue;} // Skip comments
+    ++gcode->pos;
+  }
+  if(gcode->pos>=gcode->size){return 0;}
+  static struct gcommand cmd={0,0,0,0,{0}};
+  switch(cmd.cmdtype) // I suppose functionally this doesn't make a difference, but it might if more types come up
+  {
+    case GCODE_CMD_PARAM: free(cmd.params); break;
+    case GCODE_CMD_STR: free(cmd.string); break;
+  }
+  cmd.key=toupper(gcode->code[gcode->pos]);
+  ++gcode->pos;
+  cmd.num=gcode_readnumber(gcode);
+  if(cmd.key=='M' && (cmd.num==117 || cmd.num==118))
+  {
+    cmd.cmdtype=GCODE_CMD_STR;
+    unsigned int start=gcode->pos;
+    gcode_nextline(gcode);
+    cmd.paramcount=gcode->pos-start-1;
+    cmd.string=strndup(&gcode->code[start], cmd.paramcount);
+    return &cmd;
+  }
+  cmd.cmdtype=GCODE_CMD_PARAM;
+  cmd.paramcount=0;
+  cmd.params=0;
+  int paramsize=0;
+  // readnumber should dump us right at the next command or parameter
+  while(gcode->pos<gcode->size && !strchr(GCODE_KEYS, toupper(gcode->code[gcode->pos])))
+  {
+    if(gcode->code[gcode->pos]==';'){gcode_nextline(gcode); continue;} // Skip comments
+    ++cmd.paramcount;
+    if(cmd.paramcount>paramsize)
+    {
+      cmd.params=realloc(cmd.params, sizeof(struct gparam)*(paramsize+=10));
+    }
+    cmd.params[cmd.paramcount-1].key=toupper(gcode->code[gcode->pos]);
+    ++gcode->pos;
+    cmd.params[cmd.paramcount-1].num=gcode_readnumber(gcode);
+  }
+  return &cmd;
+}
+
+double gcommand_getparam(const struct gcommand* cmd, char key)
+{
+  if(cmd->cmdtype!=GCODE_CMD_PARAM){return nan("");}
+  unsigned int i;
+  for(i=0; i<cmd->paramcount; ++i)
+  {
+    if(cmd->params[i].key==key){return cmd->params[i].num;}
+  }
+  return nan("");
+}
+
+int gcode_fwrite(struct gcode* gcode, FILE* out);
+int gcode_writeuntil(struct gcode* gcode, char cmd, FILE* out);
+void gcode_free(struct gcode* gcode)
+{
+  free(gcode->code);
+  free(gcode);
+}
diff --git a/gcode.h b/gcode.h
new file mode 100644
index 0000000..65f6560
--- /dev/null
+++ b/gcode.h
@@ -0,0 +1,61 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2018-2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#define GCODE_KEYS "GMNT"
+struct gcode
+{
+  char* code;
+  unsigned int size;
+  unsigned int pos;
+};
+struct gparam
+{
+  char key;
+  double num;
+};
+enum gcode_cmdtype
+{
+  GCODE_CMD_PARAM=0,
+  GCODE_CMD_STR
+};
+struct gcommand
+{
+  char key;
+  int num;
+  enum gcode_cmdtype cmdtype;
+  unsigned int paramcount;
+  union
+  {
+    struct gparam* params;
+    char* string;
+  };
+};
+
+#define pd(d) getprecision(d), d
+extern int getprecision(double v);
+
+#define gcode_reset(gc) (gc)->pos=0
+extern struct gcode* gcode_read(const char* filename);
+extern int gcode_write(struct gcode* gcode, const char* filename);
+extern double gcode_readnumber(struct gcode* gcode);
+extern int gcode_seekcmd(struct gcode* gcode, char cmd);
+extern double gcode_getparam(struct gcode* gcode, char param);
+extern const struct gcommand* gcode_readcommand(struct gcode* gcode);
+extern double gcommand_getparam(const struct gcommand* cmd, char key);
+extern int gcode_fwrite(struct gcode* gcode, FILE* out);
+extern int gcode_writeuntil(struct gcode* gcode, char cmd, FILE* out);
+extern void gcode_free(struct gcode* gcode);
diff --git a/info.c b/info.c
new file mode 100644
index 0000000..36bca32
--- /dev/null
+++ b/info.c
@@ -0,0 +1,195 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+#include "gcode.h"
+
+struct minmax
+{
+  double min;
+  double max;
+  double last;
+  char rel;
+};
+void minmax(struct minmax* mm, double now)
+{
+//  printf("minmax({min: %f, max: %f, last: %f, rel: %s}, now: %f)\n", mm->min, mm->max, mm->last, mm->rel?"True":"False", now);
+  if(mm->rel)
+  {
+    if(mm->last==now){return;} // Unchanged
+    now-=mm->last;
+    mm->last+=now;
+  }else{
+    mm->last=now;
+  }
+  if(isnan(mm->min) || now<mm->min){mm->min=now;}
+  if(isnan(mm->max) || now>mm->max){mm->max=now;}
+}
+
+int gtool_info(const char* filename)
+{
+  struct gcode* gcode=gcode_read(filename);
+  if(!gcode){return -1;}
+  gcode_reset(gcode);
+  struct minmax layer={nan(""),nan(""),0,1};
+  struct minmax x={nan(""),nan(""),0,0};
+  struct minmax y={nan(""),nan(""),0,0};
+  struct minmax z={0,nan(""),0,0};
+  struct minmax e={nan(""),nan(""),0,0};
+  struct minmax emove={nan(""),nan(""),0,0}; // Printing moves
+  struct minmax retraction={nan(""),nan(""),0,1};
+  const struct gcommand* cmd;
+  double zpos=nan("");
+  char tty=isatty(fileno(stdout));
+  double* tooltemps=0;
+  unsigned int tooltempcount=0;
+  double* bedtemps=0;
+  unsigned int bedtempcount=0;
+  // Loop through gcode and analyze commands
+  while((cmd=gcode_readcommand(gcode)))
+  {
+    // Show progress if outputting to a tty
+    if(tty){printf("%.2f%%\r", (double)gcode->pos*100/(double)gcode->size);}
+    if(cmd->key=='G' && cmd->num<=1)
+    {
+      double xval=gcommand_getparam(cmd, 'X');
+      double yval=gcommand_getparam(cmd, 'Y');
+      double zval=gcommand_getparam(cmd, 'Z');
+      double eval=gcommand_getparam(cmd, 'E');
+      char move=(!isnan(xval) || !isnan(yval));
+      if(!isnan(zval)){zpos=zval;} // Take things like 'lift head' into account, don't treat as Z until there's a positive E
+      // Printing line length
+      if(!isnan(eval) && move && eval>0)
+      {
+        double xdist=x.last-(isnan(xval)?x.last:xval);
+        double ydist=y.last-(isnan(yval)?y.last:yval);
+        double dist=sqrt(xdist*xdist+ydist*ydist);
+// printf("Print distance: %f\n", dist);
+        minmax(&emove, dist);
+        // Layer heights
+        if(!isnan(zpos))
+        {
+          minmax(&layer, zpos);
+          zpos=nan("");
+        }
+      }
+      // Basic XYZ
+      if(!isnan(xval)){minmax(&x, xval);}
+      if(!isnan(yval)){minmax(&y, yval);}
+      if(!isnan(zval)){minmax(&z, zval);}
+      if(!move && isnan(zval) && !isnan(eval) && (retraction.min<0 || eval<e.last)) // First retraction must be E-n
+      {
+        static double eval_=nan(""); // Buffer to ignore the last E motion, which often isn't indicative of general retractions
+        if(!isnan(eval_))
+        {
+          // TODO: count number of retractions?
+          minmax(&retraction, eval_);
+        }
+        eval_=eval;
+        if(eval<e.last){retraction.last=e.last;} // Count retraction from the last extrusion position
+      }
+      if(!isnan(eval)){minmax(&e, eval);}
+    }
+    else if(cmd->key=='G' && cmd->num==92) // Set current value
+    {
+      double xval=gcommand_getparam(cmd, 'X');
+      double yval=gcommand_getparam(cmd, 'Y');
+      double zval=gcommand_getparam(cmd, 'Z');
+      double eval=gcommand_getparam(cmd, 'E');
+      if(!isnan(xval)){x.last=xval;}
+      if(!isnan(yval)){y.last=yval;}
+      if(!isnan(zval)){z.last=zval;}
+      if(!isnan(eval)){e.last=eval;}
+    }
+    else if(cmd->key=='M' && (cmd->num==140 || cmd->num==190 || cmd->num==104 || cmd->num==109))
+    {
+      double val=gcommand_getparam(cmd, 'S');
+      if(cmd->num>110) // Bed is 140 and 190, tool is 104 and 109 ('set' and 'wait for'/'set+wait for')
+      {
+        if(bedtempcount>0 && bedtemps[bedtempcount-1]==val){continue;}
+        bedtemps=realloc(bedtemps, sizeof(double)*(++bedtempcount));
+        bedtemps[bedtempcount-1]=val;
+      }else{
+        if(tooltempcount>0 && tooltemps[tooltempcount-1]==val){continue;}
+        tooltemps=realloc(tooltemps, sizeof(double)*(++tooltempcount));
+        tooltemps[tooltempcount-1]=val;
+      }
+    }
+  }
+  gcode_free(gcode);
+  // Write report
+  unsigned int i;
+  printf("Maximum print move distance: %.*f\n", pd(emove.max));
+  printf("Minimum print move distance: %.*f\n", pd(emove.min));
+  printf("Bed temperature(s): ");
+  for(i=0; i<bedtempcount; ++i)
+  {
+    if(i==bedtempcount-1 && bedtemps[i]==0){break;}
+    printf("%s%.*f°C", (i>0)?", ":"", pd(bedtemps[i]));
+  }
+  if(i>0 && i==bedtempcount){printf(" (WARNING: This gcode file appears to leave to bed heater on)");}
+  printf("\n");
+  printf("Tool temperature(s): ");
+  for(i=0; i<tooltempcount; ++i)
+  {
+    if(i==tooltempcount-1 && tooltemps[i]==0){break;}
+    printf("%s%.*f°C", (i>0)?", ":"", pd(tooltemps[i]));
+  }
+  if(i>0 && i==tooltempcount){printf(" (WARNING: This gcode file appears to leave to tool heater on)");}
+  printf("\n");
+  printf("Size(X): %.*fmm (%.*f - %.*f)\n", pd(x.max-x.min), pd(x.min), pd(x.max));
+  printf("Size(Y): %.*fmm (%.*f - %.*f)\n", pd(y.max-y.min), pd(y.min), pd(y.max));
+  printf("Size(Z): %.*fmm (%.*f - %.*f)\n", pd(z.max-z.min), pd(z.min), pd(z.max));
+  printf("Maximum layer height: %.*fmm\n", pd(layer.max));
+  printf("Minimum layer height: %.*fmm\n", pd(layer.min));
+  printf("Retraction length: %.*fmm\n", pd(-retraction.min));
+  printf("Un-retraction length: %.*fmm\n", pd(retraction.max));
+  printf("Filament use: %.*fmm\n", pd(e.max-e.min));
+  free(bedtemps);
+  free(tooltemps);
+  return 0;
+}
+
+int gtool_info_arg(int argc, char** argv)
+{
+  const char* filename=0;
+  int i;
+  for(i=2; i<argc; ++i)
+  {
+    if(!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
+    {
+      filename=0; // Setting filename to 0 is a shortcut to avoid having to track the help option, both cases show the help
+      break;
+    }
+    else if(!filename)
+    {
+      filename=argv[i];
+    }
+  }
+  if(!filename)
+  {
+    printf("Usage: %s %s [options] <filename>\n"
+           "Options:\n"
+           "-h/--help     = display this help text\n", argv[0], argv[1]);
+    return -1;
+  }
+  return gtool_info(filename);
+}
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..91b5d6d
--- /dev/null
+++ b/main.c
@@ -0,0 +1,52 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2018-2019  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <string.h>
+extern int gtool_mirror_arg(int argc, char** argv);
+extern int gtool_move_arg(int argc, char** argv);
+extern int gtool_info_arg(int argc, char** argv);
+
+int main(int argc, char** argv)
+{
+  if(argc>2 && !strncmp(argv[1], "mirror", 6) && memchr("xyzXYZ", argv[1][6], 6) && !argv[1][7])
+  {
+    return gtool_mirror_arg(argc, argv);
+  }
+  else if(argc>2 && !strncmp(argv[1], "move", 4) && memchr("xyzXYZ", argv[1][4], 6) && !argv[1][5])
+  {
+    return gtool_move_arg(argc, argv);
+  }
+  else if(argc>2 && !strcmp(argv[1], "info"))
+  {
+    return gtool_info_arg(argc, argv);
+  }else{
+    printf("Usage: %s <subcommand> [options] <file>\n"
+           "Supported subcommands include:\n"
+           "mirror[XYZ]\n"
+           "move[XYZ]\n"
+           "info\n"
+"\nTODO:\n"
+"rotate[XYZ]\n"
+"speed (change speeds for selectable cases like travel, extrude)\n"
+"remove (exclude parts of the object)\n"
+"preview, somehow\n"
+"scale (will also need to scale E)\n", argv[0]);
+    return 1;
+  }
+  return 0;
+}
diff --git a/mirror.c b/mirror.c
new file mode 100644
index 0000000..2a0cfb5
--- /dev/null
+++ b/mirror.c
@@ -0,0 +1,100 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2018  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include "gcode.h"
+
+int gtool_mirror(const char* filename, const char* outfilename, char axis)
+{
+  struct gcode* gcode=gcode_read(filename);
+  if(!gcode){return -1;}
+  printf("Loaded %u bytes of gcode\n", gcode->size);
+  double min=999999999;
+  double max=-999999999;
+  while(gcode_seekcmd(gcode, axis))
+  {
+    ++gcode->pos;
+    double v=gcode_readnumber(gcode);
+    if(v<min){min=v;}
+    if(v>max){max=v;}
+  }
+// printf("%c min: %g\n", axis, min);
+// printf("%c max: %g\n", axis, max);
+  double center=(min+max)/2;
+  printf("%c center/pivot point: %g\n", axis, center);
+  gcode_reset(gcode);
+  // Loop through gcode, cloning most, processing axis coordinates before writing mirrored values
+  FILE* out=fopen(outfilename, "w");
+  unsigned int lastwrite=0;
+  while(gcode_seekcmd(gcode, axis))
+  {
+    ++gcode->pos;
+    fwrite(&gcode->code[lastwrite], 1, gcode->pos-lastwrite, out);
+    double v=gcode_readnumber(gcode);
+    lastwrite=gcode->pos;
+    fprintf(out, "%g ", center*2-v);
+  }
+  // Write the remaining gcode
+  fwrite(&gcode->code[lastwrite], 1, gcode->pos-lastwrite, out);
+  fclose(out);
+  gcode_free(gcode);
+  return 0;
+}
+
+int gtool_mirror_arg(int argc, char** argv)
+{
+  const char* filename=0;
+  const char* outfilename=0;
+  char axis=toupper(argv[1][6]);
+  int i;
+  for(i=2; i<argc; ++i)
+  {
+    if(!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
+    {
+      filename=0; // Setting filename to 0 is a shortcut to avoid having to track the help option, both cases show the help
+      break;
+    }
+    else if(!strcmp(argv[i], "-o") && i+1<argc)
+    {
+      outfilename=argv[++i];
+    }
+    else if(!filename)
+    {
+      filename=argv[i];
+    }else{
+      filename=0;
+      break;
+    }
+  }
+  if(!filename)
+  {
+    printf("Usage: %s %s [options] <filename>\n"
+           "Options:\n"
+           "-o <filename> = leave the input file unchanged and write\n"
+           "                  the changed version to <filename>\n"
+           "-h/--help     = display this help text\n", argv[0], argv[1]);
+    return -1;
+  }
+  if(!outfilename)
+  {
+    outfilename=filename;
+  }
+  if(axis=='Z'){printf("Warning! you have chosen to mirror on the Z axis, which is unlikely to give the desired outcome (for now anyway)\n");}
+  return gtool_mirror(filename, outfilename, axis);
+}
diff --git a/move.c b/move.c
new file mode 100644
index 0000000..9759c75
--- /dev/null
+++ b/move.c
@@ -0,0 +1,93 @@
+/*
+    gcodetool, a utility for analyzing/modifying already sliced parts
+    Copyright (C) 2018  alicia@ion.nu
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include "gcode.h"
+
+int gtool_move(const char* filename, const char* outfilename, char axis, double distance)
+{
+  struct gcode* gcode=gcode_read(filename);
+  if(!gcode){return -1;}
+  printf("Loaded %u bytes of gcode\n", gcode->size);
+  gcode_reset(gcode);
+  // Loop through gcode, cloning most, processing axis coordinates before writing offset values
+  FILE* out=fopen(outfilename, "w");
+  unsigned int lastwrite=0;
+  while(gcode_seekcmd(gcode, axis))
+  {
+    ++gcode->pos;
+    fwrite(&gcode->code[lastwrite], 1, gcode->pos-lastwrite, out);
+    double v=gcode_readnumber(gcode);
+    lastwrite=gcode->pos;
+    fprintf(out, "%g ", v+distance);
+  }
+  // Write the remaining gcode
+  fwrite(&gcode->code[lastwrite], 1, gcode->pos-lastwrite, out);
+  fclose(out);
+  gcode_free(gcode);
+  return 0;
+}
+
+int gtool_move_arg(int argc, char** argv)
+{
+  const char* filename=0;
+  const char* outfilename=0;
+  const char* distancestr=0;
+  char axis=toupper(argv[1][4]);
+  int i;
+  for(i=2; i<argc; ++i)
+  {
+    if(!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
+    {
+      filename=0; // Setting filename to 0 is a shortcut to avoid having to track the help option, both cases show the help
+      break;
+    }
+    else if(!strcmp(argv[i], "-o") && i+1<argc)
+    {
+      outfilename=argv[++i];
+    }
+    else if(!filename)
+    {
+      filename=argv[i];
+    }
+    else if(!distancestr)
+    {
+      distancestr=argv[i];
+    }else{
+      filename=0;
+      break;
+    }
+  }
+  if(!filename || !distancestr)
+  {
+    printf("Usage: %s %s [options] <filename> <distance (mm)>\n"
+           "Options:\n"
+           "-o <filename> = leave the input file unchanged and write\n"
+           "                  the changed version to <filename>\n"
+           "-h/--help     = display this help text\n", argv[0], argv[1]);
+    return -1;
+  }
+  if(!outfilename)
+  {
+    outfilename=filename;
+  }
+  double distance=strtod(distancestr, 0);
+  return gtool_move(filename, outfilename, axis, distance);
+}