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:
parent
1f8023d13a
commit
04338e3501
30
.github/workflows/build.yaml
vendored
30
.github/workflows/build.yaml
vendored
|
@ -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
7
.ignore
Normal file
|
@ -0,0 +1,7 @@
|
|||
/CHANGELOG.md
|
||||
/README.md
|
||||
/completions
|
||||
/man
|
||||
/book/src/commands
|
||||
/book/src/SUMMARY.md
|
||||
/book/src/introduction.md
|
|
@ -2,9 +2,11 @@ Changelog
|
|||
=========
|
||||
|
||||
|
||||
UNRELEASED - 2020-04-13
|
||||
UNRELEASED - 2020-04-18
|
||||
-----------------------
|
||||
- :sparkles: [`xxxxxxxxxxxx`](https://github.com/casey/intermodal/commits/master) Partially implement BEP 53 - Fixes [#245](https://github.com/casey/intermodal/issues/245) - _strickinato <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
124
Cargo.lock
generated
|
@ -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"
|
||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -62,15 +62,9 @@ temptree = "0.0.0"
|
|||
|
||||
[workspace]
|
||||
members = [
|
||||
# generate documentation
|
||||
"bin/gen",
|
||||
|
||||
# run commands for demo animation
|
||||
"bin/demo",
|
||||
|
||||
# generate table of contents and table of supported BEPs in README.md
|
||||
"bin/update-readme",
|
||||
|
||||
# generate the changelog from git commits
|
||||
"bin/changelog",
|
||||
|
||||
# generate man page
|
||||
"bin/man"
|
||||
]
|
||||
|
|
67
README.md
67
README.md
|
@ -1,4 +1,4 @@
|
|||
# intermodal: a 40' shipping container for the Internet
|
||||
# Intermodal: A 40' shipping container for the Internet
|
||||
|
||||
[![Crate](https://img.shields.io/crates/v/imdl.svg?logo=rust)](https://crates.io/crates/imdl)
|
||||
[![Build](https://github.com/casey/intermodal/workflows/Build/badge.svg)](https://github.com/casey/intermodal/actions)
|
||||
|
@ -13,37 +13,34 @@ For more about the project and its goals, check out [this post](https://rodarmor
|
|||
|
||||
![demonstration animation](https://raw.githubusercontent.com/casey/intermodal/master/www/demo.gif)
|
||||
|
||||
## Manual
|
||||
## Table of Contents
|
||||
|
||||
- [General](#general)
|
||||
- [Installation](#installation)
|
||||
- [Supported Operating Systems](#supported-operating-systems)
|
||||
- [Packages](#packages)
|
||||
- [Pre-built binaries](#pre-built-binaries)
|
||||
- [Cargo](#cargo)
|
||||
- [Shell Completion Scripts](#shell-completion-scripts)
|
||||
- [Semantic Versioning](#semantic-versioning)
|
||||
- [Unstable Features](#unstable-features)
|
||||
- [Source Signatures](#source-signatures)
|
||||
- [Installation](#installation)
|
||||
- [Supported Operating Systems](#supported-operating-systems)
|
||||
- [Packages](#packages)
|
||||
- [Pre-built binaries](#pre-built-binaries)
|
||||
- [Cargo](#cargo)
|
||||
- [Shell Completion Scripts](#shell-completion-scripts)
|
||||
- [Semantic Versioning](#semantic-versioning)
|
||||
- [Unstable Features](#unstable-features)
|
||||
- [Source Signatures](#source-signatures)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
|
||||
## General
|
||||
## Installation
|
||||
|
||||
### Installation
|
||||
|
||||
#### Supported Operating Systems
|
||||
### Supported Operating Systems
|
||||
|
||||
`imdl` supports Linux, MacOS, and Windows, and should work on other unix OSes.
|
||||
If it does not, please open an issue!
|
||||
|
||||
#### Packages
|
||||
### Packages
|
||||
|
||||
| Operating System | Package Manager | Package | Command |
|
||||
|:--------------------------------------------------------------------:|:-----------------------------------:|:-------------------------------------------------------------------------:|---------------------:|
|
||||
| [Various](https://forge.rust-lang.org/release/platform-support.html) | [Cargo](https://www.rust-lang.org) | [imdl](https://crates.io/crates/imdl) | `cargo install imdl` |
|
||||
| [Arch Linux](https://www.archlinux.org) | [Yay](https://github.com/Jguer/yay) | [intermodal](https://aur.archlinux.org/packages/intermodal)<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
|
||||
|
|
|
@ -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"]
|
|
@ -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()?;
|
||||
}
|
|
@ -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
31
bin/gen/Cargo.toml
Normal 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
41
bin/gen/config.yaml
Normal 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"
|
|
@ -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());
|
|
@ -6,8 +6,8 @@ pub(crate) struct Changelog {
|
|||
|
||||
impl Changelog {
|
||||
#[throws]
|
||||
pub(crate) fn new(repo: &Repository) -> Self {
|
||||
let mut current = repo.head()?.peel_to_commit()?;
|
||||
pub(crate) fn new(project: &Project) -> Self {
|
||||
let mut current = project.repo.head()?.peel_to_commit()?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
|
@ -24,7 +24,7 @@ impl Changelog {
|
|||
let manifest_bytes = current
|
||||
.tree()?
|
||||
.get_path("Cargo.toml".as_ref())?
|
||||
.to_object(&repo)?
|
||||
.to_object(&project.repo)?
|
||||
.as_blob()
|
||||
.unwrap()
|
||||
.content()
|
||||
|
@ -32,7 +32,12 @@ impl Changelog {
|
|||
|
||||
let manifest = Manifest::from_slice(&manifest_bytes)?;
|
||||
|
||||
let entry = Entry::new(¤t, manifest.package.unwrap().version.as_ref(), head)?;
|
||||
let entry = Entry::new(
|
||||
¤t,
|
||||
manifest.package.unwrap().version.as_ref(),
|
||||
head,
|
||||
&project.config,
|
||||
)?;
|
||||
|
||||
entries.push(entry);
|
||||
}
|
17
bin/gen/src/cmd.rs
Normal file
17
bin/gen/src/cmd.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
macro_rules! cmd {
|
||||
{
|
||||
$bin:expr,
|
||||
$($arg:expr),*
|
||||
$(,)?
|
||||
} => {
|
||||
{
|
||||
let mut command = Command::new($bin);
|
||||
|
||||
$(
|
||||
command.arg($arg);
|
||||
)*
|
||||
|
||||
command
|
||||
}
|
||||
}
|
||||
}
|
22
bin/gen/src/command_ext.rs
Normal file
22
bin/gen/src/command_ext.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
17
bin/gen/src/config.rs
Normal 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)?
|
||||
}
|
||||
}
|
|
@ -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
10
bin/gen/src/example.rs
Normal 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,
|
||||
}
|
14
bin/gen/src/exit_status_ext.rs
Normal file
14
bin/gen/src/exit_status_ext.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
15
bin/gen/src/introduction.rs
Normal file
15
bin/gen/src/introduction.rs
Normal 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
31
bin/gen/src/main.rs
Normal 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)?;
|
||||
}
|
|
@ -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
153
bin/gen/src/opt.rs
Normal 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
57
bin/gen/src/project.rs
Normal 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
37
bin/gen/src/readme.rs
Normal 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
162
bin/gen/src/subcommand.rs
Normal 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
29
bin/gen/src/summary.rs
Normal 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
140
bin/gen/templates/README.md
Normal 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.
|
16
bin/gen/templates/SUMMARY.md
Normal file
16
bin/gen/templates/SUMMARY.md
Normal 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)
|
29
bin/gen/templates/introduction.md
Normal file
29
bin/gen/templates/introduction.md
Normal 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!
|
|
@ -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
|
5
bin/lint
5
bin/lint
|
@ -2,4 +2,7 @@
|
|||
|
||||
set -euxo pipefail
|
||||
|
||||
! grep --color -REni 'FIXME|TODO|XXX|todo!|#\[ignore]' src book/src
|
||||
! rg \
|
||||
--glob !bin/lint \
|
||||
--ignore-case \
|
||||
'FIXME|TODO|XXX|todo!|#\[ignore\]'
|
||||
|
|
|
@ -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"
|
|
@ -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};
|
|
@ -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)?;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -1,7 +0,0 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) struct Bep {
|
||||
pub(crate) number: usize,
|
||||
pub(crate) title: String,
|
||||
pub(crate) status: Status,
|
||||
}
|
|
@ -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};
|
|
@ -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()
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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.]' \
|
|
@ -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.')
|
|
@ -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=()
|
||||
;;
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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'
|
||||
|
|
46
justfile
46
justfile
|
@ -51,10 +51,6 @@ preview-readme:
|
|||
book:
|
||||
mdbook serve book --open --dest-dir ../www/book
|
||||
|
||||
# add git log messages to changelog
|
||||
changes:
|
||||
git log --pretty=format:%s >> CHANGELOG.md
|
||||
|
||||
dev-deps:
|
||||
brew install grip
|
||||
cargo install mdbook
|
||||
|
@ -63,37 +59,16 @@ dev-deps:
|
|||
brew install imagemagick
|
||||
brew install gifsicle
|
||||
|
||||
# update readme table of contents
|
||||
update-toc:
|
||||
cargo run --package update-readme toc
|
||||
|
||||
generate-completions:
|
||||
./bin/generate-completions
|
||||
|
||||
man-watch:
|
||||
cargo build
|
||||
cargo watch \
|
||||
--ignore '/man' \
|
||||
--ignore '/man/*' \
|
||||
--ignore '/book/src/commands' \
|
||||
--ignore '/book/src/commands/*' \
|
||||
--clear --exec 'run --package man'
|
||||
|
||||
man:
|
||||
cargo build
|
||||
cargo run --package man
|
||||
|
||||
check-man: man
|
||||
git diff --no-ext-diff --quiet --exit-code
|
||||
# update generated documentation
|
||||
gen:
|
||||
cargo run --package gen all
|
||||
|
||||
check-minimal-versions:
|
||||
./bin/check-minimal-versions
|
||||
|
||||
check: test clippy lint check-minimal-versions changelog-update
|
||||
check: test clippy lint check-minimal-versions gen
|
||||
git diff --no-ext-diff --quiet --exit-code
|
||||
cargo +nightly fmt --all -- --check
|
||||
cargo run --package update-readme toc
|
||||
git diff --no-ext-diff --quiet --exit-code
|
||||
|
||||
draft: push
|
||||
hub pull-request -o --draft
|
||||
|
@ -109,9 +84,7 @@ merge:
|
|||
done
|
||||
just done
|
||||
|
||||
update: man changelog-update update-toc
|
||||
|
||||
publish-check: check check-man
|
||||
publish-check: check
|
||||
cargo outdated --exit-code 1
|
||||
grep {{version}} CHANGELOG.md
|
||||
|
||||
|
@ -126,15 +99,6 @@ publish: publish-check
|
|||
cargo publish
|
||||
just merge
|
||||
|
||||
changelog-update:
|
||||
cargo run --package changelog update
|
||||
|
||||
changelog-types:
|
||||
cargo run --package changelog types
|
||||
|
||||
changelog-issue-template:
|
||||
cargo run --package changelog issue-template
|
||||
|
||||
# record, upload, and render demo animation
|
||||
demo: demo-record demo-upload demo-render
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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))]
|
||||
|
|
|
@ -34,6 +34,9 @@ mod errln;
|
|||
#[macro_use]
|
||||
mod err;
|
||||
|
||||
#[macro_use]
|
||||
mod out;
|
||||
|
||||
#[macro_use]
|
||||
mod outln;
|
||||
|
||||
|
|
11
src/out.rs
Normal file
11
src/out.rs
Normal 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)
|
||||
};
|
||||
}
|
34
src/shell.rs
34
src/shell.rs
|
@ -2,7 +2,7 @@ use super::*;
|
|||
|
||||
use structopt::clap;
|
||||
|
||||
#[derive(EnumVariantNames, EnumString)]
|
||||
#[derive(Copy, Clone, EnumVariantNames, IntoStaticStr, EnumString, EnumIter, Debug)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub(crate) enum Shell {
|
||||
Zsh,
|
||||
|
@ -12,6 +12,38 @@ pub(crate) enum Shell {
|
|||
Elvish,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub(crate) fn completion_script(self) -> Result<String> {
|
||||
let buffer = Vec::new();
|
||||
let mut cursor = Cursor::new(buffer);
|
||||
|
||||
Arguments::clap().gen_completions_to(env!("CARGO_PKG_NAME"), self.into(), &mut cursor);
|
||||
|
||||
let buffer = cursor.into_inner();
|
||||
|
||||
let script = String::from_utf8(buffer).context(error::ShellDecode { shell: self })?;
|
||||
|
||||
let mut script = script.trim().to_owned();
|
||||
script.push('\n');
|
||||
|
||||
Ok(script)
|
||||
}
|
||||
|
||||
pub(crate) fn completion_script_filename(self) -> &'static str {
|
||||
match self {
|
||||
Self::Bash => "imdl.bash",
|
||||
Self::Fish => "imdl.fish",
|
||||
Self::Zsh => "_imdl",
|
||||
Self::Powershell => "_imdl.ps1",
|
||||
Self::Elvish => "imdl.elvish",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(self) -> &'static str {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<clap::Shell> for Shell {
|
||||
fn into(self) -> clap::Shell {
|
||||
match self {
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue
Block a user