From patchwork Fri Apr 3 14:04:42 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 2105 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775225188; bh=b00zC0ocb2CFekhdhOSGmj8Sd4fgVms8NyPvVooF5c4=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=MPsEC8Z4XRLcALz+FJXtUdn+8loGhd4Ud9uhRGM4fZvtDUdQlwEWuTh2ZMCNT9Zg3 362nR7pPMSzNZQBzr+UbGtawHtsNJ5Mq6vrFDN4amX3uT2Zl5+gTXGFCmP752PBCt+ pgzMR/84JcnfeOEfmSIqOcRLXBh5Mws9H/lOfphdaLL4t7IvtdKt2gOehReiIG146f +FTM+uA25YEsbIov8LYcM715VM6xm1wG9OVevD738I6Xz4ro56pO0RcCl9tzp/RB5O r/zocvizRMPDZ6rEnHav0iDf7SFlnRn6M9VhLpDPgV8RMdANXF3Vc5n6w4/1UXc8mq elTEplTIYXoSQ== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 03BDC6A34E for ; Fri, 3 Apr 2026 08:06:28 -0600 (MDT) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id Qma8J4ldjmg9 for ; Fri, 3 Apr 2026 08:06:27 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775225187; bh=b00zC0ocb2CFekhdhOSGmj8Sd4fgVms8NyPvVooF5c4=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=benqNtosc2+tTkcY8kTSVZXvRd/ko4nwW19Uxjq19GiJbwy5VdXHKPmINnZrDFs9L TwmJFVRaj3xUTxd7sWCTtyhaPn6ys1Io2riERkObjtEpjSPbOrmjWEkHECHAadB0EA rxXZe63RE1zA83Z8RU3e6B63KDFdce1xXK0TXuILb2p/DKgHUscWt/9i4N5rKhviJ5 ncqGjq/Aco+4dhHW0Ox7ehsuxbhw5fwa1K+SEf1QbZPtnjq48rSzOiPALduw+P/T3S UutqirXTx8uC2ltKsgFCQ9M6HSpgcBQ4grrslA8uyTgxT/fH0Ut3PvLwcAHN1pR2uA XdcfP3CmTnbtA== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id E4DBE67EE6 for ; Fri, 3 Apr 2026 08:06:27 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775225185; bh=GnPCY92n988F7ileZfLKUOGOiXddjuDxb0rW9CjtIyY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Dgh+GMADgyBh8wWfgZKNuNPdmTuu+NMJ3pbRppN75Css6IcY9Ls6m9pC5vIA5Q6jV 0aFLg9GlBbreHdBpCUSAUJlE0b1p1uYIkV+P5/Z0C4txr7U9YEru/ujSbLIHUECuzg W4nlnc9FyEeEiVtNoYO3JmedW1cP9jisdT647DTd4kcpRBgNckIQxGY9ZD5JujoUt8 rTG5Um5H4eQ1Cdme3Jgi0uG90J0NQKiVmFSdWy5VPfEoyBwmb6m25ihDp9oAgNxdkC 19fP0kOi81XM60YtWd4B7x/91kiGSlyCTc2un2i6UT++uWJTnkBGo+5OGdPe8R6C73 RXMurqImU8CdQ== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id BB7CB6A34C; Fri, 3 Apr 2026 08:06:25 -0600 (MDT) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id 14L08QEn8l8p; Fri, 3 Apr 2026 08:06:25 -0600 (MDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1775225179; bh=ipZ4+QVv/dDSi06og0m1BhblOeK8MQCfGtDMiGmpJ8k=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=m6RQeJXxdHeJ9KhZqQcOvRh3Dq2yYrOL7QJtID9vfilyauUEN2FfuZPOjYryojIFJ Wn7v6ypcVEo5ndP6eeszmgjQxlYr/qjtS+kl1MOjUXAQQlyLD1IpNCzlvFpgulhFw7 0eIcHvg+zx8Gc4++GZ0uR24hM8pX7yjoXukLmBrUJfBZvX9kIn4FNiAVS+j6zGoq+E KgqJqVYGOdu6AU1exg/74Hj1m5Wjc23ixiEyBalIvZWjT+XqwRxXqsnzGNbtr8F3Uz IMV3l6onRBQrr/k/lu8VXhOFnCnTg0zlhEiOuUwJhz7U2UUNpUEyIqj7vb0K3+hhVj y/TRE4PfhHIew== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id E9B3C67EE6; Fri, 3 Apr 2026 08:06:18 -0600 (MDT) From: Simon Glass To: U-Boot Concept Date: Fri, 3 Apr 2026 08:04:42 -0600 Message-ID: <20260403140523.1998228-21-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260403140523.1998228-1-sjg@u-boot.org> References: <20260403140523.1998228-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: VWOLYTAE6DCMVE5SEVR4YFWGYT7R4ULF X-Message-ID-Hash: VWOLYTAE6DCMVE5SEVR4YFWGYT7R4ULF X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 20/34] vfs: Add tab completion for VFS paths List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add vfs_complete() which matches partial paths against mounted filesystem entries, and vfs_cmd_complete() as a ready-made callback for U_BOOT_CMD_COMPLETE. Wire it up for the existing commands. Signed-off-by: Simon Glass --- cmd/fs.c | 20 ++++++---- fs/vfs.c | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++ include/vfs.h | 26 ++++++++++++ test/dm/fs.c | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 249 insertions(+), 8 deletions(-) diff --git a/cmd/fs.c b/cmd/fs.c index 7231e4efcd1..944d15b4520 100644 --- a/cmd/fs.c +++ b/cmd/fs.c @@ -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", "\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", "[]\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)", "[]\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", " [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 [ [ [ [bytes [pos]]]]]\n" - " - Legacy: load from block device interface" + " - Legacy: load from block device interface", + vfs_cmd_complete ); diff --git a/fs/vfs.c b/fs/vfs.c index 3812706af6c..328b1076280 100644 --- a/fs/vfs.c +++ b/fs/vfs.c @@ -12,6 +12,7 @@ #define LOG_CATEGORY UCLASS_MOUNT #include +#include #include #include #include @@ -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; diff --git a/include/vfs.h b/include/vfs.h index 6e0c67ca031..7c31ded7796 100644 --- a/include/vfs.h +++ b/include/vfs.h @@ -14,6 +14,7 @@ #include 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 diff --git a/test/dm/fs.c b/test/dm/fs.c index f42604fb097..0ef52ca0ee4 100644 --- a/test/dm/fs.c +++ b/test/dm/fs.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -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) {