bash-dap 0.1__tar.gz → 0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bash-dap
3
- Version: 0.1
3
+ Version: 0.3
4
4
  Summary: A debug adapter (Debug Adapter Protocol) for bash scripts, written in bash and Python
5
5
  Author-email: Ilia Maslakov <il.smind@gmail.com>
6
6
  License: GPL-3.0-or-later
@@ -18,8 +18,9 @@ Dynamic: license-file
18
18
  # bash-dap
19
19
 
20
20
  A debug adapter of the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/)
21
- for bash scripts: breakpoints, steps into, over and out of functions, the call stack, the
22
- variables (arrays and associative arrays as trees), expressions, pause.
21
+ for bash scripts: breakpoints on lines, with conditions, and on functions, steps into, over and
22
+ out of functions, the call stack, the variables (arrays and associative arrays as trees),
23
+ expressions, pause.
23
24
 
24
25
  It needs nothing but bash 4.1 or newer and Python 3.8 or newer: no bashdb, no Node.js. (The bash
25
26
  of macOS is 3.2: a newer one, from Homebrew say, is given to the launch as *bash*.) The debugger itself is a script
@@ -75,13 +76,20 @@ subshell is a process of its own, so what is said to one, a step, a breakpoint,
75
76
  others through files of that directory, which every process looks at, at a test of two files a
76
77
  command. A script under the debugger runs some 25 times slower than by itself.
77
78
 
79
+ A breakpoint on a function stops at its first command, at each call, in a subshell too;
80
+ what the script defines later, or a file it sources, is found as well, as the name is all
81
+ the debugger looks for.
82
+
83
+ The condition of a breakpoint is a command of bash, `[[ $f == report.pdf ]]` or
84
+ `(( count > 2 ))`: the script stops there when its status is 0. It runs in a subshell, so it
85
+ sees the variables of the script and changes none of them.
86
+
78
87
  When the session ends the adapter ends the script by its process group, the session it has to
79
88
  itself; never by a parent, never anything else.
80
89
 
81
90
  ## Not yet
82
91
 
83
- Conditional breakpoints, breakpoints on functions, a script that another script runs with a
84
- new bash.
92
+ A script that another script runs with a new bash.
85
93
 
86
94
  ## License
87
95
 
@@ -1,8 +1,9 @@
1
1
  # bash-dap
2
2
 
3
3
  A debug adapter of the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/)
4
- for bash scripts: breakpoints, steps into, over and out of functions, the call stack, the
5
- variables (arrays and associative arrays as trees), expressions, pause.
4
+ for bash scripts: breakpoints on lines, with conditions, and on functions, steps into, over and
5
+ out of functions, the call stack, the variables (arrays and associative arrays as trees),
6
+ expressions, pause.
6
7
 
7
8
  It needs nothing but bash 4.1 or newer and Python 3.8 or newer: no bashdb, no Node.js. (The bash
8
9
  of macOS is 3.2: a newer one, from Homebrew say, is given to the launch as *bash*.) The debugger itself is a script
@@ -58,13 +59,20 @@ subshell is a process of its own, so what is said to one, a step, a breakpoint,
58
59
  others through files of that directory, which every process looks at, at a test of two files a
59
60
  command. A script under the debugger runs some 25 times slower than by itself.
60
61
 
62
+ A breakpoint on a function stops at its first command, at each call, in a subshell too;
63
+ what the script defines later, or a file it sources, is found as well, as the name is all
64
+ the debugger looks for.
65
+
66
+ The condition of a breakpoint is a command of bash, `[[ $f == report.pdf ]]` or
67
+ `(( count > 2 ))`: the script stops there when its status is 0. It runs in a subshell, so it
68
+ sees the variables of the script and changes none of them.
69
+
61
70
  When the session ends the adapter ends the script by its process group, the session it has to
62
71
  itself; never by a parent, never anything else.
63
72
 
64
73
  ## Not yet
65
74
 
66
- Conditional breakpoints, breakpoints on functions, a script that another script runs with a
67
- new bash.
75
+ A script that another script runs with a new bash.
68
76
 
69
77
  ## License
70
78
 
@@ -1,3 +1,3 @@
1
1
  """bash-dap: a debug adapter of the Debug Adapter Protocol for bash scripts."""
2
2
 
3
- __version__ = "0.1"
3
+ __version__ = "0.3"
@@ -15,7 +15,11 @@ import tempfile
15
15
  import threading
16
16
  import time
17
17
 
18
+ from . import __version__
19
+
18
20
  HARNESS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "harness.sh")
21
+ # a name of a function of bash, as a breakpoint takes it: no space, no quote, no parenthesis
22
+ FUNCTION_NAME = re.compile(r"^[^\s()$`'\"\\]+$")
19
23
  VARIABLES_REF = 1 # the reference of the variables of a stop
20
24
 
21
25
 
@@ -108,6 +112,7 @@ class Adapter:
108
112
  self.alive = False
109
113
  self.pause_asked = False
110
114
  self.breaks = {}
115
+ self.functions = []
111
116
  self.gen = 0
112
117
  self.refs = {}
113
118
  self.next_ref = VARIABLES_REF + 1
@@ -194,8 +199,13 @@ class Adapter:
194
199
  return
195
200
  with open(os.path.join(self.dir, "breaks"), "w") as f:
196
201
  for path, lines in self.breaks.items():
197
- for line in lines:
198
- f.write("%s:%d\n" % (path, line))
202
+ for line, condition in lines:
203
+ if condition:
204
+ f.write("%s:%d\t%s\n" % (path, line, condition))
205
+ else:
206
+ f.write("%s:%d\n" % (path, line))
207
+ for name in self.functions:
208
+ f.write("%s()\n" % name)
199
209
  self.gen += 1
200
210
  open(os.path.join(self.dir, "gen.%d" % self.gen), "w").close()
201
211
 
@@ -255,6 +265,8 @@ class Adapter:
255
265
  return
256
266
  if self.pause_asked and reason == "step":
257
267
  reason = "pause"
268
+ elif reason == "function":
269
+ reason = "function breakpoint"
258
270
  self.pause_asked = False
259
271
  self.event("stopped", {"reason": reason, "threadId": 1, "allThreadsStopped": True})
260
272
 
@@ -287,6 +299,8 @@ class Adapter:
287
299
  self.client = request.get("arguments", {})
288
300
  self.respond(request, {
289
301
  "supportsConfigurationDoneRequest": True,
302
+ "supportsFunctionBreakpoints": True,
303
+ "supportsConditionalBreakpoints": True,
290
304
  "supportsEvaluateForHovers": True,
291
305
  "supportsTerminateRequest": True,
292
306
  })
@@ -345,17 +359,30 @@ class Adapter:
345
359
  def on_setBreakpoints(self, request):
346
360
  args = request.get("arguments", {})
347
361
  path = os.path.realpath(args.get("source", {}).get("path", ""))
348
- lines = [b["line"] for b in args.get("breakpoints", []) if "line" in b]
362
+ # a line and its condition, a command of bash whose status says whether to stop
363
+ lines = [(b["line"], " ".join(b.get("condition", "").split()))
364
+ for b in args.get("breakpoints", []) if "line" in b]
349
365
  self.breaks[path] = lines
350
366
  self.save_breaks()
351
367
  self.respond(request, {"breakpoints": [
352
368
  {"id": i + 1, "verified": True, "line": line, "source": {"path": path}}
353
- for i, line in enumerate(lines)]})
369
+ for i, (line, _) in enumerate(lines)]})
354
370
 
355
371
  def on_setFunctionBreakpoints(self, request):
356
- self.respond(request, {"breakpoints": [
357
- {"verified": False, "message": "bash-dap has breakpoints on lines only"}
358
- for _ in request.get("arguments", {}).get("breakpoints", [])]})
372
+ """The functions to stop in at their first command; a name that is no name of a
373
+ function of bash is not taken"""
374
+ answers = []
375
+ self.functions = []
376
+ for b in request.get("arguments", {}).get("breakpoints", []):
377
+ name = b.get("name", "")
378
+ if FUNCTION_NAME.match(name):
379
+ self.functions.append(name)
380
+ answers.append({"id": 1000 + len(self.functions), "verified": True})
381
+ else:
382
+ answers.append({"verified": False,
383
+ "message": "no name of a function of bash: %s" % name})
384
+ self.save_breaks()
385
+ self.respond(request, {"breakpoints": answers})
359
386
 
360
387
  def on_setExceptionBreakpoints(self, request):
361
388
  self.respond(request, {"breakpoints": []})
@@ -527,7 +554,7 @@ def main():
527
554
  "on stdin and stdout")
528
555
  return 0
529
556
  if len(sys.argv) > 1 and sys.argv[1] == "--version":
530
- print("bash-dap 0.1")
557
+ print("bash-dap %s" % __version__)
531
558
  return 0
532
559
  Adapter(sys.stdin.buffer, sys.stdout.buffer).run()
533
560
  return 0
@@ -9,14 +9,17 @@
9
9
  # "replies", which it reads what the debugger says from, a line each:
10
10
  #
11
11
  # commands: run | step | next | finish | print EXPRESSION | vars | stack
12
- # replies: stopped REASON LINE FUNCTION FILE (REASON: entry, breakpoint, step)
12
+ # replies: stopped REASON LINE FUNCTION FILE (REASON: entry, breakpoint, function, step)
13
13
  # value TEXT | var NAME TEXT | frame LEVEL FUNCTION LINE FILE | error TEXT
14
14
  # done, after the reply to a command | exited CODE
15
15
  #
16
16
  # TEXT is quoted as printf %q does, so that a reply is one line. A command is read only while
17
17
  # the script is stopped. At any time the adapter may write the breakpoints to the file "breaks",
18
18
  # FILE:LINE a line, then make the empty file "gen.N" of the next generation N; and stop the
19
- # script at its next command by writing "step 0" to the file "state".
19
+ # script at its next command by writing "step 0" to the file "state". A line NAME() of
20
+ # "breaks" is a breakpoint on the function NAME: the script stops at its first command. After
21
+ # FILE:LINE and a tab, a command of bash is the condition of the breakpoint: the script stops
22
+ # there only when its status is 0.
20
23
  #
21
24
  # A subshell is a process of its own: what it is told, a step, a breakpoint, goes to the others
22
25
  # through these files. The names of the debugger begin with __bdap_.
@@ -33,11 +36,14 @@ __bdap_state=$__bdap_dir/state
33
36
  __bdap_mode=run
34
37
  __bdap_depth=0
35
38
  # the breakpoints by FILE:LINE, and the generation of the file they came from; __bdap_names has
36
- # NAME:LINE, the name of the file only, that the trap tests at each command
37
- declare -A __bdap_breaks=() __bdap_names=()
39
+ # NAME:LINE, the name of the file only, that the trap tests at each command; __bdap_funcs has
40
+ # NAME() of the functions to stop in; __bdap_conds the conditions of FILE:LINE
41
+ declare -A __bdap_breaks=() __bdap_names=() __bdap_funcs=() __bdap_conds=()
38
42
  __bdap_gen=0
39
43
  __bdap_inside=
40
44
  __bdap_prev= # the place of the command before: a line stops when it is come to, not again
45
+ __bdap_fdepth=0 # the depth of the calls at the command before: more is a function entered
46
+ __bdap_fenter= # a function to stop in is entered: its first command is next
41
47
 
42
48
  # the variables of bash itself, and those it makes for a function: not shown
43
49
  __bdap_known=" $(compgen -v | tr '\n' ' ') FUNCNAME BASH_ARGC BASH_ARGV BASH_LINENO BASH_SOURCE COLUMNS LINES "
@@ -93,8 +99,18 @@ __bdap_breaks_load () {
93
99
  done
94
100
  __bdap_breaks=()
95
101
  __bdap_names=()
96
- while read -r __bdap_place; do
102
+ __bdap_funcs=()
103
+ __bdap_conds=()
104
+ while IFS= read -r __bdap_place; do
97
105
  [[ -z $__bdap_place ]] && continue
106
+ if [[ $__bdap_place == *$'\t'* ]]; then
107
+ __bdap_conds["${__bdap_place%%$'\t'*}"]=${__bdap_place#*$'\t'}
108
+ __bdap_place=${__bdap_place%%$'\t'*}
109
+ fi
110
+ if [[ $__bdap_place == *"()" ]]; then
111
+ __bdap_funcs["$__bdap_place"]=1
112
+ continue
113
+ fi
98
114
  __bdap_breaks["$__bdap_place"]=1
99
115
  __bdap_names["${__bdap_place##*/}"]=1
100
116
  done <"$__bdap_dir/breaks"
@@ -126,6 +142,7 @@ __bdap_wait () {
126
142
  __bdap_resume run 0
127
143
  __bdap_breaks=()
128
144
  __bdap_names=()
145
+ __bdap_funcs=()
129
146
  trap - DEBUG
130
147
  }
131
148
 
@@ -138,6 +155,13 @@ __bdap_on_line () {
138
155
  [[ -z $__bdap_word || ${__bdap_text[0]-} == *"$__bdap_word"* ]]
139
156
  }
140
157
 
158
+ # Whether the condition of a breakpoint holds, none always: run in a subshell, it sees the
159
+ # variables of the script and changes none of them
160
+ __bdap_condition () {
161
+ [[ -z $1 ]] && return 0
162
+ ( eval "$1" ) </dev/null >/dev/null 2>&1
163
+ }
164
+
141
165
  # Before a command of the script that may stop: what the script has set, -e, -u, -x, is not
142
166
  # the debugger's, which runs without
143
167
  __bdap_hook () {
@@ -169,9 +193,20 @@ __bdap_check () {
169
193
  next) ((__bdap_d <= __bdap_depth)) && __bdap_reason=step ;;
170
194
  finish) ((__bdap_d < __bdap_depth)) && __bdap_reason=step ;;
171
195
  esac
172
- if [[ -z $__bdap_reason && -n ${__bdap_breaks["$__bdap_file:$__bdap_line"]-} ]] && __bdap_on_line; then
196
+ if [[ -z $__bdap_reason && -n ${__bdap_breaks["$__bdap_file:$__bdap_line"]-} ]] && __bdap_on_line \
197
+ && __bdap_condition "${__bdap_conds["$__bdap_file:$__bdap_line"]-}"; then
173
198
  __bdap_reason=breakpoint
174
199
  fi
200
+ # the first command of a function to stop in: the calls are deeper than at the command
201
+ # before; bash comes first to the line that defines it, with the call as the command
202
+ if [[ -z $__bdap_reason && -n ${__bdap_funcs["${FUNCNAME[2]-}()"]-} ]]; then
203
+ if ((__bdap_d - 1 > __bdap_fdepth)) && [[ $__bdap_command == "${FUNCNAME[2]}"* ]]; then
204
+ __bdap_fenter=1
205
+ elif ((__bdap_d - 1 > __bdap_fdepth)) || [[ -n $__bdap_fenter ]]; then
206
+ __bdap_reason=function
207
+ fi
208
+ fi
209
+ [[ -n $__bdap_reason ]] && __bdap_fenter=
175
210
  # the next command of a line is no new place to stop at
176
211
  [[ -z $__bdap_reason || $__bdap_prev == "$__bdap_line:${BASH_SOURCE[2]}" ]] && return 0
177
212
  # the lines of the frames, the one stopped in first
@@ -195,12 +230,14 @@ trap '__bdap_say "exited $?"' EXIT
195
230
  shopt -s extdebug
196
231
  set -o functrace
197
232
  # the hook only when there may be something to do: a step, a breakpoint in a file of that name
198
- # on the line, what another process has said; else a command costs a test of two files. One
199
- # line: a second one would have LINENO one more
200
- # (with set -u of the script it runs too: nothing unset is read, BASH_SOURCE is empty at the end)
233
+ # on the line or on the function, what another process has said; else a command costs a test
234
+ # of two files. One line: a second one would have LINENO one more
235
+ # (with set -u of the script it runs too: nothing unset is read, BASH_SOURCE is empty at the end,
236
+ # FUNCNAME unset out of any function)
201
237
  __bdap_trap='[[ -n $__bdap_inside || ( $__bdap_mode == run'
202
238
  __bdap_trap+=' && -z ${__bdap_names["${BASH_SOURCE[0]:+${BASH_SOURCE[0]##*/}}:$LINENO"]-}'
239
+ __bdap_trap+=' && -z ${__bdap_funcs["${FUNCNAME[0]-}()"]-}'
203
240
  __bdap_trap+=' && ! -e $__bdap_state && ! -e $__bdap_dir/gen.$((__bdap_gen + 1)) ) ]] || __bdap_hook "$BASH_COMMAND";'
204
- __bdap_trap+=' __bdap_prev=$LINENO:${BASH_SOURCE[0]-}'
241
+ __bdap_trap+=' __bdap_prev=$LINENO:${BASH_SOURCE[0]-} __bdap_fdepth=${FUNCNAME[0]+${#FUNCNAME[@]}}'
205
242
  trap "$__bdap_trap" DEBUG
206
243
  . "$0" "$@"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bash-dap
3
- Version: 0.1
3
+ Version: 0.3
4
4
  Summary: A debug adapter (Debug Adapter Protocol) for bash scripts, written in bash and Python
5
5
  Author-email: Ilia Maslakov <il.smind@gmail.com>
6
6
  License: GPL-3.0-or-later
@@ -18,8 +18,9 @@ Dynamic: license-file
18
18
  # bash-dap
19
19
 
20
20
  A debug adapter of the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/)
21
- for bash scripts: breakpoints, steps into, over and out of functions, the call stack, the
22
- variables (arrays and associative arrays as trees), expressions, pause.
21
+ for bash scripts: breakpoints on lines, with conditions, and on functions, steps into, over and
22
+ out of functions, the call stack, the variables (arrays and associative arrays as trees),
23
+ expressions, pause.
23
24
 
24
25
  It needs nothing but bash 4.1 or newer and Python 3.8 or newer: no bashdb, no Node.js. (The bash
25
26
  of macOS is 3.2: a newer one, from Homebrew say, is given to the launch as *bash*.) The debugger itself is a script
@@ -75,13 +76,20 @@ subshell is a process of its own, so what is said to one, a step, a breakpoint,
75
76
  others through files of that directory, which every process looks at, at a test of two files a
76
77
  command. A script under the debugger runs some 25 times slower than by itself.
77
78
 
79
+ A breakpoint on a function stops at its first command, at each call, in a subshell too;
80
+ what the script defines later, or a file it sources, is found as well, as the name is all
81
+ the debugger looks for.
82
+
83
+ The condition of a breakpoint is a command of bash, `[[ $f == report.pdf ]]` or
84
+ `(( count > 2 ))`: the script stops there when its status is 0. It runs in a subshell, so it
85
+ sees the variables of the script and changes none of them.
86
+
78
87
  When the session ends the adapter ends the script by its process group, the session it has to
79
88
  itself; never by a parent, never anything else.
80
89
 
81
90
  ## Not yet
82
91
 
83
- Conditional breakpoints, breakpoints on functions, a script that another script runs with a
84
- new bash.
92
+ A script that another script runs with a new bash.
85
93
 
86
94
  ## License
87
95
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bash-dap"
7
- version = "0.1"
7
+ version = "0.3"
8
8
  description = "A debug adapter (Debug Adapter Protocol) for bash scripts, written in bash and Python"
9
9
  readme = "README.md"
10
10
  license = { text = "GPL-3.0-or-later" }
@@ -198,6 +198,52 @@ class SessionTest(unittest.TestCase):
198
198
  c.wait_event("terminated")
199
199
  self.assertIn("3 files, 2800 bytes", c.output())
200
200
 
201
+ def test_function_breakpoints(self):
202
+ c = self.client = Client()
203
+ c.request("initialize", {"adapterID": "bash-dap", "linesStartAt1": True})
204
+ c.request("launch", {"program": script("backup.sh"), "cwd": SCRIPTS})
205
+ c.wait_event("initialized")
206
+ response = c.request("setFunctionBreakpoints", {"breakpoints": [
207
+ {"name": "size_of"}, {"name": "not a name"}]})
208
+ verified = [b["verified"] for b in response["body"]["breakpoints"]]
209
+ self.assertEqual(verified, [True, False])
210
+ c.request("configurationDone")
211
+ # at the first command of the function, in the subshell of $(...)
212
+ self.assertEqual(c.wait_event("stopped")["body"]["reason"], "function breakpoint")
213
+ self.assertEqual(c.stop_place(), [("size_of", "backup.sh", 7), ("main", "backup.sh", 15)])
214
+ # once a call, not at each of its commands
215
+ c.request("next", {"threadId": 1})
216
+ c.wait_event("stopped")
217
+ self.assertEqual(c.stop_place()[0], ("size_of", "backup.sh", 8))
218
+ c.request("continue", {"threadId": 1})
219
+ c.wait_event("stopped")
220
+ self.assertEqual(c.evaluate("$f")["body"]["result"], "photo.jpg")
221
+ # taken off, the script runs to its end
222
+ c.request("setFunctionBreakpoints", {"breakpoints": []})
223
+ c.request("continue", {"threadId": 1})
224
+ self.assertEqual(c.wait_event("exited")["body"]["exitCode"], 3)
225
+
226
+ def test_conditional_breakpoint(self):
227
+ c = self.client = Client()
228
+ c.request("initialize", {"adapterID": "bash-dap", "linesStartAt1": True})
229
+ c.request("launch", {"program": script("backup.sh"), "cwd": SCRIPTS})
230
+ c.wait_event("initialized")
231
+ response = c.request("setBreakpoints", {"source": {"path": script("backup.sh")},
232
+ "breakpoints": [
233
+ {"line": 15, "condition": "[[ $f == photo.jpg ]]"},
234
+ # an assignment in a condition changes nothing of the script
235
+ {"line": 16, "condition": "total=1000; false"}]})
236
+ self.assertTrue(all(b["verified"] for b in response["body"]["breakpoints"]))
237
+ c.request("configurationDone")
238
+ self.assertEqual(c.wait_event("stopped")["body"]["reason"], "breakpoint")
239
+ self.assertEqual(c.stop_place(), [("main", "backup.sh", 15)])
240
+ self.assertEqual(c.evaluate("$f")["body"]["result"], "photo.jpg")
241
+ self.assertEqual(c.evaluate("$total")["body"]["result"], "900")
242
+ # no other round stops, and the total is the script's own
243
+ c.request("continue", {"threadId": 1})
244
+ self.assertEqual(c.wait_event("exited")["body"]["exitCode"], 3)
245
+ self.assertIn("3 files, 2800 bytes", c.output())
246
+
201
247
  def test_pipe_sourced_file_and_dollar_zero(self):
202
248
  c = self.client = Client()
203
249
  c.start(script("pipe.sh"), [(script("lib.sh"), 3), (script("pipe.sh"), 6)])
File without changes
File without changes
File without changes