From e39717873aed95f4887b96af6129c809fecfc50e Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Mon, 20 Nov 2023 19:44:14 +0000 Subject: Import clap_complete crate. Request Document: go/android-rust-importing-crates For CL Reviewers: go/android3p#cl-review For Build Team: go/ab-third-party-imports Bug: 312414193 Test: Treehugger Change-Id: I1e709e91b54f240767c4d6360712bb71f783feb5 --- .cargo_vcs_info.json | 6 + Android.bp | 21 + Cargo.lock | 1039 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 154 ++++++ Cargo.toml.orig | 56 +++ LICENSE | 1 + LICENSE-APACHE | 201 ++++++++ LICENSE-MIT | 21 + METADATA | 20 + MODULE_LICENSE_APACHE2 | 0 OWNERS | 1 + README.md | 23 + cargo_embargo.json | 3 + examples/completion-derive.rs | 83 ++++ examples/completion.rs | 109 +++++ examples/dynamic.rs | 37 ++ examples/exhaustive.rs | 203 ++++++++ src/dynamic/completer.rs | 341 ++++++++++++++ src/dynamic/mod.rs | 7 + src/dynamic/shells/bash.rs | 121 +++++ src/dynamic/shells/fish.rs | 46 ++ src/dynamic/shells/mod.rs | 82 ++++ src/dynamic/shells/shell.rs | 85 ++++ src/generator/mod.rs | 261 +++++++++++ src/generator/utils.rs | 278 +++++++++++ src/lib.rs | 74 +++ src/macros.rs | 21 + src/shells/bash.rs | 243 ++++++++++ src/shells/elvish.rs | 136 ++++++ src/shells/fish.rs | 201 ++++++++ src/shells/mod.rs | 15 + src/shells/powershell.rs | 142 ++++++ src/shells/shell.rs | 155 ++++++ src/shells/zsh.rs | 691 +++++++++++++++++++++++++++ 34 files changed, 4877 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 Android.bp create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 120000 LICENSE create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 METADATA create mode 100644 MODULE_LICENSE_APACHE2 create mode 100644 OWNERS create mode 100644 README.md create mode 100644 cargo_embargo.json create mode 100644 examples/completion-derive.rs create mode 100644 examples/completion.rs create mode 100644 examples/dynamic.rs create mode 100644 examples/exhaustive.rs create mode 100644 src/dynamic/completer.rs create mode 100644 src/dynamic/mod.rs create mode 100644 src/dynamic/shells/bash.rs create mode 100644 src/dynamic/shells/fish.rs create mode 100644 src/dynamic/shells/mod.rs create mode 100644 src/dynamic/shells/shell.rs create mode 100644 src/generator/mod.rs create mode 100644 src/generator/utils.rs create mode 100644 src/lib.rs create mode 100644 src/macros.rs create mode 100644 src/shells/bash.rs create mode 100644 src/shells/elvish.rs create mode 100644 src/shells/fish.rs create mode 100644 src/shells/mod.rs create mode 100644 src/shells/powershell.rs create mode 100644 src/shells/shell.rs create mode 100644 src/shells/zsh.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..2df91bd --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "9bfa5a338c6532419e2477e89708395fbb02ca06" + }, + "path_in_vcs": "clap_complete" +} \ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..f11d5ad --- /dev/null +++ b/Android.bp @@ -0,0 +1,21 @@ +// This file is generated by cargo_embargo. +// Do not modify this file as changes will be overridden on upgrade. + +// TODO: Add license. +rust_library { + name: "libclap_complete", + host_supported: true, + crate_name: "clap_complete", + cargo_env_compat: true, + cargo_pkg_version: "4.4.4", + srcs: ["src/lib.rs"], + edition: "2021", + features: ["default"], + rustlibs: ["libclap"], + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], + product_available: true, + vendor_available: true, +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..452559b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1039 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anstream" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff2cf94a3dbe2d57cbd56485e1bd7436455058034d6c2d47be51d4e5e4bc6ab" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0238ca56c96dfa37bdf7c373c8886dd591322500aceeeccdb2216fe06dc2f796" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +dependencies = [ + "anstyle", + "backtrace", + "bitflags", + "clap_lex 0.5.0", +] + +[[package]] +name = "clap_complete" +version = "4.4.4" +dependencies = [ + "clap", + "clap_lex 0.6.0", + "completest", + "is_executable", + "pathdiff", + "shlex", + "snapbox", + "trycmd", + "unicode-xid", +] + +[[package]] +name = "clap_derive" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "completest" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8084b60ec7306f1e9b4d855061147a5721eabbd860854213dd69679000cc86c" +dependencies = [ + "ptyprocess", + "vt100", +] + +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.6.5", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "escargot" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5584ba17d7ab26a8a7284f13e5bd196294dd2f2d79773cff29b9e9edef601a6" +dependencies = [ + "log", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.48.0", +] + +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", + "static_assertions", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "os_pipe" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptyprocess" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e05aef7befb11a210468a2d77d978dde2c6381a0381e33beb575e91f57fe8cf" +dependencies = [ + "nix", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustix" +version = "0.37.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "similar" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" + +[[package]] +name = "snapbox" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b439536a42c43be148b610c7f7f968fb79a457254910a9cb20900da73cd3271" +dependencies = [ + "anstream", + "anstyle", + "content_inspector", + "dunce", + "escargot", + "filetime", + "libc", + "normalize-line-endings", + "os_pipe", + "similar", + "snapbox-macros", + "tempfile", + "wait-timeout", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "snapbox-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1559baff8a696add3322b9be3e940d433e7bb4e38d79017205fd37ff28b28e" +dependencies = [ + "anstream", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "trycmd" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5bff680f217f2c7cc246aa5313ef9c1802449b1b8f859d28765355fda1c421f" +dependencies = [ + "glob", + "humantime", + "humantime-serde", + "rayon", + "serde", + "shlex", + "snapbox", + "toml_edit", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "vt100" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de" +dependencies = [ + "itoa", + "log", + "unicode-width", + "vte", +] + +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d71f151 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,154 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.70.0" +name = "clap_complete" +version = "4.4.4" +include = [ + "build.rs", + "src/**/*", + "Cargo.toml", + "LICENSE*", + "README.md", + "benches/**/*", + "examples/**/*", +] +description = "Generate shell completion scripts for your clap::Command" +readme = "README.md" +keywords = [ + "clap", + "cli", + "completion", + "bash", +] +categories = ["command-line-interface"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/clap-rs/clap/tree/master/clap_complete" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +min = 1 +replace = "{{version}}" +search = "Unreleased" + +[[package.metadata.release.pre-release-replacements]] +exactly = 1 +file = "CHANGELOG.md" +replace = "...{{tag_name}}" +search = '\.\.\.HEAD' + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +min = 1 +replace = "{{date}}" +search = "ReleaseDate" + +[[package.metadata.release.pre-release-replacements]] +exactly = 1 +file = "CHANGELOG.md" +replace = """ + +## [Unreleased] - ReleaseDate +""" +search = "" + +[[package.metadata.release.pre-release-replacements]] +exactly = 1 +file = "CHANGELOG.md" +replace = """ + +[Unreleased]: https://github.com/clap-rs/clap/compare/{{tag_name}}...HEAD""" +search = "" + +[[package.metadata.release.pre-release-replacements]] +exactly = 4 +file = "README.md" +prerelease = true +replace = "github.com/clap-rs/clap/blob/{{tag_name}}/" +search = "github.com/clap-rs/clap/blob/[^/]+/" + +[lib] +bench = false + +[[example]] +name = "dynamic" +required-features = ["unstable-dynamic"] + +[dependencies.clap] +version = "4.1.0" +features = ["std"] +default-features = false + +[dependencies.clap_lex] +version = "0.6.0" +optional = true + +[dependencies.is_executable] +version = "1.0.1" +optional = true + +[dependencies.pathdiff] +version = "0.2.1" +optional = true + +[dependencies.shlex] +version = "1.1.0" +optional = true + +[dependencies.unicode-xid] +version = "0.2.2" +optional = true + +[dev-dependencies.clap] +version = "4.0.0" +features = [ + "std", + "derive", + "help", +] +default-features = false + +[dev-dependencies.completest] +version = "0.1.0" + +[dev-dependencies.snapbox] +version = "0.4.13" +features = [ + "diff", + "path", + "examples", +] + +[dev-dependencies.trycmd] +version = "0.14.18" +features = [ + "color-auto", + "diff", + "examples", +] +default-features = false + +[features] +debug = ["clap/debug"] +default = [] +unstable-dynamic = [ + "dep:clap_lex", + "dep:shlex", + "dep:unicode-xid", + "clap/derive", + "dep:is_executable", + "dep:pathdiff", +] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..c059a33 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,56 @@ +[package] +name = "clap_complete" +version = "4.4.4" +description = "Generate shell completion scripts for your clap::Command" +repository = "https://github.com/clap-rs/clap/tree/master/clap_complete" +categories = ["command-line-interface"] +keywords = [ + "clap", + "cli", + "completion", + "bash", +] +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[package.metadata.release] +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/clap-rs/clap/compare/{{tag_name}}...HEAD", exactly=1}, + {file="README.md", search="github.com/clap-rs/clap/blob/[^/]+/", replace="github.com/clap-rs/clap/blob/{{tag_name}}/", exactly=4, prerelease = true}, +] + +[lib] +bench = false + +[dependencies] +clap = { path = "../", version = "4.1.0", default-features = false, features = ["std"] } +clap_lex = { path = "../clap_lex", version = "0.6.0", optional = true } +is_executable = { version = "1.0.1", optional = true } +pathdiff = { version = "0.2.1", optional = true } +shlex = { version = "1.1.0", optional = true } +unicode-xid = { version = "0.2.2", optional = true } + +[dev-dependencies] +snapbox = { version = "0.4.13", features = ["diff", "path", "examples"] } +# Cutting out `filesystem` feature +trycmd = { version = "0.14.18", default-features = false, features = ["color-auto", "diff", "examples"] } +completest = "0.1.0" +clap = { path = "../", version = "4.0.0", default-features = false, features = ["std", "derive", "help"] } + +[[example]] +name = "dynamic" +required-features = ["unstable-dynamic"] + +[features] +default = [] +unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff"] +debug = ["clap/debug"] diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000..6b579aa --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +LICENSE-APACHE \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..7b05b84 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2022 Kevin B. Knapp and Clap Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..d2f34a9 --- /dev/null +++ b/METADATA @@ -0,0 +1,20 @@ +name: "clap_complete" +description: "Generate shell completion scripts for your clap::Command" +third_party { + identifier { + type: "crates.io" + value: "https://crates.io/crates/clap_complete" + } + identifier { + type: "Archive" + value: "https://static.crates.io/crates/clap_complete/clap_complete-4.4.4.crate" + } + version: "4.4.4" + # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same. + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 11 + day: 20 + } +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..5a2b844 --- /dev/null +++ b/OWNERS @@ -0,0 +1 @@ +include platform/prebuilts/rust:main:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc283ce --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ + +# clap_complete + +> **Shell completion generation for `clap`** + +[![Crates.io](https://img.shields.io/crates/v/clap_complete?style=flat-square)](https://crates.io/crates/clap_complete) +[![Crates.io](https://img.shields.io/crates/d/clap_complete?style=flat-square)](https://crates.io/crates/clap_complete) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/clap_complete-v4.4.4/LICENSE-APACHE) +[![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/clap-rs/clap/blob/clap_complete-v4.4.4/LICENSE-MIT) + +Dual-licensed under [Apache 2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT). + +1. [About](#about) +2. [API Reference](https://docs.rs/clap_complete) +3. [Questions & Discussions](https://github.com/clap-rs/clap/discussions) +4. [CONTRIBUTING](https://github.com/clap-rs/clap/blob/clap_complete-v4.4.4/clap_complete/CONTRIBUTING.md) +5. [Sponsors](https://github.com/clap-rs/clap/blob/clap_complete-v4.4.4/README.md#sponsors) + +## About + +### Related Projects + +- [clap_complete_fig](https://crates.io/crates/clap_complete_fig) for [fig](https://fig.io/) shell completion support diff --git a/cargo_embargo.json b/cargo_embargo.json new file mode 100644 index 0000000..fca634f --- /dev/null +++ b/cargo_embargo.json @@ -0,0 +1,3 @@ +{ + "run_cargo": false +} diff --git a/examples/completion-derive.rs b/examples/completion-derive.rs new file mode 100644 index 0000000..9f1a55d --- /dev/null +++ b/examples/completion-derive.rs @@ -0,0 +1,83 @@ +//! How to use value hints and generate shell completions. +//! +//! Usage with zsh: +//! ```console +//! $ cargo run --example completion-derive -- --generate=zsh > /usr/local/share/zsh/site-functions/_completion_derive +//! $ compinit +//! $ ./target/debug/examples/completion_derive -- +//! ``` +//! fish: +//! ```console +//! $ cargo run --example completion-derive -- --generate=fish > completion_derive.fish +//! $ . ./completion_derive.fish +//! $ ./target/debug/examples/completion_derive -- +//! ``` +use clap::{Args, Command, CommandFactory, Parser, Subcommand, ValueHint}; +use clap_complete::{generate, Generator, Shell}; +use std::ffi::OsString; +use std::io; +use std::path::PathBuf; + +#[derive(Parser, Debug, PartialEq)] +#[command(name = "completion-derive")] +struct Opt { + // If provided, outputs the completion file for given shell + #[arg(long = "generate", value_enum)] + generator: Option, + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug, PartialEq)] +enum Commands { + #[command(visible_alias = "hint")] + ValueHint(ValueHintOpt), +} + +#[derive(Args, Debug, PartialEq)] +struct ValueHintOpt { + // Showcasing all possible ValueHints: + #[arg(long, value_hint = ValueHint::Unknown)] + unknown: Option, + #[arg(long, value_hint = ValueHint::Other)] + other: Option, + #[arg(short, long, value_hint = ValueHint::AnyPath)] + path: Option, + #[arg(short, long, value_hint = ValueHint::FilePath)] + file: Option, + #[arg(short, long, value_hint = ValueHint::DirPath)] + dir: Option, + #[arg(short, long, value_hint = ValueHint::ExecutablePath)] + exe: Option, + #[arg(long, value_hint = ValueHint::CommandName)] + cmd_name: Option, + #[arg(short, long, value_hint = ValueHint::CommandString)] + cmd: Option, + // Command::trailing_var_ar is required to use ValueHint::CommandWithArguments + #[arg(trailing_var_arg = true, value_hint = ValueHint::CommandWithArguments)] + command_with_args: Vec, + #[arg(short, long, value_hint = ValueHint::Username)] + user: Option, + #[arg(long, value_hint = ValueHint::Hostname)] + host: Option, + #[arg(long, value_hint = ValueHint::Url)] + url: Option, + #[arg(long, value_hint = ValueHint::EmailAddress)] + email: Option, +} + +fn print_completions(gen: G, cmd: &mut Command) { + generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); +} + +fn main() { + let opt = Opt::parse(); + + if let Some(generator) = opt.generator { + let mut cmd = Opt::command(); + eprintln!("Generating completion file for {generator:?}..."); + print_completions(generator, &mut cmd); + } else { + println!("{opt:#?}"); + } +} diff --git a/examples/completion.rs b/examples/completion.rs new file mode 100644 index 0000000..0890542 --- /dev/null +++ b/examples/completion.rs @@ -0,0 +1,109 @@ +//! Example to test arguments with different ValueHint values. +//! +//! Usage with zsh: +//! ```console +//! $ cargo run --example completion -- --generate=zsh > /usr/local/share/zsh/site-functions/_completion$ +//! $ compinit +//! $ ./target/debug/examples/completion -- +//! ``` +//! fish: +//! ```console +//! $ cargo run --example completion -- --generate=fish > completion.fish +//! $ . ./completion.fish +//! $ ./target/debug/examples/completion -- +//! ``` +use clap::{value_parser, Arg, Command, ValueHint}; +use clap_complete::{generate, Generator, Shell}; +use std::io; + +fn build_cli() -> Command { + let value_hint_command = Command::new("value-hint") + .visible_alias("hint") + .arg( + Arg::new("unknown") + .long("unknown") + .value_hint(ValueHint::Unknown), + ) + .arg(Arg::new("other").long("other").value_hint(ValueHint::Other)) + .arg( + Arg::new("path") + .long("path") + .short('p') + .value_hint(ValueHint::AnyPath), + ) + .arg( + Arg::new("file") + .long("file") + .short('f') + .value_hint(ValueHint::FilePath), + ) + .arg( + Arg::new("dir") + .long("dir") + .short('d') + .value_hint(ValueHint::DirPath), + ) + .arg( + Arg::new("exe") + .long("exe") + .short('e') + .value_hint(ValueHint::ExecutablePath), + ) + .arg( + Arg::new("cmd_name") + .long("cmd-name") + .value_hint(ValueHint::CommandName), + ) + .arg( + Arg::new("cmd") + .long("cmd") + .short('c') + .value_hint(ValueHint::CommandString), + ) + .arg( + Arg::new("command_with_args") + .num_args(1..) + // AppSettings::TrailingVarArg is required to use ValueHint::CommandWithArguments + .trailing_var_arg(true) + .value_hint(ValueHint::CommandWithArguments), + ) + .arg( + Arg::new("user") + .short('u') + .long("user") + .value_hint(ValueHint::Username), + ) + .arg( + Arg::new("host") + .long("host") + .value_hint(ValueHint::Hostname), + ) + .arg(Arg::new("url").long("url").value_hint(ValueHint::Url)) + .arg( + Arg::new("email") + .long("email") + .value_hint(ValueHint::EmailAddress), + ); + + Command::new("completion") + .arg( + Arg::new("generator") + .long("generate") + .value_parser(value_parser!(Shell)), + ) + .subcommand(value_hint_command) +} + +fn print_completions(gen: G, cmd: &mut Command) { + generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); +} + +fn main() { + let matches = build_cli().get_matches(); + + if let Some(generator) = matches.get_one::("generator") { + let mut cmd = build_cli(); + eprintln!("Generating completion file for {generator}..."); + print_completions(*generator, &mut cmd); + } +} diff --git a/examples/dynamic.rs b/examples/dynamic.rs new file mode 100644 index 0000000..ccaf7d8 --- /dev/null +++ b/examples/dynamic.rs @@ -0,0 +1,37 @@ +use clap::FromArgMatches; +use clap::Subcommand; + +fn command() -> clap::Command { + let cmd = clap::Command::new("dynamic") + .arg( + clap::Arg::new("input") + .long("input") + .short('i') + .value_hint(clap::ValueHint::FilePath), + ) + .arg( + clap::Arg::new("format") + .long("format") + .short('F') + .value_parser(["json", "yaml", "toml"]), + ) + .args_conflicts_with_subcommands(true); + clap_complete::dynamic::shells::CompleteCommand::augment_subcommands(cmd) +} + +fn main() { + let cmd = command(); + let matches = cmd.get_matches(); + if let Ok(completions) = + clap_complete::dynamic::shells::CompleteCommand::from_arg_matches(&matches) + { + completions.complete(&mut command()); + } else { + println!("{matches:#?}"); + } +} + +#[test] +fn verify_cli() { + command().debug_assert(); +} diff --git a/examples/exhaustive.rs b/examples/exhaustive.rs new file mode 100644 index 0000000..de00da6 --- /dev/null +++ b/examples/exhaustive.rs @@ -0,0 +1,203 @@ +use clap::builder::PossibleValue; +#[cfg(feature = "unstable-dynamic")] +use clap::{FromArgMatches, Subcommand}; +use clap_complete::{generate, Generator, Shell}; + +fn main() { + let matches = cli().get_matches(); + if let Some(generator) = matches.get_one::("generate") { + let mut cmd = cli(); + eprintln!("Generating completion file for {generator}..."); + print_completions(*generator, &mut cmd); + return; + } + + #[cfg(feature = "unstable-dynamic")] + if let Ok(completions) = + clap_complete::dynamic::shells::CompleteCommand::from_arg_matches(&matches) + { + completions.complete(&mut cli()); + return; + }; + + println!("{:?}", matches); +} + +fn print_completions(gen: G, cmd: &mut clap::Command) { + generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout()); +} + +#[allow(clippy::let_and_return)] +fn cli() -> clap::Command { + let cli = clap::Command::new("exhaustive") + .version("3.0") + .propagate_version(true) + .args([ + clap::Arg::new("global") + .long("global") + .global(true) + .action(clap::ArgAction::SetTrue) + .help("everywhere"), + clap::Arg::new("generate") + .long("generate") + .value_name("SHELL") + .value_parser(clap::value_parser!(Shell)) + .help("generate"), + ]) + .subcommands([ + clap::Command::new("action").args([ + clap::Arg::new("set-true") + .long("set-true") + .action(clap::ArgAction::SetTrue) + .help("bool"), + clap::Arg::new("set") + .long("set") + .action(clap::ArgAction::Set) + .help("value"), + clap::Arg::new("count") + .long("count") + .action(clap::ArgAction::Count) + .help("number"), + clap::Arg::new("choice") + .long("choice") + .value_parser(["first", "second"]) + .help("enum"), + ]), + clap::Command::new("quote") + .args([ + clap::Arg::new("single-quotes") + .long("single-quotes") + .action(clap::ArgAction::SetTrue) + .help("Can be 'always', 'auto', or 'never'"), + clap::Arg::new("double-quotes") + .long("double-quotes") + .action(clap::ArgAction::SetTrue) + .help("Can be \"always\", \"auto\", or \"never\""), + clap::Arg::new("backticks") + .long("backticks") + .action(clap::ArgAction::SetTrue) + .help("For more information see `echo test`"), + clap::Arg::new("backslash") + .long("backslash") + .action(clap::ArgAction::SetTrue) + .help("Avoid '\\n'"), + clap::Arg::new("brackets") + .long("brackets") + .action(clap::ArgAction::SetTrue) + .help("List packages [filter]"), + clap::Arg::new("expansions") + .long("expansions") + .action(clap::ArgAction::SetTrue) + .help("Execute the shell command with $SHELL"), + clap::Arg::new("choice") + .long("choice") + .action(clap::ArgAction::Set) + .value_parser(clap::builder::PossibleValuesParser::new([ + PossibleValue::new("bash").help("bash (shell)"), + PossibleValue::new("fish").help("fish shell"), + PossibleValue::new("zsh").help("zsh shell"), + ])), + ]) + .subcommands([ + clap::Command::new("cmd-single-quotes") + .about("Can be 'always', 'auto', or 'never'"), + clap::Command::new("cmd-double-quotes") + .about("Can be \"always\", \"auto\", or \"never\""), + clap::Command::new("cmd-backticks") + .about("For more information see `echo test`"), + clap::Command::new("cmd-backslash").about("Avoid '\\n'"), + clap::Command::new("cmd-brackets").about("List packages [filter]"), + clap::Command::new("cmd-expansions") + .about("Execute the shell command with $SHELL"), + clap::Command::new("escape-help").about("\\tab\t\"'\nNew Line"), + ]), + clap::Command::new("value").args([ + clap::Arg::new("delim").long("delim").value_delimiter(','), + clap::Arg::new("tuple").long("tuple").num_args(2), + clap::Arg::new("require-eq") + .long("require-eq") + .require_equals(true), + clap::Arg::new("term").num_args(1..).value_terminator(";"), + ]), + clap::Command::new("pacman").subcommands([ + clap::Command::new("one").long_flag("one").short_flag('o'), + clap::Command::new("two").long_flag("two").short_flag('t'), + ]), + clap::Command::new("last") + .args([clap::Arg::new("first"), clap::Arg::new("free").last(true)]), + clap::Command::new("alias").args([ + clap::Arg::new("flag") + .short('f') + .visible_short_alias('F') + .long("flag") + .action(clap::ArgAction::SetTrue) + .visible_alias("flg") + .help("cmd flag"), + clap::Arg::new("option") + .short('o') + .visible_short_alias('O') + .long("option") + .visible_alias("opt") + .help("cmd option") + .action(clap::ArgAction::Set), + clap::Arg::new("positional"), + ]), + clap::Command::new("hint").args([ + clap::Arg::new("choice") + .long("choice") + .action(clap::ArgAction::Set) + .value_parser(["bash", "fish", "zsh"]), + clap::Arg::new("unknown") + .long("unknown") + .value_hint(clap::ValueHint::Unknown), + clap::Arg::new("other") + .long("other") + .value_hint(clap::ValueHint::Other), + clap::Arg::new("path") + .long("path") + .short('p') + .value_hint(clap::ValueHint::AnyPath), + clap::Arg::new("file") + .long("file") + .short('f') + .value_hint(clap::ValueHint::FilePath), + clap::Arg::new("dir") + .long("dir") + .short('d') + .value_hint(clap::ValueHint::DirPath), + clap::Arg::new("exe") + .long("exe") + .short('e') + .value_hint(clap::ValueHint::ExecutablePath), + clap::Arg::new("cmd_name") + .long("cmd-name") + .value_hint(clap::ValueHint::CommandName), + clap::Arg::new("cmd") + .long("cmd") + .short('c') + .value_hint(clap::ValueHint::CommandString), + clap::Arg::new("command_with_args") + .action(clap::ArgAction::Set) + .num_args(1..) + .trailing_var_arg(true) + .value_hint(clap::ValueHint::CommandWithArguments), + clap::Arg::new("user") + .short('u') + .long("user") + .value_hint(clap::ValueHint::Username), + clap::Arg::new("host") + .short('H') + .long("host") + .value_hint(clap::ValueHint::Hostname), + clap::Arg::new("url") + .long("url") + .value_hint(clap::ValueHint::Url), + clap::Arg::new("email") + .long("email") + .value_hint(clap::ValueHint::EmailAddress), + ]), + ]); + #[cfg(feature = "unstable-dynamic")] + let cli = clap_complete::dynamic::shells::CompleteCommand::augment_subcommands(cli); + cli +} diff --git a/src/dynamic/completer.rs b/src/dynamic/completer.rs new file mode 100644 index 0000000..8c8cb93 --- /dev/null +++ b/src/dynamic/completer.rs @@ -0,0 +1,341 @@ +use std::ffi::OsStr; +use std::ffi::OsString; + +use clap::builder::StyledStr; +use clap_lex::OsStrExt as _; + +/// Shell-specific completions +pub trait Completer { + /// The recommended file name for the registration code + fn file_name(&self, name: &str) -> String; + /// Register for completions + fn write_registration( + &self, + name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error>; + /// Complete the command + fn write_complete( + &self, + cmd: &mut clap::Command, + args: Vec, + current_dir: Option<&std::path::Path>, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error>; +} + +/// Complete the command specified +pub fn complete( + cmd: &mut clap::Command, + args: Vec, + arg_index: usize, + current_dir: Option<&std::path::Path>, +) -> Result)>, std::io::Error> { + cmd.build(); + + let raw_args = clap_lex::RawArgs::new(args); + let mut cursor = raw_args.cursor(); + let mut target_cursor = raw_args.cursor(); + raw_args.seek( + &mut target_cursor, + clap_lex::SeekFrom::Start(arg_index as u64), + ); + // As we loop, `cursor` will always be pointing to the next item + raw_args.next_os(&mut target_cursor); + + // TODO: Multicall support + if !cmd.is_no_binary_name_set() { + raw_args.next_os(&mut cursor); + } + + let mut current_cmd = &*cmd; + let mut pos_index = 1; + let mut is_escaped = false; + while let Some(arg) = raw_args.next(&mut cursor) { + if cursor == target_cursor { + return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped); + } + + debug!("complete::next: Begin parsing '{:?}'", arg.to_value_os(),); + + if let Ok(value) = arg.to_value() { + if let Some(next_cmd) = current_cmd.find_subcommand(value) { + current_cmd = next_cmd; + pos_index = 1; + continue; + } + } + + if is_escaped { + pos_index += 1; + } else if arg.is_escape() { + is_escaped = true; + } else if let Some(_long) = arg.to_long() { + } else if let Some(_short) = arg.to_short() { + } else { + pos_index += 1; + } + } + + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "no completion generated", + )) +} + +fn complete_arg( + arg: &clap_lex::ParsedArg<'_>, + cmd: &clap::Command, + current_dir: Option<&std::path::Path>, + pos_index: usize, + is_escaped: bool, +) -> Result)>, std::io::Error> { + debug!( + "complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}", + arg, + cmd.get_name(), + current_dir, + pos_index, + is_escaped + ); + let mut completions = Vec::new(); + + if !is_escaped { + if let Some((flag, value)) = arg.to_long() { + if let Ok(flag) = flag { + if let Some(value) = value { + if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) { + completions.extend( + complete_arg_value(value.to_str().ok_or(value), arg, current_dir) + .into_iter() + .map(|(os, help)| { + // HACK: Need better `OsStr` manipulation + (format!("--{}={}", flag, os.to_string_lossy()).into(), help) + }), + ) + } + } else { + completions.extend(longs_and_visible_aliases(cmd).into_iter().filter_map( + |(f, help)| f.starts_with(flag).then(|| (format!("--{f}").into(), help)), + )); + } + } + } else if arg.is_escape() || arg.is_stdio() || arg.is_empty() { + // HACK: Assuming knowledge of is_escape / is_stdio + completions.extend( + longs_and_visible_aliases(cmd) + .into_iter() + .map(|(f, help)| (format!("--{f}").into(), help)), + ); + } + + if arg.is_empty() || arg.is_stdio() || arg.is_short() { + let dash_or_arg = if arg.is_empty() { + "-".into() + } else { + arg.to_value_os().to_string_lossy() + }; + // HACK: Assuming knowledge of is_stdio + completions.extend( + shorts_and_visible_aliases(cmd) + .into_iter() + // HACK: Need better `OsStr` manipulation + .map(|(f, help)| (format!("{}{}", dash_or_arg, f).into(), help)), + ); + } + } + + if let Some(positional) = cmd + .get_positionals() + .find(|p| p.get_index() == Some(pos_index)) + { + completions.extend(complete_arg_value(arg.to_value(), positional, current_dir)); + } + + if let Ok(value) = arg.to_value() { + completions.extend(complete_subcommand(value, cmd)); + } + + Ok(completions) +} + +fn complete_arg_value( + value: Result<&str, &OsStr>, + arg: &clap::Arg, + current_dir: Option<&std::path::Path>, +) -> Vec<(OsString, Option)> { + let mut values = Vec::new(); + debug!("complete_arg_value: arg={arg:?}, value={value:?}"); + + if let Some(possible_values) = possible_values(arg) { + if let Ok(value) = value { + values.extend(possible_values.into_iter().filter_map(|p| { + let name = p.get_name(); + name.starts_with(value) + .then(|| (name.into(), p.get_help().cloned())) + })); + } + } else { + let value_os = match value { + Ok(value) => OsStr::new(value), + Err(value_os) => value_os, + }; + match arg.get_value_hint() { + clap::ValueHint::Other => { + // Should not complete + } + clap::ValueHint::Unknown | clap::ValueHint::AnyPath => { + values.extend(complete_path(value_os, current_dir, |_| true)); + } + clap::ValueHint::FilePath => { + values.extend(complete_path(value_os, current_dir, |p| p.is_file())); + } + clap::ValueHint::DirPath => { + values.extend(complete_path(value_os, current_dir, |p| p.is_dir())); + } + clap::ValueHint::ExecutablePath => { + use is_executable::IsExecutable; + values.extend(complete_path(value_os, current_dir, |p| p.is_executable())); + } + clap::ValueHint::CommandName + | clap::ValueHint::CommandString + | clap::ValueHint::CommandWithArguments + | clap::ValueHint::Username + | clap::ValueHint::Hostname + | clap::ValueHint::Url + | clap::ValueHint::EmailAddress => { + // No completion implementation + } + _ => { + // Safe-ish fallback + values.extend(complete_path(value_os, current_dir, |_| true)); + } + } + values.sort(); + } + + values +} + +fn complete_path( + value_os: &OsStr, + current_dir: Option<&std::path::Path>, + is_wanted: impl Fn(&std::path::Path) -> bool, +) -> Vec<(OsString, Option)> { + let mut completions = Vec::new(); + + let current_dir = match current_dir { + Some(current_dir) => current_dir, + None => { + // Can't complete without a `current_dir` + return Vec::new(); + } + }; + let (existing, prefix) = value_os + .split_once("\\") + .unwrap_or((OsStr::new(""), value_os)); + let root = current_dir.join(existing); + debug!("complete_path: root={root:?}, prefix={prefix:?}"); + let prefix = prefix.to_string_lossy(); + + for entry in std::fs::read_dir(&root) + .ok() + .into_iter() + .flatten() + .filter_map(Result::ok) + { + let raw_file_name = entry.file_name(); + if !raw_file_name.starts_with(&prefix) { + continue; + } + + if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) { + let path = entry.path(); + let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); + suggestion.push(""); // Ensure trailing `/` + completions.push((suggestion.as_os_str().to_owned(), None)); + } else { + let path = entry.path(); + if is_wanted(&path) { + let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path); + completions.push((suggestion.as_os_str().to_owned(), None)); + } + } + } + + completions +} + +fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option)> { + debug!( + "complete_subcommand: cmd={:?}, value={:?}", + cmd.get_name(), + value + ); + + let mut scs = subcommands(cmd) + .into_iter() + .filter(|x| x.0.starts_with(value)) + .map(|x| (OsString::from(&x.0), x.1)) + .collect::>(); + scs.sort(); + scs.dedup(); + scs +} + +/// Gets all the long options, their visible aliases and flags of a [`clap::Command`]. +/// Includes `help` and `version` depending on the [`clap::Command`] settings. +fn longs_and_visible_aliases(p: &clap::Command) -> Vec<(String, Option)> { + debug!("longs: name={}", p.get_name()); + + p.get_arguments() + .filter_map(|a| { + a.get_long_and_visible_aliases().map(|longs| { + longs + .into_iter() + .map(|s| (s.to_string(), a.get_help().cloned())) + }) + }) + .flatten() + .collect() +} + +/// Gets all the short options, their visible aliases and flags of a [`clap::Command`]. +/// Includes `h` and `V` depending on the [`clap::Command`] settings. +fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<(char, Option)> { + debug!("shorts: name={}", p.get_name()); + + p.get_arguments() + .filter_map(|a| { + a.get_short_and_visible_aliases() + .map(|shorts| shorts.into_iter().map(|s| (s, a.get_help().cloned()))) + }) + .flatten() + .collect() +} + +/// Get the possible values for completion +fn possible_values(a: &clap::Arg) -> Option> { + if !a.get_num_args().expect("built").takes_values() { + None + } else { + a.get_value_parser() + .possible_values() + .map(|pvs| pvs.collect()) + } +} + +/// Gets subcommands of [`clap::Command`] in the form of `("name", "bin_name")`. +/// +/// Subcommand `rustup toolchain install` would be converted to +/// `("install", "rustup toolchain install")`. +fn subcommands(p: &clap::Command) -> Vec<(String, Option)> { + debug!("subcommands: name={}", p.get_name()); + debug!("subcommands: Has subcommands...{:?}", p.has_subcommands()); + + p.get_subcommands() + .map(|sc| (sc.get_name().to_string(), sc.get_about().cloned())) + .collect() +} diff --git a/src/dynamic/mod.rs b/src/dynamic/mod.rs new file mode 100644 index 0000000..f7c9857 --- /dev/null +++ b/src/dynamic/mod.rs @@ -0,0 +1,7 @@ +//! Complete commands within shells + +mod completer; + +pub mod shells; + +pub use completer::*; diff --git a/src/dynamic/shells/bash.rs b/src/dynamic/shells/bash.rs new file mode 100644 index 0000000..43c128e --- /dev/null +++ b/src/dynamic/shells/bash.rs @@ -0,0 +1,121 @@ +use unicode_xid::UnicodeXID as _; + +/// Bash completions +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Bash; + +impl crate::dynamic::Completer for Bash { + fn file_name(&self, name: &str) -> String { + format!("{name}.bash") + } + fn write_registration( + &self, + name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let escaped_name = name.replace('-', "_"); + debug_assert!( + escaped_name.chars().all(|c| c.is_xid_continue()), + "`name` must be an identifier, got `{escaped_name}`" + ); + let mut upper_name = escaped_name.clone(); + upper_name.make_ascii_uppercase(); + + let completer = shlex::quote(completer); + + let script = r#" +_clap_complete_NAME() { + export IFS=$'\013' + export _CLAP_COMPLETE_INDEX=${COMP_CWORD} + export _CLAP_COMPLETE_COMP_TYPE=${COMP_TYPE} + if compopt +o nospace 2> /dev/null; then + export _CLAP_COMPLETE_SPACE=false + else + export _CLAP_COMPLETE_SPACE=true + fi + COMPREPLY=( $("COMPLETER" complete --shell bash -- "${COMP_WORDS[@]}") ) + if [[ $? != 0 ]]; then + unset COMPREPLY + elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then + compopt -o nospace + fi +} +complete -o nospace -o bashdefault -F _clap_complete_NAME BIN +"# + .replace("NAME", &escaped_name) + .replace("BIN", bin) + .replace("COMPLETER", &completer) + .replace("UPPER", &upper_name); + + writeln!(buf, "{script}")?; + Ok(()) + } + fn write_complete( + &self, + cmd: &mut clap::Command, + args: Vec, + current_dir: Option<&std::path::Path>, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let index: usize = std::env::var("_CLAP_COMPLETE_INDEX") + .ok() + .and_then(|i| i.parse().ok()) + .unwrap_or_default(); + let _comp_type: CompType = std::env::var("_CLAP_COMPLETE_COMP_TYPE") + .ok() + .and_then(|i| i.parse().ok()) + .unwrap_or_default(); + let _space: Option = std::env::var("_CLAP_COMPLETE_SPACE") + .ok() + .and_then(|i| i.parse().ok()); + let ifs: Option = std::env::var("IFS").ok().and_then(|i| i.parse().ok()); + let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; + + for (i, (completion, _)) in completions.iter().enumerate() { + if i != 0 { + write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?; + } + write!(buf, "{}", completion.to_string_lossy())?; + } + Ok(()) + } +} + +/// Type of completion attempted that caused a completion function to be called +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +enum CompType { + /// Normal completion + Normal, + /// List completions after successive tabs + Successive, + /// List alternatives on partial word completion + Alternatives, + /// List completions if the word is not unmodified + Unmodified, + /// Menu completion + Menu, +} + +impl std::str::FromStr for CompType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "9" => Ok(Self::Normal), + "63" => Ok(Self::Successive), + "33" => Ok(Self::Alternatives), + "64" => Ok(Self::Unmodified), + "37" => Ok(Self::Menu), + _ => Err(format!("unsupported COMP_TYPE `{}`", s)), + } + } +} + +impl Default for CompType { + fn default() -> Self { + Self::Normal + } +} diff --git a/src/dynamic/shells/fish.rs b/src/dynamic/shells/fish.rs new file mode 100644 index 0000000..9d7e8c6 --- /dev/null +++ b/src/dynamic/shells/fish.rs @@ -0,0 +1,46 @@ +/// Fish completions +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Fish; + +impl crate::dynamic::Completer for Fish { + fn file_name(&self, name: &str) -> String { + format!("{name}.fish") + } + fn write_registration( + &self, + _name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let bin = shlex::quote(bin); + let completer = shlex::quote(completer); + writeln!( + buf, + r#"complete -x -c {bin} -a "("'{completer}'" complete --shell fish -- (commandline --current-process --tokenize --cut-at-cursor) (commandline --current-token))""# + ) + } + fn write_complete( + &self, + cmd: &mut clap::Command, + args: Vec, + current_dir: Option<&std::path::Path>, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + let index = args.len() - 1; + let completions = crate::dynamic::complete(cmd, args, index, current_dir)?; + + for (completion, help) in completions { + write!(buf, "{}", completion.to_string_lossy())?; + if let Some(help) = help { + write!( + buf, + "\t{}", + help.to_string().lines().next().unwrap_or_default() + )?; + } + writeln!(buf)?; + } + Ok(()) + } +} diff --git a/src/dynamic/shells/mod.rs b/src/dynamic/shells/mod.rs new file mode 100644 index 0000000..54d23a3 --- /dev/null +++ b/src/dynamic/shells/mod.rs @@ -0,0 +1,82 @@ +//! Shell support + +mod bash; +mod fish; +mod shell; + +pub use bash::*; +pub use fish::*; +pub use shell::*; + +use std::ffi::OsString; +use std::io::Write as _; + +use crate::dynamic::Completer as _; + +#[derive(clap::Subcommand)] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum CompleteCommand { + /// Register shell completions for this program + #[command(hide = true)] + Complete(CompleteArgs), +} + +#[derive(clap::Args)] +#[command(arg_required_else_help = true)] +#[command(group = clap::ArgGroup::new("complete").multiple(true).conflicts_with("register"))] +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct CompleteArgs { + /// Specify shell to complete for + #[arg(long)] + shell: Shell, + + /// Path to write completion-registration to + #[arg(long, required = true)] + register: Option, + + #[arg(raw = true, hide_short_help = true, group = "complete")] + comp_words: Vec, +} + +impl CompleteCommand { + /// Process the completion request + pub fn complete(&self, cmd: &mut clap::Command) -> std::convert::Infallible { + self.try_complete(cmd).unwrap_or_else(|e| e.exit()); + std::process::exit(0) + } + + /// Process the completion request + pub fn try_complete(&self, cmd: &mut clap::Command) -> clap::error::Result<()> { + debug!("CompleteCommand::try_complete: {self:?}"); + let CompleteCommand::Complete(args) = self; + if let Some(out_path) = args.register.as_deref() { + let mut buf = Vec::new(); + let name = cmd.get_name(); + let bin = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); + args.shell.write_registration(name, bin, bin, &mut buf)?; + if out_path == std::path::Path::new("-") { + std::io::stdout().write_all(&buf)?; + } else if out_path.is_dir() { + let out_path = out_path.join(args.shell.file_name(name)); + std::fs::write(out_path, buf)?; + } else { + std::fs::write(out_path, buf)?; + } + } else { + let current_dir = std::env::current_dir().ok(); + + let mut buf = Vec::new(); + args.shell.write_complete( + cmd, + args.comp_words.clone(), + current_dir.as_deref(), + &mut buf, + )?; + std::io::stdout().write_all(&buf)?; + } + + Ok(()) + } +} diff --git a/src/dynamic/shells/shell.rs b/src/dynamic/shells/shell.rs new file mode 100644 index 0000000..a9f48ce --- /dev/null +++ b/src/dynamic/shells/shell.rs @@ -0,0 +1,85 @@ +use std::fmt::Display; +use std::str::FromStr; + +use clap::builder::PossibleValue; +use clap::ValueEnum; + +/// Shell with auto-generated completion script available. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum Shell { + /// Bourne Again SHell (bash) + Bash, + /// Friendly Interactive SHell (fish) + Fish, +} + +impl Display for Shell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.to_possible_value() + .expect("no values are skipped") + .get_name() + .fmt(f) + } +} + +impl FromStr for Shell { + type Err = String; + + fn from_str(s: &str) -> Result { + for variant in Self::value_variants() { + if variant.to_possible_value().unwrap().matches(s, false) { + return Ok(*variant); + } + } + Err(format!("invalid variant: {s}")) + } +} + +// Hand-rolled so it can work even when `derive` feature is disabled +impl ValueEnum for Shell { + fn value_variants<'a>() -> &'a [Self] { + &[Shell::Bash, Shell::Fish] + } + + fn to_possible_value<'a>(&self) -> Option { + Some(match self { + Shell::Bash => PossibleValue::new("bash"), + Shell::Fish => PossibleValue::new("fish"), + }) + } +} + +impl Shell { + fn completer(&self) -> &dyn crate::dynamic::Completer { + match self { + Self::Bash => &super::Bash, + Self::Fish => &super::Fish, + } + } +} + +impl crate::dynamic::Completer for Shell { + fn file_name(&self, name: &str) -> String { + self.completer().file_name(name) + } + fn write_registration( + &self, + name: &str, + bin: &str, + completer: &str, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + self.completer() + .write_registration(name, bin, completer, buf) + } + fn write_complete( + &self, + cmd: &mut clap::Command, + args: Vec, + current_dir: Option<&std::path::Path>, + buf: &mut dyn std::io::Write, + ) -> Result<(), std::io::Error> { + self.completer().write_complete(cmd, args, current_dir, buf) + } +} diff --git a/src/generator/mod.rs b/src/generator/mod.rs new file mode 100644 index 0000000..a371f68 --- /dev/null +++ b/src/generator/mod.rs @@ -0,0 +1,261 @@ +//! Shell completion machinery + +pub mod utils; + +use std::ffi::OsString; +use std::fs::File; +use std::io::Error; +use std::io::Write; +use std::path::PathBuf; + +use clap::Command; + +/// Generator trait which can be used to write generators +pub trait Generator { + /// Returns the file name that is created when this generator is called during compile time. + /// + /// # Panics + /// + /// May panic when called outside of the context of [`generate`] or [`generate_to`] + /// + /// # Examples + /// + /// ``` + /// # use std::io::Write; + /// # use clap::Command; + /// use clap_complete::Generator; + /// + /// pub struct Fish; + /// + /// impl Generator for Fish { + /// fn file_name(&self, name: &str) -> String { + /// format!("{name}.fish") + /// } + /// # fn generate(&self, cmd: &Command, buf: &mut dyn Write) {} + /// } + /// ``` + fn file_name(&self, name: &str) -> String; + + /// Generates output out of [`clap::Command`](Command). + /// + /// # Panics + /// + /// May panic when called outside of the context of [`generate`] or [`generate_to`] + /// + /// # Examples + /// + /// The following example generator displays the [`clap::Command`](Command) + /// as if it is printed using [`std::println`]. + /// + /// ``` + /// use std::{io::Write, fmt::write}; + /// use clap::Command; + /// use clap_complete::Generator; + /// + /// pub struct ClapDebug; + /// + /// impl Generator for ClapDebug { + /// # fn file_name(&self, name: &str) -> String { + /// # name.into() + /// # } + /// fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + /// write!(buf, "{cmd}").unwrap(); + /// } + /// } + /// ``` + fn generate(&self, cmd: &Command, buf: &mut dyn Write); +} + +/// Generate a completions file for a specified shell at compile-time. +/// +/// **NOTE:** to generate the file at compile time you must use a `build.rs` "Build Script" or a +/// [`cargo-xtask`](https://github.com/matklad/cargo-xtask) +/// +/// # Examples +/// +/// The following example generates a bash completion script via a `build.rs` script. In this +/// simple example, we'll demo a very small application with only a single subcommand and two +/// args. Real applications could be many multiple levels deep in subcommands, and have tens or +/// potentially hundreds of arguments. +/// +/// First, it helps if we separate out our `Command` definition into a separate file. Whether you +/// do this as a function, or bare Command definition is a matter of personal preference. +/// +/// ``` +/// // src/cli.rs +/// # use clap::{Command, Arg, ArgAction}; +/// pub fn build_cli() -> Command { +/// Command::new("compl") +/// .about("Tests completions") +/// .arg(Arg::new("file") +/// .help("some input file")) +/// .subcommand(Command::new("test") +/// .about("tests things") +/// .arg(Arg::new("case") +/// .long("case") +/// .action(ArgAction::Set) +/// .help("the case to test"))) +/// } +/// ``` +/// +/// In our regular code, we can simply call this `build_cli()` function, then call +/// `get_matches()`, or any of the other normal methods directly after. For example: +/// +/// ```ignore +/// // src/main.rs +/// +/// mod cli; +/// +/// fn main() { +/// let _m = cli::build_cli().get_matches(); +/// +/// // normal logic continues... +/// } +/// ``` +/// +/// Next, we set up our `Cargo.toml` to use a `build.rs` build script. +/// +/// ```toml +/// # Cargo.toml +/// build = "build.rs" +/// +/// [dependencies] +/// clap = "*" +/// +/// [build-dependencies] +/// clap = "*" +/// clap_complete = "*" +/// ``` +/// +/// Next, we place a `build.rs` in our project root. +/// +/// ```ignore +/// use clap_complete::{generate_to, shells::Bash}; +/// use std::env; +/// use std::io::Error; +/// +/// include!("src/cli.rs"); +/// +/// fn main() -> Result<(), Error> { +/// let outdir = match env::var_os("OUT_DIR") { +/// None => return Ok(()), +/// Some(outdir) => outdir, +/// }; +/// +/// let mut cmd = build_cli(); +/// let path = generate_to( +/// Bash, +/// &mut cmd, // We need to specify what generator to use +/// "myapp", // We need to specify the bin name manually +/// outdir, // We need to specify where to write to +/// )?; +/// +/// println!("cargo:warning=completion file is generated: {path:?}"); +/// +/// Ok(()) +/// } +/// ``` +/// +/// Now, once we compile there will be a `{bin_name}.bash` file in the directory. +/// Assuming we compiled with debug mode, it would be somewhere similar to +/// `/target/debug/build/myapp-/out/myapp.bash`. +/// +/// **NOTE:** Please look at the individual [shells][crate::shells] +/// to see the name of the files generated. +/// +/// Using [`ValueEnum::value_variants()`][clap::ValueEnum::value_variants] you can easily loop over +/// all the supported shell variants to generate all the completions at once too. +/// +/// ```ignore +/// use clap::ValueEnum; +/// use clap_complete::{generate_to, Shell}; +/// use std::env; +/// use std::io::Error; +/// +/// include!("src/cli.rs"); +/// +/// fn main() -> Result<(), Error> { +/// let outdir = match env::var_os("OUT_DIR") { +/// None => return Ok(()), +/// Some(outdir) => outdir, +/// }; +/// +/// let mut cmd = build_cli(); +/// for &shell in Shell::value_variants() { +/// generate_to(shell, &mut cmd, "myapp", outdir)?; +/// } +/// +/// Ok(()) +/// } +/// ``` +pub fn generate_to( + gen: G, + cmd: &mut Command, + bin_name: S, + out_dir: T, +) -> Result +where + G: Generator, + S: Into, + T: Into, +{ + cmd.set_bin_name(bin_name); + + let out_dir = PathBuf::from(out_dir.into()); + let file_name = gen.file_name(cmd.get_bin_name().unwrap()); + + let path = out_dir.join(file_name); + let mut file = File::create(&path)?; + + _generate::(gen, cmd, &mut file); + Ok(path) +} + +/// Generate a completions file for a specified shell at runtime. +/// +/// Until `cargo install` can install extra files like a completion script, this may be +/// used e.g. in a command that outputs the contents of the completion script, to be +/// redirected into a file by the user. +/// +/// # Examples +/// +/// Assuming a separate `cli.rs` like the [`generate_to` example](generate_to()), +/// we can let users generate a completion script using a command: +/// +/// ```ignore +/// // src/main.rs +/// +/// mod cli; +/// use std::io; +/// use clap_complete::{generate, shells::Bash}; +/// +/// fn main() { +/// let matches = cli::build_cli().get_matches(); +/// +/// if matches.is_present("generate-bash-completions") { +/// generate(Bash, &mut cli::build_cli(), "myapp", &mut io::stdout()); +/// } +/// +/// // normal logic continues... +/// } +/// +/// ``` +/// +/// Usage: +/// +/// ```console +/// $ myapp generate-bash-completions > /usr/share/bash-completion/completions/myapp.bash +/// ``` +pub fn generate(gen: G, cmd: &mut Command, bin_name: S, buf: &mut dyn Write) +where + G: Generator, + S: Into, +{ + cmd.set_bin_name(bin_name); + _generate::(gen, cmd, buf) +} + +fn _generate(gen: G, cmd: &mut Command, buf: &mut dyn Write) { + cmd.build(); + gen.generate(cmd, buf) +} diff --git a/src/generator/utils.rs b/src/generator/utils.rs new file mode 100644 index 0000000..ca76d18 --- /dev/null +++ b/src/generator/utils.rs @@ -0,0 +1,278 @@ +//! Helpers for writing generators + +use clap::{Arg, Command}; + +/// Gets all subcommands including child subcommands in the form of `("name", "bin_name")`. +/// +/// Subcommand `rustup toolchain install` would be converted to +/// `("install", "rustup toolchain install")`. +pub fn all_subcommands(cmd: &Command) -> Vec<(String, String)> { + let mut subcmds: Vec<_> = subcommands(cmd); + + for sc_v in cmd.get_subcommands().map(all_subcommands) { + subcmds.extend(sc_v); + } + + subcmds +} + +/// Finds the subcommand [`clap::Command`] from the given [`clap::Command`] with the given path. +/// +/// **NOTE:** `path` should not contain the root `bin_name`. +pub fn find_subcommand_with_path<'cmd>(p: &'cmd Command, path: Vec<&str>) -> &'cmd Command { + let mut cmd = p; + + for sc in path { + cmd = cmd.find_subcommand(sc).unwrap(); + } + + cmd +} + +/// Gets subcommands of [`clap::Command`] in the form of `("name", "bin_name")`. +/// +/// Subcommand `rustup toolchain install` would be converted to +/// `("install", "rustup toolchain install")`. +pub fn subcommands(p: &Command) -> Vec<(String, String)> { + debug!("subcommands: name={}", p.get_name()); + debug!("subcommands: Has subcommands...{:?}", p.has_subcommands()); + + let mut subcmds = vec![]; + + for sc in p.get_subcommands() { + let sc_bin_name = sc.get_bin_name().unwrap(); + + debug!( + "subcommands:iter: name={}, bin_name={}", + sc.get_name(), + sc_bin_name + ); + + subcmds.push((sc.get_name().to_string(), sc_bin_name.to_string())); + } + + subcmds +} + +/// Gets all the short options, their visible aliases and flags of a [`clap::Command`]. +/// Includes `h` and `V` depending on the [`clap::Command`] settings. +pub fn shorts_and_visible_aliases(p: &Command) -> Vec { + debug!("shorts: name={}", p.get_name()); + + p.get_arguments() + .filter_map(|a| { + if !a.is_positional() { + if a.get_visible_short_aliases().is_some() && a.get_short().is_some() { + let mut shorts_and_visible_aliases = a.get_visible_short_aliases().unwrap(); + shorts_and_visible_aliases.push(a.get_short().unwrap()); + Some(shorts_and_visible_aliases) + } else if a.get_visible_short_aliases().is_none() && a.get_short().is_some() { + Some(vec![a.get_short().unwrap()]) + } else { + None + } + } else { + None + } + }) + .flatten() + .collect() +} + +/// Gets all the long options, their visible aliases and flags of a [`clap::Command`]. +/// Includes `help` and `version` depending on the [`clap::Command`] settings. +pub fn longs_and_visible_aliases(p: &Command) -> Vec { + debug!("longs: name={}", p.get_name()); + + p.get_arguments() + .filter_map(|a| { + if !a.is_positional() { + if a.get_visible_aliases().is_some() && a.get_long().is_some() { + let mut visible_aliases: Vec<_> = a + .get_visible_aliases() + .unwrap() + .into_iter() + .map(|s| s.to_string()) + .collect(); + visible_aliases.push(a.get_long().unwrap().to_string()); + Some(visible_aliases) + } else if a.get_visible_aliases().is_none() && a.get_long().is_some() { + Some(vec![a.get_long().unwrap().to_string()]) + } else { + None + } + } else { + None + } + }) + .flatten() + .collect() +} + +/// Gets all the flags of a [`clap::Command`](Command). +/// Includes `help` and `version` depending on the [`clap::Command`] settings. +pub fn flags(p: &Command) -> Vec { + debug!("flags: name={}", p.get_name()); + p.get_arguments() + .filter(|a| !a.get_num_args().expect("built").takes_values() && !a.is_positional()) + .cloned() + .collect() +} + +/// Get the possible values for completion +pub fn possible_values(a: &Arg) -> Option> { + if !a.get_num_args().expect("built").takes_values() { + None + } else { + a.get_value_parser() + .possible_values() + .map(|pvs| pvs.collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Arg; + use clap::ArgAction; + + fn common_app() -> Command { + Command::new("myapp") + .subcommand( + Command::new("test").subcommand(Command::new("config")).arg( + Arg::new("file") + .short('f') + .short_alias('c') + .visible_short_alias('p') + .long("file") + .action(ArgAction::SetTrue) + .visible_alias("path"), + ), + ) + .subcommand(Command::new("hello")) + .bin_name("my-cmd") + } + + fn built() -> Command { + let mut cmd = common_app(); + + cmd.build(); + cmd + } + + fn built_with_version() -> Command { + let mut cmd = common_app().version("3.0"); + + cmd.build(); + cmd + } + + #[test] + fn test_subcommands() { + let cmd = built_with_version(); + + assert_eq!( + subcommands(&cmd), + vec![ + ("test".to_string(), "my-cmd test".to_string()), + ("hello".to_string(), "my-cmd hello".to_string()), + ("help".to_string(), "my-cmd help".to_string()), + ] + ); + } + + #[test] + fn test_all_subcommands() { + let cmd = built_with_version(); + + assert_eq!( + all_subcommands(&cmd), + vec![ + ("test".to_string(), "my-cmd test".to_string()), + ("hello".to_string(), "my-cmd hello".to_string()), + ("help".to_string(), "my-cmd help".to_string()), + ("config".to_string(), "my-cmd test config".to_string()), + ("help".to_string(), "my-cmd test help".to_string()), + ("config".to_string(), "my-cmd test help config".to_string()), + ("help".to_string(), "my-cmd test help help".to_string()), + ("test".to_string(), "my-cmd help test".to_string()), + ("hello".to_string(), "my-cmd help hello".to_string()), + ("help".to_string(), "my-cmd help help".to_string()), + ("config".to_string(), "my-cmd help test config".to_string()), + ] + ); + } + + #[test] + fn test_find_subcommand_with_path() { + let cmd = built_with_version(); + let sc_app = find_subcommand_with_path(&cmd, "test config".split(' ').collect()); + + assert_eq!(sc_app.get_name(), "config"); + } + + #[test] + fn test_flags() { + let cmd = built_with_version(); + let actual_flags = flags(&cmd); + + assert_eq!(actual_flags.len(), 2); + assert_eq!(actual_flags[0].get_long(), Some("help")); + assert_eq!(actual_flags[1].get_long(), Some("version")); + + let sc_flags = flags(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_flags.len(), 2); + assert_eq!(sc_flags[0].get_long(), Some("file")); + assert_eq!(sc_flags[1].get_long(), Some("help")); + } + + #[test] + fn test_flag_subcommand() { + let cmd = built(); + let actual_flags = flags(&cmd); + + assert_eq!(actual_flags.len(), 1); + assert_eq!(actual_flags[0].get_long(), Some("help")); + + let sc_flags = flags(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_flags.len(), 2); + assert_eq!(sc_flags[0].get_long(), Some("file")); + assert_eq!(sc_flags[1].get_long(), Some("help")); + } + + #[test] + fn test_shorts() { + let cmd = built_with_version(); + let shorts = shorts_and_visible_aliases(&cmd); + + assert_eq!(shorts.len(), 2); + assert_eq!(shorts[0], 'h'); + assert_eq!(shorts[1], 'V'); + + let sc_shorts = shorts_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_shorts.len(), 3); + assert_eq!(sc_shorts[0], 'p'); + assert_eq!(sc_shorts[1], 'f'); + assert_eq!(sc_shorts[2], 'h'); + } + + #[test] + fn test_longs() { + let cmd = built_with_version(); + let longs = longs_and_visible_aliases(&cmd); + + assert_eq!(longs.len(), 2); + assert_eq!(longs[0], "help"); + assert_eq!(longs[1], "version"); + + let sc_longs = longs_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"])); + + assert_eq!(sc_longs.len(), 3); + assert_eq!(sc_longs[0], "path"); + assert_eq!(sc_longs[1], "file"); + assert_eq!(sc_longs[2], "help"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a442882 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,74 @@ +// Copyright ⓒ 2015-2018 Kevin B. Knapp +// +// `clap_complete` is distributed under the terms of both the MIT license and the Apache License +// (Version 2.0). +// See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files in this repository +// for more information. + +#![doc(html_logo_url = "https://raw.githubusercontent.com/clap-rs/clap/master/assets/clap.png")] +#![doc = include_str!("../README.md")] +#![warn(missing_docs, trivial_casts, unused_allocation, trivial_numeric_casts)] +#![forbid(unsafe_code)] +#![allow(clippy::needless_doctest_main)] + +//! ## Quick Start +//! +//! - For generating at compile-time, see [`generate_to`] +//! - For generating at runtime, see [`generate`] +//! +//! [`Shell`] is a convenience `enum` for an argument value type that implements `Generator` +//! for each natively-supported shell type. +//! +//! ## Example +//! +//! ```rust,no_run +//! use clap::{Command, Arg, ValueHint, value_parser, ArgAction}; +//! use clap_complete::{generate, Generator, Shell}; +//! use std::io; +//! +//! fn build_cli() -> Command { +//! Command::new("example") +//! .arg(Arg::new("file") +//! .help("some input file") +//! .value_hint(ValueHint::AnyPath), +//! ) +//! .arg( +//! Arg::new("generator") +//! .long("generate") +//! .action(ArgAction::Set) +//! .value_parser(value_parser!(Shell)), +//! ) +//! } +//! +//! fn print_completions(gen: G, cmd: &mut Command) { +//! generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); +//! } +//! +//! fn main() { +//! let matches = build_cli().get_matches(); +//! +//! if let Some(generator) = matches.get_one::("generator").copied() { +//! let mut cmd = build_cli(); +//! eprintln!("Generating completion file for {generator}..."); +//! print_completions(generator, &mut cmd); +//! } +//! } +//! ``` + +const INTERNAL_ERROR_MSG: &str = "Fatal internal error. Please consider filing a bug \ + report at https://github.com/clap-rs/clap/issues"; + +#[macro_use] +#[allow(missing_docs)] +mod macros; + +pub mod generator; +pub mod shells; + +pub use generator::generate; +pub use generator::generate_to; +pub use generator::Generator; +pub use shells::Shell; + +#[cfg(feature = "unstable-dynamic")] +pub mod dynamic; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..bc69794 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,21 @@ +macro_rules! w { + ($buf:expr, $to_w:expr) => { + match $buf.write_all($to_w) { + Ok(..) => (), + Err(..) => panic!("Failed to write to generated file"), + } + }; +} + +#[cfg(feature = "debug")] +macro_rules! debug { + ($($arg:tt)*) => { + eprint!("[{:>w$}] \t", module_path!(), w = 28); + eprintln!($($arg)*) + } +} + +#[cfg(not(feature = "debug"))] +macro_rules! debug { + ($($arg:tt)*) => {}; +} diff --git a/src/shells/bash.rs b/src/shells/bash.rs new file mode 100644 index 0000000..2a97e1d --- /dev/null +++ b/src/shells/bash.rs @@ -0,0 +1,243 @@ +use std::{fmt::Write as _, io::Write}; + +use clap::*; + +use crate::generator::{utils, Generator}; + +/// Generate bash completion file +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Bash; + +impl Generator for Bash { + fn file_name(&self, name: &str) -> String { + format!("{name}.bash") + } + + fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + let bin_name = cmd + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + w!( + buf, + format!( + "_{name}() {{ + local i cur prev opts cmd + COMPREPLY=() + cur=\"${{COMP_WORDS[COMP_CWORD]}}\" + prev=\"${{COMP_WORDS[COMP_CWORD-1]}}\" + cmd=\"\" + opts=\"\" + + for i in ${{COMP_WORDS[@]}} + do + case \"${{cmd}},${{i}}\" in + \",$1\") + cmd=\"{cmd}\" + ;;{subcmds} + *) + ;; + esac + done + + case \"${{cmd}}\" in + {cmd}) + opts=\"{name_opts}\" + if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + fi + case \"${{prev}}\" in{name_opts_details} + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + ;;{subcmd_details} + esac +}} + +complete -F _{name} -o nosort -o bashdefault -o default {name} +", + name = bin_name, + cmd = bin_name.replace('-', "__"), + name_opts = all_options_for_path(cmd, bin_name), + name_opts_details = option_details_for_path(cmd, bin_name), + subcmds = all_subcommands(cmd), + subcmd_details = subcommand_details(cmd) + ) + .as_bytes() + ); + } +} + +fn all_subcommands(cmd: &Command) -> String { + debug!("all_subcommands"); + + fn add_command( + parent_fn_name: &str, + cmd: &Command, + subcmds: &mut Vec<(String, String, String)>, + ) { + let fn_name = format!( + "{parent_fn_name}__{cmd_name}", + parent_fn_name = parent_fn_name, + cmd_name = cmd.get_name().to_string().replace('-', "__") + ); + subcmds.push(( + parent_fn_name.to_string(), + cmd.get_name().to_string(), + fn_name.clone(), + )); + for alias in cmd.get_visible_aliases() { + subcmds.push(( + parent_fn_name.to_string(), + alias.to_string(), + fn_name.clone(), + )); + } + for subcmd in cmd.get_subcommands() { + add_command(&fn_name, subcmd, subcmds); + } + } + let mut subcmds = vec![]; + let fn_name = cmd.get_name().replace('-', "__"); + for subcmd in cmd.get_subcommands() { + add_command(&fn_name, subcmd, &mut subcmds); + } + subcmds.sort(); + + let mut cases = vec![String::new()]; + for (parent_fn_name, name, fn_name) in subcmds { + cases.push(format!( + "{parent_fn_name},{name}) + cmd=\"{fn_name}\" + ;;", + )); + } + + cases.join("\n ") +} + +fn subcommand_details(cmd: &Command) -> String { + debug!("subcommand_details"); + + let mut subcmd_dets = vec![String::new()]; + let mut scs = utils::all_subcommands(cmd) + .iter() + .map(|x| x.1.replace(' ', "__")) + .collect::>(); + + scs.sort(); + + subcmd_dets.extend(scs.iter().map(|sc| { + format!( + "{subcmd}) + opts=\"{sc_opts}\" + if [[ ${{cur}} == -* || ${{COMP_CWORD}} -eq {level} ]] ; then + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + fi + case \"${{prev}}\" in{opts_details} + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") ) + return 0 + ;;", + subcmd = sc.replace('-', "__"), + sc_opts = all_options_for_path(cmd, sc), + level = sc.split("__").map(|_| 1).sum::(), + opts_details = option_details_for_path(cmd, sc) + ) + })); + + subcmd_dets.join("\n ") +} + +fn option_details_for_path(cmd: &Command, path: &str) -> String { + debug!("option_details_for_path: path={path}"); + + let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect()); + let mut opts = vec![String::new()]; + + for o in p.get_opts() { + if let Some(longs) = o.get_long_and_visible_aliases() { + opts.extend(longs.iter().map(|long| { + format!( + "--{}) + COMPREPLY=({}) + return 0 + ;;", + long, + vals_for(o) + ) + })); + } + + if let Some(shorts) = o.get_short_and_visible_aliases() { + opts.extend(shorts.iter().map(|short| { + format!( + "-{}) + COMPREPLY=({}) + return 0 + ;;", + short, + vals_for(o) + ) + })); + } + } + + opts.join("\n ") +} + +fn vals_for(o: &Arg) -> String { + debug!("vals_for: o={}", o.get_id()); + + if let Some(vals) = crate::generator::utils::possible_values(o) { + format!( + "$(compgen -W \"{}\" -- \"${{cur}}\")", + vals.iter() + .filter(|pv| !pv.is_hide_set()) + .map(|n| n.get_name()) + .collect::>() + .join(" ") + ) + } else if o.get_value_hint() == ValueHint::Other { + String::from("\"${cur}\"") + } else { + String::from("$(compgen -f \"${cur}\")") + } +} + +fn all_options_for_path(cmd: &Command, path: &str) -> String { + debug!("all_options_for_path: path={path}"); + + let p = utils::find_subcommand_with_path(cmd, path.split("__").skip(1).collect()); + + let mut opts = String::new(); + for short in utils::shorts_and_visible_aliases(p) { + write!(&mut opts, "-{short} ").unwrap(); + } + for long in utils::longs_and_visible_aliases(p) { + write!(&mut opts, "--{long} ").unwrap(); + } + for pos in p.get_positionals() { + if let Some(vals) = utils::possible_values(pos) { + for value in vals { + write!(&mut opts, "{} ", value.get_name()).unwrap(); + } + } else { + write!(&mut opts, "{pos} ").unwrap(); + } + } + for (sc, _) in utils::subcommands(p) { + write!(&mut opts, "{sc} ").unwrap(); + } + opts.pop(); + + opts +} diff --git a/src/shells/elvish.rs b/src/shells/elvish.rs new file mode 100644 index 0000000..48a0f85 --- /dev/null +++ b/src/shells/elvish.rs @@ -0,0 +1,136 @@ +use std::io::Write; + +use clap::builder::StyledStr; +use clap::*; + +use crate::generator::{utils, Generator}; +use crate::INTERNAL_ERROR_MSG; + +/// Generate elvish completion file +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Elvish; + +impl Generator for Elvish { + fn file_name(&self, name: &str) -> String { + format!("{name}.elv") + } + + fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + let bin_name = cmd + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + let subcommands_cases = generate_inner(cmd, ""); + + let result = format!( + r#" +use builtin; +use str; + +set edit:completion:arg-completer[{bin_name}] = {{|@words| + fn spaces {{|n| + builtin:repeat $n ' ' | str:join '' + }} + fn cand {{|text desc| + edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc + }} + var command = '{bin_name}' + for word $words[1..-1] {{ + if (str:has-prefix $word '-') {{ + break + }} + set command = $command';'$word + }} + var completions = [{subcommands_cases} + ] + $completions[$command] +}} +"#, + ); + + w!(buf, result.as_bytes()); + } +} + +// Escape string inside single quotes +fn escape_string(string: &str) -> String { + string.replace('\'', "''") +} + +fn get_tooltip(help: Option<&StyledStr>, data: T) -> String { + match help { + Some(help) => escape_string(&help.to_string()), + _ => data.to_string(), + } +} + +fn generate_inner(p: &Command, previous_command_name: &str) -> String { + debug!("generate_inner"); + + let command_name = if previous_command_name.is_empty() { + p.get_bin_name().expect(INTERNAL_ERROR_MSG).to_string() + } else { + format!("{};{}", previous_command_name, &p.get_name()) + }; + + let mut completions = String::new(); + let preamble = String::from("\n cand "); + + for option in p.get_opts() { + if let Some(shorts) = option.get_short_and_visible_aliases() { + let tooltip = get_tooltip(option.get_help(), shorts[0]); + for short in shorts { + completions.push_str(&preamble); + completions.push_str(format!("-{short} '{tooltip}'").as_str()); + } + } + + if let Some(longs) = option.get_long_and_visible_aliases() { + let tooltip = get_tooltip(option.get_help(), longs[0]); + for long in longs { + completions.push_str(&preamble); + completions.push_str(format!("--{long} '{tooltip}'").as_str()); + } + } + } + + for flag in utils::flags(p) { + if let Some(shorts) = flag.get_short_and_visible_aliases() { + let tooltip = get_tooltip(flag.get_help(), shorts[0]); + for short in shorts { + completions.push_str(&preamble); + completions.push_str(format!("-{short} '{tooltip}'").as_str()); + } + } + + if let Some(longs) = flag.get_long_and_visible_aliases() { + let tooltip = get_tooltip(flag.get_help(), longs[0]); + for long in longs { + completions.push_str(&preamble); + completions.push_str(format!("--{long} '{tooltip}'").as_str()); + } + } + } + + for subcommand in p.get_subcommands() { + let data = &subcommand.get_name(); + let tooltip = get_tooltip(subcommand.get_about(), data); + + completions.push_str(&preamble); + completions.push_str(format!("{data} '{tooltip}'").as_str()); + } + + let mut subcommands_cases = format!( + r" + &'{}'= {{{} + }}", + &command_name, completions + ); + + for subcommand in p.get_subcommands() { + let subcommand_subcommands_cases = generate_inner(subcommand, &command_name); + subcommands_cases.push_str(&subcommand_subcommands_cases); + } + + subcommands_cases +} diff --git a/src/shells/fish.rs b/src/shells/fish.rs new file mode 100644 index 0000000..7dae5b6 --- /dev/null +++ b/src/shells/fish.rs @@ -0,0 +1,201 @@ +use std::io::Write; + +use clap::*; + +use crate::generator::{utils, Generator}; + +/// Generate fish completion file +/// +/// Note: The fish generator currently only supports named options (-o/--option), not positional arguments. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Fish; + +impl Generator for Fish { + fn file_name(&self, name: &str) -> String { + format!("{name}.fish") + } + + fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + let bin_name = cmd + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + let mut buffer = String::new(); + gen_fish_inner(bin_name, &[], cmd, &mut buffer); + w!(buf, buffer.as_bytes()); + } +} + +// Escape string inside single quotes +fn escape_string(string: &str, escape_comma: bool) -> String { + let string = string.replace('\\', "\\\\").replace('\'', "\\'"); + if escape_comma { + string.replace(',', "\\,") + } else { + string + } +} + +fn gen_fish_inner( + root_command: &str, + parent_commands: &[&str], + cmd: &Command, + buffer: &mut String, +) { + debug!("gen_fish_inner"); + // example : + // + // complete + // -c {command} + // -d "{description}" + // -s {short} + // -l {long} + // -a "{possible_arguments}" + // -r # if require parameter + // -f # don't use file completion + // -n "__fish_use_subcommand" # complete for command "myprog" + // -n "__fish_seen_subcommand_from subcmd1" # complete for command "myprog subcmd1" + + let mut basic_template = format!("complete -c {root_command}"); + + if parent_commands.is_empty() { + if cmd.has_subcommands() { + basic_template.push_str(" -n \"__fish_use_subcommand\""); + } + } else { + basic_template.push_str( + format!( + " -n \"{}\"", + parent_commands + .iter() + .map(|command| format!("__fish_seen_subcommand_from {command}")) + .chain( + cmd.get_subcommands() + .map(|command| format!("not __fish_seen_subcommand_from {command}")) + ) + .collect::>() + .join("; and ") + ) + .as_str(), + ); + } + + debug!("gen_fish_inner: parent_commands={parent_commands:?}"); + + for option in cmd.get_opts() { + let mut template = basic_template.clone(); + + if let Some(shorts) = option.get_short_and_visible_aliases() { + for short in shorts { + template.push_str(format!(" -s {short}").as_str()); + } + } + + if let Some(longs) = option.get_long_and_visible_aliases() { + for long in longs { + template.push_str(format!(" -l {}", escape_string(long, false)).as_str()); + } + } + + if let Some(data) = option.get_help() { + template + .push_str(format!(" -d '{}'", escape_string(&data.to_string(), false)).as_str()); + } + + template.push_str(value_completion(option).as_str()); + + buffer.push_str(template.as_str()); + buffer.push('\n'); + } + + for flag in utils::flags(cmd) { + let mut template = basic_template.clone(); + + if let Some(shorts) = flag.get_short_and_visible_aliases() { + for short in shorts { + template.push_str(format!(" -s {short}").as_str()); + } + } + + if let Some(longs) = flag.get_long_and_visible_aliases() { + for long in longs { + template.push_str(format!(" -l {}", escape_string(long, false)).as_str()); + } + } + + if let Some(data) = flag.get_help() { + template + .push_str(format!(" -d '{}'", escape_string(&data.to_string(), false)).as_str()); + } + + buffer.push_str(template.as_str()); + buffer.push('\n'); + } + + for subcommand in cmd.get_subcommands() { + let mut template = basic_template.clone(); + + template.push_str(" -f"); + template.push_str(format!(" -a \"{}\"", &subcommand.get_name()).as_str()); + + if let Some(data) = subcommand.get_about() { + template.push_str(format!(" -d '{}'", escape_string(&data.to_string(), false)).as_str()) + } + + buffer.push_str(template.as_str()); + buffer.push('\n'); + } + + // generate options of subcommands + for subcommand in cmd.get_subcommands() { + let mut parent_commands: Vec<_> = parent_commands.into(); + parent_commands.push(subcommand.get_name()); + gen_fish_inner(root_command, &parent_commands, subcommand, buffer); + } +} + +fn value_completion(option: &Arg) -> String { + if !option.get_num_args().expect("built").takes_values() { + return "".to_string(); + } + + if let Some(data) = crate::generator::utils::possible_values(option) { + // We return the possible values with their own empty description e.g. {a\t,b\t} + // this makes sure that a and b don't get the description of the option or argument + format!( + " -r -f -a \"{{{}}}\"", + data.iter() + .filter_map(|value| if value.is_hide_set() { + None + } else { + // The help text after \t is wrapped in '' to make sure that the it is taken literally + // and there is no command substitution or variable expansion resulting in unexpected errors + Some(format!( + "{}\t'{}'", + escape_string(value.get_name(), true).as_str(), + escape_string(&value.get_help().unwrap_or_default().to_string(), false) + )) + }) + .collect::>() + .join(",") + ) + } else { + // NB! If you change this, please also update the table in `ValueHint` documentation. + match option.get_value_hint() { + ValueHint::Unknown => " -r", + // fish has no built-in support to distinguish these + ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => " -r -F", + ValueHint::DirPath => " -r -f -a \"(__fish_complete_directories)\"", + // It seems fish has no built-in support for completing command + arguments as + // single string (CommandString). Complete just the command name. + ValueHint::CommandString | ValueHint::CommandName => { + " -r -f -a \"(__fish_complete_command)\"" + } + ValueHint::Username => " -r -f -a \"(__fish_complete_users)\"", + ValueHint::Hostname => " -r -f -a \"(__fish_print_hostnames)\"", + // Disable completion for others + _ => " -r -f", + } + .to_string() + } +} diff --git a/src/shells/mod.rs b/src/shells/mod.rs new file mode 100644 index 0000000..a08aa87 --- /dev/null +++ b/src/shells/mod.rs @@ -0,0 +1,15 @@ +//! Shell-specific generators + +mod bash; +mod elvish; +mod fish; +mod powershell; +mod shell; +mod zsh; + +pub use bash::Bash; +pub use elvish::Elvish; +pub use fish::Fish; +pub use powershell::PowerShell; +pub use shell::Shell; +pub use zsh::Zsh; diff --git a/src/shells/powershell.rs b/src/shells/powershell.rs new file mode 100644 index 0000000..6b09b2e --- /dev/null +++ b/src/shells/powershell.rs @@ -0,0 +1,142 @@ +use std::io::Write; + +use clap::builder::StyledStr; +use clap::*; + +use crate::generator::{utils, Generator}; +use crate::INTERNAL_ERROR_MSG; + +/// Generate powershell completion file +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct PowerShell; + +impl Generator for PowerShell { + fn file_name(&self, name: &str) -> String { + format!("_{name}.ps1") + } + + fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + let bin_name = cmd + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + let subcommands_cases = generate_inner(cmd, ""); + + let result = format!( + r#" +using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +Register-ArgumentCompleter -Native -CommandName '{bin_name}' -ScriptBlock {{ + param($wordToComplete, $commandAst, $cursorPosition) + + $commandElements = $commandAst.CommandElements + $command = @( + '{bin_name}' + for ($i = 1; $i -lt $commandElements.Count; $i++) {{ + $element = $commandElements[$i] + if ($element -isnot [StringConstantExpressionAst] -or + $element.StringConstantType -ne [StringConstantType]::BareWord -or + $element.Value.StartsWith('-') -or + $element.Value -eq $wordToComplete) {{ + break + }} + $element.Value + }}) -join ';' + + $completions = @(switch ($command) {{{subcommands_cases} + }}) + + $completions.Where{{ $_.CompletionText -like "$wordToComplete*" }} | + Sort-Object -Property ListItemText +}} +"# + ); + + w!(buf, result.as_bytes()); + } +} + +// Escape string inside single quotes +fn escape_string(string: &str) -> String { + string.replace('\'', "''") +} + +fn get_tooltip(help: Option<&StyledStr>, data: T) -> String { + match help { + Some(help) => escape_string(&help.to_string()), + _ => data.to_string(), + } +} + +fn generate_inner(p: &Command, previous_command_name: &str) -> String { + debug!("generate_inner"); + + let command_name = if previous_command_name.is_empty() { + p.get_bin_name().expect(INTERNAL_ERROR_MSG).to_string() + } else { + format!("{};{}", previous_command_name, &p.get_name()) + }; + + let mut completions = String::new(); + let preamble = String::from("\n [CompletionResult]::new("); + + for option in p.get_opts() { + generate_aliases(&mut completions, &preamble, option); + } + + for flag in utils::flags(p) { + generate_aliases(&mut completions, &preamble, &flag); + } + + for subcommand in p.get_subcommands() { + let data = &subcommand.get_name(); + let tooltip = get_tooltip(subcommand.get_about(), data); + + completions.push_str(&preamble); + completions.push_str( + format!("'{data}', '{data}', [CompletionResultType]::ParameterValue, '{tooltip}')") + .as_str(), + ); + } + + let mut subcommands_cases = format!( + r" + '{}' {{{} + break + }}", + &command_name, completions + ); + + for subcommand in p.get_subcommands() { + let subcommand_subcommands_cases = generate_inner(subcommand, &command_name); + subcommands_cases.push_str(&subcommand_subcommands_cases); + } + + subcommands_cases +} + +fn generate_aliases(completions: &mut String, preamble: &String, arg: &Arg) { + use std::fmt::Write as _; + + if let Some(aliases) = arg.get_short_and_visible_aliases() { + let tooltip = get_tooltip(arg.get_help(), aliases[0]); + for alias in aliases { + let _ = write!( + completions, + "{preamble}'-{alias}', '{alias}{}', [CompletionResultType]::ParameterName, '{tooltip}')", + // make PowerShell realize there is a difference between `-s` and `-S` + if alias.is_uppercase() { " " } else { "" }, + ); + } + } + if let Some(aliases) = arg.get_long_and_visible_aliases() { + let tooltip = get_tooltip(arg.get_help(), aliases[0]); + for alias in aliases { + let _ = write!( + completions, + "{preamble}'--{alias}', '{alias}', [CompletionResultType]::ParameterName, '{tooltip}')" + ); + } + } +} diff --git a/src/shells/shell.rs b/src/shells/shell.rs new file mode 100644 index 0000000..52cb2e9 --- /dev/null +++ b/src/shells/shell.rs @@ -0,0 +1,155 @@ +use std::fmt::Display; +use std::path::Path; +use std::str::FromStr; + +use clap::builder::PossibleValue; +use clap::ValueEnum; + +use crate::shells; +use crate::Generator; + +/// Shell with auto-generated completion script available. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] +pub enum Shell { + /// Bourne Again SHell (bash) + Bash, + /// Elvish shell + Elvish, + /// Friendly Interactive SHell (fish) + Fish, + /// PowerShell + PowerShell, + /// Z SHell (zsh) + Zsh, +} + +impl Display for Shell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.to_possible_value() + .expect("no values are skipped") + .get_name() + .fmt(f) + } +} + +impl FromStr for Shell { + type Err = String; + + fn from_str(s: &str) -> Result { + for variant in Self::value_variants() { + if variant.to_possible_value().unwrap().matches(s, false) { + return Ok(*variant); + } + } + Err(format!("invalid variant: {s}")) + } +} + +// Hand-rolled so it can work even when `derive` feature is disabled +impl ValueEnum for Shell { + fn value_variants<'a>() -> &'a [Self] { + &[ + Shell::Bash, + Shell::Elvish, + Shell::Fish, + Shell::PowerShell, + Shell::Zsh, + ] + } + + fn to_possible_value<'a>(&self) -> Option { + Some(match self { + Shell::Bash => PossibleValue::new("bash"), + Shell::Elvish => PossibleValue::new("elvish"), + Shell::Fish => PossibleValue::new("fish"), + Shell::PowerShell => PossibleValue::new("powershell"), + Shell::Zsh => PossibleValue::new("zsh"), + }) + } +} + +impl Generator for Shell { + fn file_name(&self, name: &str) -> String { + match self { + Shell::Bash => shells::Bash.file_name(name), + Shell::Elvish => shells::Elvish.file_name(name), + Shell::Fish => shells::Fish.file_name(name), + Shell::PowerShell => shells::PowerShell.file_name(name), + Shell::Zsh => shells::Zsh.file_name(name), + } + } + + fn generate(&self, cmd: &clap::Command, buf: &mut dyn std::io::Write) { + match self { + Shell::Bash => shells::Bash.generate(cmd, buf), + Shell::Elvish => shells::Elvish.generate(cmd, buf), + Shell::Fish => shells::Fish.generate(cmd, buf), + Shell::PowerShell => shells::PowerShell.generate(cmd, buf), + Shell::Zsh => shells::Zsh.generate(cmd, buf), + } + } +} + +impl Shell { + /// Parse a shell from a path to the executable for the shell + /// + /// # Examples + /// + /// ``` + /// use clap_complete::shells::Shell; + /// + /// assert_eq!(Shell::from_shell_path("/bin/bash"), Some(Shell::Bash)); + /// assert_eq!(Shell::from_shell_path("/usr/bin/zsh"), Some(Shell::Zsh)); + /// assert_eq!(Shell::from_shell_path("/opt/my_custom_shell"), None); + /// ``` + pub fn from_shell_path>(path: P) -> Option { + parse_shell_from_path(path.as_ref()) + } + + /// Determine the user's current shell from the environment + /// + /// This will read the SHELL environment variable and try to determine which shell is in use + /// from that. + /// + /// If SHELL is not set, then on windows, it will default to powershell, and on + /// other OSes it will return `None`. + /// + /// If SHELL is set, but contains a value that doesn't correspond to one of the supported shell + /// types, then return `None`. + /// + /// # Example: + /// + /// ```no_run + /// # use clap::Command; + /// use clap_complete::{generate, shells::Shell}; + /// # fn build_cli() -> Command { + /// # Command::new("compl") + /// # } + /// let mut cmd = build_cli(); + /// generate(Shell::from_env().unwrap_or(Shell::Bash), &mut cmd, "myapp", &mut std::io::stdout()); + /// ``` + pub fn from_env() -> Option { + if let Some(env_shell) = std::env::var_os("SHELL") { + Shell::from_shell_path(env_shell) + } else if cfg!(windows) { + Some(Shell::PowerShell) + } else { + None + } + } +} + +// use a separate function to avoid having to monomorphize the entire function due +// to from_shell_path being generic +fn parse_shell_from_path(path: &Path) -> Option { + let name = path.file_stem()?.to_str()?; + match name { + "bash" => Some(Shell::Bash), + "zsh" => Some(Shell::Zsh), + "fish" => Some(Shell::Fish), + "elvish" => Some(Shell::Elvish), + "powershell" | "powershell_ise" => Some(Shell::PowerShell), + _ => None, + } +} diff --git a/src/shells/zsh.rs b/src/shells/zsh.rs new file mode 100644 index 0000000..65d7af6 --- /dev/null +++ b/src/shells/zsh.rs @@ -0,0 +1,691 @@ +use std::io::Write; + +use clap::*; + +use crate::generator::{utils, Generator}; +use crate::INTERNAL_ERROR_MSG; + +/// Generate zsh completion file +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Zsh; + +impl Generator for Zsh { + fn file_name(&self, name: &str) -> String { + format!("_{name}") + } + + fn generate(&self, cmd: &Command, buf: &mut dyn Write) { + let bin_name = cmd + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + w!( + buf, + format!( + "#compdef {name} + +autoload -U is-at-least + +_{name}() {{ + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext=\"$curcontext\" state line + {initial_args}{subcommands} +}} + +{subcommand_details} + +if [ \"$funcstack[1]\" = \"_{name}\" ]; then + _{name} \"$@\" +else + compdef _{name} {name} +fi +", + name = bin_name, + initial_args = get_args_of(cmd, None), + subcommands = get_subcommands_of(cmd), + subcommand_details = subcommand_details(cmd) + ) + .as_bytes() + ); + } +} + +// Displays the commands of a subcommand +// (( $+functions[_[bin_name_underscore]_commands] )) || +// _[bin_name_underscore]_commands() { +// local commands; commands=( +// '[arg_name]:[arg_help]' +// ) +// _describe -t commands '[bin_name] commands' commands "$@" +// +// Where the following variables are present: +// [bin_name_underscore]: The full space delineated bin_name, where spaces have been replaced by +// underscore characters +// [arg_name]: The name of the subcommand +// [arg_help]: The help message of the subcommand +// [bin_name]: The full space delineated bin_name +// +// Here's a snippet from rustup: +// +// (( $+functions[_rustup_commands] )) || +// _rustup_commands() { +// local commands; commands=( +// 'show:Show the active and installed toolchains' +// 'update:Update Rust toolchains' +// # ... snip for brevity +// 'help:Print this message or the help of the given subcommand(s)' +// ) +// _describe -t commands 'rustup commands' commands "$@" +// +fn subcommand_details(p: &Command) -> String { + debug!("subcommand_details"); + + let bin_name = p + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + let mut ret = vec![]; + + // First we do ourself + let parent_text = format!( + "\ +(( $+functions[_{bin_name_underscore}_commands] )) || +_{bin_name_underscore}_commands() {{ + local commands; commands=({subcommands_and_args}) + _describe -t commands '{bin_name} commands' commands \"$@\" +}}", + bin_name_underscore = bin_name.replace(' ', "__"), + bin_name = bin_name, + subcommands_and_args = subcommands_of(p) + ); + ret.push(parent_text); + + // Next we start looping through all the children, grandchildren, etc. + let mut all_subcommands = utils::all_subcommands(p); + + all_subcommands.sort(); + all_subcommands.dedup(); + + for (_, ref bin_name) in &all_subcommands { + debug!("subcommand_details:iter: bin_name={bin_name}"); + + ret.push(format!( + "\ +(( $+functions[_{bin_name_underscore}_commands] )) || +_{bin_name_underscore}_commands() {{ + local commands; commands=({subcommands_and_args}) + _describe -t commands '{bin_name} commands' commands \"$@\" +}}", + bin_name_underscore = bin_name.replace(' ', "__"), + bin_name = bin_name, + subcommands_and_args = + subcommands_of(parser_of(p, bin_name).expect(INTERNAL_ERROR_MSG)) + )); + } + + ret.join("\n") +} + +// Generates subcommand completions in form of +// +// '[arg_name]:[arg_help]' +// +// Where: +// [arg_name]: the subcommand's name +// [arg_help]: the help message of the subcommand +// +// A snippet from rustup: +// 'show:Show the active and installed toolchains' +// 'update:Update Rust toolchains' +fn subcommands_of(p: &Command) -> String { + debug!("subcommands_of"); + + let mut segments = vec![]; + + fn add_subcommands(subcommand: &Command, name: &str, ret: &mut Vec) { + debug!("add_subcommands"); + + let text = format!( + "'{name}:{help}' \\", + name = name, + help = escape_help(&subcommand.get_about().unwrap_or_default().to_string()) + ); + + ret.push(text); + } + + // The subcommands + for command in p.get_subcommands() { + debug!("subcommands_of:iter: subcommand={}", command.get_name()); + + add_subcommands(command, command.get_name(), &mut segments); + + for alias in command.get_visible_aliases() { + add_subcommands(command, alias, &mut segments); + } + } + + // Surround the text with newlines for proper formatting. + // We need this to prevent weirdly formatted `command=(\n \n)` sections. + // When there are no (sub-)commands. + if !segments.is_empty() { + segments.insert(0, "".to_string()); + segments.push(" ".to_string()); + } + + segments.join("\n") +} + +// Get's the subcommand section of a completion file +// This looks roughly like: +// +// case $state in +// ([bin_name]_args) +// curcontext=\"${curcontext%:*:*}:[name_hyphen]-command-$words[1]:\" +// case $line[1] in +// +// ([name]) +// _arguments -C -s -S \ +// [subcommand_args] +// && ret=0 +// +// [RECURSIVE_CALLS] +// +// ;;", +// +// [repeat] +// +// esac +// ;; +// esac", +// +// Where the following variables are present: +// [name] = The subcommand name in the form of "install" for "rustup toolchain install" +// [bin_name] = The full space delineated bin_name such as "rustup toolchain install" +// [name_hyphen] = The full space delineated bin_name, but replace spaces with hyphens +// [repeat] = From the same recursive calls, but for all subcommands +// [subcommand_args] = The same as zsh::get_args_of +fn get_subcommands_of(parent: &Command) -> String { + debug!( + "get_subcommands_of: Has subcommands...{:?}", + parent.has_subcommands() + ); + + if !parent.has_subcommands() { + return String::new(); + } + + let subcommand_names = utils::subcommands(parent); + let mut all_subcommands = vec![]; + + for (ref name, ref bin_name) in &subcommand_names { + debug!( + "get_subcommands_of:iter: parent={}, name={name}, bin_name={bin_name}", + parent.get_name(), + ); + let mut segments = vec![format!("({name})")]; + let subcommand_args = get_args_of( + parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG), + Some(parent), + ); + + if !subcommand_args.is_empty() { + segments.push(subcommand_args); + } + + // Get the help text of all child subcommands. + let children = get_subcommands_of(parser_of(parent, bin_name).expect(INTERNAL_ERROR_MSG)); + + if !children.is_empty() { + segments.push(children); + } + + segments.push(String::from(";;")); + all_subcommands.push(segments.join("\n")); + } + + let parent_bin_name = parent + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + + format!( + " + case $state in + ({name}) + words=($line[{pos}] \"${{words[@]}}\") + (( CURRENT += 1 )) + curcontext=\"${{curcontext%:*:*}}:{name_hyphen}-command-$line[{pos}]:\" + case $line[{pos}] in + {subcommands} + esac + ;; +esac", + name = parent.get_name(), + name_hyphen = parent_bin_name.replace(' ', "-"), + subcommands = all_subcommands.join("\n"), + pos = parent.get_positionals().count() + 1 + ) +} + +// Get the Command for a given subcommand tree. +// +// Given the bin_name "a b c" and the Command for "a" this returns the "c" Command. +// Given the bin_name "a b c" and the Command for "b" this returns the "c" Command. +fn parser_of<'cmd>(parent: &'cmd Command, bin_name: &str) -> Option<&'cmd Command> { + debug!("parser_of: p={}, bin_name={}", parent.get_name(), bin_name); + + if bin_name == parent.get_bin_name().unwrap_or_default() { + return Some(parent); + } + + for subcommand in parent.get_subcommands() { + if let Some(ret) = parser_of(subcommand, bin_name) { + return Some(ret); + } + } + + None +} + +// Writes out the args section, which ends up being the flags, opts and positionals, and a jump to +// another ZSH function if there are subcommands. +// The structure works like this: +// ([conflicting_args]) [multiple] arg [takes_value] [[help]] [: :(possible_values)] +// ^-- list '-v -h' ^--'*' ^--'+' ^-- list 'one two three' +// +// An example from the rustup command: +// +// _arguments -C -s -S \ +// '(-h --help --verbose)-v[Enable verbose output]' \ +// '(-V -v --version --verbose --help)-h[Print help information]' \ +// # ... snip for brevity +// ':: :_rustup_commands' \ # <-- displays subcommands +// '*::: :->rustup' \ # <-- displays subcommand args and child subcommands +// && ret=0 +// +// The args used for _arguments are as follows: +// -C: modify the $context internal variable +// -s: Allow stacking of short args (i.e. -a -b -c => -abc) +// -S: Do not complete anything after '--' and treat those as argument values +fn get_args_of(parent: &Command, p_global: Option<&Command>) -> String { + debug!("get_args_of"); + + let mut segments = vec![String::from("_arguments \"${_arguments_options[@]}\" \\")]; + let opts = write_opts_of(parent, p_global); + let flags = write_flags_of(parent, p_global); + let positionals = write_positionals_of(parent); + + if !opts.is_empty() { + segments.push(opts); + } + + if !flags.is_empty() { + segments.push(flags); + } + + if !positionals.is_empty() { + segments.push(positionals); + } + + if parent.has_subcommands() { + let parent_bin_name = parent + .get_bin_name() + .expect("crate::generate should have set the bin_name"); + let subcommand_bin_name = format!( + "\":: :_{name}_commands\" \\", + name = parent_bin_name.replace(' ', "__") + ); + segments.push(subcommand_bin_name); + + let subcommand_text = format!("\"*::: :->{name}\" \\", name = parent.get_name()); + segments.push(subcommand_text); + }; + + segments.push(String::from("&& ret=0")); + segments.join("\n") +} + +// Uses either `possible_vals` or `value_hint` to give hints about possible argument values +fn value_completion(arg: &Arg) -> Option { + if let Some(values) = crate::generator::utils::possible_values(arg) { + if values + .iter() + .any(|value| !value.is_hide_set() && value.get_help().is_some()) + { + Some(format!( + "(({}))", + values + .iter() + .filter_map(|value| { + if value.is_hide_set() { + None + } else { + Some(format!( + r#"{name}\:"{tooltip}""#, + name = escape_value(value.get_name()), + tooltip = + escape_help(&value.get_help().unwrap_or_default().to_string()), + )) + } + }) + .collect::>() + .join("\n") + )) + } else { + Some(format!( + "({})", + values + .iter() + .filter(|pv| !pv.is_hide_set()) + .map(|n| n.get_name()) + .collect::>() + .join(" ") + )) + } + } else { + // NB! If you change this, please also update the table in `ValueHint` documentation. + Some( + match arg.get_value_hint() { + ValueHint::Unknown => { + return None; + } + ValueHint::Other => "( )", + ValueHint::AnyPath => "_files", + ValueHint::FilePath => "_files", + ValueHint::DirPath => "_files -/", + ValueHint::ExecutablePath => "_absolute_command_paths", + ValueHint::CommandName => "_command_names -e", + ValueHint::CommandString => "_cmdstring", + ValueHint::CommandWithArguments => "_cmdambivalent", + ValueHint::Username => "_users", + ValueHint::Hostname => "_hosts", + ValueHint::Url => "_urls", + ValueHint::EmailAddress => "_email_addresses", + _ => { + return None; + } + } + .to_string(), + ) + } +} + +/// Escape help string inside single quotes and brackets +fn escape_help(string: &str) -> String { + string + .replace('\\', "\\\\") + .replace('\'', "'\\''") + .replace('[', "\\[") + .replace(']', "\\]") + .replace(':', "\\:") + .replace('$', "\\$") + .replace('`', "\\`") +} + +/// Escape value string inside single quotes and parentheses +fn escape_value(string: &str) -> String { + string + .replace('\\', "\\\\") + .replace('\'', "'\\''") + .replace('[', "\\[") + .replace(']', "\\]") + .replace(':', "\\:") + .replace('$', "\\$") + .replace('`', "\\`") + .replace('(', "\\(") + .replace(')', "\\)") + .replace(' ', "\\ ") +} + +fn write_opts_of(p: &Command, p_global: Option<&Command>) -> String { + debug!("write_opts_of"); + + let mut ret = vec![]; + + for o in p.get_opts() { + debug!("write_opts_of:iter: o={}", o.get_id()); + + let help = escape_help(&o.get_help().unwrap_or_default().to_string()); + let conflicts = arg_conflicts(p, o, p_global); + + let multiple = if let ArgAction::Count | ArgAction::Append = o.get_action() { + "*" + } else { + "" + }; + + let vn = match o.get_value_names() { + None => " ".to_string(), + Some(val) => val[0].to_string(), + }; + let vc = match value_completion(o) { + Some(val) => format!(":{vn}:{val}"), + None => format!(":{vn}: "), + }; + let vc = vc.repeat(o.get_num_args().expect("built").min_values()); + + if let Some(shorts) = o.get_short_and_visible_aliases() { + for short in shorts { + let s = format!("'{conflicts}{multiple}-{short}+[{help}]{vc}' \\"); + + debug!("write_opts_of:iter: Wrote...{}", &*s); + ret.push(s); + } + } + if let Some(longs) = o.get_long_and_visible_aliases() { + for long in longs { + let l = format!("'{conflicts}{multiple}--{long}=[{help}]{vc}' \\"); + + debug!("write_opts_of:iter: Wrote...{}", &*l); + ret.push(l); + } + } + } + + ret.join("\n") +} + +fn arg_conflicts(cmd: &Command, arg: &Arg, app_global: Option<&Command>) -> String { + fn push_conflicts(conflicts: &[&Arg], res: &mut Vec) { + for conflict in conflicts { + if let Some(s) = conflict.get_short() { + res.push(format!("-{s}")); + } + + if let Some(l) = conflict.get_long() { + res.push(format!("--{l}")); + } + } + } + + let mut res = vec![]; + match (app_global, arg.is_global_set()) { + (Some(x), true) => { + let conflicts = x.get_arg_conflicts_with(arg); + + if conflicts.is_empty() { + return String::new(); + } + + push_conflicts(&conflicts, &mut res); + } + (_, _) => { + let conflicts = cmd.get_arg_conflicts_with(arg); + + if conflicts.is_empty() { + return String::new(); + } + + push_conflicts(&conflicts, &mut res); + } + }; + + format!("({})", res.join(" ")) +} + +fn write_flags_of(p: &Command, p_global: Option<&Command>) -> String { + debug!("write_flags_of;"); + + let mut ret = vec![]; + + for f in utils::flags(p) { + debug!("write_flags_of:iter: f={}", f.get_id()); + + let help = escape_help(&f.get_help().unwrap_or_default().to_string()); + let conflicts = arg_conflicts(p, &f, p_global); + + let multiple = if let ArgAction::Count | ArgAction::Append = f.get_action() { + "*" + } else { + "" + }; + + if let Some(short) = f.get_short() { + let s = format!("'{conflicts}{multiple}-{short}[{help}]' \\"); + + debug!("write_flags_of:iter: Wrote...{}", &*s); + + ret.push(s); + + if let Some(short_aliases) = f.get_visible_short_aliases() { + for alias in short_aliases { + let s = format!("'{conflicts}{multiple}-{alias}[{help}]' \\",); + + debug!("write_flags_of:iter: Wrote...{}", &*s); + + ret.push(s); + } + } + } + + if let Some(long) = f.get_long() { + let l = format!("'{conflicts}{multiple}--{long}[{help}]' \\"); + + debug!("write_flags_of:iter: Wrote...{}", &*l); + + ret.push(l); + + if let Some(aliases) = f.get_visible_aliases() { + for alias in aliases { + let l = format!("'{conflicts}{multiple}--{alias}[{help}]' \\"); + + debug!("write_flags_of:iter: Wrote...{}", &*l); + + ret.push(l); + } + } + } + } + + ret.join("\n") +} + +fn write_positionals_of(p: &Command) -> String { + debug!("write_positionals_of;"); + + let mut ret = vec![]; + + // Completions for commands that end with two Vec arguments require special care. + // - You can have two Vec args separated with a custom value terminator. + // - You can have two Vec args with the second one set to last (raw sets last) + // which will require a '--' separator to be used before the second argument + // on the command-line. + // + // We use the '-S' _arguments option to disable completion after '--'. Thus, the + // completion for the second argument in scenario (B) does not need to be emitted + // because it is implicitly handled by the '-S' option. + // We only need to emit the first catch-all. + // + // Have we already emitted a catch-all multi-valued positional argument + // without a custom value terminator? + let mut catch_all_emitted = false; + + for arg in p.get_positionals() { + debug!("write_positionals_of:iter: arg={}", arg.get_id()); + + let num_args = arg.get_num_args().expect("built"); + let is_multi_valued = num_args.max_values() > 1; + + if catch_all_emitted && (arg.is_last_set() || is_multi_valued) { + // This is the final argument and it also takes multiple arguments. + // We've already emitted a catch-all positional argument so we don't need + // to emit anything for this argument because it is implicitly handled by + // the use of the '-S' _arguments option. + continue; + } + + let cardinality_value; + // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't + // emit one here. + let cardinality = if is_multi_valued && !p.has_subcommands() { + match arg.get_value_terminator() { + Some(terminator) => { + cardinality_value = format!("*{}:", escape_value(terminator)); + cardinality_value.as_str() + } + None => { + catch_all_emitted = true; + "*:" + } + } + } else if !arg.is_required_set() { + ":" + } else { + "" + }; + + let a = format!( + "'{cardinality}:{name}{help}:{value_completion}' \\", + cardinality = cardinality, + name = arg.get_id(), + help = arg + .get_help() + .map(|s| s.to_string()) + .map(|v| " -- ".to_owned() + &v) + .unwrap_or_else(|| "".to_owned()) + .replace('[', "\\[") + .replace(']', "\\]") + .replace('\'', "'\\''") + .replace(':', "\\:"), + value_completion = value_completion(arg).unwrap_or_default() + ); + + debug!("write_positionals_of:iter: Wrote...{a}"); + + ret.push(a); + } + + ret.join("\n") +} + +#[cfg(test)] +mod tests { + use crate::shells::zsh::{escape_help, escape_value}; + + #[test] + fn test_escape_value() { + let raw_string = "\\ [foo]() `bar https://$PATH"; + assert_eq!( + escape_value(raw_string), + "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH" + ) + } + + #[test] + fn test_escape_help() { + let raw_string = "\\ [foo]() `bar https://$PATH"; + assert_eq!( + escape_help(raw_string), + "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH" + ) + } +} -- cgit v1.2.3