Pular para o conteúdo

Ferramenta de linha de comando

Analisar uma pasta e imprimir um relatório — o tipo de utilitário que se usa todo dia.

Este é o código completo do exercício 175_cli_arquivos.df, que roda e verifica a si mesmo.

175_cli_arquivos.df
adopt Arcane.IO as IO
adopt Arcane.Text as Text
adopt Arcane.Collections as Col
adopt Arcane.Time as Time

// ═══ MODELO ═══

record Arquivo:
    nome: String
    extensao: String
    bytes: Integer
    linhas: Integer

// ═══ ANALISE ═══

action analisar(caminho: String) -> Arquivo:
    conteudo := IO.read(caminho)
    yield Arquivo(
        IO.basename(caminho),
        IO.ext(caminho) ?? "(sem)",
        IO.size(caminho),
        len(conteudo.lines())
    )

action formatar_bytes(n: Number) -> String:
    given n smaller 1024:
        yield $"{n} B"
    orif n smaller 1048576:
        yield $"{round(n / 1024, 1)} KB"
    yield $"{round(n / 1048576, 2)} MB"

// ═══ PREPARO: uma pasta de exemplo ═══

pasta := "_projeto_175"
IO.mkdir(pasta)

exemplos := {
    "notas.txt": "linha 1\nlinha 2\nlinha 3",
    "config.json": "{\"tema\": \"escuro\", \"fonte\": 14}",
    "dados.csv": "nome,valor\nA,1\nB,2\nC,3\nD,4",
    "leiame.md": "# Titulo\n\nUm paragrafo.",
    "script.df": "action f():\n    yield 1\nout f()"
}
cycle nome in exemplos.keys():
    IO.write(IO.join(pasta, nome), exemplos[nome])

// ═══ RELATORIO ═══

arquivos := IO.list_dir(pasta) >> morph nome: analisar(IO.join(pasta, nome))

out Text.box("Analise de Arquivos")
out ""
out $"  {"ARQUIVO".pad_end(14)}{"EXT".pad_end(8)}{"TAMANHO".pad_start(10)}{"LINHAS".pad_start(8)}"
out $"  {"-".repeat(40)}"

cycle a in Col.sort_by_field(arquivos, "bytes", yes):
    out $"  {a.nome.pad_end(14)}{a.extensao.pad_end(8)}{formatar_bytes(a.bytes).pad_start(10)}{str(a.linhas).pad_start(8)}"

assert len(arquivos) is 5, "cinco arquivos"

// ── totais ──
total_bytes := arquivos >> morph a: a.bytes >> distill acc, b: acc + b 0
total_linhas := arquivos >> morph a: a.linhas >> distill acc, l: acc + l 0

out ""
out $"  total: {len(arquivos)} arquivos, {formatar_bytes(total_bytes)}, {total_linhas} linhas"
assert total_linhas is 15, "soma das linhas"

// ── por extensao ──
out ""
out "  ── por extensao ──"
por_ext := Col.group_by(arquivos, "extensao")
cycle ext in sorted(por_ext.keys()):
    grupo := por_ext[ext]
    soma := grupo >> morph a: a.bytes >> distill acc, b: acc + b 0
    barra := "#".repeat(max(1, soma * 20 ~/ max(total_bytes, 1)))
    out $"  {ext.pad_end(8)}{str(len(grupo)).pad_start(3)}  {barra} {formatar_bytes(soma)}"

assert len(por_ext.keys()) is 5, "cinco extensoes distintas"

// ── o maior e o menor ──
maior := Col.sort_by_field(arquivos, "bytes", yes)[0]
menor := Col.sort_by_field(arquivos, "bytes")[0]
out ""
out $"  maior: {maior.nome} ({formatar_bytes(maior.bytes)})"
out $"  menor: {menor.nome} ({formatar_bytes(menor.bytes)})"

// ── busca por conteudo ──
out ""
out "  ── procurando por 'linha' ──"
achados := []
cycle nome in IO.list_dir(pasta):
    caminho := IO.join(pasta, nome)
    conteudo := IO.read(caminho)
    numero := 0
    cycle l in conteudo.lines():
        numero += 1
        given "linha" in l.lower():
            achados.append($"{nome}:{numero}: {l.trim()}")

cycle a in achados:
    out $"    {a}"
assert len(achados) is 3, "tres ocorrencias"

// ═══ LIMPEZA ═══
cycle nome in IO.list_dir(pasta):
    IO.delete(IO.join(pasta, nome))
IO.delete(pasta)
out ""
out "  (pasta temporaria removida)"

O que ela faz#

  • Lê cada arquivo da pasta
  • Extrai nome, extensão, tamanho e contagem de linhas
  • Agrupa por extensão com um histograma
  • Encontra o maior e o menor
  • Busca um termo em todos os arquivos

Decisões que valem notar#

Modelo primeiro#

Converter os dados brutos num record logo na entrada faz o resto do programa trabalhar com a.bytes em vez de chamar IO.size repetidamente.

Formatação legível#

"1.4 KB" comunica; "1433" não. Numa ferramenta de terminal, esse detalhe é a diferença entre útil e irritante.

Colunas alinhadas#

pad_end para texto (alinha à esquerda), pad_start para número (à direita). É como toda tabela de terminal se lê melhor.

Proteção nas bordas#

max(1, soma * 20 ~/ max(total, 1)) — dois max evitam barra de comprimento zero e divisão por zero numa pasta vazia. Vale escrever mesmo quando "não vai acontecer".