-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshell2launch.py
More file actions
190 lines (148 loc) · 5.79 KB
/
shell2launch.py
File metadata and controls
190 lines (148 loc) · 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
from __future__ import annotations
import argparse
import re
import sys
from collections.abc import Sequence
from pathlib import Path
__version__ = "0.0.1"
def _parse_args(shell_code: str) -> dict[str, list[str]]:
"""Parses the arguments of a python script call from shell code.
Args:
shell_code (str):
The part of the shell script that invokes python. For example:
python myprogram.py / --arg_one / --arg_two 1 / --arg_three 1 2 /
Returns:
dict[str, list[str]]:
A dictionary that maps argument names two their values. In the
above example:
{"--arg_one": [], "--arg_two": ["1"], "--arg_three": ["1", "2"]}
"""
shell_code = shell_code.replace("\\", "").replace("\n", "")
shell_code_split = re.split("--|-", shell_code)
positional_split = shell_code_split[0].split()
optional_split = shell_code.split()
arguments: dict[str, list[str]] = {}
for positional in positional_split:
arguments[positional] = []
current_key = None
for el in optional_split:
if el.startswith("--") or el.startswith("-"):
current_key = el
arguments[current_key] = []
elif current_key is not None:
el = el[1:-1] if el.startswith('"') and el.endswith('"') else el
arguments[current_key].append(el)
return arguments
def _clean_shell_code(shell_code: str) -> str:
"""Removes commented lines as well as leading and trailing whitespaces
Args:
shell_code (str):
The original shell code.
Returns:
str: The cleaned shell code.
"""
cleaned = []
for line in shell_code.splitlines():
stripped_line = line.strip()
if not stripped_line.startswith("#"):
cleaned.append(stripped_line)
return "\n".join(cleaned)
def _build_args_strings(args: dict[str, list[str]]) -> list[str]:
"""Builds a list of formatted argument strings.
Args:
args (dict[str, list[str]]):
The arguments as a mapping of argument name to a list of provided
values. The ouput of parse_args().
Returns:
list[str]:
The "args": [...] part of the launch.json as a list of strings,
one for each line in the launch.json.
"""
args_strings = ['"args": [\n']
for key, val in args.items():
if len(val) > 0:
val_string = '"' + '", "'.join(val) + '"'
args_strings.append(f'\t"{key}", {val_string},\n')
else:
args_strings.append(f'\t"{key}",\n')
args_strings.append("]")
return args_strings
def _build_launch_string(python_filename: str, args: list[str]) -> str:
"""Returns a configuration entry for launch.json
Args:
python_filename (str):
The name of the target python file, e.g. 'myprogram.py'
args (list[str]):
A list of the arguments the program should be invoked with.
Contains both keys (e.g. '--myarg') and values (e.g. 'myvalue')
Returns:
str: The formatted debug configuration string.
"""
launch_json = ["{\n"]
launch_json.append(
f'\t"name": "Python Debugger: {python_filename} with Arguments",\n'
)
launch_json.append('\t"type": "debugpy",\n')
launch_json.append('\t"request": "launch",\n')
launch_json.append(f'\t"program": "{python_filename}",\n')
launch_json.append('\t"console": "integratedTerminal",\n')
launch_json.extend(f"\t{x}" for x in args)
launch_json.append("\n}")
return "".join(launch_json)
def shell2launch(shell_code: str, args_only: bool = False) -> str:
"""Build a launch.json debug configuration based on shell code.
Args:
shell_code (str):
Shell code that invokes a python script.
args_only (bool, optional):
If true, only return the "args": [...] part of the debug
configuration. Defaults to False.
Raises:
ValueError: If the provided shell code does not invoke a python script
via "python *.py"
Returns:
str: A formatted string for the launch.json debug configuration.
"""
shell_code = _clean_shell_code(shell_code=shell_code)
python_call_match = re.search(r"python .+\.py ", shell_code.strip())
if python_call_match is not None:
python_call = python_call_match.group()
else:
raise ValueError(
"The provided bash string does not contain a '.py' file reference"
)
python_filename = python_call.split()[1]
python_arguments = shell_code.split(python_call)[1].split("\n\n")[0]
args = _parse_args(python_arguments)
args_strings = _build_args_strings(args=args)
if args_only:
return "".join(args_strings)
launch_json = _build_launch_string(python_filename, args_strings)
return launch_json
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("filepath", type=str, help="path to the shell script")
parser.add_argument(
"-o",
"--output_filepath",
type=str,
help="specify an output filepath to save the output",
)
parser.add_argument(
"--args_only",
action="store_true",
help="only output the 'args: [...]' section of the launch configuration",
)
args = parser.parse_args(argv)
with open(Path(args.filepath), "r") as f:
shell_code = f.read()
launch_json = shell2launch(shell_code=shell_code, args_only=args.args_only)
if args.output_filepath is not None:
Path(args.output_filepath).parent.mkdir(parents=True, exist_ok=True)
Path(args.output_filepath).touch(exist_ok=True)
with open(args.output_filepath, "w") as f:
f.write(launch_json)
print(launch_json)
return 0
if __name__ == "__main__":
sys.exit(main())