Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions rmfcli/colcon.pkg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"hooks": [
"share/rmfcli/environment/rmf-argcomplete.bash",
"share/rmfcli/environment/rmf-argcomplete.fish",
"share/rmfcli/environment/rmf-argcomplete.zsh"
]
}
19 changes: 19 additions & 0 deletions rmfcli/completion/rmf-argcomplete.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2017-2026 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

if type register-python-argcomplete3 > /dev/null 2>&1; then
eval "$(register-python-argcomplete3 rmf)"
elif type register-python-argcomplete > /dev/null 2>&1; then
eval "$(register-python-argcomplete rmf)"
fi
20 changes: 20 additions & 0 deletions rmfcli/completion/rmf-argcomplete.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2017-2026 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Register rmf tab-completion for fish shell via argcomplete.
if type -q register-python-argcomplete
register-python-argcomplete --shell fish rmf | source
else if type -q register-python-argcomplete3
register-python-argcomplete3 --shell fish rmf | source
end
25 changes: 25 additions & 0 deletions rmfcli/completion/rmf-argcomplete.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2017-2018 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

if (( ! ${+_comps} )); then
autoload -U +X compinit && compinit
fi
autoload -U +X bashcompinit && bashcompinit

# Get this scripts directory
__rmfcli_completion_dir=${0:a:h}
# Just source the bash version, it works in zsh too
source "$__rmfcli_completion_dir/rmf-argcomplete.bash"
# Cleanup
unset __rmfcli_completion_dir
27 changes: 27 additions & 0 deletions rmfcli/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>rmfcli</name>
<version>0.0.0</version>
<description>
RMF command line tools.
</description>
<maintainer email="chenbn@a-star.edu.sg">Chen Bainian</maintainer>
<license>Apache License 2.0</license>

<author email="chenbn@a-star.edu.sg">Chen Bainian</author>

<exec_depend>python3-argcomplete</exec_depend>
<exec_depend>rclpy</exec_depend>
<exec_depend>rmf_prototype_msgs</exec_depend>

<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>ament_xmllint</test_depend>
<test_depend>python3-pytest</test_depend>

<export>
<build_type>ament_python</build_type>
</export>
</package>
3 changes: 3 additions & 0 deletions rmfcli/resource/package.dsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source;share/rmfcli/environment/rmf-argcomplete.bash
source;share/rmfcli/environment/rmf-argcomplete.fish
source;share/rmfcli/environment/rmf-argcomplete.zsh
Empty file added rmfcli/resource/rmfcli
Empty file.
Empty file added rmfcli/rmfcli/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions rmfcli/rmfcli/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2026 Chen Bainian
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from argparse import ArgumentParser, RawDescriptionHelpFormatter
from signal import SIGINT
from typing import Any, Optional

from .command import extensions as command_extensions
from .extensions import ExtensionBase
from .extensions.utils import add_subparsers


def main(
*, script_name: str = "rmf", argv: Any = None, description: Optional[str] = None
) -> int | str | None:
if description is None:
description = f"{script_name} is an extensible command-line tool for RMF"

# top level parser
parser = ArgumentParser(description=description, formatter_class=RawDescriptionHelpFormatter)
extension_key = "_command"
add_subparsers(
parser, script_name, extension_key, command_extensions, required=False, argv=argv
)

# register argcomplete hook if available
try:
from argcomplete import autocomplete
except ImportError:
pass
else:
autocomplete(parser, exclude=["-h", "--help"], always_complete_options=False)

args = parser.parse_args(args=argv)

extension: Optional[ExtensionBase] = getattr(args, extension_key, None)

# handle the case that no command was passed
if extension is None:
parser.print_help()
return 0

# call the main method of the extension
try:
rc = extension.main(parser=parser, args=args)
except KeyboardInterrupt:
rc = SIGINT
except RuntimeError as e:
rc = str(e)
return rc
22 changes: 22 additions & 0 deletions rmfcli/rmfcli/command/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 Chen Bainian
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import OrderedDict

from rmfcli.extensions import ExtensionBase

from .destination import DestinationCommand

extensions: OrderedDict[str, ExtensionBase] = OrderedDict()
extensions["destination"] = DestinationCommand()
40 changes: 40 additions & 0 deletions rmfcli/rmfcli/command/destination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2026 Chen Bainian
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from argparse import ArgumentParser
from typing import Any

from rmfcli.extensions import CommandExtension, VerbExtension
from rmfcli.extensions.utils import add_subparsers
from rmfcli.verb.destination import extensions as verb_extensions


class DestinationCommand(CommandExtension):
"""Run Destination CLI sub-commands"""

def add_arguments(self, parser: ArgumentParser, cli_name: str, *, argv: Any = None) -> None:
self._subparser = parser
add_subparsers(parser, cli_name, "_verb", verb_extensions, required=False)

def main(self, *, parser: ArgumentParser, args: Any) -> None | int:
if not hasattr(args, "_verb"):
# in case no verb was passed
self._subparser.print_help()
return 0

extension: VerbExtension = args._verb

# call the verb's main method
rc: None | int = extension.main(parser=parser, args=args)
return rc
17 changes: 17 additions & 0 deletions rmfcli/rmfcli/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2026 Chen Bainian
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from .command_extension import CommandExtension
from .extension_base import ExtensionBase
from .verb_extension import VerbExtension
8 changes: 8 additions & 0 deletions rmfcli/rmfcli/extensions/command_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from .extension_base import ExtensionBase


class CommandExtension(ExtensionBase):
NAME = None

def __init__(self) -> None:
super(CommandExtension, self).__init__()
43 changes: 43 additions & 0 deletions rmfcli/rmfcli/extensions/extension_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2026 Chen Bainian
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.from argparse import ArgumentParser

from argparse import ArgumentParser
from typing import Any


class ExtensionBase:
"""
The extension point for 'command' extensions.

The following properties must be defined:
* `NAME` (will be set to the entry point name)

The following methods must be defined:
* `main`

The following methods can be defined:
* `add_arguments`
"""

NAME = None

def __init__(self) -> None:
super(ExtensionBase, self).__init__()

def add_arguments(self, parser: ArgumentParser, cli_name: str, *, argv: Any = None) -> None:
pass

def main(self, *, parser: ArgumentParser, args: Any) -> int | None:
raise NotImplementedError()
pass
49 changes: 49 additions & 0 deletions rmfcli/rmfcli/extensions/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from argparse import ArgumentParser, RawDescriptionHelpFormatter, _SubParsersAction
from collections import OrderedDict
from typing import Any, List

from .extension_base import ExtensionBase


def get_first_line_doc(any_type: Any) -> str:
if not any_type.__doc__:
return ""
lines: List[str] = any_type.__doc__.splitlines()
if not lines:
return ""
if lines[0]:
line = lines[0]
elif len(lines) > 1:
line = lines[1]
return line.strip().rstrip(".")


def add_subparsers(
parser: ArgumentParser,
cli_name: str,
dest: str,
extensions: OrderedDict[str, ExtensionBase],
required: bool = True,
argv: Any = None,
) -> _SubParsersAction:
# Generate description
description = ""
max_length = max(len(name) for name in extensions.keys())
for name, extension in extensions.items():
description += "%s %s\n" % (name.ljust(max_length), get_first_line_doc(extension))

# Create subparser
subparsers = parser.add_subparsers(
title="Commands",
description=description,
metavar=f"Call `{cli_name} <command> -h` for more detailed usage.",
)

subparsers.dest = " " + dest.lstrip("_")
subparsers.required = required
for name, extension in extensions.items():
command_parser = subparsers.add_parser(name, formatter_class=RawDescriptionHelpFormatter)
extension.add_arguments(command_parser, name, argv=argv)
command_parser.set_defaults(**{dest: extension})

return subparsers
29 changes: 29 additions & 0 deletions rmfcli/rmfcli/extensions/verb_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2026 Chen Bainian
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from argparse import ArgumentParser
from typing import Any

from .extension_base import ExtensionBase


class VerbExtension(ExtensionBase):
NAME = None

def __init__(self) -> None:
super(VerbExtension, self).__init__()

def main(self, *, parser: ArgumentParser, args: Any) -> int | None:
raise NotImplementedError()
pass
Empty file added rmfcli/rmfcli/py.typed
Empty file.
Empty file added rmfcli/rmfcli/verb/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions rmfcli/rmfcli/verb/destination/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import OrderedDict

from rmfcli.extensions import ExtensionBase

from .send import SendVerb
from .dummy import DummyVerb

extensions: OrderedDict[str, ExtensionBase] = OrderedDict()
extensions["send"] = SendVerb()
extensions["dummy"] = DummyVerb()
Loading
Loading