Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement macros #113

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 127 additions & 19 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,29 +100,34 @@ following displayed:
Remote Shell for a MicroPython board.

positional arguments:
cmd Optional command to execute
cmd Optional command to execute

optional arguments:
-h, --help show this help message and exit
-b BAUD, --baud BAUD Set the baudrate used (default = 115200)
--buffer-size BUFFER_SIZE
Set the buffer size used for transfers (default = 512)
-p PORT, --port PORT Set the serial port to use (default '/dev/ttyACM0')
--rts RTS Set the RTS state (default '')
--dtr DTR Set the DTR state (default '')
-u USER, --user USER Set username to use (default 'micro')
-w PASSWORD, --password PASSWORD
-h, --help show this help message and exit
-b BAUD, --baud BAUD Set the baudrate used (default = 115200)
--buffer-size BUFFER_SIZE
Set the buffer size used for transfers (default = 512
for USB, 32 for UART)
-p PORT, --port PORT Set the serial port to use (default 'None')
--rts RTS Set the RTS state (default '')
--dtr DTR Set the DTR state (default '')
-u USER, --user USER Set username to use (default 'micro')
-w PASSWORD, --password PASSWORD
Set password to use (default 'python')
-e EDITOR, --editor EDITOR
-e EDITOR, --editor EDITOR
Set the editor to use (default 'vi')
-f FILENAME, --file FILENAME
-f FILENAME, --file FILENAME
Specifies a file of commands to process.
-d, --debug Enable debug features
-n, --nocolor Turn off colorized output
--wait How long to wait for serial port
--binary Enable binary file transfer
--timing Print timing information about each command
--quiet Turns off some output (useful for testing)
-m MACRO_MODULE, --macros MACRO_MODULE
Specify a macro module.
-d, --debug Enable debug features
-n, --nocolor Turn off colorized output
-l, --list Display serial ports
-a, --ascii ASCII encode binary files for transfer
--wait WAIT Seconds to wait for serial port
--timing Print timing information about each command
-V, --version Reports the version and exits.
--quiet Turns off some output (useful for testing)

You can specify the default serial port using the RSHELL_PORT environment
variable.
Expand Down Expand Up @@ -163,6 +168,12 @@ be used.
Specifies a file of rshell commands to process. This allows you to
create a script which executes any valid rshell commands.

-m MACRO_MODULE, --macros MACRO_MODULE
--------------------------------------

Specifies a Python module containing macros which may be expanded at
the rshell prompt. See below for the file format and its usage.

-n, --nocolor
-------------

Expand Down Expand Up @@ -535,7 +546,6 @@ Synchronisation is performed by comparing the date and time of source
and destination files. Files are copied if the source is newer than the
destination.


shell
-----

Expand All @@ -554,6 +564,104 @@ This will invoke a command, and return back to rshell. Example:

will flash the pyboard.

lm
--

::

usage lm [macro_name]

If issued without an arg lists available macros, otheriwse lists the
specified macro.

m
-

::

usage m macro_name [arg0 [arg1 [args...]]]

Expands the named macro, passing it any supplied positional args,
and executes it.

Macros
======

Macros enable short strings to be expanded into longer ones and enable
common names to be used to similar or different effect across multiple
projects. They also enable rshell functionality to be enhanced, e.g.
by adding an mv command to move files.

Macros are defined by macro modules: these comprise Python code. Their
filenames must conform to Python rules and they should be located on the
Python path.

If a module named rshell_macros.py is found, this will be imported.

If rshell is invoked with -m MACRO_MODULE argument, the specified Python
module will (if found) be imported and its macros appended to any in
rshell_macros.py.

Macro modules should contain a dict named macros. Each key should be a string
specifying the name; the value may be a string (being the expansion) or a
2-tuple. In the case of a tuple, element[0] is the expansion with
element[1] being an arbitrary help string.

The macro name must conform to Python rules for dict keys. The expansion
string may not contain newline characters. Multi-line expansions are
supported by virtue of rshell's ; operator: see the mv macro below.

The expansion string may contain argument specifiers compatible with the
Python string format operator. This enables arguments passed to the macro
to be expanded in ways which are highly flexible.

Because macro modules contain Python code there are a variety of ways to
configure them: for example macro modules can impport other macro modules.
One approach is to use rshell_macros.py to define global macros applicable
to all projects with project-specific macros being appended with the -m
command line argument.

rshell_macros.py:

::

macros = {}
macros['..'] = 'cd ..'
macros['...'] = 'cd ../..'
macros['ll'] = 'ls -al {}', 'List a directory (default current one)'
macros['lf'] = 'ls -al /flash/{}', 'List contents of target flash'
macros['lsd'] = 'ls -al /sd/{}'
macros['lpb'] = 'ls -al /pyboard/{}'
macros['mv'] = 'cp {0} {1}; rm {0}', 'File move command'

A module specific to the foo project:

::

macros['sync'] = 'rsync foo/ /flash/foo/', 'Sync foo project'
macros['run'] = 'repl ~ import foo.demos.{}', 'Run foo demo e.g. > m run hst'
macros['proj'] = 'ls -l /flash/foo/{}', 'List directory in foo project.'
macros['cpf'] = 'cp foo/py/{} /flash/foo/py/; repl ~ import foo.demos.{}', 'Copy a py file, run a demo'
macros['cpd'] = 'cp foo/demos/{0}.py /flash/foo/demos/; repl ~ import foo.demos.{0}', 'Copy a demo file and run it'

If at the rshell prompt we issue

::

> m cpd hst

this will expand to

::

> cp foo/demos/hst.py /flash/foo/demos/; repl ~ import foo.demos.hst

In general args should be regarded as mandatory. Any excess args supplied
will be ignored. In the case where no args are passed to a macro that
expects some, the macro will be expanded and run with each placeholder
replaced with an empty string. This enables directory listing macros such as
'proj' above to run with zero or one argument.

Pattern Matching
================

Expand Down
124 changes: 124 additions & 0 deletions rshell/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@
import shlex
import itertools
from serial.tools import list_ports
import importlib

import traceback

# Macros: values are strings or 2-lists
macros = {}

if sys.platform == 'win32':
EXIT_STR = 'Use the exit command to exit rshell.'
else:
Expand Down Expand Up @@ -147,6 +151,8 @@
RTS = ''
DTR = ''

MACFILE_NAME = 'rshell_macros'

# It turns out that just because pyudev is installed doesn't mean that
# it can actually be used. So we only bother to try if we're running
# under linux.
Expand Down Expand Up @@ -2019,6 +2025,78 @@ def do_boards(self, _):
else:
print('No boards connected')

def complete_m(self, text, line, begidx, endidx): # Assume macro works on filenames for completion.
return self.filename_complete(text, line, begidx, endidx)

def do_m(self, line):
"""m macro_name [[arg0] arg1]...

Expand a macro with args and run.
"""
msg = '''usage m MACRO [[[arg0] arg1] ...]
Run macro MACRO with any required arguments.
In general args should be regarded as mandatory. In the case where
no args are passed to a macro expecting some the macro will be run
with each placeholder replaced with an empty string.'''
tokens = [x for x in line.split(' ') if x]
cmd = tokens[0]
if cmd in macros:
data = macros[cmd]
if isinstance(data, str):
go = data
else: # List or tuple: discard help
go = data[0]
if len(tokens) > 1: # Args to process
try:
to_run = go.format(*tokens[1:])
except:
print_err('Macro {} is incompatible with args {}'.format(cmd, tokens[1:]))
return
else:
to_run = go.format('')
self.print(to_run)
self.onecmd(to_run)
elif cmd == '-h' or cmd == '--help':
self.print(msg)
else:
print_err('Unknown macro', cmd)

def do_lm(self, line):
"""lm

Lists available macros.
"""
msg = '''usage lm [MACRO]
list loaded macros.
Positional argument
MACRO the name of a single macro to list.'''
if not macros:
print_err('No macros loaded.')
return
def add_col(l):
d = macros[l]
sp = ''
if isinstance(d, str):
cols.append((l, sp, d, '', ''))
else:
cols.append((l, sp, d[0], sp, d[1]))

l = line.strip()
cols = []
if l:
if l in macros:
add_col(l)
elif l == '-h' or l == '--help':
self.print(msg)
return
else:
print_err('Unknown macro {}'.format(l))
return
else:
for l in macros:
add_col(l)
column_print('<<<<<', cols, self.print)

def complete_cat(self, text, line, begidx, endidx):
return self.filename_complete(text, line, begidx, endidx)

Expand Down Expand Up @@ -2660,6 +2738,41 @@ def do_rsync(self, line):
rsync(src_dir, dst_dir, mirror=args.mirror, dry_run=args.dry_run,
print_func=pf, recursed=False, sync_hidden=args.all)

def load_macros(mod_name=None):
"""Update the global macros dict.
Validate on import to avoid runtime errors as far as possible.
"""
default = mod_name is None
if default:
mod_name = MACFILE_NAME
try:
mmod = importlib.import_module(mod_name)
except ImportError:
if not default:
print("Can't import macro module", mod_name)
return False
except:
print("Macro module {} is invalid".format(mod_name))
return False

if hasattr(mmod, 'macros') and isinstance(mmod.macros, dict):
md = mmod.macros
else:
print('Macro module {} has missing or invalid dict.'.format(mod_name))
return False
for k, v in md.items():
if isinstance(v, str):
s = v
elif isinstance(v, tuple) or isinstance(v, list):
s = v[0]
else:
print('Macro {} is invalid.'.format(k))
return False
if '\n' in s:
print('Invalid multi-line macro {} {}'.format(k, s))
return False
macros.update(md)
return True

def real_main():
"""The main program."""
Expand Down Expand Up @@ -2747,6 +2860,11 @@ def real_main():
dest="filename",
help="Specifies a file of commands to process."
)
parser.add_argument(
"-m", "--macros",
dest="macro_module",
help="Specify a macro module."
)
parser.add_argument(
"-d", "--debug",
dest="debug",
Expand Down Expand Up @@ -2854,6 +2972,12 @@ def real_main():
global FAKE_INPUT_PROMPT
FAKE_INPUT_PROMPT = True

if load_macros(): # Attempt to load default macro module
print('Default macro file {} loaded OK.'.format(MACFILE_NAME))
if args.macro_module: # Attempt to load a macro module
if load_macros(args.macro_module):
print('Macro file {} loaded OK.'.format(args.macro_module))

global ASCII_XFER
ASCII_XFER = args.ascii_xfer
RTS = args.rts
Expand Down