Skip to content
Merged
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
2 changes: 2 additions & 0 deletions launch/launch/substitutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from .not_equals_substitution import NotEqualsSubstitution
from .path_join_substitution import PathJoinSubstitution
from .python_expression import PythonExpression
from .string_join_substitution import StringJoinSubstitution
from .substitution_failure import SubstitutionFailure
from .text_substitution import TextSubstitution
from .this_launch_file import ThisLaunchFile
Expand All @@ -60,6 +61,7 @@
'OrSubstitution',
'PathJoinSubstitution',
'PythonExpression',
'StringJoinSubstitution',
'SubstitutionFailure',
'TextSubstitution',
'ThisLaunchFile',
Expand Down
98 changes: 98 additions & 0 deletions launch/launch/substitutions/string_join_substitution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright 2025 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.

"""Module for the StringJoinSubstitution substitution."""

from typing import Iterable, List, Text

from ..launch_context import LaunchContext
from ..some_substitutions_type import SomeSubstitutionsType
from ..substitution import Substitution
from ..utilities import perform_substitutions


class StringJoinSubstitution(Substitution):
"""
Substitution that joins strings and/or other substitutions.

This takes in a list of string components as substitutions.
The substitutions for each string component are performed and concatenated,
and then all string components are joined with a specified delimiter as seperation.

For example:

.. code-block:: python

StringJoinSubstitution(
[['https', '://'], LaunchConfiguration('subdomain')], 'ros', 'org'],
delimiter='.'
)

If the ``subdomain`` launch configuration was set to ``docs``
and the ``delimiter`` to ``.``, this would result in a string equal to

.. code-block:: python

'https://docs.ros.org'
"""

def __init__(
self,
substitutions: Iterable[SomeSubstitutionsType],
delimiter: SomeSubstitutionsType = '',
) -> None:
"""
Create a StringJoinSubstitution.

:param substitutions: the list of string component substitutions to join
:param delimiter: the text inbetween two consecutive components (default no text)
"""
from ..utilities import normalize_to_list_of_substitutions

self.__substitutions = [
normalize_to_list_of_substitutions(string_component_substitutions)
for string_component_substitutions in substitutions
]
self.__delimiter = normalize_to_list_of_substitutions(delimiter)

@property
def substitutions(self) -> List[List[Substitution]]:
"""Getter for substitutions."""
return self.__substitutions

@property
def delimiter(self) -> List[Substitution]:
"""Getter for delimiter."""
return self.__delimiter

def __repr__(self) -> Text:
"""Return a description of this substitution as a string."""
string_components = [
' + '.join([s.describe() for s in component_substitutions])
for component_substitutions in self.substitutions
]
delimiter_component = ' + '.join([d.describe() for d in self.delimiter])
return (
f'StringJoinSubstitution(['
f'{", ".join(string_components)}], delimiter={delimiter_component})'
)

def perform(self, context: LaunchContext) -> Text:
"""Perform the substitution by retrieving the local variable."""
string_components = [
perform_substitutions(context, component_substitutions)
for component_substitutions in self.substitutions
]
delimiter_component = perform_substitutions(context, self.delimiter)
return delimiter_component.join(string_components)
71 changes: 71 additions & 0 deletions launch/test/launch/substitutions/test_string_join_substitution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2025 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.

"""Tests for the StringJoinSubstitution substitution class."""

from launch import LaunchContext
from launch.substitutions import StringJoinSubstitution
from launch.substitutions import TextSubstitution


def test_string_join():
context = LaunchContext()

strings = ['abc', 'def', 'ghi']
sub = StringJoinSubstitution(strings)
assert sub.perform(context) == ''.join(strings)

strings = ['abc', ['def'], [TextSubstitution(text='ghi'), 'jkl'], TextSubstitution(text='mno')]
sub_with_sub = StringJoinSubstitution(strings)
assert sub_with_sub.perform(context) == 'abcdefghijklmno'


def test_string_join_with_delimiter():
context = LaunchContext()

strings = ['abc', 'def', 'ghi']
sub = StringJoinSubstitution(strings, delimiter='.')
assert sub.perform(context) == '.'.join(strings)

strings = ['abc', ['def'], [TextSubstitution(text='ghi'), 'jkl'], TextSubstitution(text='mno')]
sub_with_sub = StringJoinSubstitution(strings, delimiter='.')
assert sub_with_sub.perform(context) == 'abc.def.ghijkl.mno'


def test_string_join_with_substitution_delimiter():
context = LaunchContext()

strings = ['abc', 'def', 'ghi']
sub = StringJoinSubstitution(strings, delimiter=['-', '.', '-'])
assert sub.perform(context) == '-.-'.join(strings)

strings = ['abc', ['def'], [TextSubstitution(text='ghi'), 'jkl'], TextSubstitution(text='mno')]
sub_with_sub = StringJoinSubstitution(strings, delimiter=['-', '.', '-'])
assert sub_with_sub.perform(context) == 'abc-.-def-.-ghijkl-.-mno'

strings = ['abc', 'def', 'ghi']
sub = StringJoinSubstitution(strings, delimiter=TextSubstitution(text='_'))
assert sub.perform(context) == '_'.join(strings)

strings = ['abc', ['def'], [TextSubstitution(text='ghi'), 'jkl'], TextSubstitution(text='mno')]
sub_with_sub = StringJoinSubstitution(strings, delimiter=TextSubstitution(text='_'))
assert sub_with_sub.perform(context) == 'abc_def_ghijkl_mno'

strings = ['abc', 'def', 'ghi']
sub = StringJoinSubstitution(strings, delimiter=['(^', TextSubstitution(text='_'), '^)'])
assert sub.perform(context) == '(^_^)'.join(strings)

strings = ['abc', ['def'], [TextSubstitution(text='ghi'), 'jkl'], TextSubstitution(text='mno')]
sub_with_sub = StringJoinSubstitution(strings, delimiter=[TextSubstitution(text='(^_'), '^)'])
assert sub_with_sub.perform(context) == 'abc(^_^)def(^_^)ghijkl(^_^)mno'