Skip to content

Commit

Permalink
Merge pull request #978 from flit/bugfix/quit_command
Browse files Browse the repository at this point in the history
Several command fixes
# Conflicts:
#	test/commands_test.py
  • Loading branch information
flit committed Oct 12, 2020
1 parent 03f2596 commit 1a44970
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 80 deletions.
11 changes: 2 additions & 9 deletions pyocd/commands/commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,14 @@
from ..core.helpers import ConnectHelper
from ..core import (exceptions, session)
from ..utility.cmdline import convert_session_options
from ..commands.repl import PyocdRepl
from ..commands.repl import (PyocdRepl, ToolExitException)
from ..commands.execution_context import CommandExecutionContext

LOG = logging.getLogger(__name__)

## Default SWD clock in Hz.
DEFAULT_CLOCK_FREQ_HZ = 1000000

class ToolExitException(Exception):
"""! @brief Special exception indicating the tool should exit.
This exception is only raised by the `exit` command.
"""
pass

class PyOCDCommander(object):
"""! @brief Manages the commander interface.
Expand Down Expand Up @@ -118,7 +111,7 @@ def run_commands(self):
cmd = args[0].lower()

# Handle certain commands without connecting.
needs_connect = (cmd not in ('list', 'help'))
needs_connect = (cmd not in ('list', 'help', 'exit'))

# For others, connect first.
if needs_connect and not did_connect:
Expand Down
2 changes: 1 addition & 1 deletion pyocd/commands/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class ExitCommand(CommandBase):
}

def execute(self):
from .commander import ToolExitException
from .repl import ToolExitException
raise ToolExitException()

class StatusCommand(CommandBase):
Expand Down
96 changes: 70 additions & 26 deletions pyocd/commands/execution_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import sys
import six
import pprint
from collections import namedtuple
import subprocess

from ..core import exceptions
from ..utility.compatibility import get_terminal_size
Expand Down Expand Up @@ -106,6 +108,13 @@ def add_commands(self, commands):
self._value_classes.update(value_classes)
self._value_matcher.add_items(value_names.keys())

CommandInvocation = namedtuple('CommandInvocation', ['cmd', 'args', 'handler'])
"""! @brief Groups the command name with an iterable of args and a handler function.
The handler is a callable that will evaluate the command. It accepts a single argument of the
CommandInvocation instance.
"""

class CommandExecutionContext(object):
"""! @brief Manages command execution.
Expand Down Expand Up @@ -268,33 +277,53 @@ def selected_ap(self):

def process_command_line(self, line):
"""! @brief Run a command line consisting of one or more semicolon-separated commands."""
for cmd in line.split(';'):
self.process_command(cmd.strip())
for invoc in self.parse_command_line(line):
invoc.handler(invoc)

def process_command(self, cmd):
"""! @brief Execute a single command."""
def parse_command_line(self, line):
"""! @brief Generator yielding CommandInvocations for commands separated by semicolons."""
for cmd in self._split_commands(line):
invoc = self.parse_command(cmd)
if invoc is not None:
yield invoc

def _split_commands(self, line):
"""! @brief Generator yielding commands separated by semicolons."""
result = ''
i = 0
while i < len(line):
c = line[i]
# Don't split on escaped semicolons.
if (c == '\\') and (i < len(line) - 1) and (line[i + 1] == ';'):
i += 1
result += ';'
elif c == ';':
yield result
result = ''
else:
result += c
i += 1
if result:
yield result

def parse_command(self, cmdline):
"""! @brief Create a CommandInvocation from a single command."""
cmdline = cmdline.strip()

# Check for Python or shell command lines.
firstChar = (cmd.strip())[0]
if firstChar in '$!':
cmd = cmd[1:].strip()
if firstChar == '$':
self.handle_python(cmd)
elif firstChar == '!':
os.system(cmd)
return
first_char = cmdline[0]
if first_char in '$!':
cmdline = cmdline[1:]
if first_char == '$':
return CommandInvocation(cmdline, None, self.handle_python)
elif first_char == '!':
return CommandInvocation(cmdline, None, self.handle_system)

args = split_command_line(cmd)
# Split command into words.
args = split_command_line(cmdline)
cmd = args[0].lower()
args = args[1:]

# Must have an attached session to run commands, except for certain commands.
assert (self.session is not None) or (cmd in ('list', 'help'))

# Handle register name as command.
if (self.target is not None) and (cmd in self.target.core_registers.by_name):
self.handle_reg([cmd])
return

# Look up shorted unambiguous match for the command name.
matched_command = self._command_set.command_matcher.find_one(cmd)

Expand All @@ -307,12 +336,19 @@ def process_command(self, cmd):
else:
raise exceptions.CommandError("unrecognized command '%s'" % cmd)
return

return CommandInvocation(matched_command, args, self.execute_command)

def execute_command(self, invocation):
"""! @brief Execute a single command."""
# Must have an attached session to run commands, except for certain commands.
assert (self.session is not None) or (cmd in ('list', 'help', 'exit'))

# Run command.
cmd_class = self._command_set.commands[matched_command]
cmd_class = self._command_set.commands[invocation.cmd]
cmd_object = cmd_class(self)
cmd_object.check_arg_count(args)
cmd_object.parse(args)
cmd_object.check_arg_count(invocation.args)
cmd_object.parse(invocation.args)
cmd_object.execute()

def _build_python_namespace(self):
Expand All @@ -330,14 +366,14 @@ def _build_python_namespace(self):
'pyocd': pyocd,
}

def handle_python(self, cmd):
def handle_python(self, invocation):
"""! @brief Evaluate a python expression."""
try:
# Lazily build the python environment.
if self._python_namespace is None:
self._build_python_namespace()

result = eval(cmd, globals(), self._python_namespace)
result = eval(invocation.cmd, globals(), self._python_namespace)
if result is not None:
if isinstance(result, six.integer_types):
self.writei("0x%08x (%d)", result, result)
Expand All @@ -349,3 +385,11 @@ def handle_python(self, cmd):
if self.session.log_tracebacks:
LOG.error("Exception while executing expression: %s", e, exc_info=True)
raise exceptions.CommandError("exception while executing expression: %s" % e)

def handle_system(self, invocation):
"""! @brief Evaluate a system call command."""
try:
output = subprocess.check_output(invocation.cmd, stderr=subprocess.STDOUT, shell=True)
self.write(six.ensure_str(output), end='')
except subprocess.CalledProcessError as err:
six.raise_from(exceptions.CommandError(str(err)), err)
15 changes: 13 additions & 2 deletions pyocd/commands/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@

LOG = logging.getLogger(__name__)

class ToolExitException(Exception):
"""! @brief Special exception indicating the tool should exit.
This exception is only raised by the `exit` command.
"""
pass

class PyocdRepl(object):
"""! @brief Read-Eval-Print-Loop for pyOCD commander."""

Expand All @@ -43,7 +50,6 @@ class PyocdRepl(object):

def __init__(self, command_context):
self.context = command_context
self.last_command = ''

# Enable readline history.
self._history_path = os.environ.get(self.PYOCD_HISTORY_ENV_VAR,
Expand Down Expand Up @@ -79,14 +85,15 @@ def run(self):
# Windows exits with a Ctrl-Z+Return, so there is no need for this.
if os.name != "nt":
print()
except ToolExitException:
pass

def run_one_command(self, line):
"""! @brief Execute a single command line and handle exceptions."""
try:
line = line.strip()
if line:
self.context.process_command_line(line)
self.last_command = line
except KeyboardInterrupt:
print()
except ValueError:
Expand All @@ -99,7 +106,11 @@ def run_one_command(self, line):
traceback.print_exc()
except exceptions.CommandError as e:
print("Error:", e)
except ToolExitException:
# Catch and reraise this exception so it isn't caught by the catchall below.
raise
except Exception as e:
# Catch most other exceptions so they don't cause the REPL to exit.
print("Error:", e)
if session.Session.get_current().log_tracebacks:
traceback.print_exc()
7 changes: 0 additions & 7 deletions pyocd/gdbserver/gdbserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,13 +993,6 @@ def get_symbol(self, name):
return None
return symValue

def handle_remote_command_compatibility(self, cmd):
"""! @brief Handle certain monitor commands for OpenOCD compatibility."""
if cmd.startswith('arm semihosting'):
enable = ('enable' in cmd)
self.session.options['enable_semihosting'] = enable
return False, None

def handle_remote_command(self, cmd):
"""! @brief Pass remote commands to the commander command processor."""
# Convert the command line to a string.
Expand Down
31 changes: 6 additions & 25 deletions pyocd/utility/cmdline.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# pyOCD debugger
# Copyright (c) 2015-2019 Arm Limited
# Copyright (c) 2015-2020 Arm Limited
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -15,6 +15,9 @@
# limitations under the License.

import logging
import shlex
import six

from ..core.target import Target
from ..core.options import OPTIONS_INFO
from ..utility.compatibility import to_str_safe
Expand All @@ -24,34 +27,12 @@
def split_command_line(cmd_line):
"""! @brief Split command line by whitespace, supporting quoted strings."""
result = []
if type(cmd_line) is str:
if isinstance(cmd_line, six.string_types):
args = [cmd_line]
else:
args = cmd_line
for cmd in args:
state = 0
word = ''
open_quote = ''
for c in cmd:
if state == 0:
if c in (' ', '\t', '\r', '\n'):
if word:
result.append(word)
word = ''
elif c in ('"', "'"):
open_quote = c
state = 1
else:
word += c
elif state == 1:
if c == open_quote:
result.append(word)
word = ''
state = 0
else:
word += c
if word:
result.append(word)
result += shlex.split(cmd)
return result

## Map of vector char characters to masks.
Expand Down
2 changes: 0 additions & 2 deletions test/commander_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@
Test,
TestResult,
get_session_options,
get_target_test_params,
binary_to_elf_file,
get_test_binary_path,
PYOCD_DIR,
)

Expand Down
24 changes: 16 additions & 8 deletions test/commands_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ def commands_test(board_id):
context.attach_session(session)

COMMANDS_TO_TEST = [
"list",
"status",
"reset",
"reset halt",
Expand All @@ -118,13 +117,13 @@ def commands_test(board_id):
"write32 0x%08x 0x11223344 0x55667788" % ram_base,
"write16 0x%08x 0xabcd" % (ram_base + 8),
"write8 0x%08x 0 1 2 3 4 5 6" % (ram_base + 10),
"savemem 0x%08x 128 %s" % (boot_start_addr, temp_bin_file),
"loadmem 0x%08x %s" % (ram_base, temp_bin_file),
"loadmem 0x%08x %s" % (boot_start_addr, binary_file),
"load %s" % temp_test_hex_name,
"load %s 0x%08x" % (binary_file, boot_start_addr),
"compare 0x%08x %s" % (ram_base, temp_bin_file),
"compare 0x%08x 32 %s" % (ram_base, temp_bin_file),
"savemem 0x%08x 128 '%s'" % (boot_start_addr, temp_bin_file),
"loadmem 0x%08x '%s'" % (ram_base, temp_bin_file),
"loadmem 0x%08x '%s'" % (boot_start_addr, binary_file),
"load '%s'" % temp_test_hex_name,
"load '%s' 0x%08x" % (binary_file, boot_start_addr),
"compare 0x%08x '%s'" % (ram_base, temp_bin_file),
"compare 0x%08x 32 '%s'" % (ram_base, temp_bin_file),
"fill 0x%08x 128 0xa5" % ram_base,
"fill 16 0x%08x 64 0x55aa" % (ram_base + 64),
"find 0x%08x 128 0xaa 0x55" % ram_base, # find that will pass
Expand Down Expand Up @@ -184,8 +183,17 @@ def commands_test(board_id):
"set step-into-interrupts 1",
"set log info",
"set frequency %d" % test_params['test_clock'],

# Semicolon-separated commands.
'rw 0x%08x ; rw 0x%08x' % (ram_base, ram_base + 4),

# Python and system commands.
'$2+ 2',
'!echo hello',
'!echo hi \; echo there', # using escaped semicolon in a sytem command

# Commands not tested:
# "list",
# "erase", # chip erase
# "unlock",
# "exit",
Expand Down
11 changes: 11 additions & 0 deletions test/unit/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ def test_split_whitespace(self):
assert split_command_line('a\rb') == ['a', 'b']
assert split_command_line('a\nb') == ['a', 'b']
assert split_command_line('a \tb') == ['a', 'b']

@pytest.mark.parametrize(("input", "result"), [
(r'\h\e\l\l\o', ['hello']),
(r'"\"hello\""', ['"hello"']),
('x "a\\"b" y', ['x', 'a"b', 'y']),
('hello"there"', ['hellothere']),
(r"'raw\string'", [r'raw\string']),
('"foo said \\"hi\\"" and \'C:\\baz\'', ['foo said "hi"', 'and', 'C:\\baz'])
])
def test_em(self, input, result):
assert split_command_line(input) == result

class TestConvertVectorCatch(object):
def test_none_str(self):
Expand Down

0 comments on commit 1a44970

Please sign in to comment.