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

Ensure shebang lines are passed through as expected #158

Merged
merged 1 commit into from
Jan 3, 2025
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
22 changes: 22 additions & 0 deletions examples/shebang-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# An example of steps that use bashisms, and declare that by way of a shebang line.
# It would be better to put such a command in a script and call the script from the command field.
- step:
name: Do a thing
image: python:3.12
command: |
#!/bin/bash
echo "The wibble is {parameter:wibble} today."
if [[ -z "$1" ]]; then echo "No arguments provided"; fi
parameters:
- name: wibble
type: integer
- step:
name: Do the same thing in another way
image: python:3.12
command:
- "#!/bin/bash"
- echo "The wibble is {parameter:wibble} today."
- if [[ -z "$1" ]]; then echo "No arguments provided"; fi
parameters:
- name: wibble
type: integer
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
edge_merge_mode_config = config_fixture("edge-merge-mode.yaml")
keep_order_config = config_fixture("keep-order.yaml")
pipeline_with_retried_execution_config = config_fixture("pipeline-execution-retry-example.yaml")
shebang_example_config = config_fixture("shebang-example.yaml")
19 changes: 18 additions & 1 deletion tests/test_command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from collections import defaultdict

from valohai_yaml.commands import build_command
from valohai_yaml.commands import build_command, join_command
from valohai_yaml.objs.parameter_map import ParameterMap


Expand Down Expand Up @@ -130,3 +130,20 @@ def test_parameter_categories(example1_config):
"Output": {"output-alias"},
"Samples": {"unlabeled-samples", "labeled-samples"},
}


def test_shebang(shebang_example_config):
for step in shebang_example_config.steps.values():
command = step.build_command(parameter_values={"wibble": 42})
command = join_command(command, " && ")
assert command.startswith("#!/bin/bash\n")
assert "--wibble=42" in command


def test_join_command():
assert join_command(["foo"], " && ") == "foo"
assert join_command(["foo", " ", "bar"], " && ") == "foo && bar"
assert join_command(["foo", " ", "bar"], "\n") == "foo\nbar"
assert join_command(["#!/bin/sh", "foo", " ", "bar"], " && ") == "#!/bin/sh\nfoo && bar"
assert join_command(["#!/bin/sh\nfoo", " ", "bar"], " && ") == "#!/bin/sh\nfoo && bar"
assert join_command(["#!/bin/bash\nfoo", "#!woop"], " && ") == "#!/bin/bash\nfoo && #!woop"
30 changes: 29 additions & 1 deletion valohai_yaml/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ def build_command(

Even if passed a single `command`, this function will return a list
of shell commands. It is the caller's responsibility to concatenate them,
likely using the semicolon or double ampersands.
likely using the semicolon or double ampersands. The `join_command` function
can be used for this purpose.

:param command: The command to interpolate params into.
:param parameter_map: A ParameterMap object containing parameter knowledge.
Expand Down Expand Up @@ -94,3 +95,30 @@ def build_command(
)
out_commands.append(command.strip())
return out_commands


def join_command(commands: List[str], joiner: str) -> str:
"""
Join a list of commands into a single "script" that could be run using e.g. `sh -c`.

The commands (if not empty or purely whitespace)
are joined using the given joiner, but care is taken
to ensure that a shebang line, if any present at the start of the
first command, is preserved as a line of its own, as expected for
a script. (Note that `sh -c` wouldn't interpret it as a shebang line.)

:param commands: List of command pieces, e.g. from `build_command`.
:return: Single script string
"""
shebang_line = None
bits = []
for i, cmd in enumerate(commands):
if i == 0 and cmd.startswith("#!"):
shebang_line, _, cmd = cmd.partition("\n")
if not cmd.strip():
continue
bits.append(cmd)
joined_bits = joiner.join(bits)
if shebang_line:
return f"{shebang_line}\n{joined_bits}"
return joined_bits