Executando verificação de segurança...
6

Eco: minha implementação em Rust do programa echo

GitHub top language echo eco GitHub's license GitHub last commit (branch)

Hey, folks! Hoje quero falar sobre o meu mini-projeto, chamado "eco".

Minha implementação em Rust do programa echo.

O programa echo basicamente pega argumentos (entrada) e imprime eles na tela do seu console (stdout). O eco, atualmente, faz isso também. Escolhi esse nome porque eu sempre li o programa echo como /ɛko/. Como eu disse, o echo pega a entrada e devolve a mesma coisa, similar a uma reverberação, um eco.

A iniciativa do projeto surgiu quando pesquisei por projetos para fazer em Rust, quero praticar!

A implementação inicial (v0.1.0) foi bem simples, como esperado. Veja o código do commit 516992e:

use std::env;

fn main() {
    let mut args: Vec<String> = env::args().collect();

    /*
     * Read the `remove` docstring.
     * This `remove(0)` is the worst case,
     * all the arguments are going to be
     * shifted to the left, everytime.
     */
    args.remove(0);

    let mut output = String::new();

    for arg in args {
        output.push_str(&format!("{arg} "));
    }

    println!("{}", output.trim());
}
  1. Pega os argumentos passados;
  2. Remove o primeiro argumento, pois ele é o caminho do executável;
  3. Inicializa uma string dinâmica, chamada output;
  4. Itera sobre os argumentos adicionando cada argumento com o formato "argumento-tal " (argumento + espaço) ao final da string output;
  5. Imprime a string output.

Agora na versão 0.1.2, o código está assim:

use std::collections::VecDeque;
use std::env;
use std::process::exit;

fn main() {
    let mut args: VecDeque<String> = env::args().collect();

    args.pop_front();

    let is_help_needed = (args.len() == 1 && args[0] == "-h")
        || (args.len() == 1 && args[0] == "--help");
    if is_help_needed {
        println!("Example: eco-rs Bom dia!");
        exit(0);
    }

    let mut output = String::new();

    for arg in args {
        output.push_str(&format!("{arg} "));
    }

    println!("{}", output.trim());
}

Alterações:

  1. Na linha args.remove(0) havia possibilidade de otimização e mesmo sem conhecer VecDeque, que deve ser um vetor bidirecional (entrada e saída por ambos os lados), a fiz. Essa estrutura de dados possibilita a saída de um elemento do início sem precisar mover os elementos posteriores para trás, o que é vantajoso aqui.
  2. Adicionei um menu de ajuda, que tá bem vazio por sinal.

A distribuição está sendo feito pelo crates.io em eco-rs. Infelizmente a crate "eco" já existia. O legal é que ao executar o comando cargo install eco-rs o Cargo pega a crate e compila na arquitetura do seu computador. Com isso, eu não preciso compilar para todas as arquiteturas disponíveis e manter as releases no GitHub também (deveria, quem não gosta de só baixar o executável e pronto?).

Conclusão

tl;dr: o projeto eco é, atualmente, apenas uma implementação em Rust do programa echo que pega os argumentos de entrada, junta eles em uma string e imprime a string final na saída do terminal.

Pretendo fazer mais umas funcionalidades como colorir palavras específicas e criar uma sintaxe de marcação para estilizar também.

Se quiser acompanhar o projeto, veja o repositório eco.

Você pode me achar no:

Obrigado por ler! ❤

3

Gostei demais da iniciativa!

Porém meu instinto otimizador chamou muito a atenção nesse método: .collect();.

O Problema

Lendo a documentação do Args pode se ler o seguinte:

An iterator over the arguments of a process, yielding a String value for each argument.

E na documentação do método:

Transforms an iterator into a collection.

Um Iterador permite você percorrer do início ao final, sem poder acessar os elementos intermediários aleatóriamente. Que é exatamente a operação que você está fazendo.

A única vantagem de ter uma coleção (não todas) é justamente poder acessar qualquer elemento em qualquer ordem, o que você não está fazendo em nenhum momento.

O maior problema explicado:

O que acontece se você usar o seu comando com um arquivo de log de 1GB? Para cada palavra o método collect vai inserir em uma nova coleção, aumentando significativamente o uso de memória, sem falar na performance, para criar uma coleção pesadíssima sem toda essa necessidade.

Recomendo usar o iterator diretamente pelo método next()

Segunda evolução:

O que acontece se você usar o seu comando com um arquivo de log de 1GB?

rodando no terminal echo teste.txt todo o conteúdo desse arquivo é jogado na tela. Numa segunda versão seria interessante implementar essa funcionalidade

1

Muito obrigado pelo comentário! Obrigado também pelo apontamento. Sou novo no Rust e não conheço bem esses iterators, mas vou ficar ligado agora.

Não entendi bem como eu usaria o next() infinitamente, entretanto consegui remover o collect() e manter o funcionamento esperado.

Sobre a segunda evolução: pelo que pesquisei, o programa echo não lê o stdin e por isso não tem como passar arquivos para ele (a não ser que converta para string e passe como argumento). Também achei interessante, todavia deixaria para uma futura implementação do cat teste.txt, por exemplo.

1

Sou novo no Rust e não conheço bem esses iterators, mas vou ficar ligado agora.

Em Rust não sei como eles funcionam, mas já sofri muito com eles em C++, acredito que seja semelhante.

Não entendi bem como eu usaria o next() infinitamente

o next() sempre retorna o próximo elemento. um pseudocódigo ficaria assim:

fn main() {
    let mut args: Args = env::args();

    args.next(); // para remover o primeiro elemento
    
    let mut arg: String = args.next();

    let is_help_needed = arg == null || arg == "-h" || arg == "--help"
    if is_help_needed {
        println!("Example: eco-rs Bom dia!");
        exit(0);
    }

    do {
        output.push_str(&format!("{arg} "));
    while(arg = args.next())
        
    println!("{}", output.trim());
}

PS: não entendo de rust, o código pode estar incorreto, mas a intenção é essa.

PS2: não sei se no rust essa solução teria um ganho de desempenho, teria que fazer um teste de benchmarking. Estou questionando para você verificar isso.

Sobre a segunda evolução: pelo que pesquisei, o programa echo não lê o stdin e por isso não tem como passar arquivos para ele

A minha sugestão seria algo como:

if (args.len == 1 && fileExists(args[0])){
    printFile(args)
} else {
    printArgs(args)
}
4
2

Muito bacana como o rust "incentiva" esse tipo de implementação. Hoje em dia é comum vermos aplicativos de terminal reimplementados em rust e fazer isso para aprender é bem massa.

Uma sugestão que quero dar é utilizar o crate clap, que é o mais popular para se criar programas de terminal. Ele facilita a interpretação dos arqumentos e te dá de graça um menu de ajuda pro seu programa igual outros conhecidos, além de facilitar a colorização. Já utilizei em alguns projetos e deixa o produto final com uma cara bem profissional.

1

Bem isso! Obrigado pela sugestão, vou usar o clap em um projeto mais elaborado que pensei. Optei por não usar um parser/cli helper justamente para utilizar iterações, condicionais e o que mais fosse necessário manualmente.