Switch to generating completions at runtime (#393)

* Allow for completion and man page generation at runtime and support for Elvish and Nshull

* Refine runtime generation based on feedback provided
This commit is contained in:
Fotis Gimian 2025-01-06 23:24:36 +11:00 committed by GitHub
parent 35dfe49761
commit 196ca1f162
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 603 additions and 233 deletions

View File

@ -1,3 +1,8 @@
## Unreleased
### Features
- Add `--generate` option to generate the man page and shell completions at runtime
- Add support for Elvish and Nushell shell completions
## [0.23.1] - 2025-01-02 ## [0.23.1] - 2025-01-02
### Security fixes ### Security fixes
- Upgrade to ruzstd v0.7.3 to fix RUSTSEC-2024-0400, see #396 (@zuisong) - Upgrade to ruzstd v0.7.3 to fix RUSTSEC-2024-0400, see #396 (@zuisong)

11
Cargo.lock generated
View File

@ -289,6 +289,16 @@ dependencies = [
"clap", "clap",
] ]
[[package]]
name = "clap_complete_nushell"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "315902e790cc6e5ddd20cbd313c1d0d49db77f191e149f96397230fb82a17677"
dependencies = [
"clap",
"clap_complete",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.18" version = "4.5.18"
@ -2798,6 +2808,7 @@ dependencies = [
"chardetng", "chardetng",
"clap", "clap",
"clap_complete", "clap_complete",
"clap_complete_nushell",
"cookie_store 0.20.0", "cookie_store 0.20.0",
"digest_auth", "digest_auth",
"dirs", "dirs",

View File

@ -19,7 +19,8 @@ anyhow = "1.0.38"
brotli = { version = "3.3.0", default-features = false, features = ["std"] } brotli = { version = "3.3.0", default-features = false, features = ["std"] }
chardetng = "0.1.15" chardetng = "0.1.15"
clap = { version = "4.4", features = ["derive", "wrap_help", "string"] } clap = { version = "4.4", features = ["derive", "wrap_help", "string"] }
clap_complete = { version = "4.4", optional = true } clap_complete = "4.4"
clap_complete_nushell = "4.4"
cookie_store = { version = "0.20.0", features = ["preserve_order"] } cookie_store = { version = "0.20.0", features = ["preserve_order"] }
digest_auth = "0.3.0" digest_auth = "0.3.0"
dirs = "5.0" dirs = "5.0"
@ -38,7 +39,7 @@ once_cell = "1.8.0"
os_display = "0.1.3" os_display = "0.1.3"
pem = "3.0" pem = "3.0"
regex-lite = "0.1.5" regex-lite = "0.1.5"
roff = { version = "0.2.1", optional = true } roff = "0.2.1"
rpassword = "7.2.0" rpassword = "7.2.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde-transcode = "1.1.1" serde-transcode = "1.1.1"
@ -101,7 +102,6 @@ network-interface = ["dep:network-interface"]
online-tests = [] online-tests = []
ipv6-tests = [] ipv6-tests = []
man-completion-gen = ["clap_complete", "roff"]
[package.metadata.cross.build.env] [package.metadata.cross.build.env]
passthrough = ["CARGO_PROFILE_RELEASE_LTO"] passthrough = ["CARGO_PROFILE_RELEASE_LTO"]

View File

@ -4,9 +4,15 @@
- Update `CHANGELOG.md` (rename unreleased header to the current date, add any missing changes). - Update `CHANGELOG.md` (rename unreleased header to the current date, add any missing changes).
- Run `cargo update` to update dependencies. - Run `cargo update` to update dependencies.
- Bump up the version in `Cargo.toml` and run `cargo check` to update `Cargo.lock`. - Bump up the version in `Cargo.toml` and run `cargo check` to update `Cargo.lock`.
- Run the following to update man pages and shell-completion files. - Run the following to update shell-completion files and man pages.
```sh ```sh
cargo run --all-features -- generate-completions completions && cargo run --all-features -- generate-manpages doc cargo run --all-features -- --generate complete-bash > completions/xh.bash
cargo run --all-features -- --generate complete-elvish > completions/xh.elv
cargo run --all-features -- --generate complete-fish > completions/xh.fish
cargo run --all-features -- --generate complete-nushell > completions/xh.nu
cargo run --all-features -- --generate complete-powershell > completions/_xh.ps1
cargo run --all-features -- --generate complete-zsh > completions/_xh
cargo run --all-features -- --generate man > doc/xh.1
``` ```
- Commit changes and push them to remote. - Commit changes and push them to remote.
- Add git tag e.g `git tag v0.9.0`. - Add git tag e.g `git tag v0.9.0`.

View File

@ -49,6 +49,7 @@ none\:"Disable both coloring and formatting"))' \
'--http-version=[HTTP version to use]:VERSION:(1.0 1.1 2 2-prior-knowledge)' \ '--http-version=[HTTP version to use]:VERSION:(1.0 1.1 2 2-prior-knowledge)' \
'*--resolve=[Override DNS resolution for specific domain to a custom IP]:HOST:ADDRESS:_default' \ '*--resolve=[Override DNS resolution for specific domain to a custom IP]:HOST:ADDRESS:_default' \
'--interface=[Bind to a network interface or local IP address]:NAME:_default' \ '--interface=[Bind to a network interface or local IP address]:NAME:_default' \
'--generate=[Generate shell completions or man pages]:KIND:(complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh man)' \
'-j[(default) Serialize data items from the command line as a JSON object]' \ '-j[(default) Serialize data items from the command line as a JSON object]' \
'--json[(default) Serialize data items from the command line as a JSON object]' \ '--json[(default) Serialize data items from the command line as a JSON object]' \
'-f[Serialize data items from the command line as form fields]' \ '-f[Serialize data items from the command line as form fields]' \
@ -137,10 +138,11 @@ none\:"Disable both coloring and formatting"))' \
'--no-ignore-stdin[]' \ '--no-ignore-stdin[]' \
'--no-curl[]' \ '--no-curl[]' \
'--no-curl-long[]' \ '--no-curl-long[]' \
'--no-generate[]' \
'--no-help[]' \ '--no-help[]' \
'-V[Print version]' \ '-V[Print version]' \
'--version[Print version]' \ '--version[Print version]' \
':raw_method_or_url -- The request URL, preceded by an optional HTTP method:_default' \ '::raw_method_or_url -- The request URL, preceded by an optional HTTP method:_default' \
'*::raw_rest_args -- Optional key-value pairs to be included in the request.:_default' \ '*::raw_rest_args -- Optional key-value pairs to be included in the request.:_default' \
&& ret=0 && ret=0
} }

View File

@ -52,6 +52,7 @@ Register-ArgumentCompleter -Native -CommandName 'xh' -ScriptBlock {
[CompletionResult]::new('--http-version', '--http-version', [CompletionResultType]::ParameterName, 'HTTP version to use') [CompletionResult]::new('--http-version', '--http-version', [CompletionResultType]::ParameterName, 'HTTP version to use')
[CompletionResult]::new('--resolve', '--resolve', [CompletionResultType]::ParameterName, 'Override DNS resolution for specific domain to a custom IP') [CompletionResult]::new('--resolve', '--resolve', [CompletionResultType]::ParameterName, 'Override DNS resolution for specific domain to a custom IP')
[CompletionResult]::new('--interface', '--interface', [CompletionResultType]::ParameterName, 'Bind to a network interface or local IP address') [CompletionResult]::new('--interface', '--interface', [CompletionResultType]::ParameterName, 'Bind to a network interface or local IP address')
[CompletionResult]::new('--generate', '--generate', [CompletionResultType]::ParameterName, 'Generate shell completions or man pages')
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, '(default) Serialize data items from the command line as a JSON object') [CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, '(default) Serialize data items from the command line as a JSON object')
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, '(default) Serialize data items from the command line as a JSON object') [CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, '(default) Serialize data items from the command line as a JSON object')
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Serialize data items from the command line as form fields') [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Serialize data items from the command line as form fields')
@ -140,6 +141,7 @@ Register-ArgumentCompleter -Native -CommandName 'xh' -ScriptBlock {
[CompletionResult]::new('--no-ignore-stdin', '--no-ignore-stdin', [CompletionResultType]::ParameterName, 'no-ignore-stdin') [CompletionResult]::new('--no-ignore-stdin', '--no-ignore-stdin', [CompletionResultType]::ParameterName, 'no-ignore-stdin')
[CompletionResult]::new('--no-curl', '--no-curl', [CompletionResultType]::ParameterName, 'no-curl') [CompletionResult]::new('--no-curl', '--no-curl', [CompletionResultType]::ParameterName, 'no-curl')
[CompletionResult]::new('--no-curl-long', '--no-curl-long', [CompletionResultType]::ParameterName, 'no-curl-long') [CompletionResult]::new('--no-curl-long', '--no-curl-long', [CompletionResultType]::ParameterName, 'no-curl-long')
[CompletionResult]::new('--no-generate', '--no-generate', [CompletionResultType]::ParameterName, 'no-generate')
[CompletionResult]::new('--no-help', '--no-help', [CompletionResultType]::ParameterName, 'no-help') [CompletionResult]::new('--no-help', '--no-help', [CompletionResultType]::ParameterName, 'no-help')
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')

View File

@ -19,7 +19,7 @@ _xh() {
case "${cmd}" in case "${cmd}" in
xh) xh)
opts="-j -f -s -p -h -b -m -v -P -q -S -o -d -c -A -a -F -4 -6 -I -V --json --form --multipart --raw --pretty --format-options --style --response-charset --response-mime --print --headers --body --meta --verbose --debug --all --history-print --quiet --stream --output --download --continue --session --session-read-only --auth-type --auth --bearer --ignore-netrc --offline --check-status --follow --max-redirects --timeout --proxy --verify --cert --cert-key --ssl --native-tls --default-scheme --https --http-version --resolve --interface --ipv4 --ipv6 --ignore-stdin --curl --curl-long --help --no-json --no-form --no-multipart --no-raw --no-pretty --no-format-options --no-style --no-response-charset --no-response-mime --no-print --no-headers --no-body --no-meta --no-verbose --no-debug --no-all --no-history-print --no-quiet --no-stream --no-output --no-download --no-continue --no-session --no-session-read-only --no-auth-type --no-auth --no-bearer --no-ignore-netrc --no-offline --no-check-status --no-follow --no-max-redirects --no-timeout --no-proxy --no-verify --no-cert --no-cert-key --no-ssl --no-native-tls --no-default-scheme --no-https --no-http-version --no-resolve --no-interface --no-ipv4 --no-ipv6 --no-ignore-stdin --no-curl --no-curl-long --no-help --version <[METHOD] URL> [REQUEST_ITEM]..." opts="-j -f -s -p -h -b -m -v -P -q -S -o -d -c -A -a -F -4 -6 -I -V --json --form --multipart --raw --pretty --format-options --style --response-charset --response-mime --print --headers --body --meta --verbose --debug --all --history-print --quiet --stream --output --download --continue --session --session-read-only --auth-type --auth --bearer --ignore-netrc --offline --check-status --follow --max-redirects --timeout --proxy --verify --cert --cert-key --ssl --native-tls --default-scheme --https --http-version --resolve --interface --ipv4 --ipv6 --ignore-stdin --curl --curl-long --generate --help --no-json --no-form --no-multipart --no-raw --no-pretty --no-format-options --no-style --no-response-charset --no-response-mime --no-print --no-headers --no-body --no-meta --no-verbose --no-debug --no-all --no-history-print --no-quiet --no-stream --no-output --no-download --no-continue --no-session --no-session-read-only --no-auth-type --no-auth --no-bearer --no-ignore-netrc --no-offline --no-check-status --no-follow --no-max-redirects --no-timeout --no-proxy --no-verify --no-cert --no-cert-key --no-ssl --no-native-tls --no-default-scheme --no-https --no-http-version --no-resolve --no-interface --no-ipv4 --no-ipv6 --no-ignore-stdin --no-curl --no-curl-long --no-generate --no-help --version [[METHOD] URL] [REQUEST_ITEM]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -149,6 +149,10 @@ _xh() {
COMPREPLY=($(compgen -f "${cur}")) COMPREPLY=($(compgen -f "${cur}"))
return 0 return 0
;; ;;
--generate)
COMPREPLY=($(compgen -W "complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh man" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;

148
completions/xh.elv Normal file
View File

@ -0,0 +1,148 @@
use builtin;
use str;
set edit:completion:arg-completer[xh] = {|@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 = 'xh'
for word $words[1..-1] {
if (str:has-prefix $word '-') {
break
}
set command = $command';'$word
}
var completions = [
&'xh'= {
cand --raw 'Pass raw request data without extra processing'
cand --pretty 'Controls output processing'
cand --format-options 'Set output formatting options'
cand -s 'Output coloring style'
cand --style 'Output coloring style'
cand --response-charset 'Override the response encoding for terminal display purposes'
cand --response-mime 'Override the response mime type for coloring and formatting for the terminal'
cand -p 'String specifying what the output should contain'
cand --print 'String specifying what the output should contain'
cand -P 'The same as --print but applies only to intermediary requests/responses'
cand --history-print 'The same as --print but applies only to intermediary requests/responses'
cand -o 'Save output to FILE instead of stdout'
cand --output 'Save output to FILE instead of stdout'
cand --session 'Create, or reuse and update a session'
cand --session-read-only 'Create or read a session without updating it form the request/response exchange'
cand -A 'Specify the auth mechanism'
cand --auth-type 'Specify the auth mechanism'
cand -a 'Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)'
cand --auth 'Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)'
cand --bearer 'Authenticate with a bearer token'
cand --max-redirects 'Number of redirects to follow. Only respected if --follow is used'
cand --timeout 'Connection timeout of the request'
cand --proxy 'Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080'
cand --verify 'If "no", skip SSL verification. If a file path, use it as a CA bundle'
cand --cert 'Use a client side certificate for SSL'
cand --cert-key 'A private key file to use with --cert'
cand --ssl 'Force a particular TLS version'
cand --default-scheme 'The default scheme to use if not specified in the URL'
cand --http-version 'HTTP version to use'
cand --resolve 'Override DNS resolution for specific domain to a custom IP'
cand --interface 'Bind to a network interface or local IP address'
cand --generate 'Generate shell completions or man pages'
cand -j '(default) Serialize data items from the command line as a JSON object'
cand --json '(default) Serialize data items from the command line as a JSON object'
cand -f 'Serialize data items from the command line as form fields'
cand --form 'Serialize data items from the command line as form fields'
cand --multipart 'Like --form, but force a multipart/form-data request even without files'
cand -h 'Print only the response headers. Shortcut for --print=h'
cand --headers 'Print only the response headers. Shortcut for --print=h'
cand -b 'Print only the response body. Shortcut for --print=b'
cand --body 'Print only the response body. Shortcut for --print=b'
cand -m 'Print only the response metadata. Shortcut for --print=m'
cand --meta 'Print only the response metadata. Shortcut for --print=m'
cand -v 'Print the whole request as well as the response'
cand --verbose 'Print the whole request as well as the response'
cand --debug 'Print full error stack traces and debug log messages'
cand --all 'Show any intermediary requests/responses while following redirects with --follow'
cand -q 'Do not print to stdout or stderr'
cand --quiet 'Do not print to stdout or stderr'
cand -S 'Always stream the response body'
cand --stream 'Always stream the response body'
cand -d 'Download the body to a file instead of printing it'
cand --download 'Download the body to a file instead of printing it'
cand -c 'Resume an interrupted download. Requires --download and --output'
cand --continue 'Resume an interrupted download. Requires --download and --output'
cand --ignore-netrc 'Do not use credentials from .netrc'
cand --offline 'Construct HTTP requests without sending them anywhere'
cand --check-status '(default) Exit with an error status code if the server replies with an error'
cand -F 'Do follow redirects'
cand --follow 'Do follow redirects'
cand --native-tls 'Use the system TLS library instead of rustls (if enabled at compile time)'
cand --https 'Make HTTPS requests if not specified in the URL'
cand -4 'Resolve hostname to ipv4 addresses only'
cand --ipv4 'Resolve hostname to ipv4 addresses only'
cand -6 'Resolve hostname to ipv6 addresses only'
cand --ipv6 'Resolve hostname to ipv6 addresses only'
cand -I 'Do not attempt to read stdin'
cand --ignore-stdin 'Do not attempt to read stdin'
cand --curl 'Print a translation to a curl command'
cand --curl-long 'Use the long versions of curl''s flags'
cand --help 'Print help'
cand --no-json 'no-json'
cand --no-form 'no-form'
cand --no-multipart 'no-multipart'
cand --no-raw 'no-raw'
cand --no-pretty 'no-pretty'
cand --no-format-options 'no-format-options'
cand --no-style 'no-style'
cand --no-response-charset 'no-response-charset'
cand --no-response-mime 'no-response-mime'
cand --no-print 'no-print'
cand --no-headers 'no-headers'
cand --no-body 'no-body'
cand --no-meta 'no-meta'
cand --no-verbose 'no-verbose'
cand --no-debug 'no-debug'
cand --no-all 'no-all'
cand --no-history-print 'no-history-print'
cand --no-quiet 'no-quiet'
cand --no-stream 'no-stream'
cand --no-output 'no-output'
cand --no-download 'no-download'
cand --no-continue 'no-continue'
cand --no-session 'no-session'
cand --no-session-read-only 'no-session-read-only'
cand --no-auth-type 'no-auth-type'
cand --no-auth 'no-auth'
cand --no-bearer 'no-bearer'
cand --no-ignore-netrc 'no-ignore-netrc'
cand --no-offline 'no-offline'
cand --no-check-status 'no-check-status'
cand --no-follow 'no-follow'
cand --no-max-redirects 'no-max-redirects'
cand --no-timeout 'no-timeout'
cand --no-proxy 'no-proxy'
cand --no-verify 'no-verify'
cand --no-cert 'no-cert'
cand --no-cert-key 'no-cert-key'
cand --no-ssl 'no-ssl'
cand --no-native-tls 'no-native-tls'
cand --no-default-scheme 'no-default-scheme'
cand --no-https 'no-https'
cand --no-http-version 'no-http-version'
cand --no-resolve 'no-resolve'
cand --no-interface 'no-interface'
cand --no-ipv4 'no-ipv4'
cand --no-ipv6 'no-ipv6'
cand --no-ignore-stdin 'no-ignore-stdin'
cand --no-curl 'no-curl'
cand --no-curl-long 'no-curl-long'
cand --no-generate 'no-generate'
cand --no-help 'no-help'
cand -V 'Print version'
cand --version 'Print version'
}
]
$completions[$command]
}

View File

@ -23,6 +23,7 @@ complete -c xh -l default-scheme -d 'The default scheme to use if not specified
complete -c xh -l http-version -d 'HTTP version to use' -r -f -a "{1.0\t'',1.1\t'',2\t'',2-prior-knowledge\t''}" complete -c xh -l http-version -d 'HTTP version to use' -r -f -a "{1.0\t'',1.1\t'',2\t'',2-prior-knowledge\t''}"
complete -c xh -l resolve -d 'Override DNS resolution for specific domain to a custom IP' -r complete -c xh -l resolve -d 'Override DNS resolution for specific domain to a custom IP' -r
complete -c xh -l interface -d 'Bind to a network interface or local IP address' -r complete -c xh -l interface -d 'Bind to a network interface or local IP address' -r
complete -c xh -l generate -d 'Generate shell completions or man pages' -r -f -a "{complete-bash\t'',complete-elvish\t'',complete-fish\t'',complete-nushell\t'',complete-powershell\t'',complete-zsh\t'',man\t''}"
complete -c xh -s j -l json -d '(default) Serialize data items from the command line as a JSON object' complete -c xh -s j -l json -d '(default) Serialize data items from the command line as a JSON object'
complete -c xh -s f -l form -d 'Serialize data items from the command line as form fields' complete -c xh -s f -l form -d 'Serialize data items from the command line as form fields'
complete -c xh -l multipart -d 'Like --form, but force a multipart/form-data request even without files' complete -c xh -l multipart -d 'Like --form, but force a multipart/form-data request even without files'
@ -97,5 +98,6 @@ complete -c xh -l no-ipv6
complete -c xh -l no-ignore-stdin complete -c xh -l no-ignore-stdin
complete -c xh -l no-curl complete -c xh -l no-curl
complete -c xh -l no-curl-long complete -c xh -l no-curl-long
complete -c xh -l no-generate
complete -c xh -l no-help complete -c xh -l no-help
complete -c xh -s V -l version -d 'Print version' complete -c xh -s V -l version -d 'Print version'

138
completions/xh.nu Normal file
View File

@ -0,0 +1,138 @@
module completions {
def "nu-complete xh pretty" [] {
[ "all" "colors" "format" "none" ]
}
def "nu-complete xh style" [] {
[ "auto" "solarized" "monokai" "fruity" ]
}
def "nu-complete xh auth_type" [] {
[ "basic" "bearer" "digest" ]
}
def "nu-complete xh ssl" [] {
[ "auto" "tls1" "tls1.1" "tls1.2" "tls1.3" ]
}
def "nu-complete xh http_version" [] {
[ "1.0" "1.1" "2" "2-prior-knowledge" ]
}
def "nu-complete xh generate" [] {
[ "complete-bash" "complete-elvish" "complete-fish" "complete-nushell" "complete-powershell" "complete-zsh" "man" ]
}
# xh is a friendly and fast tool for sending HTTP requests
export extern xh [
--json(-j) # (default) Serialize data items from the command line as a JSON object
--form(-f) # Serialize data items from the command line as form fields
--multipart # Like --form, but force a multipart/form-data request even without files
--raw: string # Pass raw request data without extra processing
--pretty: string@"nu-complete xh pretty" # Controls output processing
--format-options: string # Set output formatting options
--style(-s): string@"nu-complete xh style" # Output coloring style
--response-charset: string # Override the response encoding for terminal display purposes
--response-mime: string # Override the response mime type for coloring and formatting for the terminal
--print(-p): string # String specifying what the output should contain
--headers(-h) # Print only the response headers. Shortcut for --print=h
--body(-b) # Print only the response body. Shortcut for --print=b
--meta(-m) # Print only the response metadata. Shortcut for --print=m
--verbose(-v) # Print the whole request as well as the response
--debug # Print full error stack traces and debug log messages
--all # Show any intermediary requests/responses while following redirects with --follow
--history-print(-P): string # The same as --print but applies only to intermediary requests/responses
--quiet(-q) # Do not print to stdout or stderr
--stream(-S) # Always stream the response body
--output(-o): string # Save output to FILE instead of stdout
--download(-d) # Download the body to a file instead of printing it
--continue(-c) # Resume an interrupted download. Requires --download and --output
--session: string # Create, or reuse and update a session
--session-read-only: string # Create or read a session without updating it form the request/response exchange
--auth-type(-A): string@"nu-complete xh auth_type" # Specify the auth mechanism
--auth(-a): string # Authenticate as USER with PASS (-A basic|digest) or with TOKEN (-A bearer)
--bearer: string # Authenticate with a bearer token
--ignore-netrc # Do not use credentials from .netrc
--offline # Construct HTTP requests without sending them anywhere
--check-status # (default) Exit with an error status code if the server replies with an error
--follow(-F) # Do follow redirects
--max-redirects: string # Number of redirects to follow. Only respected if --follow is used
--timeout: string # Connection timeout of the request
--proxy: string # Use a proxy for a protocol. For example: --proxy https:http://proxy.host:8080
--verify: string # If "no", skip SSL verification. If a file path, use it as a CA bundle
--cert: string # Use a client side certificate for SSL
--cert-key: string # A private key file to use with --cert
--ssl: string@"nu-complete xh ssl" # Force a particular TLS version
--native-tls # Use the system TLS library instead of rustls (if enabled at compile time)
--default-scheme: string # The default scheme to use if not specified in the URL
--https # Make HTTPS requests if not specified in the URL
--http-version: string@"nu-complete xh http_version" # HTTP version to use
--resolve: string # Override DNS resolution for specific domain to a custom IP
--interface: string # Bind to a network interface or local IP address
--ipv4(-4) # Resolve hostname to ipv4 addresses only
--ipv6(-6) # Resolve hostname to ipv6 addresses only
--ignore-stdin(-I) # Do not attempt to read stdin
--curl # Print a translation to a curl command
--curl-long # Use the long versions of curl's flags
--generate: string@"nu-complete xh generate" # Generate shell completions or man pages
--help # Print help
raw_method_or_url?: string # The request URL, preceded by an optional HTTP method
...raw_rest_args: string # Optional key-value pairs to be included in the request.
--no-json
--no-form
--no-multipart
--no-raw
--no-pretty
--no-format-options
--no-style
--no-response-charset
--no-response-mime
--no-print
--no-headers
--no-body
--no-meta
--no-verbose
--no-debug
--no-all
--no-history-print
--no-quiet
--no-stream
--no-output
--no-download
--no-continue
--no-session
--no-session-read-only
--no-auth-type
--no-auth
--no-bearer
--no-ignore-netrc
--no-offline
--no-check-status
--no-follow
--no-max-redirects
--no-timeout
--no-proxy
--no-verify
--no-cert
--no-cert-key
--no-ssl
--no-native-tls
--no-default-scheme
--no-https
--no-http-version
--no-resolve
--no-interface
--no-ipv4
--no-ipv6
--no-ignore-stdin
--no-curl
--no-curl-long
--no-generate
--no-help
--version(-V) # Print version
]
}
export use completions *

View File

@ -1,4 +1,4 @@
.TH XH 1 2025-01-02 0.23.1 "User Commands" .TH XH 1 2025-01-04 0.23.1 "User Commands"
.SH NAME .SH NAME
xh \- Friendly and fast tool for sending HTTP requests xh \- Friendly and fast tool for sending HTTP requests
@ -320,6 +320,11 @@ For translating the other way, try https://curl2httpie.online/.
\fB\-\-curl\-long\fR \fB\-\-curl\-long\fR
Use the long versions of curl's flags. Use the long versions of curl's flags.
.TP 4 .TP 4
\fB\-\-generate\fR=\fIKIND\fR
Generate shell completions or man pages.
[possible values: complete\-bash, complete\-elvish, complete\-fish, complete\-nushell, complete\-powershell, complete\-zsh, man]
.TP 4
\fB\-\-help\fR \fB\-\-help\fR
Print help. Print help.
.TP 4 .TP 4

View File

@ -367,6 +367,27 @@ Example: --print=Hb"
#[clap(long)] #[clap(long)]
pub curl_long: bool, pub curl_long: bool,
/// Generate shell completions or man pages.
#[arg(
long,
value_name = "KIND",
hide_possible_values = true,
long_help = "\
Generate shell completions or man pages. Possible values are:
complete-bash
complete-elvish
complete-fish
complete-nushell
complete-powershell
complete-zsh
man
Example: xh --generate=complete-bash > xh.bash",
conflicts_with = "raw_method_or_url"
)]
pub generate: Option<Generate>,
/// Print help. /// Print help.
#[clap(long, action = ArgAction::HelpShort)] #[clap(long, action = ArgAction::HelpShort)]
pub help: Option<bool>, pub help: Option<bool>,
@ -381,8 +402,8 @@ Example: --print=Hb"
/// ///
/// A leading colon works as shorthand for localhost. ":8000" is equivalent /// A leading colon works as shorthand for localhost. ":8000" is equivalent
/// to "localhost:8000", and ":/path" is equivalent to "localhost/path". /// to "localhost:8000", and ":/path" is equivalent to "localhost/path".
#[clap(value_name = "[METHOD] URL")] #[clap(value_name = "[METHOD] URL", required = true)]
raw_method_or_url: String, raw_method_or_url: Option<String>,
/// Optional key-value pairs to be included in the request. /// Optional key-value pairs to be included in the request.
/// ///
@ -480,21 +501,28 @@ impl Cli {
let matches = app.try_get_matches_from_mut(iter)?; let matches = app.try_get_matches_from_mut(iter)?;
let mut cli = Self::from_arg_matches(&matches)?; let mut cli = Self::from_arg_matches(&matches)?;
match cli.raw_method_or_url.as_str() { app.get_bin_name()
"help" => { .and_then(|name| name.split('.').next())
// opt-out of clap's auto-generated possible values help for --pretty .unwrap_or("xh")
// as we already list them in the long_help .clone_into(&mut cli.bin_name);
app = app.mut_arg("pretty", |a| a.hide_possible_values(true));
app.print_long_help().unwrap(); if cli.generate.is_some() {
safe_exit(); return Ok(cli);
}
"generate-completions" => return Err(generate_completions(app, cli.raw_rest_args)),
"generate-manpages" => return Err(generate_manpages(app, cli.raw_rest_args)),
_ => {}
} }
let mut raw_method_or_url = cli.raw_method_or_url.clone().unwrap();
if raw_method_or_url == "help" {
// opt-out of clap's auto-generated possible values help for --pretty
// as we already list them in the long_help
app = app.mut_arg("pretty", |a| a.hide_possible_values(true));
app.print_long_help().unwrap();
safe_exit();
}
let mut rest_args = mem::take(&mut cli.raw_rest_args).into_iter(); let mut rest_args = mem::take(&mut cli.raw_rest_args).into_iter();
let raw_url = match parse_method(&cli.raw_method_or_url) { let raw_url = match parse_method(&raw_method_or_url) {
Some(method) => { Some(method) => {
cli.method = Some(method); cli.method = Some(method);
rest_args.next().ok_or_else(|| { rest_args.next().ok_or_else(|| {
@ -506,7 +534,7 @@ impl Cli {
} }
None => { None => {
cli.method = None; cli.method = None;
mem::take(&mut cli.raw_method_or_url) mem::take(&mut raw_method_or_url)
} }
}; };
for request_item in rest_args { for request_item in rest_args {
@ -517,11 +545,6 @@ impl Cli {
); );
} }
app.get_bin_name()
.and_then(|name| name.split('.').next())
.unwrap_or("xh")
.clone_into(&mut cli.bin_name);
if matches!(cli.bin_name.as_str(), "https" | "xhs" | "xhttps") { if matches!(cli.bin_name.as_str(), "https" | "xhs" | "xhttps") {
cli.https = true; cli.https = true;
} }
@ -625,9 +648,12 @@ impl Cli {
}) })
.collect(); .collect();
app.args(negations) let mut app = app.args(negations)
.after_help(format!("Each option can be reset with a --no-OPTION argument.\n\nRun \"{} help\" for more complete documentation.", env!("CARGO_PKG_NAME"))) .after_help(format!("Each option can be reset with a --no-OPTION argument.\n\nRun \"{} help\" for more complete documentation.", env!("CARGO_PKG_NAME")))
.after_long_help("Each option can be reset with a --no-OPTION argument.") .after_long_help("Each option can be reset with a --no-OPTION argument.");
app.build();
app
} }
pub fn logger_config(&self) -> env_logger::Builder { pub fn logger_config(&self) -> env_logger::Builder {
@ -744,209 +770,6 @@ fn construct_url(
Ok(url) Ok(url)
} }
#[cfg(feature = "man-completion-gen")]
// This signature is a little weird: we either return an error or don't return at all
fn generate_completions(mut app: clap::Command, rest_args: Vec<String>) -> clap::error::Error {
let bin_name = app.get_bin_name().unwrap().to_string();
if rest_args.len() != 1 {
return app.error(
clap::error::ErrorKind::WrongNumberOfValues,
"Usage: xh generate-completions <DIRECTORY>",
);
}
for &shell in clap_complete::Shell::value_variants() {
// Elvish complains about multiple deprecations and these don't seem to work
if shell != clap_complete::Shell::Elvish {
clap_complete::generate_to(shell, &mut app, &bin_name, &rest_args[0]).unwrap();
}
}
safe_exit();
}
#[cfg(feature = "man-completion-gen")]
fn generate_manpages(mut app: clap::Command, rest_args: Vec<String>) -> clap::error::Error {
use roff::{bold, italic, roman, Roff};
use time::OffsetDateTime as DateTime;
if rest_args.len() != 1 {
return app.error(
clap::error::ErrorKind::WrongNumberOfValues,
"Usage: xh generate-manpages <DIRECTORY>",
);
}
let items: Vec<_> = app.get_arguments().filter(|i| !i.is_hide_set()).collect();
let mut request_items_roff = Roff::new();
let request_items = items
.iter()
.find(|opt| opt.get_id() == "raw_rest_args")
.unwrap();
let request_items_help = request_items
.get_long_help()
.or_else(|| request_items.get_help())
.expect("request_items is missing help")
.to_string();
// replace the indents in request_item help with proper roff controls
// For example:
//
// ```
// normal help normal help
// normal help normal help
//
// request-item-1
// help help
//
// request-item-2
// help help
//
// normal help normal help
// ```
//
// Should look like this with roff controls
//
// ```
// normal help normal help
// normal help normal help
// .RS 12
// .TP
// request-item-1
// help help
// .TP
// request-item-2
// help help
// .RE
//
// .RS
// normal help normal help
// .RE
// ```
let lines: Vec<&str> = request_items_help.lines().collect();
let mut rs = false;
for i in 0..lines.len() {
if lines[i].is_empty() {
let prev = lines[i - 1].chars().take_while(|&x| x == ' ').count();
let next = lines[i + 1].chars().take_while(|&x| x == ' ').count();
if prev != next && next > 0 {
if !rs {
request_items_roff.control("RS", ["8"]);
rs = true;
}
request_items_roff.control("TP", ["4"]);
} else if prev != next && next == 0 {
request_items_roff.control("RE", []);
request_items_roff.text(vec![roman("")]);
request_items_roff.control("RS", []);
} else {
request_items_roff.text(vec![roman(lines[i])]);
}
} else {
request_items_roff.text(vec![roman(lines[i].trim())]);
}
}
request_items_roff.control("RE", []);
let mut options_roff = Roff::new();
let non_pos_items = items
.iter()
.filter(|a| !a.is_positional())
.collect::<Vec<_>>();
for opt in non_pos_items {
let mut header = vec![];
if let Some(short) = opt.get_short() {
header.push(bold(format!("-{}", short)));
}
if let Some(long) = opt.get_long() {
if !header.is_empty() {
header.push(roman(", "));
}
header.push(bold(format!("--{}", long)));
}
if opt.get_action().takes_values() {
let value_name = &opt.get_value_names().unwrap();
if opt.get_long().is_some() {
header.push(roman("="));
} else {
header.push(roman(" "));
}
if opt.get_id() == "auth" {
header.push(italic("USER"));
header.push(roman("["));
header.push(italic(":PASS"));
header.push(roman("] | "));
header.push(italic("TOKEN"));
} else {
header.push(italic(value_name.join(" ")));
}
}
let mut body = vec![];
let mut help = opt
.get_long_help()
.or_else(|| opt.get_help())
.expect("option is missing help")
.to_string();
if !help.ends_with('.') {
help.push('.')
}
body.push(roman(help));
let possible_values = opt.get_possible_values();
if !possible_values.is_empty()
&& !opt.is_hide_possible_values_set()
&& opt.get_id() != "pretty"
{
let possible_values_text = format!(
"\n\n[possible values: {}]",
possible_values
.iter()
.map(|v| v.get_name())
.collect::<Vec<_>>()
.join(", ")
);
body.push(roman(possible_values_text));
}
options_roff.control("TP", ["4"]);
options_roff.text(header);
options_roff.text(body);
}
let mut manpage = fs::read_to_string(format!("{}/man-template.roff", rest_args[0])).unwrap();
let current_date = {
let (year, month, day) = DateTime::now_utc().date().to_calendar_date();
format!("{}-{:02}-{:02}", year, u8::from(month), day)
};
manpage = manpage.replace("{{date}}", &current_date);
manpage = manpage.replace("{{version}}", app.get_version().unwrap());
manpage = manpage.replace("{{request_items}}", request_items_roff.to_roff().trim());
manpage = manpage.replace("{{options}}", options_roff.to_roff().trim());
fs::write(format!("{}/xh.1", rest_args[0]), manpage).unwrap();
safe_exit();
}
#[cfg(not(feature = "man-completion-gen"))]
fn generate_completions(mut _app: clap::Command, _rest_args: Vec<String>) -> clap::error::Error {
clap::Error::raw(
clap::error::ErrorKind::InvalidSubcommand,
"generate-completions requires enabling man-completion-gen feature\n",
)
}
#[cfg(not(feature = "man-completion-gen"))]
fn generate_manpages(mut _app: clap::Command, _rest_args: Vec<String>) -> clap::error::Error {
clap::Error::raw(
clap::error::ErrorKind::InvalidSubcommand,
"generate-manpages requires enabling man-completion-gen feature\n",
)
}
#[derive(Default, ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] #[derive(Default, ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum AuthType { pub enum AuthType {
#[default] #[default]
@ -1353,6 +1176,17 @@ pub enum HttpVersion {
Http2PriorKnowledge, Http2PriorKnowledge,
} }
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum Generate {
CompleteBash,
CompleteElvish,
CompleteFish,
CompleteNushell,
CompletePowershell,
CompleteZsh,
Man,
}
/// HTTPie uses Python's str.decode(). That one's very accepting of different spellings. /// HTTPie uses Python's str.decode(). That one's very accepting of different spellings.
/// encoding_rs is not. /// encoding_rs is not.
/// ///
@ -1870,4 +1704,16 @@ mod tests {
assert!(Resolve::from_str("example.com:::1").is_ok()); assert!(Resolve::from_str("example.com:::1").is_ok());
assert!(Resolve::from_str("example.com:[::1]").is_ok()); assert!(Resolve::from_str("example.com:[::1]").is_ok());
} }
#[test]
fn generate() {
let cli = parse(["--generate", "complete-bash"]).unwrap();
assert_eq!(cli.generate, Some(Generate::CompleteBash));
assert_eq!(cli.raw_method_or_url, None);
}
#[test]
fn generate_with_url() {
parse(["--generate", "complete-zsh", "example.org"]).unwrap_err();
}
} }

195
src/generation.rs Normal file
View File

@ -0,0 +1,195 @@
use std::io;
use clap_complete::Shell;
use clap_complete_nushell::Nushell;
use crate::cli::Cli;
use crate::cli::Generate;
const MAN_TEMPLATE: &str = include_str!("../doc/man-template.roff");
pub fn generate(bin_name: &str, generate: Generate) {
let mut app = Cli::into_app();
match generate {
Generate::CompleteBash => {
clap_complete::generate(Shell::Bash, &mut app, bin_name, &mut io::stdout());
}
Generate::CompleteElvish => {
clap_complete::generate(Shell::Elvish, &mut app, bin_name, &mut io::stdout());
}
Generate::CompleteFish => {
clap_complete::generate(Shell::Fish, &mut app, bin_name, &mut io::stdout());
}
Generate::CompleteNushell => {
clap_complete::generate(Nushell, &mut app, bin_name, &mut io::stdout());
}
Generate::CompletePowershell => {
clap_complete::generate(Shell::PowerShell, &mut app, bin_name, &mut io::stdout());
}
Generate::CompleteZsh => {
clap_complete::generate(Shell::Zsh, &mut app, bin_name, &mut io::stdout());
}
Generate::Man => {
generate_manpages(&mut app);
}
}
}
fn generate_manpages(app: &mut clap::Command) {
use roff::{bold, italic, roman, Roff};
use time::OffsetDateTime as DateTime;
let items: Vec<_> = app.get_arguments().filter(|i| !i.is_hide_set()).collect();
let mut request_items_roff = Roff::new();
let request_items = items
.iter()
.find(|opt| opt.get_id() == "raw_rest_args")
.unwrap();
let request_items_help = request_items
.get_long_help()
.or_else(|| request_items.get_help())
.expect("request_items is missing help")
.to_string();
// replace the indents in request_item help with proper roff controls
// For example:
//
// ```
// normal help normal help
// normal help normal help
//
// request-item-1
// help help
//
// request-item-2
// help help
//
// normal help normal help
// ```
//
// Should look like this with roff controls
//
// ```
// normal help normal help
// normal help normal help
// .RS 12
// .TP
// request-item-1
// help help
// .TP
// request-item-2
// help help
// .RE
//
// .RS
// normal help normal help
// .RE
// ```
let lines: Vec<&str> = request_items_help.lines().collect();
let mut rs = false;
for i in 0..lines.len() {
if lines[i].is_empty() {
let prev = lines[i - 1].chars().take_while(|&x| x == ' ').count();
let next = lines[i + 1].chars().take_while(|&x| x == ' ').count();
if prev != next && next > 0 {
if !rs {
request_items_roff.control("RS", ["8"]);
rs = true;
}
request_items_roff.control("TP", ["4"]);
} else if prev != next && next == 0 {
request_items_roff.control("RE", []);
request_items_roff.text(vec![roman("")]);
request_items_roff.control("RS", []);
} else {
request_items_roff.text(vec![roman(lines[i])]);
}
} else {
request_items_roff.text(vec![roman(lines[i].trim())]);
}
}
request_items_roff.control("RE", []);
let mut options_roff = Roff::new();
let non_pos_items = items
.iter()
.filter(|a| !a.is_positional())
.collect::<Vec<_>>();
for opt in non_pos_items {
let mut header = vec![];
if let Some(short) = opt.get_short() {
header.push(bold(format!("-{}", short)));
}
if let Some(long) = opt.get_long() {
if !header.is_empty() {
header.push(roman(", "));
}
header.push(bold(format!("--{}", long)));
}
if opt.get_action().takes_values() {
let value_name = &opt.get_value_names().unwrap();
if opt.get_long().is_some() {
header.push(roman("="));
} else {
header.push(roman(" "));
}
if opt.get_id() == "auth" {
header.push(italic("USER"));
header.push(roman("["));
header.push(italic(":PASS"));
header.push(roman("] | "));
header.push(italic("TOKEN"));
} else {
header.push(italic(value_name.join(" ")));
}
}
let mut body = vec![];
let mut help = opt
.get_long_help()
.or_else(|| opt.get_help())
.expect("option is missing help")
.to_string();
if !help.ends_with('.') {
help.push('.')
}
body.push(roman(help));
let possible_values = opt.get_possible_values();
if !possible_values.is_empty()
&& !opt.is_hide_possible_values_set()
&& opt.get_id() != "pretty"
{
let possible_values_text = format!(
"\n\n[possible values: {}]",
possible_values
.iter()
.map(|v| v.get_name())
.collect::<Vec<_>>()
.join(", ")
);
body.push(roman(possible_values_text));
}
options_roff.control("TP", ["4"]);
options_roff.text(header);
options_roff.text(body);
}
let mut manpage = MAN_TEMPLATE.to_string();
let current_date = {
let (year, month, day) = DateTime::now_utc().date().to_calendar_date();
format!("{}-{:02}-{:02}", year, u8::from(month), day)
};
manpage = manpage.replace("{{date}}", &current_date);
manpage = manpage.replace("{{version}}", app.get_version().unwrap());
manpage = manpage.replace("{{request_items}}", request_items_roff.to_roff().trim());
manpage = manpage.replace("{{options}}", options_roff.to_roff().trim());
print!("{manpage}");
}

View File

@ -5,6 +5,7 @@ mod cli;
mod decoder; mod decoder;
mod download; mod download;
mod formatting; mod formatting;
mod generation;
mod middleware; mod middleware;
mod nested_json; mod nested_json;
mod netrc; mod netrc;
@ -101,6 +102,11 @@ fn main() {
} }
fn run(args: Cli) -> Result<i32> { fn run(args: Cli) -> Result<i32> {
if let Some(generate) = args.generate {
generation::generate(&args.bin_name, generate);
return Ok(0);
}
if args.curl { if args.curl {
to_curl::print_curl_translation(args)?; to_curl::print_curl_translation(args)?;
return Ok(0); return Ok(0);