From 04338e3501afd155af47d0c4bda2c680d2a7a519 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 16 Apr 2020 04:16:40 -0700 Subject: [PATCH] Merge documentation and changelog generation Merge documentation generation into a single binary, `bin/gen`. This includes: The changelog, man pages, the readme, and the book. type: reform --- .github/workflows/build.yaml | 30 ++- .ignore | 7 + CHANGELOG.md | 6 +- Cargo.lock | 124 ++++++---- Cargo.toml | 12 +- README.md | 67 ++++-- bin/changelog/Cargo.toml | 25 -- bin/changelog/src/main.rs | 14 -- bin/changelog/src/opt.rs | 35 --- bin/gen/Cargo.toml | 31 +++ bin/gen/config.yaml | 41 ++++ bin/{man => gen}/src/bin.rs | 9 +- bin/{changelog => gen}/src/changelog.rs | 13 +- bin/gen/src/cmd.rs | 17 ++ bin/gen/src/command_ext.rs | 22 ++ bin/{changelog => gen}/src/common.rs | 13 +- bin/gen/src/config.rs | 17 ++ bin/{changelog => gen}/src/entry.rs | 8 +- bin/gen/src/example.rs | 10 + bin/gen/src/exit_status_ext.rs | 14 ++ bin/gen/src/introduction.rs | 15 ++ bin/{changelog => gen}/src/kind.rs | 0 bin/gen/src/main.rs | 31 +++ bin/{changelog => gen}/src/metadata.rs | 2 +- bin/gen/src/opt.rs | 153 ++++++++++++ bin/gen/src/project.rs | 57 +++++ bin/gen/src/readme.rs | 37 +++ bin/{changelog => gen}/src/release.rs | 0 bin/gen/src/subcommand.rs | 162 +++++++++++++ bin/gen/src/summary.rs | 29 +++ bin/gen/templates/README.md | 140 +++++++++++ bin/gen/templates/SUMMARY.md | 16 ++ bin/gen/templates/introduction.md | 29 +++ bin/generate-completions | 10 - bin/lint | 5 +- bin/man/Cargo.toml | 12 - bin/man/src/common.rs | 12 - bin/man/src/main.rs | 57 ----- bin/man/src/subcommand.rs | 156 ------------ bin/update-readme/Cargo.toml | 11 - bin/update-readme/src/bep.rs | 7 - bin/update-readme/src/common.rs | 15 -- bin/update-readme/src/main.rs | 10 - bin/update-readme/src/opt.rs | 223 ------------------ bin/update-readme/src/status.rs | 78 ------ book/src/SUMMARY.md | 2 +- book/src/commands/imdl-completions.md | 9 +- book/src/commands/imdl-torrent-link.md | 6 +- .../src/commands/imdl-torrent-piece-length.md | 2 +- book/src/introduction.md | 45 +++- completions/{imdl.zsh => _imdl} | 6 +- completions/{imdl.powershell => _imdl.ps1} | 6 +- completions/imdl.bash | 10 +- completions/imdl.elvish | 6 +- completions/imdl.fish | 3 +- justfile | 46 +--- man/imdl-completions.1 | 10 +- man/imdl-torrent-create.1 | 2 +- man/imdl-torrent-link.1 | 8 +- man/imdl-torrent-piece-length.1 | 2 +- man/imdl-torrent-show.1 | 2 +- man/imdl-torrent-stats.1 | 2 +- man/imdl-torrent-verify.1 | 2 +- man/imdl-torrent.1 | 2 +- man/imdl.1 | 2 +- src/common.rs | 5 +- src/env.rs | 5 + src/error.rs | 2 + src/main.rs | 3 + src/out.rs | 11 + src/shell.rs | 34 ++- src/subcommand/completions.rs | 86 ++++++- src/test_env.rs | 4 + 73 files changed, 1239 insertions(+), 866 deletions(-) create mode 100644 .ignore delete mode 100644 bin/changelog/Cargo.toml delete mode 100644 bin/changelog/src/main.rs delete mode 100644 bin/changelog/src/opt.rs create mode 100644 bin/gen/Cargo.toml create mode 100644 bin/gen/config.yaml rename bin/{man => gen}/src/bin.rs (76%) rename bin/{changelog => gen}/src/changelog.rs (86%) create mode 100644 bin/gen/src/cmd.rs create mode 100644 bin/gen/src/command_ext.rs rename bin/{changelog => gen}/src/common.rs (52%) create mode 100644 bin/gen/src/config.rs rename bin/{changelog => gen}/src/entry.rs (86%) create mode 100644 bin/gen/src/example.rs create mode 100644 bin/gen/src/exit_status_ext.rs create mode 100644 bin/gen/src/introduction.rs rename bin/{changelog => gen}/src/kind.rs (100%) create mode 100644 bin/gen/src/main.rs rename bin/{changelog => gen}/src/metadata.rs (96%) create mode 100644 bin/gen/src/opt.rs create mode 100644 bin/gen/src/project.rs create mode 100644 bin/gen/src/readme.rs rename bin/{changelog => gen}/src/release.rs (100%) create mode 100644 bin/gen/src/subcommand.rs create mode 100644 bin/gen/src/summary.rs create mode 100644 bin/gen/templates/README.md create mode 100644 bin/gen/templates/SUMMARY.md create mode 100644 bin/gen/templates/introduction.md delete mode 100755 bin/generate-completions delete mode 100644 bin/man/Cargo.toml delete mode 100644 bin/man/src/common.rs delete mode 100644 bin/man/src/main.rs delete mode 100644 bin/man/src/subcommand.rs delete mode 100644 bin/update-readme/Cargo.toml delete mode 100644 bin/update-readme/src/bep.rs delete mode 100644 bin/update-readme/src/common.rs delete mode 100644 bin/update-readme/src/main.rs delete mode 100644 bin/update-readme/src/opt.rs delete mode 100644 bin/update-readme/src/status.rs rename completions/{imdl.zsh => _imdl} (97%) rename completions/{imdl.powershell => _imdl.ps1} (97%) create mode 100644 src/out.rs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 42e7877..8deee76 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -35,6 +35,15 @@ jobs: steps: - uses: actions/checkout@v2 + if: github.event_name == 'pull_request' + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - uses: actions/checkout@v2 + if: github.event_name != 'pull_request' + with: + fetch-depth: 0 - name: Cache cargo registry uses: actions/cache@v1 @@ -63,7 +72,7 @@ jobs: components: clippy, rustfmt override: true - - name: Version + - name: Info run: | rustup --version cargo --version @@ -79,8 +88,10 @@ jobs: run: cargo clippy --all - name: Lint - if: matrix.os != 'windows-latest' - run: ./bin/lint + if: matrix.os == 'macos-latest' + run: | + brew install ripgrep + ./bin/lint - name: Install Nightly uses: actions-rs/toolchain@v1 @@ -93,16 +104,11 @@ jobs: - name: Check Formatting run: cargo +nightly fmt --all -- --check - - name: Check Completion Scripts - if: matrix.os != 'windows-latest' + - name: Check Generated + if: matrix.os == 'macos-latest' run: | - ./bin/generate-completions - git diff --no-ext-diff --exit-code - - - name: Check Readme Table of Contents - if: matrix.os != 'windows-latest' - run: | - cargo run --package update-readme toc + brew install help2man + cargo run --package gen all git diff --no-ext-diff --exit-code - name: Install `mdbook` diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..394c093 --- /dev/null +++ b/.ignore @@ -0,0 +1,7 @@ +/CHANGELOG.md +/README.md +/completions +/man +/book/src/commands +/book/src/SUMMARY.md +/book/src/introduction.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b0c40..bee2b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ Changelog ========= -UNRELEASED - 2020-04-13 +UNRELEASED - 2020-04-18 ----------------------- -- :sparkles: [`xxxxxxxxxxxx`](https://github.com/casey/intermodal/commits/master) Partially implement BEP 53 - Fixes [#245](https://github.com/casey/intermodal/issues/245) - _strickinato _ +- :art: [`xxxxxxxxxxxx`](https://github.com/casey/intermodal/commits/master) Merge documentation and changelog generation - _Casey Rodarmor _ +- :books: [`1f8023d13a39`](https://github.com/casey/intermodal/commit/1f8023d13a399e381176c20bbb6a71763b7c352a) Fix directory link in README - _Matt Boulanger _ +- :sparkles: [`cb8b5a691945`](https://github.com/casey/intermodal/commit/cb8b5a691945b8108676f95d2888774263be8cc8) Partially implement BEP 53 - Fixes [#245](https://github.com/casey/intermodal/issues/245) - _strickinato _ - :books: [`6185d6c8a27c`](https://github.com/casey/intermodal/commit/6185d6c8a27c0d603f0434e98000c8e4a868dcc8) Add table of packages to readme ([#372](https://github.com/casey/intermodal/pull/372)) - Fixes [#369](https://github.com/casey/intermodal/issues/369) - _Casey Rodarmor _ - :wrench: [`ddf097c83690`](https://github.com/casey/intermodal/commit/ddf097c8369002748992165f81e9a1bdbe6eff98) Fix `publish` recipe ([#368](https://github.com/casey/intermodal/pull/368)) - _Casey Rodarmor _ diff --git a/Cargo.lock b/Cargo.lock index 4e8f810..873091b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,6 +42,49 @@ dependencies = [ "nodrop", ] +[[package]] +name = "askama" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a1fb9e41eb366cbcd267da2094be5b7e62fdbca9f82091e7503e80f885050d" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", +] + +[[package]] +name = "askama_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1012c270085fa35ece6a48a569544fde85b6d9ee41074c7b706cc912a03f939" +dependencies = [ + "askama_shared", + "nom", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a577aeba5fec1aafb9f195d98cfcc38a78b588e4ebf9b15f62ca1c7aa33795a" + +[[package]] +name = "askama_shared" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee517f4e33c27b129928e71d8a044d54c513e72e0b72ec5c4f5f1823e9de353" +dependencies = [ + "askama_escape", + "humansize", + "num-traits", + "serde", + "toml", +] + [[package]] name = "atty" version = "0.2.14" @@ -133,23 +176,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -[[package]] -name = "changelog" -version = "0.0.0" -dependencies = [ - "anyhow", - "cargo_toml", - "chrono", - "fehler", - "git2", - "serde", - "serde_yaml", - "structopt", - "strum", - "strum_macros", - "url", -] - [[package]] name = "chrono" version = "0.4.11" @@ -313,6 +339,29 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +[[package]] +name = "gen" +version = "0.0.0" +dependencies = [ + "anyhow", + "askama", + "cargo_toml", + "chrono", + "fehler", + "git2", + "globset", + "log", + "pretty_env_logger", + "regex", + "serde", + "serde_yaml", + "structopt", + "strum", + "strum_macros", + "tempfile", + "url", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -339,12 +388,6 @@ dependencies = [ "url", ] -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - [[package]] name = "globset" version = "0.4.5" @@ -376,6 +419,12 @@ dependencies = [ "libc", ] +[[package]] +name = "humansize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" + [[package]] name = "humantime" version = "1.3.0" @@ -532,16 +581,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "man" -version = "0.0.0" -dependencies = [ - "anyhow", - "fehler", - "regex", - "tempfile", -] - [[package]] name = "matches" version = "0.1.8" @@ -572,6 +611,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "num-integer" version = "0.1.42" @@ -1143,15 +1192,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" -[[package]] -name = "update-readme" -version = "0.0.0" -dependencies = [ - "glob", - "regex", - "structopt", -] - [[package]] name = "url" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index 508b1f9..758775d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,15 +62,9 @@ temptree = "0.0.0" [workspace] members = [ + # generate documentation + "bin/gen", + # run commands for demo animation "bin/demo", - - # generate table of contents and table of supported BEPs in README.md - "bin/update-readme", - - # generate the changelog from git commits - "bin/changelog", - - # generate man page - "bin/man" ] diff --git a/README.md b/README.md index dd610af..9c42144 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# intermodal: a 40' shipping container for the Internet +# Intermodal: A 40' shipping container for the Internet [![Crate](https://img.shields.io/crates/v/imdl.svg?logo=rust)](https://crates.io/crates/imdl) [![Build](https://github.com/casey/intermodal/workflows/Build/badge.svg)](https://github.com/casey/intermodal/actions) @@ -13,37 +13,34 @@ For more about the project and its goals, check out [this post](https://rodarmor ![demonstration animation](https://raw.githubusercontent.com/casey/intermodal/master/www/demo.gif) -## Manual +## Table of Contents -- [General](#general) - - [Installation](#installation) - - [Supported Operating Systems](#supported-operating-systems) - - [Packages](#packages) - - [Pre-built binaries](#pre-built-binaries) - - [Cargo](#cargo) - - [Shell Completion Scripts](#shell-completion-scripts) - - [Semantic Versioning](#semantic-versioning) - - [Unstable Features](#unstable-features) - - [Source Signatures](#source-signatures) +- [Installation](#installation) + - [Supported Operating Systems](#supported-operating-systems) + - [Packages](#packages) + - [Pre-built binaries](#pre-built-binaries) + - [Cargo](#cargo) +- [Shell Completion Scripts](#shell-completion-scripts) +- [Semantic Versioning](#semantic-versioning) +- [Unstable Features](#unstable-features) +- [Source Signatures](#source-signatures) - [Acknowledgments](#acknowledgments) -## General +## Installation -### Installation - -#### Supported Operating Systems +### Supported Operating Systems `imdl` supports Linux, MacOS, and Windows, and should work on other unix OSes. If it does not, please open an issue! -#### Packages +### Packages | Operating System | Package Manager | Package | Command | |:--------------------------------------------------------------------:|:-----------------------------------:|:-------------------------------------------------------------------------:|---------------------:| | [Various](https://forge.rust-lang.org/release/platform-support.html) | [Cargo](https://www.rust-lang.org) | [imdl](https://crates.io/crates/imdl) | `cargo install imdl` | | [Arch Linux](https://www.archlinux.org) | [Yay](https://github.com/Jguer/yay) | [intermodal](https://aur.archlinux.org/packages/intermodal)AUR | `yay -S intermodal` | -#### Pre-built binaries +### Pre-built binaries Pre-built binaries for Linux, macOS, and Windows can be found on [the releases page](https://github.com/casey/intermodal/releases). @@ -79,17 +76,29 @@ For `fish`, it should be done in `~/.config/fish/config.fish`: echo 'set -gx PATH ~/bin $PATH' >> ~/.config/fish/config.fish ``` -#### Cargo +### Cargo `imdl` is written in [Rust](https://www.rust-lang.org/) and can be built from source and installed with `cargo install imdl`. To get Rust, use the [rustup installer](https://rustup.rs/). -### Shell Completion Scripts +## Shell Completion Scripts Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are -available in the [completions directory](./completions). Please refer to your -shell's documentation for how to install them. +available in the [completions directory](./completions), included in all +[binary releases](https://github.com/casey/imdl/releases). + +For Bash, move `imdl.bash` to `$XDG_CONFIG_HOME/bash_completion` or +`/etc/bash_completion.d/`. + +For Fish, move `imdl.fish` to `$HOME/.config/fish/completions/`. + +For the Z shell, move `_imdl` to one of your `$fpath` directories. + +For PowerShell, add `. _imdl.ps1` to your PowerShell +[profile](https://technet.microsoft.com/en-us/library/bb613488(v=vs.85).aspx) +(note the leading period). If the `_imdl.ps1` file is not on your `PATH`, do +`. /path/to/_imdl.ps1` instead. The `imdl` binary can also generate the same completion scripts at runtime, using the `completions` command: @@ -98,7 +107,15 @@ using the `completions` command: $ imdl completions --shell bash > imdl.bash ``` -### Semantic Versioning +The `--dir` argument can be used to write a completion script into a directory +with a filename that's appropriate for the shell. For example, the following +command will write the Z shell completion script to `$fpath[0]/_imdl`: + +```sh +$ imdl completions --shell zsh --dir $fpath[0] +``` + +## Semantic Versioning Intermodal follows [semantic versioning](https://semver.org/). @@ -110,14 +127,14 @@ In particular: - vX.Y.Z: Breaking changes may only be introduced with a major version number bump -### Unstable Features +## Unstable Features To avoid premature stabilization and excessive version churn, unstable features are unavailable unless the `--unstable` / `-u` flag is passed, for example `imdl --unstable torrent create .`. Unstable features may be changed or removed at any time. -### Source Signatures +## Source Signatures All commits to the intermodal master branch signed with Casey Rodarmor's PGP key with fingerprint `3259DAEDB29636B0E2025A70556186B153EC6FE0`, which can be diff --git a/bin/changelog/Cargo.toml b/bin/changelog/Cargo.toml deleted file mode 100644 index 48ea233..0000000 --- a/bin/changelog/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "changelog" -version = "0.0.0" -authors = ["Casey Rodarmor "] -edition = "2018" -publish = false - -[dependencies] -anyhow = "1.0.28" -cargo_toml = "0.8.0" -chrono = "0.4.11" -fehler = "1.0.0" -git2 = "0.13.1" -serde_yaml = "0.8.11" -structopt = "0.3.12" -strum = "0.18.0" -strum_macros = "0.18.0" - -[dependencies.serde] -version = "1.0.106" -features = ["derive"] - -[dependencies.url] -version = "2.1.1" -features = ["serde"] diff --git a/bin/changelog/src/main.rs b/bin/changelog/src/main.rs deleted file mode 100644 index a88b819..0000000 --- a/bin/changelog/src/main.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::common::*; - -mod changelog; -mod common; -mod entry; -mod kind; -mod metadata; -mod opt; -mod release; - -#[throws] -fn main() { - Opt::from_args().run()?; -} diff --git a/bin/changelog/src/opt.rs b/bin/changelog/src/opt.rs deleted file mode 100644 index bbe36ca..0000000 --- a/bin/changelog/src/opt.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::common::*; - -#[derive(StructOpt)] -pub(crate) enum Opt { - IssueTemplate, - Types, - Update, -} - -impl Opt { - #[throws] - pub(crate) fn run(self) { - match self { - Self::Types => { - for kind in Kind::VARIANTS { - println!("{}", kind) - } - } - Self::IssueTemplate => { - println!("{}", Metadata::default().to_string()); - } - Self::Update => { - let cwd = env::current_dir()?; - - let repo = Repository::discover(cwd)?; - - let changelog = Changelog::new(&repo)?; - - let dst = repo.workdir().unwrap().join("CHANGELOG.md"); - - fs::write(dst, changelog.to_string())?; - } - } - } -} diff --git a/bin/gen/Cargo.toml b/bin/gen/Cargo.toml new file mode 100644 index 0000000..73ae8a9 --- /dev/null +++ b/bin/gen/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "gen" +version = "0.0.0" +authors = ["Casey Rodarmor "] +edition = "2018" +publish = false + +[dependencies] +anyhow = "1.0.28" +askama = "0.9.0" +cargo_toml = "0.8.0" +chrono = "0.4.11" +fehler = "1.0.0" +git2 = "0.13.1" +globset = "0.4.5" +log = "0.4.8" +pretty_env_logger = "0.4.0" +regex = "1.3.6" +serde_yaml = "0.8.11" +structopt = "0.3.12" +strum = "0.18.0" +strum_macros = "0.18.0" +tempfile = "3.1.0" + +[dependencies.serde] +version = "1.0.106" +features = ["derive"] + +[dependencies.url] +version = "2.1.1" +features = ["serde"] diff --git a/bin/gen/config.yaml b/bin/gen/config.yaml new file mode 100644 index 0000000..f41f060 --- /dev/null +++ b/bin/gen/config.yaml @@ -0,0 +1,41 @@ +changelog: + 1f8023d13a399e381176c20bbb6a71763b7c352a: + type: documentation + +examples: +- command: imdl + text: "The binary is called `imdl`:" + code: "imdl --help" + +- command: imdl torrent + text: "BitTorrent metainfo related functionality is under the `torrent` subcommand:" + code: "imdl torrent --help" + +- command: imdl torrent create + text: "Intermodal can be used to create `.torrent` files:" + code: "imdl torrent create --input foo" + +- command: imdl torrent show + text: "Print information about existing `.torrent` files:" + code: "imdl torrent show --input foo.torrent" + +- command: imdl torrent verify + text: "Verify downloaded torrents:" + code: "imdl torrent verify --input foo.torrent --content foo" + +- command: imdl torrent link + text: "Generate magnet links from `.torrent` files:" + code: "imdl torrent link --input foo.torrent" + +- command: imdl torrent piece-length + text: "Show infromation about the piece length picker:" + code: "imdl torrent piece-length" + +- command: imdl completions + text: "Print completion scripts for the `imdl` binary:" + code: "imdl completions --shell zsh" + +- command: imdl torrent stats + unstable: true + text: "Print information about a collection of torrents:" + code: "imdl --unstable torrent stats --input dir" diff --git a/bin/man/src/bin.rs b/bin/gen/src/bin.rs similarity index 76% rename from bin/man/src/bin.rs rename to bin/gen/src/bin.rs index 326cb07..f57234f 100644 --- a/bin/man/src/bin.rs +++ b/bin/gen/src/bin.rs @@ -1,16 +1,15 @@ use crate::common::*; -#[derive(Debug)] pub(crate) struct Bin { - pub(crate) bin: String, + path: PathBuf, pub(crate) subcommands: Vec, } impl Bin { #[throws] - pub(crate) fn new(bin: &str) -> Self { + pub(crate) fn new(path: &Path) -> Bin { let mut bin = Bin { - bin: bin.into(), + path: path.into(), subcommands: Vec::new(), }; @@ -23,7 +22,7 @@ impl Bin { #[throws] fn add_subcommands(&mut self, command: &mut Vec) { - let subcommand = Subcommand::new(&self.bin, command.clone())?; + let subcommand = Subcommand::new(&self.path, command.clone())?; for name in &subcommand.subcommands { command.push(name.into()); diff --git a/bin/changelog/src/changelog.rs b/bin/gen/src/changelog.rs similarity index 86% rename from bin/changelog/src/changelog.rs rename to bin/gen/src/changelog.rs index c61ecf5..b04b472 100644 --- a/bin/changelog/src/changelog.rs +++ b/bin/gen/src/changelog.rs @@ -6,8 +6,8 @@ pub(crate) struct Changelog { impl Changelog { #[throws] - pub(crate) fn new(repo: &Repository) -> Self { - let mut current = repo.head()?.peel_to_commit()?; + pub(crate) fn new(project: &Project) -> Self { + let mut current = project.repo.head()?.peel_to_commit()?; let mut entries = Vec::new(); @@ -24,7 +24,7 @@ impl Changelog { let manifest_bytes = current .tree()? .get_path("Cargo.toml".as_ref())? - .to_object(&repo)? + .to_object(&project.repo)? .as_blob() .unwrap() .content() @@ -32,7 +32,12 @@ impl Changelog { let manifest = Manifest::from_slice(&manifest_bytes)?; - let entry = Entry::new(¤t, manifest.package.unwrap().version.as_ref(), head)?; + let entry = Entry::new( + ¤t, + manifest.package.unwrap().version.as_ref(), + head, + &project.config, + )?; entries.push(entry); } diff --git a/bin/gen/src/cmd.rs b/bin/gen/src/cmd.rs new file mode 100644 index 0000000..8b03dfb --- /dev/null +++ b/bin/gen/src/cmd.rs @@ -0,0 +1,17 @@ +macro_rules! cmd { + { + $bin:expr, + $($arg:expr),* + $(,)? + } => { + { + let mut command = Command::new($bin); + + $( + command.arg($arg); + )* + + command + } + } +} diff --git a/bin/gen/src/command_ext.rs b/bin/gen/src/command_ext.rs new file mode 100644 index 0000000..aca743e --- /dev/null +++ b/bin/gen/src/command_ext.rs @@ -0,0 +1,22 @@ +use crate::common::*; + +pub(crate) trait CommandExt { + #[throws] + fn out(&mut self) -> String; +} + +impl CommandExt for Command { + #[throws] + fn out(&mut self) -> String { + let output = self + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output()?; + + output.status.into_result()?; + + let text = String::from_utf8(output.stdout)?; + + text + } +} diff --git a/bin/changelog/src/common.rs b/bin/gen/src/common.rs similarity index 52% rename from bin/changelog/src/common.rs rename to bin/gen/src/common.rs index e9ceb1b..09be87a 100644 --- a/bin/changelog/src/common.rs +++ b/bin/gen/src/common.rs @@ -1,15 +1,21 @@ pub(crate) use std::{ cmp::{Ord, PartialOrd}, + collections::{BTreeMap, BTreeSet}, env, fmt::{self, Display, Formatter}, - fs, str, + fs::{self, File}, + path::{Path, PathBuf}, + process::{Command, ExitStatus, Stdio}, + str, }; pub(crate) use anyhow::{anyhow, Error}; +pub(crate) use askama::Template; pub(crate) use cargo_toml::Manifest; pub(crate) use chrono::{DateTime, NaiveDateTime, Utc}; pub(crate) use fehler::{throw, throws}; pub(crate) use git2::{Commit, Repository}; +pub(crate) use regex::Regex; pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use structopt::StructOpt; pub(crate) use strum::VariantNames; @@ -17,5 +23,8 @@ pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr}; pub(crate) use url::Url; pub(crate) use crate::{ - changelog::Changelog, entry::Entry, kind::Kind, metadata::Metadata, opt::Opt, release::Release, + bin::Bin, changelog::Changelog, command_ext::CommandExt, config::Config, entry::Entry, + example::Example, exit_status_ext::ExitStatusExt, introduction::Introduction, kind::Kind, + metadata::Metadata, opt::Opt, project::Project, readme::Readme, release::Release, + subcommand::Subcommand, summary::Summary, }; diff --git a/bin/gen/src/config.rs b/bin/gen/src/config.rs new file mode 100644 index 0000000..7318a27 --- /dev/null +++ b/bin/gen/src/config.rs @@ -0,0 +1,17 @@ +use crate::common::*; + +const PATH: &str = "bin/gen/config.yaml"; + +#[derive(Deserialize)] +pub(crate) struct Config { + pub(crate) changelog: BTreeMap, + pub(crate) examples: Vec, +} + +impl Config { + #[throws] + pub(crate) fn load(root: &Path) -> Config { + let file = File::open(root.join(PATH))?; + serde_yaml::from_reader(file)? + } +} diff --git a/bin/changelog/src/entry.rs b/bin/gen/src/entry.rs similarity index 86% rename from bin/changelog/src/entry.rs rename to bin/gen/src/entry.rs index dd8b548..c57b961 100644 --- a/bin/changelog/src/entry.rs +++ b/bin/gen/src/entry.rs @@ -12,13 +12,17 @@ pub(crate) struct Entry { impl Entry { #[throws] - pub(crate) fn new(commit: &Commit, version: &str, head: bool) -> Self { + pub(crate) fn new(commit: &Commit, version: &str, head: bool, config: &Config) -> Self { let time = DateTime::::from_utc( NaiveDateTime::from_timestamp(commit.time().seconds(), 0), Utc, ); - let metadata = Metadata::from_commit(commit)?; + let metadata = if let Some(metadata) = config.changelog.get(&commit.id().to_string()) { + metadata.clone() + } else { + Metadata::from_commit(commit)? + }; Entry { version: version.into(), diff --git a/bin/gen/src/example.rs b/bin/gen/src/example.rs new file mode 100644 index 0000000..a482ba9 --- /dev/null +++ b/bin/gen/src/example.rs @@ -0,0 +1,10 @@ +use crate::common::*; + +#[derive(Deserialize, Clone)] +pub(crate) struct Example { + pub(crate) command: String, + #[serde(default)] + pub(crate) unstable: bool, + pub(crate) text: String, + pub(crate) code: String, +} diff --git a/bin/gen/src/exit_status_ext.rs b/bin/gen/src/exit_status_ext.rs new file mode 100644 index 0000000..687ce5d --- /dev/null +++ b/bin/gen/src/exit_status_ext.rs @@ -0,0 +1,14 @@ +use crate::common::*; + +pub(crate) trait ExitStatusExt { + fn into_result(self) -> anyhow::Result<()>; +} + +impl ExitStatusExt for ExitStatus { + #[throws] + fn into_result(self) { + if !self.success() { + throw!(anyhow!(self)); + } + } +} diff --git a/bin/gen/src/introduction.rs b/bin/gen/src/introduction.rs new file mode 100644 index 0000000..b362106 --- /dev/null +++ b/bin/gen/src/introduction.rs @@ -0,0 +1,15 @@ +use crate::common::*; + +#[derive(Template)] +#[template(path = "introduction.md")] +pub(crate) struct Introduction { + pub(crate) examples: Vec, +} + +impl Introduction { + pub(crate) fn new(config: &Config) -> Self { + Self { + examples: config.examples.clone(), + } + } +} diff --git a/bin/changelog/src/kind.rs b/bin/gen/src/kind.rs similarity index 100% rename from bin/changelog/src/kind.rs rename to bin/gen/src/kind.rs diff --git a/bin/gen/src/main.rs b/bin/gen/src/main.rs new file mode 100644 index 0000000..e6e6790 --- /dev/null +++ b/bin/gen/src/main.rs @@ -0,0 +1,31 @@ +use crate::common::*; + +#[macro_use] +mod cmd; + +mod bin; +mod changelog; +mod command_ext; +mod common; +mod config; +mod entry; +mod example; +mod exit_status_ext; +mod introduction; +mod kind; +mod metadata; +mod opt; +mod project; +mod readme; +mod release; +mod subcommand; +mod summary; + +#[throws] +fn main() { + pretty_env_logger::init(); + + let project = Project::load()?; + + Opt::from_args().run(&project)?; +} diff --git a/bin/changelog/src/metadata.rs b/bin/gen/src/metadata.rs similarity index 96% rename from bin/changelog/src/metadata.rs rename to bin/gen/src/metadata.rs index 16b6e08..75b4fa3 100644 --- a/bin/changelog/src/metadata.rs +++ b/bin/gen/src/metadata.rs @@ -1,6 +1,6 @@ use crate::common::*; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Clone)] pub(crate) struct Metadata { #[serde(rename = "type")] pub(crate) kind: Kind, diff --git a/bin/gen/src/opt.rs b/bin/gen/src/opt.rs new file mode 100644 index 0000000..c94051a --- /dev/null +++ b/bin/gen/src/opt.rs @@ -0,0 +1,153 @@ +use crate::common::*; + +#[derive(StructOpt)] +pub(crate) enum Opt { + #[structopt(about("Update all generated docs"))] + All, + #[structopt(about("Generate the changelog"))] + Changelog, + #[structopt(about("Print a commit template to standard output"))] + CommitTemplate, + #[structopt(about("Print possible values for `type` field of commit metadata"))] + CommitTypes, + #[structopt(about("Generate completion scripts"))] + CompletionScripts, + #[structopt(about("Generate readme"))] + Readme, + #[structopt(about("Generate book"))] + Book, + #[structopt(about("Generate man pages"))] + Man, +} + +#[throws] +fn clean_dir(dir: impl AsRef) { + let dir = dir.as_ref(); + + eprintln!("Cleaning `{}`…", dir.display()); + + if dir.is_dir() { + fs::remove_dir_all(dir)?; + } + + fs::create_dir_all(dir)?; +} + +impl Opt { + #[throws] + pub(crate) fn run(self, project: &Project) { + match self { + Self::Changelog => Self::changelog(project)?, + Self::CommitTemplate => { + println!("{}", Metadata::default().to_string()); + } + Self::CommitTypes => { + for kind in Kind::VARIANTS { + println!("{}", kind) + } + } + Self::CompletionScripts => Self::completion_scripts(project)?, + Self::Readme => Self::readme(project)?, + Self::Book => Self::book(project)?, + Self::Man => Self::man(project)?, + Self::All => { + Self::changelog(project)?; + Self::completion_scripts(project)?; + Self::readme(project)?; + Self::book(project)?; + Self::man(project)?; + } + } + } + + #[throws] + pub(crate) fn changelog(project: &Project) { + eprintln!("Generating changelog…"); + let changelog = Changelog::new(&project)?; + + let dst = project.root.join("CHANGELOG.md"); + + fs::write(dst, changelog.to_string())?; + } + + #[throws] + pub(crate) fn completion_scripts(project: &Project) { + eprintln!("Generating completion scripts…"); + let completions = project.root.join("completions"); + + clean_dir(&completions)?; + + cmd!( + "cargo", + "run", + "--package", + "imdl", + "completions", + "--dir", + completions + ) + .status()? + .into_result()?; + } + + #[throws] + pub(crate) fn readme(project: &Project) { + eprintln!("Generating readme…"); + let template = project.root.join("bin/gen/templates/README.md"); + + let readme = Readme::load(&template)?; + + let mut text = readme.render()?; + text.push('\n'); + + fs::write(project.root.join("README.md"), text)?; + } + + #[throws] + pub(crate) fn book(project: &Project) { + eprintln!("Generating book…"); + let commands = project.root.join("book/src/commands/"); + + clean_dir(&commands)?; + + for subcommand in &project.bin.subcommands { + let page = subcommand.page()?; + + let dst = commands.join(format!("{}.md", subcommand.slug())); + + fs::write(dst, page)?; + } + + let summary = Summary::new(&project.bin); + + let mut text = summary.render()?; + text.push('\n'); + + fs::write(project.root.join("book/src/SUMMARY.md"), text)?; + + let introduction = Introduction::new(&project.config); + + let mut text = introduction.render()?; + text.push('\n'); + + fs::write(project.root.join("book/src/introduction.md"), text)?; + } + + #[throws] + pub(crate) fn man(project: &Project) { + eprintln!("Generating man pages…"); + let mans = project.root.join("man"); + + clean_dir(&mans)?; + + for subcommand in &project.bin.subcommands { + let man = subcommand.man()?; + + let dst = mans.join(format!("{}.1", subcommand.slug())); + + eprintln!("Writing man page to `{}`", dst.display()); + + fs::write(dst, man)?; + } + } +} diff --git a/bin/gen/src/project.rs b/bin/gen/src/project.rs new file mode 100644 index 0000000..cd466c3 --- /dev/null +++ b/bin/gen/src/project.rs @@ -0,0 +1,57 @@ +use crate::common::*; + +pub(crate) struct Project { + pub(crate) repo: Repository, + pub(crate) root: PathBuf, + pub(crate) config: Config, + pub(crate) bin: Bin, +} + +impl Project { + #[throws] + pub(crate) fn load() -> Self { + let repo = Repository::discover(env::current_dir()?)?; + + let root = repo + .workdir() + .ok_or_else(|| anyhow!("Repository at `{}` had no workdir", repo.path().display()))? + .to_owned(); + + let config = Config::load(&root)?; + + let bin = Bin::new(&root.join("target/debug/imdl"))?; + + let example_commands = config + .examples + .iter() + .map(|example| example.command.clone()) + .collect::>(); + + let bin_commands = bin + .subcommands + .iter() + .map(|subcommand| subcommand.command_line()) + .collect::>(); + + if example_commands != bin_commands { + println!("Example commands:"); + for command in example_commands { + println!("{}", command); + } + + println!("…don't match bin commands:"); + for command in bin_commands { + println!("{}", command); + } + + throw!(anyhow!("")); + } + + Project { + repo, + root, + config, + bin, + } + } +} diff --git a/bin/gen/src/readme.rs b/bin/gen/src/readme.rs new file mode 100644 index 0000000..53d0859 --- /dev/null +++ b/bin/gen/src/readme.rs @@ -0,0 +1,37 @@ +use crate::common::*; + +#[derive(Template)] +#[template(path = "README.md")] +pub(crate) struct Readme { + pub(crate) table_of_contents: String, +} + +const HEADING_PATTERN: &str = "(?m)^(?P#+) (?P.*)$"; + +impl Readme { + #[throws] + pub(crate) fn load(template: &Path) -> Readme { + let text = fs::read_to_string(template)?; + + let header_re = Regex::new(HEADING_PATTERN)?; + + let mut lines = Vec::new(); + + for captures in header_re.captures_iter(&text).skip(2) { + let marker = captures.name("MARKER").unwrap().as_str(); + let text = captures.name("TEXT").unwrap().as_str(); + let level = marker.len(); + let indentation = " ".repeat((level - 2) * 2); + let slug = text + .to_lowercase() + .replace(' ', "-") + .replace('.', "") + .replace('&', ""); + lines.push(format!("{}- [{}](#{})", indentation, text, slug)); + } + + Readme { + table_of_contents: lines.join("\n"), + } + } +} diff --git a/bin/changelog/src/release.rs b/bin/gen/src/release.rs similarity index 100% rename from bin/changelog/src/release.rs rename to bin/gen/src/release.rs diff --git a/bin/gen/src/subcommand.rs b/bin/gen/src/subcommand.rs new file mode 100644 index 0000000..a6f4390 --- /dev/null +++ b/bin/gen/src/subcommand.rs @@ -0,0 +1,162 @@ +use crate::common::*; + +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub(crate) struct Subcommand { + pub(crate) bin: PathBuf, + pub(crate) command: Vec, + pub(crate) subcommands: Vec, +} + +impl Subcommand { + #[throws] + pub(crate) fn new(bin: &Path, command: Vec) -> Self { + let wide_help = Command::new(bin) + .args(command.as_slice()) + .env("IMDL_TERM_WIDTH", "200") + .arg("--help") + .out()?; + + const MARKER: &str = "\nSUBCOMMANDS:\n"; + + let mut subcommands = Vec::new(); + + if let Some(marker) = wide_help.find(MARKER) { + let block = &wide_help[marker + MARKER.len()..]; + + for line in block.lines() { + let name = line.trim().split_whitespace().next().unwrap(); + subcommands.push(name.into()); + } + } + + Self { + bin: bin.into(), + command, + subcommands, + } + } + + #[throws] + fn help(&self) -> String { + eprintln!("Getting help for `{}`", self.command_line()); + + Command::new(&self.bin) + .args(self.command.as_slice()) + .env("IMDL_TERM_WIDTH", "80") + .arg("--help") + .out()? + } + + #[throws] + pub(crate) fn man(&self) -> String { + let command_line = self.command_line(); + + eprintln!("Generating man page for `{}`", command_line); + + let name = command_line.replace(" ", "\\ "); + + let help = self.help()?; + + let description = if self.command.is_empty() { + "A 40' shipping container for the Internet".to_string() + } else { + help.lines().nth(1).unwrap().into() + }; + + let include = format!( + "\ +[NAME] +\\fB{}\\fR +- {} +", + name, description + ); + + let tmp = tempfile::tempdir()?; + + let include_path = tmp.path().join("include"); + + fs::write(&include_path, include)?; + + let version = cmd!(&self.bin, "--version") + .out()? + .split_whitespace() + .nth(1) + .unwrap() + .to_owned(); + + eprintln!("Running help2man for `{}`", command_line); + + let mut command = self.bin.as_os_str().to_owned(); + for arg in &self.command { + command.push(" "); + command.push(arg); + } + + let output = cmd!( + "help2man", + "--include", + &include_path, + "--manual", + "Intermodal Manual", + "--no-info", + "--source", + &format!("Intermodal {}", version), + command + ) + .out()?; + + let man = output + .replace("📦 ", "\n") + .replace("\n.SS ", "\n.SH ") + .replace("\"USAGE:\"", "\"SYNOPSIS:\""); + + let re = Regex::new(r"(?ms).SH DESCRIPTION.*?.SH").unwrap(); + + let man = re.replace(&man, ".SH").into_owned(); + + man + } + + pub(crate) fn slug(&self) -> String { + let mut slug = self + .bin + .display() + .to_string() + .split('/') + .last() + .unwrap() + .to_owned(); + + for name in &self.command { + slug.push('-'); + slug.push_str(&name); + } + + slug + } + + pub(crate) fn command_line(&self) -> String { + let mut line = self + .bin + .display() + .to_string() + .split('/') + .last() + .unwrap() + .to_owned(); + + for name in &self.command { + line.push(' '); + line.push_str(&name); + } + + line + } + + #[throws] + pub(crate) fn page(&self) -> String { + let help = self.help()?; + format!("# `{}`\n```\n{}\n```", self.command_line(), help) + } +} diff --git a/bin/gen/src/summary.rs b/bin/gen/src/summary.rs new file mode 100644 index 0000000..ec3296d --- /dev/null +++ b/bin/gen/src/summary.rs @@ -0,0 +1,29 @@ +use crate::common::*; + +#[derive(Template)] +#[template(path = "SUMMARY.md")] +pub(crate) struct Summary { + pub(crate) commands: String, +} + +impl Summary { + pub(crate) fn new(bin: &Bin) -> Summary { + let mut lines = Vec::new(); + + lines.push("- [Commands](./commands.md)".to_string()); + + for subcommand in &bin.subcommands { + let slug = subcommand.slug(); + + lines.push(format!( + " - [`{}`](./commands/{}.md)", + subcommand.command_line(), + slug + )) + } + + Summary { + commands: lines.join("\n"), + } + } +} diff --git a/bin/gen/templates/README.md b/bin/gen/templates/README.md new file mode 100644 index 0000000..2818ff5 --- /dev/null +++ b/bin/gen/templates/README.md @@ -0,0 +1,140 @@ +# Intermodal: A 40' shipping container for the Internet + +[![Crate](https://img.shields.io/crates/v/imdl.svg?logo=rust)](https://crates.io/crates/imdl) +[![Build](https://github.com/casey/intermodal/workflows/Build/badge.svg)](https://github.com/casey/intermodal/actions) +[![Book](https://img.shields.io/static/v1?logo=read-the-docs&label=book&message=imdl.io&color=informational)](https://imdl.io/book/) +[![Chat](https://img.shields.io/discord/679283456261226516.svg?logo=discord&color=7289da)](https://discord.gg/HaaT5Qz) + +Intermodal is a user-friendly and featureful command-line BitTorrent metainfo utility. The binary is called `imdl` and runs on Linux, Windows, and macOS. + +At the moment, creation, viewing, and verification of `.torrent` files is supported. + +For more about the project and its goals, check out [this post](https://rodarmor.com/blog/intermodal). + +![demonstration animation](https://raw.githubusercontent.com/casey/intermodal/master/www/demo.gif) + +## Table of Contents + +{{table_of_contents}} + +## Installation + +### Supported Operating Systems + +`imdl` supports Linux, MacOS, and Windows, and should work on other unix OSes. +If it does not, please open an issue! + +### Packages + +| Operating System | Package Manager | Package | Command | +|:--------------------------------------------------------------------:|:-----------------------------------:|:-------------------------------------------------------------------------:|---------------------:| +| [Various](https://forge.rust-lang.org/release/platform-support.html) | [Cargo](https://www.rust-lang.org) | [imdl](https://crates.io/crates/imdl) | `cargo install imdl` | +| [Arch Linux](https://www.archlinux.org) | [Yay](https://github.com/Jguer/yay) | [intermodal](https://aur.archlinux.org/packages/intermodal)AUR | `yay -S intermodal` | + +### Pre-built binaries + +Pre-built binaries for Linux, macOS, and Windows can be found on +[the releases page](https://github.com/casey/intermodal/releases). + +You can use the following command to download the latest binary for Linux, +MacOS, or Windows, just replace `DEST` with the directory where you'd like to +install the `imdl` binary: + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://imdl.io/install.sh | bash -s -- --to DEST +``` + +A good place to install personal binaries is `~/bin`, which `install.sh` uses +when `--to` is not supplied. To create the `~/bin` directory and install `imdl` +there, do: + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://imdl.io/install.sh | bash +``` + +Additionally, you'll have to add `~/bin` to the `PATH` environment variable, +which the system uses to find executables. How to do this depends on the shell. + +For `sh`, `bash`, and `zsh`, it should be done in `~/.profile`: + +```sh +echo 'export PATH=$HOME/bin:$PATH' >> ~/.profile +``` + +For `fish`, it should be done in `~/.config/fish/config.fish`: + +```fish +echo 'set -gx PATH ~/bin $PATH' >> ~/.config/fish/config.fish +``` + +### Cargo + +`imdl` is written in [Rust](https://www.rust-lang.org/) and can be built from +source and installed with `cargo install imdl`. To get Rust, use the +[rustup installer](https://rustup.rs/). + +## Shell Completion Scripts + +Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are +available in the [completions directory](./completions), included in all +[binary releases](https://github.com/casey/imdl/releases). + +For Bash, move `imdl.bash` to `$XDG_CONFIG_HOME/bash_completion` or +`/etc/bash_completion.d/`. + +For Fish, move `imdl.fish` to `$HOME/.config/fish/completions/`. + +For the Z shell, move `_imdl` to one of your `$fpath` directories. + +For PowerShell, add `. _imdl.ps1` to your PowerShell +[profile](https://technet.microsoft.com/en-us/library/bb613488(v=vs.85).aspx) +(note the leading period). If the `_imdl.ps1` file is not on your `PATH`, do +`. /path/to/_imdl.ps1` instead. + +The `imdl` binary can also generate the same completion scripts at runtime, +using the `completions` command: + +```sh +$ imdl completions --shell bash > imdl.bash +``` + +The `--dir` argument can be used to write a completion script into a directory +with a filename that's appropriate for the shell. For example, the following +command will write the Z shell completion script to `$fpath[0]/_imdl`: + +```sh +$ imdl completions --shell zsh --dir $fpath[0] +``` + +## Semantic Versioning + +Intermodal follows [semantic versioning](https://semver.org/). + +In particular: + +- v0.0.X: Breaking changes may be introduced at any time. +- v0.X.Y: Breaking changes may only be introduced with a minor version number + bump. +- vX.Y.Z: Breaking changes may only be introduced with a major version number + bump + +## Unstable Features + +To avoid premature stabilization and excessive version churn, unstable features +are unavailable unless the `--unstable` / `-u` flag is passed, for example +`imdl --unstable torrent create .`. Unstable features may be changed or removed +at any time. + +## Source Signatures + +All commits to the intermodal master branch signed with Casey Rodarmor's PGP +key with fingerprint `3259DAEDB29636B0E2025A70556186B153EC6FE0`, which can be +found +[on keybase](https://keybase.io/rodarmor/pgp_keys.asc?fingerprint=3259daedb29636b0e2025a70556186b153ec6fe0) and on +[his homepage](https://rodarmor.com/static/rodarmor.asc). + +## Acknowledgments + +The formatting of `imdl torrent show` is entirely copied from +[torf](https://github.com/rndusr/torf-cli), an excellent command-line torrent +creator, editor, and viewer. diff --git a/bin/gen/templates/SUMMARY.md b/bin/gen/templates/SUMMARY.md new file mode 100644 index 0000000..dbf4962 --- /dev/null +++ b/bin/gen/templates/SUMMARY.md @@ -0,0 +1,16 @@ +Summary +======= + +[Intermodal](./introduction.md) + +{{commands}} + +- [Bittorrent](./bittorrent.md) + - [Distributing Large Data Sets](./bittorrent/distributing-large-data-sets.md) + - [BEP Support](./bittorrent/bep-support.md) + - [Alternatives & Prior Art](./bittorrent/prior-art.md) + - [UDP Tracker Protocol](./bittorrent/udp-tracker-protocol.md) + - [References](./bittorrent/references.md) + +- [Metadata](./metadata.md) + - [Prior Art](./metadata/prior-art.md) diff --git a/bin/gen/templates/introduction.md b/bin/gen/templates/introduction.md new file mode 100644 index 0000000..e279a45 --- /dev/null +++ b/bin/gen/templates/introduction.md @@ -0,0 +1,29 @@ +# Intermodal: A 40' shipping container for the Internet + +Intermodal is a user-friendly and featureful command-line BitTorrent metainfo utility for Linux, Windows, and macOS. + +Project development is hosted on [GitHub](https://github.com/casey/intermodal). +{%- for example in examples -%} +{%- if !example.unstable %} + +{{example.text}} + +```sh +$ {{example.code}} +``` +{%- endif %} +{%- endfor %} + +Functionality that is not yet finalized, but still available for preview, can be accessed with the `--unstable` flag: + +{%- for example in examples -%} +{%- if example.unstable %} + +{{example.text}} + +```sh +$ {{example.code}} +``` +{%- endif %} +{%- endfor %} +Happy sharing! diff --git a/bin/generate-completions b/bin/generate-completions deleted file mode 100755 index 771af2b..0000000 --- a/bin/generate-completions +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -cargo build - -for script in completions/*; do - shell=${script##*.} - ./target/debug/imdl completions --shell $shell > $script -done diff --git a/bin/lint b/bin/lint index 5d76f88..389270c 100755 --- a/bin/lint +++ b/bin/lint @@ -2,4 +2,7 @@ set -euxo pipefail -! grep --color -REni 'FIXME|TODO|XXX|todo!|#\[ignore]' src book/src +! rg \ + --glob !bin/lint \ + --ignore-case \ + 'FIXME|TODO|XXX|todo!|#\[ignore\]' diff --git a/bin/man/Cargo.toml b/bin/man/Cargo.toml deleted file mode 100644 index 76b84f5..0000000 --- a/bin/man/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "man" -version = "0.0.0" -authors = ["Casey Rodarmor "] -edition = "2018" -publish = false - -[dependencies] -anyhow = "1.0.28" -fehler = "1.0.0" -tempfile = "3.1.0" -regex = "1.3.6" diff --git a/bin/man/src/common.rs b/bin/man/src/common.rs deleted file mode 100644 index ca7406e..0000000 --- a/bin/man/src/common.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub(crate) use std::{ - fs, - path::Path, - process::{Command, Stdio}, - str, -}; - -pub(crate) use anyhow::{anyhow, Error}; -pub(crate) use fehler::{throw, throws}; -pub(crate) use regex::Regex; - -pub(crate) use crate::{bin::Bin, subcommand::Subcommand}; diff --git a/bin/man/src/main.rs b/bin/man/src/main.rs deleted file mode 100644 index 5716e47..0000000 --- a/bin/man/src/main.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::common::*; - -mod bin; -mod common; -mod subcommand; - -#[throws] -fn clean(dir: impl AsRef) { - let dir = dir.as_ref(); - fs::remove_dir_all(dir)?; - fs::create_dir_all(dir)?; -} - -#[throws] -fn write(dst: impl AsRef, contents: &str) { - let dst = dst.as_ref(); - println!("Writing `{}`…", dst.display()); - fs::write(dst, contents)?; -} - -#[throws] -fn main() { - let bin = Bin::new("target/debug/imdl")?; - - clean("man")?; - clean("book/src/commands")?; - - let mut pages = "- [Commands](./commands.md)\n".to_string(); - - for subcommand in bin.subcommands { - let slug = subcommand.slug(); - - let dst = format!("man/{}.1", slug); - write(dst, &subcommand.man)?; - - let dst = format!("book/src/commands/{}.md", slug); - write(dst, &subcommand.page())?; - - pages.push_str(&format!( - " - [`{}`](./commands/{}.md)\n", - subcommand.command_line(), - slug - )) - } - - pages.push('\n'); - - let path = "book/src/SUMMARY.md"; - - let original = fs::read_to_string(path)?; - - let re = Regex::new(r"(?ms)^- \[Commands\]\(./commands.md\).*?\n\n").unwrap(); - - let text = re.replace(&original, pages.as_str()).into_owned(); - - fs::write(path, text)?; -} diff --git a/bin/man/src/subcommand.rs b/bin/man/src/subcommand.rs deleted file mode 100644 index b7ba333..0000000 --- a/bin/man/src/subcommand.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::common::*; - -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] -pub(crate) struct Subcommand { - pub(crate) bin: String, - pub(crate) command: Vec, - pub(crate) help: String, - pub(crate) man: String, - pub(crate) subcommands: Vec, -} - -trait CommandExt { - #[throws] - fn out(&mut self) -> String; -} - -impl CommandExt for Command { - #[throws] - fn out(&mut self) -> String { - let output = self.stdout(Stdio::piped()).output()?; - - if !output.status.success() { - throw!(anyhow!("Command `{:?}` failed: {}", self, output.status)); - } - - let text = String::from_utf8(output.stdout)?; - - text - } -} - -impl Subcommand { - #[throws] - pub(crate) fn new(bin: &str, command: Vec) -> Self { - let wide_help = Command::new(bin) - .args(command.as_slice()) - .env("IMDL_TERM_WIDTH", "200") - .arg("--help") - .out()?; - - const MARKER: &str = "\nSUBCOMMANDS:\n"; - - let mut subcommands = Vec::new(); - - if let Some(marker) = wide_help.find(MARKER) { - let block = &wide_help[marker + MARKER.len()..]; - - for line in block.lines() { - let name = line.trim().split_whitespace().next().unwrap(); - subcommands.push(name.into()); - } - } - - let command_line = format!("{} {}", bin, command.join(" ")); - - let name = command_line - .split('/') - .last() - .unwrap() - .trim() - .replace(" ", "\\ "); - - let description = if command.is_empty() { - "A 40' shipping container for the Internet".to_string() - } else { - wide_help.lines().nth(1).unwrap().into() - }; - - let include = format!( - "\ -[NAME] -\\fB{}\\fR -- {} -", - name, description - ); - - let tmp = tempfile::tempdir()?; - - fs::write(tmp.path().join("include"), include)?; - - let include = tmp.path().join("include").to_string_lossy().into_owned(); - - let version = Command::new(bin) - .arg("--version") - .out()? - .split_whitespace() - .nth(1) - .unwrap() - .to_owned(); - - let output = Command::new("help2man") - .args(&[ - "--include", - &include, - "--manual", - "Intermodal Manual", - "--no-info", - "--source", - &format!("Intermodal {}", version), - ]) - .arg(&command_line) - .stdout(Stdio::piped()) - .output()?; - - if !output.status.success() { - throw!(anyhow!( - "Failed to generate man page for `{}` failed: {}", - command_line, - output.status - )); - } - - let man = str::from_utf8(&output.stdout)? - .replace("📦 ", "\n") - .replace("\n.SS ", "\n.SH ") - .replace("\"USAGE:\"", "\"SYNOPSIS:\""); - - let re = Regex::new(r"(?ms).SH DESCRIPTION.*?.SH").unwrap(); - - let man = re.replace(&man, ".SH").into_owned(); - - let narrow_help = Command::new(bin) - .args(command.as_slice()) - .env("IMDL_TERM_WIDTH", "80") - .arg("--help") - .out()?; - - Self { - bin: bin.into(), - help: narrow_help, - command, - man, - subcommands, - } - } - - pub(crate) fn slug(&self) -> String { - let mut slug = self.bin.split('/').last().unwrap().to_owned(); - - for name in &self.command { - slug.push('-'); - slug.push_str(&name); - } - - slug - } - - pub(crate) fn command_line(&self) -> String { - self.slug().replace('-', " ") - } - - pub(crate) fn page(&self) -> String { - format!("# `{}`\n```\n{}\n```", self.command_line(), self.help) - } -} diff --git a/bin/update-readme/Cargo.toml b/bin/update-readme/Cargo.toml deleted file mode 100644 index 414aedd..0000000 --- a/bin/update-readme/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "update-readme" -version = "0.0.0" -authors = ["Casey Rodarmor "] -edition = "2018" -publish = false - -[dependencies] -glob = "0.3.0" -regex = "1.3.3" -structopt = "0.3" diff --git a/bin/update-readme/src/bep.rs b/bin/update-readme/src/bep.rs deleted file mode 100644 index 714dd23..0000000 --- a/bin/update-readme/src/bep.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::common::*; - -pub(crate) struct Bep { - pub(crate) number: usize, - pub(crate) title: String, - pub(crate) status: Status, -} diff --git a/bin/update-readme/src/common.rs b/bin/update-readme/src/common.rs deleted file mode 100644 index f6d89e1..0000000 --- a/bin/update-readme/src/common.rs +++ /dev/null @@ -1,15 +0,0 @@ -// stdlib -pub(crate) use std::{ - error::Error, - fmt::{self, Display, Formatter}, - fs, - str::FromStr, -}; - -// crates.io -pub(crate) use glob::glob; -pub(crate) use regex::Regex; -pub(crate) use structopt::StructOpt; - -// local -pub(crate) use crate::{bep::Bep, opt::Opt, status::Status}; diff --git a/bin/update-readme/src/main.rs b/bin/update-readme/src/main.rs deleted file mode 100644 index 0a7dade..0000000 --- a/bin/update-readme/src/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod bep; -mod common; -mod opt; -mod status; - -use crate::common::*; - -fn main() -> Result<(), Box> { - Opt::from_args().run() -} diff --git a/bin/update-readme/src/opt.rs b/bin/update-readme/src/opt.rs deleted file mode 100644 index 8b2656d..0000000 --- a/bin/update-readme/src/opt.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::common::*; - -const README: &str = "README.md"; - -const HEADING_PATTERN: &str = "(?m)^(?P#+) (?P.*)$"; - -const TOC_PATTERN: &str = "(?ms)## Manual.*## General"; - -#[derive(StructOpt)] -pub(crate) enum Opt { - SupportedBeps, - Toc, -} - -impl Opt { - pub(crate) fn run(self) -> Result<(), Box> { - match self { - Self::Toc => Self::update_toc(), - Self::SupportedBeps => Self::update_supported_beps(), - } - } - - fn update_toc() -> Result<(), Box> { - let readme = fs::read_to_string(README)?; - - let header_re = Regex::new(HEADING_PATTERN)?; - - let mut toc = Vec::new(); - for captures in header_re.captures_iter(&readme).skip(2) { - let marker = captures.name("MARKER").unwrap().as_str(); - let text = captures.name("TEXT").unwrap().as_str(); - let level = marker.len(); - let indentation = " ".repeat((level - 2) * 2); - let slug = text - .to_lowercase() - .replace(' ', "-") - .replace('.', "") - .replace('&', ""); - toc.push(format!("{}- [{}](#{})", indentation, text, slug)); - } - - let toc = toc.join("\n"); - - let toc_re = Regex::new(TOC_PATTERN)?; - - let readme = toc_re.replace( - &readme, - format!("## Manual\n\n{}\n\n## General", toc).as_str(), - ); - - fs::write(README, readme.as_bytes())?; - - Ok(()) - } - - fn update_supported_beps() -> Result<(), Box> { - let title_re = Regex::new("(?m)^:Title: (?P.*)$")?; - - let mut beps = Vec::new(); - - for result in glob("tmp/bittorrent.org/beps/bep_*.rst")? { - let path = result?; - - let number = path - .file_stem() - .unwrap() - .to_string_lossy() - .split('_') - .nth(1) - .unwrap() - .parse::<usize>()?; - - if number == 1000 { - continue; - } - - let rst = fs::read_to_string(path)?; - - let title = title_re - .captures(&rst) - .unwrap() - .name("title") - .unwrap() - .as_str() - .trim() - .to_owned(); - - beps.push(Bep { - status: Status::Unknown, - number, - title, - }); - } - - beps.sort_by_key(|bep| bep.number); - - let table_re = Regex::new( - r"(?mx) - ^[|]\ BEP.* - ( - \n - [|] - .* - )* - ", - )?; - - let readme = fs::read_to_string(README)?; - - let parts = table_re.split(&readme).collect::<Vec<&str>>(); - - assert_eq!(parts.len(), 2); - - let before = parts[0]; - let after = parts[1]; - let original = table_re - .captures(&readme) - .unwrap() - .get(0) - .unwrap() - .as_str() - .trim(); - - let row_re = Regex::new( - r"(?x) - ^ - \| - \s* - \[ - (?P<number>[0-9]+) - \] - .* - \s* - \| - (?P<status>.*) - \| - (?P<title>.*) - \| - $ - ", - )?; - - let mut originals = Vec::new(); - - for row in original.lines().skip(2) { - let captures = row_re.captures(row).unwrap(); - originals.push(Bep { - number: captures.name("number").unwrap().as_str().parse()?, - status: captures.name("status").unwrap().as_str().trim().parse()?, - title: captures.name("title").unwrap().as_str().to_owned(), - }); - } - - assert_eq!(originals.len(), beps.len()); - - let mut width = (0, 0, 0); - - let rows = beps - .into_iter() - .zip(originals) - .map(|(bep, original)| { - assert_eq!(bep.number, original.number); - - let row = ( - format!( - "[{:02}](http://bittorrent.org/beps/bep_{:04}.html)", - bep.number, bep.number - ), - original.status.to_string(), - bep.title, - ); - - width.0 = width.0.max(row.0.len()); - width.1 = width.1.max(row.1.len()); - width.2 = width.2.max(row.2.len()); - - row - }) - .collect::<Vec<(String, String, String)>>(); - - let mut lines = Vec::new(); - - lines.push(format!( - "| {:w0$} | {:w1$} | {:w2$} |", - "BEP", - "Status", - "Title", - w0 = width.0, - w1 = width.1, - w2 = width.2, - )); - - lines.push(format!( - "|:{:-<w0$}:|:{:-<w1$}:|:{:-<w2$}-|", - "", - "", - "", - w0 = width.0, - w1 = width.1, - w2 = width.2, - )); - - for (bep, status, title) in rows { - lines.push(format!( - "| {:w0$} | {:w1$} | {:w2$} |", - bep, - status, - title, - w0 = width.0, - w1 = width.1, - w2 = width.2, - )); - } - - let table = lines.join("\n"); - - let readme = &[before.trim(), "", &table, after.trim(), ""].join("\n"); - - fs::write(README, readme.as_bytes())?; - - Ok(()) - } -} diff --git a/bin/update-readme/src/status.rs b/bin/update-readme/src/status.rs deleted file mode 100644 index e4ca46f..0000000 --- a/bin/update-readme/src/status.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::common::*; - -pub(crate) enum Status { - Unknown, - NotApplicable, - Supported, - NotSupported { tracking_issue: Option<u64> }, -} - -impl FromStr for Status { - type Err = String; - - fn from_str(text: &str) -> Result<Self, Self::Err> { - let error = || format!("invalid status: {}", text); - - let unescaped = text.replace('\\', ""); - - let (emoji, tracking_issue) = if !unescaped.starts_with('[') { - (text, None) - } else { - let status_pattern = Regex::new( - r"(?x) - ^ - \[ - (?P<emoji>:[a-zA-Z0-9]+:) - \] - \( - https://github.com/casey/intermodal/issues/(?P<tracking_issue>[0-9]+) - \) - $ - ", - ) - .unwrap(); - - let captures = status_pattern.captures(&unescaped).ok_or_else(error)?; - - let emoji = captures.name("emoji").unwrap().as_str(); - - let tracking_issue = captures - .name("tracking_issue") - .map(|text| text.as_str().parse::<u64>().unwrap()); - - (emoji, tracking_issue) - }; - - match emoji { - "x" => Ok(Status::NotSupported { tracking_issue }), - "+" => Ok(Status::Supported), - "-" => Ok(Status::NotApplicable), - "?" => Ok(Status::Unknown), - ":x:" => Ok(Status::NotSupported { tracking_issue }), - ":white_check_mark:" => Ok(Status::Supported), - ":heavy_minus_sign:" => Ok(Status::NotApplicable), - ":grey_question:" => Ok(Status::Unknown), - _ => Err(error()), - } - } -} - -impl Display for Status { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Unknown => write!(f, ":grey_question:"), - Self::NotApplicable => write!(f, ":heavy_minus_sign:"), - Self::Supported => write!(f, ":white_check_mark:"), - Self::NotSupported { - tracking_issue: None, - } => write!(f, ":x:"), - Self::NotSupported { - tracking_issue: Some(number), - } => write!( - f, - "[:x:](https://github.com/casey/intermodal/issues/{})", - number - ), - } - } -} diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 082f075..08c9391 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -9,7 +9,7 @@ Summary - [`imdl torrent`](./commands/imdl-torrent.md) - [`imdl torrent create`](./commands/imdl-torrent-create.md) - [`imdl torrent link`](./commands/imdl-torrent-link.md) - - [`imdl torrent piece length`](./commands/imdl-torrent-piece-length.md) + - [`imdl torrent piece-length`](./commands/imdl-torrent-piece-length.md) - [`imdl torrent show`](./commands/imdl-torrent-show.md) - [`imdl torrent stats`](./commands/imdl-torrent-stats.md) - [`imdl torrent verify`](./commands/imdl-torrent-verify.md) diff --git a/book/src/commands/imdl-completions.md b/book/src/commands/imdl-completions.md index dc531da..aa03aa4 100644 --- a/book/src/commands/imdl-completions.md +++ b/book/src/commands/imdl-completions.md @@ -4,14 +4,17 @@ imdl-completions 0.1.5 Print shell completion scripts to standard output. USAGE: - imdl completions --shell <SHELL> + imdl completions [OPTIONS] --shell <SHELL> FLAGS: -h, --help Print help message. -V, --version Print version number. OPTIONS: - -s, --shell <SHELL> Print completions for `SHELL`. [possible values: zsh, - bash, fish, powershell, elvish] + -d, --dir <DIR> Write completion script to `DIR` with an appropriate + filename. If `--shell` is not given, write all + completion scripts. + -s, --shell <SHELL> Print completion script for `SHELL`. [possible + values: zsh, bash, fish, powershell, elvish] ``` \ No newline at end of file diff --git a/book/src/commands/imdl-torrent-link.md b/book/src/commands/imdl-torrent-link.md index c066aad..bf99114 100644 --- a/book/src/commands/imdl-torrent-link.md +++ b/book/src/commands/imdl-torrent-link.md @@ -10,13 +10,13 @@ FLAGS: -h, --help Print help message. -O, --open Open generated magnet link. Uses `xdg-open`, `gnome-open`, or `kde-open` on Linux; `open` on macOS; and `cmd /C start` - on Windows + on Windows. -V, --version Print version number. OPTIONS: -s, --select-only <INDICES>... - Specify files that torrent clients select for download. Values are - indices into the info.files list. e.g. `--select-only 1,2,3` + Select files to download. Values are indices into the `info.files` + list, e.g. `--select-only 1,2,3`. -i, --input <METAINFO> Generate magnet link from metainfo at `PATH`. If `PATH` is `-`, read metainfo from standard input. diff --git a/book/src/commands/imdl-torrent-piece-length.md b/book/src/commands/imdl-torrent-piece-length.md index 37a7295..80d6661 100644 --- a/book/src/commands/imdl-torrent-piece-length.md +++ b/book/src/commands/imdl-torrent-piece-length.md @@ -1,4 +1,4 @@ -# `imdl torrent piece length` +# `imdl torrent piece-length` ``` imdl-torrent-piece-length 0.1.5 Display information about automatic piece length selection. diff --git a/book/src/introduction.md b/book/src/introduction.md index bcc87be..c232e14 100644 --- a/book/src/introduction.md +++ b/book/src/introduction.md @@ -1,39 +1,62 @@ -# Intermodal +# Intermodal: A 40' shipping container for the Internet -Intermodal is, as the moment, a BitTorrent metainfo utility. The binary is called `imdl`. +Intermodal is a user-friendly and featureful command-line BitTorrent metainfo utility for Linux, Windows, and macOS. -Project development is hosted on GitHub at [github.com/casey/intermodal](https://github.com/casey/intermodal). +Project development is hosted on [GitHub](https://github.com/casey/intermodal). + +The binary is called `imdl`: + +```sh +$ imdl --help +``` + +BitTorrent metainfo related functionality is under the `torrent` subcommand: + +```sh +$ imdl torrent --help +``` Intermodal can be used to create `.torrent` files: -``` +```sh $ imdl torrent create --input foo ``` -Print information about existing torrent files: +Print information about existing `.torrent` files: -``` +```sh $ imdl torrent show --input foo.torrent ``` Verify downloaded torrents: -``` +```sh $ imdl torrent verify --input foo.torrent --content foo ``` -Generate magnet links from torrent files: +Generate magnet links from `.torrent` files: -``` +```sh $ imdl torrent link --input foo.torrent ``` +Show infromation about the piece length picker: + +```sh +$ imdl torrent piece-length +``` + +Print completion scripts for the `imdl` binary: + +```sh +$ imdl completions --shell zsh +``` + Functionality that is not yet finalized, but still available for preview, can be accessed with the `--unstable` flag: Print information about a collection of torrents: -``` +```sh $ imdl --unstable torrent stats --input dir ``` - Happy sharing! diff --git a/completions/imdl.zsh b/completions/_imdl similarity index 97% rename from completions/imdl.zsh rename to completions/_imdl index 60a625d..a1cd104 100644 --- a/completions/imdl.zsh +++ b/completions/_imdl @@ -213,8 +213,10 @@ esac ;; (completions) _arguments "${_arguments_options[@]}" \ -'-s+[Print completions for `SHELL`.]: :(zsh bash fish powershell elvish)' \ -'--shell=[Print completions for `SHELL`.]: :(zsh bash fish powershell elvish)' \ +'-s+[Print completion script for `SHELL`.]: :(zsh bash fish powershell elvish)' \ +'--shell=[Print completion script for `SHELL`.]: :(zsh bash fish powershell elvish)' \ +'-d+[Write completion script to `DIR` with an appropriate filename. If `--shell` is not given, write all completion scripts.]' \ +'--dir=[Write completion script to `DIR` with an appropriate filename. If `--shell` is not given, write all completion scripts.]' \ '-h[Print help message.]' \ '--help[Print help message.]' \ '-V[Print version number.]' \ diff --git a/completions/imdl.powershell b/completions/_imdl.ps1 similarity index 97% rename from completions/imdl.powershell rename to completions/_imdl.ps1 index 0c50388..b70f8c3 100644 --- a/completions/imdl.powershell +++ b/completions/_imdl.ps1 @@ -192,8 +192,10 @@ Sort in ascending order by size, break ties in descending path order: break } 'imdl;completions' { - [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Print completions for `SHELL`.') - [CompletionResult]::new('--shell', 'shell', [CompletionResultType]::ParameterName, 'Print completions for `SHELL`.') + [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Print completion script for `SHELL`.') + [CompletionResult]::new('--shell', 'shell', [CompletionResultType]::ParameterName, 'Print completion script for `SHELL`.') + [CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Write completion script to `DIR` with an appropriate filename. If `--shell` is not given, write all completion scripts.') + [CompletionResult]::new('--dir', 'dir', [CompletionResultType]::ParameterName, 'Write completion script to `DIR` with an appropriate filename. If `--shell` is not given, write all completion scripts.') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help message.') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help message.') [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version number.') diff --git a/completions/imdl.bash b/completions/imdl.bash index bc59a28..1693f3d 100644 --- a/completions/imdl.bash +++ b/completions/imdl.bash @@ -70,7 +70,7 @@ _imdl() { ;; imdl__completions) - opts=" -h -V -s --help --version --shell " + opts=" -h -V -s -d --help --version --shell --dir " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -85,6 +85,14 @@ _imdl() { COMPREPLY=($(compgen -W "zsh bash fish powershell elvish" -- "${cur}")) return 0 ;; + --dir) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -d) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/completions/imdl.elvish b/completions/imdl.elvish index ae6f602..dc09a5a 100644 --- a/completions/imdl.elvish +++ b/completions/imdl.elvish @@ -178,8 +178,10 @@ Sort in ascending order by size, break ties in descending path order: cand --version 'Prints version information' } &'imdl;completions'= { - cand -s 'Print completions for `SHELL`.' - cand --shell 'Print completions for `SHELL`.' + cand -s 'Print completion script for `SHELL`.' + cand --shell 'Print completion script for `SHELL`.' + cand -d 'Write completion script to `DIR` with an appropriate filename. If `--shell` is not given, write all completion scripts.' + cand --dir 'Write completion script to `DIR` with an appropriate filename. If `--shell` is not given, write all completion scripts.' cand -h 'Print help message.' cand --help 'Print help message.' cand -V 'Print version number.' diff --git a/completions/imdl.fish b/completions/imdl.fish index 7603dfa..ca19c02 100644 --- a/completions/imdl.fish +++ b/completions/imdl.fish @@ -90,7 +90,8 @@ complete -c imdl -n "__fish_seen_subcommand_from verify" -s h -l help -d 'Print complete -c imdl -n "__fish_seen_subcommand_from verify" -s V -l version -d 'Print version number.' complete -c imdl -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information' complete -c imdl -n "__fish_seen_subcommand_from help" -s V -l version -d 'Prints version information' -complete -c imdl -n "__fish_seen_subcommand_from completions" -s s -l shell -d 'Print completions for `SHELL`.' -r -f -a "zsh bash fish powershell elvish" +complete -c imdl -n "__fish_seen_subcommand_from completions" -s s -l shell -d 'Print completion script for `SHELL`.' -r -f -a "zsh bash fish powershell elvish" +complete -c imdl -n "__fish_seen_subcommand_from completions" -s d -l dir -d 'Write completion script to `DIR` with an appropriate filename. If `--shell` is not given, write all completion scripts.' complete -c imdl -n "__fish_seen_subcommand_from completions" -s h -l help -d 'Print help message.' complete -c imdl -n "__fish_seen_subcommand_from completions" -s V -l version -d 'Print version number.' complete -c imdl -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information' diff --git a/justfile b/justfile index d4ae315..59213b2 100644 --- a/justfile +++ b/justfile @@ -51,10 +51,6 @@ preview-readme: book: mdbook serve book --open --dest-dir ../www/book -# add git log messages to changelog -changes: - git log --pretty=format:%s >> CHANGELOG.md - dev-deps: brew install grip cargo install mdbook @@ -63,37 +59,16 @@ dev-deps: brew install imagemagick brew install gifsicle -# update readme table of contents -update-toc: - cargo run --package update-readme toc - -generate-completions: - ./bin/generate-completions - -man-watch: - cargo build - cargo watch \ - --ignore '/man' \ - --ignore '/man/*' \ - --ignore '/book/src/commands' \ - --ignore '/book/src/commands/*' \ - --clear --exec 'run --package man' - -man: - cargo build - cargo run --package man - -check-man: man - git diff --no-ext-diff --quiet --exit-code +# update generated documentation +gen: + cargo run --package gen all check-minimal-versions: ./bin/check-minimal-versions -check: test clippy lint check-minimal-versions changelog-update +check: test clippy lint check-minimal-versions gen git diff --no-ext-diff --quiet --exit-code cargo +nightly fmt --all -- --check - cargo run --package update-readme toc - git diff --no-ext-diff --quiet --exit-code draft: push hub pull-request -o --draft @@ -109,9 +84,7 @@ merge: done just done -update: man changelog-update update-toc - -publish-check: check check-man +publish-check: check cargo outdated --exit-code 1 grep {{version}} CHANGELOG.md @@ -126,15 +99,6 @@ publish: publish-check cargo publish just merge -changelog-update: - cargo run --package changelog update - -changelog-types: - cargo run --package changelog types - -changelog-issue-template: - cargo run --package changelog issue-template - # record, upload, and render demo animation demo: demo-record demo-upload demo-render diff --git a/man/imdl-completions.1 b/man/imdl-completions.1 index 6e99b81..1fbc35f 100644 --- a/man/imdl-completions.1 +++ b/man/imdl-completions.1 @@ -1,11 +1,11 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-COMPLETIONS "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ completions\fR - Print shell completion scripts to standard output. .SH "SYNOPSIS:" .IP -imdl completions \fB\-\-shell\fR <SHELL> +imdl completions [OPTIONS] \fB\-\-shell\fR <SHELL> .SH "FLAGS:" .TP \fB\-h\fR, \fB\-\-help\fR @@ -15,5 +15,9 @@ Print help message. Print version number. .SH "OPTIONS:" .TP +\fB\-d\fR, \fB\-\-dir\fR <DIR> +Write completion script to `DIR` with an appropriate filename. If `\-\-shell` is not given, +write all completion scripts. +.TP \fB\-s\fR, \fB\-\-shell\fR <SHELL> -Print completions for `SHELL`. [possible values: zsh, bash, fish, powershell, elvish] +Print completion script for `SHELL`. [possible values: zsh, bash, fish, powershell, elvish] diff --git a/man/imdl-torrent-create.1 b/man/imdl-torrent-create.1 index ba854cd..8a97a97 100644 --- a/man/imdl-torrent-create.1 +++ b/man/imdl-torrent-create.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-TORRENT-CREATE "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ torrent\ create\fR diff --git a/man/imdl-torrent-link.1 b/man/imdl-torrent-link.1 index ceb7406..61015da 100644 --- a/man/imdl-torrent-link.1 +++ b/man/imdl-torrent-link.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-TORRENT-LINK "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ torrent\ link\fR @@ -13,15 +13,15 @@ Print help message. .TP \fB\-O\fR, \fB\-\-open\fR Open generated magnet link. Uses `xdg\-open`, `gnome\-open`, or `kde\-open` on Linux; `open` on macOS; -and `cmd \fI\,/C\/\fP start` on Windows +and `cmd \fI\,/C\/\fP start` on Windows. .TP \fB\-V\fR, \fB\-\-version\fR Print version number. .SH "OPTIONS:" .TP \fB\-s\fR, \fB\-\-select\-only\fR <INDICES>... -Specify files that torrent clients select for download. Values are indices into -the info.files list. e.g. `\-\-select\-only 1,2,3` +Select files to download. Values are indices into the `info.files` list, e.g. +`\-\-select\-only 1,2,3`. .TP \fB\-i\fR, \fB\-\-input\fR <METAINFO> Generate magnet link from metainfo at `PATH`. If `PATH` is `\-`, read metainfo from diff --git a/man/imdl-torrent-piece-length.1 b/man/imdl-torrent-piece-length.1 index aadfe9a..2796d92 100644 --- a/man/imdl-torrent-piece-length.1 +++ b/man/imdl-torrent-piece-length.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-TORRENT-PIECE-LENGTH "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ torrent\ piece-length\fR diff --git a/man/imdl-torrent-show.1 b/man/imdl-torrent-show.1 index aac4f34..7cae614 100644 --- a/man/imdl-torrent-show.1 +++ b/man/imdl-torrent-show.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-TORRENT-SHOW "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ torrent\ show\fR diff --git a/man/imdl-torrent-stats.1 b/man/imdl-torrent-stats.1 index 1605491..f42167a 100644 --- a/man/imdl-torrent-stats.1 +++ b/man/imdl-torrent-stats.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-TORRENT-STATS "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ torrent\ stats\fR diff --git a/man/imdl-torrent-verify.1 b/man/imdl-torrent-verify.1 index 0999262..4d2867a 100644 --- a/man/imdl-torrent-verify.1 +++ b/man/imdl-torrent-verify.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-TORRENT-VERIFY "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ torrent\ verify\fR diff --git a/man/imdl-torrent.1 b/man/imdl-torrent.1 index 672f98b..b258493 100644 --- a/man/imdl-torrent.1 +++ b/man/imdl-torrent.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH IMDL-TORRENT "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\ torrent\fR diff --git a/man/imdl.1 b/man/imdl.1 index fe1d767..7c05f2c 100644 --- a/man/imdl.1 +++ b/man/imdl.1 @@ -1,4 +1,4 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.11. +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13. .TH \FBIMDL\FR "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual" .SH NAME \fBimdl\fR diff --git a/src/common.rs b/src/common.rs index f0eb76a..55c4ba7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -17,6 +17,7 @@ pub(crate) use std::{ path::{self, Path, PathBuf}, process::{self, ExitStatus}, str::{self, FromStr}, + string::FromUtf8Error, sync::Once, time::{SystemTime, SystemTimeError}, usize, @@ -39,8 +40,8 @@ pub(crate) use structopt::{ clap::{self, AppSettings}, StructOpt, }; -pub(crate) use strum::VariantNames; -pub(crate) use strum_macros::{EnumString, EnumVariantNames, IntoStaticStr}; +pub(crate) use strum::{IntoEnumIterator, VariantNames}; +pub(crate) use strum_macros::{EnumIter, EnumString, EnumVariantNames, IntoStaticStr}; pub(crate) use unicode_width::UnicodeWidthStr; pub(crate) use url::{Host, Url}; pub(crate) use walkdir::WalkDir; diff --git a/src/env.rs b/src/env.rs index d901d8b..0a94f1b 100644 --- a/src/env.rs +++ b/src/env.rs @@ -187,6 +187,11 @@ impl Env { Ok(self.dir().join(path).clean()) } + pub(crate) fn write(&mut self, path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> { + let path = path.as_ref(); + fs::write(self.resolve(path)?, contents).context(error::Filesystem { path }) + } + pub(crate) fn read(&mut self, source: InputTarget) -> Result<Input> { let data = match &source { InputTarget::Path(path) => { diff --git a/src/error.rs b/src/error.rs index f53aa62..c869798 100644 --- a/src/error.rs +++ b/src/error.rs @@ -111,6 +111,8 @@ pub(crate) enum Error { PieceLengthZero, #[snafu(display("Private torrents must have tracker"))] PrivateTrackerless, + #[snafu(display("Completion script for shell `{}` not UTF-8: {}", shell.name(), source))] + ShellDecode { shell: Shell, source: FromUtf8Error }, #[snafu(display("Failed to write to standard error: {}", source))] Stderr { source: io::Error }, #[snafu(display("Failed to read from standard input: {}", source))] diff --git a/src/main.rs b/src/main.rs index 356ac8b..4b8159d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,9 @@ mod errln; #[macro_use] mod err; +#[macro_use] +mod out; + #[macro_use] mod outln; diff --git a/src/out.rs b/src/out.rs new file mode 100644 index 0000000..9009c84 --- /dev/null +++ b/src/out.rs @@ -0,0 +1,11 @@ +macro_rules! out { + ($env:expr) => { + write!($env.out_mut(), "").context(crate::error::Stdout) + }; + ($env:expr, $fmt:expr) => { + write!($env.out_mut(), $fmt).context(crate::error::Stdout) + }; + ($env:expr, $fmt:expr, $($arg:tt)*) => { + write!($env.out_mut(), $fmt, $($arg)*).context(crate::error::Stdout) + }; +} diff --git a/src/shell.rs b/src/shell.rs index d3bfec2..fdc9b04 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -2,7 +2,7 @@ use super::*; use structopt::clap; -#[derive(EnumVariantNames, EnumString)] +#[derive(Copy, Clone, EnumVariantNames, IntoStaticStr, EnumString, EnumIter, Debug)] #[strum(serialize_all = "kebab-case")] pub(crate) enum Shell { Zsh, @@ -12,6 +12,38 @@ pub(crate) enum Shell { Elvish, } +impl Shell { + pub(crate) fn completion_script(self) -> Result<String> { + let buffer = Vec::new(); + let mut cursor = Cursor::new(buffer); + + Arguments::clap().gen_completions_to(env!("CARGO_PKG_NAME"), self.into(), &mut cursor); + + let buffer = cursor.into_inner(); + + let script = String::from_utf8(buffer).context(error::ShellDecode { shell: self })?; + + let mut script = script.trim().to_owned(); + script.push('\n'); + + Ok(script) + } + + pub(crate) fn completion_script_filename(self) -> &'static str { + match self { + Self::Bash => "imdl.bash", + Self::Fish => "imdl.fish", + Self::Zsh => "_imdl", + Self::Powershell => "_imdl.ps1", + Self::Elvish => "imdl.elvish", + } + } + + pub(crate) fn name(self) -> &'static str { + self.into() + } +} + impl Into<clap::Shell> for Shell { fn into(self) -> clap::Shell { match self { diff --git a/src/subcommand/completions.rs b/src/subcommand/completions.rs index bde2cec..a86edb9 100644 --- a/src/subcommand/completions.rs +++ b/src/subcommand/completions.rs @@ -12,24 +12,48 @@ pub(crate) struct Completions { short = "s", value_name = "SHELL", possible_values = Shell::VARIANTS, - help = "Print completions for `SHELL`.", + required_unless = "dir", + help = "Print completion script for `SHELL`.", )] - shell: Shell, + shell: Option<Shell>, + #[structopt( + long = "dir", + short = "d", + value_name = "DIR", + empty_values = false, + parse(from_os_str), + help = "Write completion script to `DIR` with an appropriate filename. If `--shell` is not \ + given, write all completion scripts." + )] + dir: Option<PathBuf>, } impl Completions { - pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { - let buffer = Vec::new(); - let mut cursor = Cursor::new(buffer); + pub(crate) fn run(self, env: &mut Env) -> Result<()> { + if let Some(shell) = self.shell { + if let Some(dir) = self.dir { + Self::write(env, &dir, shell)?; + } else { + let script = shell.completion_script()?; + out!(env, "{}", script)?; + } + } else { + let dir = self + .dir + .ok_or_else(|| Error::internal("Expected `--dir` to be set"))?; - Arguments::clap().gen_completions_to(env!("CARGO_PKG_NAME"), self.shell.into(), &mut cursor); + for shell in Shell::iter() { + Self::write(env, &dir, shell)?; + } + } - let buffer = cursor.into_inner(); - - let script = String::from_utf8(buffer).expect("Clap completion not UTF-8"); - - outln!(env, "{}", script.trim())?; + Ok(()) + } + fn write(env: &mut Env, dir: &Path, shell: Shell) -> Result<()> { + let script = shell.completion_script()?; + let dst = dir.join(shell.completion_script_filename()); + env.write(dst, script)?; Ok(()) } } @@ -53,4 +77,44 @@ mod tests { assert!(env.out().starts_with("_imdl() {")); } + + #[test] + fn single_dir() { + let mut env = test_env! { + args: [ + "completions", + "--shell", + "bash", + "--dir", + ".", + ], + tree: {}, + }; + + env.assert_ok(); + + let script = env.read_to_string("imdl.bash"); + + assert!(script.starts_with("_imdl() {")); + } + + #[test] + fn all_dir() { + let mut env = test_env! { + args: [ + "completions", + "--dir", + ".", + ], + tree: {}, + }; + + env.assert_ok(); + + let script = env.read_to_string("imdl.bash"); + assert!(script.starts_with("_imdl() {")); + + let script = env.read_to_string("_imdl.ps1"); + assert!(script.starts_with("using namespace")); + } } diff --git a/src/test_env.rs b/src/test_env.rs index 5c86b05..a2bf544 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -67,6 +67,10 @@ impl TestEnv { fs::create_dir(self.env.resolve(path).unwrap()).unwrap(); } + pub(crate) fn read_to_string(&self, path: impl AsRef<Path>) -> String { + fs::read_to_string(self.env.resolve(path).unwrap()).unwrap() + } + #[cfg(unix)] pub(crate) fn metadata(&self, path: impl AsRef<Path>) -> fs::Metadata { fs::metadata(self.env.resolve(path).unwrap()).unwrap()