def create_cmd_task(
    ctx,
    parts,
    inputs=None,
    outputs=None,
    env=None,
    cwd=None,
    task_name=None,
    cache_key=None,
    always=False,
    add_to_group=True,
):
    """
    Create task that runs given command.

    :param ctx: BuildContext object.

    :param parts: Command parts list.

        Each part can be:
            - **None**: Will be ignored.
            - **String**: Will be used as-is.
            - **Node object**: Will be converted to absolute path.
            - **List of these**: Will be flattened.
            - **Item wrapper of these**: Will be unwrapped. Item marked as
              input or output target will be added to created task.

    :param inputs: Input items list to add to created task.

        Each item can be:
            - **None**: Will be ignored.
            - **Path**: Relative path relative to top directory. Will be
              converted to node.
            - **Node object**: Will be used as-is.
            - **Task object**: Will set created task to run after the task.
            - **List of these**: Will be flattened.
            - **Item wrapper of these**: Will be unwrapped.

    :param outputs: Outputs items list to add to created task.

        Each item can be:
            - **None**: Will be ignored.
            - **Path**: Relative path relative to top directory. Will be
              converted to node.
            - **Node object**: Will be used as-is.
            - **Task object**: Will set the task to run after created task.
            - **List of these**: Will be flattened.
            - **Item wrapper of these**: Will be unwrapped.

    :param env: Environment dict.

        Default is use given context object's environment dict.

    :param cwd: Working directory in which to run given command.

        Default is top directory.

    :param task_name: Task name for display purpose.

        Default is use the caller function's name.

    :param cache_key: Task cache key.

    :param always: Whether mark created task as always run.

    :param add_to_group: Whether add created task to given context.

    :return: Created task.

    :notes:
        It is recommended to refer to files inside project using relative
        paths. This is even required if the files are used as task's input or
        output targets.

        It is not ideal to make relative paths relative to current working
        directory, or relative to `wscript` file, because neither of the two
        choices guarantee a fixed start directory.

        It is not ideal to let `Waf` resolve relative paths neither, because
        the start directory used for resolving relative paths varies with
        different types of command context (i.e. OptionsContext,
        ConfigurationContext, BuildContext, and Context).

        It is best to make relative paths relative to project's top directory.
        To do so, first use `Waf`'s pre-defined path variable `top` to locate
        project's top directory (Notice path variable `top`'s value is relative
        to `wscript` file.). Then `aoikwafutil`'s functions like
        `create_cmd_task` will use path variable `top`'s value to resolve other
        relative paths.

        `create_cmd_task` handles relative paths using rules below:

            - Relative paths marked by `mark_path`, `mark_input`, or
              `mark_output` are relative to top directory.

            - Relative paths passed to argument `parts` are used as-is. They
              are relative to the working directory in which to run the
              command. However, they can be marked to be relative to top
              directory instead.

            - Relative paths passed to argument `inputs` or `outputs` are
              relative to top directory. They need not be marked.
    """
    # Ensure given context object is BuildContext object
    _ensure_build_context(ctx)

    # If task name is not given
    if not task_name:
        # Get the caller function's name
        task_name = inspect.stack()[1][3]

    # Print title
    print_title('Create task: {}'.format(task_name))

    # If cache key is given
    if cache_key:
        # Find cached task
        cached_task = _TASKS_CACHE.get(cache_key, None)

        # If have found cached task
        if cached_task is not None:
            # Print info
            print_text('Use cached task `{}`.'.format(cache_key))

            # Print end title
            print_title('Create task: {}'.format(task_name), is_end=True)

            # Return the cached task
            return cached_task

        # If have not found cached task.
        #     Continue to create task.

    # If cache key is not given.
    #     Continue to create task.

    # Normalized command parts list
    norm_part_s = []

    # Input nodes list
    input_node_s = []

    # Output nodes list
    output_node_s = []

    # Get the first command part that is program path
    first_part = parts[0]

    # If the first command part is list.
    # Notice the first command part can be list if the value is from `ctx.env`,
    # e.g `ctx.env.PYTHON`.
    if isinstance(first_part, list):
        # Use the first item in the list as the first command part
        parts[0] = first_part[0]

    # For given command parts list's each command part
    for part, wrapper_type in _normalize_items(ctx=ctx, items=parts):
        # If the command part is string
        if isinstance(part, str):
            # Add the string to the normalized list
            norm_part_s.append(part)

        # If the command part is node
        elif isinstance(part, Node):
            # Add the node's absolute path to the normalized list
            norm_part_s.append(part.abspath())

            # If the wrapper type is `input`
            if wrapper_type == 'input':
                # Add the node to the input nodes list
                input_node_s.append(part)

            # If the wrapper type is `output`
            elif wrapper_type == 'output':
                # Add the node to the output nodes list
                output_node_s.append(part)

            # If the wrapper type is not `input` or `output`
            else:
                # Do nothing
                pass

        # If the command part is not string or node
        else:
            # Get error message
            msg = 'Error (2W9YD): Command part is not valid: {0}.'.format(part)

            # Raise error
            raise ValueError(msg)

    # If environment dict is not given
    if env is None:
        # Use given context object's environment dict
        env = ctx.env

    # If working directory path is not given
    if cwd is None:
        # Set working directory path be top directory absolute path
        cwd = ctx.top_dir

    # If the working directory path is not absolute path.
    if not os.path.isabs(cwd):
        # Convert the working directory path to absolute path
        cwd = create_node(ctx, cwd).abspath()

    # Create task that runs command
    task = CmdTask(
        # Context object
        ctx=ctx,

        # Command parts list
        parts=norm_part_s,

        # Environment dict
        env=env,

        # Working directory
        cwd=cwd,

        # Task name
        task_name=task_name,
    )

    # For each input or output item.
    # Notice the code structure for handling input and output items are the
    # same so use the same code with different variables to avoid duplicate
    # code.
    for wrapper_type, item_s, node_s, nodes_set_func in [
        ('input', inputs, input_node_s, task.set_inputs),
        ('output', outputs, output_node_s, task.set_outputs),
    ]:
        # For each normalized item
        for item, _ in _normalize_items(
            # Context
            ctx=ctx,

            # Items to normalize
            items=item_s,

            # Convert string to node
            str_to_node=True,

            # Allow task item
            allow_task=True,
        ):
            # If the item is node
            if isinstance(item, Node):
                # Add the node to the nodes list
                node_s.append(item)

            # If the item is task
            elif isinstance(item, Task):
                # If the wrapper type is `input`
                if wrapper_type == 'input':
                    # Set the created task to run after the task
                    task.set_run_after(item)

                # If the wrapper type is `output`
                elif wrapper_type == 'output':
                    # Set the task to run after the created task
                    item.set_run_after(task)

                # If the wrapper type is not `input` or `output`
                else:
                    # Get error message
                    msg = (
                        'Error (3ZLGJ): Wrapper type is not valid: {0}.'
                    ).format(wrapper_type)

                    # Raise error
                    raise ValueError(msg)

            # If the item is not node or task
            else:
                # Get error message
                msg = 'Error (5H4GC): Item type is not valid: {0}.'.format(
                    item
                )

                # Raise error
                raise ValueError(msg)

        # Add these nodes to the created task as input or output targets
        nodes_set_func(node_s)

    # If need mark the created task as always run
    if always:
        # Mark the created task as always run
        task.always_run = True  # pylint: disable=W0201

    # If need add the created task to given context
    if add_to_group:
        # Add the created task to given context
        ctx.add_to_group(task)

    # If cache key is given
    if cache_key:
        # Add the created task to the cache
        _TASKS_CACHE[cache_key] = task

    # Print end title
    print_title('Create task: {}'.format(task_name), is_end=True)

    # Return the task
    return task