kopia lustrzana https://github.com/dhylands/rshell
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
rodzic
5e1dfd15c4
commit
9a00c237ed
91
README.rst
91
README.rst
|
@ -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
|
||||
|
|
505
rshell/main.py
505
rshell/main.py
|
@ -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():
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
Ładowanie…
Reference in New Issue