├── CNAME ├── .codespellignore ├── assets ├── halp-demo.gif ├── halp-logo.png ├── halp-example1.gif └── halp-example2.gif ├── .github ├── FUNDING.yml ├── CODEOWNERS ├── dependabot.yml ├── mergify.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docker.yml │ ├── cd.yml │ └── ci.yml ├── _config.yml ├── .gitignore ├── rustfmt.toml ├── src ├── helper │ ├── mod.rs │ ├── docs │ │ ├── man.rs │ │ ├── cheatsheets.rs │ │ ├── eg.rs │ │ ├── cheat_sh.rs │ │ └── mod.rs │ ├── args │ │ ├── common.rs │ │ └── mod.rs │ └── tty.rs ├── bin │ ├── test.rs │ ├── completions.rs │ └── mangen.rs ├── main.rs ├── lib.rs ├── error.rs ├── cli.rs └── config.rs ├── .editorconfig ├── .dockerignore ├── codecov.yml ├── SECURITY.md ├── config └── halp.toml ├── committed.toml ├── deny.toml ├── RELEASE.md ├── Dockerfile ├── scripts └── release.sh ├── LICENSE-MIT ├── tests └── integration.rs ├── Cargo.toml ├── cliff.toml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── LICENSE-APACHE ├── README.md └── Cargo.lock /CNAME: -------------------------------------------------------------------------------- 1 | halp.cli.rs -------------------------------------------------------------------------------- /.codespellignore: -------------------------------------------------------------------------------- 1 | halp 2 | crate 3 | ser 4 | -------------------------------------------------------------------------------- /assets/halp-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orhun/halp/HEAD/assets/halp-demo.gif -------------------------------------------------------------------------------- /assets/halp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orhun/halp/HEAD/assets/halp-logo.png -------------------------------------------------------------------------------- /assets/halp-example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orhun/halp/HEAD/assets/halp-example1.gif -------------------------------------------------------------------------------- /assets/halp-example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orhun/halp/HEAD/assets/halp-example2.gif -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: orhun 2 | patreon: orhunp 3 | custom: ["https://www.buymeacoffee.com/orhun"] 4 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # configuration for https://jekyllrb.com/docs/configuration 2 | 3 | theme: jekyll-theme-slate 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files and executables 2 | /target/ 3 | 4 | # Backup files generated by rustfmt 5 | **/*.rs.bk 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://rust-lang.github.io/rustfmt/ 2 | 3 | edition = "2021" 4 | max_width = 100 5 | use_field_init_shorthand = true 6 | use_try_shorthand = true 7 | -------------------------------------------------------------------------------- /src/helper/mod.rs: -------------------------------------------------------------------------------- 1 | /// Command-line argument helper. 2 | pub mod args; 3 | 4 | /// Documentation/usage helper. 5 | pub mod docs; 6 | 7 | /// Command executor for TTY. 8 | pub mod tty; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # configuration for https://editorconfig.org 2 | 3 | root = true 4 | 5 | [{*.sh, *.rs}] 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.yml] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ 2 | # for more info about CODEOWNERS file 3 | 4 | # It uses the same pattern rule for gitignore file 5 | # https://git-scm.com/docs/gitignore#_pattern_format 6 | 7 | # Core 8 | * @orhun 9 | -------------------------------------------------------------------------------- /src/bin/test.rs: -------------------------------------------------------------------------------- 1 | use clap::{ColorChoice, Parser}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(bin_name = "test", version, disable_colored_help = true, color = ColorChoice::Never)] 5 | struct Args {} 6 | 7 | fn main() { 8 | Args::parse(); 9 | } 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | /.git/ 3 | /.github/ 4 | /assets/ 5 | /config/ 6 | /scripts/ 7 | 8 | # Files 9 | .gitignore 10 | *.md 11 | LICENSE-* 12 | committed.toml 13 | cliff.toml 14 | deny.toml 15 | CNAME 16 | rustfmt.toml 17 | _config.yml 18 | codecov.yml 19 | .editorconfig 20 | .codespellignore 21 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # configuration for https://docs.codecov.com/docs/codecovyml-reference 2 | 3 | coverage: 4 | range: 80..100 5 | round: up 6 | precision: 2 7 | status: 8 | project: 9 | default: 10 | target: auto 11 | threshold: 2% 12 | branches: 13 | - main 14 | if_ci_failed: error 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use halp::cli::CliArgs; 3 | use std::io; 4 | use std::process; 5 | 6 | fn main() { 7 | let args = CliArgs::parse(); 8 | let mut stdout = io::stdout(); 9 | match halp::run(args, &mut stdout) { 10 | Ok(_) => process::exit(0), 11 | Err(e) => { 12 | eprintln!("{e}"); 13 | process::exit(1) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for Cargo 4 | - package-ecosystem: cargo 5 | directory: "/" 6 | schedule: 7 | interval: monthly 8 | open-pull-requests-limit: 10 9 | 10 | # Maintain dependencies for GitHub Actions 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: monthly 15 | open-pull-requests-limit: 10 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions are supported with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 0.1.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please use the [GitHub Security Advisories](https://github.com/orhun/halp/security/advisories/new) feature to report vulnerabilities. 14 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot[bot] 5 | actions: 6 | merge: 7 | method: squash 8 | 9 | - name: Automatic update to the main branch for pull requests 10 | conditions: 11 | - -conflict # skip PRs with conflicts 12 | - -draft # skip GH draft PRs 13 | - -author=dependabot[bot] # skip dependabot PRs 14 | actions: 15 | update: 16 | -------------------------------------------------------------------------------- /src/helper/docs/man.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use std::process::Command; 3 | 4 | /// Runs the manual page command. 5 | pub fn show_man_page(man_cmd: &str, cmd: &str) -> Result<()> { 6 | let command = format!("{} {}", man_cmd, cmd); 7 | let mut process = if cfg!(target_os = "windows") { 8 | Command::new("cmd").args(["/C", &command]).spawn() 9 | } else { 10 | Command::new("sh").args(["-c", &command]).spawn() 11 | }?; 12 | process.wait()?; 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /config/halp.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/orhun/halp 2 | 3 | # check the version flag 4 | check_version = true 5 | # check the help flag 6 | check_help = true 7 | # arguments to check 8 | check = [["-v", "-V", "--version"], ["-h", "--help", "help", "-H"]] 9 | # command to run for manual pages 10 | man_command = "man" 11 | # pager to use for command outputs 12 | pager_command = "less -R" 13 | # Cheat.sh URL 14 | cheat_sh_url = "https://cheat.sh" 15 | # Timeout for the commands 16 | timeout = 5 17 | -------------------------------------------------------------------------------- /committed.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/crate-ci/committed 2 | 3 | # https://www.conventionalcommits.org 4 | style="conventional" 5 | # allow merge commits 6 | merge_commit = true 7 | # subject is not required to be capitalized 8 | subject_capitalized = false 9 | # subject should start with an imperative verb 10 | imperative_subject = true 11 | # subject should not end with a punctuation 12 | subject_not_punctuated = true 13 | # disable line length 14 | line_length = 0 15 | # disable subject length 16 | subject_length = 0 17 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/EmbarkStudios/cargo-deny 2 | 3 | [licenses] 4 | default = "deny" 5 | unlicensed = "deny" 6 | copyleft = "deny" 7 | confidence-threshold = 0.8 8 | allow = [ 9 | "MIT", 10 | "Apache-2.0", 11 | "Unicode-DFS-2016", 12 | "MPL-2.0", 13 | "ISC", 14 | "BSD-3-Clause", 15 | "OpenSSL" 16 | ] 17 | 18 | [[licenses.clarify]] 19 | name = "ring" 20 | expression = "MIT AND ISC AND OpenSSL" 21 | license-files = [ 22 | { path = "LICENSE", hash = 0xbd0eed23 } 23 | ] 24 | 25 | [sources] 26 | unknown-registry = "deny" 27 | unknown-git = "warn" 28 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 29 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Creating a Release 2 | 3 | [GitHub](https://github.com/orhun/halp/releases) and [crates.io](https://crates.io/crates/halp) releases are automated via [GitHub actions](.github/workflows/cd.yml) and triggered by pushing a tag. 4 | 5 | 1. Run the [release script](./scripts/release.sh): `./scripts/release.sh v[X.Y.Z]` (requires [git-cliff](https://github.com/orhun/git-cliff) for changelog generation) 6 | 2. Push the changes: `git push` 7 | 3. Check if [Continuous Integration](https://github.com/orhun/halp/actions) workflow is completed successfully. 8 | 4. Push the tags: `git push --tags` 9 | 5. Wait for [Continuous Deployment](https://github.com/orhun/halp/actions) workflow to finish. 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 🐙 3 | about: Make a suggestion for the project 4 | title: "" 5 | labels: "enhancement" 6 | assignees: "orhun" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | **Describe alternatives you've considered** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:0.1.66-rust-1.78-buster AS chef 2 | WORKDIR /app 3 | 4 | FROM chef AS planner 5 | COPY . . 6 | RUN cargo chef prepare --recipe-path recipe.json 7 | 8 | FROM chef AS builder 9 | COPY --from=planner /app/recipe.json recipe.json 10 | ENV CARGO_NET_GIT_FETCH_WITH_CLI=true 11 | RUN cargo chef cook --release --recipe-path recipe.json 12 | COPY . . 13 | RUN cargo build --release --locked \ 14 | && rm -f target/release/deps/halp* 15 | 16 | FROM debian:buster-slim as runner 17 | RUN sed -i '/path\-exclude\s\/usr\/share\/man\/\*/d' /etc/dpkg/dpkg.cfg.d/docker 18 | RUN apt-get update && \ 19 | apt-get install -y --no-install-recommends --allow-unauthenticated \ 20 | less \ 21 | man-db \ 22 | manpages && \ 23 | apt-get install -y --reinstall coreutils && \ 24 | apt-get clean && \ 25 | rm -rf /var/lib/apt/lists/* 26 | RUN mandb 27 | COPY --from=builder /app/target/release/halp /usr/local/bin 28 | WORKDIR /app 29 | ENTRYPOINT ["halp"] 30 | -------------------------------------------------------------------------------- /src/bin/completions.rs: -------------------------------------------------------------------------------- 1 | use clap::{CommandFactory, ValueEnum}; 2 | use clap_complete::Shell; 3 | use halp::cli::CliArgs; 4 | use std::env; 5 | use std::io::Result; 6 | 7 | /// Shell completions can be created with: 8 | /// 9 | /// ```sh 10 | /// cargo run --bin halp-completions 11 | /// ``` 12 | /// 13 | /// in a directory specified by the environment variable OUT_DIR. 14 | /// See 15 | fn main() -> Result<()> { 16 | let out_dir = env::var("OUT_DIR").expect("OUT_DIR is not set"); 17 | let mut app = CliArgs::command(); 18 | for &shell in Shell::value_variants() { 19 | clap_complete::generate_to(shell, &mut app, env!("CARGO_PKG_NAME"), &out_dir)?; 20 | } 21 | println!("Completion scripts are generated in {out_dir:?}"); 22 | Ok(()) 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | #[test] 29 | fn generate_completions() -> Result<()> { 30 | main() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ -z "$1" ]; then 6 | echo "Please provide a tag." 7 | echo "Usage: ./release.sh v[X.Y.Z]" 8 | exit 9 | fi 10 | 11 | echo "Preparing $1..." 12 | # update the version 13 | msg="# bumped by release.sh" 14 | sed -E -i "s/^version = .* $msg$/version = \"${1#v}\" $msg/" Cargo.toml 15 | cargo build 16 | # update the changelog 17 | git cliff --tag "$1" >CHANGELOG.md 18 | git add -A 19 | git commit -m "chore(release): prepare for $1" 20 | git show 21 | # generate a changelog for the tag message 22 | changelog=$(git cliff --tag "$1" --unreleased --strip all | sed -e '/^#/d' -e '/^$/d') 23 | # create a signed tag 24 | # https://keyserver.ubuntu.com/pks/lookup?search=0xFB41AE0358378256&op=vindex 25 | git -c user.name="halp-cli" \ 26 | -c user.email="halp-cli@proton.me" \ 27 | -c user.signingkey="B2DA025C21DD8374960CE8F4FB41AE0358378256" \ 28 | tag -f -s -a "$1" -m "Release $1" -m "$changelog" 29 | git tag -v "$1" 30 | echo "Done!" 31 | echo "Now push the commit (git push) and the tag (git push --tags)." 32 | -------------------------------------------------------------------------------- /src/bin/mangen.rs: -------------------------------------------------------------------------------- 1 | use clap::CommandFactory; 2 | use clap_mangen::Man; 3 | use halp::cli::CliArgs; 4 | use std::env; 5 | use std::fs; 6 | use std::io::Result; 7 | use std::path::PathBuf; 8 | 9 | /// Man page can be created with: 10 | /// 11 | /// ```sh 12 | /// cargo run --bin halp-mangen 13 | /// ```` 14 | /// 15 | /// in a directory specified by the environment variable OUT_DIR. 16 | /// See 17 | fn main() -> Result<()> { 18 | let out_dir = env::var("OUT_DIR").expect("OUT_DIR is not set"); 19 | let out_path = PathBuf::from(out_dir).join(concat!(env!("CARGO_PKG_NAME"), ".1")); 20 | let app = CliArgs::command(); 21 | let man = Man::new(app); 22 | let mut buffer = Vec::::new(); 23 | man.render(&mut buffer)?; 24 | fs::write(&out_path, buffer)?; 25 | println!("Man page is generated at {out_path:?}"); 26 | Ok(()) 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use super::*; 32 | #[test] 33 | fn generate_manpage() -> Result<()> { 34 | main() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Report a bug to help us improve 4 | title: "" 5 | labels: "bug" 6 | assignees: "orhun" 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 12 | 13 | **To reproduce** 14 | 15 | 22 | 23 | **Expected behavior** 24 | 25 | 26 | 27 | **Screenshots / Logs** 28 | 29 | 30 | 31 | **Software information** 32 | 33 | 34 | 35 | - Operating system: 36 | - Rust version: 37 | - Project version: 38 | 39 | **Additional context** 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-2024 Orhun Parmaksız 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/helper/docs/cheatsheets.rs: -------------------------------------------------------------------------------- 1 | use crate::helper::docs::HelpProvider; 2 | use ureq::{AgentBuilder, Request}; 3 | 4 | /// The default cheatsheets provider URL. 5 | pub const DEFAULT_CHEATSHEETS_PROVIDER: &str = 6 | "https://raw.githubusercontent.com/cheat/cheatsheets/master"; 7 | 8 | /// The `cheatsheets` provider 9 | pub struct Cheatsheets; 10 | 11 | impl HelpProvider for Cheatsheets { 12 | fn url(&self) -> &'static str { 13 | DEFAULT_CHEATSHEETS_PROVIDER 14 | } 15 | 16 | fn build_request(&self, cmd: &str, url: &str) -> Request { 17 | AgentBuilder::new().build().get(&format!("{}/{}", url, cmd)) 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::*; 24 | use crate::error::Result; 25 | 26 | #[test] 27 | fn test_fetch_cheatsheets() -> Result<()> { 28 | let output = Cheatsheets.fetch("ls", &None)?; 29 | assert!(output.contains( 30 | r##"# To display everything in , including hidden files: 31 | ls -a 32 | "## 33 | )); 34 | assert!(output.contains( 35 | r##"# To display directories only, include hidden: 36 | ls -d .*/ */ 37 | "## 38 | )); 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/helper/docs/eg.rs: -------------------------------------------------------------------------------- 1 | use crate::helper::docs::HelpProvider; 2 | use ureq::{AgentBuilder, Request}; 3 | 4 | /// EG page provider URL. 5 | pub const DEFAULT_EG_PAGES_PROVIDER: &str = 6 | "https://raw.githubusercontent.com/srsudar/eg/master/eg/examples"; 7 | 8 | /// The `eg` pages provider 9 | pub struct Eg; 10 | 11 | impl HelpProvider for Eg { 12 | fn url(&self) -> &'static str { 13 | DEFAULT_EG_PAGES_PROVIDER 14 | } 15 | 16 | fn build_request(&self, cmd: &str, url: &str) -> Request { 17 | AgentBuilder::new() 18 | .build() 19 | .get(&format!("{}/{}.md", url, cmd)) 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use super::*; 26 | use crate::error::Result; 27 | 28 | #[test] 29 | fn test_eg_page_fetch() -> Result<()> { 30 | let output = Eg.fetch("ls", &None)?; 31 | assert!(output.contains("show contents of current directory")); 32 | assert!(output.contains("ls -alh")); 33 | assert!(output.contains( 34 | r#"`ls` is often aliased to make the defaults a bit more useful. Here are three 35 | basic aliases. The second two can be remembered by "list long" and "list all": 36 | "# 37 | )); 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use halp::cli::CliArgs; 2 | use halp::error::Result; 3 | use pretty_assertions::assert_eq; 4 | use std::path::PathBuf; 5 | 6 | /// Test binary. 7 | const BIN: &str = env!("CARGO_BIN_EXE_halp-test"); 8 | 9 | #[test] 10 | fn get_argument_help() -> Result<()> { 11 | let args = CliArgs { 12 | cmd: Some(BIN.to_string()), 13 | timeout: Some(10), 14 | config: Some( 15 | PathBuf::from(env!("CARGO_MANIFEST_DIR")) 16 | .join("config") 17 | .join(concat!(env!("CARGO_PKG_NAME"), ".toml")), 18 | ), 19 | ..Default::default() 20 | }; 21 | let mut output = Vec::new(); 22 | halp::run(args, &mut output)?; 23 | println!("{}", String::from_utf8_lossy(&output)); 24 | assert_eq!( 25 | r"(°ロ°) checking 'test -v' 26 | (×﹏×) fail '-v' argument not found. 27 | (°ロ°) checking 'test -V' 28 | \(^ヮ^)/ success '-V' argument found! 29 | --- 30 | halp 0.1.0 31 | --- 32 | (°ロ°) checking 'test -h' 33 | \(^ヮ^)/ success '-h' argument found! 34 | --- 35 | Usage: test 36 | 37 | Options: 38 | -h, --help Print help 39 | -V, --version Print version 40 | ---", 41 | String::from_utf8_lossy(&output) 42 | .replace('\r', "") 43 | .replace(BIN, "test") 44 | .replace(env!("CARGO_PKG_VERSION"), "0.1.0") 45 | .trim() 46 | ); 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "halp" 3 | version = "0.2.0" # bumped by release.sh 4 | description = "A CLI tool to get help with CLI tools 🐙" 5 | authors = ["Orhun Parmaksız "] 6 | license = "MIT OR Apache-2.0" 7 | readme = "README.md" 8 | homepage = "https://github.com/orhun/halp" 9 | repository = "https://github.com/orhun/halp" 10 | categories = ["command-line-utilities"] 11 | keywords = ["cli", "help"] 12 | default-run = "halp" 13 | edition = "2021" 14 | rust-version = "1.74.1" 15 | 16 | [[bin]] 17 | name = "halp-completions" 18 | path = "src/bin/completions.rs" 19 | 20 | [[bin]] 21 | name = "halp-mangen" 22 | path = "src/bin/mangen.rs" 23 | 24 | [[bin]] 25 | name = "halp-test" 26 | path = "src/bin/test.rs" 27 | 28 | [dependencies] 29 | clap = { version = "4.5.53", features = ["derive", "env", "wrap_help"] } 30 | clap_complete = "4.5.61" 31 | clap_mangen = "0.2.31" 32 | colored = "3.0.0" 33 | console = "0.16.1" 34 | dialoguer = { version = "0.12.0", default-features = false } 35 | dirs = "6.0.0" 36 | process_control = "5.2.0" 37 | serde = { version = "1.0.219", features = ["derive"] } 38 | thiserror = "2.0.17" 39 | toml = "0.9.8" 40 | ureq = "3.1.4" 41 | 42 | [dev-dependencies] 43 | pretty_assertions = "1.4.1" 44 | 45 | [profile.dev] 46 | opt-level = 0 47 | debug = true 48 | panic = "abort" 49 | 50 | [profile.test] 51 | opt-level = 0 52 | debug = true 53 | 54 | [profile.release] 55 | opt-level = 3 56 | debug = false 57 | panic = "unwind" 58 | lto = true 59 | codegen-units = 1 60 | strip = true 61 | 62 | [profile.bench] 63 | opt-level = 3 64 | debug = false 65 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Motivation and Context 8 | 9 | 10 | 11 | 12 | ## How Has This Been Tested? 13 | 14 | 15 | 16 | 17 | 18 | ## Screenshots / Logs (if applicable) 19 | 20 | ## Types of Changes 21 | 22 | 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | - [ ] Documentation (no code change) 28 | - [ ] Refactor (refactoring production code) 29 | - [ ] Other 30 | 31 | ## Checklist: 32 | 33 | 34 | 35 | - [ ] My code follows the code style of this project. 36 | - [ ] I have updated the documentation accordingly. 37 | - [ ] I have formatted the code with [rustfmt](https://github.com/rust-lang/rustfmt). 38 | - [ ] I checked the lints with [clippy](https://github.com/rust-lang/rust-clippy). 39 | - [ ] I have added tests to cover my changes. 40 | - [ ] All new and existing tests passed. 41 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A CLI tool to get help with CLI tools 🐙 2 | 3 | #![warn(missing_docs, clippy::unwrap_used)] 4 | 5 | /// Help implementation. 6 | pub mod helper; 7 | 8 | /// Command-line argument parser. 9 | pub mod cli; 10 | 11 | /// Error handling implementation. 12 | pub mod error; 13 | 14 | /// Configuration. 15 | pub mod config; 16 | 17 | use crate::cli::CliArgs; 18 | use crate::error::Result; 19 | use crate::helper::args::FAIL_EMOTICON; 20 | use cli::CliCommands; 21 | use colored::*; 22 | use config::Config; 23 | use helper::args::get_args_help; 24 | use helper::docs::get_docs_help; 25 | use std::io::Write; 26 | 27 | /// Runs `halp`. 28 | pub fn run(cli_args: CliArgs, output: &mut Output) -> Result<()> { 29 | let mut config = if let Some(config_file) = cli_args 30 | .config 31 | .to_owned() 32 | .or_else(Config::get_default_location) 33 | { 34 | Config::parse(&config_file)? 35 | } else { 36 | let config = Config::default(); 37 | if let Err(e) = config.write(output) { 38 | eprintln!( 39 | "{} {}: {}", 40 | FAIL_EMOTICON.magenta(), 41 | "failed to write default config".red().bold(), 42 | e.to_string().white().italic() 43 | ); 44 | } 45 | config 46 | }; 47 | cli_args.update_config(&mut config); 48 | if let Some(ref cmd) = cli_args.cmd { 49 | get_args_help(cmd, &config, cli_args.verbose, output)?; 50 | } else if let Some(CliCommands::Plz { ref cmd, .. }) = cli_args.subcommand { 51 | get_docs_help(cmd, &config, output)?; 52 | } 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /src/helper/docs/cheat_sh.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use crate::helper::docs::HelpProvider; 3 | use ureq::{AgentBuilder, Request}; 4 | 5 | /// Default cheat sheet provider URL. 6 | pub const DEFAULT_CHEAT_SHEET_PROVIDER: &str = "https://cheat.sh"; 7 | 8 | /// User agent for the cheat sheet provider. 9 | /// 10 | /// See 11 | const CHEAT_SHEET_USER_AGENT: &str = "fetch"; 12 | 13 | /// The `cheat.sh` provider 14 | pub struct CheatDotSh; 15 | 16 | impl HelpProvider for CheatDotSh { 17 | fn url(&self) -> &'static str { 18 | DEFAULT_CHEAT_SHEET_PROVIDER 19 | } 20 | 21 | fn build_request(&self, cmd: &str, url: &str) -> Request { 22 | AgentBuilder::new() 23 | .user_agent(CHEAT_SHEET_USER_AGENT) 24 | .build() 25 | .get(&format!("{}/{}", url, cmd)) 26 | } 27 | 28 | fn fetch(&self, cmd: &str, custom_url: &Option) -> Result { 29 | let response = self._fetch(cmd, custom_url); 30 | if let Ok(page) = &response { 31 | if page.starts_with("Unknown topic.") { 32 | return Err(Error::ProviderError(page.to_owned())); 33 | } 34 | } 35 | response 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | 43 | #[test] 44 | fn test_fetch_cheat_sheet() -> Result<()> { 45 | let output = CheatDotSh.fetch("ls", &None)?; 46 | assert!(output.contains( 47 | "# To display all files, along with the size (with unit suffixes) and timestamp:" 48 | )); 49 | assert!(output.contains( 50 | "# Long format list with size displayed using human-readable units (KiB, MiB, GiB):" 51 | )); 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/helper/args/common.rs: -------------------------------------------------------------------------------- 1 | /// Display trait for an argument. 2 | pub trait ArgDisplay { 3 | /// Returns the string representation. 4 | fn as_str(&self) -> &'static str; 5 | } 6 | 7 | macro_rules! generate_argument { 8 | ($name: ident, 9 | $($variant: ident => $str_repr: expr,)+ 10 | ) => { 11 | /// Argument. 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 13 | pub enum $name { 14 | $( 15 | /// Variant of the argument. 16 | $variant 17 | ),+ 18 | } 19 | 20 | impl $name { 21 | /// Returns the variants. 22 | pub fn variants() -> Vec> { 23 | vec![$(Box::new(Self::$variant),)+] 24 | } 25 | 26 | } 27 | 28 | impl ArgDisplay for $name { 29 | fn as_str(&self) -> &'static str { 30 | match self { 31 | $(Self::$variant => $str_repr,)+ 32 | } 33 | } 34 | } 35 | }; 36 | } 37 | 38 | generate_argument!( 39 | VersionArg, 40 | Version => "-v", 41 | CapitalVersion => "-V", 42 | LongVersion => "--version", 43 | SubcommandVersion => "version", 44 | ); 45 | 46 | generate_argument!( 47 | HelpArg, 48 | Help => "-h", 49 | LongHelp => "--help", 50 | SubcommandHelp => "help", 51 | CapitalHelp => "-H", 52 | ); 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::*; 57 | use pretty_assertions::assert_eq; 58 | 59 | generate_argument!( 60 | Test, 61 | One => "one", 62 | Two => "two", 63 | Three => "three", 64 | ); 65 | 66 | #[test] 67 | fn test_enum_generation() { 68 | assert_eq!( 69 | vec!["one", "two", "three"], 70 | Test::variants() 71 | .iter() 72 | .map(|v| v.as_str()) 73 | .collect::>() 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error as ThisError; 2 | 3 | /// Custom error type. 4 | #[derive(Debug, ThisError)] 5 | pub enum Error { 6 | /// Error that may occur during I/O operations. 7 | #[error("IO error: `{0}`")] 8 | IoError(#[from] std::io::Error), 9 | /// Error that might occur while parsing TOML. 10 | #[error("TOML parsing error: `{0}`")] 11 | TomlError(#[from] toml::de::Error), 12 | /// Error that might occur while processing/sending requests. 13 | #[error("Request error: `{0}`")] 14 | RequestError(#[from] Box), 15 | /// Error that might occur when running on unsupported platforms. 16 | #[error("Unsupported platform.")] 17 | UnsupportedPlatformError, 18 | /// Error that might occur while serializing the configuration into TOML. 19 | #[error("TOML serialization error: `{0}`")] 20 | TomlSerializeError(#[from] toml::ser::Error), 21 | /// Error that might occur when tray to get help from an external provider. 22 | #[error("External help provider error: `{0}`")] 23 | ProviderError(String), 24 | /// Error that might occur during showing dialogues. 25 | #[error("Dialogue error: `{0}`")] 26 | DialogueError(#[from] dialoguer::Error), 27 | /// Error that might occur when the command times out. 28 | #[error("Command timed out after {0} seconds x_x")] 29 | TimeoutError(u64), 30 | } 31 | 32 | /// Type alias for the standard [`Result`] type. 33 | pub type Result = std::result::Result; 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use pretty_assertions::assert_eq; 39 | use std::io::{Error as IoError, ErrorKind}; 40 | 41 | #[test] 42 | fn test_error() { 43 | let message = "your computer is on fire!"; 44 | let error = Error::from(IoError::new(ErrorKind::Other, message)); 45 | assert_eq!(format!("IO error: `{message}`"), error.to_string()); 46 | assert_eq!( 47 | format!("\"IO error: `{message}`\""), 48 | format!("{:?}", error.to_string()) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Automated Builds 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*.*.*" 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: "0 0 * * 0" 14 | 15 | jobs: 16 | docker: 17 | name: Docker Build and Push 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Docker meta 24 | id: meta 25 | uses: docker/metadata-action@v5 26 | with: 27 | images: | 28 | orhunp/halp 29 | ghcr.io/${{ github.repository_owner }}/halp 30 | tags: | 31 | type=schedule 32 | type=ref,event=branch 33 | type=ref,event=pr 34 | type=sha 35 | type=raw,value=latest 36 | type=semver,pattern={{version}} 37 | 38 | - name: Set up QEMU 39 | uses: docker/setup-qemu-action@v3 40 | with: 41 | platforms: arm64 42 | 43 | - name: Set up Docker Buildx 44 | id: buildx 45 | uses: docker/setup-buildx-action@v3 46 | 47 | - name: Cache Docker layers 48 | uses: actions/cache@v4 49 | with: 50 | path: /tmp/.buildx-cache 51 | key: ${{ runner.os }}-buildx-${{ github.sha }} 52 | restore-keys: | 53 | ${{ runner.os }}-buildx- 54 | 55 | - name: Login to Docker Hub 56 | if: github.event_name != 'pull_request' 57 | uses: docker/login-action@v3 58 | with: 59 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 60 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 61 | 62 | - name: Login to GHCR 63 | if: github.event_name != 'pull_request' 64 | uses: docker/login-action@v3 65 | with: 66 | registry: ghcr.io 67 | username: ${{ github.repository_owner }} 68 | password: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: Build and push 71 | id: docker_build 72 | uses: docker/build-push-action@v6 73 | with: 74 | context: ./ 75 | file: ./Dockerfile 76 | platforms: linux/amd64,linux/arm64 77 | builder: ${{ steps.buildx.outputs.name }} 78 | push: ${{ github.event_name != 'pull_request' }} 79 | tags: ${{ steps.meta.outputs.tags }} 80 | sbom: true 81 | provenance: true 82 | labels: ${{ steps.meta.outputs.labels }} 83 | cache-from: type=local,src=/tmp/.buildx-cache 84 | cache-to: type=local,dest=/tmp/.buildx-cache 85 | 86 | - name: Scan the image 87 | uses: anchore/sbom-action@v0 88 | with: 89 | image: ghcr.io/${{ github.repository_owner }}/halp 90 | 91 | - name: Image digest 92 | run: echo ${{ steps.docker_build.outputs.digest }} 93 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration for https://github.com/orhun/git-cliff 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog\n 7 | All notable changes to this project will be documented in this file.\n 8 | """ 9 | # template for the changelog body 10 | # https://tera.netlify.app/docs/#introduction 11 | body = """ 12 | {% if version %}\ 13 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 14 | {% else %}\ 15 | ## [unreleased] 16 | {% endif %}\ 17 | {% for group, commits in commits | group_by(attribute="group") %} 18 | ### {{ group | striptags | trim | upper_first }} 19 | {% for commit in commits 20 | | filter(attribute="scope") 21 | | sort(attribute="scope") %} 22 | - *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %} 23 | {%- endfor -%} 24 | {% raw %}\n{% endraw %}\ 25 | {%- for commit in commits %} 26 | {%- if commit.scope -%} 27 | {% else -%} 28 | - *(no category)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %} 29 | {% endif -%} 30 | {% endfor -%} 31 | {% endfor %}\n 32 | """ 33 | # remove the leading and trailing whitespace from the template 34 | trim = true 35 | # changelog footer 36 | footer = """ 37 | 38 | """ 39 | 40 | [git] 41 | # parse the commits based on https://www.conventionalcommits.org 42 | conventional_commits = true 43 | # filter out the commits that are not conventional 44 | filter_unconventional = true 45 | # process each line of a commit as an individual commit 46 | split_commits = false 47 | # regex for preprocessing the commit messages 48 | commit_preprocessors = [ 49 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/halp/issues/${2}))" }, 50 | ] 51 | # regex for parsing and grouping commits 52 | commit_parsers = [ 53 | { message = "^feat", group = "🐙 Features" }, 54 | { message = "^fix", group = "🐛 Bug Fixes" }, 55 | { message = "^doc", group = "📚 Documentation" }, 56 | { message = "^perf", group = "⚡ Performance" }, 57 | { message = "^refactor", group = "🚜 Refactor" }, 58 | { message = "^style", group = "🎨 Styling" }, 59 | { message = "^test", group = "🧪 Testing" }, 60 | { message = "^chore\\(release\\): prepare for", skip = true }, 61 | { message = "^chore\\(deps\\)", skip = true }, 62 | { message = "^chore\\(pr\\)", skip = true }, 63 | { message = "^chore\\(pull\\)", skip = true }, 64 | { message = "^chore", group = "⚙️ Miscellaneous Tasks" }, 65 | { body = ".*security", group = "🛡️ Security" }, 66 | ] 67 | # protect breaking changes from being skipped due to matching a skipping commit_parser 68 | protect_breaking_commits = false 69 | # filter out the commits that are not matched by commit parsers 70 | filter_commits = false 71 | # glob pattern for matching git tags 72 | tag_pattern = "v[0-9]*" 73 | # regex for skipping tags 74 | skip_tags = "v0.1.0-rc.1" 75 | # regex for ignoring tags 76 | ignore_tags = "" 77 | # sort the tags topologically 78 | topo_order = false 79 | # sort the commits inside sections by oldest/newest order 80 | sort_commits = "newest" 81 | -------------------------------------------------------------------------------- /src/helper/tty.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use std::process::Command as OsCommand; 3 | 4 | /// Default shell to use. 5 | const DEFAULT_SHELL: &str = "sh"; 6 | 7 | /// Command wrapper. 8 | #[derive(Debug)] 9 | pub struct TtyCommand; 10 | 11 | impl TtyCommand { 12 | /// Creates a command with the default shell. 13 | #[allow(clippy::new_ret_no_self)] 14 | pub fn new(command: &str) -> Result { 15 | Self::new_with_shell(command, None) 16 | } 17 | 18 | /// Creates a command that is executed by a shell, pretending to be a TTY. 19 | /// 20 | /// This means that the command will assume that terminal colors and 21 | /// other terminal features are available. 22 | /// 23 | /// On Linux, the command is wrapped in the `script` command. 24 | /// 25 | /// - [Linux docs](https://man7.org/linux/man-pages/man1/script.1.html) 26 | /// - [FreeBSD docs](https://www.freebsd.org/cgi/man.cgi?query=script&sektion=0&manpath=FreeBSD+12.2-RELEASE+and+Ports&arch=default&format=html) 27 | /// - [Apple docs](https://opensource.apple.com/source/shell_cmds/shell_cmds-170/script/script.1.auto.html) 28 | /// 29 | /// On Windows (which is unsupported), the command is returned as-is. 30 | pub fn new_with_shell(cmd: &str, shell: Option<&str>) -> Result { 31 | if cfg!(any(target_os = "linux", target_os = "android")) { 32 | let mut command = OsCommand::new("script"); 33 | command.args(["-q", "-e", "-c", cmd, "/dev/null"]); 34 | if let Some(shell) = shell { 35 | command.env("SHELL", shell.trim()); 36 | } 37 | Ok(command) 38 | } else if cfg!(any(target_os = "macos", target_os = "freebsd")) { 39 | let mut command = OsCommand::new("script"); 40 | command.args([ 41 | "-q", 42 | "/dev/null", 43 | shell.unwrap_or(DEFAULT_SHELL).trim(), 44 | "-c", 45 | cmd, 46 | ]); 47 | Ok(command) 48 | } else if cfg!(target_os = "windows") { 49 | let mut command = OsCommand::new("cmd"); 50 | command.args(["/C", cmd]); 51 | Ok(command) 52 | } else { 53 | Err(Error::UnsupportedPlatformError) 54 | } 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | use pretty_assertions::assert_eq; 62 | use std::process::Stdio; 63 | 64 | fn run_command(cmd: &str, shell: Option<&str>) -> Result { 65 | let command = TtyCommand::new_with_shell(cmd, shell)? 66 | .stderr(Stdio::inherit()) 67 | .output()?; 68 | Ok(String::from_utf8_lossy(&command.stdout).replace("\r\n", "\n")) 69 | } 70 | 71 | #[test] 72 | fn run_echo() -> Result<()> { 73 | assert_eq!("hello world\n", run_command("echo hello world", None)?); 74 | Ok(()) 75 | } 76 | 77 | #[test] 78 | fn run_seq() -> Result<()> { 79 | assert_eq!("1\n2\n3\n", run_command("seq 3", None)?); 80 | Ok(()) 81 | } 82 | 83 | #[test] 84 | fn run_echo_quotes() -> Result<()> { 85 | assert_eq!( 86 | "Hello $`' world!\n", 87 | run_command(r#"echo "Hello \$\`' world!""#, None)? 88 | ); 89 | Ok(()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.2.0] - 2024-06-20 6 | 7 | ### 🐙 Features 8 | 9 | - *(cheat)* Support `cheat.sh` custom URL 10 | - *(command)* Add timeout for terminating process ([#227](https://github.com/orhun/halp/issues/227)) 11 | - *(config)* Add default value for cheat.sh url 12 | - *(help)* Improve the `cheat-sh-url` help 13 | - *(help)* Improve the `cheat-sh-url` help 14 | - *(helper)* Add more external help providers to plz menu 15 | - *(log)* Show the path of the saved configuration file 16 | 17 | ### 🐛 Bug Fixes 18 | 19 | - *(dialog)* Update usage according to the latest version of dialoguer 20 | 21 | ### 🚜 Refactor 22 | 23 | - *(config)* Rename update_conf function to update_config 24 | - *(config)* Refactor the config system ([#62](https://github.com/orhun/halp/issues/62)) 25 | - *(format)* Run cargo fmt 26 | - *(lint)* Apply clippy suggestions 27 | - *(lint)* Apply clippy suggestions 28 | - *(test)* Fix the `fetch_cheat_sheet` test 29 | 30 | ### 📚 Documentation 31 | 32 | - *(coc)* Fix typo 33 | - *(license)* Update license copyright years 34 | - *(license)* Update MIT license 35 | - *(readme)* Update config format link 36 | - *(readme)* Add usage example with coutom cheat.sh host URL 37 | - *(readme)* Update `plz` subcommand usage 38 | - *(readme)* Update `plz` subcommand usage 39 | 40 | ### ⚙️ Miscellaneous Tasks 41 | 42 | - *(bors)* Remove bors config 43 | - *(cargo)* Bump MSRV to 1.74.1 44 | - *(cargo)* Add keywords for the crates.io release 45 | - *(cargo)* Bump MSRV to 1.70.0 46 | - *(changelog)* Skip dependency updates in changelog 47 | - *(ci)* Update macos runner 48 | - *(ci)* Update message while checking for MSRV 49 | - *(docker)* Update the cargo-chef image 50 | - *(git)* Remove IDEA directory from .gitignore 51 | - *(mergify)* Add mergify config for automatic merges 52 | 53 | ### Clean 54 | 55 | - *(docker)* Remove the duplicated `.gitignore` entry 56 | 57 | ## [0.1.7] - 2023-06-17 58 | 59 | ### 🚜 Refactor 60 | 61 | - *(cli)* Use formatted usage text 62 | 63 | ### 🎨 Styling 64 | 65 | - *(cli)* Override the help usage ([#30](https://github.com/orhun/halp/issues/30)) 66 | 67 | ## [0.1.6] - 2023-04-18 68 | 69 | ### 🐙 Features 70 | 71 | - *(docker)* Generate SBOM/provenance for the Docker image 72 | - *(output)* Add separators for command output ([#27](https://github.com/orhun/halp/issues/27)) 73 | 74 | ### 🎨 Styling 75 | 76 | - *(output)* Add color to the separator 77 | - *(readme)* Update license text 78 | 79 | ### ⚙️ Miscellaneous Tasks 80 | 81 | - *(ci)* Switch back to line coverage 82 | - *(ci)* Use codecov format with cargo-llvm-cov 83 | 84 | ## [0.1.5] - 2023-04-03 85 | 86 | ### ⚙️ Miscellaneous Tasks 87 | 88 | - *(changelog)* Update git-cliff template about breaking changes 89 | 90 | ## [0.1.4] - 2023-03-17 91 | 92 | ### 🐛 Bug Fixes 93 | 94 | - *(cheat)* Don't use a pager when no cheat sheet found for the command 95 | 96 | ### ⚙️ Miscellaneous Tasks 97 | 98 | - *(ci)* Specify token for codecov uploads 99 | 100 | ## [0.1.3] - 2023-03-16 101 | 102 | ### 🐛 Bug Fixes 103 | 104 | - *(args)* Unset pager for command output ([#9](https://github.com/orhun/halp/issues/9)) 105 | 106 | ## [0.1.2] - 2023-03-14 107 | 108 | ### 🐙 Features 109 | 110 | - *(args)* Check `version` subcommand for version info 111 | 112 | ### 📚 Documentation 113 | 114 | - *(readme)* Update installation instructions for Arch Linux 115 | 116 | ### ⚙️ Miscellaneous Tasks 117 | 118 | - *(changelog)* Skip PR related commits for changelog 119 | - *(github)* Remove codeowner approval requirement from bors 120 | - *(github)* Remove approval requirement from bors config 121 | - *(github)* Remove docker status check from bors config 122 | 123 | ## [0.1.1] - 2023-03-13 124 | 125 | ### 📚 Documentation 126 | 127 | - *(readme)* Match up common "help" and "version" arguments ([#3](https://github.com/orhun/halp/issues/3)) 128 | 129 | ## [0.1.0] - 2023-03-12 130 | 131 | ### 📚 Documentation 132 | 133 | - *(lib)* Add emoji to docs.rs description 134 | 135 | ## [0.1.0-rc.2] - 2023-03-12 136 | 137 | ### 🐛 Bug Fixes 138 | 139 | - *(cd)* Update the packaging step for license files 140 | 141 | 142 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 🐙 2 | 3 | A big welcome and thank you for considering contributing to `halp`! It is people like you that make it a reality for users in the open source community. 4 | 5 | Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing this project. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ## Quicklinks 8 | 9 | - [Code of Conduct](#code-of-conduct) 10 | - [Getting Started](#getting-started) 11 | - [Issues](#issues) 12 | - [Pull Requests](#pull-requests) 13 | - [License](#license) 14 | 15 | ## Code of Conduct 16 | 17 | We take our open source community seriously and hold ourselves and other contributors to high standards of communication. By participating and contributing to this project, you agree to uphold our [Code of Conduct](./CODE_OF_CONDUCT.md). 18 | 19 | ## Getting Started 20 | 21 | Contributions are made to this repo via Issues and Pull Requests (PRs). A few general guidelines that cover both: 22 | 23 | - First, discuss the change you wish to make via creating an [issue](https://github.com/orhun/halp/issues/new/choose), [email](mailto:orhunparmaksiz@gmail.com), or any other method with the owners of this repository before making a change. 24 | - Search for existing issues and PRs before creating your own. 25 | - We work hard to make sure issues are handled in a timely manner but, depending on the impact, it could take a while to investigate the root cause. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking. 26 | 27 | ### Issues 28 | 29 | Issues should be used to report problems with the project, request a new feature, or discuss potential changes before a PR is created. When you create a new issue, a template will be loaded that will guide you through collecting and providing the information we need to investigate. 30 | 31 | If you find an issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help be indicating to our maintainers that a particular problem is affecting more than just the reporter. 32 | 33 | ### Pull Requests 34 | 35 | PRs are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should: 36 | 37 | - Only fix/add the functionality in question **or** address wide-spread whitespace/style issues, not both. 38 | - Add unit or integration tests for fixed or changed functionality (if a test suite already exists). 39 | - Address a single concern in the least number of changed lines as possible. 40 | - Include documentation. 41 | - Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). 42 | 43 | For changes that address core functionality or would require breaking changes (e.g. a major release), it's best to open an issue to discuss your proposal first. This is not required but can save time creating and reviewing changes. 44 | 45 | In general, we follow the "[fork-and-pull](https://github.com/susam/gitpr)" Git workflow: 46 | 47 | 1. Fork the repository to your own GitHub account. 48 | 49 | 2. Clone the project to your local environment. 50 | 51 | ```sh 52 | git clone https://github.com//halp && cd halp/ 53 | ``` 54 | 55 | 3. Create a branch locally with a succinct but descriptive name. 56 | 57 | ```sh 58 | git checkout -b 59 | ``` 60 | 61 | 4. Make sure you have [Rust](https://rustup.rs) installed and build the project. 62 | 63 | ```sh 64 | cargo build 65 | ``` 66 | 67 | 5. Start committing changes to the branch. 68 | 69 | 6. Add your tests or update the existing tests according to the changes and check if the tests are passed. 70 | 71 | ```sh 72 | OUT_DIR=target NO_COLOR=1 cargo test 73 | ``` 74 | 75 | 7. Make sure [rustfmt](https://github.com/rust-lang/rustfmt) and [clippy](https://github.com/rust-lang/rust-clippy) don't show any errors/warnings. 76 | 77 | ```sh 78 | cargo fmt --all -- --check --verbose 79 | ``` 80 | 81 | ```sh 82 | cargo clippy --verbose -- -D warnings 83 | ``` 84 | 85 | 8. Push changes to your fork. 86 | 87 | 9. Open a PR in our repository and follow the [PR template](./.github/PULL_REQUEST_TEMPLATE.md) so that we can efficiently review the changes. 88 | 89 | 10. Wait for approval from the repository owners. Discuss the possible changes and update your PR if necessary. 90 | 91 | 11. The PR will be merged once you have the sign-off of the repository owners. 92 | 93 | ## License 94 | 95 | By contributing, you agree that your contributions will be licensed under [The MIT License](./LICENSE-MIT) or [Apache License 2.0](./LICENSE-APACHE). 96 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use clap::{Parser, Subcommand}; 3 | use std::path::PathBuf; 4 | 5 | /// Command-line arguments. 6 | #[derive(Debug, Default, Parser)] 7 | #[command( 8 | version, 9 | author, 10 | about, 11 | subcommand_negates_reqs = true, 12 | disable_help_subcommand = true, 13 | override_usage = format!(" 14 | {bin} [OPTIONS] 15 | {bin} [OPTIONS] ", bin = env!("CARGO_PKG_NAME")) 16 | )] 17 | pub struct CliArgs { 18 | /// Command or binary name. 19 | #[arg(required = true)] 20 | pub cmd: Option, 21 | /// Sets the argument to check. 22 | #[arg(long = "check", value_name = "ARG", value_parser = CliArgs::parse_arg)] 23 | pub check_args: Option>, 24 | /// Disable checking the version information. 25 | #[arg(long)] 26 | pub no_version: bool, 27 | /// Disable checking the help information. 28 | #[arg(long)] 29 | pub no_help: bool, 30 | /// Sets the configuration file. 31 | #[arg(short, long, env = "HALP_CONFIG", value_name = "PATH")] 32 | pub config: Option, 33 | /// Sets the timeout for the command. 34 | #[arg(short, long, value_name = "S")] 35 | pub timeout: Option, 36 | /// Enables verbose logging. 37 | #[arg(short, long)] 38 | pub verbose: bool, 39 | /// Subcommands. 40 | #[command(subcommand)] 41 | pub subcommand: Option, 42 | } 43 | 44 | /// Subcommands. 45 | #[derive(Debug, Subcommand)] 46 | pub enum CliCommands { 47 | /// Get additional help. 48 | Plz { 49 | /// Command or binary name. 50 | cmd: String, 51 | /// Sets the manual page command to run. 52 | #[arg(short, long)] 53 | man_cmd: Option, 54 | /// Use a custom URL for cheat.sh. 55 | #[arg(long, env = "CHEAT_SH_URL", value_name = "URL")] 56 | cheat_sh_url: Option, 57 | /// Use a custom provider URL for `eg` pages. 58 | #[arg(long, env = "EG_PAGES_URL", value_name = "URL")] 59 | eg_url: Option, 60 | /// Use a custom URL for cheat sheets. 61 | #[arg(long, env = "CHEATSHEETS_URL", value_name = "URL")] 62 | cheat_url: Option, 63 | /// Sets the pager to use. 64 | #[arg(short, long)] 65 | pager: Option, 66 | /// Disables the pager. 67 | #[arg(long)] 68 | no_pager: bool, 69 | }, 70 | } 71 | 72 | impl CliArgs { 73 | /// Custom argument parser for escaping the '-' character. 74 | fn parse_arg(arg: &str) -> Result { 75 | Ok(arg.replace("\\-", "-")) 76 | } 77 | 78 | /// Update the configuration based on the command-line arguments (the command-line arguments will override the configuration). 79 | pub fn update_config(&self, config: &mut Config) { 80 | config.check_help = !self.no_help; 81 | config.check_version = !self.no_version; 82 | if let Some(ref args) = self.check_args { 83 | config.check_args = Some(args.iter().map(|s| vec![s.to_string()]).collect()); 84 | } 85 | if self.timeout.is_some() { 86 | config.timeout = self.timeout; 87 | } 88 | if let Some(CliCommands::Plz { 89 | ref man_cmd, 90 | ref cheat_sh_url, 91 | ref eg_url, 92 | no_pager, 93 | ref pager, 94 | .. 95 | }) = self.subcommand 96 | { 97 | if let Some(man_cmd) = man_cmd { 98 | config.man_command.clone_from(man_cmd); 99 | } 100 | if let Some(cheat_sh_url) = cheat_sh_url { 101 | config.cheat_sh_url = Some(cheat_sh_url.clone()); 102 | } 103 | if let Some(eg_url) = eg_url { 104 | config.eg_url = Some(eg_url.to_owned()); 105 | } 106 | if no_pager { 107 | config.pager_command = None; 108 | } else if let Some(pager) = pager { 109 | config.pager_command = Some(pager.clone()); 110 | } 111 | } 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | use clap::CommandFactory; 119 | use pretty_assertions::assert_eq; 120 | 121 | #[test] 122 | fn test_cli_args() { 123 | CliArgs::command().debug_assert(); 124 | assert_eq!(Ok("--help"), CliArgs::parse_arg("\\--help").as_deref()); 125 | } 126 | 127 | #[test] 128 | fn test_update_config() { 129 | let mut config = Config::default(); 130 | let args = CliArgs { 131 | subcommand: Some(CliCommands::Plz { 132 | cmd: "ps".to_string(), 133 | pager: Some("bat".to_string()), 134 | cheat_sh_url: None, 135 | cheat_url: None, 136 | eg_url: None, 137 | man_cmd: None, 138 | no_pager: false, 139 | }), 140 | ..Default::default() 141 | }; 142 | args.update_config(&mut config); 143 | assert!(config.check_help); 144 | assert_eq!(Some(String::from("bat")), config.pager_command); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::helper::args::common::{HelpArg, VersionArg}; 3 | use crate::helper::args::FOUND_EMOTICON; 4 | use crate::helper::docs::cheat_sh::DEFAULT_CHEAT_SHEET_PROVIDER; 5 | use crate::helper::docs::cheatsheets::DEFAULT_CHEATSHEETS_PROVIDER; 6 | use crate::helper::docs::eg::DEFAULT_EG_PAGES_PROVIDER; 7 | use colored::*; 8 | use serde::{Deserialize, Serialize}; 9 | use std::env; 10 | use std::fs; 11 | use std::io::Write; 12 | use std::path::{Path, PathBuf}; 13 | 14 | /// Configuration. 15 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 16 | pub struct Config { 17 | /// Check the version flag. 18 | pub check_version: bool, 19 | /// Check the help flag. 20 | pub check_help: bool, 21 | /// Arguments to check. 22 | #[serde(rename = "check")] 23 | pub check_args: Option>>, 24 | /// Command to run for manual pages. 25 | pub man_command: String, 26 | /// Pager to use for command outputs, None to disable. 27 | pub pager_command: Option, 28 | /// Use a custom URL for cheat.sh. 29 | pub cheat_sh_url: Option, 30 | /// Use a custom URL for `eg` pages provider. 31 | pub eg_url: Option, 32 | /// Use a custom URL for cheatsheets provider. 33 | pub cheatsheets_url: Option, 34 | /// Timeout for running the commands. 35 | pub timeout: Option, 36 | } 37 | 38 | impl Default for Config { 39 | fn default() -> Self { 40 | Config { 41 | check_version: true, 42 | check_help: true, 43 | check_args: Some(vec![ 44 | VersionArg::variants() 45 | .iter() 46 | .map(|s| s.as_str().to_string()) 47 | .collect(), 48 | HelpArg::variants() 49 | .iter() 50 | .map(|s| s.as_str().to_string()) 51 | .collect(), 52 | ]), 53 | man_command: "man".to_string(), 54 | pager_command: Some("less -R".to_string()), 55 | cheat_sh_url: Some(DEFAULT_CHEAT_SHEET_PROVIDER.to_string()), 56 | eg_url: Some(DEFAULT_EG_PAGES_PROVIDER.to_string()), 57 | cheatsheets_url: Some(DEFAULT_CHEATSHEETS_PROVIDER.to_string()), 58 | timeout: Some(5), 59 | } 60 | } 61 | } 62 | 63 | impl Config { 64 | /// Checks the possible locations for the configuration file. 65 | /// 66 | /// - `/halp.toml` 67 | /// - `/halp/halp.toml` 68 | /// - `/halp/config` 69 | /// 70 | /// Returns the path if the configuration file is found. 71 | pub fn get_default_location() -> Option { 72 | if let Some(config_dirs) = Config::get_default_locations() { 73 | for config_dir in config_dirs { 74 | if config_dir.exists() { 75 | return Some(config_dir); 76 | } 77 | } 78 | } 79 | None 80 | } 81 | 82 | #[inline(always)] 83 | fn get_default_locations() -> Option> { 84 | if let Some(config_dir) = dirs::config_dir() { 85 | let file_name = concat!(env!("CARGO_PKG_NAME"), ".toml"); 86 | return Some(vec![ 87 | config_dir.join(&file_name), 88 | config_dir.join(env!("CARGO_PKG_NAME")).join(&file_name), // XDG style 89 | config_dir.join(env!("CARGO_PKG_NAME")).join("config"), 90 | ]); 91 | } 92 | None 93 | } 94 | 95 | /// Parses the configuration file. 96 | pub fn parse(file: &Path) -> Result { 97 | let contents = fs::read_to_string(file)?; 98 | let config: Config = toml::from_str(&contents)?; 99 | Ok(config) 100 | } 101 | 102 | /// Writes the configuration file to the default location (XDG style). 103 | pub fn write(&self, output: &mut Output) -> Result<()> { 104 | if let Some(config_dirs) = Config::get_default_locations() { 105 | let xdg_conf_path = &config_dirs[1]; 106 | if let Some(parent) = xdg_conf_path.parent() { 107 | if !parent.exists() { 108 | fs::create_dir_all(parent)?; 109 | } 110 | } 111 | let contents = toml::to_string(&self)?; 112 | writeln!( 113 | output, 114 | "{} {} {}", 115 | FOUND_EMOTICON.magenta(), 116 | "writing the default configuration to".green().bold(), 117 | format!("{:?}", xdg_conf_path).white().italic() 118 | )?; 119 | fs::write(xdg_conf_path, contents)?; 120 | } 121 | Ok(()) 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::*; 128 | use pretty_assertions::assert_eq; 129 | use std::path::PathBuf; 130 | 131 | #[test] 132 | fn test_parse_config() -> Result<()> { 133 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 134 | .join("config") 135 | .join(concat!(env!("CARGO_PKG_NAME"), ".toml")); 136 | if let Some(global_path) = Config::get_default_location() { 137 | path = global_path; 138 | } 139 | let config = Config::parse(&path)?; 140 | assert!(config.check_help); 141 | assert!(config.check_version); 142 | assert_eq!( 143 | config.cheat_sh_url, 144 | Some(DEFAULT_CHEAT_SHEET_PROVIDER.to_string()) 145 | ); 146 | Ok(()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socioeconomic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | halp-cli@proton.me 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | changelog: 10 | name: Generate changelog 11 | runs-on: ubuntu-latest 12 | outputs: 13 | release_body: ${{ steps.git-cliff.outputs.content }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Generate a changelog 21 | uses: orhun/git-cliff-action@v4 22 | id: git-cliff 23 | with: 24 | config: cliff.toml 25 | args: --latest --strip header 26 | 27 | publish-github: 28 | name: Publish on GitHub 29 | needs: changelog 30 | runs-on: ${{ matrix.build.OS }} 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | build: 35 | - { 36 | OS: ubuntu-22.04, 37 | TOOLCHAIN: stable, 38 | TARGET: x86_64-unknown-linux-gnu, 39 | } 40 | - { 41 | OS: ubuntu-22.04, 42 | TOOLCHAIN: stable, 43 | TARGET: x86_64-unknown-linux-musl, 44 | } 45 | - { 46 | OS: ubuntu-22.04, 47 | TOOLCHAIN: stable, 48 | TARGET: i686-unknown-linux-gnu, 49 | } 50 | - { 51 | OS: ubuntu-22.04, 52 | TOOLCHAIN: stable, 53 | TARGET: i686-unknown-linux-musl, 54 | } 55 | - { 56 | OS: ubuntu-22.04, 57 | TOOLCHAIN: stable, 58 | TARGET: aarch64-unknown-linux-gnu, 59 | } 60 | - { 61 | OS: ubuntu-22.04, 62 | TOOLCHAIN: stable, 63 | TARGET: aarch64-unknown-linux-musl, 64 | } 65 | - { 66 | OS: ubuntu-22.04, 67 | TOOLCHAIN: stable, 68 | TARGET: armv5te-unknown-linux-gnueabi, 69 | } 70 | - { 71 | OS: ubuntu-22.04, 72 | TOOLCHAIN: stable, 73 | TARGET: armv7-unknown-linux-gnueabihf, 74 | } 75 | - { 76 | OS: ubuntu-22.04, 77 | TOOLCHAIN: stable, 78 | TARGET: arm-unknown-linux-gnueabi, 79 | } 80 | - { 81 | OS: ubuntu-22.04, 82 | TOOLCHAIN: stable, 83 | TARGET: arm-unknown-linux-gnueabihf, 84 | } 85 | - { 86 | OS: windows-2022, 87 | TOOLCHAIN: stable, 88 | TARGET: x86_64-pc-windows-msvc, 89 | } 90 | - { OS: macos-14, TOOLCHAIN: stable, TARGET: x86_64-apple-darwin } 91 | - { OS: macos-14, TOOLCHAIN: stable, TARGET: aarch64-apple-darwin } 92 | steps: 93 | - name: Checkout the repository 94 | uses: actions/checkout@v4 95 | 96 | - name: Set the release version 97 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV 98 | 99 | - name: Install Rust 100 | uses: actions-rs/toolchain@v1 101 | with: 102 | toolchain: ${{ matrix.build.TOOLCHAIN }} 103 | target: ${{ matrix.build.TARGET }} 104 | profile: minimal 105 | override: true 106 | 107 | - name: Build 108 | uses: actions-rs/cargo@v1 109 | with: 110 | command: build 111 | args: --release --locked --target ${{ matrix.build.TARGET }} 112 | use-cross: ${{ matrix.build.OS == 'ubuntu-22.04' }} 113 | 114 | - name: Prepare release assets 115 | shell: bash 116 | run: | 117 | mkdir -p release/{man,completions} 118 | cp {LICENSE-*,README.md,CHANGELOG.md} release/ 119 | OUT_DIR=release/completions/ cargo run --release --bin halp-completions 120 | OUT_DIR=release/man/ cargo run --release --bin halp-mangen 121 | for bin in 'halp' 'halp-completions' 'halp-mangen'; do 122 | if [ "${{ matrix.build.OS }}" = "windows-2022" ]; then 123 | bin="${bin}.exe" 124 | fi 125 | cp "target/${{ matrix.build.TARGET }}/release/${bin}" release/ 126 | done 127 | mv release/ halp-${{ env.RELEASE_VERSION }}/ 128 | 129 | - name: Create release artifacts 130 | shell: bash 131 | run: | 132 | if [ "${{ matrix.build.OS }}" = "windows-2022" ]; then 133 | 7z a -tzip "halp-${{ env.RELEASE_VERSION }}-${{ matrix.build.TARGET }}.zip" \ 134 | halp-${{ env.RELEASE_VERSION }}/ 135 | else 136 | tar -czvf halp-${{ env.RELEASE_VERSION }}-${{ matrix.build.TARGET }}.tar.gz \ 137 | halp-${{ env.RELEASE_VERSION }}/ 138 | shasum -a 512 halp-${{ env.RELEASE_VERSION }}-${{ matrix.build.TARGET }}.tar.gz \ 139 | > halp-${{ env.RELEASE_VERSION }}-${{ matrix.build.TARGET }}.tar.gz.sha512 140 | fi 141 | 142 | - name: Sign the release 143 | if: matrix.build.OS != 'windows-2022' 144 | run: | 145 | echo "${{ secrets.GPG_RELEASE_KEY }}" | base64 --decode > private.key 146 | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback \ 147 | --passphrase-fd 0 --import private.key 148 | echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback \ 149 | --passphrase-fd 0 --detach-sign \ 150 | halp-${{ env.RELEASE_VERSION }}-${{ matrix.build.TARGET }}.tar.gz 151 | 152 | - name: Upload the binary releases 153 | uses: svenstaro/upload-release-action@v2 154 | with: 155 | file: halp-${{ env.RELEASE_VERSION }}-${{ matrix.build.TARGET }}* 156 | file_glob: true 157 | overwrite: true 158 | tag: ${{ github.ref }} 159 | release_name: "Release v${{ env.RELEASE_VERSION }}" 160 | body: ${{ needs.changelog.outputs.release_body }} 161 | repo_token: ${{ secrets.GITHUB_TOKEN }} 162 | 163 | publish-crates-io: 164 | name: Publish on crates.io 165 | runs-on: ubuntu-22.04 166 | steps: 167 | - name: Checkout the repository 168 | uses: actions/checkout@v4 169 | 170 | - name: Install Rust toolchain 171 | uses: actions-rs/toolchain@v1 172 | with: 173 | toolchain: stable 174 | target: x86_64-unknown-linux-gnu 175 | override: true 176 | 177 | - name: Publish 178 | run: cargo publish --locked --token ${{ secrets.CARGO_TOKEN }} 179 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - staging # for bors 9 | - trying # for bors 10 | schedule: 11 | - cron: "0 0 * * 0" 12 | 13 | jobs: 14 | build: 15 | name: Build on ${{ matrix.build.OS }} (${{ matrix.build.TARGET }}) 16 | runs-on: ${{ matrix.build.OS }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | build: 21 | - { 22 | OS: ubuntu-22.04, 23 | TOOLCHAIN: stable, 24 | TARGET: x86_64-unknown-linux-gnu, 25 | } 26 | - { 27 | OS: ubuntu-22.04, 28 | TOOLCHAIN: stable, 29 | TARGET: x86_64-unknown-linux-musl, 30 | } 31 | - { 32 | OS: ubuntu-22.04, 33 | TOOLCHAIN: stable, 34 | TARGET: i686-unknown-linux-gnu, 35 | } 36 | - { 37 | OS: ubuntu-22.04, 38 | TOOLCHAIN: stable, 39 | TARGET: i686-unknown-linux-musl, 40 | } 41 | - { 42 | OS: ubuntu-22.04, 43 | TOOLCHAIN: stable, 44 | TARGET: aarch64-unknown-linux-gnu, 45 | } 46 | - { 47 | OS: ubuntu-22.04, 48 | TOOLCHAIN: stable, 49 | TARGET: aarch64-unknown-linux-musl, 50 | } 51 | - { 52 | OS: ubuntu-22.04, 53 | TOOLCHAIN: stable, 54 | TARGET: armv5te-unknown-linux-gnueabi, 55 | } 56 | - { 57 | OS: ubuntu-22.04, 58 | TOOLCHAIN: stable, 59 | TARGET: armv7-unknown-linux-gnueabihf, 60 | } 61 | - { 62 | OS: ubuntu-22.04, 63 | TOOLCHAIN: stable, 64 | TARGET: arm-unknown-linux-gnueabi, 65 | } 66 | - { 67 | OS: ubuntu-22.04, 68 | TOOLCHAIN: stable, 69 | TARGET: arm-unknown-linux-gnueabihf, 70 | } 71 | - { 72 | OS: windows-2022, 73 | TOOLCHAIN: stable, 74 | TARGET: x86_64-pc-windows-msvc, 75 | } 76 | - { OS: macos-14, TOOLCHAIN: stable, TARGET: x86_64-apple-darwin } 77 | - { OS: macos-14, TOOLCHAIN: stable, TARGET: aarch64-apple-darwin } 78 | steps: 79 | - name: Checkout the repository 80 | uses: actions/checkout@v4 81 | 82 | - name: Install Rust 83 | uses: actions-rs/toolchain@v1 84 | with: 85 | toolchain: ${{ matrix.build.TOOLCHAIN }} 86 | target: ${{ matrix.build.TARGET }} 87 | profile: minimal 88 | override: true 89 | 90 | - name: Build 91 | uses: actions-rs/cargo@v1 92 | with: 93 | command: build 94 | args: --locked --target ${{ matrix.build.TARGET }} 95 | use-cross: ${{ matrix.build.OS == 'ubuntu-22.04' }} 96 | 97 | test: 98 | name: Test 99 | runs-on: ubuntu-latest 100 | steps: 101 | - name: Checkout the repository 102 | uses: actions/checkout@v4 103 | 104 | - name: Install Rust 105 | uses: actions-rs/toolchain@v1 106 | with: 107 | profile: minimal 108 | toolchain: stable 109 | override: true 110 | 111 | - name: Install cargo-llvm-cov 112 | uses: taiki-e/install-action@cargo-llvm-cov 113 | 114 | - name: Cache Cargo dependencies 115 | uses: Swatinem/rust-cache@v2 116 | 117 | - name: Build test binary 118 | run: cargo build --bin halp-test 119 | 120 | - name: Generate code coverage 121 | run: cargo llvm-cov --lcov --output-path lcov.info 122 | env: 123 | NO_COLOR: 1 124 | OUT_DIR: target 125 | 126 | - name: Upload coverage to Codecov 127 | uses: codecov/codecov-action@v5 128 | with: 129 | name: code-coverage-report 130 | files: lcov.info 131 | fail_ci_if_error: true 132 | verbose: true 133 | token: ${{ secrets.CODECOV_TOKEN }} 134 | 135 | lint: 136 | name: Lint 137 | runs-on: ubuntu-latest 138 | steps: 139 | - name: Checkout the repository 140 | if: github.event_name != 'pull_request' 141 | uses: actions/checkout@v4 142 | - name: Checkout the repository 143 | if: github.event_name == 'pull_request' 144 | uses: actions/checkout@v4 145 | with: 146 | ref: ${{ github.event.pull_request.head.sha }} 147 | 148 | - name: Install Rust 149 | uses: actions-rs/toolchain@v1 150 | with: 151 | profile: minimal 152 | toolchain: stable 153 | override: true 154 | components: rustfmt, clippy 155 | 156 | - name: Cache Cargo dependencies 157 | uses: Swatinem/rust-cache@v2 158 | 159 | - name: Run rustfmt 160 | uses: actions-rs/cargo@v1 161 | with: 162 | command: fmt 163 | args: --all -- --check 164 | 165 | - name: Run clippy 166 | uses: actions-rs/cargo@v1 167 | with: 168 | command: clippy 169 | args: --tests -- -D warnings 170 | 171 | - name: Run cargo-deny 172 | uses: EmbarkStudios/cargo-deny-action@v2 173 | with: 174 | command: check licenses sources 175 | 176 | - name: Run cargo-audit 177 | uses: actions-rs/audit-check@v1 178 | with: 179 | token: ${{ secrets.GITHUB_TOKEN }} 180 | 181 | - name: Run committed 182 | uses: crate-ci/committed@master 183 | with: 184 | args: "-vv" 185 | commits: "HEAD" 186 | 187 | - name: Run lychee 188 | uses: lycheeverse/lychee-action@v2 189 | with: 190 | args: -v *.md 191 | env: 192 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 193 | 194 | - name: Run codespell 195 | uses: codespell-project/actions-codespell@master 196 | with: 197 | check_filenames: true 198 | check_hidden: true 199 | ignore_words_file: .codespellignore 200 | skip: target,.git 201 | 202 | - name: Run cargo-msrv 203 | shell: bash 204 | run: | 205 | curl -s 'https://api.github.com/repos/foresterre/cargo-msrv/releases' | \ 206 | jq -r "[.[] | select(.prerelease == false)][0].assets[] | \ 207 | select(.name | ascii_downcase | test(\"linux.*x86_64|x86_64.*linux\")).browser_download_url" | \ 208 | wget -qi - 209 | tar -xvf cargo-msrv*.tar* -C ~/.cargo/bin/ cargo-msrv 210 | printf "%s" "Checking MSRV..." 211 | cargo msrv --output-format json verify | tail -n 1 | jq --exit-status '.success' 212 | -------------------------------------------------------------------------------- /src/helper/docs/mod.rs: -------------------------------------------------------------------------------- 1 | /// Man page helper. 2 | pub mod man; 3 | 4 | /// Cheat sheet helper. 5 | pub mod cheat_sh; 6 | /// cheat helper. 7 | pub mod cheatsheets; 8 | /// eg page helper. 9 | pub mod eg; 10 | 11 | use crate::config::Config; 12 | use crate::error::{Error, Result}; 13 | use crate::helper::docs::cheat_sh::CheatDotSh; 14 | use crate::helper::docs::cheatsheets::Cheatsheets; 15 | use crate::helper::docs::eg::Eg; 16 | use crate::helper::docs::man::show_man_page; 17 | use console::{style, Style, Term}; 18 | use dialoguer::theme::ColorfulTheme; 19 | use dialoguer::Select; 20 | use std::io::Write; 21 | use std::process::{Command, Stdio}; 22 | use ureq::Request; 23 | 24 | /// The `HelpProvider` trait defines essential methods for fetching help content related to commands from a provider. 25 | /// 26 | /// Each provider that implements this trait should provide a default URL used to retrieve the command help content. 27 | /// However, it also allows specifying a custom URL to override the default one. 28 | /// 29 | /// This trait is generic and not tied to any specific command help system such as man pages or POSIX documentation, 30 | /// instead it relies on the implementation to define how to fetch and mark up the content. 31 | /// 32 | /// # Methods 33 | /// 34 | /// - `url`: Returns the default URL of the provider. 35 | /// - `build_req`: Uses the given command and URL to create an HTTP GET request. 36 | /// - `err_handle`: Handles possible request errors, such as a non-existent command page on the provider. 37 | /// - `fetch`: Attempts to retrieve the command page from the provider, optionally from a custom URL. 38 | /// 39 | /// # Example 40 | /// 41 | /// An implementation could be created for a provider that supplies help pages in Markdown format. 42 | /// The `url` method would return the base URL of this provider. 43 | /// The `build_request` method could construct a GET request for `{base_url}/{command}.md`. 44 | /// The `handle_error` could interpret a 404 status code as 'Command page not found'. 45 | /// The `fetch` would handle fetching the specified command page using the constructed request. 46 | pub trait HelpProvider { 47 | /// Return the default provider URL. 48 | fn url(&self) -> &'static str; 49 | 50 | /// Builds an HTTP request using the given `cmd` and `url`. 51 | /// 52 | /// # Parameters 53 | /// 54 | /// - `cmd`: The name of the command to be included in the request. 55 | /// - `url`: The root URL. 56 | /// 57 | /// # Returns 58 | /// This method returns a new `Request` instance configured with the `GET` method and the formatted URL. 59 | fn build_request(&self, cmd: &str, url: &str) -> Request; 60 | 61 | /// Handle the request error. 62 | /// aka return a custom message if the error means that **provider** doesn't have a page for the command. 63 | fn handle_error(&self, e: ureq::Error) -> Error { 64 | if e.kind() == ureq::ErrorKind::HTTP { 65 | Error::ProviderError( 66 | "Unknown topic, This topic/command might has no page in this provider yet." 67 | .to_string(), 68 | ) 69 | } else { 70 | Error::from(Box::new(e)) 71 | } 72 | } 73 | 74 | /// **The default** fetch implementation. 75 | /// 76 | /// This method attempts to retrieve the specified command page from the given provider. 77 | /// If a `custom_url` is provided, this URL is used instead of the default URL. 78 | /// The method will return the content of the command page if the fetch operation is successful. 79 | #[inline(always)] 80 | fn _fetch(&self, cmd: &str, custom_url: &Option) -> Result { 81 | let url = { 82 | if let Some(u) = custom_url { 83 | u.as_str() 84 | } else { 85 | self.url() 86 | } 87 | }; 88 | let response = self.build_request(cmd, url).call(); 89 | 90 | let response = response.map_err(|e| self.handle_error(e)); 91 | 92 | match response { 93 | Ok(response) => Ok(response.into_string()?), 94 | Err(e) => Err(e), 95 | } 96 | } 97 | 98 | /// Fetches the command page from the provider. 99 | /// 100 | /// # Parameters 101 | /// 102 | /// - `cmd`: The name of the command for which the page should be fetched. 103 | /// - `custom_url`: Optional parameter that, if supplied, specifies a custom URL from which to fetch the command page. 104 | /// 105 | /// # Returns 106 | /// 107 | /// This method returns a Result type. On successful fetch, it contains a `String` with the content of the fetched page. 108 | /// In case of failure, it contains an error that provides further details about the issue encountered during the fetch operation. 109 | /// 110 | /// # Errors 111 | /// 112 | /// This method will return an error if the fetch operation fails. 113 | fn fetch(&self, cmd: &str, custom_url: &Option) -> Result { 114 | self._fetch(cmd, custom_url) 115 | } 116 | } 117 | 118 | /// Shows documentation/usage help about the given command. 119 | pub fn get_docs_help(cmd: &str, config: &Config, output: &mut Output) -> Result<()> { 120 | const MAN_PAGE: usize = 0; 121 | const CHEAT_SHEET: usize = 1; 122 | const EG_PAGE: usize = 2; 123 | const CHEATSHEETS: usize = 3; 124 | let menu_options = [ 125 | "Show man page", 126 | "Show cheat.sh page", 127 | "Show the eg page", 128 | "Show the cheatsheet page", 129 | "Exit", 130 | ]; 131 | let mut selection = Some(MAN_PAGE); 132 | loop { 133 | selection = Select::with_theme(&get_selection_theme()) 134 | .with_prompt("Select operation") 135 | .default(selection.unwrap_or_default()) 136 | .items(&menu_options) 137 | .interact_on_opt(&Term::stderr())?; 138 | if let Some(MAN_PAGE) = selection { 139 | show_man_page(&config.man_command, cmd)? 140 | } else { 141 | let page = match selection { 142 | Some(CHEAT_SHEET) => CheatDotSh.fetch(cmd, &config.cheat_sh_url)?, 143 | Some(EG_PAGE) => Eg.fetch(cmd, &config.eg_url)?, 144 | Some(CHEATSHEETS) => Cheatsheets.fetch(cmd, &config.cheatsheets_url)?, 145 | _ => return Ok(()), 146 | }; 147 | // Show the page using the user selected pager or write it directly into the output 148 | if let Some(pager) = config.pager_command.as_ref() { 149 | let mut process = if cfg!(target_os = "windows") { 150 | Command::new("cmd") 151 | .args(["/C", pager]) 152 | .stdin(Stdio::piped()) 153 | .spawn() 154 | } else { 155 | Command::new("sh") 156 | .args(["-c", pager]) 157 | .stdin(Stdio::piped()) 158 | .spawn() 159 | }?; 160 | if let Some(stdin) = process.stdin.as_mut() { 161 | writeln!(stdin, "{}", page)?; 162 | process.wait()?; 163 | } 164 | } else { 165 | writeln!(output, "{}", page)?; 166 | } 167 | } 168 | } 169 | } 170 | 171 | /// Returns the theme for selection prompt. 172 | fn get_selection_theme() -> ColorfulTheme { 173 | ColorfulTheme { 174 | defaults_style: Style::new().for_stderr().cyan(), 175 | prompt_style: Style::new().for_stderr().bold(), 176 | prompt_prefix: style("(ノ´ヮ`)ノ*: ・゚\n".to_string()).for_stderr().magenta(), 177 | prompt_suffix: style("›".to_string()).for_stderr().magenta().bright(), 178 | success_prefix: style("❤".to_string()).for_stderr().magenta(), 179 | success_suffix: style("·".to_string()).for_stderr().black().bright(), 180 | error_prefix: style("✘".to_string()).for_stderr().red(), 181 | error_style: Style::new().for_stderr().red(), 182 | hint_style: Style::new().for_stderr().black().bright(), 183 | values_style: Style::new().for_stderr().green(), 184 | active_item_style: Style::new().for_stderr().green().bold(), 185 | inactive_item_style: Style::new().for_stderr().italic(), 186 | active_item_prefix: style("✧".to_string()).for_stderr().magenta().bold(), 187 | inactive_item_prefix: style(" ".to_string()).for_stderr(), 188 | checked_item_prefix: style("❤".to_string()).for_stderr().magenta(), 189 | unchecked_item_prefix: style("❤".to_string()).for_stderr().black(), 190 | picked_item_prefix: style("❯".to_string()).for_stderr().green(), 191 | unpicked_item_prefix: style(" ".to_string()).for_stderr(), 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/helper/args/mod.rs: -------------------------------------------------------------------------------- 1 | /// Helper module for Help and Version checks variants. 2 | pub mod common; 3 | 4 | use crate::config::Config; 5 | use crate::error::{Error, Result}; 6 | use crate::helper::tty::TtyCommand; 7 | use colored::*; 8 | use process_control::{ChildExt, Control}; 9 | use std::io::Write; 10 | use std::process::Stdio; 11 | use std::time::Duration; 12 | 13 | /// Emoticon for "checking" message. 14 | const CHECK_EMOTICON: &str = "(°ロ°)"; 15 | /// Emoticon for "found" message. 16 | pub const FOUND_EMOTICON: &str = "\\(^ヮ^)/"; 17 | /// Emoticon for "fail" message. 18 | pub const FAIL_EMOTICON: &str = "(×﹏×)"; 19 | /// Emoticon for debug messages. 20 | const DEBUG_EMOTICON: &str = "(o_O)"; 21 | /// Separator for output. 22 | const OUTPUT_SEPARATOR: &str = "---"; 23 | 24 | /// Checks if the given arguments exist. 25 | fn check_args<'a, ArgsIter: Iterator, Output: Write>( 26 | cmd: &str, 27 | args: ArgsIter, 28 | verbose: bool, 29 | timeout: u64, 30 | output: &mut Output, 31 | ) -> Result<()> { 32 | for arg in args { 33 | let command = format!("{} {}", cmd, arg); 34 | writeln!( 35 | output, 36 | "{} {} '{}'", 37 | CHECK_EMOTICON.magenta(), 38 | "checking".green().bold(), 39 | command.white().italic() 40 | )?; 41 | let cmd_out = TtyCommand::new(&command)? 42 | .env("PAGER", "") 43 | .stderr(Stdio::inherit()) 44 | .stdout(Stdio::piped()) 45 | .spawn()? 46 | .controlled_with_output() 47 | .time_limit(Duration::from_secs(timeout)) 48 | .terminate_for_timeout() 49 | .wait()? 50 | .ok_or_else(|| Error::TimeoutError(timeout))?; 51 | if cmd_out.status.success() { 52 | writeln!( 53 | output, 54 | "{} {} '{}' argument found!", 55 | FOUND_EMOTICON.magenta(), 56 | "success".cyan().bold(), 57 | arg.white().italic() 58 | )?; 59 | writeln!(output, "{}", OUTPUT_SEPARATOR.bright_black())?; 60 | output.write_all(&cmd_out.stdout)?; 61 | writeln!(output, "{}", OUTPUT_SEPARATOR.bright_black())?; 62 | break; 63 | } else { 64 | writeln!( 65 | output, 66 | "{} {} '{}' argument not found.", 67 | FAIL_EMOTICON.magenta(), 68 | "fail".red().bold(), 69 | arg.white().italic() 70 | )?; 71 | if verbose { 72 | writeln!( 73 | output, 74 | "{} {}", 75 | DEBUG_EMOTICON.magenta(), 76 | "debug".yellow().bold(), 77 | )?; 78 | if !cmd_out.stdout.is_empty() { 79 | writeln!(output, "{}:", "stdout".white().italic())?; 80 | writeln!(output, "{}", OUTPUT_SEPARATOR.bright_black())?; 81 | output.write_all(&cmd_out.stdout)?; 82 | writeln!(output, "{}", OUTPUT_SEPARATOR.bright_black())?; 83 | } 84 | if !cmd_out.stderr.is_empty() { 85 | writeln!(output, "{}:", "stderr".white().italic())?; 86 | writeln!(output, "{}", OUTPUT_SEPARATOR.bright_black())?; 87 | output.write_all(&cmd_out.stderr)?; 88 | writeln!(output, "{}", OUTPUT_SEPARATOR.bright_black())?; 89 | } 90 | } 91 | } 92 | } 93 | Ok(()) 94 | } 95 | 96 | /// Shows command-line help about the given command. 97 | pub fn get_args_help( 98 | cmd: &str, 99 | config: &Config, 100 | verbose: bool, 101 | output: &mut Output, 102 | ) -> Result<()> { 103 | if cmd.trim().is_empty() { 104 | return Ok(()); 105 | } 106 | if let Some(ref args) = config.check_args { 107 | if args.is_empty() { 108 | return Ok(()); 109 | } 110 | for arg_variants in [ 111 | (config.check_version).then(|| &args[0]), 112 | (config.check_help && args.len() >= 2).then(|| &args[1]), 113 | ] 114 | .iter() 115 | .flatten() 116 | { 117 | check_args( 118 | cmd, 119 | arg_variants.iter().map(|v| v.as_str()), 120 | verbose, 121 | config 122 | .timeout 123 | .unwrap_or_else(|| Config::default().timeout.unwrap_or_default()), 124 | output, 125 | )?; 126 | } 127 | } 128 | Ok(()) 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | use crate::helper::args::common::{HelpArg, VersionArg}; 135 | use pretty_assertions::assert_eq; 136 | use std::path::PathBuf; 137 | 138 | /// Returns the path of the test binary. 139 | fn get_test_bin() -> String { 140 | PathBuf::from(env!("CARGO_MANIFEST_DIR")) 141 | .join("target") 142 | .join("debug") 143 | .join(concat!(env!("CARGO_PKG_NAME"), "-test")) 144 | .to_string_lossy() 145 | .to_string() 146 | } 147 | 148 | #[test] 149 | fn test_check_version_args() -> Result<()> { 150 | let mut output = Vec::new(); 151 | check_args( 152 | &get_test_bin(), 153 | VersionArg::variants().iter().map(|v| v.as_str()), 154 | false, 155 | 5, 156 | &mut output, 157 | )?; 158 | println!("{}", String::from_utf8_lossy(&output)); 159 | assert_eq!( 160 | r"(°ロ°) checking 'test -v' 161 | (×﹏×) fail '-v' argument not found. 162 | (°ロ°) checking 'test -V' 163 | \(^ヮ^)/ success '-V' argument found! 164 | --- 165 | halp 0.1.0 166 | ---", 167 | String::from_utf8_lossy(&output) 168 | .replace('\r', "") 169 | .replace(&get_test_bin(), "test") 170 | .replace(env!("CARGO_PKG_VERSION"), "0.1.0") 171 | .trim() 172 | ); 173 | 174 | Ok(()) 175 | } 176 | 177 | #[test] 178 | fn test_check_help_args() -> Result<()> { 179 | let mut output = Vec::new(); 180 | check_args( 181 | &get_test_bin(), 182 | HelpArg::variants().iter().rev().map(|v| v.as_str()), 183 | true, 184 | 5, 185 | &mut output, 186 | )?; 187 | assert_eq!( 188 | r"(°ロ°) checking 'test -H' 189 | (×﹏×) fail '-H' argument not found. 190 | (o_O) debug 191 | stdout: 192 | --- 193 | error: unexpected argument '-H' found 194 | 195 | Usage: test 196 | 197 | For more information, try '--help'. 198 | --- 199 | (°ロ°) checking 'test help' 200 | (×﹏×) fail 'help' argument not found. 201 | (o_O) debug 202 | stdout: 203 | --- 204 | error: unexpected argument 'help' found 205 | 206 | Usage: test 207 | 208 | For more information, try '--help'. 209 | --- 210 | (°ロ°) checking 'test --help' 211 | \(^ヮ^)/ success '--help' argument found! 212 | --- 213 | Usage: test 214 | 215 | Options: 216 | -h, --help Print help 217 | -V, --version Print version 218 | ---", 219 | String::from_utf8_lossy(&output) 220 | .replace('\r', "") 221 | .replace(&get_test_bin(), "test") 222 | .trim() 223 | ); 224 | 225 | Ok(()) 226 | } 227 | 228 | #[test] 229 | fn test_get_default_help() -> Result<()> { 230 | let config = Config::default(); 231 | let mut output = Vec::new(); 232 | get_args_help(&get_test_bin(), &config, false, &mut output)?; 233 | println!("{}", String::from_utf8_lossy(&output)); 234 | assert_eq!( 235 | r"(°ロ°) checking 'test -v' 236 | (×﹏×) fail '-v' argument not found. 237 | (°ロ°) checking 'test -V' 238 | \(^ヮ^)/ success '-V' argument found! 239 | --- 240 | halp 0.1.0 241 | --- 242 | (°ロ°) checking 'test -h' 243 | \(^ヮ^)/ success '-h' argument found! 244 | --- 245 | Usage: test 246 | 247 | Options: 248 | -h, --help Print help 249 | -V, --version Print version 250 | ---", 251 | String::from_utf8_lossy(&output) 252 | .replace('\r', "") 253 | .replace(&get_test_bin(), "test") 254 | .replace(env!("CARGO_PKG_VERSION"), "0.1.0") 255 | .trim() 256 | ); 257 | Ok(()) 258 | } 259 | 260 | #[test] 261 | fn test_get_args_help() -> Result<()> { 262 | let config = Config { 263 | check_args: Some(vec![vec![String::from("-x")], vec![String::from("-V")]]), 264 | ..Default::default() 265 | }; 266 | let mut output = Vec::new(); 267 | get_args_help(&get_test_bin(), &config, false, &mut output)?; 268 | println!("{}", String::from_utf8_lossy(&output)); 269 | assert_eq!( 270 | r"(°ロ°) checking 'test -x' 271 | (×﹏×) fail '-x' argument not found. 272 | (°ロ°) checking 'test -V' 273 | \(^ヮ^)/ success '-V' argument found! 274 | --- 275 | halp 0.1.0 276 | ---", 277 | String::from_utf8_lossy(&output) 278 | .replace('\r', "") 279 | .replace(&get_test_bin(), "test") 280 | .replace(env!("CARGO_PKG_VERSION"), "0.1.0") 281 | .trim() 282 | ); 283 | Ok(()) 284 | } 285 | 286 | #[test] 287 | fn test_do_nothing() -> Result<()> { 288 | let config = Config { 289 | check_version: false, 290 | check_help: false, 291 | ..Default::default() 292 | }; 293 | let mut output = Vec::new(); 294 | get_args_help("", &config, false, &mut output)?; 295 | assert!(String::from_utf8_lossy(&output).is_empty()); 296 | Ok(()) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |

A CLI tool to get help with CLI tools 🐙

8 | 9 | GitHub Release 10 | Crate Release 11 | Coverage 12 |
13 | Continuous Integration 14 | Continuous Deployment 15 | Docker Builds 16 | Documentation 17 | 18 | halp demo 19 | 20 |
21 | 22 | `halp` aims to help find the **correct arguments** for command-line tools by checking the predefined list of commonly used options/flags. Additionally, it provides a prompt for quick access to the **manual page** or **cheat sheet** of the given command. 23 | 24 | If you deal with command-line tools often, it might take some time to figure out how to get help or check the version of a particular command (especially when shell completions are not available). In that case, you might try the most-known flags such as `-h` and `-v` but unfortunately not all the command-line tools follow these conventions (either due to conflicts with other flags or they just use another form). Instead of _brute-forcing_ manually into getting help, you can run `halp ` and it will check the following arguments for you: 25 | 26 | - for **help**: `-h`, `--help`, `help`, `-H` 27 | - for **version info**: `-v`, `-V`, `--version`, `version` 28 | 29 | If one of these arguments succeeds (with exit code 0), it prints the output and exits. This way, you can get informed about the version and help in one single command. You can also customize this list with a configuration file or provide a list of arguments via command-line arguments. 30 | 31 | On the other hand, if you _really_ need help, you can use the `plz` subcommand which will prompt a selection for: 32 | 33 | 1. show the **man page** (runs [`man(1)`](https://man7.org/linux/man-pages/man1/man.1.html)) 34 | 2. show the **cheat sheet** (via [`cheat.sh`](http://cheat.sh)) 35 | 36 |
37 | Table of Contents 38 | 39 | 40 | 41 | - [Example](#example) 42 | - [Installation](#installation) 43 | - [Cargo](#cargo) 44 | - [Arch Linux](#arch-linux) 45 | - [Docker](#docker) 46 | - [Images](#images) 47 | - [Usage](#usage) 48 | - [Building](#building) 49 | - [Binary releases](#binary-releases) 50 | - [Build from source](#build-from-source) 51 | - [Usage](#usage-1) 52 | - [`plz`](#plz) 53 | - [Examples](#examples) 54 | - [Check `help` and `version` (default)](#check-help-and-version-default) 55 | - [Check a custom argument](#check-a-custom-argument) 56 | - [Disable defaults](#disable-defaults) 57 | - [Verbose logging](#verbose-logging) 58 | - [Get additional help (via `plz`)](#get-additional-help-via-plz) 59 | - [Custom pager](#custom-pager) 60 | - [Custom cheat.sh host URL](#custom-cheatsh-host-url) 61 | - [Configuration](#configuration) 62 | - [Funding](#funding) 63 | - [Contributing](#contributing) 64 | - [License](#license) 65 | - [Copyright](#copyright) 66 | 67 | 68 | 69 |
70 | 71 | ### Example 72 | 73 | Have you ever experienced this: 74 | 75 | ```sh 76 | $ cli_tool -v 77 | unknown flag -v 78 | ``` 79 | 80 | ```sh 81 | $ cli_tool -V 82 | unknown flag -V 83 | ``` 84 | 85 | ```sh 86 | $ cli_tool -h 87 | unknown flag -h 88 | ``` 89 | 90 | ```sh 91 | $ asdjw1jwhdajh1idojad # frustration 92 | bash: asdjw1jwhdajh1idojad: command not found 93 | ``` 94 | 95 | ```sh 96 | $ cli_tool --help # f*cking finally! 97 | Some CLI Tool Version 1.42.69 98 | Usage: 99 | cli_tool [--parameter1 value1 --parameter2 value2 ...] 100 | ``` 101 | 102 | Whereas with `halp`: 103 | 104 | ``` 105 | $ halp cli_tool 106 | 107 | (°ロ°) checking 'cli_tool -v' 108 | (×﹏×) fail '-v' argument not found. 109 | (°ロ°) checking 'cli_tool -V' 110 | (×﹏×) fail '-V' argument not found. 111 | (°ロ°) checking 'cli_tool -h' 112 | (×﹏×) fail '-h' argument not found. 113 | (°ロ°) checking 'cli_tool --help' 114 | \(^ヮ^)/ success '--help' argument found! 115 | 116 | Some CLI Tool Version 1.42.69 117 | Usage: 118 | cli_tool [--parameter1 value1 --parameter2 value2 ...] 119 | ``` 120 | 121 | ## Installation 122 | 123 |
124 | Packaging status 125 | 126 | [![Packaging status](https://repology.org/badge/vertical-allrepos/halp.svg)](https://repology.org/project/halp/versions) 127 | 128 |
129 | 130 | ### Cargo 131 | 132 | `halp` can be installed from [crates.io](https://crates.io/crates/halp): 133 | 134 | ```sh 135 | cargo install halp 136 | ``` 137 | 138 | The minimum supported Rust version is `1.74.1`. 139 | 140 | ### Arch Linux 141 | 142 | `halp` can be installed from the [community repository](https://archlinux.org/packages/community/x86_64/halp/) using [pacman](https://wiki.archlinux.org/title/Pacman): 143 | 144 | ```sh 145 | pacman -S halp 146 | ``` 147 | 148 | Or you can install the available [AUR packages](https://aur.archlinux.org/packages/?O=0&SeB=b&K=halp&outdated=&SB=n&SO=a&PP=50&do_Search=Go) using an [AUR helper](https://wiki.archlinux.org/index.php/AUR_helpers). For example, 149 | 150 | ```sh 151 | paru -S halp-git 152 | ``` 153 | 154 | Alternatively, you can clone the AUR package and then build it with [makepkg](https://wiki.archlinux.org/index.php/Makepkg). For example, 155 | 156 | ```sh 157 | git clone https://aur.archlinux.org/halp-git.git && cd halp-git && makepkg -si 158 | ``` 159 | 160 | ### Docker 161 | 162 | #### Images 163 | 164 | Docker builds are [automated](./.github/workflows/docker.yml) and images are available in the following registries: 165 | 166 | - [Docker Hub](https://hub.docker.com/r/orhunp/halp) 167 | - [GitHub Container Registry](https://github.com/orhun/halp/pkgs/container/halp) 168 | 169 | #### Usage 170 | 171 | The following commands can be used to get help for a binary inside the container: 172 | 173 | ```sh 174 | docker run --rm -it "orhunp/halp:${TAG:-latest}" whoami 175 | docker run --rm -it "orhunp/halp:${TAG:-latest}" plz whoami 176 | ``` 177 | 178 | Or you can provide a custom binary as follows (please note that you might get shared library errors): 179 | 180 | ```sh 181 | docker run -v "bin:/app/bin:rw" --rm -it "orhunp/halp:${TAG:-latest}" -v ./bin 182 | ``` 183 | 184 | #### Building 185 | 186 | Custom Docker images can be built from the [Dockerfile](./Dockerfile): 187 | 188 | ```sh 189 | docker build -t halp . 190 | ``` 191 | 192 | ### Binary releases 193 | 194 | See the available binaries for different targets from the [releases page](https://github.com/orhun/halp/releases). They are automated via [Continuous Deployment](.github/workflows/cd.yml) workflow 195 | 196 | Release tarballs are signed with the following PGP key: [0xFB41AE0358378256](https://keyserver.ubuntu.com/pks/lookup?search=0xFB41AE0358378256&op=vindex) 197 | 198 | ### Build from source 199 | 200 | 1. Clone the repository. 201 | 202 | ```sh 203 | git clone https://github.com/orhun/halp && cd halp/ 204 | ``` 205 | 206 | 2. Build. 207 | 208 | ```sh 209 | CARGO_TARGET_DIR=target cargo build --release 210 | ``` 211 | 212 | Binary will be located at `target/release/halp`. 213 | 214 | ## Usage 215 | 216 | ``` 217 | halp [OPTIONS] 218 | ``` 219 | 220 | ``` 221 | Options: 222 | --check Sets the argument to check 223 | --no-version Disable checking the version information 224 | --no-help Disable checking the help information 225 | -c, --config Sets the configuration file [env: HALP_CONFIG=] 226 | -t, --timeout Sets the timeout for the command [default: 5] 227 | -v, --verbose Enables verbose logging 228 | -h, --help Print help 229 | -V, --version Print version 230 | ``` 231 | 232 | ### `plz` 233 | 234 | ``` 235 | halp [OPTIONS] plz 236 | ``` 237 | 238 | ``` 239 | Options: 240 | -m, --man-cmd Sets the manual page command to run 241 | --cheat-sh-url Use a custom URL for cheat.sh [env: CHEAT_SH_URL=] 242 | -p, --pager Sets the pager to use 243 | --no-pager Disables the pager 244 | -h, --help Print help 245 | ``` 246 | 247 | ## Examples 248 | 249 | #### Check `help` and `version` (default) 250 | 251 | ```sh 252 | halp whoami 253 | ``` 254 | 255 | ![halp example I](./assets/halp-example1.gif) 256 | 257 | #### Check a custom argument 258 | 259 | ```sh 260 | halp --check "\--silent" zps 261 | ``` 262 | 263 | (You can escape `-` with using `\-`.) 264 | 265 | You can also provide multiple arguments as follows: 266 | 267 | ```sh 268 | halp --check "help" --check "test" menyoki 269 | ``` 270 | 271 | #### Disable defaults 272 | 273 | ```sh 274 | halp --no-version sha512sum 275 | ``` 276 | 277 | ```sh 278 | halp --no-help sha512sum 279 | ``` 280 | 281 | #### Verbose logging 282 | 283 | ```sh 284 | halp --verbose git-cliff 285 | ``` 286 | 287 | This will result in `stderr`/`stdout` being printed if there was an error. For example: 288 | 289 | ```sh 290 | (°ロ°) checking 'git-cliff -v' 291 | (×﹏×) fail '-v' argument not found. 292 | (o_O) debug 293 | stdout: 294 | WARN git_cliff > "cliff.toml" is not found, using the default configuration. 295 | ERROR git_cliff > Git error: `could not find repository from '.'; class=Repository (6); code=NotFound (-3)` 296 | ``` 297 | 298 | #### Get additional help (via `plz`) 299 | 300 | ```sh 301 | halp plz vim 302 | ``` 303 | 304 | ![halp example II](./assets/halp-example2.gif) 305 | 306 | ##### Custom pager 307 | 308 | ```sh 309 | halp plz --pager bat vim 310 | ``` 311 | 312 | To disable the pager: 313 | 314 | ```sh 315 | halp plz --no-pager bat vim 316 | ``` 317 | 318 | ##### Custom cheat.sh host URL 319 | 320 | ```sh 321 | halp plz --cheat-sh-url https://cht.sh vim 322 | ``` 323 | 324 | ## Configuration 325 | 326 | `halp` can be configured with a configuration file that uses the [TOML](https://en.wikipedia.org/wiki/TOML) format. It can be specified via `--config` or `HALP_CONFIG` environment variable. It can also be placed in one of the following global locations: 327 | 328 | - `` `/` `halp.toml` 329 | - `` `/` `halp/halp.toml` 330 | - `` `/` `halp/config` 331 | 332 | `` depends on the platform as shown in the following table: 333 | 334 | | Platform | Value | Example | 335 | | -------- | ------------------------------------- | ---------------------------------------- | 336 | | Linux | `$XDG_CONFIG_HOME` or `$HOME`/.config | /home/orhun/.config | 337 | | macOS | `$HOME`/Library/Application Support | /Users/Orhun/Library/Application Support | 338 | | Windows | `{FOLDERID_RoamingAppData}` | C:\Users\Orhun\AppData\Roaming | 339 | 340 | See [halp.toml](config/halp.toml) for the default configuration values. 341 | 342 | ## Funding 343 | 344 | If you find `halp` and/or other projects on my [GitHub profile](https://github.com/orhun/) useful, consider supporting me on [GitHub Sponsors](https://github.com/sponsors/orhun) or [becoming a patron](https://www.patreon.com/join/orhunp)! 345 | 346 | [![Support me on GitHub Sponsors](https://img.shields.io/github/sponsors/orhun?style=flat&logo=GitHub&labelColor=342a5e&color=684d81&logoColor=white)](https://github.com/sponsors/orhun) 347 | [![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dorhunp%26type%3Dpatrons&style=flat&logo=Patreon&labelColor=342a5e&color=684d81&logoColor=white)](https://patreon.com/join/orhunp) 348 | [![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dorhunp%26type%3Dpledges&style=flat&logo=Patreon&labelColor=342a5e&color=684d81&logoColor=white&label=)](https://patreon.com/join/orhunp) 349 | 350 | ## Contributing 351 | 352 | See our [Contribution Guide](./CONTRIBUTING.md) and please follow the [Code of Conduct](./CODE_OF_CONDUCT.md) in all your interactions with the project. 353 | 354 | ## License 355 | 356 | Licensed under either of [Apache License Version 2.0](./LICENSE-APACHE) or [The MIT License](./LICENSE-MIT) at your option. 357 | 358 | 🦀 ノ( º \_ º ノ) - respect crables! 359 | 360 | ## Copyright 361 | 362 | Copyright © 2023-2024, [Orhun Parmaksız](mailto:orhunparmaksiz@gmail.com) 363 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "anstream" 13 | version = "0.6.14" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 16 | dependencies = [ 17 | "anstyle", 18 | "anstyle-parse", 19 | "anstyle-query", 20 | "anstyle-wincon", 21 | "colorchoice", 22 | "is_terminal_polyfill", 23 | "utf8parse", 24 | ] 25 | 26 | [[package]] 27 | name = "anstyle" 28 | version = "1.0.8" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 31 | 32 | [[package]] 33 | name = "anstyle-parse" 34 | version = "0.2.4" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 37 | dependencies = [ 38 | "utf8parse", 39 | ] 40 | 41 | [[package]] 42 | name = "anstyle-query" 43 | version = "1.1.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 46 | dependencies = [ 47 | "windows-sys 0.52.0", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-wincon" 52 | version = "3.0.3" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 55 | dependencies = [ 56 | "anstyle", 57 | "windows-sys 0.52.0", 58 | ] 59 | 60 | [[package]] 61 | name = "attr_alias" 62 | version = "0.1.3" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "b9dcf97260bbb12df5bb782740a6c735244debba888a821cef4264c07bd11aa1" 65 | 66 | [[package]] 67 | name = "base64" 68 | version = "0.22.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 71 | 72 | [[package]] 73 | name = "bitflags" 74 | version = "2.5.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 77 | 78 | [[package]] 79 | name = "bytes" 80 | version = "1.9.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 83 | 84 | [[package]] 85 | name = "cc" 86 | version = "1.2.11" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" 89 | dependencies = [ 90 | "shlex", 91 | ] 92 | 93 | [[package]] 94 | name = "cfg-if" 95 | version = "1.0.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 98 | 99 | [[package]] 100 | name = "clap" 101 | version = "4.5.53" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" 104 | dependencies = [ 105 | "clap_builder", 106 | "clap_derive", 107 | ] 108 | 109 | [[package]] 110 | name = "clap_builder" 111 | version = "4.5.53" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" 114 | dependencies = [ 115 | "anstream", 116 | "anstyle", 117 | "clap_lex", 118 | "strsim", 119 | "terminal_size", 120 | ] 121 | 122 | [[package]] 123 | name = "clap_complete" 124 | version = "4.5.61" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" 127 | dependencies = [ 128 | "clap", 129 | ] 130 | 131 | [[package]] 132 | name = "clap_derive" 133 | version = "4.5.49" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 136 | dependencies = [ 137 | "heck", 138 | "proc-macro2", 139 | "quote", 140 | "syn", 141 | ] 142 | 143 | [[package]] 144 | name = "clap_lex" 145 | version = "0.7.4" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 148 | 149 | [[package]] 150 | name = "clap_mangen" 151 | version = "0.2.31" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" 154 | dependencies = [ 155 | "clap", 156 | "roff", 157 | ] 158 | 159 | [[package]] 160 | name = "colorchoice" 161 | version = "1.0.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 164 | 165 | [[package]] 166 | name = "colored" 167 | version = "3.0.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 170 | dependencies = [ 171 | "windows-sys 0.59.0", 172 | ] 173 | 174 | [[package]] 175 | name = "console" 176 | version = "0.16.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" 179 | dependencies = [ 180 | "encode_unicode", 181 | "libc", 182 | "once_cell", 183 | "unicode-width", 184 | "windows-sys 0.61.1", 185 | ] 186 | 187 | [[package]] 188 | name = "crc32fast" 189 | version = "1.4.2" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 192 | dependencies = [ 193 | "cfg-if", 194 | ] 195 | 196 | [[package]] 197 | name = "dialoguer" 198 | version = "0.12.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" 201 | dependencies = [ 202 | "console", 203 | "shell-words", 204 | ] 205 | 206 | [[package]] 207 | name = "diff" 208 | version = "0.1.13" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 211 | 212 | [[package]] 213 | name = "dirs" 214 | version = "6.0.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 217 | dependencies = [ 218 | "dirs-sys", 219 | ] 220 | 221 | [[package]] 222 | name = "dirs-sys" 223 | version = "0.5.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 226 | dependencies = [ 227 | "libc", 228 | "option-ext", 229 | "redox_users", 230 | "windows-sys 0.61.1", 231 | ] 232 | 233 | [[package]] 234 | name = "encode_unicode" 235 | version = "1.0.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 238 | 239 | [[package]] 240 | name = "equivalent" 241 | version = "1.0.1" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 244 | 245 | [[package]] 246 | name = "errno" 247 | version = "0.3.9" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 250 | dependencies = [ 251 | "libc", 252 | "windows-sys 0.52.0", 253 | ] 254 | 255 | [[package]] 256 | name = "flate2" 257 | version = "1.0.30" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" 260 | dependencies = [ 261 | "crc32fast", 262 | "miniz_oxide", 263 | ] 264 | 265 | [[package]] 266 | name = "fnv" 267 | version = "1.0.7" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 270 | 271 | [[package]] 272 | name = "getrandom" 273 | version = "0.2.15" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 276 | dependencies = [ 277 | "cfg-if", 278 | "libc", 279 | "wasi", 280 | ] 281 | 282 | [[package]] 283 | name = "halp" 284 | version = "0.2.0" 285 | dependencies = [ 286 | "clap", 287 | "clap_complete", 288 | "clap_mangen", 289 | "colored", 290 | "console", 291 | "dialoguer", 292 | "dirs", 293 | "pretty_assertions", 294 | "process_control", 295 | "serde", 296 | "thiserror", 297 | "toml", 298 | "ureq", 299 | ] 300 | 301 | [[package]] 302 | name = "hashbrown" 303 | version = "0.15.2" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 306 | 307 | [[package]] 308 | name = "heck" 309 | version = "0.5.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 312 | 313 | [[package]] 314 | name = "http" 315 | version = "1.2.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 318 | dependencies = [ 319 | "bytes", 320 | "fnv", 321 | "itoa", 322 | ] 323 | 324 | [[package]] 325 | name = "httparse" 326 | version = "1.10.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 329 | 330 | [[package]] 331 | name = "indexmap" 332 | version = "2.11.4" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 335 | dependencies = [ 336 | "equivalent", 337 | "hashbrown", 338 | ] 339 | 340 | [[package]] 341 | name = "is_terminal_polyfill" 342 | version = "1.70.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 345 | 346 | [[package]] 347 | name = "itoa" 348 | version = "1.0.14" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 351 | 352 | [[package]] 353 | name = "libc" 354 | version = "0.2.155" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 357 | 358 | [[package]] 359 | name = "libredox" 360 | version = "0.1.3" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 363 | dependencies = [ 364 | "bitflags", 365 | "libc", 366 | ] 367 | 368 | [[package]] 369 | name = "linux-raw-sys" 370 | version = "0.4.14" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 373 | 374 | [[package]] 375 | name = "log" 376 | version = "0.4.25" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 379 | 380 | [[package]] 381 | name = "miniz_oxide" 382 | version = "0.7.4" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 385 | dependencies = [ 386 | "adler", 387 | ] 388 | 389 | [[package]] 390 | name = "once_cell" 391 | version = "1.19.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 394 | 395 | [[package]] 396 | name = "option-ext" 397 | version = "0.2.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 400 | 401 | [[package]] 402 | name = "percent-encoding" 403 | version = "2.3.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 406 | 407 | [[package]] 408 | name = "pretty_assertions" 409 | version = "1.4.1" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 412 | dependencies = [ 413 | "diff", 414 | "yansi", 415 | ] 416 | 417 | [[package]] 418 | name = "proc-macro2" 419 | version = "1.0.85" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" 422 | dependencies = [ 423 | "unicode-ident", 424 | ] 425 | 426 | [[package]] 427 | name = "process_control" 428 | version = "5.2.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "21ed1a985a1edcf511a65a9927151fab108e72206a2ee3c62f8946ab73dfc33d" 431 | dependencies = [ 432 | "attr_alias", 433 | "libc", 434 | "signal-hook", 435 | "windows-sys 0.61.1", 436 | ] 437 | 438 | [[package]] 439 | name = "quote" 440 | version = "1.0.36" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 443 | dependencies = [ 444 | "proc-macro2", 445 | ] 446 | 447 | [[package]] 448 | name = "redox_users" 449 | version = "0.5.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 452 | dependencies = [ 453 | "getrandom", 454 | "libredox", 455 | "thiserror", 456 | ] 457 | 458 | [[package]] 459 | name = "ring" 460 | version = "0.17.8" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 463 | dependencies = [ 464 | "cc", 465 | "cfg-if", 466 | "getrandom", 467 | "libc", 468 | "spin", 469 | "untrusted", 470 | "windows-sys 0.52.0", 471 | ] 472 | 473 | [[package]] 474 | name = "roff" 475 | version = "0.2.1" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" 478 | 479 | [[package]] 480 | name = "rustix" 481 | version = "0.38.34" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 484 | dependencies = [ 485 | "bitflags", 486 | "errno", 487 | "libc", 488 | "linux-raw-sys", 489 | "windows-sys 0.52.0", 490 | ] 491 | 492 | [[package]] 493 | name = "rustls" 494 | version = "0.23.23" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" 497 | dependencies = [ 498 | "log", 499 | "once_cell", 500 | "ring", 501 | "rustls-pki-types", 502 | "rustls-webpki", 503 | "subtle", 504 | "zeroize", 505 | ] 506 | 507 | [[package]] 508 | name = "rustls-pki-types" 509 | version = "1.11.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 512 | 513 | [[package]] 514 | name = "rustls-webpki" 515 | version = "0.102.8" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 518 | dependencies = [ 519 | "ring", 520 | "rustls-pki-types", 521 | "untrusted", 522 | ] 523 | 524 | [[package]] 525 | name = "serde" 526 | version = "1.0.228" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 529 | dependencies = [ 530 | "serde_core", 531 | "serde_derive", 532 | ] 533 | 534 | [[package]] 535 | name = "serde_core" 536 | version = "1.0.228" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 539 | dependencies = [ 540 | "serde_derive", 541 | ] 542 | 543 | [[package]] 544 | name = "serde_derive" 545 | version = "1.0.228" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 548 | dependencies = [ 549 | "proc-macro2", 550 | "quote", 551 | "syn", 552 | ] 553 | 554 | [[package]] 555 | name = "serde_spanned" 556 | version = "1.0.3" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" 559 | dependencies = [ 560 | "serde_core", 561 | ] 562 | 563 | [[package]] 564 | name = "shell-words" 565 | version = "1.1.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 568 | 569 | [[package]] 570 | name = "shlex" 571 | version = "1.3.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 574 | 575 | [[package]] 576 | name = "signal-hook" 577 | version = "0.3.17" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 580 | dependencies = [ 581 | "libc", 582 | "signal-hook-registry", 583 | ] 584 | 585 | [[package]] 586 | name = "signal-hook-registry" 587 | version = "1.4.2" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 590 | dependencies = [ 591 | "libc", 592 | ] 593 | 594 | [[package]] 595 | name = "spin" 596 | version = "0.9.8" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 599 | 600 | [[package]] 601 | name = "strsim" 602 | version = "0.11.1" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 605 | 606 | [[package]] 607 | name = "subtle" 608 | version = "2.6.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" 611 | 612 | [[package]] 613 | name = "syn" 614 | version = "2.0.87" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 617 | dependencies = [ 618 | "proc-macro2", 619 | "quote", 620 | "unicode-ident", 621 | ] 622 | 623 | [[package]] 624 | name = "terminal_size" 625 | version = "0.4.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" 628 | dependencies = [ 629 | "rustix", 630 | "windows-sys 0.59.0", 631 | ] 632 | 633 | [[package]] 634 | name = "thiserror" 635 | version = "2.0.17" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 638 | dependencies = [ 639 | "thiserror-impl", 640 | ] 641 | 642 | [[package]] 643 | name = "thiserror-impl" 644 | version = "2.0.17" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 647 | dependencies = [ 648 | "proc-macro2", 649 | "quote", 650 | "syn", 651 | ] 652 | 653 | [[package]] 654 | name = "toml" 655 | version = "0.9.8" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" 658 | dependencies = [ 659 | "indexmap", 660 | "serde_core", 661 | "serde_spanned", 662 | "toml_datetime", 663 | "toml_parser", 664 | "toml_writer", 665 | "winnow", 666 | ] 667 | 668 | [[package]] 669 | name = "toml_datetime" 670 | version = "0.7.3" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 673 | dependencies = [ 674 | "serde_core", 675 | ] 676 | 677 | [[package]] 678 | name = "toml_parser" 679 | version = "1.0.4" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 682 | dependencies = [ 683 | "winnow", 684 | ] 685 | 686 | [[package]] 687 | name = "toml_writer" 688 | version = "1.0.4" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" 691 | 692 | [[package]] 693 | name = "unicode-ident" 694 | version = "1.0.12" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 697 | 698 | [[package]] 699 | name = "unicode-width" 700 | version = "0.2.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 703 | 704 | [[package]] 705 | name = "untrusted" 706 | version = "0.9.0" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 709 | 710 | [[package]] 711 | name = "ureq" 712 | version = "3.1.4" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" 715 | dependencies = [ 716 | "base64", 717 | "flate2", 718 | "log", 719 | "percent-encoding", 720 | "rustls", 721 | "rustls-pki-types", 722 | "ureq-proto", 723 | "utf-8", 724 | "webpki-roots", 725 | ] 726 | 727 | [[package]] 728 | name = "ureq-proto" 729 | version = "0.5.2" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" 732 | dependencies = [ 733 | "base64", 734 | "http", 735 | "httparse", 736 | "log", 737 | ] 738 | 739 | [[package]] 740 | name = "utf-8" 741 | version = "0.7.6" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 744 | 745 | [[package]] 746 | name = "utf8parse" 747 | version = "0.2.2" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 750 | 751 | [[package]] 752 | name = "wasi" 753 | version = "0.11.0+wasi-snapshot-preview1" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 756 | 757 | [[package]] 758 | name = "webpki-roots" 759 | version = "1.0.2" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 762 | dependencies = [ 763 | "rustls-pki-types", 764 | ] 765 | 766 | [[package]] 767 | name = "windows-link" 768 | version = "0.2.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 771 | 772 | [[package]] 773 | name = "windows-sys" 774 | version = "0.52.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 777 | dependencies = [ 778 | "windows-targets", 779 | ] 780 | 781 | [[package]] 782 | name = "windows-sys" 783 | version = "0.59.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 786 | dependencies = [ 787 | "windows-targets", 788 | ] 789 | 790 | [[package]] 791 | name = "windows-sys" 792 | version = "0.61.1" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" 795 | dependencies = [ 796 | "windows-link", 797 | ] 798 | 799 | [[package]] 800 | name = "windows-targets" 801 | version = "0.52.6" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 804 | dependencies = [ 805 | "windows_aarch64_gnullvm", 806 | "windows_aarch64_msvc", 807 | "windows_i686_gnu", 808 | "windows_i686_gnullvm", 809 | "windows_i686_msvc", 810 | "windows_x86_64_gnu", 811 | "windows_x86_64_gnullvm", 812 | "windows_x86_64_msvc", 813 | ] 814 | 815 | [[package]] 816 | name = "windows_aarch64_gnullvm" 817 | version = "0.52.6" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 820 | 821 | [[package]] 822 | name = "windows_aarch64_msvc" 823 | version = "0.52.6" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 826 | 827 | [[package]] 828 | name = "windows_i686_gnu" 829 | version = "0.52.6" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 832 | 833 | [[package]] 834 | name = "windows_i686_gnullvm" 835 | version = "0.52.6" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 838 | 839 | [[package]] 840 | name = "windows_i686_msvc" 841 | version = "0.52.6" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 844 | 845 | [[package]] 846 | name = "windows_x86_64_gnu" 847 | version = "0.52.6" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 850 | 851 | [[package]] 852 | name = "windows_x86_64_gnullvm" 853 | version = "0.52.6" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 856 | 857 | [[package]] 858 | name = "windows_x86_64_msvc" 859 | version = "0.52.6" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 862 | 863 | [[package]] 864 | name = "winnow" 865 | version = "0.7.13" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 868 | 869 | [[package]] 870 | name = "yansi" 871 | version = "1.0.1" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 874 | 875 | [[package]] 876 | name = "zeroize" 877 | version = "1.8.1" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 880 | --------------------------------------------------------------------------------