From 839110368b39ac280cad8ca6ccd0b91c5be2792c Mon Sep 17 00:00:00 2001 From: Filip Razek Date: Mon, 22 Jan 2024 09:10:06 +0100 Subject: [PATCH] feat: Generate shell completions on build --- .github/workflows/CICD.yml | 12 ++ Cargo.lock | 10 ++ Cargo.toml | 10 ++ src/build.rs | 23 ++++ src/cli.rs | 238 ++++++++++++++++++++++++++++++++++++ src/main.rs | 239 +------------------------------------ 6 files changed, 297 insertions(+), 235 deletions(-) create mode 100644 src/build.rs create mode 100644 src/cli.rs diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index c5adaef..8eb6ccd 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -179,6 +179,7 @@ jobs: PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package" ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/" mkdir -p "${ARCHIVE_DIR}" + mkdir -p "${ARCHIVE_DIR}/autocomplete" # Binary cp "${{ steps.bin.outputs.BIN_PATH }}" "$ARCHIVE_DIR" @@ -189,6 +190,12 @@ jobs: # Man page cp "doc/${{ needs.crate_metadata.outputs.name }}.1" "$ARCHIVE_DIR" + # Autocompletion files + cp 'target/${{ matrix.job.target }}/release/build/${{ needs.crate_metadata.outputs.name }}'*/out/'${{ needs.crate_metadata.outputs.name }}.bash' "$ARCHIVE_DIR/autocomplete/" + cp 'target/${{ matrix.job.target }}/release/build/${{ needs.crate_metadata.outputs.name }}'*/out/'${{ needs.crate_metadata.outputs.name }}.fish' "$ARCHIVE_DIR/autocomplete/" + cp 'target/${{ matrix.job.target }}/release/build/${{ needs.crate_metadata.outputs.name }}'*/out/'_${{ needs.crate_metadata.outputs.name }}.ps1' "$ARCHIVE_DIR/autocomplete/" + cp 'target/${{ matrix.job.target }}/release/build/${{ needs.crate_metadata.outputs.name }}'*/out/'_${{ needs.crate_metadata.outputs.name }}' "$ARCHIVE_DIR/autocomplete/" + # base compressed package pushd "${PKG_STAGING}/" >/dev/null case ${{ matrix.job.target }} in @@ -234,6 +241,11 @@ jobs: install -Dm644 'doc/${{ needs.crate_metadata.outputs.name }}.1' "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1" gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1" + # Autocompletion files + install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ needs.crate_metadata.outputs.name }}'*/out/'${{ needs.crate_metadata.outputs.name }}.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ needs.crate_metadata.outputs.name }}" + install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ needs.crate_metadata.outputs.name }}'*/out/'${{ needs.crate_metadata.outputs.name }}.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ needs.crate_metadata.outputs.name }}.fish" + install -Dm644 'target/${{ matrix.job.target }}/release/build/${{ needs.crate_metadata.outputs.name }}'*/out/'_${{ needs.crate_metadata.outputs.name }}' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ needs.crate_metadata.outputs.name }}" + # README and LICENSE install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md" install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT" diff --git a/Cargo.lock b/Cargo.lock index 78015a9..15304b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,15 @@ dependencies = [ "terminal_size", ] +[[package]] +name = "clap_complete" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" +dependencies = [ + "clap", +] + [[package]] name = "clap_lex" version = "0.5.0" @@ -241,6 +250,7 @@ dependencies = [ "anyhow", "assert_cmd", "clap", + "clap_complete", "const_format", "libc", "owo-colors", diff --git a/Cargo.toml b/Cargo.toml index 6001673..1b24f11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/sharkdp/hexyl" version = "0.14.0" edition = "2021" rust-version = "1.66" +build = "src/build.rs" [dependencies] anyhow = "1.0" @@ -30,6 +31,15 @@ assert_cmd = "2.0" predicates = "3.0" pretty_assertions = "1.3.0" +[build-dependencies.clap] +version = "4" +default-features = false +features = ["std", "suggestions", "color", "wrap_help", "cargo", "help", "usage", "error-context"] + +[build-dependencies] +clap_complete = "4.3" +const_format = "0.2" + [profile.release] lto = true codegen-units = 1 diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..a0af2d1 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,23 @@ +#[macro_use] +extern crate clap; + +use clap::ValueEnum; +use clap_complete::{generate_to, Shell}; +use std::env; +use std::io::Error; + +include!("cli.rs"); + +fn main() -> Result<(), Error> { + let outdir = match env::var_os("OUT_DIR") { + None => return Ok(()), + Some(outdir) => outdir, + }; + + let mut cmd = build_cli(); + for &shell in Shell::value_variants() { + generate_to(shell, &mut cmd, "hexyl", &outdir)?; + } + + Ok(()) +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..4d911f2 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,238 @@ +use clap::builder::ArgPredicate; +use clap::{crate_name, crate_version, Arg, ArgAction, ColorChoice, Command}; + +use const_format::formatcp; + +pub const DEFAULT_BLOCK_SIZE: i64 = 512; + +pub fn build_cli() -> Command { + Command::new(crate_name!()) + .color(ColorChoice::Auto) + .max_term_width(90) + .version(crate_version!()) + .about(crate_description!()) + .arg( + Arg::new("FILE") + .help("The file to display. If no FILE argument is given, read from STDIN."), + ) + .arg( + Arg::new("length") + .short('n') + .long("length") + .num_args(1) + .value_name("N") + .help( + "Only read N bytes from the input. The N argument can also include a \ + unit with a decimal prefix (kB, MB, ..) or binary prefix (kiB, MiB, ..), \ + or can be specified using a hex number. \ + The short option '-l' can be used as an alias.\n\ + Examples: --length=64, --length=4KiB, --length=0xff", + ), + ) + .arg( + Arg::new("bytes") + .short('c') + .long("bytes") + .num_args(1) + .value_name("N") + .conflicts_with("length") + .help("An alias for -n/--length"), + ) + .arg( + Arg::new("count") + .short('l') + .num_args(1) + .value_name("N") + .conflicts_with_all(["length", "bytes"]) + .hide(true) + .help("Yet another alias for -n/--length"), + ) + .arg( + Arg::new("skip") + .short('s') + .long("skip") + .num_args(1) + .value_name("N") + .help( + "Skip the first N bytes of the input. The N argument can also include \ + a unit (see `--length` for details)\n\ + A negative value is valid and will seek from the end of the file.", + ), + ) + .arg( + Arg::new("block_size") + .long("block-size") + .num_args(1) + .value_name("SIZE") + .help(formatcp!( + "Sets the size of the `block` unit to SIZE (default is {}).\n\ + Examples: --block-size=1024, --block-size=4kB", + DEFAULT_BLOCK_SIZE + )), + ) + .arg( + Arg::new("nosqueezing") + .short('v') + .long("no-squeezing") + .action(ArgAction::SetFalse) + .help( + "Displays all input data. Otherwise any number of groups of output \ + lines which would be identical to the preceding group of lines, are \ + replaced with a line comprised of a single asterisk.", + ), + ) + .arg( + Arg::new("color") + .long("color") + .num_args(1) + .value_name("WHEN") + .value_parser(["always", "auto", "never", "force"]) + .default_value_if("plain", ArgPredicate::IsPresent, Some("never")) + .default_value("always") + .help( + "When to use colors. The 'auto' mode only displays colors if the output \ + goes to an interactive terminal. 'force' can be used to override the \ + NO_COLOR environment variable.", + ), + ) + .arg( + Arg::new("border") + .long("border") + .num_args(1) + .value_name("STYLE") + .value_parser(["unicode", "ascii", "none"]) + .default_value_if("plain", ArgPredicate::IsPresent, Some("none")) + .default_value("unicode") + .help( + "Whether to draw a border with Unicode characters, ASCII characters, \ + or none at all", + ), + ) + .arg(Arg::new("plain").short('p').long("plain").action(ArgAction::SetTrue).help( + "Display output with --no-characters, --no-position, --border=none, and --color=never.", + )) + .arg( + Arg::new("no_chars") + .long("no-characters") + .action(ArgAction::SetFalse) + .help("Do not show the character panel on the right."), + ) + .arg( + Arg::new("chars") + .short('C') + .long("characters") + .overrides_with("no_chars") + .action(ArgAction::SetTrue) + .help("Show the character panel on the right. This is the default, unless --no-characters has been specified."), + ) + .arg( + Arg::new("character-table") + .long("character-table") + .value_name("FORMAT") + .value_parser(["default", "ascii", "codepage-437"]) + .default_value("default") + .help( + "Defines how bytes are mapped to characters:\n \ + \"default\": show printable ASCII characters as-is, '⋄' for NULL bytes, \ + ' ' for space, '_' for other ASCII whitespace, \ + '•' for other ASCII characters, and '×' for non-ASCII bytes.\n \ + \"ascii\": show printable ASCII as-is, ' ' for space, '.' for everything else.\n \ + \"codepage-437\": uses code page 437 (for non-ASCII bytes).\n" + ), + ) + .arg( + Arg::new("no_position") + .short('P') + .long("no-position") + .action(ArgAction::SetFalse) + .help("Whether to display the position panel on the left."), + ) + .arg( + Arg::new("display_offset") + .short('o') + .long("display-offset") + .num_args(1) + .value_name("N") + .help( + "Add N bytes to the displayed file position. The N argument can also \ + include a unit (see `--length` for details)\n\ + A negative value is valid and calculates an offset relative to the \ + end of the file.", + ), + ) + .arg( + Arg::new("panels") + .long("panels") + .num_args(1) + .value_name("N") + .help( + "Sets the number of hex data panels to be displayed. \ + `--panels=auto` will display the maximum number of hex data panels \ + based on the current terminal width. By default, hexyl will show \ + two panels, unless the terminal is not wide enough for that.", + ), + ) + .arg( + Arg::new("group_size") + .short('g') + .long("group-size") + .alias("groupsize") + .num_args(1) + .value_name("N") + .help( + "Number of bytes/octets that should be grouped together. \ + Possible group sizes are 1, 2, 4, 8. The default is 1. You \ + can use the '--endianness' option to control the ordering of \ + the bytes within a group. '--groupsize' can be used as an \ + alias (xxd-compatibility).", + ), + ) + .arg( + Arg::new("endianness") + .long("endianness") + .num_args(1) + .value_name("FORMAT") + .value_parser(["big", "little"]) + .default_value("big") + .help( + "Whether to print out groups in little-endian or big-endian \ + format. This option only has an effect if the '--group-size' \ + is larger than 1. '-e' can be used as an alias for \ + '--endianness=little'.", + ), + ) + .arg( + Arg::new("little_endian_format") + .short('e') + .action(ArgAction::SetTrue) + .overrides_with("endianness") + .hide(true) + .help("An alias for '--endianness=little'."), + ) + .arg( + Arg::new("base") + .short('b') + .long("base") + .num_args(1) + .value_name("B") + .help( + "Sets the base used for the bytes. The possible options are \ + binary, octal, decimal, and hexadecimal. The default base \ + is hexadecimal." + ) + ) + .arg( + Arg::new("terminal_width") + .long("terminal-width") + .num_args(1) + .value_name("N") + .conflicts_with("panels") + .help( + "Sets the number of terminal columns to be displayed.\nSince the terminal \ + width may not be an evenly divisible by the width per hex data column, this \ + will use the greatest number of hex data panels that can fit in the requested \ + width but still leave some space to the right.\nCannot be used with other \ + width-setting options.", + ), + ) +} diff --git a/src/main.rs b/src/main.rs index ddb4500..4a3edd9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,254 +6,23 @@ use std::fs::File; use std::io::{self, prelude::*, BufWriter, SeekFrom}; use std::num::{NonZeroI64, NonZeroU64, NonZeroU8}; -use clap::builder::ArgPredicate; -use clap::{crate_name, crate_version, Arg, ArgAction, ColorChoice, Command}; - use anyhow::{anyhow, Context, Result}; -use const_format::formatcp; - use thiserror::Error as ThisError; use terminal_size::terminal_size; +use cli::DEFAULT_BLOCK_SIZE; + use hexyl::{Base, BorderStyle, CharacterTable, Endianness, Input, PrinterBuilder}; #[cfg(test)] mod tests; -const DEFAULT_BLOCK_SIZE: i64 = 512; +mod cli; fn run() -> Result<()> { - let command = Command::new(crate_name!()) - .color(ColorChoice::Auto) - .max_term_width(90) - .version(crate_version!()) - .about(crate_description!()) - .arg( - Arg::new("FILE") - .help("The file to display. If no FILE argument is given, read from STDIN."), - ) - .arg( - Arg::new("length") - .short('n') - .long("length") - .num_args(1) - .value_name("N") - .help( - "Only read N bytes from the input. The N argument can also include a \ - unit with a decimal prefix (kB, MB, ..) or binary prefix (kiB, MiB, ..), \ - or can be specified using a hex number. \ - The short option '-l' can be used as an alias.\n\ - Examples: --length=64, --length=4KiB, --length=0xff", - ), - ) - .arg( - Arg::new("bytes") - .short('c') - .long("bytes") - .num_args(1) - .value_name("N") - .conflicts_with("length") - .help("An alias for -n/--length"), - ) - .arg( - Arg::new("count") - .short('l') - .num_args(1) - .value_name("N") - .conflicts_with_all(["length", "bytes"]) - .hide(true) - .help("Yet another alias for -n/--length"), - ) - .arg( - Arg::new("skip") - .short('s') - .long("skip") - .num_args(1) - .value_name("N") - .help( - "Skip the first N bytes of the input. The N argument can also include \ - a unit (see `--length` for details)\n\ - A negative value is valid and will seek from the end of the file.", - ), - ) - .arg( - Arg::new("block_size") - .long("block-size") - .num_args(1) - .value_name("SIZE") - .help(formatcp!( - "Sets the size of the `block` unit to SIZE (default is {}).\n\ - Examples: --block-size=1024, --block-size=4kB", - DEFAULT_BLOCK_SIZE - )), - ) - .arg( - Arg::new("nosqueezing") - .short('v') - .long("no-squeezing") - .action(ArgAction::SetFalse) - .help( - "Displays all input data. Otherwise any number of groups of output \ - lines which would be identical to the preceding group of lines, are \ - replaced with a line comprised of a single asterisk.", - ), - ) - .arg( - Arg::new("color") - .long("color") - .num_args(1) - .value_name("WHEN") - .value_parser(["always", "auto", "never", "force"]) - .default_value_if("plain", ArgPredicate::IsPresent, Some("never")) - .default_value("always") - .help( - "When to use colors. The 'auto' mode only displays colors if the output \ - goes to an interactive terminal. 'force' can be used to override the \ - NO_COLOR environment variable.", - ), - ) - .arg( - Arg::new("border") - .long("border") - .num_args(1) - .value_name("STYLE") - .value_parser(["unicode", "ascii", "none"]) - .default_value_if("plain", ArgPredicate::IsPresent, Some("none")) - .default_value("unicode") - .help( - "Whether to draw a border with Unicode characters, ASCII characters, \ - or none at all", - ), - ) - .arg(Arg::new("plain").short('p').long("plain").action(ArgAction::SetTrue).help( - "Display output with --no-characters, --no-position, --border=none, and --color=never.", - )) - .arg( - Arg::new("no_chars") - .long("no-characters") - .action(ArgAction::SetFalse) - .help("Do not show the character panel on the right."), - ) - .arg( - Arg::new("chars") - .short('C') - .long("characters") - .overrides_with("no_chars") - .action(ArgAction::SetTrue) - .help("Show the character panel on the right. This is the default, unless --no-characters has been specified."), - ) - .arg( - Arg::new("character-table") - .long("character-table") - .value_name("FORMAT") - .value_parser(["default", "ascii", "codepage-437"]) - .default_value("default") - .help( - "Defines how bytes are mapped to characters:\n \ - \"default\": show printable ASCII characters as-is, '⋄' for NULL bytes, \ - ' ' for space, '_' for other ASCII whitespace, \ - '•' for other ASCII characters, and '×' for non-ASCII bytes.\n \ - \"ascii\": show printable ASCII as-is, ' ' for space, '.' for everything else.\n \ - \"codepage-437\": uses code page 437 (for non-ASCII bytes).\n" - ), - ) - .arg( - Arg::new("no_position") - .short('P') - .long("no-position") - .action(ArgAction::SetFalse) - .help("Whether to display the position panel on the left."), - ) - .arg( - Arg::new("display_offset") - .short('o') - .long("display-offset") - .num_args(1) - .value_name("N") - .help( - "Add N bytes to the displayed file position. The N argument can also \ - include a unit (see `--length` for details)\n\ - A negative value is valid and calculates an offset relative to the \ - end of the file.", - ), - ) - .arg( - Arg::new("panels") - .long("panels") - .num_args(1) - .value_name("N") - .help( - "Sets the number of hex data panels to be displayed. \ - `--panels=auto` will display the maximum number of hex data panels \ - based on the current terminal width. By default, hexyl will show \ - two panels, unless the terminal is not wide enough for that.", - ), - ) - .arg( - Arg::new("group_size") - .short('g') - .long("group-size") - .alias("groupsize") - .num_args(1) - .value_name("N") - .help( - "Number of bytes/octets that should be grouped together. \ - Possible group sizes are 1, 2, 4, 8. The default is 1. You \ - can use the '--endianness' option to control the ordering of \ - the bytes within a group. '--groupsize' can be used as an \ - alias (xxd-compatibility).", - ), - ) - .arg( - Arg::new("endianness") - .long("endianness") - .num_args(1) - .value_name("FORMAT") - .value_parser(["big", "little"]) - .default_value("big") - .help( - "Whether to print out groups in little-endian or big-endian \ - format. This option only has an effect if the '--group-size' \ - is larger than 1. '-e' can be used as an alias for \ - '--endianness=little'.", - ), - ) - .arg( - Arg::new("little_endian_format") - .short('e') - .action(ArgAction::SetTrue) - .overrides_with("endianness") - .hide(true) - .help("An alias for '--endianness=little'."), - ) - .arg( - Arg::new("base") - .short('b') - .long("base") - .num_args(1) - .value_name("B") - .help( - "Sets the base used for the bytes. The possible options are \ - binary, octal, decimal, and hexadecimal. The default base \ - is hexadecimal." - ) - ) - .arg( - Arg::new("terminal_width") - .long("terminal-width") - .num_args(1) - .value_name("N") - .conflicts_with("panels") - .help( - "Sets the number of terminal columns to be displayed.\nSince the terminal \ - width may not be an evenly divisible by the width per hex data column, this \ - will use the greatest number of hex data panels that can fit in the requested \ - width but still leave some space to the right.\nCannot be used with other \ - width-setting options.", - ), - ); + let command = cli::build_cli(); let matches = command.get_matches();