summaryrefslogtreecommitdiffstatshomepage
path: root/tools/mpremote
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mpremote')
-rw-r--r--tools/mpremote/mpremote/commands.py49
-rw-r--r--tools/mpremote/mpremote/main.py56
-rwxr-xr-xtools/mpremote/tests/test_filesystem.sh3
-rw-r--r--tools/mpremote/tests/test_filesystem.sh.exp2
-rwxr-xr-xtools/mpremote/tests/test_fs_tree.sh114
-rw-r--r--tools/mpremote/tests/test_fs_tree.sh.exp225
6 files changed, 442 insertions, 7 deletions
diff --git a/tools/mpremote/mpremote/commands.py b/tools/mpremote/mpremote/commands.py
index 452384728a..428600baf4 100644
--- a/tools/mpremote/mpremote/commands.py
+++ b/tools/mpremote/mpremote/commands.py
@@ -334,6 +334,49 @@ def do_filesystem_recursive_rm(state, path, args):
print(f"removed: '{path}'")
+def human_size(size, decimals=1):
+ for unit in ['B', 'K', 'M', 'G', 'T']:
+ if size < 1024.0 or unit == 'T':
+ break
+ size /= 1024.0
+ return f"{size:.{decimals}f}{unit}" if unit != 'B' else f"{int(size)}"
+
+
+def do_filesystem_tree(state, path, args):
+ """Print a tree of the device's filesystem starting at path."""
+ connectors = ("├── ", "└── ")
+
+ def _tree_recursive(path, prefix=""):
+ entries = state.transport.fs_listdir(path)
+ entries.sort(key=lambda e: e.name)
+ for i, entry in enumerate(entries):
+ connector = connectors[1] if i == len(entries) - 1 else connectors[0]
+ is_dir = entry.st_mode & 0x4000 # Directory
+ size_str = ""
+ # most MicroPython filesystems don't support st_size on directories, reduce clutter
+ if entry.st_size > 0 or not is_dir:
+ if args.size:
+ size_str = f"[{entry.st_size:>9}] "
+ elif args.human:
+ size_str = f"[{human_size(entry.st_size):>6}] "
+ print(f"{prefix}{connector}{size_str}{entry.name}")
+ if is_dir:
+ _tree_recursive(
+ _remote_path_join(path, entry.name),
+ prefix + (" " if i == len(entries) - 1 else "│ "),
+ )
+
+ if not path or path == ".":
+ path = state.transport.exec("import os;print(os.getcwd())").strip().decode("utf-8")
+ if not (path == "." or state.transport.fs_isdir(path)):
+ raise CommandError(f"tree: '{path}' is not a directory")
+ if args.verbose:
+ print(f":{path} on {state.transport.device_name}")
+ else:
+ print(f":{path}")
+ _tree_recursive(path)
+
+
def do_filesystem(state, args):
state.ensure_raw_repl()
state.did_action()
@@ -361,8 +404,8 @@ def do_filesystem(state, args):
# leading ':' if the user included them.
paths = [path[1:] if path.startswith(":") else path for path in paths]
- # ls implicitly lists the cwd.
- if command == "ls" and not paths:
+ # ls and tree implicitly lists the cwd.
+ if command in ("ls", "tree") and not paths:
paths = [""]
try:
@@ -404,6 +447,8 @@ def do_filesystem(state, args):
)
else:
do_filesystem_cp(state, path, cp_dest, len(paths) > 1, not args.force)
+ elif command == "tree":
+ do_filesystem_tree(state, path, args)
except OSError as er:
raise CommandError("{}: {}: {}.".format(command, er.strerror, os.strerror(er.errno)))
except TransportError as er:
diff --git a/tools/mpremote/mpremote/main.py b/tools/mpremote/mpremote/main.py
index b30a1a2135..d122e93c0f 100644
--- a/tools/mpremote/mpremote/main.py
+++ b/tools/mpremote/mpremote/main.py
@@ -181,7 +181,11 @@ def argparse_rtc():
def argparse_filesystem():
- cmd_parser = argparse.ArgumentParser(description="execute filesystem commands on the device")
+ cmd_parser = argparse.ArgumentParser(
+ description="execute filesystem commands on the device",
+ add_help=False,
+ )
+ cmd_parser.add_argument("--help", action="help", help="show this help message and exit")
_bool_flag(cmd_parser, "recursive", "r", False, "recursive (for cp and rm commands)")
_bool_flag(
cmd_parser,
@@ -197,10 +201,26 @@ def argparse_filesystem():
None,
"enable verbose output (defaults to True for all commands except cat)",
)
+ size_group = cmd_parser.add_mutually_exclusive_group()
+ size_group.add_argument(
+ "--size",
+ "-s",
+ default=False,
+ action="store_true",
+ help="show file size in bytes(tree command only)",
+ )
+ size_group.add_argument(
+ "--human",
+ "-h",
+ default=False,
+ action="store_true",
+ help="show file size in a more human readable way (tree command only)",
+ )
+
cmd_parser.add_argument(
"command",
nargs=1,
- help="filesystem command (e.g. cat, cp, sha256sum, ls, rm, rmdir, touch)",
+ help="filesystem command (e.g. cat, cp, sha256sum, ls, rm, rmdir, touch, tree)",
)
cmd_parser.add_argument("path", nargs="+", help="local and remote paths")
return cmd_parser
@@ -355,10 +375,31 @@ _BUILTIN_COMMAND_EXPANSIONS = {
"rmdir": "fs rmdir",
"sha256sum": "fs sha256sum",
"touch": "fs touch",
+ "tree": "fs tree",
# Disk used/free.
"df": [
"exec",
- "import os\nprint('mount \\tsize \\tused \\tavail \\tuse%')\nfor _m in [''] + os.listdir('/'):\n _s = os.stat('/' + _m)\n if not _s[0] & 1 << 14: continue\n _s = os.statvfs(_m)\n if _s[0]:\n _size = _s[0] * _s[2]; _free = _s[0] * _s[3]; print(_m, _size, _size - _free, _free, int(100 * (_size - _free) / _size), sep='\\t')",
+ """
+import os,vfs
+_f = "{:<10}{:>9}{:>9}{:>9}{:>5} {}"
+print(_f.format("filesystem", "size", "used", "avail", "use%", "mounted on"))
+try:
+ _ms = vfs.mount()
+except:
+ _ms = []
+ for _m in [""] + os.listdir("/"):
+ _m = "/" + _m
+ _s = os.stat(_m)
+ if _s[0] & 1 << 14:
+ _ms.append(("<unknown>",_m))
+for _v,_p in _ms:
+ _s = os.statvfs(_p)
+ _sz = _s[0]*_s[2]
+ if _sz:
+ _av = _s[0]*_s[3]
+ _us = 100*(_sz-_av)//_sz
+ print(_f.format(str(_v), _sz, _sz-_av, _av, _us, _p))
+""",
],
# Other shortcuts.
"reset": {
@@ -552,8 +593,13 @@ def main():
command_args = remaining_args
extra_args = []
- # Special case: "fs ls" allowed have no path specified.
- if cmd == "fs" and len(command_args) == 1 and command_args[0] == "ls":
+ # Special case: "fs ls" and "fs tree" can have only options and no path specified.
+ if (
+ cmd == "fs"
+ and len(command_args) >= 1
+ and command_args[0] in ("ls", "tree")
+ and sum(1 for a in command_args if not a.startswith('-')) == 1
+ ):
command_args.append("")
# Use the command-specific argument parser.
diff --git a/tools/mpremote/tests/test_filesystem.sh b/tools/mpremote/tests/test_filesystem.sh
index a29015e987..13c5394f71 100755
--- a/tools/mpremote/tests/test_filesystem.sh
+++ b/tools/mpremote/tests/test_filesystem.sh
@@ -237,3 +237,6 @@ echo -----
# try to delete existing folder in mounted filesystem
$MPREMOTE mount "${TMP}" + rm -rv :package || echo "expect error"
echo -----
+# fs without command should raise error
+$MPREMOTE fs 2>/dev/null || echo "expect error: $?"
+echo -----
diff --git a/tools/mpremote/tests/test_filesystem.sh.exp b/tools/mpremote/tests/test_filesystem.sh.exp
index 3d9d0fe9ae..63411580b7 100644
--- a/tools/mpremote/tests/test_filesystem.sh.exp
+++ b/tools/mpremote/tests/test_filesystem.sh.exp
@@ -272,3 +272,5 @@ rm :package
mpremote: rm -r not permitted on /remote directory
expect error
-----
+expect error: 2
+-----
diff --git a/tools/mpremote/tests/test_fs_tree.sh b/tools/mpremote/tests/test_fs_tree.sh
new file mode 100755
index 0000000000..d7fc433ae6
--- /dev/null
+++ b/tools/mpremote/tests/test_fs_tree.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+set -e
+
+# Creates a RAM disk big enough to hold two copies of the test directory
+# structure.
+cat << EOF > "${TMP}/ramdisk.py"
+class RAMBlockDev:
+ def __init__(self, block_size, num_blocks):
+ self.block_size = block_size
+ self.data = bytearray(block_size * num_blocks)
+
+ def readblocks(self, block_num, buf):
+ for i in range(len(buf)):
+ buf[i] = self.data[block_num * self.block_size + i]
+
+ def writeblocks(self, block_num, buf):
+ for i in range(len(buf)):
+ self.data[block_num * self.block_size + i] = buf[i]
+
+ def ioctl(self, op, arg):
+ if op == 4: # get number of blocks
+ return len(self.data) // self.block_size
+ if op == 5: # get block size
+ return self.block_size
+
+import os
+
+bdev = RAMBlockDev(512, 50)
+os.VfsFat.mkfs(bdev)
+os.mount(bdev, '/ramdisk')
+os.chdir('/ramdisk')
+EOF
+
+# setup
+echo -----
+$MPREMOTE run "${TMP}/ramdisk.py"
+$MPREMOTE resume ls
+
+echo -----
+echo "empty tree"
+$MPREMOTE resume tree :
+
+echo -----
+$MPREMOTE resume touch :a.py + touch :b.py
+$MPREMOTE resume mkdir :foo + touch :foo/aa.py + touch :foo/ba.py
+
+echo "small tree - :"
+$MPREMOTE resume tree :
+
+echo -----
+echo "no path"
+$MPREMOTE resume tree
+
+echo -----
+echo "path = '.'"
+$MPREMOTE resume tree .
+
+echo -----
+echo "path = ':.'"
+$MPREMOTE resume tree :.
+
+
+echo -----
+echo "multiple trees"
+$MPREMOTE resume mkdir :bar + touch :bar/aaa.py + touch :bar/bbbb.py
+$MPREMOTE resume mkdir :bar/baz + touch :bar/baz/aaa.py + touch :bar/baz/bbbb.py
+$MPREMOTE resume mkdir :bar/baz/quux + touch :bar/baz/quux/aaa.py + touch :bar/baz/quux/bbbb.py
+$MPREMOTE resume mkdir :bar/baz/quux/xen + touch :bar/baz/quux/xen/aaa.py
+
+$MPREMOTE resume tree
+
+echo -----
+echo single path
+$MPREMOTE resume tree :foo
+
+echo -----
+echo "multiple paths"
+$MPREMOTE resume tree :foo :bar
+
+echo -----
+echo "subtree"
+$MPREMOTE resume tree bar/baz
+
+echo -----
+echo mountpoint
+$MPREMOTE resume tree :/ramdisk
+
+echo -----
+echo non-existent folder : error
+$MPREMOTE resume tree :not_there || echo "expect error: $?"
+
+echo -----
+echo file : error
+$MPREMOTE resume tree :a.py || echo "expect error: $?"
+
+echo -----
+echo "tree -s :"
+mkdir -p "${TMP}/data"
+dd if=/dev/zero of="${TMP}/data/file1.txt" bs=1 count=20 > /dev/null 2>&1
+dd if=/dev/zero of="${TMP}/data/file2.txt" bs=1 count=204 > /dev/null 2>&1
+dd if=/dev/zero of="${TMP}/data/file3.txt" bs=1 count=1096 > /dev/null 2>&1
+dd if=/dev/zero of="${TMP}/data/file4.txt" bs=1 count=2192 > /dev/null 2>&1
+
+$MPREMOTE resume cp -r "${TMP}/data" :
+$MPREMOTE resume tree -s :
+echo -----
+echo "tree -s"
+$MPREMOTE resume tree -s
+echo -----
+$MPREMOTE resume tree --human :
+echo -----
+$MPREMOTE resume tree -s --human : || echo "expect error: $?"
+echo -----
+
diff --git a/tools/mpremote/tests/test_fs_tree.sh.exp b/tools/mpremote/tests/test_fs_tree.sh.exp
new file mode 100644
index 0000000000..9a67883b1c
--- /dev/null
+++ b/tools/mpremote/tests/test_fs_tree.sh.exp
@@ -0,0 +1,225 @@
+-----
+ls :
+-----
+empty tree
+tree :
+:/ramdisk
+-----
+touch :a.py
+touch :b.py
+mkdir :foo
+touch :foo/aa.py
+touch :foo/ba.py
+small tree - :
+tree :
+:/ramdisk
+├── a.py
+├── b.py
+└── foo
+ ├── aa.py
+ └── ba.py
+-----
+no path
+tree :
+:/ramdisk
+├── a.py
+├── b.py
+└── foo
+ ├── aa.py
+ └── ba.py
+-----
+path = '.'
+tree :.
+:/ramdisk
+├── a.py
+├── b.py
+└── foo
+ ├── aa.py
+ └── ba.py
+-----
+path = ':.'
+tree :.
+:/ramdisk
+├── a.py
+├── b.py
+└── foo
+ ├── aa.py
+ └── ba.py
+-----
+multiple trees
+mkdir :bar
+touch :bar/aaa.py
+touch :bar/bbbb.py
+mkdir :bar/baz
+touch :bar/baz/aaa.py
+touch :bar/baz/bbbb.py
+mkdir :bar/baz/quux
+touch :bar/baz/quux/aaa.py
+touch :bar/baz/quux/bbbb.py
+mkdir :bar/baz/quux/xen
+touch :bar/baz/quux/xen/aaa.py
+tree :
+:/ramdisk
+├── a.py
+├── b.py
+├── bar
+│ ├── aaa.py
+│ ├── baz
+│ │ ├── aaa.py
+│ │ ├── bbbb.py
+│ │ └── quux
+│ │ ├── aaa.py
+│ │ ├── bbbb.py
+│ │ └── xen
+│ │ └── aaa.py
+│ └── bbbb.py
+└── foo
+ ├── aa.py
+ └── ba.py
+-----
+single path
+tree :foo
+:foo
+├── aa.py
+└── ba.py
+-----
+multiple paths
+tree :foo
+:foo
+├── aa.py
+└── ba.py
+tree :bar
+:bar
+├── aaa.py
+├── baz
+│ ├── aaa.py
+│ ├── bbbb.py
+│ └── quux
+│ ├── aaa.py
+│ ├── bbbb.py
+│ └── xen
+│ └── aaa.py
+└── bbbb.py
+-----
+subtree
+tree :bar/baz
+:bar/baz
+├── aaa.py
+├── bbbb.py
+└── quux
+ ├── aaa.py
+ ├── bbbb.py
+ └── xen
+ └── aaa.py
+-----
+mountpoint
+tree :/ramdisk
+:/ramdisk
+├── a.py
+├── b.py
+├── bar
+│ ├── aaa.py
+│ ├── baz
+│ │ ├── aaa.py
+│ │ ├── bbbb.py
+│ │ └── quux
+│ │ ├── aaa.py
+│ │ ├── bbbb.py
+│ │ └── xen
+│ │ └── aaa.py
+│ └── bbbb.py
+└── foo
+ ├── aa.py
+ └── ba.py
+-----
+non-existent folder : error
+tree :not_there
+mpremote: tree: 'not_there' is not a directory
+expect error: 1
+-----
+file : error
+tree :a.py
+mpremote: tree: 'a.py' is not a directory
+expect error: 1
+-----
+tree -s :
+cp ${TMP}/data :
+tree :
+:/ramdisk
+├── [ 0] a.py
+├── [ 0] b.py
+├── bar
+│ ├── [ 0] aaa.py
+│ ├── baz
+│ │ ├── [ 0] aaa.py
+│ │ ├── [ 0] bbbb.py
+│ │ └── quux
+│ │ ├── [ 0] aaa.py
+│ │ ├── [ 0] bbbb.py
+│ │ └── xen
+│ │ └── [ 0] aaa.py
+│ └── [ 0] bbbb.py
+├── data
+│ ├── [ 20] file1.txt
+│ ├── [ 204] file2.txt
+│ ├── [ 1096] file3.txt
+│ └── [ 2192] file4.txt
+└── foo
+ ├── [ 0] aa.py
+ └── [ 0] ba.py
+-----
+tree -s
+tree :
+:/ramdisk
+├── [ 0] a.py
+├── [ 0] b.py
+├── bar
+│ ├── [ 0] aaa.py
+│ ├── baz
+│ │ ├── [ 0] aaa.py
+│ │ ├── [ 0] bbbb.py
+│ │ └── quux
+│ │ ├── [ 0] aaa.py
+│ │ ├── [ 0] bbbb.py
+│ │ └── xen
+│ │ └── [ 0] aaa.py
+│ └── [ 0] bbbb.py
+├── data
+│ ├── [ 20] file1.txt
+│ ├── [ 204] file2.txt
+│ ├── [ 1096] file3.txt
+│ └── [ 2192] file4.txt
+└── foo
+ ├── [ 0] aa.py
+ └── [ 0] ba.py
+-----
+tree :
+:/ramdisk
+├── [ 0] a.py
+├── [ 0] b.py
+├── bar
+│ ├── [ 0] aaa.py
+│ ├── baz
+│ │ ├── [ 0] aaa.py
+│ │ ├── [ 0] bbbb.py
+│ │ └── quux
+│ │ ├── [ 0] aaa.py
+│ │ ├── [ 0] bbbb.py
+│ │ └── xen
+│ │ └── [ 0] aaa.py
+│ └── [ 0] bbbb.py
+├── data
+│ ├── [ 20] file1.txt
+│ ├── [ 204] file2.txt
+│ ├── [ 1.1K] file3.txt
+│ └── [ 2.1K] file4.txt
+└── foo
+ ├── [ 0] aa.py
+ └── [ 0] ba.py
+-----
+usage: fs [--help] [--recursive | --no-recursive] [--force | --no-force]
+ [--verbose | --no-verbose] [--size | --human]
+ command path [path ...] ...
+fs: error: argument --human/-h: not allowed with argument --size/-s
+expect error: 2
+-----