Skip to content
Open
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
28 changes: 27 additions & 1 deletion ros2cli/ros2cli/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion ros2service/ros2service/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
19 changes: 17 additions & 2 deletions ros2service/ros2service/verb/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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))
Expand Down
13 changes: 0 additions & 13 deletions ros2topic/ros2topic/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

import warnings

from argcomplete import CompletionFinder

import rclpy

from rclpy.expand_topic_name import expand_topic_name
Expand Down Expand Up @@ -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."""

Expand Down
8 changes: 4 additions & 4 deletions ros2topic/ros2topic/verb/pub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down