Merge lp:~jtv/maas/bug-1052876 into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 1355
Proposed branch: lp:~jtv/maas/bug-1052876
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 58 lines (+21/-5)
2 files modified
src/maascli/__init__.py (+8/-0)
src/maascli/tests/test_integration.py (+13/-5)
To merge this branch: bzr merge lp:~jtv/maas/bug-1052876
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+134915@code.launchpad.net

Commit message

Print more helpful error message when maas-cli is run without arguments.

Description of the change

As discussed very briefly on the daily call. I make the invocation fail with error code 2 because that's what it did previously.

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

How about sparing the user an extra step and print the 'usage' string directly instead of message asking the user to re-run the command with -h (same as what, say, 'nmap' does)?

Revision history for this message
Gavin Panella (allenap) wrote :

[1]

+    output_file = '/dev/null'

'/dev/null' is defined as os.devnull too. I doubt /dev/null will
change in the next 10 years, but it may be microscopically less
magic to use os.devnull.

[2]

+        try:
+            self.run_command()
+        except CalledProcessError:
+            pass

You're expecting the command to exit with 2, so the following might be
more appropriate:

        error = self.assertRaises(CalledProcessError, self.run_command)
        self.assertEqual(2, error.returncode)

In fact, if run_command() were changed, this test could be simplified
further without affecting other tests:

    from subprocess import check_output, STDOUT

    def run_command(self, *args):
         return check_output(
             [locate_maascli()] + list(args),
             stderr=STDOUT)

    def test_run_without_args_shows_help_reminder(self):
        error = self.assertRaises(CalledProcessError, self.run_command)
        self.assertEqual(2, error.returncode)
        self.assertIn(
            "Run %s --help for usage details." % locate_maascli(),
            error.output)

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks. Taking the error from the exception simplifies things nicely. On the other hand, I really don't care what the return code is, as long as it's not zero! No need to make tests depend on that, I think.

Jeroen

Revision history for this message
MAAS Lander (maas-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Revision history for this message
Gavin Panella (allenap) wrote :

[3]

+    def test_run_without_args_shows_help_reminder(self):
+        self.output_file = self.make_file('output')
+        try:
+            self.run_command()
+        except CalledProcessError as e:
+            pass
+        self.assertIn(
+            "Run %s --help for usage details." % locate_maascli(),
+            e.output)

This should use assertRaises(). If run_command() does not raise any
exception, this test will break with `NameError: name 'e' is not
defined`.

[4]

Don't forget rvba's question:

> How about sparing the user an extra step and print the 'usage'
> string directly instead of message asking the user to re-run the
> command with -h (same as what, say, 'nmap' does)?

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 21/11/12 00:08, Gavin Panella wrote:
> [3]
>
> + def test_run_without_args_shows_help_reminder(self):
> + self.output_file = self.make_file('output')
> + try:
> + self.run_command()
> + except CalledProcessError as e:
> + pass
> + self.assertIn(
> + "Run %s --help for usage details." % locate_maascli(),
> + e.output)
>
> This should use assertRaises(). If run_command() does not raise any
> exception, this test will break with `NameError: name 'e' is not
> defined`.

Also remember that assertRaises() returns the exception, so you can
further assert the exception text if you want.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Sorry for forgetting about that other question: why not print out usage info if no options are passed? This is getting into taste, where mine is to separate user-doesn't-know-what-they're-doing from user-wants-usage-information. It can be annoying to someone who's not familiar with the Unix-style command line at all, but it does make some sense and it is established in Unix tradition.

Command-line users probably won't run a command without arguments to find out how it works: we have the -h convention and manpages for that. But they might run it without arguments for other reasons. They expect it to do something else (interactive MAAS CLI?) or they might do it by accident (a script passes $ARGS but there's nothing in ARGS). In those cases, the important message is “this is not how it works” and we shouldn't be drowning that out with usage details.

Revision history for this message
Raphaël Badin (rvb) wrote :

Thanks for the reply.

> It can be annoying to someone who's not familiar with the Unix-style command line at all, but it does make some sense > and it is established in Unix tradition.
[...]

Maybe it's a matter of taste but for complex cli tools with a rich feature set (think nmap, or sudo maas [i.e. the django cli]), I think it's best to show the help directly because (at least it's what I do), when you've forgotten about the usage, you just type the command and get the nice help text to refresh your memory. Of course, it's ok to think about adding '-h' but I don't see any reason to impose that extra step.

> But they might run it without arguments for other reasons. They expect it to do something else (interactive MAAS CLI?) > or they might do it by accident (a script passes $ARGS but there's nothing in ARGS). In those cases, the important
> message is “this is not how it works” and we shouldn't be drowning that out with usage details.

If there is a human on the other side, then it does not hurt to show the help directly. If this is done by a script, then the non-zero return code is all that matters.

But anyway, it's really a detail.

Revision history for this message
Gavin Panella (allenap) wrote :

Fwiw, I'm with jtv on this. Having the full help printed is useful the
first 3 or 4 times I incorrectly invoke the command, or after a period
of months without using it, perhaps, but after that it's a nuisance,
especially when I suddenly get 30-40 lines of text because of a typo,
and my context has scrolled off the screen. I'd rather live with a
terse response and having to add -h.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 23/11/12 02:22, Gavin Panella wrote:
> Fwiw, I'm with jtv on this. Having the full help printed is useful the
> first 3 or 4 times I incorrectly invoke the command, or after a period
> of months without using it, perhaps, but after that it's a nuisance,
> especially when I suddenly get 30-40 lines of text because of a typo,
> and my context has scrolled off the screen. I'd rather live with a
> terse response and having to add -h.
>

I have the exact same thought. I did initially think what rvb says but
Gavin nailed it - context scrolling off the top of your terminal is
*really* annoying.

Cheers

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maascli/__init__.py'
2--- src/maascli/__init__.py 2012-10-08 14:07:24 +0000
3+++ src/maascli/__init__.py 2012-11-19 16:14:20 +0000
4@@ -28,6 +28,14 @@
5 if argv is None:
6 argv = sys.argv[:1] + osutils.get_unicode_argv()
7
8+ if len(argv) == 1:
9+ # No arguments passed. Be helpful and point out the --help option.
10+ sys.stderr.write(
11+ "Error: no arguments given.\n"
12+ "Run %s --help for usage details.\n"
13+ % argv[0])
14+ raise SystemExit(2)
15+
16 parser = prepare_parser(argv)
17
18 # Run, doing polite things with exceptions.
19
20=== modified file 'src/maascli/tests/test_integration.py'
21--- src/maascli/tests/test_integration.py 2012-10-04 08:36:34 +0000
22+++ src/maascli/tests/test_integration.py 2012-11-19 16:14:20 +0000
23@@ -15,7 +15,8 @@
24 import os.path
25 from subprocess import (
26 CalledProcessError,
27- check_call,
28+ check_output,
29+ STDOUT,
30 )
31
32 from maastesting.testcase import TestCase
33@@ -34,14 +35,21 @@
34 class TestMAASCli(TestCase):
35
36 def run_command(self, *args):
37- with open('/dev/null', 'ab') as dev_null:
38- check_call(
39- [locate_maascli()] + list(args),
40- stdout=dev_null, stderr=dev_null)
41+ check_output([locate_maascli()] + list(args), stderr=STDOUT)
42
43 def test_run_without_args_fails(self):
44 self.assertRaises(CalledProcessError, self.run_command)
45
46+ def test_run_without_args_shows_help_reminder(self):
47+ self.output_file = self.make_file('output')
48+ try:
49+ self.run_command()
50+ except CalledProcessError as e:
51+ pass
52+ self.assertIn(
53+ "Run %s --help for usage details." % locate_maascli(),
54+ e.output)
55+
56 def test_help_option_succeeds(self):
57 self.run_command('-h')
58 # The test is that we get here without error.