Erlang basics · Lesson 30 (open contents)
01 · Open erl02 · Meet common terms03 · Tell two kinds of text apart04 · Bind a variable once05 · Split and filter a list06 · Parse a number safely07 · Choose one path with case08 · Let a function keep going09 · Put code in a module10 · Compare terms and Boolean results11 · Read and update nested maps12 · Build and inspect UTF-8 binaries13 · Let clauses choose the function path14 · Solve a list twice15 · Choose with patterns, guards, and errors16 · Describe a module and one record17 · Put one tested module in Rebar318 · Return success or failure as data19 · Read and write one file safely20 · Build a data pipeline with functions21 · Keep records behind a module boundary22 · Define a module contract23 · Describe the data contract24 · Test small rules and whole flows25 · Shape builds and make a command26 · Write the project promise27 · Parse one real line28 · Keep the public API small29 · Make the promise executable30 · Package the edge and leave clues31 · Prove the project is done
ERLANG · PROJECT · LESSON 3035 minutes

Package the edge and leave clues

Keep `main/1` thin, log safe context, and build an escript.

01 · Start with the whole example

Run this first

Run it once, change one input, and compare the new result.

Erlang
%% src/cleaner_cli.erl
-module(cleaner_cli).
-export([main/1]).

main(Args) ->
  case run(Args) of
    ok -> ok;
    {error, _Reason} -> halt(1)
  end.

run([Path]) ->
  case file:read_file(Path) of
    {ok, Text} ->
      Lines = binary:split(Text, <<"\n">>, [global, trim]),
      finish(Path, [cleaner:parse_line(Line) || Line <- Lines]);
    {error, Reason} ->
      fail(Reason)
  end;
run(_Args) ->
  fail(usage).

finish(Path, Results) ->
  case [Reason || {error, Reason} <- Results] of
    [] ->
      Records = [Record || {ok, Record} <- Results],
      {ok, Counts} = cleaner:run(Records),
      logger:info("cleaned path=~ts count=~B", [Path, length(Records)]),
      io:format("levels=~tp~n", [Counts]),
      ok;
    [Reason | _] ->
      fail(Reason)
  end.

fail(Reason) ->
  logger:error("clean_failed reason=~tp", [Reason]),
  {error, Reason}.
Check the result first
  • The escript reads one file, calls cleaner, logs the accepted count, prints level totals, and exits with status 1 on failure.
02 · Take the code apart

Read from the first line down

  1. Read logger:info/2: Write a formatted operational message.
  2. Read main([Path]): Accept exactly one file argument.
  3. Read rebar3 escriptize: Package the command after tests pass.
03 · Meet the new symbols

Symbols are not secret signs

logger:info/2

Write a formatted operational message.

main([Path])

Accept exactly one file argument.

rebar3 escriptize

Package the command after tests pass.

04 · Ideas inside the code

Match each name to its meaning

01

thin entry point

A CLI entry point parses arguments, calls the API, prints a result, and does little else.

02

structured context

Logger metadata keeps fields such as path and count separate from the message.

03

privacy boundary

Operational clues should not expose full user data or secrets.

05 · Make the idea clear

Why these forms are useful

Run the example first. Predict one result, then change one input and run it again.

A CLI entry point parses arguments, calls the API, prints a result, and does little else.

Logger metadata keeps fields such as path and count separate from the message.

06 · Change it yourself

Close the answer and try

Return a usage error when no path is supplied.

Practice starting point
main([]) -> ____.

Target result: Return {error,usage}.

Stuck? Read one hint

Keep the error tagged.

After you run it, see one answer
One answer
main([]) -> {error, usage}.
Think about it: Should logs copy every source line?

No. Record enough context to diagnose a run without exposing the full input.

Take with you

Remember these three lines

  1. 1

    Keep CLI code thin.

  2. 2

    Log decisions and counts.

  3. 3

    Run tests before packaging.

Lesson completeRun the code once, then mark this lesson as done.