Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ros2launch] legal tab completion of launch files #126

Merged
merged 13 commits into from
Feb 27, 2020
2 changes: 2 additions & 0 deletions ros2launch/ros2launch/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from launch.launch_description_sources import InvalidPythonLaunchFileError

from .api import get_share_file_path_from_package
from .api import is_launch_file
from .api import launch_a_launch_file
from .api import LaunchFileNameCompleter
from .api import MultipleLaunchFilesError
Expand All @@ -28,6 +29,7 @@

__all__ = [
'get_share_file_path_from_package',
'is_launch_file',
'InvalidLaunchFileError',
'InvalidPythonLaunchFileError',
'LaunchFileNameCompleter',
Expand Down
16 changes: 11 additions & 5 deletions ros2launch/ros2launch/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,22 @@ def get_launch_file_paths(*, path):
launch_file_paths = []
for root, dirs, files in os.walk(path):
for file_name in files:
if file_name.endswith(get_launch_file_paths.extensions):
launch_file_paths.append(os.path.join(root, file_name))
file_path = os.path.join(root, file_name)
if is_launch_file(path=file_path):
launch_file_paths.append(file_path)
return launch_file_paths


get_launch_file_paths.extensions = [
def is_launch_file(path):
"""Return True if the path is a launch file."""
return path.endswith(is_launch_file.extensions) and os.path.isfile(path)


is_launch_file.extensions = [
'launch.' + extension for extension in Parser.get_available_extensions()
]
get_launch_file_paths.extensions.append('launch.py')
get_launch_file_paths.extensions = tuple(get_launch_file_paths.extensions)
is_launch_file.extensions.append('launch.py')
is_launch_file.extensions = tuple(is_launch_file.extensions)


def print_a_launch_file(*, launch_file_path):
Expand Down
38 changes: 36 additions & 2 deletions ros2launch/ros2launch/command/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@

from ament_index_python.packages import get_package_prefix
from ament_index_python.packages import PackageNotFoundError
from argcomplete.completers import FilesCompleter
try:
from argcomplete.completers import SuppressCompleter
except ImportError:
# argcomplete < 1.9.0
SuppressCompleter = object
from ros2cli.command import CommandExtension
from ros2launch.api import get_share_file_path_from_package
from ros2launch.api import is_launch_file
from ros2launch.api import launch_a_launch_file
from ros2launch.api import LaunchFileNameCompleter
from ros2launch.api import MultipleLaunchFilesError
Expand All @@ -26,6 +33,32 @@
from ros2pkg.api import package_name_completer


class SuppressCompleterWorkaround(SuppressCompleter):
"""Workaround https://github.com/kislyuk/argcomplete/pull/289 ."""

def __call__(self, *args, **kwargs):
"""Make SupressCompleter callable by returning no completions."""
return ()


def package_name_or_launch_file_completer(prefix, parsed_args, **kwargs):
"""Complete package names or paths to launch files."""
pass_through_kwargs = dict(kwargs)
pass_through_kwargs['prefix'] = prefix
pass_through_kwargs['parsed_args'] = parsed_args

# Complete package names
completions = list(package_name_completer(**pass_through_kwargs))

def is_launch_file_or_dir(path):
return is_launch_file(path) or os.path.isdir(path)

# Complete paths to launch files
completions.extend(filter(is_launch_file_or_dir, FilesCompleter()(**pass_through_kwargs)))

return completions


class LaunchCommand(CommandExtension):
"""Run a launch file."""

Expand All @@ -49,17 +82,18 @@ def add_arguments(self, parser, cli_name):
arg = parser.add_argument(
'package_name',
help='Name of the ROS package which contains the launch file')
arg.completer = package_name_completer
arg.completer = package_name_or_launch_file_completer
arg = parser.add_argument(
'launch_file_name',
# TODO(wjwwood) make this not optional when full launch path is supported.
nargs='?',
help='Name of the launch file')
arg.completer = LaunchFileNameCompleter()
arg = parser.add_argument(
'launch_arguments',
nargs='*',
help="Arguments to the launch file; '<name>:=<value>' (for duplicates, last one wins)")
arg.completer = LaunchFileNameCompleter()
arg.completer = SuppressCompleterWorkaround()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate why it does work without the workaround in other cases but not here?

Using

$ tree
.
├── composition
│   ├── asdf.launch.py
│   ├── foo
│   │   ├── bar.launch.py
│   │   └── baz.launch.py
│   └── foo.launch.py
├── composition_launch.py
└── file_i_dont_want_completed.txt

With SupressCompleter()

$ ros2 launch composition <tab><tab>
composition/                    composition_launch.py           file_i_dont_want_completed.txt

With SupressCompleterWorkaround()

$ ros2 launch composition <tab><tab>
-a                              --debug                         --print-description             --show-args
composition_demo.launch.py      -p                              -s                              --show-arguments
-d                              --print                         --show-all-subprocesses-output 

I haven't tested colcon-package-information, so I don't know if SuppressCompleter is working as intended there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if SuppressCompleter is working as intended there.

It does.

What version of argcomplete are you using?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argcomplete==1.11.1, and I was also able to reproduce using upstream master

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am missing the part why it is necessary in this case only.


def main(self, *, parser, args):
"""Entry point for CLI program."""
Expand Down