diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d854788..f804870e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased ### Features -- Allow for completion and man page generation at runtime and add Nushell support +- Allow for completion and man page generation at runtime +- Add support for Nushell and Elvish shell completions ## [0.23.0] - 2024-10-12 ### Features diff --git a/Cargo.toml b/Cargo.toml index b469780c..36712a74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ brotli = { version = "3.3.0", default-features = false, features = ["std"] } chardetng = "0.1.15" clap = { version = "4.4", features = ["derive", "wrap_help", "string"] } clap_complete = "4.4" -clap_complete_nushell = "4.5.4" +clap_complete_nushell = "4.4" cookie_store = { version = "0.20.0", features = ["preserve_order"] } digest_auth = "0.3.0" dirs = "5.0" diff --git a/RELEASE-CHECKLIST.md b/RELEASE-CHECKLIST.md index 5cbcc4c3..b3989fcc 100644 --- a/RELEASE-CHECKLIST.md +++ b/RELEASE-CHECKLIST.md @@ -6,7 +6,7 @@ - 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. ```sh - cargo run --all-features -- --generate complete-all --generate-to completions && cargo run --all-features -- --generate man-pages --generate-to doc + cargo run --all-features -- --generate complete-all --generate-to completions && cargo run --all-features -- --generate man --generate-to doc ``` - Commit changes and push them to remote. - Add git tag e.g `git tag v0.9.0`. diff --git a/completions/_xh b/completions/_xh index c48c5897..348ba491 100644 --- a/completions/_xh +++ b/completions/_xh @@ -49,6 +49,8 @@ none\:"Disable both coloring and formatting"))' \ '--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: ' \ '--interface=[Bind to a network interface or local IP address]:NAME: ' \ +'--generate=[Generate shell completions or man pages]:KIND:(complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh complete-all man)' \ +'--generate-to=[Save generated shell completions or man pages to DIRECTORY instead of stdout]:DIRECTORY:_files' \ '-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]' \ '-f[Serialize data items from the command line as form fields]' \ @@ -137,10 +139,12 @@ none\:"Disable both coloring and formatting"))' \ '--no-ignore-stdin[]' \ '--no-curl[]' \ '--no-curl-long[]' \ +'--no-generate[]' \ +'--no-generate-to[]' \ '--no-help[]' \ '-V[Print version]' \ '--version[Print version]' \ -':raw_method_or_url -- The request URL, preceded by an optional HTTP method:' \ +'::raw_method_or_url -- The request URL, preceded by an optional HTTP method:' \ '*::raw_rest_args -- Optional key-value pairs to be included in the request.:' \ && ret=0 } diff --git a/completions/_xh.ps1 b/completions/_xh.ps1 index 6172f0ed..11415f95 100644 --- a/completions/_xh.ps1 +++ b/completions/_xh.ps1 @@ -52,6 +52,8 @@ Register-ArgumentCompleter -Native -CommandName 'xh' -ScriptBlock { [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('--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('--generate-to', '--generate-to', [CompletionResultType]::ParameterName, 'Save generated shell completions or man pages to DIRECTORY instead of stdout') [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('-f', '-f', [CompletionResultType]::ParameterName, 'Serialize data items from the command line as form fields') @@ -140,6 +142,8 @@ Register-ArgumentCompleter -Native -CommandName 'xh' -ScriptBlock { [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-long', '--no-curl-long', [CompletionResultType]::ParameterName, 'no-curl-long') + [CompletionResult]::new('--no-generate', '--no-generate', [CompletionResultType]::ParameterName, 'no-generate') + [CompletionResult]::new('--no-generate-to', '--no-generate-to', [CompletionResultType]::ParameterName, 'no-generate-to') [CompletionResult]::new('--no-help', '--no-help', [CompletionResultType]::ParameterName, 'no-help') [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') diff --git a/completions/xh.bash b/completions/xh.bash index cf26ec25..b1ced8d2 100644 --- a/completions/xh.bash +++ b/completions/xh.bash @@ -19,7 +19,7 @@ _xh() { case "${cmd}" in 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 --generate-to --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-generate-to --no-help --version [[METHOD] URL] [REQUEST_ITEM]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -149,6 +149,14 @@ _xh() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --generate) + COMPREPLY=($(compgen -W "complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh complete-all man" -- "${cur}")) + return 0 + ;; + --generate-to) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/completions/xh.elv b/completions/xh.elv new file mode 100644 index 00000000..189780d7 --- /dev/null +++ b/completions/xh.elv @@ -0,0 +1,150 @@ + +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 --generate-to 'Save generated shell completions or man pages to DIRECTORY instead of stdout' + 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-generate-to 'no-generate-to' + cand --no-help 'no-help' + cand -V 'Print version' + cand --version 'Print version' + } + ] + $completions[$command] +} diff --git a/completions/xh.fish b/completions/xh.fish index 882b707a..b493bafb 100644 --- a/completions/xh.fish +++ b/completions/xh.fish @@ -23,6 +23,8 @@ 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 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 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'',complete-all\t'',man\t''}" +complete -c xh -l generate-to -d 'Save generated shell completions or man pages to DIRECTORY instead of stdout' -r -F 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 -l multipart -d 'Like --form, but force a multipart/form-data request even without files' @@ -97,5 +99,7 @@ complete -c xh -l no-ipv6 complete -c xh -l no-ignore-stdin complete -c xh -l no-curl complete -c xh -l no-curl-long +complete -c xh -l no-generate +complete -c xh -l no-generate-to complete -c xh -l no-help complete -c xh -s V -l version -d 'Print version' diff --git a/completions/xh.nu b/completions/xh.nu new file mode 100644 index 00000000..9419d538 --- /dev/null +++ b/completions/xh.nu @@ -0,0 +1,140 @@ +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" "complete-all" "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 + --generate-to: string # Save generated shell completions or man pages to DIRECTORY instead of stdout + --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-generate-to + --no-help + --version(-V) # Print version + ] + +} + +export use completions * diff --git a/doc/xh.1 b/doc/xh.1 index 0bb1c8cb..067ce1e2 100644 --- a/doc/xh.1 +++ b/doc/xh.1 @@ -1,4 +1,4 @@ -.TH XH 1 2024-10-12 0.23.0 "User Commands" +.TH XH 1 2025-01-01 0.23.0 "User Commands" .SH NAME 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 Use the long versions of curl's flags. .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, complete\-all, man] +.TP 4 \fB\-\-help\fR Print help. .TP 4 diff --git a/src/cli.rs b/src/cli.rs index 44d991c1..594d374c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -372,17 +372,13 @@ Example: --print=Hb" pub generate: Option, /// Save generated shell completions or man pages to DIRECTORY instead of stdout. - #[clap(long, value_name = "DIRECTORY", requires = "generate")] + #[clap(long, value_name = "DIRECTORY", requires = "generate", hide = true)] pub generate_to: Option, /// Print help. #[clap(long, action = ArgAction::HelpShort)] pub help: Option, - /// Print long help. - #[clap(long, action = ArgAction::HelpLong)] - pub help_long: Option, - /// The request URL, preceded by an optional HTTP method. /// /// If the method is omitted, it will default to GET, or to POST @@ -513,9 +509,16 @@ impl Cli { return Ok(cli); } - let Some(mut raw_method_or_url) = cli.raw_method_or_url.clone() else { - unreachable!() - }; + 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 raw_url = match parse_method(&raw_method_or_url) { @@ -644,9 +647,12 @@ impl Cli { }) .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_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 { @@ -1178,7 +1184,7 @@ pub enum Generate { CompletePowershell, CompleteZsh, CompleteAll, - ManPages, + Man, } /// HTTPie uses Python's str.decode(). That one's very accepting of different spellings. @@ -1235,6 +1241,13 @@ fn parse_encoding(encoding: &str) -> anyhow::Result<&'static Encoding> { )) } +/// Based on the function used by clap to abort +fn safe_exit() -> ! { + let _ = std::io::stdout().lock().flush(); + let _ = std::io::stderr().lock().flush(); + std::process::exit(0); +} + fn long_version() -> &'static str { concat!(env!("CARGO_PKG_VERSION"), "\n", env!("XH_FEATURES")) } diff --git a/src/generation.rs b/src/generation.rs new file mode 100644 index 00000000..914d4659 --- /dev/null +++ b/src/generation.rs @@ -0,0 +1,260 @@ +use std::fs; +use std::io; +use std::path::Path; + +use anyhow::{Context, Result}; +use clap::ValueEnum as _; +use clap_complete::Generator; +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(args: Cli) -> Result<()> { + if let Some(generate) = args.generate { + let bin_name = args.bin_name.clone(); + let mut app = Cli::into_app(); + + let out_directory = args.generate_to.as_deref(); + match generate { + Generate::CompleteBash => { + generate_completions(&bin_name, &mut app, Shell::Bash, out_directory) + } + Generate::CompleteElvish => { + generate_completions(&bin_name, &mut app, Shell::Elvish, out_directory) + } + Generate::CompleteFish => { + generate_completions(&bin_name, &mut app, Shell::Fish, out_directory) + } + Generate::CompleteNushell => { + generate_completions(&bin_name, &mut app, Nushell, out_directory) + } + Generate::CompletePowershell => { + generate_completions(&bin_name, &mut app, Shell::PowerShell, out_directory) + } + Generate::CompleteZsh => { + generate_completions(&bin_name, &mut app, Shell::Zsh, out_directory) + } + Generate::CompleteAll => { + if let Some(out_directory) = out_directory { + generate_all_completions(&bin_name, &mut app, out_directory) + } else { + unreachable!() + } + } + Generate::Man => generate_manpages(&mut app, out_directory), + } + } else { + Ok(()) + } +} + +fn generate_completions( + bin_name: &str, + app: &mut clap::Command, + generator: G, + out_directory: Option<&Path>, +) -> Result<()> { + if let Some(out_directory) = out_directory { + clap_complete::generate_to(generator, app, bin_name, out_directory).with_context(|| { + format!( + "Failed to generate completions to directory: {}", + out_directory.display() + ) + })?; + } else { + clap_complete::generate(generator, app, bin_name, &mut io::stdout()); + } + Ok(()) +} + +fn generate_all_completions( + bin_name: &str, + app: &mut clap::Command, + out_directory: &Path, +) -> Result<()> { + for &shell in clap_complete::Shell::value_variants() { + clap_complete::generate_to(shell, app, bin_name, out_directory).with_context(|| { + format!( + "Failed to generate completions to directory: {}", + out_directory.display() + ) + })?; + } + + clap_complete::generate_to(Nushell, app, bin_name, out_directory).with_context(|| { + format!( + "Failed to generate completions to directory: {}", + out_directory.display() + ) + })?; + + Ok(()) +} + +fn generate_manpages(app: &mut clap::Command, out_directory: Option<&Path>) -> Result<()> { + 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::>(); + + 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::>() + .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}}", ¤t_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()); + + if let Some(out_directory) = out_directory { + fs::write(out_directory.join("xh.1"), manpage)?; + } else { + println!("{manpage}"); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 5d0308f3..de9d265d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod cli; mod decoder; mod download; mod formatting; +mod generation; mod middleware; mod nested_json; mod netrc; @@ -17,19 +18,16 @@ mod to_curl; mod utils; mod vendored; +use std::env; use std::fs::File; use std::io::{self, IsTerminal, Read}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process; use std::str::FromStr; use std::sync::Arc; -use std::{env, fs}; use anyhow::{anyhow, Context, Result}; -use clap::ValueEnum as _; -use clap_complete::{Generator, Shell}; -use clap_complete_nushell::Nushell; use cookie_store::{CookieStore, RawCookie}; use redirect::RedirectFollower; use reqwest::blocking::Client; @@ -42,7 +40,7 @@ use utils::reason_phrase; use crate::auth::{Auth, DigestAuthMiddleware}; use crate::buffer::Buffer; -use crate::cli::{Cli, FormatOptions, Generate, HttpVersion, Print, Proxy, Verify}; +use crate::cli::{Cli, FormatOptions, HttpVersion, Print, Proxy, Verify}; use crate::download::{download_file, get_file_size}; use crate::middleware::ClientWithMiddleware; use crate::printer::Printer; @@ -54,8 +52,6 @@ use crate::vendored::reqwest_cookie_store; #[cfg(not(any(feature = "native-tls", feature = "rustls")))] compile_error!("Either native-tls or rustls feature must be enabled!"); -const MAN_TEMPLATE: &str = include_str!("../doc/man-template.roff"); - fn get_user_agent() -> &'static str { if test_mode() { // Hard-coded user agent for the benefit of tests @@ -67,50 +63,6 @@ fn get_user_agent() -> &'static str { fn main() { let args = Cli::parse(); - let bin_name = args.bin_name.clone(); - - if let Some(generate) = args.generate { - let app = Cli::into_app(); - - let result = match generate { - Generate::CompleteBash - | Generate::CompleteElvish - | Generate::CompleteFish - | Generate::CompletePowershell - | Generate::CompleteZsh => { - let shell = match generate { - Generate::CompleteBash => Shell::Bash, - Generate::CompleteElvish => Shell::Elvish, - Generate::CompleteFish => Shell::Fish, - Generate::CompletePowershell => Shell::PowerShell, - Generate::CompleteZsh => Shell::Zsh, - _ => unreachable!(), - }; - - generate_completions(&bin_name, app, shell, args.generate_to) - } - Generate::CompleteNushell => { - generate_completions(&bin_name, app, Nushell, args.generate_to) - } - Generate::CompleteAll => { - if let Some(generate_to) = args.generate_to { - generate_all_completions(&bin_name, app, &generate_to) - } else { - unreachable!() - } - } - Generate::ManPages => generate_manpages(app, args.generate_to), - }; - - match result { - Ok(()) => process::exit(0), - Err(err) => { - log::debug!("{err:#?}"); - eprintln!("{bin_name}: error: {err:?}"); - process::exit(1); - } - } - } if args.debug { setup_backtraces(); @@ -122,6 +74,7 @@ fn main() { log::debug!("{args:#?}"); let native_tls = args.native_tls; + let bin_name = args.bin_name.clone(); match run(args) { Ok(exit_code) => { @@ -149,6 +102,11 @@ fn main() { } fn run(args: Cli) -> Result { + if args.generate.is_some() { + generation::generate(args)?; + return Ok(0); + } + if args.curl { to_curl::print_curl_translation(args)?; return Ok(0); @@ -701,214 +659,3 @@ fn setup_backtraces() { std::env::set_var("RUST_BACKTRACE", "1"); } } - -fn generate_completions( - bin_name: &str, - mut app: clap::Command, - generator: G, - out_directory: Option>, -) -> Result<()> { - if let Some(out_directory) = out_directory { - clap_complete::generate_to(generator, &mut app, bin_name, out_directory.as_ref()) - .with_context(|| { - format!( - "Failed to generate completions to directory: {}", - out_directory.as_ref().display() - ) - })?; - } else { - clap_complete::generate(generator, &mut app, bin_name, &mut io::stdout()); - } - Ok(()) -} - -fn generate_all_completions( - bin_name: &str, - mut app: clap::Command, - out_directory: impl AsRef, -) -> Result<()> { - for &shell in clap_complete::Shell::value_variants() { - clap_complete::generate_to(shell, &mut app, bin_name, out_directory.as_ref()) - .with_context(|| { - format!( - "Failed to generate completions to directory: {}", - out_directory.as_ref().display() - ) - })?; - } - - clap_complete::generate_to(Nushell, &mut app, bin_name, out_directory.as_ref()).with_context( - || { - format!( - "Failed to generate completions to directory: {}", - out_directory.as_ref().display() - ) - }, - )?; - - Ok(()) -} - -fn generate_manpages(app: clap::Command, out_directory: Option>) -> Result<()> { - 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::>(); - - 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::>() - .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}}", ¤t_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()); - - if let Some(out_directory) = out_directory { - fs::write(out_directory.as_ref().join("xh.1"), manpage)?; - } else { - println!("{manpage}"); - } - - Ok(()) -}