rsync and cp -r added. cp, ls, and rm match wildcards. (#23)

* rsync and cp -r added. cp, ls, and rm match wildcards.
* tree_cmp.py added

Thanks - this is awesome.
pull/25/head
Peter Hinch 2017-02-12 18:53:35 +00:00 zatwierdzone przez Dave Hylands
rodzic 5e1dfd15c4
commit 9a00c237ed
4 zmienionych plików z 578 dodań i 144 usunięć

Wyświetl plik

@ -299,13 +299,36 @@ cp
::
cp SOURCE DEST
usage: cp SOURCE DEST
cp SOURCE... DIRECTORY
cp [-r|--recursive] [SOURCE|SRC_DIR]... DIRECTORY
cp [-r|--recursive] PATTERN DIRECTORY
positional arguments:
DEST A destination file
SOURCE File to copy
SRC_DIR Directory to copy
PATTERN File or directory pattern match string e.g. foo/*.py
optional arguments:
-h, --help show this help message and exit
-r, --recursive copy directories recursively
Copies the SOURCE file to DEST. DEST may be a filename or a directory
name. If more than one source file is specified, then the destination
should be a directory.
Directories will only be copied if -r is specified.
A single pattern may be specified, in which case the destination
should be a directory. Pattern matching is performed according to a subset
of the Unix rules (see below).
Recursive copying uses rsync (see below): where a file exists on source
and destination, it will only be copied if the source is newer than the
destination.
echo
----
@ -366,18 +389,23 @@ ls
::
usage: ls [-a] [-l] FILE...
usage: ls [-a] [-l] [FILE|DIRECTORY|PATTERN]...
List directory contents.
positional arguments:
FILE Files or directories to list
FILE File to list (show absolute path)
DIRECTORY Directory (list contents)
PATTERN File or directory pattern match string e.g. foo/*.py
optional arguments:
-h, --help show this help message and exit
-a, --all do not ignore hidden files
-l, --long use a long listing format
Pattern matching is performed according to a subset of the Unix rules
(see below).
mkdir
-----
@ -419,18 +447,56 @@ rm
::
usage: rm [-r|--recursive][-f|--force] FILE...
usage: rm [-f|--force] FILE...
rm [-f|--force] PATTERN
rm -r [-f|--force] PATTERN
rm -r [-f|--force] [FILE|DIRECTORY]...
Removes files or directories (directories must be empty).
Removes files or directories (including their contents).
positional arguments:
FILE File to remove
DIRECTORY Directory to remove (-r required)
PATTERN File matching pattern e.g. *.py
optional arguments:
-h, --help show this help message and exit
-r, --recursive remove directories and their contents recursively
-f, --force ignore nonexistant files and arguments
A single pattern may be specified. Pattern matching is performed
according to a subset of the Unix rules (see below). Directories
can only be removed if the recursive argument is provided.
Beware of rm -r * or worse.
rsync
-----
::
usage: rsync [-m|--mirror] [-n|--dry-run] [-v|--verbose] SRC_DIR DEST_DIR
Recursively synchronises a source directory to a destination.
Directories must exist.
positional arguments:
SRC_DIR Directory containing source files.
DEST_DIR Directory for destination
optional arguments:
-h, --help show this help message and exit
-m, --mirror remove files or directories from destination if
absent from source.
-n, --dry-run make no changes but report what would be done. Implies -v
-v, --verbose report changes made.
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
-----
@ -448,3 +514,18 @@ This will invoke a command, and return back to rshell. Example:
!make deploy
will flash the pyboard.
Pattern Matching
================
This is performed according to a subset of the Unix rules. The limitations
are that wildcards are only allowed in the rightmost directory of a path and
curly bracket {} syntax is unsupported:
::
*.py Match files in current directory with a .py extension
temp/x[0-9]a.* Match temp/x1a.bmp but not temp/x00a.bmp
t*/*.bmp Invalid: will produce an error message
{*.doc,*.pdf} Invalid: will produce an error message

Wyświetl plik

@ -13,7 +13,7 @@
# To run rshell from the git repository, cd into the top level rshell directory
# and run:
# python3 -m rhsell.main
# python3 -m rshell.main
#
# that sets things up so that the "from rshell.xxx" will import from the git
# tree and not from some installed version.
@ -33,6 +33,7 @@ import calendar
import cmd
import inspect
import os
import fnmatch
import select
import serial
import shutil
@ -219,10 +220,10 @@ def is_micropython_usb_device(port):
# We don't check the last digit of the PID since there are 3 possible
# values.
if usb_id.startswith('usb vid:pid=f055:980'):
return True;
return True
# Check for Teensy VID:PID
if usb_id.startswith('usb vid:pid=16c0:0483'):
return True;
return True
return False
@ -359,6 +360,79 @@ def find_macthing_files(match):
return [result_prefix + filename for filename in os.listdir(dirname) if filename.startswith(match_prefix)]
def print_err(*args, end='\n'):
"""Similar to print, but prints to stderr.
"""
print(*args, end=end, file=sys.stderr)
sys.stderr.flush()
def is_pattern(s):
"""Return True if a string contains Unix wildcard pattern characters.
"""
return not set('*?[{').intersection(set(s)) == set()
# Disallow patterns like path/t*/bar* because handling them on remote
# system is difficult without the glob library.
def parse_pattern(s):
"""Parse a string such as 'foo/bar/*.py'
Assumes is_pattern(s) has been called and returned True
1. directory to process
2. pattern to match"""
if '{' in s:
return None, None # Unsupported by fnmatch
if s and s[0] == '~':
s = os.path.expanduser(s)
parts = s.split('/')
absolute = len(parts) > 1 and not parts[0]
if parts[-1] == '': # # Outcome of trailing /
parts = parts[:-1] # discard
if len(parts) == 0:
directory = ''
pattern = ''
else:
directory = '/'.join(parts[:-1])
pattern = parts[-1]
if not is_pattern(directory): # Check for e.g. /abc/*/def
if is_pattern(pattern):
if not directory:
directory = '/' if absolute else '.'
return directory, pattern
return None, None # Invalid or nonexistent pattern
def validate_pattern(fn):
"""On success return an absolute path and a pattern.
Otherwise print a message and return None, None
"""
directory, pattern = parse_pattern(fn)
if directory is None:
print_err("Invalid pattern {}.".format(fn))
return None, None
target = resolve_path(directory)
mode = auto(get_mode, target)
if not mode_exists(mode):
print_err("cannot access '{}': No such file or directory".format(fn))
return None, None
if not mode_isdir(mode):
print_err("cannot access '{}': Not a directory".format(fn))
return None, None
return directory, pattern
def process_pattern(fn):
"""Return a list of paths matching a pattern (or None on error).
"""
directory, pattern = validate_pattern(fn)
if directory is not None:
filenames = fnmatch.filter(auto(listdir, directory), pattern)
if filenames:
return [directory + '/' + sfn for sfn in filenames]
else:
print_err("cannot access '{}': No such file or directory".format(fn))
def resolve_path(path):
"""Resolves path and converts it into an absolute path."""
if path[0] == '~':
@ -609,8 +683,9 @@ def listdir_matches(match):
def listdir_stat(dirname):
"""Returns a list of tuples for each file contained in the named
directory. Each tuple contains the filename, followed by the tuple
returned by calling os.stat on the filename.
directory, or None if the directory does not exist. Each tuple
contains the filename, followed by the tuple returned by
calling os.stat on the filename.
"""
import os
@ -620,12 +695,17 @@ def listdir_stat(dirname):
# Micropython dates are relative to Jan 1, 2000. On the host, time
# is relative to Jan 1, 1970.
return rstat[:7] + tuple(tim + TIME_OFFSET for tim in rstat[7:])
return rstat
return tuple(rstat) # PGH formerly returned an os.stat_result instance
try:
files = os.listdir(dirname)
except OSError:
return None
if dirname == '/':
return tuple((file, stat('/' + file))
for file in os.listdir(dirname))
return tuple((file, stat(dirname + '/' + file))
for file in os.listdir(dirname))
return list((file, stat('/' + file)) for file in files)
return list((file, stat(dirname + '/' + file)) for file in files)
def make_directory(dirname):
@ -655,7 +735,10 @@ def remove_file(filename, recursive=False, force=False):
success = remove_file(filename + '/' + file, recursive, force)
if not success and not force:
return False
os.rmdir(filename)
os.rmdir(filename) # PGH Work like Unix: require recursive
else:
if not force:
return False
else:
os.remove(filename)
except:
@ -669,53 +752,110 @@ def rm(filename, recursive=False, force=False):
return auto(remove_file, filename, recursive, force)
def sync(src_dir, dst_dir, mirror=False, dry_run=False, print_func=None):
def make_dir(dst_dir, dry_run, print_func, recursed):
"""Creates a directory. Produces information in case of dry run.
Isues error where necessary.
"""
parent = os.path.split(dst_dir.rstrip('/'))[0] # Check for nonexistent parent
parent_files = auto(listdir_stat, parent) if parent else True # Relative dir
if dry_run:
if recursed: # Assume success: parent not actually created yet
print_func("Creating directory {}".format(dst_dir))
elif parent_files is None:
print_func("Unable to create {}".format(dst_dir))
return True
if not mkdir(dst_dir):
print_err("Unable to create {}".format(dst_dir))
return False
return True
def rsync(src_dir, dst_dir, mirror, dry_run, print_func, recursed):
"""Synchronizes 2 directory trees."""
src_files = sorted(auto(listdir_stat, src_dir), key=lambda entry: entry[0])
dst_files = sorted(auto(listdir_stat, dst_dir), key=lambda entry: entry[0])
for src_basename, src_stat in src_files:
dst_basename, dst_stat = dst_files[0]
# This test is a hack to avoid errors when accessing /flash. When the
# cache synchronisation issue is solved it should be removed
if not isinstance(src_dir, str) or not len(src_dir):
return
sstat = auto(get_stat, src_dir)
smode = stat_mode(sstat)
if mode_isfile(smode):
print_err('Source is a file not a directory.')
return
d_src = {} # Look up stat tuple from name in current directory
src_files = auto(listdir_stat, src_dir)
if src_files is None:
print_err('Source directory {} does not exist.'.format(src_dir))
return
for name, stat in src_files:
d_src[name] = stat
d_dst = {}
dst_files = auto(listdir_stat, dst_dir)
if dst_files is None: # Directory does not exist
if not recursed:
print_err('Destination directory {} does not exist.'.format(dst_dir))
return
if not make_dir(dst_dir, dry_run, print_func, recursed):
return
else: # dest exists
for name, stat in dst_files:
d_dst[name] = stat
set_dst = set(d_dst.keys())
set_src = set(d_src.keys())
to_add = set_src - set_dst # Files to copy to dest
to_del = set_dst - set_src # To delete from dest
to_upd = set_dst.intersection(set_src) # In both: may need updating
for src_basename in to_add: # Name in source but absent from destination
src_filename = src_dir + '/' + src_basename
dst_filename = dst_dir + '/' + dst_basename
if src_basename < dst_basename:
# Source file/dir which doesn't exist in dest - add it
continue
if src_basename == dst_basename:
src_mode = stat_mode(src_stat)
dst_mode = stat_mode(dst_stat)
if mode_isdir(src_mode):
if mode_isdir(dst_mode):
# src and dst re both directories - recurse
sync(src_filename, dst_filename,
mirror=mirror, dry_run=dry_run, stdout=sys.stdout)
else:
if print_func:
print_func("Source '%s' is a directory and "
"destination '%s' is a file. Ignoring"
% (src_filename, dst_filename))
dst_filename = dst_dir + '/' + src_basename
print_func("Adding %s" % dst_filename)
src_stat = d_src[src_basename]
src_mode = stat_mode(src_stat)
if not dry_run:
if not mode_isdir(src_mode):
cp(src_filename, dst_filename)
if mode_isdir(src_mode):
rsync(src_filename, dst_filename, mirror=mirror, dry_run=dry_run,
print_func=print_func, recursed=True)
if mirror: # May delete
for dst_basename in to_del: # In dest but not in source
dst_filename = dst_dir + '/' + dst_basename
print_func("Removing %s" % dst_filename)
if not dry_run:
rm(dst_filename, recursive=True, force=True)
for src_basename in to_upd: # Names are identical
src_stat = d_src[src_basename]
dst_stat = d_dst[src_basename]
src_filename = src_dir + '/' + src_basename
dst_filename = dst_dir + '/' + src_basename
src_mode = stat_mode(src_stat)
dst_mode = stat_mode(dst_stat)
if mode_isdir(src_mode):
if mode_isdir(dst_mode):
# src and dst are both directories - recurse
rsync(src_filename, dst_filename, mirror=mirror, dry_run=dry_run,
print_func=print_func, recursed=True)
else:
if mode_isdir(dst_mode):
if print_func:
print_func("Source '%s' is a file and "
"destination '%s' is a directory. Ignoring"
% (src_filename, dst_filename))
else:
if stat_mtime(src_stat) > stat_mtime(dst_stat):
if print_func:
print_func('%s is newer than %s - copying'
% (src_filename, dst_filename))
if not dry_run:
cp(src_filename, dst_filename)
continue
while src_basename > dst_basename:
# file exists in dst and not in src
if mirror:
if print_func:
print_func("Removing %s" % dst_filename)
if not dry_run:
rm(dst_filename)
del dst_files[0]
dst_basename, dst_stat = dst_files[0]
msg = "Source '{}' is a directory and destination " \
"'{}' is a file. Ignoring"
print_err(msg.format(src_filename, dst_filename))
else:
if mode_isdir(dst_mode):
msg = "Source '{}' is a file and destination " \
"'{}' is a directory. Ignoring"
print_err(msg.format(src_filename, dst_filename))
else:
if stat_mtime(src_stat) > stat_mtime(dst_stat):
msg = "{} is newer than {} - copying"
print_func(msg.format(src_filename, dst_filename))
if not dry_run:
cp(src_filename, dst_filename)
def set_time(rtc_time):
@ -1449,9 +1589,9 @@ class Shell(cmd.Cmd):
else:
return cmd.Cmd.onecmd(self, line)
except DeviceError as err:
self.print_err(err)
print_err(err)
except ShellError as err:
self.print_err(err)
print_err(err)
except SystemExit:
# When you use -h with argparse it winds up call sys.exit, which
# raises a SystemExit. We intercept it because we don't want to
@ -1459,7 +1599,7 @@ class Shell(cmd.Cmd):
return False
def default(self, line):
self.print_err("Unrecognized command:", line)
print_err("Unrecognized command:", line)
def emptyline(self):
"""We want empty lines to do nothing. By default they would repeat the
@ -1500,12 +1640,6 @@ class Shell(cmd.Cmd):
s = ' '.join(str(arg) for arg in args) + end
file.write(s)
def print_err(self, *args, end='\n'):
"""Similar to print, but prints to stderr.
"""
self.print(*args, end=end, file=self.stderr)
self.stderr.flush()
def create_argparser(self, command):
try:
argparse_args = getattr(self, "argparse_" + command)
@ -1705,10 +1839,10 @@ class Shell(cmd.Cmd):
filename = resolve_path(filename)
mode = auto(get_mode, filename)
if not mode_exists(mode):
self.print_err("Cannot access '%s': No such file" % filename)
print_err("Cannot access '%s': No such file" % filename)
continue
if not mode_isfile(mode):
self.print_err("'%s': is not a file" % filename)
print_err("'%s': is not a file" % filename)
continue
cat(filename, self.stdout)
@ -1738,7 +1872,7 @@ class Shell(cmd.Cmd):
cur_dir = dirname
auto(chdir, dirname)
else:
self.print_err("Directory '%s' does not exist" % dirname)
print_err("Directory '%s' does not exist" % dirname)
def do_connect(self, line):
"""connect TYPE TYPE_PARAMS
@ -1750,12 +1884,12 @@ class Shell(cmd.Cmd):
args = self.line_to_args(line)
num_args = len(args)
if num_args < 1:
self.print_err('Missing connection TYPE')
print_err('Missing connection TYPE')
return
connect_type = args[0]
if connect_type == 'serial':
if num_args < 2:
self.print_err('Missing serial port')
print_err('Missing serial port')
return
port = args[1]
if num_args < 3:
@ -1764,48 +1898,98 @@ class Shell(cmd.Cmd):
try:
baud = int(args[2])
except ValueError:
self.print_err("Expecting baud to be numeric. Found '{}'".format(args[2]))
print_err("Expecting baud to be numeric. Found '{}'".format(args[2]))
return
connect_serial(port, baud)
elif connect_type == 'telnet':
if num_args < 2:
self.print_err('Missing hostname or ip-address')
print_err('Missing hostname or ip-address')
return
name = args[1]
connect_telnet(name)
else:
self.print_err('Unrecognized connection TYPE: {}'.format(connect_type))
print_err('Unrecognized connection TYPE: {}'.format(connect_type))
def complete_cp(self, text, line, begidx, endidx):
return self.filename_complete(text, line, begidx, endidx)
def do_cp(self, line):
"""cp SOURCE DEST
cp SOURCE... DIRECTORY
"""cp SOURCE DEST Copy a single SOURCE file to DEST file.
cp SOURCE... DIRECTORY Copy multiple SOURCE files to a directory.
cp [-r|--recursive] [SOURCE|SOURCE_DIR]... DIRECTORY
cp [-r] PATTERN DIRECTORY Copy matching files to DIRECTORY.
Copies the SOURCE file to DEST. DEST may be a filename or a
directory name. If more than one source file is specified, then
the destination should be a directory.
"""
The destination must be a directory except in the case of
copying a single file. To copy directories -r must be specified.
This will cause directories and their contents to be recursively
copied.
"""
args = self.line_to_args(line)
if len(args) < 2:
self.print_err('Missing desintation file')
if len(args.filenames) < 2:
print_err('Missing destination file')
return
dst_dirname = resolve_path(args[-1])
dst_dirname = resolve_path(args.filenames[-1])
dst_mode = auto(get_mode, dst_dirname)
for src_filename in args[:-1]:
d_dst = {} # Destination directory: lookup stat by basename
if args.recursive:
dst_files = auto(listdir_stat, dst_dirname)
if dst_files is None:
err = "cp: target {} is not a directory"
print_err(err.format(dst_dirname))
return
for name, stat in dst_files:
d_dst[name] = stat
src_filenames = args.filenames[:-1]
# Process PATTERN
sfn = src_filenames[0]
if is_pattern(sfn):
if len(src_filenames) > 1:
print_err("Usage: cp [-r] PATTERN DIRECTORY")
return
src_filenames = process_pattern(sfn)
if src_filenames is None:
return
for src_filename in src_filenames:
if is_pattern(src_filename):
print_err("Only one pattern permitted.")
return
src_filename = resolve_path(src_filename)
src_mode = auto(get_mode, src_filename)
if not mode_exists(src_mode):
self.print_err("File '{}' doesn't exist".format(src_filename))
return False
print_err("File '{}' doesn't exist".format(src_filename))
return
if mode_isdir(src_mode):
if args.recursive: # Copying a directory
src_basename = os.path.basename(src_filename)
dst_filename = dst_dirname + '/' + src_basename
if src_basename in d_dst:
dst_stat = d_dst[src_basename]
dst_mode = stat_mode(dst_stat)
if not mode_isdir(dst_mode):
err = "Destination {} is not a directory"
print_err(err.format(dst_filename))
return
else:
if not mkdir(dst_filename):
err = "Unable to create directory {}"
print_err(err.format(dst_filename))
return
rsync(src_filename, dst_filename, mirror=False, dry_run=False,
print_func=lambda *args: None, recursed=False)
else:
print_err("Omitting directory {}".format(src_filename))
continue
if mode_isdir(dst_mode):
dst_filename = dst_dirname + '/' + os.path.basename(src_filename)
else:
dst_filename = dst_dirname
if not cp(src_filename, dst_filename):
self.print_err("Unable to copy '%s' to '%s'" %
(src_filename, dst_filename))
err = "Unable to copy '{}' to '{}'"
print_err(err.format(src_filename, dst_filename))
break
def do_echo(self, line):
@ -1831,13 +2015,13 @@ class Shell(cmd.Cmd):
environment variable. if none of those are set, then vi will be used.
"""
if len(line) == 0:
self.print_err("Must provide a filename")
print_err("Must provide a filename")
return
filename = resolve_path(line)
dev, dev_filename = get_dev_and_path(filename)
mode = auto(get_mode, filename)
if mode_exists(mode) and mode_isdir(mode):
self.print_err("Unable to edit directory '{}'".format(filename))
print_err("Unable to edit directory '{}'".format(filename))
return
if dev is None:
# File is local
@ -1866,7 +2050,7 @@ class Shell(cmd.Cmd):
testing.
"""
if len(line) == 0:
self.print_err("Must provide a filename")
print_err("Must provide a filename")
return
filename = resolve_path(line)
self.print(auto(get_filesize, filename))
@ -1881,7 +2065,7 @@ class Shell(cmd.Cmd):
for testing.
"""
if len(line) == 0:
self.print_err("Must provide a filename")
print_err("Must provide a filename")
return
filename = resolve_path(line)
mode = auto(get_mode, filename)
@ -1939,7 +2123,7 @@ class Shell(cmd.Cmd):
'filenames',
metavar='FILE',
nargs='*',
help='Files or directories to list'
help='Files directories or patterns to list'
),
)
@ -1947,39 +2131,52 @@ class Shell(cmd.Cmd):
return self.filename_complete(text, line, begidx, endidx)
def do_ls(self, line):
"""ls [-a] [-l] FILE...
"""ls [-a] [-l] [FILE|DIRECTORY|PATTERN]...
PATTERN supports * ? [seq] [!seq] Unix filename matching
List directory contents.
"""
args = self.line_to_args(line)
if len(args.filenames) == 0:
args.filenames = ['.']
for idx in range(len(args.filenames)):
filename = resolve_path(args.filenames[idx])
stat = auto(get_stat, filename)
mode = stat_mode(stat)
if not mode_exists(mode):
self.print_err("Cannot access '%s': No such file or directory" %
filename)
continue
if not mode_isdir(mode):
if args.long:
print_long(filename, stat, self.print)
else:
self.print(filename)
continue
if len(args.filenames) > 1:
if idx > 0:
self.print('')
self.print("%s:" % filename)
files = []
for filename, stat in sorted(auto(listdir_stat, filename),
key=lambda entry: entry[0]):
if is_visible(filename) or args.all:
for idx, fn in enumerate(args.filenames):
if not is_pattern(fn):
filename = resolve_path(fn)
stat = auto(get_stat, filename)
mode = stat_mode(stat)
if not mode_exists(mode):
err = "Cannot access '{}': No such file or directory"
print_err(err.format(filename))
continue
if not mode_isdir(mode):
if args.long:
print_long(filename, stat, self.print)
else:
files.append(decorated_filename(filename, stat))
self.print(filename)
continue
if len(args.filenames) > 1:
if idx > 0:
self.print('')
self.print("%s:" % filename)
pattern = '*'
else: # A pattern was specified
filename, pattern = validate_pattern(fn)
if filename is None: # An error was printed
continue
files = []
ldir_stat = auto(listdir_stat, filename)
if ldir_stat is None:
err = "Cannot access '{}': No such file or directory"
print_err(err.format(filename))
else:
for filename, stat in sorted(ldir_stat,
key=lambda entry: entry[0]):
if is_visible(filename) or args.all:
if fnmatch.fnmatch(filename, pattern):
if args.long:
print_long(filename, stat, self.print)
else:
files.append(decorated_filename(filename, stat))
if len(files) > 0:
print_cols(sorted(files), self.print, self.columns)
@ -1995,7 +2192,7 @@ class Shell(cmd.Cmd):
for filename in args:
filename = resolve_path(filename)
if not mkdir(filename):
self.print_err('Unable to create %s' % filename)
print_err('Unable to create %s' % filename)
def repl_serial_to_stdout(self, dev):
"""Runs as a thread which has a sole purpose of readding bytes from
@ -2054,7 +2251,7 @@ class Shell(cmd.Cmd):
name = ''
dev = find_device_by_name(name)
if not dev:
self.print_err("Unable to find board '%s'" % name)
print_err("Unable to find board '%s'" % name)
return
if line[0:2] == '~ ':
@ -2110,9 +2307,25 @@ class Shell(cmd.Cmd):
# The device is no longer present.
self.print('')
self.stdout.flush()
self.print_err(err)
print_err(err)
repl_thread.join()
argparse_cp = (
add_arg(
'-r', '--recursive',
dest='recursive',
action='store_true',
help='Copy directories recursively',
default=False
),
add_arg(
'filenames',
metavar='FILE',
nargs='+',
help='Pattern or files and directories to copy'
),
)
argparse_rm = (
add_arg(
'-r', '--recursive',
@ -2125,14 +2338,14 @@ class Shell(cmd.Cmd):
'-f', '--force',
dest='force',
action='store_true',
help='ignore nonexistant files and arguments',
help='ignore nonexistent files and arguments',
default=False
),
add_arg(
'filename',
metavar='FILE',
nargs='+',
help='File to remove'
help='Pattern or files and directories to remove'
),
)
@ -2140,16 +2353,32 @@ class Shell(cmd.Cmd):
return self.filename_complete(text, line, begidx, endidx)
def do_rm(self, line):
"""rm [-r|--recursive][-f|--force] FILE...
"""rm [-f|--force] FILE... Remove one or more files
rm [-f|--force] PATTERN Remove multiple files
rm -r [-f|--force] [FILE|DIRECTORY]... Files and/or directories
rm -r [-f|--force] PATTERN Multiple files and/or directories
Removes files or directories. To remove directories (and
any contents) -r must be specified.
Removes files or directories (directories must be empty).
"""
args = self.line_to_args(line)
for filename in args.filename:
filenames = args.filename
# Process PATTERN
sfn = filenames[0]
if is_pattern(sfn):
if len(filenames) > 1:
print_err("Usage: rm [-r] [-f] PATTERN")
return
filenames = process_pattern(sfn)
if filenames is None:
return
for filename in filenames:
filename = resolve_path(filename)
if not rm(filename, recursive=args.recursive, force=args.force):
if not args.force:
self.print_err('Unable to remove', filename)
print_err("Unable to remove '{}'".format(filename))
break
def do_shell(self, line):
@ -2163,21 +2392,29 @@ class Shell(cmd.Cmd):
line = '/bin/bash'
os.system(line)
argparse_sync = (
argparse_rsync = (
add_arg(
'-m', '--mirror',
dest='mirror',
action='store_true',
help="causes files in the destination which don't exist in"
"the source to be removed. Without --mirror only file"
"copies occur, not deletions will occur.",
help="causes files in the destination which don't exist in "
"the source to be removed. Without --mirror only file "
"copies occur. No deletions will take place.",
default=False,
),
add_arg(
'-n', '--dry-run',
dest='dry_run',
action='store_true',
help='shows what would be done without actually performing any file copies',
help='shows what would be done without actually performing '
'any file copies. Implies --verbose.',
default=False
),
add_arg(
'-v', '--verbose',
dest='verbose',
action='store_true',
help='shows what has been done.',
default=False
),
add_arg(
@ -2191,16 +2428,18 @@ class Shell(cmd.Cmd):
help='Destination directory'
),
)
# Do_sync isn't fully implemented/tested yet, hence the leading underscore.
def _do_sync(self, line):
"""sync [-m|--mirror] [-n|--dry-run] SRC_DIR DEST_DIR
def do_rsync(self, line):
"""rsync [-m|--mirror] [-n|--dry-run] [-v|--verbose] SRC_DIR DEST_DIR
Synchronizes a destination directory tree with a source directory tree.
"""
args = self.line_to_args(line)
src_dir = resolve_path(args.src_dir)
dst_dir = resolve_path(args.dst_dir)
sync(src_dir, dst_dir, mirror=args.mirror, dry_run=args.dry_run)
pf = print if args.dry_run or args.verbose else lambda *args : None
rsync(src_dir, dst_dir, mirror=args.mirror, dry_run=args.dry_run,
print_func=pf, recursed=False)
def real_main():

Wyświetl plik

@ -2,12 +2,13 @@
# set -x
LOCAL_DIR='./rshell-test'
LOCAL_DIR="./rshell-test"
RSHELL_DIR=rshell
TESTS_DIR=tests
RSHELL="$(pwd)/${RSHELL_DIR}/main.py --quiet --nocolor"
#RSHELL="$(pwd)/${RSHELL_DIR}/main.py --quiet --nocolor"
RSHELL="$(pwd)/r.py --quiet --nocolor"
MAKE_ALL_BYTES="$(pwd)/${TESTS_DIR}/make_all_bytes.py"
cmp_results() {
@ -76,14 +77,92 @@ EOF
${RSHELL} rm -rf test-out
}
make_tree() {
dirname=$1
content="Pyboard test"
rm -r ${dirname} 2> /dev/null
mkdir ${dirname}
echo ${content} > ${dirname}/file1
echo ${content} > ${dirname}/file2
mkdir ${dirname}/sub
echo ${content} > ${dirname}/sub/file1
echo ${content} > ${dirname}/sub/file2
}
report() {
if [ $1 -eq 0 ]; then
echo $2 " - PASS"
else
echo $2 " - FAIL"
exit 1
fi
echo
}
rsync_test() {
# rsync tests
local LOCAL_ROOT="/tmp"
local REMOTE_ROOT="/sd"
local TMP_REF="pyboard_ref"
local TMP_OUT="pyboard_out"
local TMP_RESULT="pyboard"
local TREE_CMP="$(pwd)/${TESTS_DIR}/tree_cmp.py"
# local FLAGS=""
local FLAGS="--verbose"
echo
make_tree ${LOCAL_ROOT}/${TMP_REF} # Unchanging
make_tree ${LOCAL_ROOT}/${TMP_OUT} # Subject to deletions
THIS_TEST="rsync test basic"
echo Testing ${THIS_TEST}
${RSHELL} rm -r ${REMOTE_ROOT}/${TMP_RESULT} 2> /dev/null
${RSHELL} mkdir ${REMOTE_ROOT}/${TMP_RESULT}
${RSHELL} cp -r ${LOCAL_ROOT}/${TMP_OUT}/* ${REMOTE_ROOT}/${TMP_RESULT}
rm -r ${LOCAL_ROOT}/${TMP_RESULT} 2> /dev/null
mkdir ${LOCAL_ROOT}/${TMP_RESULT}
${RSHELL} cp -r ${REMOTE_ROOT}/${TMP_RESULT}/* ${LOCAL_ROOT}/${TMP_RESULT}
${TREE_CMP} ${LOCAL_ROOT}/${TMP_OUT} ${LOCAL_ROOT}/${TMP_RESULT} ${FLAGS}
report $? "${THIS_TEST}"
# Sync without -m but one file missing from source
THIS_TEST="rsync test no delete"
echo Testing ${THIS_TEST}
rm ${LOCAL_ROOT}/${TMP_OUT}/sub/file1
${RSHELL} rsync ${FLAGS} ${LOCAL_ROOT}/${TMP_OUT} ${REMOTE_ROOT}/${TMP_RESULT}
${RSHELL} rsync ${FLAGS} ${REMOTE_ROOT}/${TMP_RESULT} ${LOCAL_ROOT}/${TMP_RESULT}
${TREE_CMP} ${LOCAL_ROOT}/${TMP_REF} ${LOCAL_ROOT}/${TMP_RESULT} ${FLAGS}
report $? "${THIS_TEST}"
THIS_TEST="rsync test delete file"
echo Testing ${THIS_TEST}
${RSHELL} rsync ${FLAGS} -m ${LOCAL_ROOT}/${TMP_OUT} ${REMOTE_ROOT}/${TMP_RESULT}
${RSHELL} rsync ${FLAGS} -m ${REMOTE_ROOT}//${TMP_RESULT} ${LOCAL_ROOT}/${TMP_RESULT}
${TREE_CMP} ${LOCAL_ROOT}/${TMP_OUT} ${LOCAL_ROOT}/${TMP_RESULT} ${FLAGS}
report $? "${THIS_TEST}"
THIS_TEST="rsync test delete directory"
echo Testing ${THIS_TEST}
rm -r ${LOCAL_ROOT}/${TMP_OUT}/sub
${RSHELL} rsync ${FLAGS} -m ${LOCAL_ROOT}/${TMP_OUT} ${REMOTE_ROOT}/${TMP_RESULT}
${RSHELL} rsync ${FLAGS} -m ${REMOTE_ROOT}/${TMP_RESULT} ${LOCAL_ROOT}/${TMP_RESULT}
${TREE_CMP} ${LOCAL_ROOT}/${TMP_OUT} ${LOCAL_ROOT}/${TMP_RESULT} ${FLAGS}
report $? "${THIS_TEST}"
echo Removing test data
${RSHELL} rm -r ${REMOTE_ROOT}/${TMP_RESULT}
rm -r ${LOCAL_ROOT}/${TMP_REF} 2> /dev/null
rm -r ${LOCAL_ROOT}/${TMP_OUT} 2> /dev/null
rm -r ${LOCAL_ROOT}/${TMP_RESULT} 2> /dev/null
}
test_dir ${LOCAL_DIR}
echo
ROOT_DIRS=$(${RSHELL} ls /pyboard)
for root_dir in ${ROOT_DIRS}; do
test_dir /${root_dir}rshell-test
done
rsync_test
echo
echo "PASS"

35
tests/tree_cmp.py 100755
Wyświetl plik

@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Test program which compares two directory trees on local system
args:
full path to directory 1
full path to directory 2
(optional) --verbose
Exit value 0 on success 1 on fail
"""
import sys
import os
def main():
verbose = len(sys.argv) == 4 and sys.argv[3] == '--verbose'
source = sys.argv[1]
dest = sys.argv[2]
source_list = [x[1:] for x in os.walk(source)]
dest_list = [x[1:] for x in os.walk(dest)]
lens = len(source_list)
if lens != len(dest_list):
if verbose:
print('Length fail ', lens, len(dest_list))
sys.exit(1)
for subdir in range(lens):
if False in map(lambda str0, str1 : str0 == str1, source_list[subdir], dest_list[subdir]):
if verbose:
print('Subdir fail ', source_list[subdir], dest_list[subdir])
sys.exit(1)
if verbose:
print('Directories match')
sys.exit(0)
if __name__ == '__main__':
main()