11from __future__ import annotations
22
33import asyncio
4- import base64
54import json
65import logging
76import posixpath
8- import shlex
97import time
108from asyncio .subprocess import STDOUT
119from collections .abc import MutableMapping , MutableSequence
3028 MapCommandOutputProcessor ,
3129 UnionCommandOutputProcessor ,
3230)
33- from streamflow .core .utils import flatten_list
31+ from streamflow .core .utils import create_shell_command , flatten_list , quote
3432from streamflow .core .workflow import (
3533 Command ,
3634 CommandOptions ,
@@ -226,11 +224,11 @@ def _build_command_output_processor(
226224 )
227225
228226
229- def _escape_value (value : Any ) -> Any :
227+ def _escape_value (value : Any , local : bool ) -> Any :
230228 if isinstance (value , MutableSequence ):
231- return [_escape_value (v ) for v in value ]
229+ return [_escape_value (value = v , local = local ) for v in value ]
232230 else :
233- return shlex . quote (_get_value_repr (value ))
231+ return quote (value = _get_value_repr (value ), local = local )
234232
235233
236234async def _get_source_location (
@@ -710,17 +708,23 @@ async def _load(
710708 )
711709
712710 def _get_executable_command (
713- self , context : MutableMapping [str , Any ], inputs : MutableMapping [str , Token ]
711+ self ,
712+ context : MutableMapping [str , Any ],
713+ inputs : MutableMapping [str , Token ],
714+ local : bool ,
714715 ) -> MutableSequence [str ]:
715- command = []
716716 options = CWLCommandOptions (
717717 context = context ,
718718 expression_lib = self .expression_lib ,
719719 full_js = self .full_js ,
720+ local = local ,
720721 )
721722 # Process baseCommand
722- if self .base_command :
723- command .append (shlex .join (self .base_command ))
723+ command = (
724+ [quote (cmd , local = options .local ) for cmd in self .base_command ]
725+ if self .base_command
726+ else []
727+ )
724728 # Process tokens
725729 bindings = ListCommandToken (name = None , position = None , value = [])
726730 for processor in self .processors :
@@ -807,8 +811,14 @@ async def execute(self, job: Job) -> CWLCommandOutput:
807811 )
808812 else :
809813 inputs = job .inputs
814+ # Get execution target
815+ connector = self .step .workflow .context .scheduler .get_connector (job .name )
816+ locations = self .step .workflow .context .scheduler .get_locations (job .name )
817+ local = all (loc .local for loc in locations )
810818 # Build command string
811- cmd = self ._get_executable_command (context , inputs )
819+ cmd = self ._get_executable_command (context = context , inputs = inputs , local = local )
820+ if self .is_shell_command :
821+ cmd = create_shell_command (cmd , local = local )
812822 # Build environment variables
813823 parsed_env = {
814824 k : str (
@@ -825,24 +835,14 @@ async def execute(self, job: Job) -> CWLCommandOutput:
825835 parsed_env ["HOME" ] = job .output_directory
826836 if "TMPDIR" not in parsed_env :
827837 parsed_env ["TMPDIR" ] = job .tmp_directory
828- # Get execution target
829- connector = self .step .workflow .context .scheduler .get_connector (job .name )
830- locations = self .step .workflow .context .scheduler .get_locations (job .name )
831- cmd_string = " \\ \n \t " .join (
832- ["/bin/sh" , "-c" , '"{cmd}"' .format (cmd = " " .join (cmd ))]
833- if self .is_shell_command
834- else cmd
835- )
838+ # Log and persist command
839+ cmd_string = " \\ \n \t " .join (cmd )
836840 if logger .isEnabledFor (logging .INFO ):
837841 logger .info (
838842 "EXECUTING step {step} (job {job}) {location} into directory {outdir}:\n {command}" .format (
839843 step = self .step .name ,
840844 job = job .name ,
841- location = (
842- "locally"
843- if locations [0 ].local
844- else f"on location { locations [0 ]} "
845- ),
845+ location = ("locally" if local else f"on location { locations [0 ]} " ),
846846 outdir = job .output_directory ,
847847 command = cmd_string ,
848848 )
@@ -856,17 +856,6 @@ async def execute(self, job: Job) -> CWLCommandOutput:
856856 job_token_id = job_token .persistent_id ,
857857 cmd = cmd_string ,
858858 )
859- # Escape shell command when needed
860- if self .is_shell_command :
861- cmd = [
862- "/bin/sh" ,
863- "-c" ,
864- '"$(echo {command} | base64 -d)"' .format (
865- command = base64 .b64encode (" " .join (cmd ).encode ("utf-8" )).decode (
866- "utf-8"
867- )
868- ),
869- ]
870859 # If step is assigned to multiple locations, add the STREAMFLOW_HOSTS environment variable
871860 if len (locations ) > 1 and (
872861 hostnames := [loc .hostname for loc in locations if loc .hostname is not None ]
@@ -979,17 +968,19 @@ async def execute(self, job: Job) -> CWLCommandOutput:
979968
980969
981970class CWLCommandOptions (CommandOptions ):
982- __slots__ = ("context" , "expression_lib" , "full_js" )
971+ __slots__ = ("context" , "expression_lib" , "full_js" , "local" )
983972
984973 def __init__ (
985974 self ,
986975 context : MutableMapping [str , Any ],
987976 expression_lib : MutableSequence [str ] | None = None ,
988977 full_js : bool = False ,
978+ local : bool = False ,
989979 ):
990980 self .context : MutableMapping [str , Any ] = context
991981 self .expression_lib : MutableSequence [str ] | None = expression_lib
992982 self .full_js : bool = full_js
983+ self .local : bool = local
993984
994985
995986class CWLCommandTokenProcessor (CommandTokenProcessor ):
@@ -1075,7 +1066,7 @@ def bind(
10751066 value = [value ]
10761067 # Process shell escape only on the single command token
10771068 if not self .is_shell_command or self .shell_quote :
1078- value = [_escape_value (v ) for v in value ]
1069+ value = [_escape_value (value = v , local = options . local ) for v in value ]
10791070 # Obtain token position
10801071 if isinstance (self .position , str ) and not self .position .isnumeric ():
10811072 position = utils .eval_expression (
@@ -1219,6 +1210,7 @@ def _update_options(
12191210 | {"inputs" : {self .name : get_token_value (token )}},
12201211 expression_lib = options .expression_lib ,
12211212 full_js = options .full_js ,
1213+ local = options .local ,
12221214 )
12231215
12241216
@@ -1236,6 +1228,7 @@ def _update_options(
12361228 | {"inputs" : {self .name : value }, "self" : value },
12371229 expression_lib = options .expression_lib ,
12381230 full_js = options .full_js ,
1231+ local = options .local ,
12391232 )
12401233
12411234
0 commit comments