@@ -145,11 +145,12 @@ static int do_umount(struct cmd_tbl *cmdtp, int flag, int argc,
return CMD_RET_SUCCESS;
}
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
umount, 2, 1, do_umount,
"unmount a filesystem",
"<mountpoint>\n"
- " - Unmount the filesystem at 'mountpoint'"
+ " - Unmount the filesystem at 'mountpoint'",
+ vfs_cmd_complete
);
static int do_cd(struct cmd_tbl *cmdtp, int flag, int argc,
@@ -167,11 +168,12 @@ static int do_cd(struct cmd_tbl *cmdtp, int flag, int argc,
return CMD_RET_SUCCESS;
}
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
cd, 2, 1, do_cd,
"change working directory",
"[<path>]\n"
- " - Change to 'path' in the VFS (default /)"
+ " - Change to 'path' in the VFS (default /)",
+ vfs_cmd_complete
);
static int do_pwd(struct cmd_tbl *cmdtp, int flag, int argc,
@@ -203,11 +205,12 @@ static int do_ls(struct cmd_tbl *cmdtp, int flag, int argc,
return CMD_RET_SUCCESS;
}
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
ls, 2, 1, do_ls,
"list files in a directory (default cwd)",
"[<path>]\n"
- " - List files at 'path' in the VFS (default cwd)"
+ " - List files at 'path' in the VFS (default cwd)",
+ vfs_cmd_complete
);
static int do_size(struct cmd_tbl *cmdtp, int flag, int argc,
@@ -314,7 +317,7 @@ U_BOOT_CMD(
"List supported filesystem types", ""
);
-U_BOOT_CMD(
+U_BOOT_CMD_COMPLETE(
load, 7, 0, do_load_vfs,
"load binary file from a filesystem",
"<addr> <path> [bytes [pos]]\n"
@@ -324,5 +327,6 @@ U_BOOT_CMD(
" 'pos' gives the file byte position to start reading from.\n"
" If 'pos' is 0 or omitted, the file is read from the start.\n"
"load <interface> [<dev[:part]> [<addr> [<filename> [bytes [pos]]]]]\n"
- " - Legacy: load from block device interface"
+ " - Legacy: load from block device interface",
+ vfs_cmd_complete
);
@@ -12,6 +12,7 @@
#define LOG_CATEGORY UCLASS_MOUNT
#include <blk.h>
+#include <ctype.h>
#include <dir.h>
#include <dm.h>
#include <event.h>
@@ -751,6 +752,112 @@ int fs_mount_blkdev(const char *type, struct blk_desc *desc, int part_num,
return 0;
}
+#ifdef CONFIG_AUTO_COMPLETE
+int vfs_complete(char *buf, const char *path, int maxv, char *cmdv[])
+{
+ char resolved[FILE_MAX_PATH_LEN];
+ struct udevice *vfs, *mnt_dev, *dir;
+ struct fs_dir_stream *strm;
+ const char *subpath, *prefix;
+ struct vfsmount *mnt;
+ struct fs_dirent dent;
+ int n = 0;
+
+ vfs = vfs_root();
+ if (!vfs)
+ return 0;
+
+ path = vfs_path_resolve(vfs_getcwd(), path, resolved, sizeof(resolved));
+ if (!path)
+ return 0;
+
+ /*
+ * Split into directory part and prefix to match.
+ * "/host/ar" -> dir="/host", prefix="ar"
+ * "/host/" -> dir="/host", prefix=""
+ * "/" -> dir="/", prefix=""
+ */
+ prefix = strrchr(path, '/');
+ if (!prefix)
+ return 0;
+ prefix++; /* skip the '/' */
+
+ /* Complete from root mount points */
+ if (prefix == path + 1) {
+ struct dir_uc_priv *uc_priv;
+
+ vfs_foreach_mount(mnt, mnt_dev) {
+ uc_priv = dev_get_uclass_priv(mnt->dir);
+ if (!strncmp(uc_priv->path, prefix, strlen(prefix))) {
+ if (n >= maxv - 1)
+ break;
+ sprintf(buf, "/%s/", uc_priv->path);
+ cmdv[n++] = buf;
+ buf += strlen(buf) + 1;
+ }
+ }
+ cmdv[n] = NULL;
+ return n;
+ }
+
+ /* Resolve the directory portion */
+ if (vfs_find_mount(vfs, path, &mnt_dev, &subpath))
+ return 0;
+
+ mnt = dev_get_uclass_priv(mnt_dev);
+
+ /* Get the directory part of subpath (everything before prefix) */
+ {
+ char dirpath[FILE_MAX_PATH_LEN];
+ int dir_len;
+
+ dir_len = prefix - 1 - (path + strlen(path) - strlen(subpath));
+ if (dir_len < 0)
+ dir_len = 0;
+ memcpy(dirpath, subpath, dir_len);
+ dirpath[dir_len] = '\0';
+
+ if (fs_lookup_dir(mnt->target, dirpath, &dir))
+ return 0;
+ }
+
+ if (dir_open(dir, &strm))
+ return 0;
+
+ while (!dir_read(dir, strm, &dent)) {
+ if (strncmp(dent.name, prefix, strlen(prefix)))
+ continue;
+ if (n >= maxv - 1)
+ break;
+ strcpy(buf, dent.name);
+ if (dent.type == FS_DT_DIR)
+ strcat(buf, "/");
+ cmdv[n++] = buf;
+ buf += strlen(buf) + 1;
+ }
+
+ dir_close(dir, strm);
+ cmdv[n] = NULL;
+
+ return n;
+}
+
+int vfs_cmd_complete(int argc, char *const argv[], char last_char,
+ int maxv, char *cmdv[])
+{
+ static char complete_buf[2048];
+ int space = last_char == '\0' || isblank(last_char);
+
+ /* Complete the last argument as a path */
+ if (space && argc >= 1)
+ return vfs_complete(complete_buf, "", maxv, cmdv);
+ if (!space && argc >= 2)
+ return vfs_complete(complete_buf, argv[argc - 1], maxv, cmdv);
+
+ return 0;
+}
+#endif
+
struct udevice *vfs_root(void)
{
struct udevice *dev;
@@ -14,6 +14,7 @@
#include <dir.h>
struct blk_desc;
+struct cmd_tbl;
struct disk_partition;
struct fs_dirent;
struct fs_statfs;
@@ -291,4 +292,29 @@ int fs_mount_blkdev(const char *type, struct blk_desc *desc, int part_num,
*/
void vfs_print_mounts(void);
+#ifdef CONFIG_AUTO_COMPLETE
+/**
+ * vfs_complete() - Complete a partial VFS path
+ *
+ * Suitable for use as (or called from) a U_BOOT_CMD complete callback.
+ * Fills @cmdv with matching directory entries.
+ *
+ * @buf: Scratch buffer (must be at least FILE_MAX_PATH_LEN bytes)
+ * @path: Partial path to complete (absolute or relative)
+ * @maxv: Maximum number of entries in @cmdv
+ * @cmdv: Output array of matching names (NULL-terminated)
+ * Return: number of matches
+ */
+int vfs_complete(char *buf, const char *path, int maxv, char *cmdv[]);
+
+/**
+ * vfs_cmd_complete() - Complete callback for commands taking a VFS path
+ *
+ * Completes the last argument as a VFS path. Can be passed directly to
+ * U_BOOT_CMD_COMPLETE.
+ */
+int vfs_cmd_complete(int argc, char *const argv[], char last_char,
+ int maxv, char *cmdv[]);
+#endif
+
#endif
@@ -10,6 +10,7 @@
#include <dm.h>
#include <file.h>
#include <fs.h>
+#include <mapmem.h>
#include <os.h>
#include <vfs.h>
#include <dm/test.h>
@@ -138,6 +139,109 @@ static int dm_test_fs_file_write(struct unit_test_state *uts)
DM_TEST(dm_test_fs_file_write, UTF_SCAN_FDT);
#if IS_ENABLED(CONFIG_VFS)
+/* Helper: resolve and compare */
+static int check_resolve(struct unit_test_state *uts, const char *cwd,
+ const char *path, const char *expected)
+{
+ char buf[256];
+ const char *ret;
+
+ ret = vfs_path_resolve(cwd, path, buf, sizeof(buf));
+ ut_assertnonnull(ret);
+ ut_asserteq_str(expected, ret);
+
+ return 0;
+}
+
+/* Test vfs_path_resolve() - no VFS device needed */
+static int dm_test_vfs_path_resolve(struct unit_test_state *uts)
+{
+ char buf[512], buf2[512];
+ int i;
+
+ /* NULL/empty path returns cwd */
+ ut_assertok(check_resolve(uts, "/", NULL, "/"));
+ ut_assertok(check_resolve(uts, "/", "", "/"));
+ ut_assertok(check_resolve(uts, "/host", NULL, "/host"));
+
+ /* Absolute path ignores cwd */
+ ut_assertok(check_resolve(uts, "/host", "/mnt", "/mnt"));
+ ut_assertok(check_resolve(uts, "/host", "/a/b", "/a/b"));
+
+ /* Relative path is joined with cwd */
+ ut_assertok(check_resolve(uts, "/", "foo", "/foo"));
+ ut_assertok(check_resolve(uts, "/host", "file.txt", "/host/file.txt"));
+ ut_assertok(check_resolve(uts, "/host", "sub/file", "/host/sub/file"));
+
+ /* . is resolved */
+ ut_assertok(check_resolve(uts, "/host", ".", "/host"));
+ ut_assertok(check_resolve(uts, "/host", "./a", "/host/a"));
+
+ /* .. is resolved */
+ ut_assertok(check_resolve(uts, "/host", "..", "/"));
+ ut_assertok(check_resolve(uts, "/host/sub", "..", "/host"));
+ ut_assertok(check_resolve(uts, "/host/a", "../b", "/host/b"));
+
+ /* .. at root stays at root */
+ ut_assertok(check_resolve(uts, "/", "..", "/"));
+ ut_assertok(check_resolve(uts, "/", "../..", "/"));
+
+ /* Absolute path with . and .. */
+ ut_assertok(check_resolve(uts, "/", "/host/./sub/..", "/host"));
+ ut_assertok(check_resolve(uts, "/", "/a/b/../c", "/a/c"));
+
+ /* Trailing slash */
+ ut_assertok(check_resolve(uts, "/", "/host/", "/host"));
+
+ /* Stack overflow returns NULL (path with >64 components) */
+ strcpy(buf, "/");
+ for (i = 0; i < 65; i++) {
+ if (i)
+ strcat(buf, "/");
+ strcat(buf, "a");
+ }
+ ut_assertnull(vfs_path_resolve("/", buf, buf2, sizeof(buf2)));
+
+ return 0;
+}
+DM_TEST(dm_test_vfs_path_resolve, 0);
+
+/* Test vfs_complete() tab completion */
+static int dm_test_vfs_complete(struct unit_test_state *uts)
+{
+ struct udevice *vfs, *fsdev, *dir;
+ char buf[2048];
+ char *cmdv[16];
+
+ ut_assertok(vfs_init());
+ vfs = vfs_root();
+ ut_assertnonnull(vfs);
+
+ ut_assertok(uclass_get_device_by_name(UCLASS_FS, "hostfs", &fsdev));
+ ut_assertok(vfs_resolve(vfs, "/host", &dir));
+ ut_assertok(vfs_mount(vfs, dir, fsdev));
+
+ /* Complete from root - should find "host" mount point */
+ ut_assert(vfs_complete(buf, "/", 16, cmdv) >= 1);
+ ut_asserteq_str("/host/", cmdv[0]);
+
+ /* Partial mount name */
+ ut_assert(vfs_complete(buf, "/h", 16, cmdv) >= 1);
+ ut_asserteq_str("/host/", cmdv[0]);
+
+ /* Non-matching prefix should return 0 */
+ ut_asserteq(0, vfs_complete(buf, "/z", 16, cmdv));
+
+ /* Complete with empty prefix shows entries */
+ ut_assert(vfs_complete(buf, "/host/", 16, cmdv) >= 1);
+
+ os_unlink(".comp_test");
+ ut_assertok(vfs_umount_path(vfs, "/host"));
+
+ return 0;
+}
+DM_TEST(dm_test_vfs_complete, UTF_SCAN_FDT);
+
/* Test VFS init and root directory operations */
static int dm_test_vfs_init(struct unit_test_state *uts)
{