Criando e empacotando uma aplicação CLI
fn main() { println!("Olá, rustáceos!"); }
- Slides criados com mdBook
- Disponíveis em cauebs.github.io/rust-cli-talk
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
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
O que não é uma CLI?
Uma interface gráfica (GUI), usando algo como Qt ou GTK
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 retornarReturn<(), E: Debug>
?
pode ser usado comounwrap
/early return
em funções que retornamOption
ouResult
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
Edição do README
github.com/yoshuawuyts/markdown-edit
Exemplos de sucesso
github.com/ogham/exa
github.com/BurntSushi/ripgrep
github.com/sharkdp/fd
github.com/sharkdp/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
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!)