Criando e empacotando uma aplicação CLI

fn main() {
    println!("Olá, rustáceos!");
}

Domain Working Groups de 2018

Fonte: internals.rust-lang.org/t/announcing-the-2018-domain-working-groups/6737

Serviços de rede

Com foco na experiência de ponta a ponta para código tanto síncrono quanto assíncrono, em coordenação com o crescente ecossistema desse espaço.

WebAssembly

Com foco na experiência de ponta a ponta de embutir código Rust em bibliotecas e apps em JS via WebAssembly.

Aplicações CLI

Com foco na experiência de ponta a ponta de escrever aplicações CLI, pequenas ou grande, em Rust.

Dispositivos embarcados

Com foco na experiência de ponta a ponta de usar Rust em ambientes com recursos limitados e plataformas não tradicionais.

CLI Working Group

github.com/rust-lang-nursery/cli-wg

cli-wg/issues

O que é uma CLI?

Command Line Interface

  • Inicia em um terminal
  • Aceita configuração de várias fontes, e.g.:
    • Argumentos de linha de comando
    • Variáveis de ambiente
    • Arquivos de configuração
  • Executa até terminar, com pouca ou nenhuma intervenção do usuário
  • Aceita dados de entrada da stdin, de arquivos ou da rede
  • Executa operações sobre alguma entrada de acordo com a configuração especificada
  • Se comunica através das saídas padrão (arquivos, rede, stdout, stderr)

O que não é uma CLI?

O que não é uma CLI?

Uma interface textual (TUI), usando algo como ncurses

ncmpcpp, exemplo de TUI

O que não é uma CLI?

Uma interface gráfica (GUI), usando algo como Qt ou GTK

pavucontrol, exemplo de GUI

Parsing de Argumentos

Objetivos

  • Definir a interface
  • Impôr ao usuário
  • Apresentar documentação e mensagens de erro úteis

Abordagens

  • CLAP
  • StructOpt
  • Thunder
  • Docopt
  • std

CLAP

Command Line Argument Parser

  • Usado até mesmo no Cargo e no Rustup
  • É o mais robusto de todos
  • Porém é o mais verboso
  • Várias formas de definir a interface

(Os exemplos a seguir foram extraídos da documentação oficial do CLAP)

Builder Pattern

  • Permite configurações mais avançadas (inclusive gerar argumentos dinamicamente)
  • Muito verboso

# #![allow(unused_variables)]
#fn main() {
extern crate clap;
use clap::{Arg, App, SubCommand};

let matches = App::new("My Super Program")
                      .version("1.0")
                      .author("Kevin K. <kbknapp@gmail.com>")
                      .about("Does awesome things")
                      .arg(Arg::with_name("config")
                          .short("c")
                          .long("config")
                          .value_name("FILE")
                          .help("Sets a custom config file")
                          .takes_value(true))
                      .arg(Arg::with_name("INPUT")
                          .help("Sets the input file to use")
                          .required(true)
                          .index(1))
                      .arg(Arg::with_name("v")
                          .short("v")
                          .multiple(true)
                          .help("Sets the level of verbosity"))
                      .subcommand(SubCommand::with_name("test")
                                  .about("controls testing features")
                                  .version("1.3")
                                  .author("Someone E. <someone_else@other.com>")
                                  .arg(Arg::with_name("debug")
                                      .short("d")
                                      .help("print debug information verbosely")))
                      .get_matches();
#}

Usage String

  • Menos verboso
  • Sacrifica alguma das configurações avançadas
  • Implica em impacto muito pequeno no desempenho em tempo de execução

# #![allow(unused_variables)]
#fn main() {
extern crate clap;
use clap::{Arg, App, SubCommand};

let matches = App::new("myapp")
                      .version("1.0")
                      .author("Kevin K. <kbknapp@gmail.com>")
                      .about("Does awesome things")
                      .args_from_usage(
                          "-c, --config=[FILE] 'Sets a custom config file'
                          <INPUT>              'Sets the input file to use'
                          -v...                'Sets the level of verbosity'")
                      .subcommand(SubCommand::with_name("test")
                                  .about("controls testing features")
                                  .version("1.3")
                                  .author("Someone E. <someone_else@other.com>")
                                  .arg_from_usage("-d, --debug 'Print debug information'"))
                      .get_matches();
#}

YAML

  • Mantém o código Rust mais limpo
  • Facilita a internacionalização
  • Adiciona mais dependências à crate
  • Tem um impacto um pouco maior no desempenho em tempo de execução
name: myapp
version: "1.0"
author: Kevin K. <kbknapp@gmail.com>
about: Does awesome things
args:
    - config:
        short: c
        long: config
        value_name: FILE
        help: Sets a custom config file
        takes_value: true
    - INPUT:
        help: Sets the input file to use
        required: true
        index: 1
    - verbose:
        short: v
        multiple: true
        help: Sets the level of verbosity
subcommands:
    - test:
        about: controls testing features
        version: "1.3"
        author: Someone E. <someone_else@other.com>
        args:
            - debug:
                short: d
                help: print debug information

# #![allow(unused_variables)]
#fn main() {
#[macro_use]
extern crate clap;
use clap::App;

let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml).get_matches();
#}

Macro

Abordagem híbrida: desempenho do builder pattern sem o boiler plate.


# #![allow(unused_variables)]
#fn main() {
#[macro_use]
extern crate clap;

let matches = clap_app!(myapp =>
    (version: "1.0")
    (author: "Kevin K. <kbknapp@gmail.com>")
    (about: "Does awesome things")
    (@arg CONFIG: -c --config +takes_value "Sets a custom config file")
    (@arg INPUT: +required "Sets the input file to use")
    (@arg debug: -d ... "Sets the level of debugging information")
    (@subcommand test =>
        (about: "controls testing features")
        (version: "1.3")
        (author: "Someone E. <someone_else@other.com>")
        (@arg verbose: -v --verbose "Print test information verbosely")
    )
).get_matches();
#}

Extração dos Argumentos


# #![allow(unused_variables)]
#fn main() {
// Extrai o valor de `config`, se passado pelo usuário, senão usa "default.conf"
let config = matches.value_of("config").unwrap_or("default.conf");
println!("Value for config: {}", config);

// Chamar .unwrap() é seguro aqui, porque `INPUT` é necessário.
// Se `INPUT` não fosse obrigatório, poderia-se usar um `if let` para extrair o valor condicionalmente
println!("Using input file: {}", matches.value_of("INPUT").unwrap());

// A saída varia de acordo com quantas vezes o usuário passou a flag `verbose`.
// i.e. `myprog -v -v -v` or `myprog -vvv` vs `myprog -v`
match matches.occurrences_of("v") {
    0 => println!("No verbose info"),
    1 => println!("Some verbose info"),
    2 => println!("Tons of verbose info"),
    3 | _ => println!("Don't be crazy"),
}

// ...resto da lógica da aplicação
#}
$ myprog --help
My Super Program 1.0
Kevin K. <kbknapp@gmail.com>
Does awesome things

USAGE:
    MyApp [FLAGS] [OPTIONS] <INPUT> [SUBCOMMAND]

FLAGS:
    -h, --help       Prints this message
    -v               Sets the level of verbosity
    -V, --version    Prints version information

OPTIONS:
    -c, --config <FILE>    Sets a custom config file

ARGS:
    INPUT    The input file to use

SUBCOMMANDS:
    help    Prints this message
    test    Controls testing features

StructOpt

  • Usa macros pra gerar código que usa CLAP
  • Expõe os argumentos recebidos como uma struct normal
  • (é o meu preferido)

Struct

#[macro_use]
extern crate structopt;
use structopt::StructOpt;

use std::path::PathBuf;

#[derive(StructOpt, Debug)]
#[structopt(name = "example", about = "An example of StructOpt usage.")]
struct Opt {

    /// Activate debug mode
    #[structopt(short = "d", long = "debug")]
    debug: bool,

    /// Set speed
    #[structopt(short = "s", long = "speed", default_value = "42")]
    speed: f64,

    /// Input file
    #[structopt(parse(from_os_str))]
    input: PathBuf,

    /// Output file, stdout if not present
    #[structopt(parse(from_os_str))]
    output: Option<PathBuf>,

}

fn main() {
    let opt = Opt::from_args();
    println!("{:?}", opt);
}

Enum


# #![allow(unused_variables)]
#fn main() {
#[derive(StructOpt)]
#[structopt(name = "git", about = "the stupid content tracker")]
enum Git {

    #[structopt(name = "add")]
    Add {
        #[structopt(short = "i")]
        interactive: bool,

        #[structopt(short = "p")]
        patch: bool,

        #[structopt(parse(from_os_str))]
        files: Vec<PathBuf>
    },

    #[structopt(name = "fetch")]
    Fetch {
        #[structopt(long = "dry-run")]
        dry_run: bool,

        #[structopt(long = "all")]
        all: bool,

        repository: Option<String>
    },

    #[structopt(name = "commit")]
    Commit {
        #[structopt(short = "m")]
        message: Option<String>,

        #[structopt(short = "a")]
        all: bool
    }

}
#}

Thunder

  • Também usa macros pra gerar código que usa CLAP
  • Expõe métodos como subcomandos
  • Os argumentos são criados a partir dos parâmetros
struct MyApp;

#[thunderclap]
impl MyApp {
    /// Say hello to someone on the other side
    fn say_hello(name: &str, age: Option<u16>) { /* ... */ }

    /// It was nice to meet you!
    fn goodybe(name: Option<&str>) { /* ... */ }
}

fn main() {
    MyApp::start();
}

Docopt

  • Biblioteca disponível em várias linguagens, originalmente criada para Python
  • A interface é definida a partir da string que a documenta
#[macro_use]
extern crate serde_derive;

extern crate docopt;
use docopt::Docopt;

const USAGE: &'static str = "
Naval Fate.

Usage:
  naval_fate.py ship new <name>...
  naval_fate.py ship <name> move <x> <y> [--speed=<kn>]
  naval_fate.py ship shoot <x> <y>
  naval_fate.py mine (set|remove) <x> <y> [--moored | --drifting]
  naval_fate.py (-h | --help)
  naval_fate.py --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.
";

#[derive(Debug, Deserialize)]
struct Args {
    flag_speed: isize,
    flag_drifting: bool,
    arg_name: Vec<String>,
    arg_x: Option<i32>,
    arg_y: Option<i32>,
    cmd_ship: bool,
    cmd_mine: bool,
}

fn main() {
    let args: Args = Docopt::new(USAGE)
                            .and_then(|d| d.deserialize())
                            .unwrap_or_else(|e| e.exit());
    println!("{:?}", args);
}

std

  • Para tarefas simples, ferramentas simples

# #![allow(unused_variables)]
#fn main() {
use std::env;

for argument in env::args() {
    println!("{}", argument);
}

for (key, value) in env::vars() {
    println!("{}: {}", key, value);
}

let key = "HOME";
match env::var(key) {
    Ok(val) => println!("{}: {:?}", key, val),
    Err(e) => println!("couldn't interpret {}: {}", key, e),
}
#}

Tratamento de Erros

unwrap/expect

use std::fs;

fn main() {
    let contents = fs::read_to_string("file.txt");
    println!("{}", contents.unwrap());
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', libcore/result.rs:945:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
  • unwrap deve ser usado com muita moderação.
  • Se for usar, prefira sempre expect.
  • Idealmente, deixe um comentário justificando o uso.

? no main

  • main pode retornar Return<(), E: Debug>
  • ? pode ser usado como unwrap/early return em funções que retornam Option ou Result
use std::{fs, io};

fn main() -> Result<(), io::Error> {
    let contents = fs::read_to_string("file.txt");
    println!("{}", contents?);

    Ok(())
}
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Lidando com vários tipos de erros

use std::{error::Error, fs};

fn main() -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string("file.txt");
    println!("{}", contents?);

    Ok(())
}
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

# #![allow(unused_variables)]
#fn main() {
pub trait Error: Debug + Display {
    fn description(&self) -> &str { ... }
    fn cause(&self) -> Option<&Error> { ... }
}
#}

Failure


# #![allow(unused_variables)]
#fn main() {
extern crate failure;
#[macro_use] extern crate failure_derive;

use std::fmt

#[derive(Fail, Debug)]
struct MyError {
    code: i32,
    message: String,
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "An error occurred with error code {}. ({})", self.code, self.message)
    }
}
#}

# #![allow(unused_variables)]
#fn main() {
extern crate failure;
#[macro_use] extern crate failure_derive;

#[derive(Fail, Debug)]
#[fail(display = "An error occurred with error code {}. ({})", code, message)]
struct MyError {
    code: i32,
    message: String,
}
#}
extern crate failure;

use std::fs;

fn main() -> Result<(), failure::Error> {
    let contents = fs::read_to_string("file.txt");
    println!("{}", contents?);

    Ok(())
}
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

ExitFailure

extern crate failure;

extern crate exitfailure;
use exitfailure::ExitFailure;

use std::{env, fs, io};

fn main() -> Result<(), ExitFailure> {
    let contents = fs::read_to_string("file.txt");
    println!("{}", contents?);

    Ok(())
}
Error: No such file or directory (os error 2)

human-panic

Se tudo falhar...

#[macro_use]
extern crate human_panic;

fn main() {
   setup_panic!();

   println!("A normal log message");
   panic!("OMG EVERYTHING IS ON FIRE!!!")
}

Sem human-panic

thread 'main' panicked at 'OMG EVERYTHING IS ON FIRE!!!', examples/main.rs:2:3
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Com human-panic

Well, this is embarrassing.

human-panic had a problem and crashed.
To help us diagnose the problem you can send us a crash report.

We have generated a report file at "/var/folders/zw/bpfvmq390lv2c6gn_6byyv0w0000gn/T/report-8351cad6-d2b5-4fe8-accd-1fcbf4538792.toml".
Submit an issue or email with the subject of "human-panic Crash Report" and include the report as an attachment.

- Homepage: https://github.com/yoshuawuyts/human-panic
- Authors: Yoshua Wuyts <yoshuawuyts@gmail.com>

We take privacy seriously, and do not perform any automated error collection.
In order to improve the software, we rely on people to submit reports.

Thank you kindly!

Testes

assert_cmd


# #![allow(unused_variables)]
#fn main() {
extern crate assert_cmd;

use std::process::Command;
use assert_cmd::prelude::*;

Command::main_binary()
    .unwrap()
    .assert()
    .success();
#}

rexpect

extern crate rexpect;

use rexpect::spawn;
use rexpect::errors::*;

fn do_ftp() -> Result<()> {
    let mut p = spawn("ftp speedtest.tele2.net", Some(30_000))?;
    p.exp_regex("Name \\(.*\\):")?;
    p.send_line("anonymous")?;
    p.exp_string("Password")?;
    p.send_line("test")?;
    p.exp_string("ftp>")?;
    p.send_line("cd upload")?;
    p.exp_string("successfully changed.\r\nftp>")?;
    p.send_line("pwd")?;
    p.exp_regex("[0-9]+ \"/upload\"")?;
    p.send_line("exit")?;
    p.exp_eof()?;
    Ok(())
}


fn main() {
    do_ftp().unwrap_or_else(|e| panic!("ftp job failed with {}", e));
}

Empacotamento

Cargo

~ $ cargo new foo
     Created binary (application) `foo` project

~ $ exa -T foo
foo
├── Cargo.toml
└── src
   └── main.rs
[package]
name = "foo"
version = "0.1.0"
authors = ["Cauê Baasch de Souza <cauebs@pm.me>"]

[dependencies]
~/foo $ cargo run
   Compiling foo v0.1.0 (file:///home/cauebs/foo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/foo`
Hello, world!
~/foo $ cargo publish
~ $ cargo install foo

Compilação Cruzada

~ $ rustup target add x86_64-unknown-linux-musl
~/foo $ cargo build --target x86_64-unknown-linux-musl

github.com/japaric/cross

CI: github.com/japaric/trust (Travis + AppVeyor)

Geração de Pacotes

  • cargo-deb
  • cargo-rpm
  • cargo-pkgbuild
  • cargo-ebuild
  • cargo-wix

Documentação

Geração de manpages

github.com/rust-clique/man

Edição do README

github.com/yoshuawuyts/markdown-edit

Exemplos de sucesso

github.com/ogham/exa

exa

github.com/BurntSushi/ripgrep

rg

bench

github.com/sharkdp/fd

fd


github.com/sharkdp/bat

bat

github.com/Aaronepower/tokei

~/durt $ tokei
----------------------------------------------------------
 Language    Files    Lines    Code    Comments    Blanks
----------------------------------------------------------
 Markdown        1       47      47           0         0
 Rust            1      164     123          11        30
 TOML            1       18      15           0         3
----------------------------------------------------------
 Total           3      229     185          11        33
----------------------------------------------------------

github.com/sharkdp/hyperfine

hyperfine

github.com/cauebs/durt

~/durt $ durt --sort --total --percentage *
     499 B  ( 0.49%)  Cargo.toml
   1.26 kB  ( 1.23%)  README.md
   9.87 kB  ( 9.67%)  Cargo.lock
  35.15 kB  (34.43%)  LICENSE
  55.30 kB  (54.17%)  src
 ---------
 102.08 kB

(Todo feedback é bem vindo!)

Obrigado pela atenção!