From ef85f4c6faebb5b0b9e5f1bf26207f2555b443e6 Mon Sep 17 00:00:00 2001 From: Warp Agent Date: Thu, 6 Aug 2026 02:09:16 +0000 Subject: [PATCH 1/3] Add completion spec: Vagrant (vagrant) Adds a hand-written Fig-style spec for the vagrant CLI covering every subcommand reported by `vagrant list-commands` (Vagrant 2.4.9), the nested box/cloud/plugin/snapshot groups, and each subcommand's flags. Two generators back the dynamic values fish completes: `vagrant_machines` reads the project's .vagrant/machines directory (described by provider) and `vagrant_boxes` reads the Vagrant home box directory, un-escaping the -VAGRANTSLASH-/-VAGRANTCOLON- placeholders Vagrant stores names with. Co-Authored-By: Warp --- command-signatures/json/vagrant.json | 2678 +++++++++++++++++ command-signatures/src/generators/mod.rs | 4 + command-signatures/src/generators/vagrant.rs | 85 + .../src/generators/vagrant_tests.rs | 210 ++ 4 files changed, 2977 insertions(+) create mode 100644 command-signatures/json/vagrant.json create mode 100644 command-signatures/src/generators/vagrant.rs create mode 100644 command-signatures/src/generators/vagrant_tests.rs diff --git a/command-signatures/json/vagrant.json b/command-signatures/json/vagrant.json new file mode 100644 index 00000000..08069fe0 --- /dev/null +++ b/command-signatures/json/vagrant.json @@ -0,0 +1,2678 @@ +{ + "name": "vagrant", + "description": "Build and manage reproducible virtual machine environments", + "options": [ + { + "name": [ + "-h", + "--help" + ], + "description": "Print this help", + "isPersistent": true + }, + { + "name": [ + "-v", + "--version" + ], + "description": "Display Vagrant version", + "isPersistent": true + }, + { + "name": "--color", + "description": "Enable color output", + "isPersistent": true, + "exclusiveOn": ["--no-color"] + }, + { + "name": "--no-color", + "description": "Disable color output", + "isPersistent": true, + "exclusiveOn": ["--color"] + }, + { + "name": "--machine-readable", + "description": "Enable machine readable output", + "isPersistent": true + }, + { + "name": "--debug", + "description": "Enable debug output", + "isPersistent": true + }, + { + "name": "--timestamp", + "description": "Enable timestamps on log output", + "isPersistent": true + }, + { + "name": "--debug-timestamp", + "description": "Enable debug output with timestamps", + "isPersistent": true + }, + { + "name": "--no-tty", + "description": "Enable non-interactive output", + "isPersistent": true + } + ], + "subcommands": [ + { + "name": "autocomplete", + "description": "Manage autocomplete installation on host", + "subcommands": [ + { + "name": "install", + "description": "Install shell autocomplete for Vagrant", + "args": { + "name": "SHELL", + "isOptional": true, + "isVariadic": true, + "suggestions": [ + "bash", + "zsh" + ] + }, + "options": [ + { + "name": [ + "-b", + "--bash" + ], + "description": "Install bash autocomplete" + }, + { + "name": [ + "-z", + "--zsh" + ], + "description": "Install zsh autocomplete" + } + ] + } + ] + }, + { + "name": "box", + "description": "Manage boxes: installation, removal, etc", + "subcommands": [ + { + "name": "add", + "description": "Add a box to the local box catalog", + "args": { + "name": "ADDRESS", + "description": "Vagrant Cloud box name, URL, local .box file, or local catalog .json file", + "template": ["filepaths"] + }, + "options": [ + { + "name": [ + "-c", + "--clean" + ], + "description": "Clean any temporary download files" + }, + { + "name": [ + "-f", + "--force" + ], + "description": "Overwrite an existing box if it exists" + }, + { + "name": "--insecure", + "description": "Do not validate SSL certificates" + }, + { + "name": "--cacert", + "description": "CA certificate for SSL download", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + }, + { + "name": "--capath", + "description": "CA certificate directory for SSL download", + "args": { + "name": "DIR", + "template": ["folders"] + } + }, + { + "name": "--cert", + "description": "A client SSL cert, if needed", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + }, + { + "name": "--location-trusted", + "description": "Trust the 'Location' header from HTTP redirects and reuse the initial credentials" + }, + { + "name": [ + "-a", + "--architecture" + ], + "description": "Architecture the box should satisfy", + "args": { + "name": "ARCH", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": "--provider", + "description": "Provider the box should satisfy", + "args": { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": "--box-version", + "description": "Constrain version of the added box", + "args": { + "name": "VERSION" + } + }, + { + "name": "--checksum", + "description": "Checksum for the box (only when adding a box file directly)", + "args": { + "name": "CHECKSUM" + } + }, + { + "name": "--checksum-type", + "description": "Checksum type (only when adding a box file directly)", + "args": { + "name": "TYPE", + "suggestions": [ + "md5", + "sha1", + "sha256" + ] + } + }, + { + "name": "--name", + "description": "Name of the box (only when adding a box file directly)", + "args": { + "name": "BOX" + } + } + ] + }, + { + "name": "list", + "description": "List all installed boxes", + "options": [ + { + "name": [ + "-i", + "--box-info" + ], + "description": "Display additional information about the boxes" + } + ] + }, + { + "name": "outdated", + "description": "Check whether a newer version of the boxes in use is available", + "options": [ + { + "name": "--global", + "description": "Check all boxes installed" + }, + { + "name": [ + "-f", + "--force" + ], + "description": "Force checks for latest box updates" + }, + { + "name": "--insecure", + "description": "Do not validate SSL certificates" + }, + { + "name": "--cacert", + "description": "CA certificate for SSL download", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + }, + { + "name": "--capath", + "description": "CA certificate directory for SSL download", + "args": { + "name": "DIR", + "template": ["folders"] + } + }, + { + "name": "--cert", + "description": "A client SSL cert, if needed", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + } + ] + }, + { + "name": "prune", + "description": "Remove old versions of installed boxes", + "options": [ + { + "name": [ + "-p", + "--provider" + ], + "description": "The specific provider type for the boxes to destroy", + "args": { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": [ + "-n", + "--dry-run" + ], + "description": "Only print the boxes that would be removed" + }, + { + "name": "--name", + "description": "The specific box name to check for outdated versions", + "args": { + "name": "NAME", + "generatorName": "vagrant_boxes" + } + }, + { + "name": [ + "-f", + "--force" + ], + "description": "Destroy without confirmation even when box is in use" + }, + { + "name": [ + "-k", + "--keep-active-boxes" + ], + "description": "When combined with --force, keep boxes still actively in use" + } + ] + }, + { + "name": "remove", + "description": "Remove a box from the local box catalog", + "args": { + "name": "NAME", + "generatorName": "vagrant_boxes" + }, + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Remove without confirmation" + }, + { + "name": [ + "-a", + "--architecture" + ], + "description": "The specific architecture for the box to remove", + "args": { + "name": "ARCH", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": "--provider", + "description": "The specific provider type for the box to remove", + "args": { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": "--box-version", + "description": "The specific version of the box to remove", + "args": { + "name": "VERSION" + } + }, + { + "name": "--all", + "description": "Remove all available versions of the box" + }, + { + "name": "--all-providers", + "description": "Remove all providers within a version of the box" + }, + { + "name": "--all-architectures", + "description": "Remove all architectures within a provider a version of the box" + } + ] + }, + { + "name": "repackage", + "description": "Repackage an installed box into a .box file", + "args": [ + { + "name": "NAME", + "generatorName": "vagrant_boxes" + }, + { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + }, + { + "name": "VERSION" + } + ] + }, + { + "name": "update", + "description": "Update the boxes in use in the current environment", + "options": [ + { + "name": "--box", + "description": "Update a specific box", + "args": { + "name": "BOX", + "generatorName": "vagrant_boxes" + } + }, + { + "name": "--architecture", + "description": "Update box with specific architecture", + "args": { + "name": "ARCHITECTURE", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": "--provider", + "description": "Update box with specific provider", + "args": { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": [ + "-f", + "--force" + ], + "description": "Overwrite an existing box if it exists" + }, + { + "name": "--insecure", + "description": "Do not validate SSL certificates" + }, + { + "name": "--cacert", + "description": "CA certificate for SSL download", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + }, + { + "name": "--capath", + "description": "CA certificate directory for SSL download", + "args": { + "name": "DIR", + "template": ["folders"] + } + }, + { + "name": "--cert", + "description": "A client SSL cert, if needed", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + } + ] + }, + { + "name": "help", + "description": "Show help for a box subcommand", + "args": { + "name": "SUBCOMMAND", + "isOptional": true, + "suggestions": [ + "add", + "list", + "outdated", + "prune", + "remove", + "repackage", + "update" + ] + } + } + ] + }, + { + "name": "cloud", + "description": "Manage everything related to Vagrant Cloud", + "subcommands": [ + { + "name": "auth", + "description": "Authorization operations on Vagrant Cloud", + "subcommands": [ + { + "name": "login", + "description": "Log in to Vagrant Cloud", + "options": [ + { + "name": [ + "-c", + "--check" + ], + "description": "Check if currently logged in" + }, + { + "name": [ + "-d", + "--description" + ], + "description": "Set description for the Vagrant Cloud token", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-t", + "--token" + ], + "description": "Set the Vagrant Cloud token", + "args": { + "name": "TOKEN" + } + }, + { + "name": [ + "-u", + "--username" + ], + "description": "Vagrant Cloud username or email address", + "args": { + "name": "USERNAME_OR_EMAIL" + } + } + ] + }, + { + "name": "logout", + "description": "Log out of Vagrant Cloud" + }, + { + "name": "whoami", + "description": "Display currently logged in user", + "args": { + "name": "TOKEN", + "isOptional": true + } + } + ] + }, + { + "name": "box", + "description": "Manage a Vagrant box entry on Vagrant Cloud", + "subcommands": [ + { + "name": "create", + "description": "Create an empty box entry on Vagrant Cloud", + "args": { + "name": "organization/box-name" + }, + "options": [ + { + "name": [ + "-d", + "--description" + ], + "description": "Full description of the box", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-s", + "--short-description" + ], + "description": "Short description of the box", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-p", + "--private" + ], + "description": "Make box private", + "exclusiveOn": ["--no-private"] + }, + { + "name": "--no-private", + "description": "Make box public", + "exclusiveOn": [ + "-p", + "--private" + ] + } + ] + }, + { + "name": "delete", + "description": "Delete a box entry on Vagrant Cloud", + "args": { + "name": "organization/box-name" + }, + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Do not prompt for deletion confirmation", + "exclusiveOn": ["--no-force"] + }, + { + "name": "--no-force", + "description": "Prompt for deletion confirmation", + "exclusiveOn": [ + "-f", + "--force" + ] + } + ] + }, + { + "name": "show", + "description": "Display a box's attributes on Vagrant Cloud", + "args": { + "name": "organization/box-name" + }, + "options": [ + { + "name": "--architectures", + "description": "Filter results by architecture support (can be defined multiple times)", + "args": { + "name": "ARCH", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": "--versions", + "description": "Display box information for a specific version (can be defined multiple times)", + "args": { + "name": "VERSION" + } + }, + { + "name": "--providers", + "description": "Filter results by provider support (can be defined multiple times)", + "args": { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": "--auth", + "description": "Authenticate with Vagrant Cloud if required before searching", + "exclusiveOn": ["--no-auth"] + }, + { + "name": "--no-auth", + "description": "Do not authenticate with Vagrant Cloud before searching", + "exclusiveOn": ["--auth"] + } + ] + }, + { + "name": "update", + "description": "Update a box entry on Vagrant Cloud", + "args": { + "name": "organization/box-name" + }, + "options": [ + { + "name": [ + "-d", + "--description" + ], + "description": "Full description of the box", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-s", + "--short-description" + ], + "description": "Short description of the box", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-p", + "--private" + ], + "description": "Make box private", + "exclusiveOn": ["--no-private"] + }, + { + "name": "--no-private", + "description": "Make box public", + "exclusiveOn": [ + "-p", + "--private" + ] + } + ] + } + ] + }, + { + "name": "provider", + "description": "Manage a Vagrant box's provider options", + "subcommands": [ + { + "name": "create", + "description": "Create a provider entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "provider-name", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + }, + { + "name": "version" + }, + { + "name": "url", + "isOptional": true + } + ], + "options": [ + { + "name": [ + "-a", + "--architecture" + ], + "description": "Architecture of guest box (defaults to current host architecture)", + "args": { + "name": "ARCH", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": [ + "-c", + "--checksum" + ], + "description": "Checksum of the box for this provider (requires --checksum-type)", + "args": { + "name": "CHECKSUM_VALUE" + }, + "dependsOn": ["--checksum-type"] + }, + { + "name": [ + "-C", + "--checksum-type" + ], + "description": "Type of checksum used (requires --checksum)", + "args": { + "name": "TYPE", + "suggestions": [ + "md5", + "sha1", + "sha256", + "sha384", + "sha512" + ] + }, + "dependsOn": ["--checksum"] + }, + { + "name": "--default-architecture", + "description": "Mark as default architecture for specific provider", + "exclusiveOn": ["--no-default-architecture"] + }, + { + "name": "--no-default-architecture", + "description": "Do not mark as default architecture for specific provider", + "exclusiveOn": ["--default-architecture"] + } + ] + }, + { + "name": "delete", + "description": "Delete a provider entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "provider-name", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + }, + { + "name": "version" + }, + { + "name": "architecture", + "isOptional": true, + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + ], + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Force deletion of box version provider without confirmation", + "exclusiveOn": ["--no-force"] + }, + { + "name": "--no-force", + "description": "Prompt before deleting the box version provider", + "exclusiveOn": [ + "-f", + "--force" + ] + } + ] + }, + { + "name": "update", + "description": "Update a provider entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "provider-name", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + }, + { + "name": "version" + }, + { + "name": "architecture", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + }, + { + "name": "url", + "isOptional": true + } + ], + "options": [ + { + "name": [ + "-a", + "--architecture" + ], + "description": "Update architecture value of guest box", + "args": { + "name": "ARCH", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": [ + "-c", + "--checksum" + ], + "description": "Checksum of the box for this provider (requires --checksum-type)", + "args": { + "name": "CHECKSUM_VALUE" + }, + "dependsOn": ["--checksum-type"] + }, + { + "name": [ + "-C", + "--checksum-type" + ], + "description": "Type of checksum used (requires --checksum)", + "args": { + "name": "TYPE", + "suggestions": [ + "md5", + "sha1", + "sha256", + "sha384", + "sha512" + ] + }, + "dependsOn": ["--checksum"] + }, + { + "name": "--default-architecture", + "description": "Mark as default architecture for specific provider", + "exclusiveOn": ["--no-default-architecture"] + }, + { + "name": "--no-default-architecture", + "description": "Do not mark as default architecture for specific provider", + "exclusiveOn": ["--default-architecture"] + } + ] + }, + { + "name": "upload", + "description": "Upload a box file to Vagrant Cloud for a specific provider", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "provider-name", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + }, + { + "name": "version" + }, + { + "name": "architecture", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + }, + { + "name": "box-file", + "template": ["filepaths"] + } + ], + "options": [ + { + "name": [ + "-D", + "--direct" + ], + "description": "Upload asset directly to backend storage", + "exclusiveOn": ["--no-direct"] + }, + { + "name": "--no-direct", + "description": "Upload asset through Vagrant Cloud", + "exclusiveOn": [ + "-D", + "--direct" + ] + } + ] + } + ] + }, + { + "name": "publish", + "description": "Create and release a new Vagrant box on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "version" + }, + { + "name": "provider-name", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + }, + { + "name": "provider-file", + "isOptional": true, + "template": ["filepaths"] + } + ], + "options": [ + { + "name": [ + "-a", + "--architecture" + ], + "description": "Architecture of guest box (defaults to current host architecture)", + "args": { + "name": "ARCH", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": "--url", + "description": "Remote URL to download this provider (cannot be used with provider-file)", + "args": { + "name": "URL" + } + }, + { + "name": [ + "-d", + "--description" + ], + "description": "Full description of box", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": "--version-description", + "description": "Description of the version to create", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-f", + "--force" + ], + "description": "Disable confirmation to create or update box", + "exclusiveOn": ["--no-force"] + }, + { + "name": "--no-force", + "description": "Confirm before creating or updating the box", + "exclusiveOn": [ + "-f", + "--force" + ] + }, + { + "name": [ + "-p", + "--private" + ], + "description": "Make box private", + "exclusiveOn": ["--no-private"] + }, + { + "name": "--no-private", + "description": "Make box public", + "exclusiveOn": [ + "-p", + "--private" + ] + }, + { + "name": [ + "-r", + "--release" + ], + "description": "Release box", + "exclusiveOn": ["--no-release"] + }, + { + "name": "--no-release", + "description": "Do not release box", + "exclusiveOn": [ + "-r", + "--release" + ] + }, + { + "name": [ + "-s", + "--short-description" + ], + "description": "Short description of the box", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-c", + "--checksum" + ], + "description": "Checksum of the box for this provider (requires --checksum-type)", + "args": { + "name": "CHECKSUM_VALUE" + }, + "dependsOn": ["--checksum-type"] + }, + { + "name": [ + "-C", + "--checksum-type" + ], + "description": "Type of checksum used (requires --checksum)", + "args": { + "name": "TYPE", + "suggestions": [ + "md5", + "sha1", + "sha256", + "sha384", + "sha512" + ] + }, + "dependsOn": ["--checksum"] + }, + { + "name": "--direct-upload", + "description": "Upload asset directly to backend storage", + "exclusiveOn": ["--no-direct-upload"] + }, + { + "name": "--no-direct-upload", + "description": "Upload asset through Vagrant Cloud", + "exclusiveOn": ["--direct-upload"] + }, + { + "name": "--default-architecture", + "description": "Mark as default architecture for specific provider", + "exclusiveOn": ["--no-default-architecture"] + }, + { + "name": "--no-default-architecture", + "description": "Do not mark as default architecture for specific provider", + "exclusiveOn": ["--default-architecture"] + } + ] + }, + { + "name": "search", + "description": "Search Vagrant Cloud for available boxes", + "args": { + "name": "query" + }, + "options": [ + { + "name": [ + "-a", + "--architecture" + ], + "description": "Filter search results to a single architecture (defaults to all)", + "args": { + "name": "ARCH", + "suggestions": [ + "amd64", + "arm64", + "i386", + "unknown" + ] + } + }, + { + "name": [ + "-j", + "--json" + ], + "description": "Format results in JSON" + }, + { + "name": "--page", + "description": "The page to display (default: 1)", + "args": { + "name": "PAGE" + } + }, + { + "name": [ + "-s", + "--short" + ], + "description": "Show a simple list of box names" + }, + { + "name": [ + "-o", + "--order" + ], + "description": "Order to display results (default: desc)", + "args": { + "name": "ORDER", + "suggestions": [ + "desc", + "asc" + ] + } + }, + { + "name": [ + "-l", + "--limit" + ], + "description": "Max number of search results (default: 25)", + "args": { + "name": "LIMIT" + } + }, + { + "name": [ + "-p", + "--provider" + ], + "description": "Filter search results to a single provider (defaults to all)", + "args": { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": "--sort-by", + "description": "Field to sort results on (default: downloads)", + "args": { + "name": "SORT", + "suggestions": [ + "created", + "downloads", + "updated" + ] + } + }, + { + "name": "--auth", + "description": "Authenticate with Vagrant Cloud if required before searching", + "exclusiveOn": ["--no-auth"] + }, + { + "name": "--no-auth", + "description": "Do not authenticate with Vagrant Cloud before searching", + "exclusiveOn": ["--auth"] + } + ] + }, + { + "name": "version", + "description": "Manage a Vagrant box's versions", + "subcommands": [ + { + "name": "create", + "description": "Create a version entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "version" + } + ], + "options": [ + { + "name": [ + "-d", + "--description" + ], + "description": "A description for this version", + "args": { + "name": "DESCRIPTION" + } + } + ] + }, + { + "name": "delete", + "description": "Delete a version entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "version" + } + ], + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Force deletion without confirmation", + "exclusiveOn": ["--no-force"] + }, + { + "name": "--no-force", + "description": "Prompt before deleting the version", + "exclusiveOn": [ + "-f", + "--force" + ] + } + ] + }, + { + "name": "release", + "description": "Release a version entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "version" + } + ], + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Release without confirmation", + "exclusiveOn": ["--no-force"] + }, + { + "name": "--no-force", + "description": "Prompt before releasing the version", + "exclusiveOn": [ + "-f", + "--force" + ] + } + ] + }, + { + "name": "revoke", + "description": "Revoke a version entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "version" + } + ], + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Force revocation without confirmation", + "exclusiveOn": ["--no-force"] + }, + { + "name": "--no-force", + "description": "Prompt before revoking the version", + "exclusiveOn": [ + "-f", + "--force" + ] + } + ] + }, + { + "name": "update", + "description": "Update a version entry on Vagrant Cloud", + "args": [ + { + "name": "organization/box-name" + }, + { + "name": "version" + } + ], + "options": [ + { + "name": [ + "-d", + "--description" + ], + "description": "A description for this version", + "args": { + "name": "DESCRIPTION" + } + } + ] + } + ] + } + ] + }, + { + "name": "destroy", + "description": "Stop and delete all traces of the vagrant machine", + "args": { + "name": "name|id", + "description": "Machine name or id (defaults to every machine in the environment)", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Destroy without confirmation" + }, + { + "name": "--parallel", + "description": "Enable parallelism if the provider supports it (automatically enables force)", + "exclusiveOn": ["--no-parallel"] + }, + { + "name": "--no-parallel", + "description": "Disable parallelism", + "exclusiveOn": ["--parallel"] + }, + { + "name": [ + "-g", + "--graceful" + ], + "description": "Gracefully poweroff of VM" + } + ] + }, + { + "name": "docker-exec", + "description": "Attach to an already-running docker container", + "args": [ + { + "name": "name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + { + "name": "command", + "description": "Command to run inside the container, after a `--` separator", + "isOptional": true, + "isVariadic": true, + "isCommand": true + } + ], + "options": [ + { + "name": "--detach", + "description": "Run in the background", + "exclusiveOn": ["--no-detach"] + }, + { + "name": "--no-detach", + "description": "Run in the foreground", + "exclusiveOn": ["--detach"] + }, + { + "name": [ + "-i", + "--interactive" + ], + "description": "Keep STDIN open even if not attached", + "exclusiveOn": ["--no-interactive"] + }, + { + "name": "--no-interactive", + "description": "Do not keep STDIN open", + "exclusiveOn": [ + "-i", + "--interactive" + ] + }, + { + "name": [ + "-t", + "--tty" + ], + "description": "Allocate a pty", + "exclusiveOn": ["--no-tty"] + }, + { + "name": [ + "-u", + "--user" + ], + "description": "User or UID", + "args": { + "name": "USER" + } + }, + { + "name": "--prefix", + "description": "Prefix output with machine names", + "exclusiveOn": ["--no-prefix"] + }, + { + "name": "--no-prefix", + "description": "Do not prefix output with machine names", + "exclusiveOn": ["--prefix"] + } + ] + }, + { + "name": "docker-logs", + "description": "Output the logs from the Docker container", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--follow", + "description": "Continue streaming in log output", + "exclusiveOn": ["--no-follow"] + }, + { + "name": "--no-follow", + "description": "Stop after the current log output", + "exclusiveOn": ["--follow"] + }, + { + "name": "--prefix", + "description": "Prefix output with machine names", + "exclusiveOn": ["--no-prefix"] + }, + { + "name": "--no-prefix", + "description": "Do not prefix output with machine names", + "exclusiveOn": ["--prefix"] + } + ] + }, + { + "name": "docker-run", + "description": "Run a one-off command in the context of a container", + "args": { + "name": "command", + "isOptional": true, + "isVariadic": true, + "isCommand": true + }, + "options": [ + { + "name": "--detach", + "description": "Run in the background", + "exclusiveOn": ["--no-detach"] + }, + { + "name": "--no-detach", + "description": "Run in the foreground", + "exclusiveOn": ["--detach"] + }, + { + "name": [ + "-t", + "--tty" + ], + "description": "Allocate a pty", + "exclusiveOn": ["--no-tty"] + }, + { + "name": [ + "-r", + "--rm" + ], + "description": "Remove container after execution", + "exclusiveOn": ["--no-rm"] + }, + { + "name": "--no-rm", + "description": "Keep container after execution", + "exclusiveOn": [ + "-r", + "--rm" + ] + } + ] + }, + { + "name": "global-status", + "description": "Output status of Vagrant environments for this user", + "options": [ + { + "name": "--prune", + "description": "Prune invalid entries" + } + ] + }, + { + "name": "halt", + "description": "Stop the vagrant machine", + "args": { + "name": "name|id", + "description": "Machine name or id (defaults to every machine in the environment)", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Force shut down (equivalent of pulling power)" + } + ] + }, + { + "name": "help", + "description": "Show the help for a subcommand", + "args": { + "name": "COMMAND", + "isOptional": true + } + }, + { + "name": "init", + "description": "Initialize a new Vagrant environment by creating a Vagrantfile", + "args": [ + { + "name": "name", + "description": "Box to use in the generated Vagrantfile", + "isOptional": true, + "generatorName": "vagrant_boxes" + }, + { + "name": "url", + "description": "URL the box should be fetched from", + "isOptional": true + } + ], + "options": [ + { + "name": "--box-version", + "description": "Version of the box to add", + "args": { + "name": "VERSION" + } + }, + { + "name": [ + "-f", + "--force" + ], + "description": "Overwrite existing Vagrantfile" + }, + { + "name": [ + "-m", + "--minimal" + ], + "description": "Use minimal Vagrantfile template (no help comments). Ignored with --template" + }, + { + "name": "--output", + "description": "Output path for the Vagrantfile ('-' for stdout)", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + }, + { + "name": "--template", + "description": "Path to custom Vagrantfile template", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + } + ] + }, + { + "name": "list-commands", + "description": "Output all available Vagrant subcommands, even non-primary ones" + }, + { + "name": "login", + "description": "Log in to Vagrant Cloud (deprecated alias for `vagrant cloud auth login`)", + "options": [ + { + "name": [ + "-c", + "--check" + ], + "description": "Check if currently logged in" + }, + { + "name": [ + "-d", + "--description" + ], + "description": "Set description for the Vagrant Cloud token", + "args": { + "name": "DESCRIPTION" + } + }, + { + "name": [ + "-t", + "--token" + ], + "description": "Set the Vagrant Cloud token", + "args": { + "name": "TOKEN" + } + }, + { + "name": [ + "-u", + "--username" + ], + "description": "Vagrant Cloud username or email address", + "args": { + "name": "USERNAME_OR_EMAIL" + } + } + ] + }, + { + "name": "package", + "description": "Package a running vagrant environment into a box", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--base", + "description": "Name of a VM in VirtualBox to package as a base box (VirtualBox only)", + "args": { + "name": "NAME" + } + }, + { + "name": "--output", + "description": "Name of the file to output", + "args": { + "name": "NAME", + "template": ["filepaths"] + } + }, + { + "name": "--include", + "description": "Comma separated additional files to package with the box", + "args": { + "name": "FILE,FILE..", + "template": ["filepaths"] + } + }, + { + "name": "--info", + "description": "Path to a custom info.json file containing additional box information", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + }, + { + "name": "--vagrantfile", + "description": "Vagrantfile to package with the box", + "args": { + "name": "FILE", + "template": ["filepaths"] + } + } + ] + }, + { + "name": "plugin", + "description": "Manage plugins: install, uninstall, update, etc", + "subcommands": [ + { + "name": "install", + "description": "Install a plugin", + "args": { + "name": "name", + "isVariadic": true + }, + "options": [ + { + "name": "--entry-point", + "description": "The name of the entry point file for loading the plugin", + "args": { + "name": "NAME" + } + }, + { + "name": "--plugin-clean-sources", + "description": "Remove all plugin sources defined so far (including defaults)" + }, + { + "name": "--plugin-source", + "description": "Add a RubyGems repository source", + "args": { + "name": "PLUGIN_SOURCE" + } + }, + { + "name": "--plugin-version", + "description": "Install a specific version of the plugin", + "args": { + "name": "PLUGIN_VERSION" + } + }, + { + "name": "--local", + "description": "Install plugin for local project only" + }, + { + "name": "--verbose", + "description": "Enable verbose output for plugin installation" + } + ] + }, + { + "name": "license", + "description": "Install a license for a proprietary Vagrant plugin", + "args": [ + { + "name": "name" + }, + { + "name": "license-file", + "template": ["filepaths"] + } + ] + }, + { + "name": "list", + "description": "List installed plugins", + "options": [ + { + "name": "--local", + "description": "Include local project plugins" + } + ] + }, + { + "name": "uninstall", + "description": "Uninstall plugins", + "args": { + "name": "name", + "isVariadic": true + }, + "options": [ + { + "name": "--local", + "description": "Remove plugin from local project" + } + ] + }, + { + "name": "update", + "description": "Update plugins", + "args": { + "name": "names", + "isOptional": true, + "isVariadic": true + }, + "options": [ + { + "name": "--local", + "description": "Update plugin in local project" + } + ] + }, + { + "name": "repair", + "description": "Repair installed plugins", + "options": [ + { + "name": "--local", + "description": "Repair plugins in local project" + } + ] + }, + { + "name": "expunge", + "description": "Remove all user installed plugin information", + "options": [ + { + "name": "--force", + "description": "Do not prompt for confirmation" + }, + { + "name": "--local", + "description": "Include plugins from local project for expunge" + }, + { + "name": "--local-only", + "description": "Only expunge local project plugins" + }, + { + "name": "--global-only", + "description": "Only expunge global plugins" + }, + { + "name": "--reinstall", + "description": "Reinstall current plugins after expunge" + } + ] + } + ] + }, + { + "name": "port", + "description": "Display information about guest port mappings", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--guest", + "description": "Output the host port that maps to the given guest port", + "args": { + "name": "PORT" + } + } + ] + }, + { + "name": "powershell", + "description": "Connect to machine via PowerShell remoting", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": [ + "-c", + "--command" + ], + "description": "Execute a PowerShell command directly", + "args": { + "name": "COMMAND", + "isCommand": true + } + }, + { + "name": [ + "-e", + "--elevated" + ], + "description": "Execute a PowerShell command with elevated permissions" + } + ] + }, + { + "name": "provider", + "description": "Show provider for this environment", + "options": [ + { + "name": "--install", + "description": "Install the provider if possible" + }, + { + "name": "--usable", + "description": "Check if the named provider is usable" + } + ], + "args": { + "name": "PROVIDER", + "isOptional": true, + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": "provision", + "description": "Provision the vagrant machine", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--provision-with", + "description": "Enable only certain provisioners, by type or by name", + "args": { + "name": "x,y,z" + } + } + ] + }, + { + "name": "push", + "description": "Deploy code in this environment to a configured destination", + "args": { + "name": "strategy", + "isOptional": true + } + }, + { + "name": "rdp", + "description": "Connect to machine via RDP", + "args": [ + { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + { + "name": "extra args", + "description": "Extra RDP client arguments, after a `--` separator", + "isOptional": true, + "isVariadic": true + } + ] + }, + { + "name": "reload", + "description": "Restart vagrant machine, load new Vagrantfile configuration", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--provision", + "description": "Enable provisioning", + "exclusiveOn": ["--no-provision"] + }, + { + "name": "--no-provision", + "description": "Disable provisioning", + "exclusiveOn": ["--provision"] + }, + { + "name": "--provision-with", + "description": "Enable only certain provisioners, by type or by name", + "args": { + "name": "x,y,z" + } + }, + { + "name": [ + "-f", + "--force" + ], + "description": "Force shut down (equivalent of pulling power)" + } + ] + }, + { + "name": "resume", + "description": "Resume a suspended vagrant machine", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--provision", + "description": "Enable provisioning", + "exclusiveOn": ["--no-provision"] + }, + { + "name": "--no-provision", + "description": "Disable provisioning", + "exclusiveOn": ["--provision"] + }, + { + "name": "--provision-with", + "description": "Enable only certain provisioners, by type or by name", + "args": { + "name": "x,y,z" + } + } + ] + }, + { + "name": "rsync", + "description": "Sync rsync synced folders to remote machine", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--rsync-chown", + "description": "Use rsync to modify ownership", + "exclusiveOn": ["--no-rsync-chown"] + }, + { + "name": "--no-rsync-chown", + "description": "Do not use rsync to modify ownership", + "exclusiveOn": ["--rsync-chown"] + } + ] + }, + { + "name": "rsync-auto", + "description": "Sync rsync synced folders automatically when files change", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--poll", + "description": "Force polling filesystem (slow)", + "exclusiveOn": ["--no-poll"] + }, + { + "name": "--no-poll", + "description": "Do not force polling filesystem", + "exclusiveOn": ["--poll"] + }, + { + "name": "--rsync-chown", + "description": "Use rsync to modify ownership", + "exclusiveOn": ["--no-rsync-chown"] + }, + { + "name": "--no-rsync-chown", + "description": "Do not use rsync to modify ownership", + "exclusiveOn": ["--rsync-chown"] + } + ] + }, + { + "name": "snapshot", + "description": "Manage snapshots: saving, restoring, etc", + "subcommands": [ + { + "name": "delete", + "description": "Delete a snapshot taken previously with snapshot save", + "args": [ + { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + { + "name": "name" + } + ] + }, + { + "name": "list", + "description": "List all snapshots taken for a machine", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + } + }, + { + "name": "pop", + "description": "Restore state that was pushed onto the snapshot stack", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--provision", + "description": "Enable provisioning", + "exclusiveOn": ["--no-provision"] + }, + { + "name": "--no-provision", + "description": "Disable provisioning", + "exclusiveOn": ["--provision"] + }, + { + "name": "--provision-with", + "description": "Enable only certain provisioners, by type or by name", + "args": { + "name": "x,y,z" + } + }, + { + "name": "--no-delete", + "description": "Don't delete the snapshot after the restore" + }, + { + "name": "--no-start", + "description": "Don't start the snapshot after the restore" + } + ] + }, + { + "name": "push", + "description": "Take a snapshot and push it onto the stack of states", + "args": { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + } + }, + { + "name": "restore", + "description": "Restore a snapshot taken previously with snapshot save", + "args": [ + { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + { + "name": "name" + } + ], + "options": [ + { + "name": "--provision", + "description": "Enable provisioning", + "exclusiveOn": ["--no-provision"] + }, + { + "name": "--no-provision", + "description": "Disable provisioning", + "exclusiveOn": ["--provision"] + }, + { + "name": "--provision-with", + "description": "Enable only certain provisioners, by type or by name", + "args": { + "name": "x,y,z" + } + }, + { + "name": "--no-start", + "description": "Don't start the snapshot after the restore" + } + ] + }, + { + "name": "save", + "description": "Take a snapshot of the current state of the machine", + "args": [ + { + "name": "vm-name", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + { + "name": "name" + } + ], + "options": [ + { + "name": [ + "-f", + "--force" + ], + "description": "Replace snapshot without confirmation" + } + ] + } + ] + }, + { + "name": "ssh", + "description": "Connect to machine via SSH", + "args": [ + { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + { + "name": "extra ssh args", + "description": "Extra ssh arguments, after a `--` separator", + "isOptional": true, + "isVariadic": true + } + ], + "options": [ + { + "name": [ + "-c", + "--command" + ], + "description": "Execute an SSH command directly", + "args": { + "name": "COMMAND", + "isCommand": true + } + }, + { + "name": [ + "-p", + "--plain" + ], + "description": "Plain mode, leaves authentication up to user" + }, + { + "name": [ + "-t", + "--tty" + ], + "description": "Enable tty when executing an ssh command (default)", + "exclusiveOn": ["--no-tty"] + } + ] + }, + { + "name": "ssh-config", + "description": "Output OpenSSH valid configuration to connect to the machine", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--host", + "description": "Name the host for the config", + "args": { + "name": "NAME" + } + } + ] + }, + { + "name": "status", + "description": "Output status of the vagrant machine", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + } + }, + { + "name": "suspend", + "description": "Suspend the machine", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": [ + "-a", + "--all-global" + ], + "description": "Suspend all running vms globally" + } + ] + }, + { + "name": "up", + "description": "Start and provision the vagrant environment", + "args": { + "name": "name|id", + "description": "Machine name or id (defaults to every machine in the environment)", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--provision", + "description": "Enable provisioning", + "exclusiveOn": ["--no-provision"] + }, + { + "name": "--no-provision", + "description": "Disable provisioning", + "exclusiveOn": ["--provision"] + }, + { + "name": "--provision-with", + "description": "Enable only certain provisioners, by type or by name", + "args": { + "name": "x,y,z" + } + }, + { + "name": "--destroy-on-error", + "description": "Destroy machine if any fatal error happens (default)", + "exclusiveOn": ["--no-destroy-on-error"] + }, + { + "name": "--no-destroy-on-error", + "description": "Keep machine if any fatal error happens", + "exclusiveOn": ["--destroy-on-error"] + }, + { + "name": "--parallel", + "description": "Enable parallelism if the provider supports it", + "exclusiveOn": ["--no-parallel"] + }, + { + "name": "--no-parallel", + "description": "Disable parallelism", + "exclusiveOn": ["--parallel"] + }, + { + "name": "--provider", + "description": "Back the machine with a specific provider", + "args": { + "name": "PROVIDER", + "suggestions": [ + "virtualbox", + "vmware_desktop", + "vmware_fusion", + "vmware_workstation", + "hyperv", + "docker", + "libvirt", + "parallels" + ] + } + }, + { + "name": "--install-provider", + "description": "If possible, install the provider if it isn't installed", + "exclusiveOn": ["--no-install-provider"] + }, + { + "name": "--no-install-provider", + "description": "Do not install the provider if it isn't installed", + "exclusiveOn": ["--install-provider"] + } + ] + }, + { + "name": "upload", + "description": "Upload to machine via communicator", + "args": [ + { + "name": "source", + "template": ["filepaths"] + }, + { + "name": "destination", + "isOptional": true + }, + { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + } + ], + "options": [ + { + "name": [ + "-t", + "--temporary" + ], + "description": "Upload source to temporary directory" + }, + { + "name": [ + "-c", + "--compress" + ], + "description": "Use gzip compression for upload" + }, + { + "name": [ + "-C", + "--compression-type" + ], + "description": "Type of compression to use", + "args": { + "name": "TYPE", + "suggestions": [ + "tgz", + "zip" + ] + } + } + ] + }, + { + "name": "validate", + "description": "Validate the Vagrantfile", + "options": [ + { + "name": [ + "-p", + "--ignore-provider" + ], + "description": "Ignore provider config options" + } + ] + }, + { + "name": "version", + "description": "Print current and latest Vagrant version" + }, + { + "name": "winrm", + "description": "Execute commands on a machine via WinRM", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": [ + "-c", + "--command" + ], + "description": "Execute a WinRM command directly", + "args": { + "name": "COMMAND", + "isCommand": true + } + }, + { + "name": [ + "-e", + "--elevated" + ], + "description": "Run with elevated credentials" + }, + { + "name": [ + "-s", + "--shell" + ], + "description": "Use specified shell", + "args": { + "name": "SHELL", + "suggestions": [ + "powershell", + "cmd" + ] + } + } + ] + }, + { + "name": "winrm-config", + "description": "Output WinRM configuration to connect to the machine", + "args": { + "name": "name|id", + "isOptional": true, + "generatorName": "vagrant_machines" + }, + "options": [ + { + "name": "--host", + "description": "Name the host for the config", + "args": { + "name": "NAME" + } + } + ] + } + ] +} diff --git a/command-signatures/src/generators/mod.rs b/command-signatures/src/generators/mod.rs index c9acf056..f797f4e2 100644 --- a/command-signatures/src/generators/mod.rs +++ b/command-signatures/src/generators/mod.rs @@ -75,6 +75,9 @@ mod tmux; mod tmuxinator; mod tsh; mod uv; +mod vagrant; +#[cfg(test)] +mod vagrant_tests; mod yc; /// Used for gcloud and gsutil completions. @@ -159,6 +162,7 @@ pub fn dynamic_command_signature_data() -> HashMap GeneratorResults { + let mut machines: Vec<(&str, Option<&str>)> = Vec::new(); + for entry in output + .lines() + .filter_map(|line| line.trim().strip_prefix("./")) + .filter(|entry| !entry.is_empty()) + { + match entry.split_once('/') { + None => { + if !machines.iter().any(|(name, _)| *name == entry) { + machines.push((entry, None)); + } + } + Some((name, provider)) => { + if let Some((_, existing)) = machines.iter_mut().find(|(m, _)| *m == name) { + existing.get_or_insert(provider); + } + } + } + } + machines + .into_iter() + .map(|(name, provider)| match provider { + Some(provider) => Suggestion::with_description(name, provider), + None => Suggestion::new(name), + }) + .collect_unordered_results() +} + +/// Parses the box directory listing into box-name suggestions. +/// +/// Vagrant escapes the characters that cannot appear in a directory name when it stores a +/// box, so the placeholders have to be reversed to recover the name the CLI accepts. The +/// colon is un-escaped before the slash, mirroring the order Vagrant's own +/// `BoxCollection#undir_name` uses. +pub(super) fn parse_boxes(output: &str) -> GeneratorResults { + output + .lines() + .map(str::trim) + .filter(|directory| !directory.is_empty()) + .map(|directory| { + let name = directory + .replace("-VAGRANTCOLON-", ":") + .replace("-VAGRANTSLASH-", "/"); + Suggestion::with_description(name, "Installed box") + }) + .collect_unordered_results() +} + +pub fn generator() -> CommandSignatureGenerators { + CommandSignatureGenerators::new("vagrant") + .add_generator( + "vagrant_machines", + Generator::script( + CommandBuilder::single_command_and_ignore_stderr(MACHINES_COMMAND), + parse_machines, + ), + ) + .add_generator( + "vagrant_boxes", + Generator::script( + CommandBuilder::single_command_and_ignore_stderr(BOXES_COMMAND), + parse_boxes, + ), + ) +} diff --git a/command-signatures/src/generators/vagrant_tests.rs b/command-signatures/src/generators/vagrant_tests.rs new file mode 100644 index 00000000..b08c84e1 --- /dev/null +++ b/command-signatures/src/generators/vagrant_tests.rs @@ -0,0 +1,210 @@ +use super::vagrant::{parse_boxes, parse_machines}; + +/// The subcommands `vagrant` exposes, per `vagrant list-commands`. +#[cfg(feature = "embed-signatures")] +const TOP_LEVEL_SUBCOMMANDS: [&str; 36] = [ + "autocomplete", + "box", + "cloud", + "destroy", + "docker-exec", + "docker-logs", + "docker-run", + "global-status", + "halt", + "help", + "init", + "list-commands", + "login", + "package", + "plugin", + "port", + "powershell", + "provider", + "provision", + "push", + "rdp", + "reload", + "resume", + "rsync", + "rsync-auto", + "snapshot", + "ssh", + "ssh-config", + "status", + "suspend", + "up", + "upload", + "validate", + "version", + "winrm", + "winrm-config", +]; + +/// The command groups whose own subcommands the spec has to keep completing. +#[cfg(feature = "embed-signatures")] +const NESTED_GROUPS: [(&str, &[&str]); 4] = [ + ( + "box", + &[ + "add", + "help", + "list", + "outdated", + "prune", + "remove", + "repackage", + "update", + ], + ), + ( + "cloud", + &["auth", "box", "provider", "publish", "search", "version"], + ), + ( + "plugin", + &[ + "expunge", + "install", + "license", + "list", + "repair", + "uninstall", + "update", + ], + ), + ( + "snapshot", + &["delete", "list", "pop", "push", "restore", "save"], + ), +]; + +#[cfg(feature = "embed-signatures")] +#[test] +fn test_vagrant_spec_covers_every_subcommand_and_nested_group() { + let vagrant = crate::signature_by_name("vagrant").expect("vagrant spec should be bundled"); + + let subcommands: Vec<&str> = vagrant + .subcommands() + .iter() + .map(|subcommand| subcommand.name.as_str()) + .collect(); + for name in TOP_LEVEL_SUBCOMMANDS { + assert!( + subcommands.contains(&name), + "`vagrant {name}` is missing from the spec" + ); + } + + for (group, expected) in NESTED_GROUPS { + let signature = vagrant + .subcommands() + .iter() + .find(|subcommand| subcommand.name == group) + .unwrap_or_else(|| panic!("`vagrant {group}` is missing from the spec")); + let nested: Vec<&str> = signature + .subcommands() + .iter() + .map(|subcommand| subcommand.name.as_str()) + .collect(); + for name in expected { + assert!( + nested.contains(name), + "`vagrant {group} {name}` is missing from the spec" + ); + } + } +} + +#[test] +fn test_parse_machines_describes_each_machine_by_its_provider() { + let output = "./default\n./default/virtualbox\n./web\n./web/libvirt\n"; + let results = parse_machines(output); + let names: Vec<&str> = results + .suggestions + .iter() + .map(|s| s.exact_string.as_str()) + .collect(); + assert_eq!(names, vec!["default", "web"]); + assert_eq!( + results.suggestions[0].description.as_deref(), + Some("virtualbox") + ); + assert_eq!( + results.suggestions[1].description.as_deref(), + Some("libvirt") + ); +} + +#[test] +fn test_parse_machines_without_a_provider_directory() { + let results = parse_machines("./default\n"); + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "default"); + assert_eq!(results.suggestions[0].description, None); +} + +#[test] +fn test_parse_machines_keeps_the_first_provider_of_a_multi_provider_machine() { + let output = "./default\n./default/virtualbox\n./default/libvirt\n"; + let results = parse_machines(output); + assert_eq!(results.suggestions.len(), 1); + assert_eq!( + results.suggestions[0].description.as_deref(), + Some("virtualbox") + ); +} + +#[test] +fn test_parse_machines_skips_lines_that_are_not_listing_entries() { + let output = "find: .: Permission denied\n.\n./default\n\n"; + let results = parse_machines(output); + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "default"); +} + +#[test] +fn test_parse_machines_empty_output() { + assert!(parse_machines("").suggestions.is_empty()); +} + +#[test] +fn test_parse_boxes_restores_escaped_characters_in_box_names() { + let output = "hashicorp-VAGRANTSLASH-bionic64\ngeneric-VAGRANTSLASH-ubuntu2204\nmybox\n"; + let results = parse_boxes(output); + let names: Vec<&str> = results + .suggestions + .iter() + .map(|s| s.exact_string.as_str()) + .collect(); + assert_eq!( + names, + vec!["hashicorp/bionic64", "generic/ubuntu2204", "mybox"] + ); + assert_eq!( + results.suggestions[0].description.as_deref(), + Some("Installed box") + ); +} + +#[test] +fn test_parse_boxes_restores_escaped_colons() { + let results = + parse_boxes("http-VAGRANTCOLON--VAGRANTSLASH--VAGRANTSLASH-example.com-VAGRANTSLASH-box\n"); + assert_eq!( + results.suggestions[0].exact_string, + "http://example.com/box" + ); +} + +#[test] +fn test_parse_boxes_skips_blank_lines() { + let results = parse_boxes("\n \nmybox\n\n"); + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "mybox"); +} + +#[test] +fn test_parse_boxes_empty_output() { + assert!(parse_boxes("").suggestions.is_empty()); +} From a3132001ada9b0051dc17f265d4969c4e68223ba Mon Sep 17 00:00:00 2001 From: Warp Agent Date: Thu, 6 Aug 2026 02:39:13 +0000 Subject: [PATCH 2/3] Address review: quote box path, support Windows shells, inline tests - Quote the resolved boxes path so a VAGRANT_HOME or HOME containing spaces is no longer split into separate `ls` arguments. - Select the generator command from the session shell: POSIX shells get the `sh`/`find` pipeline, PowerShell gets a native script, and cmd.exe runs that script via `powershell -NoProfile -Command`. This adds `CommandBuilder::per_shell_and_ignore_stderr`, and the newline invariant test now covers every shell rather than only POSIX. - Fold vagrant_tests.rs into vagrant.rs as an inline `mod tests`, matching git.rs and yc.rs, and add execution-path tests that run the real listings against fixtures whose paths contain spaces. Co-Authored-By: Warp --- command-signatures/src/generators/mod.rs | 2 - command-signatures/src/generators/vagrant.rs | 278 +++++++++++++++++- .../src/generators/vagrant_tests.rs | 210 ------------- command-signatures/src/lib.rs | 59 ++-- completion-metadata/src/command_builder.rs | 37 +++ 5 files changed, 341 insertions(+), 245 deletions(-) delete mode 100644 command-signatures/src/generators/vagrant_tests.rs diff --git a/command-signatures/src/generators/mod.rs b/command-signatures/src/generators/mod.rs index f797f4e2..471d6976 100644 --- a/command-signatures/src/generators/mod.rs +++ b/command-signatures/src/generators/mod.rs @@ -76,8 +76,6 @@ mod tmuxinator; mod tsh; mod uv; mod vagrant; -#[cfg(test)] -mod vagrant_tests; mod yc; /// Used for gcloud and gsutil completions. diff --git a/command-signatures/src/generators/vagrant.rs b/command-signatures/src/generators/vagrant.rs index 4a2a07f9..4267ceef 100644 --- a/command-signatures/src/generators/vagrant.rs +++ b/command-signatures/src/generators/vagrant.rs @@ -4,16 +4,47 @@ use warp_completion_metadata::{ }; /// Lists the machine directories Vagrant keeps for the project enclosing the working -/// directory. +/// directory, as `./` followed by `.//`. /// /// Walking up for `.vagrant/machines` is how Vagrant itself locates the project root, and /// reading that directory avoids paying `vagrant status`'s multi-second Ruby start-up on -/// every keystroke. The second level of the listing is the provider each machine was -/// brought up with, which [`parse_machines`] turns into the suggestion's description. -const MACHINES_COMMAND: &str = "sh -c 'dir=$PWD; while [ -n \"$dir\" ]; do if [ -d \"$dir/.vagrant/machines\" ]; then cd \"$dir/.vagrant/machines\" && find . -mindepth 1 -maxdepth 2 -type d; break; fi; dir=${dir%/*}; done'"; +/// every keystroke. The provider level is what [`parse_machines`] turns into the +/// suggestion's description. +const MACHINES_POSIX: &str = "sh -c 'dir=$PWD; while [ -n \"$dir\" ]; do if [ -d \"$dir/.vagrant/machines\" ]; then cd \"$dir/.vagrant/machines\" && find . -mindepth 1 -maxdepth 2 -type d; break; fi; dir=${dir%/*}; done'"; + +/// The PowerShell equivalent of [`MACHINES_POSIX`], emitting the same `./` and +/// `.//` lines so both platforms share one parser. +/// +/// The machine name is captured into `$n` and the provider directories are listed in a +/// nested loop, rather than recursing and rewriting path separators, so the script needs +/// no backslash literals and survives being passed through `cmd.exe`. +const MACHINES_POWERSHELL: &str = "$d = $PWD.Path; while ($d) { $m = Join-Path $d '.vagrant/machines'; if (Test-Path -LiteralPath $m) { Get-ChildItem -LiteralPath $m -Directory | ForEach-Object { $n = $_.Name; './' + $n; Get-ChildItem -LiteralPath $_.FullName -Directory | ForEach-Object { './' + $n + '/' + $_.Name } }; break }; $d = Split-Path -Parent $d }"; /// Lists the box directories under the Vagrant home, honoring `$VAGRANT_HOME`. -const BOXES_COMMAND: &str = "sh -c 'ls -1 ${VAGRANT_HOME:-$HOME/.vagrant.d}/boxes'"; +/// +/// The resolved path is quoted so a home directory containing spaces stays a single +/// argument to `ls` instead of being split into several. +const BOXES_POSIX: &str = "sh -c 'ls -1 \"${VAGRANT_HOME:-$HOME/.vagrant.d}/boxes\"'"; + +/// The PowerShell equivalent of [`BOXES_POSIX`]. +const BOXES_POWERSHELL: &str = "$h = $env:VAGRANT_HOME; if (-not $h) { $h = Join-Path $HOME '.vagrant.d' }; Get-ChildItem -LiteralPath (Join-Path $h 'boxes') -Directory -Name"; + +/// Wraps a PowerShell script so it can be launched from `cmd.exe`. +/// +/// `cmd.exe` leaves `$` alone and expands only `%VAR%`, so double-quoting the script keeps +/// it intact as long as the script itself quotes with `'`. +fn powershell_from_cmd_exe(script: &str) -> String { + format!("powershell -NoProfile -Command \"{script}\"") +} + +/// Builds the shell-appropriate command for one of the two directory listings. +fn per_shell_command(posix: &str, powershell: &str) -> CommandBuilder { + CommandBuilder::per_shell_and_ignore_stderr( + posix, + powershell, + powershell_from_cmd_exe(powershell), + ) +} /// Parses the two-level `.vagrant/machines` listing into machine-name suggestions, /// described by the provider directory nested under each machine when there is one. @@ -71,15 +102,248 @@ pub fn generator() -> CommandSignatureGenerators { .add_generator( "vagrant_machines", Generator::script( - CommandBuilder::single_command_and_ignore_stderr(MACHINES_COMMAND), + per_shell_command(MACHINES_POSIX, MACHINES_POWERSHELL), parse_machines, ), ) .add_generator( "vagrant_boxes", Generator::script( - CommandBuilder::single_command_and_ignore_stderr(BOXES_COMMAND), + per_shell_command(BOXES_POSIX, BOXES_POWERSHELL), parse_boxes, ), ) } + +#[cfg(test)] +mod tests { + use super::*; + use warp_completion_metadata::Shell; + + #[test] + fn test_parse_machines_describes_each_machine_by_its_provider() { + let output = "./default\n./default/virtualbox\n./web\n./web/libvirt\n"; + let results = parse_machines(output); + let names: Vec<&str> = results + .suggestions + .iter() + .map(|s| s.exact_string.as_str()) + .collect(); + assert_eq!(names, vec!["default", "web"]); + assert_eq!( + results.suggestions[0].description.as_deref(), + Some("virtualbox") + ); + assert_eq!( + results.suggestions[1].description.as_deref(), + Some("libvirt") + ); + } + + #[test] + fn test_parse_machines_without_a_provider_directory() { + let results = parse_machines("./default\n"); + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "default"); + assert_eq!(results.suggestions[0].description, None); + } + + #[test] + fn test_parse_machines_keeps_the_first_provider_of_a_multi_provider_machine() { + let output = "./default\n./default/virtualbox\n./default/libvirt\n"; + let results = parse_machines(output); + assert_eq!(results.suggestions.len(), 1); + assert_eq!( + results.suggestions[0].description.as_deref(), + Some("virtualbox") + ); + } + + #[test] + fn test_parse_machines_skips_lines_that_are_not_listing_entries() { + let output = "find: .: Permission denied\n.\n./default\n\n"; + let results = parse_machines(output); + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "default"); + } + + #[test] + fn test_parse_machines_empty_output() { + assert!(parse_machines("").suggestions.is_empty()); + } + + #[test] + fn test_parse_boxes_restores_escaped_characters_in_box_names() { + let output = "hashicorp-VAGRANTSLASH-bionic64\ngeneric-VAGRANTSLASH-ubuntu2204\nmybox\n"; + let results = parse_boxes(output); + let names: Vec<&str> = results + .suggestions + .iter() + .map(|s| s.exact_string.as_str()) + .collect(); + assert_eq!( + names, + vec!["hashicorp/bionic64", "generic/ubuntu2204", "mybox"] + ); + assert_eq!( + results.suggestions[0].description.as_deref(), + Some("Installed box") + ); + } + + #[test] + fn test_parse_boxes_restores_escaped_colons() { + let results = parse_boxes( + "http-VAGRANTCOLON--VAGRANTSLASH--VAGRANTSLASH-example.com-VAGRANTSLASH-box\n", + ); + assert_eq!( + results.suggestions[0].exact_string, + "http://example.com/box" + ); + } + + #[test] + fn test_parse_boxes_skips_blank_lines() { + let results = parse_boxes("\n \nmybox\n\n"); + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "mybox"); + } + + #[test] + fn test_parse_boxes_empty_output() { + assert!(parse_boxes("").suggestions.is_empty()); + } + + #[test] + fn test_machines_command_is_selected_per_shell() { + let command = per_shell_command(MACHINES_POSIX, MACHINES_POWERSHELL); + + let posix = command.build(Shell::Posix); + assert!(posix.starts_with("sh -c ")); + assert!(posix.contains("find . -mindepth 1 -maxdepth 2 -type d")); + + let powershell = command.build(Shell::Powershell); + assert!(powershell.starts_with("$d = $PWD.Path")); + assert!(!powershell.contains("sh -c ")); + assert!(!powershell.contains("find ")); + + let cmd_exe = command.build(Shell::CmdExe); + assert!(cmd_exe.starts_with("powershell -NoProfile -Command \"")); + assert!(!cmd_exe.contains("sh -c ")); + } + + #[test] + fn test_boxes_command_is_selected_per_shell() { + let command = per_shell_command(BOXES_POSIX, BOXES_POWERSHELL); + + let posix = command.build(Shell::Posix); + assert!(posix.starts_with("sh -c ")); + assert!(posix.contains("ls -1 ")); + + let powershell = command.build(Shell::Powershell); + assert!(powershell.contains("Get-ChildItem")); + assert!(!powershell.contains("sh -c ")); + assert!(!powershell.contains("ls -1 ")); + + let cmd_exe = command.build(Shell::CmdExe); + assert!(cmd_exe.starts_with("powershell -NoProfile -Command \"")); + assert!(cmd_exe.contains("Get-ChildItem")); + } + + /// A `cmd.exe` wrapper only stays intact while the script quotes with `'`; an embedded + /// `"` would terminate the wrapper's own quoting and truncate the script. + #[test] + fn test_powershell_scripts_carry_no_double_quotes() { + for script in [MACHINES_POWERSHELL, BOXES_POWERSHELL] { + assert!(!script.contains('"'), "`{script}` contains a double quote"); + } + } + + #[test] + fn test_boxes_posix_command_quotes_the_resolved_path() { + assert!(BOXES_POSIX.contains("\"${VAGRANT_HOME:-$HOME/.vagrant.d}/boxes\"")); + } + + /// Runs the real POSIX listings against on-disk fixtures whose paths contain spaces, + /// which an unquoted expansion would split into separate arguments. + #[cfg(unix)] + mod posix_execution { + use super::*; + use std::fs; + use std::path::{Path, PathBuf}; + use std::process::Command; + + fn run(command: &str, working_directory: &Path, vagrant_home: &Path) -> String { + let output = Command::new("sh") + .arg("-c") + .arg(command) + .current_dir(working_directory) + .env("VAGRANT_HOME", vagrant_home) + .output() + .expect("the fixture command should run"); + String::from_utf8(output.stdout).expect("the listing should be utf-8") + } + + fn fixture_root(name: &str) -> PathBuf { + let directory = std::env::temp_dir().join(format!("vagrant generator {name}")); + let _ = fs::remove_dir_all(&directory); + fs::create_dir_all(&directory).expect("the fixture directory should be creatable"); + directory + } + + #[test] + fn test_boxes_listing_survives_a_vagrant_home_containing_spaces() { + let root = fixture_root("boxes"); + let vagrant_home = root.join("my vagrant home"); + let boxes = vagrant_home.join("boxes"); + fs::create_dir_all(boxes.join("hashicorp-VAGRANTSLASH-bionic64")).unwrap(); + fs::create_dir_all(boxes.join("mybox")).unwrap(); + + let results = parse_boxes(&run(BOXES_POSIX, &root, &vagrant_home)); + let mut names: Vec<&str> = results + .suggestions + .iter() + .map(|s| s.exact_string.as_str()) + .collect(); + names.sort_unstable(); + assert_eq!(names, vec!["hashicorp/bionic64", "mybox"]); + + fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn test_machines_listing_walks_up_from_a_directory_containing_spaces() { + let root = fixture_root("machines"); + let project = root.join("my project"); + let machines = project.join(".vagrant").join("machines"); + fs::create_dir_all(machines.join("default").join("virtualbox")).unwrap(); + let nested = project.join("a nested").join("sub dir"); + fs::create_dir_all(&nested).unwrap(); + + let results = parse_machines(&run(MACHINES_POSIX, &nested, Path::new("/nonexistent"))); + assert_eq!(results.suggestions.len(), 1); + assert_eq!(results.suggestions[0].exact_string, "default"); + assert_eq!( + results.suggestions[0].description.as_deref(), + Some("virtualbox") + ); + + fs::remove_dir_all(&root).unwrap(); + } + + #[test] + fn test_listings_are_silent_when_there_is_nothing_to_list() { + let root = fixture_root("empty"); + let missing = Path::new("/nonexistent"); + + assert!(parse_machines(&run(MACHINES_POSIX, &root, missing)) + .suggestions + .is_empty()); + assert!(parse_boxes(&run(BOXES_POSIX, &root, missing)) + .suggestions + .is_empty()); + + fs::remove_dir_all(&root).unwrap(); + } + } +} diff --git a/command-signatures/src/generators/vagrant_tests.rs b/command-signatures/src/generators/vagrant_tests.rs deleted file mode 100644 index b08c84e1..00000000 --- a/command-signatures/src/generators/vagrant_tests.rs +++ /dev/null @@ -1,210 +0,0 @@ -use super::vagrant::{parse_boxes, parse_machines}; - -/// The subcommands `vagrant` exposes, per `vagrant list-commands`. -#[cfg(feature = "embed-signatures")] -const TOP_LEVEL_SUBCOMMANDS: [&str; 36] = [ - "autocomplete", - "box", - "cloud", - "destroy", - "docker-exec", - "docker-logs", - "docker-run", - "global-status", - "halt", - "help", - "init", - "list-commands", - "login", - "package", - "plugin", - "port", - "powershell", - "provider", - "provision", - "push", - "rdp", - "reload", - "resume", - "rsync", - "rsync-auto", - "snapshot", - "ssh", - "ssh-config", - "status", - "suspend", - "up", - "upload", - "validate", - "version", - "winrm", - "winrm-config", -]; - -/// The command groups whose own subcommands the spec has to keep completing. -#[cfg(feature = "embed-signatures")] -const NESTED_GROUPS: [(&str, &[&str]); 4] = [ - ( - "box", - &[ - "add", - "help", - "list", - "outdated", - "prune", - "remove", - "repackage", - "update", - ], - ), - ( - "cloud", - &["auth", "box", "provider", "publish", "search", "version"], - ), - ( - "plugin", - &[ - "expunge", - "install", - "license", - "list", - "repair", - "uninstall", - "update", - ], - ), - ( - "snapshot", - &["delete", "list", "pop", "push", "restore", "save"], - ), -]; - -#[cfg(feature = "embed-signatures")] -#[test] -fn test_vagrant_spec_covers_every_subcommand_and_nested_group() { - let vagrant = crate::signature_by_name("vagrant").expect("vagrant spec should be bundled"); - - let subcommands: Vec<&str> = vagrant - .subcommands() - .iter() - .map(|subcommand| subcommand.name.as_str()) - .collect(); - for name in TOP_LEVEL_SUBCOMMANDS { - assert!( - subcommands.contains(&name), - "`vagrant {name}` is missing from the spec" - ); - } - - for (group, expected) in NESTED_GROUPS { - let signature = vagrant - .subcommands() - .iter() - .find(|subcommand| subcommand.name == group) - .unwrap_or_else(|| panic!("`vagrant {group}` is missing from the spec")); - let nested: Vec<&str> = signature - .subcommands() - .iter() - .map(|subcommand| subcommand.name.as_str()) - .collect(); - for name in expected { - assert!( - nested.contains(name), - "`vagrant {group} {name}` is missing from the spec" - ); - } - } -} - -#[test] -fn test_parse_machines_describes_each_machine_by_its_provider() { - let output = "./default\n./default/virtualbox\n./web\n./web/libvirt\n"; - let results = parse_machines(output); - let names: Vec<&str> = results - .suggestions - .iter() - .map(|s| s.exact_string.as_str()) - .collect(); - assert_eq!(names, vec!["default", "web"]); - assert_eq!( - results.suggestions[0].description.as_deref(), - Some("virtualbox") - ); - assert_eq!( - results.suggestions[1].description.as_deref(), - Some("libvirt") - ); -} - -#[test] -fn test_parse_machines_without_a_provider_directory() { - let results = parse_machines("./default\n"); - assert_eq!(results.suggestions.len(), 1); - assert_eq!(results.suggestions[0].exact_string, "default"); - assert_eq!(results.suggestions[0].description, None); -} - -#[test] -fn test_parse_machines_keeps_the_first_provider_of_a_multi_provider_machine() { - let output = "./default\n./default/virtualbox\n./default/libvirt\n"; - let results = parse_machines(output); - assert_eq!(results.suggestions.len(), 1); - assert_eq!( - results.suggestions[0].description.as_deref(), - Some("virtualbox") - ); -} - -#[test] -fn test_parse_machines_skips_lines_that_are_not_listing_entries() { - let output = "find: .: Permission denied\n.\n./default\n\n"; - let results = parse_machines(output); - assert_eq!(results.suggestions.len(), 1); - assert_eq!(results.suggestions[0].exact_string, "default"); -} - -#[test] -fn test_parse_machines_empty_output() { - assert!(parse_machines("").suggestions.is_empty()); -} - -#[test] -fn test_parse_boxes_restores_escaped_characters_in_box_names() { - let output = "hashicorp-VAGRANTSLASH-bionic64\ngeneric-VAGRANTSLASH-ubuntu2204\nmybox\n"; - let results = parse_boxes(output); - let names: Vec<&str> = results - .suggestions - .iter() - .map(|s| s.exact_string.as_str()) - .collect(); - assert_eq!( - names, - vec!["hashicorp/bionic64", "generic/ubuntu2204", "mybox"] - ); - assert_eq!( - results.suggestions[0].description.as_deref(), - Some("Installed box") - ); -} - -#[test] -fn test_parse_boxes_restores_escaped_colons() { - let results = - parse_boxes("http-VAGRANTCOLON--VAGRANTSLASH--VAGRANTSLASH-example.com-VAGRANTSLASH-box\n"); - assert_eq!( - results.suggestions[0].exact_string, - "http://example.com/box" - ); -} - -#[test] -fn test_parse_boxes_skips_blank_lines() { - let results = parse_boxes("\n \nmybox\n\n"); - assert_eq!(results.suggestions.len(), 1); - assert_eq!(results.suggestions[0].exact_string, "mybox"); -} - -#[test] -fn test_parse_boxes_empty_output() { - assert!(parse_boxes("").suggestions.is_empty()); -} diff --git a/command-signatures/src/lib.rs b/command-signatures/src/lib.rs index b0a74f1d..ec92fe36 100644 --- a/command-signatures/src/lib.rs +++ b/command-signatures/src/lib.rs @@ -212,10 +212,13 @@ mod tests { /// We want to send commands through TMUX control mode, and our current implementation /// only supports one-line commands. This may be a constraint we don't need to /// uphold in the future. + /// + /// A generator may emit a different command per shell, so every shell is checked. fn all_command_specs_have_no_newlines() { let generators = generators::dynamic_command_signature_data(); let token_test_cases = ["true", "hello world", "1", "1.0", "127.0.0.1", "\\n"]; + let shells = [Shell::Posix, Shell::Powershell, Shell::CmdExe]; for (generator_name, completion_data) in generators { completion_data @@ -223,35 +226,39 @@ mod tests { .values() .for_each(|generator| match &generator.process { GeneratorProcess::CommandFromTokens(func) => { - token_test_cases.iter().for_each(|&tokens| { - let builder = func(&[tokens, " "], true, &[]); - let trailing_whitespace_result = builder.build(Shell::Posix); - assert!( - !has_unsafe_newlines(&trailing_whitespace_result), - "[has_trailing_whitespace: true] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", - tokens, - generator_name, - trailing_whitespace_result - ); - let command_builder = func(&[tokens], false, &[]); - let no_trailing_whitespace_result = command_builder.build(Shell::Posix); + for shell in shells { + token_test_cases.iter().for_each(|&tokens| { + let builder = func(&[tokens, " "], true, &[]); + let trailing_whitespace_result = builder.build(shell); + assert!( + !has_unsafe_newlines(&trailing_whitespace_result), + "[has_trailing_whitespace: true] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", + tokens, + generator_name, + trailing_whitespace_result + ); + let command_builder = func(&[tokens], false, &[]); + let no_trailing_whitespace_result = command_builder.build(shell); + assert!( + !has_unsafe_newlines(&no_trailing_whitespace_result), + "[has_trailing_whitespace: false] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", + tokens, + generator_name, + no_trailing_whitespace_result + ); + }); + } + } + GeneratorProcess::ShellCommand(str) => { + for shell in shells { + let str = str.build(shell); assert!( - !has_unsafe_newlines(&no_trailing_whitespace_result), - "[has_trailing_whitespace: false] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", - tokens, + !has_unsafe_newlines(&str), + "Generator `{}` has an unquoted newline in it: `{}`", generator_name, - no_trailing_whitespace_result + str ); - }); - } - GeneratorProcess::ShellCommand(str) => { - let str = str.build(Shell::Posix); - assert!( - !has_unsafe_newlines(&str), - "Generator `{}` has an unquoted newline in it: `{}`", - generator_name, - str - ); + } } }); } diff --git a/completion-metadata/src/command_builder.rs b/completion-metadata/src/command_builder.rs index 27587b16..584b9656 100644 --- a/completion-metadata/src/command_builder.rs +++ b/completion-metadata/src/command_builder.rs @@ -9,6 +9,13 @@ enum CommandPart { /// A single command. SingleCommand(String), SingleCommandWithStdErrIgnored(String), + /// One command per shell family, for data that cannot be read the same way everywhere. + /// All stderr output is ignored. + PerShellWithStdErrIgnored { + posix: String, + powershell: String, + cmd_exe: String, + }, /// Two commands separated by an and (i.e. `A && B`) And(Box, Box), /// Two command separated by a pipe (i.e. `A | B`) @@ -36,6 +43,18 @@ impl CommandPart { CommandPart::SingleCommandWithStdErrIgnored(command) => { format!("{command} {}", shell.ignore_stderr()).into() } + CommandPart::PerShellWithStdErrIgnored { + posix, + powershell, + cmd_exe, + } => { + let command = match shell { + Shell::Posix => posix, + Shell::Powershell => powershell, + Shell::CmdExe => cmd_exe, + }; + format!("{command} {}", shell.ignore_stderr()).into() + } CommandPart::Concat(first_command, second_command) => format!( "{} {}", first_command.command(shell), @@ -64,6 +83,24 @@ impl CommandBuilder { Self(CommandPart::SingleCommandWithStdErrIgnored(command.into())) } + /// Constructs a new [`CommandBuilder`] that runs a different command per shell family, + /// ignoring all stderr output. + /// + /// Most generators read their data with a POSIX pipeline that a Windows shell cannot + /// run at all. Selecting per shell keeps such a generator working in every session + /// instead of silently producing nothing outside of a POSIX shell. + pub fn per_shell_and_ignore_stderr( + posix: impl Into, + powershell: impl Into, + cmd_exe: impl Into, + ) -> Self { + Self(CommandPart::PerShellWithStdErrIgnored { + posix: posix.into(), + powershell: powershell.into(), + cmd_exe: cmd_exe.into(), + }) + } + /// Constructs a new [`CommandBuilder`] for a series of commands that should be and'd together /// (i.e. `second_command` should only run iff `first_command` succeeds). pub fn and(first_command: CommandBuilder, second_command: CommandBuilder) -> Self { From 078cee6ac61c3b1038abadd3dfe3ff1272325aa1 Mon Sep 17 00:00:00 2001 From: Warp Agent Date: Thu, 6 Aug 2026 02:54:34 +0000 Subject: [PATCH 3/3] Revert Windows support, keeping the box path quoting fix The requester scoped Windows out. Both generators go back to the plain POSIX command, so `completion-metadata` is untouched again: the `per_shell_and_ignore_stderr` constructor and its `CommandBuilder` variant are gone, along with the tests that only covered them. `all_command_specs_have_no_newlines` returns to checking POSIX alone, since with one command string per generator the only per-shell difference left is the constant stderr redirect. The quoting fix and its three execution-path tests stay - that defect was unrelated to Windows. Also restores the spec-surface regression test, which was dropped when the tests moved inline. Co-Authored-By: Warp --- command-signatures/src/generators/vagrant.rs | 215 +++++++++++-------- command-signatures/src/lib.rs | 59 +++-- completion-metadata/src/command_builder.rs | 37 ---- 3 files changed, 155 insertions(+), 156 deletions(-) diff --git a/command-signatures/src/generators/vagrant.rs b/command-signatures/src/generators/vagrant.rs index 4267ceef..7df09adf 100644 --- a/command-signatures/src/generators/vagrant.rs +++ b/command-signatures/src/generators/vagrant.rs @@ -10,41 +10,13 @@ use warp_completion_metadata::{ /// reading that directory avoids paying `vagrant status`'s multi-second Ruby start-up on /// every keystroke. The provider level is what [`parse_machines`] turns into the /// suggestion's description. -const MACHINES_POSIX: &str = "sh -c 'dir=$PWD; while [ -n \"$dir\" ]; do if [ -d \"$dir/.vagrant/machines\" ]; then cd \"$dir/.vagrant/machines\" && find . -mindepth 1 -maxdepth 2 -type d; break; fi; dir=${dir%/*}; done'"; - -/// The PowerShell equivalent of [`MACHINES_POSIX`], emitting the same `./` and -/// `.//` lines so both platforms share one parser. -/// -/// The machine name is captured into `$n` and the provider directories are listed in a -/// nested loop, rather than recursing and rewriting path separators, so the script needs -/// no backslash literals and survives being passed through `cmd.exe`. -const MACHINES_POWERSHELL: &str = "$d = $PWD.Path; while ($d) { $m = Join-Path $d '.vagrant/machines'; if (Test-Path -LiteralPath $m) { Get-ChildItem -LiteralPath $m -Directory | ForEach-Object { $n = $_.Name; './' + $n; Get-ChildItem -LiteralPath $_.FullName -Directory | ForEach-Object { './' + $n + '/' + $_.Name } }; break }; $d = Split-Path -Parent $d }"; +const MACHINES_COMMAND: &str = "sh -c 'dir=$PWD; while [ -n \"$dir\" ]; do if [ -d \"$dir/.vagrant/machines\" ]; then cd \"$dir/.vagrant/machines\" && find . -mindepth 1 -maxdepth 2 -type d; break; fi; dir=${dir%/*}; done'"; /// Lists the box directories under the Vagrant home, honoring `$VAGRANT_HOME`. /// /// The resolved path is quoted so a home directory containing spaces stays a single /// argument to `ls` instead of being split into several. -const BOXES_POSIX: &str = "sh -c 'ls -1 \"${VAGRANT_HOME:-$HOME/.vagrant.d}/boxes\"'"; - -/// The PowerShell equivalent of [`BOXES_POSIX`]. -const BOXES_POWERSHELL: &str = "$h = $env:VAGRANT_HOME; if (-not $h) { $h = Join-Path $HOME '.vagrant.d' }; Get-ChildItem -LiteralPath (Join-Path $h 'boxes') -Directory -Name"; - -/// Wraps a PowerShell script so it can be launched from `cmd.exe`. -/// -/// `cmd.exe` leaves `$` alone and expands only `%VAR%`, so double-quoting the script keeps -/// it intact as long as the script itself quotes with `'`. -fn powershell_from_cmd_exe(script: &str) -> String { - format!("powershell -NoProfile -Command \"{script}\"") -} - -/// Builds the shell-appropriate command for one of the two directory listings. -fn per_shell_command(posix: &str, powershell: &str) -> CommandBuilder { - CommandBuilder::per_shell_and_ignore_stderr( - posix, - powershell, - powershell_from_cmd_exe(powershell), - ) -} +const BOXES_COMMAND: &str = "sh -c 'ls -1 \"${VAGRANT_HOME:-$HOME/.vagrant.d}/boxes\"'"; /// Parses the two-level `.vagrant/machines` listing into machine-name suggestions, /// described by the provider directory nested under each machine when there is one. @@ -102,14 +74,14 @@ pub fn generator() -> CommandSignatureGenerators { .add_generator( "vagrant_machines", Generator::script( - per_shell_command(MACHINES_POSIX, MACHINES_POWERSHELL), + CommandBuilder::single_command_and_ignore_stderr(MACHINES_COMMAND), parse_machines, ), ) .add_generator( "vagrant_boxes", Generator::script( - per_shell_command(BOXES_POSIX, BOXES_POWERSHELL), + CommandBuilder::single_command_and_ignore_stderr(BOXES_COMMAND), parse_boxes, ), ) @@ -118,7 +90,122 @@ pub fn generator() -> CommandSignatureGenerators { #[cfg(test)] mod tests { use super::*; - use warp_completion_metadata::Shell; + + /// The subcommands `vagrant` exposes, per `vagrant list-commands`. + #[cfg(feature = "embed-signatures")] + const TOP_LEVEL_SUBCOMMANDS: [&str; 36] = [ + "autocomplete", + "box", + "cloud", + "destroy", + "docker-exec", + "docker-logs", + "docker-run", + "global-status", + "halt", + "help", + "init", + "list-commands", + "login", + "package", + "plugin", + "port", + "powershell", + "provider", + "provision", + "push", + "rdp", + "reload", + "resume", + "rsync", + "rsync-auto", + "snapshot", + "ssh", + "ssh-config", + "status", + "suspend", + "up", + "upload", + "validate", + "version", + "winrm", + "winrm-config", + ]; + + /// The command groups whose own subcommands the spec has to keep completing. + #[cfg(feature = "embed-signatures")] + const NESTED_GROUPS: [(&str, &[&str]); 4] = [ + ( + "box", + &[ + "add", + "help", + "list", + "outdated", + "prune", + "remove", + "repackage", + "update", + ], + ), + ( + "cloud", + &["auth", "box", "provider", "publish", "search", "version"], + ), + ( + "plugin", + &[ + "expunge", + "install", + "license", + "list", + "repair", + "uninstall", + "update", + ], + ), + ( + "snapshot", + &["delete", "list", "pop", "push", "restore", "save"], + ), + ]; + + #[cfg(feature = "embed-signatures")] + #[test] + fn test_vagrant_spec_covers_every_subcommand_and_nested_group() { + let vagrant = crate::signature_by_name("vagrant").expect("vagrant spec should be bundled"); + + let subcommands: Vec<&str> = vagrant + .subcommands() + .iter() + .map(|subcommand| subcommand.name.as_str()) + .collect(); + for name in TOP_LEVEL_SUBCOMMANDS { + assert!( + subcommands.contains(&name), + "`vagrant {name}` is missing from the spec" + ); + } + + for (group, expected) in NESTED_GROUPS { + let signature = vagrant + .subcommands() + .iter() + .find(|subcommand| subcommand.name == group) + .unwrap_or_else(|| panic!("`vagrant {group}` is missing from the spec")); + let nested: Vec<&str> = signature + .subcommands() + .iter() + .map(|subcommand| subcommand.name.as_str()) + .collect(); + for name in expected { + assert!( + nested.contains(name), + "`vagrant {group} {name}` is missing from the spec" + ); + } + } + } #[test] fn test_parse_machines_describes_each_machine_by_its_provider() { @@ -215,57 +302,12 @@ mod tests { } #[test] - fn test_machines_command_is_selected_per_shell() { - let command = per_shell_command(MACHINES_POSIX, MACHINES_POWERSHELL); - - let posix = command.build(Shell::Posix); - assert!(posix.starts_with("sh -c ")); - assert!(posix.contains("find . -mindepth 1 -maxdepth 2 -type d")); - - let powershell = command.build(Shell::Powershell); - assert!(powershell.starts_with("$d = $PWD.Path")); - assert!(!powershell.contains("sh -c ")); - assert!(!powershell.contains("find ")); - - let cmd_exe = command.build(Shell::CmdExe); - assert!(cmd_exe.starts_with("powershell -NoProfile -Command \"")); - assert!(!cmd_exe.contains("sh -c ")); - } - - #[test] - fn test_boxes_command_is_selected_per_shell() { - let command = per_shell_command(BOXES_POSIX, BOXES_POWERSHELL); - - let posix = command.build(Shell::Posix); - assert!(posix.starts_with("sh -c ")); - assert!(posix.contains("ls -1 ")); - - let powershell = command.build(Shell::Powershell); - assert!(powershell.contains("Get-ChildItem")); - assert!(!powershell.contains("sh -c ")); - assert!(!powershell.contains("ls -1 ")); - - let cmd_exe = command.build(Shell::CmdExe); - assert!(cmd_exe.starts_with("powershell -NoProfile -Command \"")); - assert!(cmd_exe.contains("Get-ChildItem")); - } - - /// A `cmd.exe` wrapper only stays intact while the script quotes with `'`; an embedded - /// `"` would terminate the wrapper's own quoting and truncate the script. - #[test] - fn test_powershell_scripts_carry_no_double_quotes() { - for script in [MACHINES_POWERSHELL, BOXES_POWERSHELL] { - assert!(!script.contains('"'), "`{script}` contains a double quote"); - } - } - - #[test] - fn test_boxes_posix_command_quotes_the_resolved_path() { - assert!(BOXES_POSIX.contains("\"${VAGRANT_HOME:-$HOME/.vagrant.d}/boxes\"")); + fn test_boxes_command_quotes_the_resolved_path() { + assert!(BOXES_COMMAND.contains("\"${VAGRANT_HOME:-$HOME/.vagrant.d}/boxes\"")); } - /// Runs the real POSIX listings against on-disk fixtures whose paths contain spaces, - /// which an unquoted expansion would split into separate arguments. + /// Runs the real listings against on-disk fixtures whose paths contain spaces, which + /// an unquoted expansion would split into separate arguments. #[cfg(unix)] mod posix_execution { use super::*; @@ -299,7 +341,7 @@ mod tests { fs::create_dir_all(boxes.join("hashicorp-VAGRANTSLASH-bionic64")).unwrap(); fs::create_dir_all(boxes.join("mybox")).unwrap(); - let results = parse_boxes(&run(BOXES_POSIX, &root, &vagrant_home)); + let results = parse_boxes(&run(BOXES_COMMAND, &root, &vagrant_home)); let mut names: Vec<&str> = results .suggestions .iter() @@ -320,7 +362,8 @@ mod tests { let nested = project.join("a nested").join("sub dir"); fs::create_dir_all(&nested).unwrap(); - let results = parse_machines(&run(MACHINES_POSIX, &nested, Path::new("/nonexistent"))); + let results = + parse_machines(&run(MACHINES_COMMAND, &nested, Path::new("/nonexistent"))); assert_eq!(results.suggestions.len(), 1); assert_eq!(results.suggestions[0].exact_string, "default"); assert_eq!( @@ -336,10 +379,10 @@ mod tests { let root = fixture_root("empty"); let missing = Path::new("/nonexistent"); - assert!(parse_machines(&run(MACHINES_POSIX, &root, missing)) + assert!(parse_machines(&run(MACHINES_COMMAND, &root, missing)) .suggestions .is_empty()); - assert!(parse_boxes(&run(BOXES_POSIX, &root, missing)) + assert!(parse_boxes(&run(BOXES_COMMAND, &root, missing)) .suggestions .is_empty()); diff --git a/command-signatures/src/lib.rs b/command-signatures/src/lib.rs index ec92fe36..b0a74f1d 100644 --- a/command-signatures/src/lib.rs +++ b/command-signatures/src/lib.rs @@ -212,13 +212,10 @@ mod tests { /// We want to send commands through TMUX control mode, and our current implementation /// only supports one-line commands. This may be a constraint we don't need to /// uphold in the future. - /// - /// A generator may emit a different command per shell, so every shell is checked. fn all_command_specs_have_no_newlines() { let generators = generators::dynamic_command_signature_data(); let token_test_cases = ["true", "hello world", "1", "1.0", "127.0.0.1", "\\n"]; - let shells = [Shell::Posix, Shell::Powershell, Shell::CmdExe]; for (generator_name, completion_data) in generators { completion_data @@ -226,39 +223,35 @@ mod tests { .values() .for_each(|generator| match &generator.process { GeneratorProcess::CommandFromTokens(func) => { - for shell in shells { - token_test_cases.iter().for_each(|&tokens| { - let builder = func(&[tokens, " "], true, &[]); - let trailing_whitespace_result = builder.build(shell); - assert!( - !has_unsafe_newlines(&trailing_whitespace_result), - "[has_trailing_whitespace: true] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", - tokens, - generator_name, - trailing_whitespace_result - ); - let command_builder = func(&[tokens], false, &[]); - let no_trailing_whitespace_result = command_builder.build(shell); - assert!( - !has_unsafe_newlines(&no_trailing_whitespace_result), - "[has_trailing_whitespace: false] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", - tokens, - generator_name, - no_trailing_whitespace_result - ); - }); - } - } - GeneratorProcess::ShellCommand(str) => { - for shell in shells { - let str = str.build(shell); + token_test_cases.iter().for_each(|&tokens| { + let builder = func(&[tokens, " "], true, &[]); + let trailing_whitespace_result = builder.build(Shell::Posix); assert!( - !has_unsafe_newlines(&str), - "Generator `{}` has an unquoted newline in it: `{}`", + !has_unsafe_newlines(&trailing_whitespace_result), + "[has_trailing_whitespace: true] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", + tokens, generator_name, - str + trailing_whitespace_result ); - } + let command_builder = func(&[tokens], false, &[]); + let no_trailing_whitespace_result = command_builder.build(Shell::Posix); + assert!( + !has_unsafe_newlines(&no_trailing_whitespace_result), + "[has_trailing_whitespace: false] Tokens: `{}` - Generator `{}` has an unquoted newline in it: `{}`", + tokens, + generator_name, + no_trailing_whitespace_result + ); + }); + } + GeneratorProcess::ShellCommand(str) => { + let str = str.build(Shell::Posix); + assert!( + !has_unsafe_newlines(&str), + "Generator `{}` has an unquoted newline in it: `{}`", + generator_name, + str + ); } }); } diff --git a/completion-metadata/src/command_builder.rs b/completion-metadata/src/command_builder.rs index 584b9656..27587b16 100644 --- a/completion-metadata/src/command_builder.rs +++ b/completion-metadata/src/command_builder.rs @@ -9,13 +9,6 @@ enum CommandPart { /// A single command. SingleCommand(String), SingleCommandWithStdErrIgnored(String), - /// One command per shell family, for data that cannot be read the same way everywhere. - /// All stderr output is ignored. - PerShellWithStdErrIgnored { - posix: String, - powershell: String, - cmd_exe: String, - }, /// Two commands separated by an and (i.e. `A && B`) And(Box, Box), /// Two command separated by a pipe (i.e. `A | B`) @@ -43,18 +36,6 @@ impl CommandPart { CommandPart::SingleCommandWithStdErrIgnored(command) => { format!("{command} {}", shell.ignore_stderr()).into() } - CommandPart::PerShellWithStdErrIgnored { - posix, - powershell, - cmd_exe, - } => { - let command = match shell { - Shell::Posix => posix, - Shell::Powershell => powershell, - Shell::CmdExe => cmd_exe, - }; - format!("{command} {}", shell.ignore_stderr()).into() - } CommandPart::Concat(first_command, second_command) => format!( "{} {}", first_command.command(shell), @@ -83,24 +64,6 @@ impl CommandBuilder { Self(CommandPart::SingleCommandWithStdErrIgnored(command.into())) } - /// Constructs a new [`CommandBuilder`] that runs a different command per shell family, - /// ignoring all stderr output. - /// - /// Most generators read their data with a POSIX pipeline that a Windows shell cannot - /// run at all. Selecting per shell keeps such a generator working in every session - /// instead of silently producing nothing outside of a POSIX shell. - pub fn per_shell_and_ignore_stderr( - posix: impl Into, - powershell: impl Into, - cmd_exe: impl Into, - ) -> Self { - Self(CommandPart::PerShellWithStdErrIgnored { - posix: posix.into(), - powershell: powershell.into(), - cmd_exe: cmd_exe.into(), - }) - } - /// Constructs a new [`CommandBuilder`] for a series of commands that should be and'd together /// (i.e. `second_command` should only run iff `first_command` succeeds). pub fn and(first_command: CommandBuilder, second_command: CommandBuilder) -> Self {