Files
mercury/samples/mcowsay.m
Julien Fischer 4f9ecf65b1 Use io.write_prefixed_lines in more places.
profiler/options.m:
    Use io.write_prefixed_lines for writing the usage message.

    Add -? as a synonym for --help.

samples/diff/options.m:
samples/mcowsay.m:
    Use io.write_prefixed_lines for writing the usage messages.
2023-09-03 23:28:29 +10:00

532 lines
18 KiB
Mathematica

%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%---------------------------------------------------------------------------%
%
% File: mcowsay.m.
% Author: juliensf.
%
% A Mercury version of the cowsay program originally written by Tony Monroe.
% It prints an ASCII art picture of a cow saying/thinking a user-supplied
% message.
%
% Implements most of the functionality of the original cowsay aside from the
% ability to use .cow files (-f option and COWPATH environment variable) and
% the ability to list .cow files in the COWPATH (-l option). Implementing
% those is left as an exercise for the reader.
%
% This source file is hereby placed in the public domain.
%
%---------------------------------------------------------------------------%
:- module mcowsay.
:- interface.
:- import_module io.
:- pred main(io::di, io::uo) is det.
%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%
:- implementation.
:- import_module bool.
:- import_module char.
:- import_module cord.
:- import_module getopt.
:- import_module int.
:- import_module list.
:- import_module map.
:- import_module stream.
:- import_module string.
:- import_module require.
%---------------------------------------------------------------------------%
main(!IO) :-
io.command_line_arguments(Args, !IO),
OptionOps = option_ops_multi(
short_option,
long_option,
option_default,
special_option_handler
),
getopt.process_options(OptionOps, Args, NonOptionArgs, OptionResult),
(
OptionResult = ok(OptionTable),
( if getopt.lookup_bool_option(OptionTable, help, yes) then
print_help_message(!IO)
else if getopt.lookup_bool_option(OptionTable, version, yes) then
print_version_message(!IO)
else
(
NonOptionArgs = [],
read_and_print_message_from_stdin(OptionTable, !IO)
;
NonOptionArgs = [Message],
% We replicate the behaviour of the original cowsay here.
% If the message is empty, then read from standard input.
( if Message = "" then
read_and_print_message_from_stdin(OptionTable, !IO)
else
read_and_print_message_from_arg(OptionTable, Message, !IO)
)
;
NonOptionArgs = [_, _ | _],
print_usage_error(!IO)
)
)
;
OptionResult = error(Error),
print_option_error(Error, !IO)
).
%---------------------------------------------------------------------------%
:- pred read_and_print_message_from_stdin(option_table(option)::in,
io::di, io::uo) is det.
read_and_print_message_from_stdin(OptionTable, !IO) :-
io.stdin_stream(Stdin, !IO),
getopt.lookup_bool_option(OptionTable, word_wrap, WordWrap),
(
WordWrap = no,
stream.input_stream_fold(Stdin, add_line, cord.empty, Result, !IO),
(
Result = ok(Lines),
print_cow_and_message(OptionTable, Lines, !IO)
;
Result = error(_, IO_Error),
print_io_error(IO_Error, !IO)
)
;
WordWrap = yes,
io.read_file_as_string(Stdin, Result, !IO),
(
Result = ok(Message),
Lines = wrap_message(OptionTable, Message),
print_cow_and_message(OptionTable, Lines, !IO)
;
Result = error(_, IO_Error),
print_io_error(IO_Error, !IO)
)
).
:- pred add_line(line::in, cord(string)::in, cord(string)::out) is det.
add_line(Line, !Lines) :-
Line = line(String),
ExpandedString = expand_tabs(String),
cord.snoc(string.chomp(ExpandedString), !Lines).
%---------------------------------------------------------------------------%
:- pred read_and_print_message_from_arg(option_table(option)::in,
string::in, io::di, io::uo) is det.
read_and_print_message_from_arg(OptionTable, Message, !IO) :-
getopt.lookup_bool_option(OptionTable, word_wrap, WordWrap),
(
WordWrap = no,
ExpandedMessage = expand_tabs(Message),
LineList = string.split_into_lines(ExpandedMessage),
Lines = cord.from_list(LineList)
;
WordWrap = yes,
Lines = wrap_message(OptionTable, Message)
),
print_cow_and_message(OptionTable, Lines, !IO).
%---------------------------------------------------------------------------%
:- func wrap_message(option_table(option), string) = cord(string).
wrap_message(OptionTable, Message) = Lines :-
getopt.lookup_int_option(OptionTable, wrap_width, WrapWidth),
WrappedMessage = string.word_wrap(Message, WrapWidth),
LineList = string.split_into_lines(WrappedMessage),
Lines = cord.from_list(LineList).
:- func expand_tabs(string) = string.
expand_tabs(String) = string.replace_all(String, "\t", " ").
%---------------------------------------------------------------------------%
:- type cow_action
---> speaking
; thinking.
:- pred get_cow_action(cow_action::out, io::di, io::uo) is det.
get_cow_action(Action, !IO) :-
io.progname_base("mcowsay", ProgName, !IO),
( if (ProgName = "cowthink" ; ProgName = "mcowthink") then
Action = thinking
else
Action = speaking
).
:- func thoughts_string(cow_action) = string.
thoughts_string(speaking) = "\\".
thoughts_string(thinking) = "o".
%---------------------------------------------------------------------------%
:- pred print_cow_and_message(option_table(option)::in, cord(string)::in,
io::di, io::uo) is det.
print_cow_and_message(OptionTable, Lines, !IO) :-
get_cow_action(Action, !IO),
MaxLineWidth = max_line_width(Lines),
print_message_bubble(Action, MaxLineWidth, Lines, !IO),
getopt.lookup_string_option(OptionTable, eyes_string, EyesString),
getopt.lookup_string_option(OptionTable, tongue_string, TongueString),
ThoughtsString = thoughts_string(Action),
io.write_string(cow(EyesString, TongueString, ThoughtsString), !IO).
:- func max_line_width(cord(string)) = int.
max_line_width(Lines) = MaxWidth :-
cord.foldl_pred(acc_line_width, Lines, 0, MaxWidth).
:- pred acc_line_width(string::in, int::in, int::out) is det.
acc_line_width(Line, !MaxWidth) :-
string.count_code_points(Line, LineWidth),
( if LineWidth > !.MaxWidth then
!:MaxWidth = LineWidth
else
true
).
:- pred print_message_bubble(cow_action::in, int::in, cord(string)::in,
io::di, io::uo) is det.
print_message_bubble(Action, MaxLineWidth, Lines, !IO) :-
io.print_line(top_bubble_border(MaxLineWidth), !IO),
( if cord.is_empty(Lines) then
print_single_line_bubble(Action, "", !IO)
else if cord.is_singleton(Lines, FirstLine) then
print_single_line_bubble(Action, FirstLine, !IO)
else
NumLines = cord.length(Lines),
(
Action = speaking,
cord.foldl2(print_speech_bubble_line(MaxLineWidth, NumLines),
Lines, 1, _, !IO)
;
Action = thinking,
cord.foldl_pred(print_thought_bubble_line(MaxLineWidth), Lines,
!IO)
)
),
io.print_line(bottom_bubble_border(MaxLineWidth), !IO).
:- pred print_single_line_bubble(cow_action::in, string::in,
io::di, io::uo) is det.
print_single_line_bubble(Action, Line, !IO) :-
(
Action = speaking,
io.format("< %s >\n", [s(Line)], !IO)
;
Action = thinking,
io.format("( %s )\n", [s(Line)], !IO)
).
:- pred print_speech_bubble_line(int::in, int::in, string::in,
int::in, int::out, io::di, io::uo) is det.
print_speech_bubble_line(MaxLineWidth, NumLines, Line, !LineCount, !IO) :-
( if !.LineCount = 1 then
Prefix = "/", Suffix = "\\"
else if !.LineCount < NumLines then
Prefix = "|", Suffix = "|"
else
Prefix = "\\", Suffix = "/"
),
NLine = normalize_line(MaxLineWidth, Line),
io.format("%s %s %s\n", [s(Prefix), s(NLine), s(Suffix)], !IO),
!:LineCount = !.LineCount + 1.
:- pred print_thought_bubble_line(int::in, string::in, io::di, io::uo) is det.
print_thought_bubble_line(MaxLineWidth, Line, !IO) :-
NLine = normalize_line(MaxLineWidth, Line),
io.format("( %s )\n", [s(NLine)], !IO).
:- func top_bubble_border(int) = string.
top_bubble_border(MaxLineWidth) =
" " ++ string.duplicate_char('_', MaxLineWidth + 2).
:- func bottom_bubble_border(int) = string.
bottom_bubble_border(MaxLineWidth) =
" " ++ string.duplicate_char('-', MaxLineWidth + 2).
:- func normalize_line(int, string) = string.
normalize_line(MaxLineWidth, Line) = NormalLine :-
string.count_code_points(Line, LineWidth),
NormalLine = Line ++ string.duplicate_char(' ', MaxLineWidth - LineWidth).
%---------------------------------------------------------------------------%
:- func cow(string, string, string) = string.
cow(Eyes, Tongue, Thoughts) = string.append_list([
" ", Thoughts, " ^__^\n",
" ", Thoughts, " (", Eyes, ")\\_______\n",
" (__)\\ )\\/\\\n",
" ", Tongue, " ||----w |\n",
" || ||\n"
]).
%---------------------------------------------------------------------------%
:- type option
---> help
; version
% Options to control word wrapping.
; user_wrap_width
; no_format
% Options to control the cow mode.
; borg_mode
; dead_mode
; greedy_mode
; paranoid_mode
; stoned_mode
; tired_mode
; wired_mode
; youthful_mode
; user_eyes
; user_tongue
% Internal options.
; word_wrap
; wrap_width
; eyes_string
; tongue_string.
:- pred short_option(char::in, option::out) is semidet.
short_option('h', help).
short_option('n', no_format).
short_option('b', borg_mode).
short_option('d', dead_mode).
short_option('g', greedy_mode).
short_option('p', paranoid_mode).
short_option('s', stoned_mode).
short_option('t', tired_mode).
short_option('w', wired_mode).
short_option('y', youthful_mode).
short_option('e', user_eyes).
short_option('T', user_tongue).
short_option('W', user_wrap_width).
:- pred long_option(string::in, option::out) is semidet.
long_option("help", help).
long_option("version", version).
long_option("no-wrap", no_format).
long_option("borg", borg_mode).
long_option("dead", dead_mode).
long_option("greedy", greedy_mode).
long_option("stoned", stoned_mode).
long_option("tired", tired_mode).
long_option("wired", wired_mode).
long_option("youthful", youthful_mode).
long_option("eyes", user_eyes).
long_option("tongue", user_tongue).
long_option("width", user_wrap_width).
:- pred option_default(option::out, option_data::out) is multi.
option_default(help, bool(no)).
option_default(version, bool(no)).
option_default(no_format, special).
option_default(borg_mode, special).
option_default(dead_mode, special).
option_default(greedy_mode, special).
option_default(paranoid_mode, special).
option_default(stoned_mode, special).
option_default(tired_mode, special).
option_default(wired_mode, special).
option_default(youthful_mode, special).
option_default(user_eyes, string_special).
option_default(user_tongue, string_special).
option_default(eyes_string, string("oo")).
option_default(tongue_string, string(" ")).
option_default(word_wrap, bool(yes)).
option_default(user_wrap_width, int_special).
option_default(wrap_width, int(40)).
:- pred special_option_handler(option::in, special_data::in,
option_table(option)::in, maybe_option_table(option)::out) is semidet.
special_option_handler(no_format, none, !.OptionTable, Result) :-
map.set(word_wrap, bool(no), !OptionTable),
Result = ok(!.OptionTable).
special_option_handler(borg_mode, none, OptionTable, Result) :-
set_cow_mode("==", " ", OptionTable, Result).
special_option_handler(dead_mode, none, OptionTable, Result) :-
set_cow_mode("XX", "U ", OptionTable, Result).
special_option_handler(greedy_mode, none, OptionTable, Result) :-
set_cow_mode("$$", " ", OptionTable, Result).
special_option_handler(paranoid_mode, none, OptionTable, Result) :-
set_cow_mode("@@", " ", OptionTable, Result).
special_option_handler(stoned_mode, none, OptionTable, Result) :-
set_cow_mode("**", "U ", OptionTable, Result).
special_option_handler(tired_mode, none, OptionTable, Result) :-
set_cow_mode("--", " ", OptionTable, Result).
special_option_handler(wired_mode, none, OptionTable, Result) :-
set_cow_mode("OO", " ", OptionTable, Result).
special_option_handler(youthful_mode, none, OptionTable, Result) :-
set_cow_mode("..", " ", OptionTable, Result).
special_option_handler(user_eyes, string(UserEyes), !.OptionTable,
Result) :-
string.count_code_points(UserEyes, NumEyeCodePoints),
( if NumEyeCodePoints < 1 then
Eyes = " "
else if NumEyeCodePoints = 1 then
Eyes = UserEyes ++ " "
else
Eyes = string.left_by_code_point(UserEyes, 2)
),
map.set(eyes_string, string(Eyes), !OptionTable),
Result = ok(!.OptionTable).
special_option_handler(user_tongue, string(UserTongue), !.OptionTable,
Result) :-
string.count_code_points(UserTongue, NumTongueCodePoints),
( if NumTongueCodePoints < 1 then
Tongue = " "
else if NumTongueCodePoints = 1 then
Tongue = UserTongue ++ " "
else
Tongue = string.left_by_code_point(UserTongue, 2)
),
map.set(tongue_string, string(Tongue), !OptionTable),
Result = ok(!.OptionTable).
special_option_handler(user_wrap_width, int(WrapCol), !.OptionTable,
Result) :-
( if WrapCol < 1 then
Result = error("the value of option --width must be greater than zero")
else
map.set(wrap_width, int(WrapCol), !OptionTable),
Result = ok(!.OptionTable)
).
:- pred set_cow_mode(string::in, string::in, option_table(option)::in,
maybe_option_table(option)::out) is det.
set_cow_mode(Eyes, Tongue, !.OptionTable, Result) :-
map.set(eyes_string, string(Eyes), !OptionTable),
map.set(tongue_string, string(Tongue), !OptionTable),
Result = ok(!.OptionTable).
%---------------------------------------------------------------------------%
:- pred print_help_message(io::di, io::uo) is det.
print_help_message(!IO) :-
io.write_strings([
"Name: mcowsay - a Mercury version of cowsay\n",
"\n",
"Usage: mcowsay [<options>] [<message>]\n",
"\n",
"Description:\n"
], !IO),
io.write_prefixed_lines("\t", [
"Prints an ASCII art picture of a cow saying a message provided",
"by the user. If the message is not provided as an argument on",
"the command line, then it will be read from the standard input."
], !IO),
io.nl(!IO),
io.write_prefixed_lines("\t", [
"Any tab characters in the message will be replaced in the output",
"by a sequence of four space characters."
], !IO),
io.nl(!IO),
io.write_prefixed_lines("\t", [
"If the program is invoked as 'mcowthink' or 'cowthink' then the",
"cow will think its message instead of saying it."
], !IO),
io.nl(!IO),
io.write_string("Options:\n", !IO),
io.write_prefixed_lines("\t", [
"-h, --help",
"\tPrint this information and exit.",
"--version",
"\tPrint version information and exit.",
"-n, --no-wrap",
"\tDo not wrap lines.",
"-W <wrap-col>, --width <wrap-col>",
"\tSpecify the column at which to wrap words.",
"\t<wrap-col> must be greater than zero and defaults to 40.",
"-b, --borg",
"\t\"Borg mode\", uses == for the cow's eyes.",
"-d, --dead",
"\t\"Dead mode\", uses XX for the cow's eyes and U for its tongue.",
"-g, --greedy",
"\t\"Greedy mode\", uses $$ for the cow's eyes.",
"-p, --paranoid",
"\t\"Paranoid mode\", uses @@ for the cow's eyes.",
"-s, --stoned",
"\t\"Stoned mode\", uses ** for the cow's eyes and U for its tongue.",
"-t, --tired",
"\t\"Tired mode\", uses -- for the cow's eyes.",
"-w, --wired",
"\t\"Wired mode\", uses OO for the cow's eyes.",
"-y, --youthful",
"\t\"Youthful mode\", uses .. for the cow's eyes.",
"-e <eye-string>, --eyes <eye-string>",
"\tSpecifies the cow's eye type. Only the first two characters of",
"\t<eye-string> are used.",
"-T <tongue-string>, --tongue <tongue-string>",
"\tSpecifies the cow's tongue shape. Only the first two characters of",
"\t<tongue-string> are used."
], !IO).
:- pred print_version_message(io::di, io::uo) is det.
print_version_message(!IO) :-
io.write_string("Mercury cowsay version 1.0\n", !IO).
%---------------------------------------------------------------------------%
:- pred print_option_error(option_error(option)::in, io::di, io::uo) is det.
print_option_error(Error, !IO) :-
Msg = option_error_to_string(Error),
io.stderr_stream(Stderr, !IO),
io.format(Stderr, "error: %s.\n", [s(Msg)], !IO),
io.set_exit_status(1, !IO).
:- pred print_io_error(io.error::in, io::di, io::uo) is det.
print_io_error(IO_Error, !IO) :-
io.error_message(IO_Error, Msg),
io.stderr_stream(Stderr, !IO),
io.format(Stderr, "error: %s\n", [s(Msg)], !IO),
io.set_exit_status(1, !IO).
:- pred print_usage_error(io::di, io::uo) is det.
print_usage_error(!IO) :-
io.stderr_stream(Stderr, !IO),
io.print_line(Stderr, "Usage: mcowsay [<options>] [<message>]", !IO),
io.set_exit_status(1, !IO).
%---------------------------------------------------------------------------%
:- end_module mcowsay.
%---------------------------------------------------------------------------%