11from __future__ import annotations as _annotations
22
3+ from typing import TYPE_CHECKING as _TYPE_CHECKING
34from pathlib import Path as _Path
45from xml .etree import ElementTree as _ElementTree
56import copy as _copy
67import os as _os
8+ import re as _re
9+ import shlex as _shlex
710
811from loggerman import logger
912import pyserials as _ps
1316from controlman .file_gen import unit as _unit
1417from controlman import const as _const
1518
19+ if _TYPE_CHECKING :
20+ from typing import Literal
1621
1722class ConfigFileGenerator :
1823 def __init__ (
@@ -98,6 +103,7 @@ def dynamic_file(self, key: str, file: dict, file_before: dict | None):
98103 file_info = {
99104 "type" : DynamicFileType .CUSTOM ,
100105 "subtype" : (key , file .get ("name" , key )),
106+ "executable" : file ["type" ] == "exec" ,
101107 }
102108 if file ["status" ] == "delete" :
103109 file_info ["path_before" ] = file ["path" ]
@@ -214,18 +220,65 @@ def create_docker_compose():
214220 out .append (docker_compose_file )
215221 return
216222
217- def create_task_function (task : dict , env_name : str , task_in_env_prefix : str ) -> str :
223+ def quote_shell_arguments (args : list [str ] | None ) -> list [str ]:
224+ """Quotes arguments safely for Bash while preserving special shell parameters."""
225+ return [
226+ f'"{ arg } "' if unquoted_task_process_patterns .match (arg ) else _shlex .quote (arg )
227+ for arg in (args or [])
228+ ]
229+
230+ def resolve_task_settings (
231+ devcontainer : dict ,
232+ typ : Literal ["local" , "global" ],
233+ environment : dict | None = None
234+ ):
235+ typ2 = "environment" if environment else "root"
236+ jsonpath = f"default.task_setting.{ typ } .{ typ2 } "
237+ settings = self ._data .get (jsonpath , {})
238+ settings_filled = _unit .fill_jinja_templates (
239+ templates = settings ,
240+ jsonpath = jsonpath ,
241+ env_vars = {"devcontainer" : devcontainer , "environment" : environment or {}}
242+ )
243+ out = _copy .deepcopy (devcontainer .get ("task_setting" , {}).get (typ , {}).get (typ2 , {}))
244+ _ps .update .recursive_update (
245+ source = out ,
246+ addon = settings_filled ,
247+ )
248+ return out
249+
250+ def create_task_function (
251+ task : dict ,
252+ task_setting : dict ,
253+ ) -> str :
254+ def add_lines (content : str ):
255+ lines .extend ([(f"{ indent } { line } " if line else "" ) for line in content .splitlines ()])
256+ return
257+
218258 lines = [f"{ task ["alias" ]} () {{" ]
219259 indent = 4 * " "
220260 if "script" in task :
221- lines .extend ([f"{ indent } { line } " for line in task ["script" ].strip ().splitlines ()])
261+ settings = task_setting .get ("script" , {})
262+ add_lines (settings .get ("prepend" , "" ))
263+ add_lines (task ["script" ])
264+ add_lines (settings .get ("append" , "" ))
222265 else :
223- cmd_prefix = task_in_env_prefix .format (env_name = env_name ).strip ()
224- cmd = f"{ cmd_prefix } { " " .join (task ["process" ])} "
225- lines .append (f"{ indent } { cmd } " )
266+ settings = task_setting .get ("process" , {})
267+ cmd = settings .get ("prepend" , [])
268+ cmd .extend (task ["process" ])
269+ cmd .extend (settings .get ("append" , []))
270+ cmd_quoted = quote_shell_arguments (cmd )
271+ add_lines (" " .join (cmd_quoted ))
226272 lines .append ("}" )
227273 return "\n " .join (lines )
228274
275+ unquoted_task_process_patterns = _re .compile (
276+ r'^\$(\d+|\*|@)$' # Matches $1, $2, ..., $@, $*
277+ r'|^\$\{[^}]+\}$' # Matches ${VAR}, ${ARRAY[@]}, ${1}, etc.
278+ r'|^\$\w+$' # Matches $VAR
279+ r'|^\$\(.+\)$' # Matches $(command)
280+ )
281+
229282 out = []
230283 docker_compose_data = self ._data ["devcontainer.docker-compose" ]
231284 docker_compose_path = docker_compose_data ["path" ]
@@ -234,15 +287,6 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
234287 for k , v in self ._data .items () if k .startswith ("devcontainer_" )
235288 }
236289 create_docker_compose ()
237- env_dirname = self ._data ["devcontainer.containers.rel_path.environment" ]
238- apt_path = self ._data ["devcontainer.containers.rel_path.apt" ]
239- conda_path = self ._data ["devcontainer.containers.rel_path.conda" ]
240- tasks_path = self ._data ["devcontainer.containers.rel_path.tasks" ]
241-
242- env_dirname_before = self ._data_before ["devcontainer.containers.rel_path.environment" ] or env_dirname
243- apt_path_before = self ._data_before ["devcontainer.containers.rel_path.apt" ] or apt_path
244- conda_path_before = self ._data_before ["devcontainer.containers.rel_path.conda" ] or conda_path
245- tasks_path_before = self ._data_before ["devcontainer.containers.rel_path.tasks" ] or tasks_path
246290
247291 for container_id , container in devcontainers .items ():
248292 container_before = self ._data_before .get (f"devcontainer_{ container_id } " , {})
@@ -255,8 +299,8 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
255299 file_type = "txt" ,
256300 content = container ["dockerfile" ],
257301 ),
258- path = f"{ dir_path } /Dockerfile " ,
259- path_before = f"{ dir_path_before } /Dockerfile " ,
302+ path = f"{ container [ "path" ][ "dockerfile" ] } " ,
303+ path_before = f"{ container_before . get ( "path" , {}). get ( "dockerfile" ) } " ,
260304 )
261305 out .append (dockerfile )
262306 # devcontainer.json file
@@ -284,8 +328,8 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
284328 file_type = "txt" ,
285329 content = [pkg ["spec" ]["full" ] for pkg in container ["apt" ].values ()],
286330 ) if container .get ("apt" ) else None ,
287- path = f"{ dir_path } / { env_dirname } / { apt_path } " ,
288- path_before = f"{ dir_path_before } / { env_dirname_before } / { apt_path_before } " ,
331+ path = f"{ container [ "path" ][ "apt" ] } " ,
332+ path_before = f"{ container_before . get ( "path" , {}). get ( "apt" ) } " ,
289333 )
290334 out .append (apt_file )
291335 # conda environment files
@@ -307,36 +351,37 @@ def create_task_function(task: dict, env_name: str, task_in_env_prefix: str) ->
307351 )
308352 out .append (env_file )
309353 # bash task file
310- tasks = []
354+ tasks = { "local" : [], "global" : []}
311355 for task in container .get ("task" , {}).values ():
312- tasks .append (
313- create_task_function (
314- task = task ,
315- env_name = "base" ,
316- task_in_env_prefix = container ["task_in_env_prefix" ]
317- )
318- )
319- for environment in container .get ("environment" , {}).values ():
320- for task in environment .get ("task" , {}).values ():
321- tasks .append (
356+ for typ in tasks .keys ():
357+ tasks [typ ].append (
322358 create_task_function (
323359 task = task ,
324- env_name = environment ["name" ],
325- task_in_env_prefix = container ["task_in_env_prefix" ]
360+ task_setting = resolve_task_settings (devcontainer = container , typ = typ ),
326361 )
327362 )
328- task_file = DynamicFile (
329- type = DynamicFileType .DEVCONTAINER_TASK ,
330- subtype = (container_id , container .get ("name" , container_id )),
331- content = _unit .create_dynamic_file (
332- file_type = "txt" ,
333- content = tasks ,
334- content_item_separator = "\n \n " ,
335- ) if tasks else None ,
336- path = f"{ dir_path } /{ tasks_path } " ,
337- path_before = f"{ dir_path_before } /{ tasks_path_before } " ,
338- )
339- out .append (task_file )
363+ for environment in container .get ("environment" , {}).values ():
364+ for task in environment .get ("task" , {}).values ():
365+ for typ in tasks .keys ():
366+ tasks [typ ].append (
367+ create_task_function (
368+ task = task ,
369+ task_setting = resolve_task_settings (devcontainer = container , typ = typ , environment = environment ),
370+ )
371+ )
372+ for typ in tasks .keys ():
373+ task_file = DynamicFile (
374+ type = DynamicFileType .DEVCONTAINER_TASK ,
375+ subtype = (container_id , container .get ("name" , container_id )),
376+ content = _unit .create_dynamic_file (
377+ file_type = "txt" ,
378+ content = tasks [typ ],
379+ content_item_separator = "\n \n " ,
380+ ) if tasks [typ ] else None ,
381+ path = f"{ container ["path" ][f"tasks_{ typ } " ]} " ,
382+ path_before = f"{ container_before .get ("path" , {}).get (f"tasks_{ typ } " )} " ,
383+ )
384+ out .append (task_file )
340385 return out
341386
342387 def devcontainer_features (self ) -> list [DynamicFile ]:
0 commit comments