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:
- 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`

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>_
- :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",
]
[[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"

View File

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

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)
[![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)<sup>AUR</sup> | `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

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::*;
#[derive(Debug)]
pub(crate) struct Bin {
pub(crate) bin: String,
path: PathBuf,
pub(crate) subcommands: Vec<Subcommand>,
}
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<String>) {
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());

View File

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

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::{
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,
};

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 {
#[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(
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(),

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::*;
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, Clone)]
pub(crate) struct Metadata {
#[serde(rename = "type")]
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
! 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 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)

View File

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

View File

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

View File

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

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:
```
```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!

View File

@ -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.]' \

View File

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

View File

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

View File

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

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

View File

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

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"
.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]

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"
.SH NAME
\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"
.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

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"
.SH NAME
\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"
.SH NAME
\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"
.SH NAME
\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"
.SH NAME
\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"
.SH NAME
\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"
.SH NAME
\fBimdl\fR

View File

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

View File

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

View File

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

View File

@ -34,6 +34,9 @@ mod errln;
#[macro_use]
mod err;
#[macro_use]
mod out;
#[macro_use]
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;
#[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 {

View File

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

View File

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