diff --git a/ros2cli/ros2cli/helpers.py b/ros2cli/ros2cli/helpers.py index ccf74df91..79eb30fdc 100644 --- a/ros2cli/ros2cli/helpers.py +++ b/ros2cli/ros2cli/helpers.py @@ -20,10 +20,11 @@ import subprocess import sys import time - from typing import Dict from typing import Optional +from argcomplete import CompletionFinder + def get_ros_domain_id(): return int(os.environ.get('ROS_DOMAIN_ID', 0)) @@ -238,3 +239,28 @@ def interactive_select( except (OSError, subprocess.SubprocessError) as e: print(f'Error during interactive selection: {e}', file=sys.stderr) return None + + +class UnescapedCompletionFinder(CompletionFinder): + + def quote_completions( + self, + completions: list[str], + cword_prequote: str, + last_wordbreak_pos: Optional[int], + ) -> list[str]: + """ + Return completions without shell escaping. + + Overrides the parent method to prevent mangling of YAML tokens + (dashes, braces, colons, etc.) that would otherwise be escaped + by the default shell quoting logic. + + :param completions: List of completion strings to process. + :param cword_prequote: The quote character preceding the word + being completed, if any. + :param last_wordbreak_pos: Position of the last word-break + character in the current word, or None. + :return: The completions list, unmodified. + """ + return completions diff --git a/ros2service/ros2service/api/__init__.py b/ros2service/ros2service/api/__init__.py index d4605af6f..61c6402f3 100644 --- a/ros2service/ros2service/api/__init__.py +++ b/ros2service/ros2service/api/__init__.py @@ -140,4 +140,5 @@ def __init__(self, *, service_type_key=None): def __call__(self, prefix, parsed_args, **kwargs): service = get_service(getattr(parsed_args, self.service_type_key)) - return [message_to_yaml(service.Request())] + yaml_snippet = "'" + message_to_yaml(service.Request()) + "'" + return [yaml_snippet] diff --git a/ros2service/ros2service/verb/call.py b/ros2service/ros2service/verb/call.py index cd20e15e4..8b4d73ec9 100644 --- a/ros2service/ros2service/verb/call.py +++ b/ros2service/ros2service/verb/call.py @@ -15,11 +15,13 @@ import time from typing import Optional +import argcomplete + import rclpy from rclpy.qos import QoSPresetProfiles from rclpy.qos import QoSProfile -from ros2cli.helpers import collect_stdin +from ros2cli.helpers import collect_stdin, UnescapedCompletionFinder from ros2cli.helpers import interactive_select from ros2cli.node import NODE_NAME_PREFIX from ros2cli.node.strategy import NodeStrategy @@ -67,6 +69,10 @@ def add_arguments(self, parser, cli_name): parser.add_argument( '-r', '--rate', metavar='N', type=float, help='Repeat the call at a specific rate in Hz') + + # Prevent argcomplete from escaping special characters in the YAML string + argcomplete.autocomplete = UnescapedCompletionFinder(parser) + add_qos_arguments(parser, 'service client', 'services_default') def main(self, *, args): @@ -117,7 +123,16 @@ def requester(service_type: str, service_name: str, values, period: Optional[flo except (AttributeError, ModuleNotFoundError): raise RuntimeError('The passed service type is invalid') - values_dictionary = yaml.safe_load(values) + try: + # Handle cases where the user pastes the autocompleted bash safe string + if '^J' in values: + values = values.replace("'", '') + values = values.replace('^J', '\n') + + values_dictionary = yaml.safe_load(values) + + except (yaml.parser.ParserError, yaml.scanner.ScannerError): + return 'The passed value needs to be in YAML string or a dictionary' with rclpy.init(): node = rclpy.create_node(NODE_NAME_PREFIX + '_requester_%s_%s' % (package_name, srv_name)) diff --git a/ros2topic/ros2topic/api/__init__.py b/ros2topic/ros2topic/api/__init__.py index dc55ab237..1a81d88fb 100644 --- a/ros2topic/ros2topic/api/__init__.py +++ b/ros2topic/ros2topic/api/__init__.py @@ -19,8 +19,6 @@ import warnings -from argcomplete import CompletionFinder - import rclpy from rclpy.expand_topic_name import expand_topic_name @@ -163,17 +161,6 @@ def _get_msg_class(node, topic, include_hidden_topics): raise RuntimeError("The message type '%s' is invalid" % message_type) -class YamlCompletionFinder(CompletionFinder): - def quote_completions( - self, completions: list[str], - cword_prequote: str, last_wordbreak_pos: Optional[int]): - - # For YAML content, return as-is without escaping - if not any('-' in c for c in completions): - return completions - return super().quote_completions(completions, cword_prequote, last_wordbreak_pos) - - class TopicMessagePrototypeCompleter: """Callable returning a message prototype.""" diff --git a/ros2topic/ros2topic/verb/pub.py b/ros2topic/ros2topic/verb/pub.py index 9732ab1ee..05ca1f799 100644 --- a/ros2topic/ros2topic/verb/pub.py +++ b/ros2topic/ros2topic/verb/pub.py @@ -22,14 +22,14 @@ from rclpy.node import Node from rclpy.qos import QoSProfile -from ros2cli.helpers import collect_stdin +from ros2cli.helpers import collect_stdin, UnescapedCompletionFinder from ros2cli.node.direct import add_arguments as add_direct_node_arguments from ros2cli.node.direct import DirectNode from ros2cli.qos import add_qos_arguments from ros2cli.qos import profile_configure_short_keys from ros2topic.api import positive_float -from ros2topic.api import TopicMessagePrototypeCompleter, YamlCompletionFinder +from ros2topic.api import TopicMessagePrototypeCompleter from ros2topic.api import TopicNameCompleter from ros2topic.api import TopicTypeCompleter from ros2topic.verb import VerbExtension @@ -115,8 +115,8 @@ def add_arguments(self, parser, cli_name): '-n', '--node-name', help='Name of the created publishing node') - # Use the custom completion finder - argcomplete.autocomplete = YamlCompletionFinder(parser) + # Prevent argcomplete from escaping special characters in the YAML string + argcomplete.autocomplete = UnescapedCompletionFinder(parser) add_qos_arguments(parser, 'publish', 'default') add_direct_node_arguments(parser)