Introduction
If you don't mind the fact that it is not void safe, Eiffel-Loop has a useful implementation of the "split the root" design pattern discussed in Prof. Meyer's technology blog. To which I have added a comment.
What is interesting about the Eiffel-Loop implementation is it offers an implicit way of mapping command line arguments to the make routine arguments of a core application object implementing EL_COMMAND, so you don't need to query the command line arguments directly. This is achieved via class EL_COMMAND_LINE_SUB_APPLICATION which takes a generic argument conforming to EL_COMMAND. The latter corresponds to the "CORE" class in Prof. Meyer's article.
Splitting the Root
Eiffel-Loop supports the philosophy that it's more useful (and convenient) to have a command line application that does a number of related (or unrelated things). A typical example is the the Eiffel-Loop toolkit application el_toolkit. Each of these sub-applications is reachable by a command line switch defined by {EL_SUB_APPLICATION}.option_name which defaults to generator.as_lower unless you redefine it with a short name.
class
APPLICATION_ROOT
inherit
EL_MULTI_APPLICATION_ROOT [BUILD_INFO]
create
make
feature {NONE} -- Implementation
Application_types: ARRAY [TYPE [EL_SUB_APPLICATION]]
--
once
Result := <<
{AUTOTEST_DEVELOPMENT_APP},
{UNDATED_PHOTOS_APP},
{CRYPTO_APP},
{FILTER_INVALID_UTF_8_APP},
{FTP_BACKUP_APP}, -- uses ftp (depends eposix)
{HTML_BODY_WORD_COUNTER_APP},
{JOBSERVE_SEARCH_APP},
{PRAAT_GCC_SOURCE_TO_MSVC_CONVERTOR_APP},
{PYXIS_ENCRYPTER_APP},
{PYXIS_TO_XML_APP},
{PYXIS_TREE_TO_XML_COMPILER_APP},
{LOCALIZATION_COMMAND_SHELL_APP},
{PYXIS_TRANSLATION_TREE_COMPILER_APP},
{THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP},
{THUNDERBIRD_WWW_EXPORTER_APP},
{VCF_CONTACT_SPLITTER_APP},
{VCF_CONTACT_NAME_SWITCHER_APP},
{XML_TO_PYXIS_APP},
{YOUTUBE_HD_DOWNLOAD_APP}
>>
end
end
Command Argument Mapping
The class EL_COMMAND_LINE_SUB_APPLICATION (a descendant of EL_SUB_APPLICATION) takes a generic parameter conforming to class EL_COMMAND. By implementing the function default_make: PROCEDURE you can implicitly map command line argument to the arguments for the make routine for the class implementing EL_COMMAND. Take for example the following code fragment for class THUNDERBIRD_LOCALIZED_HTML_EXPORTER which has a make routine taking 5 arguments:
note
description: "Export Thunderbird email client HTML as XHTML for selected folders"
class
THUNDERBIRD_LOCALIZED_HTML_EXPORTER
inherit
THUNDERBIRD_EXPORTER
rename
make as make_exporter
end
EL_COMMAND
create
make
feature {EL_SUB_APPLICATION} -- Initialization
make (
a_account_name: ZSTRING; a_export_path, thunderbird_home_dir: EL_DIR_PATH
a_is_xhtml: BOOLEAN; a_included_folders: like included_folders
)
do
make_exporter (a_account_name, a_export_path, thunderbird_home_dir)
is_xhtml := a_is_xhtml; included_folders := a_included_folders
included_folders.compare_objects
end
feature {NONE} -- Internal attributes
is_xhtml: BOOLEAN
included_folders: EL_ZSTRING_LIST
-- .sbd folders
end
These make arguments are mapped to command line arguments in the class THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP as shown below. The routine default_make provides default arguments for {THUNDERBIRD_LOCALIZED_HTML_EXPORTER}.make which are over-ridden by command line arguments defined by the function argument_specs. The class EL_REGRESSION_TESTABLE_COMMAND_LINE_SUB_APPLICATION is just a variant of class EL_COMMAND_LINE_SUB_APPLICATION that provides some regressions testing capabilities during development and is triggered by the command line switch -test. The various descriptions found in argument_specs and Description are for the benefit of the "quick help" mode invoked by the command switch -help.
class
THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP
inherit
EL_REGRESSION_TESTABLE_COMMAND_LINE_SUB_APPLICATION [THUNDERBIRD_LOCALIZED_HTML_EXPORTER]
redefine
Option_name
end
create
make
feature -- Test
test_run
--
do
-- Test.do_file_tree_test (".thunderbird", agent test_xhtml_export ("pop.myching.co", ?), 2477712861)
-- Test.do_file_tree_test (".thunderbird", agent test_xhtml_export ("small.myching.co", ?), 4123295270)
Test.do_file_tree_test (".thunderbird", agent test_html_body_export ("pop.myching.co", ?), 2383008038)
-- Test.do_file_tree_test (".thunderbird", agent test_html_body_export ("small.myching.co", ?), 4015841579)
end
test_xhtml_export (account: ZSTRING; a_dir_path: EL_DIR_PATH)
--
do
create command.make (
account, a_dir_path.joined_dir_path ("export"), a_dir_path.parent, True, Empty_inluded_sbd_dirs
)
normal_run
end
test_html_body_export (account: ZSTRING; a_dir_path: EL_DIR_PATH)
--
local
en_file_path: EL_FILE_PATH; en_text, subject_line: STRING; en_out: PLAIN_TEXT_FILE
pos_subject: INTEGER
do
create command.make (
account, a_dir_path.joined_dir_path ("export"), a_dir_path.parent, False, Empty_inluded_sbd_dirs
)
normal_run
-- Change name of "Home" to "Home Page"
en_file_path := a_dir_path + "21h18lg7.default/Mail/pop.myching.co/Product Tour.sbd/en"
en_text := File_system.plain_text (en_file_path)
subject_line := "Subject: Home"
pos_subject := en_text.substring_index (subject_line, 1)
if pos_subject > 0 then
en_text.replace_substring (subject_line + " Page", pos_subject, pos_subject + subject_line.count - 1)
end
create en_out.make_open_write (en_file_path)
en_out.put_string (en_text)
en_out.close
normal_run
end
feature {NONE} -- Implementation
argument_specs: ARRAY [like specs.item]
do
Result := <<
required_argument ("account", "Thunderbird account name"),
required_argument ("output", "Output directory path"),
optional_argument ("thunderbird_home", "Location of .thunderbird"),
optional_argument ("as_xhtml", "Export as xhtml"),
optional_argument ("folders", "Folders to include")
>>
end
default_make: PROCEDURE
do
Result := agent {like command}.make ("", "", Directory.Home, False, create {EL_ZSTRING_LIST}.make (7))
end
feature {NONE} -- Constants
Empty_inluded_sbd_dirs: EL_ZSTRING_LIST
once
create Result.make (0)
end
Option_name: STRING = "export_thunderbird"
Description: STRING = "Export multi-lingual HTML content from Thunderbird"
Log_filter: ARRAY [like CLASS_ROUTINES]
--
do
Result := <<
[{THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP}, All_routines],
[{THUNDERBIRD_LOCALIZED_HTML_EXPORTER}, All_routines],
[{THUNDERBIRD_EXPORT_AS_XHTML}, All_routines],
[{THUNDERBIRD_EXPORT_AS_XHTML_BODY}, All_routines]
>>
end
end
The full range of make arguments which can be mapped to command line arguments is defined by the following hierarchy of agent operand setter classes:
EL_MAKE_OPERAND_SETTER* [G]
EL_BOOLEAN_OPERAND_SETTER
EL_ZSTRING_OPERAND_SETTER
EL_STRING_8_OPERAND_SETTER
EL_STRING_32_OPERAND_SETTER
EL_ZSTRING_TABLE_OPERAND_SETTER
EL_INTEGER_OPERAND_SETTER
EL_ENVIRON_VARIABLE_OPERAND_SETTER [E -> EL_ENVIRON_VARIABLE create make_from_string end]
EL_NATURAL_OPERAND_SETTER
EL_INTEGER_64_OPERAND_SETTER
EL_NATURAL_64_OPERAND_SETTER
EL_REAL_OPERAND_SETTER
EL_DOUBLE_OPERAND_SETTER
EL_PATH_OPERAND_SETTER* [G -> EL_PATH]
EL_FILE_PATH_OPERAND_SETTER
EL_BUILDABLE_FROM_FILE_OPERAND_SETTER
EL_DIR_PATH_OPERAND_SETTER
To see how these classes are used see class EL_COMMAND_ARGUMENT. The once table constant Setter_types defines a mapping between a make argument and it's command line setter. Looking at the routine set_operand you will notice that you can also map chains of these basic types to a single command line argument.
elseif attached {CHAIN [ANY]} operand as list then
if list.generating_type.generic_parameter_count = 1 then
Setter_types.search (list.generating_type.generic_parameter_type (1))
You can for example have an argument number: ARRAYED_LIST [INTEGER]. This will be mapped to a command line argument because it conforms to the type CHAIN [INTEGER].
Argument Validation
A system of agent based argument validation is also supported by using either of the functions valid_required_argument or valid_optional_argument when implementing the function argument_specs. Here is an example from class PYXIS_TO_XML_APP. Note that because the final argument to valid_required_argument is an array, you can add as many validators as you wish.
feature {NONE} -- Implementation
argument_specs: ARRAY [like specs.item]
do
Result := <<
valid_required_argument ("in", "Input file path", << file_must_exist >>),
optional_argument ("out", "Output file path")
>>
end
file_must_exist is an "out of the box" validator, which can serve as an example for defining your own. Here is the complete list of "out of the box" validators:
feature {NONE} -- Validations
always_valid: TUPLE [key: READABLE_STRING_GENERAL; value: PREDICATE]
do
Result := ["Always true", agent: BOOLEAN do Result := True end]
end
file_must_exist: like always_valid
do
Result := [
"The file must exist", agent (path: EL_FILE_PATH): BOOLEAN
do
Result := not path.is_empty implies path.exists
end
]
end
directory_must_exist: like always_valid
do
Result := [
"The directory must exist", agent (path: EL_DIR_PATH): BOOLEAN
do
Result := not path.is_empty implies path.exists
end
]
end
If the validation fails then the text description is displayed with a helpful error message for the bad argument.