diff --git a/tools/pyboard.py b/tools/pyboard.py
index d906235066be577e9e1ae990233cdcc26940e240..c32fb002ca6ec59e9b757228038ed36aa5d932f6 100755
--- a/tools/pyboard.py
+++ b/tools/pyboard.py
@@ -4,7 +4,7 @@
 #
 # The MIT License (MIT)
 #
-# Copyright (c) 2014-2016 Damien P. George
+# Copyright (c) 2014-2019 Damien P. George
 # Copyright (c) 2017 Paul Sokolovsky
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -363,8 +363,8 @@ class Pyboard:
         ret = ret.strip()
         return ret
 
-    def exec_(self, command):
-        ret, ret_err = self.exec_raw(command)
+    def exec_(self, command, data_consumer=None):
+        ret, ret_err = self.exec_raw(command, data_consumer=data_consumer)
         if ret_err:
             raise PyboardError('exception', ret, ret_err)
         return ret
@@ -378,6 +378,52 @@ class Pyboard:
         t = str(self.eval('pyb.RTC().datetime()'), encoding='utf8')[1:-1].split(', ')
         return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])
 
+    def fs_ls(self, src):
+        cmd = "import uos\nfor f in uos.ilistdir(%s):\n" \
+            " print('{:12} {}{}'.format(f[3]if len(f)>3 else 0,f[0],'/'if f[1]&0x4000 else ''))" % \
+            (("'%s'" % src) if src else '')
+        self.exec_(cmd, data_consumer=stdout_write_bytes)
+
+    def fs_cat(self, src, chunk_size=256):
+        cmd = "with open('%s') as f:\n while 1:\n" \
+            "  b=f.read(%u)\n  if not b:break\n  print(b,end='')" % (src, chunk_size)
+        self.exec_(cmd, data_consumer=stdout_write_bytes)
+
+    def fs_get(self, src, dest, chunk_size=256):
+        self.exec_("f=open('%s','rb')\nr=f.read" % src)
+        with open(dest, 'wb') as f:
+            while True:
+                data = bytearray()
+                self.exec_("print(r(%u))" % chunk_size, data_consumer=lambda d:data.extend(d))
+                assert data.endswith(b'\r\n\x04')
+                data = eval(str(data[:-3], 'ascii'))
+                if not data:
+                    break
+                f.write(data)
+        self.exec_("f.close()")
+
+    def fs_put(self, src, dest, chunk_size=256):
+        self.exec_("f=open('%s','wb')\nw=f.write" % dest)
+        with open(src, 'rb') as f:
+            while True:
+                data = f.read(chunk_size)
+                if not data:
+                    break
+                if sys.version_info < (3,):
+                    self.exec_('w(b' + repr(data) + ')')
+                else:
+                    self.exec_('w(' + repr(data) + ')')
+        self.exec_("f.close()")
+
+    def fs_mkdir(self, dir):
+        self.exec_("import uos\nuos.mkdir('%s')" % dir)
+
+    def fs_rmdir(self, dir):
+        self.exec_("import uos\nuos.rmdir('%s')" % dir)
+
+    def fs_rm(self, src):
+        self.exec_("import uos\nuos.remove('%s')" % src)
+
 # in Python2 exec is a keyword so one must use "exec_"
 # but for Python3 we want to provide the nicer version "exec"
 setattr(Pyboard, "exec", Pyboard.exec_)
@@ -390,6 +436,54 @@ def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', pas
     pyb.exit_raw_repl()
     pyb.close()
 
+def filesystem_command(pyb, args):
+    def fname_remote(src):
+        if src.startswith(':'):
+            src = src[1:]
+        return src
+    def fname_cp_dest(src, dest):
+        src = src.rsplit('/', 1)[-1]
+        if dest is None or dest == '':
+            dest = src
+        elif dest == '.':
+            dest = './' + src
+        elif dest.endswith('/'):
+            dest += src
+        return dest
+
+    cmd = args[0]
+    args = args[1:]
+    try:
+        if cmd == 'cp':
+            srcs = args[:-1]
+            dest = args[-1]
+            if srcs[0].startswith('./') or dest.startswith(':'):
+                op = pyb.fs_put
+                fmt = 'cp %s :%s'
+                dest = fname_remote(dest)
+            else:
+                op = pyb.fs_get
+                fmt = 'cp :%s %s'
+            for src in srcs:
+                src = fname_remote(src)
+                dest2 = fname_cp_dest(src, dest)
+                print(fmt % (src, dest2))
+                op(src, dest2)
+        else:
+            op = {'ls': pyb.fs_ls, 'cat': pyb.fs_cat, 'mkdir': pyb.fs_mkdir,
+                'rmdir': pyb.fs_rmdir, 'rm': pyb.fs_rm}[cmd]
+            if cmd == 'ls' and not args:
+                args = ['']
+            for src in args:
+                src = fname_remote(src)
+                print('%s :%s' % (cmd, src))
+                op(src)
+    except PyboardError as er:
+        print(str(er.args[2], 'ascii'))
+        pyb.exit_raw_repl()
+        pyb.close()
+        sys.exit(1)
+
 def main():
     import argparse
     cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.')
@@ -400,6 +494,7 @@ def main():
     cmd_parser.add_argument('-c', '--command', help='program passed in as string')
     cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available')
     cmd_parser.add_argument('--follow', action='store_true', help='follow the output after running the scripts [default if no scripts given]')
+    cmd_parser.add_argument('-f', '--filesystem', action='store_true', help='perform a filesystem action')
     cmd_parser.add_argument('files', nargs='*', help='input files')
     args = cmd_parser.parse_args()
 
@@ -411,7 +506,7 @@ def main():
         sys.exit(1)
 
     # run any command or file(s)
-    if args.command is not None or len(args.files):
+    if args.command is not None or args.filesystem or len(args.files):
         # we must enter raw-REPL mode to execute commands
         # this will do a soft-reset of the board
         try:
@@ -436,6 +531,11 @@ def main():
                 stdout_write_bytes(ret_err)
                 sys.exit(1)
 
+        # do filesystem commands, if given
+        if args.filesystem:
+            filesystem_command(pyb, args.files)
+            args.files.clear()
+
         # run the command, if given
         if args.command is not None:
             execbuffer(args.command.encode('utf-8'))
@@ -450,7 +550,7 @@ def main():
         pyb.exit_raw_repl()
 
     # if asked explicitly, or no files given, then follow the output
-    if args.follow or (args.command is None and len(args.files) == 0):
+    if args.follow or (args.command is None and not args.filesystem and len(args.files) == 0):
         try:
             ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes)
         except PyboardError as er: