
On 8/17/06, Oleg Abrosimov <beholder@gorodok.net> wrote:
Hello Julio,
First of all, I want to thank you and your mentor for the new great library. I've started to read it's docs from top to bottom and I have a few comments till now:
Hi Oleg,
1) The motivation part is too long. Process handling is a hot topic for most of us and there is no need to prove that your library is great and very useful. It was important when you made your proposal for SoC, but now your goal is to provide a very short start for your reader and potential user of your library. (Sorry for so long paragraph, hope it would be useful)
Noted.
2) why you don't abstract out things like EXIT_FAILURE/EXIT_SUCCESS? they are artifacts from C and looks ugly in C++ code. simple enum would be just enough.
I was going to do it but then reconsidered the decision because those constants are standard, aren't they? Same for STD{IN,OUT,ERR}_FILENO. I'm not really sure; maybe it is indeed a good idea to abstract all of them.
3) seems that you need to fix you quickbook code: // quickbook:begin(command-line) bp::command_line cl("svn"); cl.argument("update"); // quickbook:end(command-line)
What do you mean? I can't see the problem there.
4) command_line constructor is defined as command_line(const std::string & executable, const std::string & firstarg = "", const std::string & path = "");
docs says that executable The full path to the executable program. firstarg The program's name. If empty, the constructor sets it to be the executable's base name (i.e., skips the directory). path Set of directories where to look for the executable, if it does not include any path component.
the difference of first and second arguments are not clear and purpose of the second argument too. command_line class provides a member function called argument, so the name firstarg suggests that it is the first argument passed to the executable under question. Resolve this confusion, please.
OK.
5) the argument member function is non-intuitive. it is a noun but should be a verb like add_argument. The name proposed is even more verbose than current one, it leads to the next suggestion: use 'operator<<' to append arguments to command line object.
Agreed. I'd rather call it 'add'. And maybe also provide operator<<. Could the operator make things clearer? cl.add("foo").add("bar").add(4); cl << "foo" << "bar" << 4;
6) const arguments_vector & get_arguments(void ) const; is it really needed? in what scenario other then dump for debugging purposes?
No... I don't think it is needed.
what an invariant it tries to maintain? if there is no one, then it can be simply public: arguments_vector arguments;
But what if there is a need to maintain an invariant in the future? You couldn't do it with a public member without breaking backwards compatibility.
the one argument for member function against simple member is to ensure
I guess you mean "the one member function for argument"?
immutability (the immutable object pattern) but it is not true for arguments. they can be added. (but not removed oops!)
Yes they can be added, but then you want to make the attribute private, right? Thus breaking compatibility as mentioned above. (I'm not too fond on breaking the API but if it is common practice in Boost I don't have a problem.)
It seems that command_line tries to be both, path to executable and arguments to it. first can be made immutable and it can be proved to be a good thing (or can it?) but arguments are different. one needs to pass different arguments for the same executable in a case when it fails for previous attempt for example. May be it is worth to factor it out of command line and make command_line immutable, but arguments - mutable?
bp::command_line cl("svn"); cl.argument("update");
becomes
fs::path exec("svn"); bp::command_line_arguments args("update"); // supports up to N parameters to be passed in constructor + modifier/composition functions for reuse bp::command_line cl(exec, args);
Hmm... separating the two concepts sounds good. Maybe the executable can be completely separated from the command_line, without adding a command_line_arguments class. After all, the command line is an entity on its own that has nothing to do with the binary. I.e.: std::string exec("/my/program"); bp::command_line cl("a", "b", "c"); bp::launcher l; l.start(exec, cl);
7) const std::string & get_executable(void ) const; same questions are applied here + one more - did you considered to return boost::filesystem::path here? if yes, why it was rejected?
Yes, it was considered. It was rejected to avoid a dependency on the Boost Filesystem (binary) library because Boost.Process is completely header-based at the moment. If it goes binary, the decision shall be reconsidered. (One of the things that needs to go in the design decisions chapter...)
+ I believe that get_executable_path() would better reflect its purpose.
Not if it is separated from the class ;)
(Note, I would prefer executable_path public member)
8) static command_line shell(const std::string & command) ; from docs to this function I've realized that this function can not be used in portable way. The question is - why it is in a portable part of the library? it can be replaced by a pair of free factory functions in corresponding platform specific namespaces, like: command_line posix_shell_command(const std::string & command, const std::string & shell_name)
The function is portable in that there is no difference when using it from Windows or Unix. What is (generally) not portable is the string passed to it. E.g.: command_line c = command_line::shell("svn update"); could work equally well under Windows and Unix.
shell_name allows to pass command not only for sh, but any other platform specific shell, like python, for example.
No. The shell() constructor is supposed to provide an easy way to execute external commands without having to construct their command line argument by argument. It is very similar to the standard system(3) function which is restricted to the default system shell. Running, e.g., python is no different than executing any other application. E.g. if you want to execute "svn update *.cpp" you do not care about the interpreter used to parse this. (If you do, you must construct the command line on your own with the appropriate interpreter and its flags.) Summarizing: the basic purpose behind shell() is to ease command line construction. We thought it was a good idea because calling the 'argument' method can be cumbersome.
9) the launcher. It seems to be just a container for different process' attributes. (It's documentation shows only one function start. fix your quickbook please)
Don't know how yet. launcher inherits from the detail::launcher_base class, which is documented in the source code but not in the generate documents. Ideally, the launcher documentation in the manual could simply inherit that from its parent... Somewhat related to this, maybe it'd also be worth to make launcher_base a public class. It can be useful to people implementing a custom launcher. Not that matters any more if launcher becomes 'context' as you suggest below.
Same invariant issues applies here too. it has no invariant at all and can be a simple struct with all members are public and simply assignable without setter methods.
The current getters check for several sanity conditions (currently using assertions, but exceptions should be considered). Moving to plain attributes means that these cannot take place any more :(
bp::launcher l; l.set_stdout_behavior(bp::redirect_stream); l.set_merge_out_err(true); l.set_work_directory(dir);
would be
bp::launcher l; l.stdout_behavior = bp::redirect_stream; l.merge_out_err = true; l.work_directory = dir; //should be a fs::path object ?
Same as above. If Filesystem was used, this could certainly be a path object. And how could you convert this, which includes containers in the launcher and not just "plain" objects? bp::posix_launcher l; l.set_input_behavior(1, bp::silent_stream); l.set_input_behavior(2, bp::silent_stream); l.set_input_behavior(3, bp::silent_stream); Something like: bp::posix_launcher l; l.input_set.insert(bp::input_stream(1, bp::silent_stream)); l.input_set.insert(bp::input_stream(2, bp::silent_stream)); l.input_set.insert(bp::input_stream(3, bp::silent_stream)); or similar?
Here again two concepts are coupled: a collection of attributes or context and the action of launching the child process. if we decouple it we get the following:
bp::context cntx; cntx.stdout_behavior = bp::redirect_stream; cntx.merge_out_err = true; cntx.work_directory = dir; //should be a fs::path object ?
bp::child c = bp::launch(cntx, cl);
// was: bp::child c = l.start(cl); - the start member function looks like a strange artifact.
I like this idea. Very much in fact.
10) all relevant pieces should be parameterized against char_type and traits_type
"Relevant pieces" refers to any class including/dealing with strings?
11) docs says that child' constructor is protected, but in synopsis it is under public section...
Grr, blame BoostBook... I've seen that it has several problems formatting Doxygen, not only this one...
12) the following methods adds nothing over exposing underlying streams directly (overincapsulated again): postream & get_stdin(void) const; pistream & get_stdout(void) const; pistream & get_stderr(void) const;
Again, these do sanity checks like e.g. not allowing you to get a reference to stdin if it was not redirected by the launcher. (Yes, assertions could become exceptions here.) (With "sanity checks" I mean ensuring that the preconditions are met.)
+ they are strange because they are const, but returns non-const reference.
13) description for status class says: "This class represents the status returned by a child process after it has terminated. It contains many methods that may make no sense in some platforms, but this is done for simplicity. These methods are supported everywhere, although they may simply return fake values"
but it smells not very good. immediate question arises - do there alternatives exists? enumerate them and prove that the current solution is better.
I don't like it either, but have not payed much attention to it until recently (have been busy with other issues). I wrote a couple of days ago about this issue to my mentor with a possible solution: templatize the wait method on the return parameter so: bp::status s = c.wait<bp::status>(); bp::posix_status s = c.wait<bp::posix_status>();
I propose to make two platform-specific status classes say, posix_status and win32_status and their (set) intersection - status
then if one doesn't care about platform specific issues he writes: bp::status s = c.wait();
and if one is care about it, he writes: bp::posix_status s = c.wait(); //posix_status' constructor accepts status object. it is a simplified form of polimorphism.
I'm assuming that you'd store all the status on the 'status' class, and its children could only provide getters for additional information, right? If so, sounds better than the template approach because it's easier to use. The only "strange" situation that can arise is this: bp::status s = c.wait(); if (s.exited()) // OK, deal with exit status. else // Uh... something happened but we cannot know exactly what. Why? (I'm only afraid of people trying to use exit_status() when exited() returns false in this specific situation because they'd see that exited() never returns true under Windows...)
14) constructor status(int flags); is public. Why? Am I (as a user) allowed to construct it directly?
Oops, no. It should be private.
15) if (s.exited() && s.exit_status() == EXIT_SUCCESS) looks not so good ;-) see the issue (2). I would prefer if (s.exited() && s == status::exit_success)
or
if (s.exited() && s == bp::exit_success)
where exit_success is a const status object or object of any other special type (to avoid unsafe comparisons to int in case of using enum)
Indeed. See reply to 2 ;-)
16) one final comment: status object is already immutable. it can be enforced if child::wait() would returns const status object. after this you can eliminate most of member functions and replace em with plain members. it would simplify its usage.
See status' code. Again, getters do some sanity checks on the preconditions... About making the resulting object const, yes, sounds appropriate.
It is already too much for one letter. I'm stop here. Hope that my comments would help you to improve the library.
Of course they are. Thank you very much for your time and all the suggestions! -- Julio M. Merino Vidal <jmmv84@gmail.com> The Julipedia - http://julipedia.blogspot.com/