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
This commit is contained in:
Casey Rodarmor 2020-04-16 04:16:40 -07:00
parent 1f8023d13a
commit 04338e3501
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
73 changed files with 1239 additions and 866 deletions

View File

@ -35,6 +35,15 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - 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 - name: Cache cargo registry
uses: actions/cache@v1 uses: actions/cache@v1
@ -63,7 +72,7 @@ jobs:
components: clippy, rustfmt components: clippy, rustfmt
override: true override: true
- name: Version - name: Info
run: | run: |
rustup --version rustup --version
cargo --version cargo --version
@ -79,8 +88,10 @@ jobs:
run: cargo clippy --all run: cargo clippy --all
- name: Lint - name: Lint
if: matrix.os != 'windows-latest' if: matrix.os == 'macos-latest'
run: ./bin/lint run: |
brew install ripgrep
./bin/lint
- name: Install Nightly - name: Install Nightly
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@ -93,16 +104,11 @@ jobs:
- name: Check Formatting - name: Check Formatting
run: cargo +nightly fmt --all -- --check run: cargo +nightly fmt --all -- --check
- name: Check Completion Scripts - name: Check Generated
if: matrix.os != 'windows-latest' if: matrix.os == 'macos-latest'
run: | run: |
./bin/generate-completions brew install help2man
git diff --no-ext-diff --exit-code cargo run --package gen all
- name: Check Readme Table of Contents
if: matrix.os != 'windows-latest'
run: |
cargo run --package update-readme toc
git diff --no-ext-diff --exit-code git diff --no-ext-diff --exit-code
- name: Install `mdbook` - name: Install `mdbook`

7
.ignore Normal file
View File

@ -0,0 +1,7 @@
/CHANGELOG.md
/README.md
/completions
/man
/book/src/commands
/book/src/SUMMARY.md
/book/src/introduction.md

View File

@ -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 <aaronstrick@gmail.com>_ - :art: [`xxxxxxxxxxxx`](https://github.com/casey/intermodal/commits/master) Merge documentation and changelog generation - _Casey Rodarmor <casey@rodarmor.com>_
- :books: [`1f8023d13a39`](https://github.com/casey/intermodal/commit/1f8023d13a399e381176c20bbb6a71763b7c352a) Fix directory link in README - _Matt Boulanger <Celeo@users.noreply.github.com>_
- :sparkles: [`cb8b5a691945`](https://github.com/casey/intermodal/commit/cb8b5a691945b8108676f95d2888774263be8cc8) Partially implement BEP 53 - Fixes [#245](https://github.com/casey/intermodal/issues/245) - _strickinato <aaronstrick@gmail.com>_
- :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 <casey@rodarmor.com>_ - :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 <casey@rodarmor.com>_
- :wrench: [`ddf097c83690`](https://github.com/casey/intermodal/commit/ddf097c8369002748992165f81e9a1bdbe6eff98) Fix `publish` recipe ([#368](https://github.com/casey/intermodal/pull/368)) - _Casey Rodarmor <casey@rodarmor.com>_ - :wrench: [`ddf097c83690`](https://github.com/casey/intermodal/commit/ddf097c8369002748992165f81e9a1bdbe6eff98) Fix `publish` recipe ([#368](https://github.com/casey/intermodal/pull/368)) - _Casey Rodarmor <casey@rodarmor.com>_

124
Cargo.lock generated
View File

@ -42,6 +42,49 @@ dependencies = [
"nodrop", "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]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -133,23 +176,6 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 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]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.11" version = "0.4.11"
@ -313,6 +339,29 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.1.14" version = "0.1.14"
@ -339,12 +388,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]] [[package]]
name = "globset" name = "globset"
version = "0.4.5" version = "0.4.5"
@ -376,6 +419,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "humansize"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "1.3.0" version = "1.3.0"
@ -532,16 +581,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "man"
version = "0.0.0"
dependencies = [
"anyhow",
"fehler",
"regex",
"tempfile",
]
[[package]] [[package]]
name = "matches" name = "matches"
version = "0.1.8" version = "0.1.8"
@ -572,6 +611,16 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 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]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.42" version = "0.1.42"
@ -1143,15 +1192,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "update-readme"
version = "0.0.0"
dependencies = [
"glob",
"regex",
"structopt",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.1.1" version = "2.1.1"

View File

@ -62,15 +62,9 @@ temptree = "0.0.0"
[workspace] [workspace]
members = [ members = [
# generate documentation
"bin/gen",
# run commands for demo animation # run commands for demo animation
"bin/demo", "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"
] ]

View File

@ -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) [![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) [![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) ![demonstration animation](https://raw.githubusercontent.com/casey/intermodal/master/www/demo.gif)
## Manual ## Table of Contents
- [General](#general) - [Installation](#installation)
- [Installation](#installation) - [Supported Operating Systems](#supported-operating-systems)
- [Supported Operating Systems](#supported-operating-systems) - [Packages](#packages)
- [Packages](#packages) - [Pre-built binaries](#pre-built-binaries)
- [Pre-built binaries](#pre-built-binaries) - [Cargo](#cargo)
- [Cargo](#cargo) - [Shell Completion Scripts](#shell-completion-scripts)
- [Shell Completion Scripts](#shell-completion-scripts) - [Semantic Versioning](#semantic-versioning)
- [Semantic Versioning](#semantic-versioning) - [Unstable Features](#unstable-features)
- [Unstable Features](#unstable-features) - [Source Signatures](#source-signatures)
- [Source Signatures](#source-signatures)
- [Acknowledgments](#acknowledgments) - [Acknowledgments](#acknowledgments)
## General ## Installation
### Installation ### Supported Operating Systems
#### Supported Operating Systems
`imdl` supports Linux, MacOS, and Windows, and should work on other unix OSes. `imdl` supports Linux, MacOS, and Windows, and should work on other unix OSes.
If it does not, please open an issue! If it does not, please open an issue!
#### Packages ### Packages
| Operating System | Package Manager | Package | Command | | 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` | | [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)<sup>AUR</sup> | `yay -S intermodal` | | [Arch Linux](https://www.archlinux.org) | [Yay](https://github.com/Jguer/yay) | [intermodal](https://aur.archlinux.org/packages/intermodal)<sup>AUR</sup> | `yay -S intermodal` |
#### Pre-built binaries ### Pre-built binaries
Pre-built binaries for Linux, macOS, and Windows can be found on Pre-built binaries for Linux, macOS, and Windows can be found on
[the releases page](https://github.com/casey/intermodal/releases). [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 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 `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 source and installed with `cargo install imdl`. To get Rust, use the
[rustup installer](https://rustup.rs/). [rustup installer](https://rustup.rs/).
### Shell Completion Scripts ## Shell Completion Scripts
Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are
available in the [completions directory](./completions). Please refer to your available in the [completions directory](./completions), included in all
shell's documentation for how to install them. [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, The `imdl` binary can also generate the same completion scripts at runtime,
using the `completions` command: using the `completions` command:
@ -98,7 +107,15 @@ using the `completions` command:
$ imdl completions --shell bash > imdl.bash $ 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/). 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 - vX.Y.Z: Breaking changes may only be introduced with a major version number
bump bump
### Unstable Features ## Unstable Features
To avoid premature stabilization and excessive version churn, unstable features To avoid premature stabilization and excessive version churn, unstable features
are unavailable unless the `--unstable` / `-u` flag is passed, for example are unavailable unless the `--unstable` / `-u` flag is passed, for example
`imdl --unstable torrent create .`. Unstable features may be changed or removed `imdl --unstable torrent create .`. Unstable features may be changed or removed
at any time. at any time.
### Source Signatures ## Source Signatures
All commits to the intermodal master branch signed with Casey Rodarmor's PGP All commits to the intermodal master branch signed with Casey Rodarmor's PGP
key with fingerprint `3259DAEDB29636B0E2025A70556186B153EC6FE0`, which can be key with fingerprint `3259DAEDB29636B0E2025A70556186B153EC6FE0`, which can be

View File

@ -1,25 +0,0 @@
[package]
name = "changelog"
version = "0.0.0"
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
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"]

View File

@ -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()?;
}

View File

@ -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())?;
}
}
}
}

31
bin/gen/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "gen"
version = "0.0.0"
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
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"]

41
bin/gen/config.yaml Normal file
View File

@ -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"

View File

@ -1,16 +1,15 @@
use crate::common::*; use crate::common::*;
#[derive(Debug)]
pub(crate) struct Bin { pub(crate) struct Bin {
pub(crate) bin: String, path: PathBuf,
pub(crate) subcommands: Vec<Subcommand>, pub(crate) subcommands: Vec<Subcommand>,
} }
impl Bin { impl Bin {
#[throws] #[throws]
pub(crate) fn new(bin: &str) -> Self { pub(crate) fn new(path: &Path) -> Bin {
let mut bin = Bin { let mut bin = Bin {
bin: bin.into(), path: path.into(),
subcommands: Vec::new(), subcommands: Vec::new(),
}; };
@ -23,7 +22,7 @@ impl Bin {
#[throws] #[throws]
fn add_subcommands(&mut self, command: &mut Vec<String>) { fn add_subcommands(&mut self, command: &mut Vec<String>) {
let subcommand = Subcommand::new(&self.bin, command.clone())?; let subcommand = Subcommand::new(&self.path, command.clone())?;
for name in &subcommand.subcommands { for name in &subcommand.subcommands {
command.push(name.into()); command.push(name.into());

View File

@ -6,8 +6,8 @@ pub(crate) struct Changelog {
impl Changelog { impl Changelog {
#[throws] #[throws]
pub(crate) fn new(repo: &Repository) -> Self { pub(crate) fn new(project: &Project) -> Self {
let mut current = repo.head()?.peel_to_commit()?; let mut current = project.repo.head()?.peel_to_commit()?;
let mut entries = Vec::new(); let mut entries = Vec::new();
@ -24,7 +24,7 @@ impl Changelog {
let manifest_bytes = current let manifest_bytes = current
.tree()? .tree()?
.get_path("Cargo.toml".as_ref())? .get_path("Cargo.toml".as_ref())?
.to_object(&repo)? .to_object(&project.repo)?
.as_blob() .as_blob()
.unwrap() .unwrap()
.content() .content()
@ -32,7 +32,12 @@ impl Changelog {
let manifest = Manifest::from_slice(&manifest_bytes)?; let manifest = Manifest::from_slice(&manifest_bytes)?;
let entry = Entry::new(&current, manifest.package.unwrap().version.as_ref(), head)?; let entry = Entry::new(
&current,
manifest.package.unwrap().version.as_ref(),
head,
&project.config,
)?;
entries.push(entry); entries.push(entry);
} }

17
bin/gen/src/cmd.rs Normal file
View File

@ -0,0 +1,17 @@
macro_rules! cmd {
{
$bin:expr,
$($arg:expr),*
$(,)?
} => {
{
let mut command = Command::new($bin);
$(
command.arg($arg);
)*
command
}
}
}

View File

@ -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
}
}

View File

@ -1,15 +1,21 @@
pub(crate) use std::{ pub(crate) use std::{
cmp::{Ord, PartialOrd}, cmp::{Ord, PartialOrd},
collections::{BTreeMap, BTreeSet},
env, env,
fmt::{self, Display, Formatter}, 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 anyhow::{anyhow, Error};
pub(crate) use askama::Template;
pub(crate) use cargo_toml::Manifest; pub(crate) use cargo_toml::Manifest;
pub(crate) use chrono::{DateTime, NaiveDateTime, Utc}; pub(crate) use chrono::{DateTime, NaiveDateTime, Utc};
pub(crate) use fehler::{throw, throws}; pub(crate) use fehler::{throw, throws};
pub(crate) use git2::{Commit, Repository}; pub(crate) use git2::{Commit, Repository};
pub(crate) use regex::Regex;
pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use serde::{Deserialize, Serialize};
pub(crate) use structopt::StructOpt; pub(crate) use structopt::StructOpt;
pub(crate) use strum::VariantNames; pub(crate) use strum::VariantNames;
@ -17,5 +23,8 @@ pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr};
pub(crate) use url::Url; pub(crate) use url::Url;
pub(crate) use crate::{ 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,
}; };

17
bin/gen/src/config.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::common::*;
const PATH: &str = "bin/gen/config.yaml";
#[derive(Deserialize)]
pub(crate) struct Config {
pub(crate) changelog: BTreeMap<String, Metadata>,
pub(crate) examples: Vec<Example>,
}
impl Config {
#[throws]
pub(crate) fn load(root: &Path) -> Config {
let file = File::open(root.join(PATH))?;
serde_yaml::from_reader(file)?
}
}

View File

@ -12,13 +12,17 @@ pub(crate) struct Entry {
impl Entry { impl Entry {
#[throws] #[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::<Utc>::from_utc( let time = DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp(commit.time().seconds(), 0), NaiveDateTime::from_timestamp(commit.time().seconds(), 0),
Utc, 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 { Entry {
version: version.into(), version: version.into(),

10
bin/gen/src/example.rs Normal file
View File

@ -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,
}

View File

@ -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));
}
}
}

View File

@ -0,0 +1,15 @@
use crate::common::*;
#[derive(Template)]
#[template(path = "introduction.md")]
pub(crate) struct Introduction {
pub(crate) examples: Vec<Example>,
}
impl Introduction {
pub(crate) fn new(config: &Config) -> Self {
Self {
examples: config.examples.clone(),
}
}
}

31
bin/gen/src/main.rs Normal file
View File

@ -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)?;
}

View File

@ -1,6 +1,6 @@
use crate::common::*; use crate::common::*;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize, Clone)]
pub(crate) struct Metadata { pub(crate) struct Metadata {
#[serde(rename = "type")] #[serde(rename = "type")]
pub(crate) kind: Kind, pub(crate) kind: Kind,

153
bin/gen/src/opt.rs Normal file
View File

@ -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<Path>) {
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)?;
}
}
}

57
bin/gen/src/project.rs Normal file
View File

@ -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::<BTreeSet<String>>();
let bin_commands = bin
.subcommands
.iter()
.map(|subcommand| subcommand.command_line())
.collect::<BTreeSet<String>>();
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,
}
}
}

37
bin/gen/src/readme.rs Normal file
View File

@ -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<MARKER>#+) (?P<TEXT>.*)$";
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"),
}
}
}

162
bin/gen/src/subcommand.rs Normal file
View File

@ -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<String>,
pub(crate) subcommands: Vec<String>,
}
impl Subcommand {
#[throws]
pub(crate) fn new(bin: &Path, command: Vec<String>) -> 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)
}
}

29
bin/gen/src/summary.rs Normal file
View File

@ -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"),
}
}
}

140
bin/gen/templates/README.md Normal file
View File

@ -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)<sup>AUR</sup> | `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.

View File

@ -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)

View File

@ -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!

View File

@ -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

View File

@ -2,4 +2,7 @@
set -euxo pipefail 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\]'

View File

@ -1,12 +0,0 @@
[package]
name = "man"
version = "0.0.0"
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
edition = "2018"
publish = false
[dependencies]
anyhow = "1.0.28"
fehler = "1.0.0"
tempfile = "3.1.0"
regex = "1.3.6"

View File

@ -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};

View File

@ -1,57 +0,0 @@
use crate::common::*;
mod bin;
mod common;
mod subcommand;
#[throws]
fn clean(dir: impl AsRef<Path>) {
let dir = dir.as_ref();
fs::remove_dir_all(dir)?;
fs::create_dir_all(dir)?;
}
#[throws]
fn write(dst: impl AsRef<Path>, 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)?;
}

View File

@ -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<String>,
pub(crate) help: String,
pub(crate) man: String,
pub(crate) subcommands: Vec<String>,
}
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<String>) -> 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)
}
}

View File

@ -1,11 +0,0 @@
[package]
name = "update-readme"
version = "0.0.0"
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
edition = "2018"
publish = false
[dependencies]
glob = "0.3.0"
regex = "1.3.3"
structopt = "0.3"

View File

@ -1,7 +0,0 @@
use crate::common::*;
pub(crate) struct Bep {
pub(crate) number: usize,
pub(crate) title: String,
pub(crate) status: Status,
}

View File

@ -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};

View File

@ -1,10 +0,0 @@
mod bep;
mod common;
mod opt;
mod status;
use crate::common::*;
fn main() -> Result<(), Box<dyn Error>> {
Opt::from_args().run()
}

View File

@ -1,223 +0,0 @@
use crate::common::*;
const README: &str = "README.md";
const HEADING_PATTERN: &str = "(?m)^(?P<MARKER>#+) (?P<TEXT>.*)$";
const TOC_PATTERN: &str = "(?ms)## Manual.*## General";
#[derive(StructOpt)]
pub(crate) enum Opt {
SupportedBeps,
Toc,
}
impl Opt {
pub(crate) fn run(self) -> Result<(), Box<dyn Error>> {
match self {
Self::Toc => Self::update_toc(),
Self::SupportedBeps => Self::update_supported_beps(),
}
}
fn update_toc() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
let title_re = Regex::new("(?m)^:Title: (?P<title>.*)$")?;
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(())
}
}

View File

@ -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
),
}
}
}

View File

@ -9,7 +9,7 @@ Summary
- [`imdl torrent`](./commands/imdl-torrent.md) - [`imdl torrent`](./commands/imdl-torrent.md)
- [`imdl torrent create`](./commands/imdl-torrent-create.md) - [`imdl torrent create`](./commands/imdl-torrent-create.md)
- [`imdl torrent link`](./commands/imdl-torrent-link.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 show`](./commands/imdl-torrent-show.md)
- [`imdl torrent stats`](./commands/imdl-torrent-stats.md) - [`imdl torrent stats`](./commands/imdl-torrent-stats.md)
- [`imdl torrent verify`](./commands/imdl-torrent-verify.md) - [`imdl torrent verify`](./commands/imdl-torrent-verify.md)

View File

@ -4,14 +4,17 @@ imdl-completions 0.1.5
Print shell completion scripts to standard output. Print shell completion scripts to standard output.
USAGE: USAGE:
imdl completions --shell <SHELL> imdl completions [OPTIONS] --shell <SHELL>
FLAGS: FLAGS:
-h, --help Print help message. -h, --help Print help message.
-V, --version Print version number. -V, --version Print version number.
OPTIONS: OPTIONS:
-s, --shell <SHELL> Print completions for `SHELL`. [possible values: zsh, -d, --dir <DIR> Write completion script to `DIR` with an appropriate
bash, fish, powershell, elvish] 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]
``` ```

View File

@ -10,13 +10,13 @@ FLAGS:
-h, --help Print help message. -h, --help Print help message.
-O, --open Open generated magnet link. Uses `xdg-open`, `gnome-open`, -O, --open Open generated magnet link. Uses `xdg-open`, `gnome-open`,
or `kde-open` on Linux; `open` on macOS; and `cmd /C start` or `kde-open` on Linux; `open` on macOS; and `cmd /C start`
on Windows on Windows.
-V, --version Print version number. -V, --version Print version number.
OPTIONS: OPTIONS:
-s, --select-only <INDICES>... -s, --select-only <INDICES>...
Specify files that torrent clients select for download. Values are Select files to download. Values are indices into the `info.files`
indices into the info.files list. e.g. `--select-only 1,2,3` list, e.g. `--select-only 1,2,3`.
-i, --input <METAINFO> -i, --input <METAINFO>
Generate magnet link from metainfo at `PATH`. If `PATH` is `-`, read Generate magnet link from metainfo at `PATH`. If `PATH` is `-`, read
metainfo from standard input. metainfo from standard input.

View File

@ -1,4 +1,4 @@
# `imdl torrent piece length` # `imdl torrent piece-length`
``` ```
imdl-torrent-piece-length 0.1.5 imdl-torrent-piece-length 0.1.5
Display information about automatic piece length selection. Display information about automatic piece length selection.

View File

@ -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: Intermodal can be used to create `.torrent` files:
``` ```sh
$ imdl torrent create --input foo $ imdl torrent create --input foo
``` ```
Print information about existing torrent files: Print information about existing `.torrent` files:
``` ```sh
$ imdl torrent show --input foo.torrent $ imdl torrent show --input foo.torrent
``` ```
Verify downloaded torrents: Verify downloaded torrents:
``` ```sh
$ imdl torrent verify --input foo.torrent --content foo $ 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 $ 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: 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: Print information about a collection of torrents:
``` ```sh
$ imdl --unstable torrent stats --input dir $ imdl --unstable torrent stats --input dir
``` ```
Happy sharing! Happy sharing!

View File

@ -213,8 +213,10 @@ esac
;; ;;
(completions) (completions)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'-s+[Print completions for `SHELL`.]: :(zsh bash fish powershell elvish)' \ '-s+[Print completion script for `SHELL`.]: :(zsh bash fish powershell elvish)' \
'--shell=[Print completions 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.]' \ '-h[Print help message.]' \
'--help[Print help message.]' \ '--help[Print help message.]' \
'-V[Print version number.]' \ '-V[Print version number.]' \

View File

@ -192,8 +192,10 @@ Sort in ascending order by size, break ties in descending path order:
break break
} }
'imdl;completions' { 'imdl;completions' {
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Print completions for `SHELL`.') [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Print completion script for `SHELL`.')
[CompletionResult]::new('--shell', 'shell', [CompletionResultType]::ParameterName, 'Print completions 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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help message.')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help message.') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help message.')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version number.') [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version number.')

View File

@ -70,7 +70,7 @@ _imdl() {
;; ;;
imdl__completions) 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 if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -85,6 +85,14 @@ _imdl() {
COMPREPLY=($(compgen -W "zsh bash fish powershell elvish" -- "${cur}")) COMPREPLY=($(compgen -W "zsh bash fish powershell elvish" -- "${cur}"))
return 0 return 0
;; ;;
--dir)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;

View File

@ -178,8 +178,10 @@ Sort in ascending order by size, break ties in descending path order:
cand --version 'Prints version information' cand --version 'Prints version information'
} }
&'imdl;completions'= { &'imdl;completions'= {
cand -s 'Print completions for `SHELL`.' cand -s 'Print completion script for `SHELL`.'
cand --shell 'Print completions 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 -h 'Print help message.'
cand --help 'Print help message.' cand --help 'Print help message.'
cand -V 'Print version number.' cand -V 'Print version number.'

View File

@ -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 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 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 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 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 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' complete -c imdl -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information'

View File

@ -51,10 +51,6 @@ preview-readme:
book: book:
mdbook serve book --open --dest-dir ../www/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: dev-deps:
brew install grip brew install grip
cargo install mdbook cargo install mdbook
@ -63,37 +59,16 @@ dev-deps:
brew install imagemagick brew install imagemagick
brew install gifsicle brew install gifsicle
# update readme table of contents # update generated documentation
update-toc: gen:
cargo run --package update-readme toc cargo run --package gen all
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
check-minimal-versions: check-minimal-versions:
./bin/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 git diff --no-ext-diff --quiet --exit-code
cargo +nightly fmt --all -- --check cargo +nightly fmt --all -- --check
cargo run --package update-readme toc
git diff --no-ext-diff --quiet --exit-code
draft: push draft: push
hub pull-request -o --draft hub pull-request -o --draft
@ -109,9 +84,7 @@ merge:
done done
just done just done
update: man changelog-update update-toc publish-check: check
publish-check: check check-man
cargo outdated --exit-code 1 cargo outdated --exit-code 1
grep {{version}} CHANGELOG.md grep {{version}} CHANGELOG.md
@ -126,15 +99,6 @@ publish: publish-check
cargo publish cargo publish
just merge 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 # record, upload, and render demo animation
demo: demo-record demo-upload demo-render demo: demo-record demo-upload demo-render

View File

@ -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" .TH IMDL-COMPLETIONS "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ completions\fR \fBimdl\ completions\fR
- Print shell completion scripts to standard output. - Print shell completion scripts to standard output.
.SH "SYNOPSIS:" .SH "SYNOPSIS:"
.IP .IP
imdl completions \fB\-\-shell\fR <SHELL> imdl completions [OPTIONS] \fB\-\-shell\fR <SHELL>
.SH "FLAGS:" .SH "FLAGS:"
.TP .TP
\fB\-h\fR, \fB\-\-help\fR \fB\-h\fR, \fB\-\-help\fR
@ -15,5 +15,9 @@ Print help message.
Print version number. Print version number.
.SH "OPTIONS:" .SH "OPTIONS:"
.TP .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> \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]

View File

@ -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" .TH IMDL-TORRENT-CREATE "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ torrent\ create\fR \fBimdl\ torrent\ create\fR

View File

@ -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" .TH IMDL-TORRENT-LINK "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ torrent\ link\fR \fBimdl\ torrent\ link\fR
@ -13,15 +13,15 @@ Print help message.
.TP .TP
\fB\-O\fR, \fB\-\-open\fR \fB\-O\fR, \fB\-\-open\fR
Open generated magnet link. Uses `xdg\-open`, `gnome\-open`, or `kde\-open` on Linux; `open` on macOS; 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 .TP
\fB\-V\fR, \fB\-\-version\fR \fB\-V\fR, \fB\-\-version\fR
Print version number. Print version number.
.SH "OPTIONS:" .SH "OPTIONS:"
.TP .TP
\fB\-s\fR, \fB\-\-select\-only\fR <INDICES>... \fB\-s\fR, \fB\-\-select\-only\fR <INDICES>...
Specify files that torrent clients select for download. Values are indices into Select files to download. Values are indices into the `info.files` list, e.g.
the info.files list. e.g. `\-\-select\-only 1,2,3` `\-\-select\-only 1,2,3`.
.TP .TP
\fB\-i\fR, \fB\-\-input\fR <METAINFO> \fB\-i\fR, \fB\-\-input\fR <METAINFO>
Generate magnet link from metainfo at `PATH`. If `PATH` is `\-`, read metainfo from Generate magnet link from metainfo at `PATH`. If `PATH` is `\-`, read metainfo from

View File

@ -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" .TH IMDL-TORRENT-PIECE-LENGTH "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ torrent\ piece-length\fR \fBimdl\ torrent\ piece-length\fR

View File

@ -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" .TH IMDL-TORRENT-SHOW "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ torrent\ show\fR \fBimdl\ torrent\ show\fR

View File

@ -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" .TH IMDL-TORRENT-STATS "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ torrent\ stats\fR \fBimdl\ torrent\ stats\fR

View File

@ -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" .TH IMDL-TORRENT-VERIFY "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ torrent\ verify\fR \fBimdl\ torrent\ verify\fR

View File

@ -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" .TH IMDL-TORRENT "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\ torrent\fR \fBimdl\ torrent\fR

View File

@ -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" .TH \FBIMDL\FR "1" "April 2020" "Intermodal v0.1.5" "Intermodal Manual"
.SH NAME .SH NAME
\fBimdl\fR \fBimdl\fR

View File

@ -17,6 +17,7 @@ pub(crate) use std::{
path::{self, Path, PathBuf}, path::{self, Path, PathBuf},
process::{self, ExitStatus}, process::{self, ExitStatus},
str::{self, FromStr}, str::{self, FromStr},
string::FromUtf8Error,
sync::Once, sync::Once,
time::{SystemTime, SystemTimeError}, time::{SystemTime, SystemTimeError},
usize, usize,
@ -39,8 +40,8 @@ pub(crate) use structopt::{
clap::{self, AppSettings}, clap::{self, AppSettings},
StructOpt, StructOpt,
}; };
pub(crate) use strum::VariantNames; pub(crate) use strum::{IntoEnumIterator, VariantNames};
pub(crate) use strum_macros::{EnumString, EnumVariantNames, IntoStaticStr}; pub(crate) use strum_macros::{EnumIter, EnumString, EnumVariantNames, IntoStaticStr};
pub(crate) use unicode_width::UnicodeWidthStr; pub(crate) use unicode_width::UnicodeWidthStr;
pub(crate) use url::{Host, Url}; pub(crate) use url::{Host, Url};
pub(crate) use walkdir::WalkDir; pub(crate) use walkdir::WalkDir;

View File

@ -187,6 +187,11 @@ impl Env {
Ok(self.dir().join(path).clean()) 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> { pub(crate) fn read(&mut self, source: InputTarget) -> Result<Input> {
let data = match &source { let data = match &source {
InputTarget::Path(path) => { InputTarget::Path(path) => {

View File

@ -111,6 +111,8 @@ pub(crate) enum Error {
PieceLengthZero, PieceLengthZero,
#[snafu(display("Private torrents must have tracker"))] #[snafu(display("Private torrents must have tracker"))]
PrivateTrackerless, 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))] #[snafu(display("Failed to write to standard error: {}", source))]
Stderr { source: io::Error }, Stderr { source: io::Error },
#[snafu(display("Failed to read from standard input: {}", source))] #[snafu(display("Failed to read from standard input: {}", source))]

View File

@ -34,6 +34,9 @@ mod errln;
#[macro_use] #[macro_use]
mod err; mod err;
#[macro_use]
mod out;
#[macro_use] #[macro_use]
mod outln; mod outln;

11
src/out.rs Normal file
View File

@ -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)
};
}

View File

@ -2,7 +2,7 @@ use super::*;
use structopt::clap; use structopt::clap;
#[derive(EnumVariantNames, EnumString)] #[derive(Copy, Clone, EnumVariantNames, IntoStaticStr, EnumString, EnumIter, Debug)]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
pub(crate) enum Shell { pub(crate) enum Shell {
Zsh, Zsh,
@ -12,6 +12,38 @@ pub(crate) enum Shell {
Elvish, 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 { impl Into<clap::Shell> for Shell {
fn into(self) -> clap::Shell { fn into(self) -> clap::Shell {
match self { match self {

View File

@ -12,24 +12,48 @@ pub(crate) struct Completions {
short = "s", short = "s",
value_name = "SHELL", value_name = "SHELL",
possible_values = Shell::VARIANTS, 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 { impl Completions {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { pub(crate) fn run(self, env: &mut Env) -> Result<()> {
let buffer = Vec::new(); if let Some(shell) = self.shell {
let mut cursor = Cursor::new(buffer); 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(); Ok(())
}
let script = String::from_utf8(buffer).expect("Clap completion not UTF-8");
outln!(env, "{}", script.trim())?;
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(()) Ok(())
} }
} }
@ -53,4 +77,44 @@ mod tests {
assert!(env.out().starts_with("_imdl() {")); 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"));
}
} }

View File

@ -67,6 +67,10 @@ impl TestEnv {
fs::create_dir(self.env.resolve(path).unwrap()).unwrap(); 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)] #[cfg(unix)]
pub(crate) fn metadata(&self, path: impl AsRef<Path>) -> fs::Metadata { pub(crate) fn metadata(&self, path: impl AsRef<Path>) -> fs::Metadata {
fs::metadata(self.env.resolve(path).unwrap()).unwrap() fs::metadata(self.env.resolve(path).unwrap()).unwrap()