def main(argv=None):
    """Run the iotile shell tool.

    You can optionally pass the arguments that should be run
    in the argv parameter.  If nothing is passed, the args
    are pulled from sys.argv.

    The return value of this function is the return value
    of the shell command.
    """

    if argv is None:
        argv = sys.argv[1:]

    args = parse_global_args(argv)

    type_system.interactive = True
    line = args.commands

    timeout_thread = None
    timeout_stop_event = threading.Event()

    if args.timeout:
        timeout_thread = threading.Thread(target=timeout_thread_handler, args=(args.timeout, timeout_stop_event))
        timeout_thread.daemon = True
        timeout_thread.start()

    shell = HierarchicalShell('iotile')

    shell.root_add("registry", "iotile.core.dev.annotated_registry,registry")
    shell.root_add("config", "iotile.core.dev.config,ConfigManager")
    shell.root_add('hw', "iotile.core.hw.hwmanager,HardwareManager")

    reg = ComponentRegistry()
    plugins = reg.list_plugins()
    for key, val in plugins.items():
        shell.root_add(key, val)

    finished = False

    try:
        if len(line) > 0:
            finished = shell.invoke(line)
    except IOTileException as exc:
        print(exc.format())
        # if the command passed on the command line fails, then we should
        # just exit rather than drop the user into a shell.
        SharedLoop.stop()
        return 1
    except Exception:  # pylint:disable=broad-except; We need to make sure we always call cmdstream.do_final_close()
        # Catch all exceptions because otherwise we won't properly close cmdstreams
        # since the program will be said to except 'abnormally'
        traceback.print_exc()
        SharedLoop.stop()
        return 1

    # If the user tells us to never spawn a shell, make sure we don't
    # Also, if we finished our command and there is no context, quit now
    if args.quit or finished:
        SharedLoop.stop()
        return 0

    setup_completion(shell)

    # We ended the initial command with a context, start a shell
    try:
        while True:
            try:
                linebuf = input("(%s) " % shell.context_name())

                # Skip comments automatically
                if len(linebuf) > 0 and linebuf[0] == '#':
                    continue
            except KeyboardInterrupt:
                print("")
                continue

            # Catch exception outside the loop so we stop invoking submethods if a parent
            # fails because then the context and results would be unpredictable
            try:
                finished = shell.invoke_string(linebuf)
            except KeyboardInterrupt:
                print("")
                if timeout_stop_event.is_set():
                    break
            except IOTileException as exc:
                print(exc.format())
            except Exception:  #pylint:disable=broad-except;
                # We want to make sure the iotile tool never crashes when in interactive shell mode
                traceback.print_exc()

            if shell.finished():
                break

    # Make sure to catch ^C and ^D so that we can cleanly dispose of subprocess resources if
    # there are any.
    except EOFError:
        print("")
    except KeyboardInterrupt:
        print("")
    finally:
        # Make sure we close any open CMDStream communication channels so that we don't lockup at exit
        SharedLoop.stop()

    # Make sure we cleanly join our timeout thread before exiting
    if timeout_thread is not None:
        timeout_stop_event.set()
        timeout_thread.join()
Run the iotile shell tool.

    You can optionally pass the arguments that should be run
    in the argv parameter.  If nothing is passed, the args
    are pulled from sys.argv.

    The return value of this function is the return value
    of the shell command.