Revision d971402f

b/lib/cli.py
1480 1480
  ]
1481 1481

  
1482 1482

  
1483
def _ParseArgs(argv, commands, aliases, env_override):
1483
class _ShowUsage(Exception):
1484
  """Exception class for L{_ParseArgs}.
1485

  
1486
  """
1487
  def __init__(self, exit_error):
1488
    """Initializes instances of this class.
1489

  
1490
    @type exit_error: bool
1491
    @param exit_error: Whether to report failure on exit
1492

  
1493
    """
1494
    Exception.__init__(self)
1495
    self.exit_error = exit_error
1496

  
1497

  
1498
class _ShowVersion(Exception):
1499
  """Exception class for L{_ParseArgs}.
1500

  
1501
  """
1502

  
1503

  
1504
def _ParseArgs(binary, argv, commands, aliases, env_override):
1484 1505
  """Parser for the command line arguments.
1485 1506

  
1486 1507
  This function parses the arguments and returns the function which
1487 1508
  must be executed together with its (modified) arguments.
1488 1509

  
1489
  @param argv: the command line
1490
  @param commands: dictionary with special contents, see the design
1491
      doc for cmdline handling
1492
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
1510
  @param binary: Script name
1511
  @param argv: Command line arguments
1512
  @param commands: Dictionary containing command definitions
1513
  @param aliases: dictionary with command aliases {"alias": "target", ...}
1493 1514
  @param env_override: list of env variables allowed for default args
1515
  @raise _ShowUsage: If usage description should be shown
1516
  @raise _ShowVersion: If version should be shown
1494 1517

  
1495 1518
  """
1496 1519
  assert not (env_override - set(commands))
1520
  assert not (set(aliases.keys()) & set(commands.keys()))
1497 1521

  
1498
  if len(argv) == 0:
1499
    binary = "<command>"
1522
  if len(argv) > 1:
1523
    cmd = argv[1]
1500 1524
  else:
1501
    binary = argv[0].split("/")[-1]
1525
    # No option or command given
1526
    raise _ShowUsage(exit_error=True)
1502 1527

  
1503
  if len(argv) > 1 and argv[1] == "--version":
1504
    ToStdout("%s (ganeti %s) %s", binary, constants.VCS_VERSION,
1505
             constants.RELEASE_VERSION)
1506
    # Quit right away. That way we don't have to care about this special
1507
    # argument. optparse.py does it the same.
1508
    sys.exit(0)
1509

  
1510
  if len(argv) < 2 or not (argv[1] in commands or
1511
                           argv[1] in aliases):
1512
    # let's do a nice thing
1513
    sortedcmds = commands.keys()
1514
    sortedcmds.sort()
1515

  
1516
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
1517
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
1518
    ToStdout("")
1519

  
1520
    # compute the max line length for cmd + usage
1521
    mlen = max([len(" %s" % cmd) for cmd in commands])
1522
    mlen = min(60, mlen) # should not get here...
1523

  
1524
    # and format a nice command list
1525
    ToStdout("Commands:")
1526
    for cmd in sortedcmds:
1527
      cmdstr = " %s" % (cmd,)
1528
      help_text = commands[cmd][4]
1529
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
1530
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
1531
      for line in help_lines:
1532
        ToStdout("%-*s   %s", mlen, "", line)
1533

  
1534
    ToStdout("")
1535

  
1536
    return None, None, None
1528
  if cmd == "--version":
1529
    raise _ShowVersion()
1530
  elif cmd == "--help":
1531
    raise _ShowUsage(exit_error=False)
1532
  elif not (cmd in commands or cmd in aliases):
1533
    raise _ShowUsage(exit_error=True)
1537 1534

  
1538 1535
  # get command, unalias it, and look it up in commands
1539
  cmd = argv.pop(1)
1540 1536
  if cmd in aliases:
1541
    if cmd in commands:
1542
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
1543
                                   " command" % cmd)
1544

  
1545 1537
    if aliases[cmd] not in commands:
1546 1538
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
1547 1539
                                   " command '%s'" % (cmd, aliases[cmd]))
......
1552 1544
    args_env_name = ("%s_%s" % (binary.replace("-", "_"), cmd)).upper()
1553 1545
    env_args = os.environ.get(args_env_name)
1554 1546
    if env_args:
1555
      argv = utils.InsertAtPos(argv, 1, shlex.split(env_args))
1547
      argv = utils.InsertAtPos(argv, 2, shlex.split(env_args))
1556 1548

  
1557 1549
  func, args_def, parser_opts, usage, description = commands[cmd]
1558 1550
  parser = OptionParser(option_list=parser_opts + COMMON_OPTS,
......
1560 1552
                        formatter=TitledHelpFormatter(),
1561 1553
                        usage="%%prog %s %s" % (cmd, usage))
1562 1554
  parser.disable_interspersed_args()
1563
  options, args = parser.parse_args(args=argv[1:])
1555
  options, args = parser.parse_args(args=argv[2:])
1564 1556

  
1565 1557
  if not _CheckArguments(cmd, args_def, args):
1566 1558
    return None, None, None
......
1568 1560
  return func, options, args
1569 1561

  
1570 1562

  
1563
def _FormatUsage(binary, commands):
1564
  """Generates a nice description of all commands.
1565

  
1566
  @param binary: Script name
1567
  @param commands: Dictionary containing command definitions
1568

  
1569
  """
1570
  # compute the max line length for cmd + usage
1571
  mlen = min(60, max(map(len, commands)))
1572

  
1573
  yield "Usage: %s {command} [options...] [argument...]" % binary
1574
  yield "%s <command> --help to see details, or man %s" % (binary, binary)
1575
  yield ""
1576
  yield "Commands:"
1577

  
1578
  # and format a nice command list
1579
  for (cmd, (_, _, _, _, help_text)) in sorted(commands.items()):
1580
    help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
1581
    yield " %-*s - %s" % (mlen, cmd, help_lines.pop(0))
1582
    for line in help_lines:
1583
      yield " %-*s   %s" % (mlen, "", line)
1584

  
1585
  yield ""
1586

  
1587

  
1571 1588
def _CheckArguments(cmd, args_def, args):
1572 1589
  """Verifies the arguments using the argument definition.
1573 1590

  
......
2242 2259
    aliases = {}
2243 2260

  
2244 2261
  try:
2245
    func, options, args = _ParseArgs(sys.argv, commands, aliases, env_override)
2262
    (func, options, args) = _ParseArgs(binary, sys.argv, commands, aliases,
2263
                                       env_override)
2264
  except _ShowVersion:
2265
    ToStdout("%s (ganeti %s) %s", binary, constants.VCS_VERSION,
2266
             constants.RELEASE_VERSION)
2267
    return constants.EXIT_SUCCESS
2268
  except _ShowUsage, err:
2269
    for line in _FormatUsage(binary, commands):
2270
      ToStdout(line)
2271

  
2272
    if err.exit_error:
2273
      return constants.EXIT_FAILURE
2274
    else:
2275
      return constants.EXIT_SUCCESS
2246 2276
  except errors.ParameterError, err:
2247 2277
    result, err_msg = FormatError(err)
2248 2278
    ToStderr(err_msg)
b/test/ganeti.cli_unittest.py
922 922
      self.assertEqual(cli.FormatTimestamp(i), "?")
923 923

  
924 924

  
925
if __name__ == '__main__':
925
class TestFormatUsage(unittest.TestCase):
926
  def test(self):
927
    binary = "gnt-unittest"
928
    commands = {
929
      "cmdA":
930
        (NotImplemented, NotImplemented, NotImplemented, NotImplemented,
931
         "description of A"),
932
      "bbb":
933
        (NotImplemented, NotImplemented, NotImplemented, NotImplemented,
934
         "Hello World," * 10),
935
      "longname":
936
        (NotImplemented, NotImplemented, NotImplemented, NotImplemented,
937
         "Another description"),
938
      }
939

  
940
    self.assertEqual(list(cli._FormatUsage(binary, commands)), [
941
      "Usage: gnt-unittest {command} [options...] [argument...]",
942
      "gnt-unittest <command> --help to see details, or man gnt-unittest",
943
      "",
944
      "Commands:",
945
      (" bbb      - Hello World,Hello World,Hello World,Hello World,Hello"
946
       " World,Hello"),
947
      "            World,Hello World,Hello World,Hello World,Hello World,",
948
      " cmdA     - description of A",
949
      " longname - Another description",
950
      "",
951
      ])
952

  
953

  
954
class TestParseArgs(unittest.TestCase):
955
  def testNoArguments(self):
956
    for argv in [[], ["gnt-unittest"]]:
957
      try:
958
        cli._ParseArgs("gnt-unittest", argv, {}, {}, set())
959
      except cli._ShowUsage, err:
960
        self.assertTrue(err.exit_error)
961
      else:
962
        self.fail("Did not raise exception")
963

  
964
  def testVersion(self):
965
    for argv in [["test", "--version"], ["test", "--version", "somethingelse"]]:
966
      try:
967
        cli._ParseArgs("test", argv, {}, {}, set())
968
      except cli._ShowVersion:
969
        pass
970
      else:
971
        self.fail("Did not raise exception")
972

  
973
  def testHelp(self):
974
    for argv in [["test", "--help"], ["test", "--help", "somethingelse"]]:
975
      try:
976
        cli._ParseArgs("test", argv, {}, {}, set())
977
      except cli._ShowUsage, err:
978
        self.assertFalse(err.exit_error)
979
      else:
980
        self.fail("Did not raise exception")
981

  
982
  def testUnknownCommandOrAlias(self):
983
    for argv in [["test", "list"], ["test", "somethingelse", "--help"]]:
984
      try:
985
        cli._ParseArgs("test", argv, {}, {}, set())
986
      except cli._ShowUsage, err:
987
        self.assertTrue(err.exit_error)
988
      else:
989
        self.fail("Did not raise exception")
990

  
991
  def testInvalidAliasList(self):
992
    cmd = {
993
      "list": NotImplemented,
994
      "foo": NotImplemented,
995
      }
996
    aliases = {
997
      "list": NotImplemented,
998
      "foo": NotImplemented,
999
      }
1000
    assert sorted(cmd.keys()) == sorted(aliases.keys())
1001
    self.assertRaises(AssertionError, cli._ParseArgs, "test",
1002
                      ["test", "list"], cmd, aliases, set())
1003

  
1004
  def testAliasForNonExistantCommand(self):
1005
    cmd = {}
1006
    aliases = {
1007
      "list": NotImplemented,
1008
      }
1009
    self.assertRaises(errors.ProgrammerError, cli._ParseArgs, "test",
1010
                      ["test", "list"], cmd, aliases, set())
1011

  
1012

  
1013
if __name__ == "__main__":
926 1014
  testutils.GanetiTestProgram()
b/test/gnt-cli.test
2 2
$SCRIPTS/gnt-node --help
3 3
>>>/Usage:/
4 4
>>>2
5
>>>= 0
6
$SCRIPTS/gnt-node UNKNOWN
7
>>>/Usage:/
8
>>>2
5 9
>>>= 1
6 10
$SCRIPTS/gnt-node --version
7 11
>>>/^gnt-/
......
11 15
$SCRIPTS/gnt-instance --help
12 16
>>>/Usage:/
13 17
>>>2
18
>>>= 0
19
$SCRIPTS/gnt-instance UNKNOWN
20
>>>/Usage:/
21
>>>2
14 22
>>>= 1
15 23
$SCRIPTS/gnt-instance --version
16 24
>>>/^gnt-instance/
......
20 28
$SCRIPTS/gnt-os --help
21 29
>>>/Usage:/
22 30
>>>2
31
>>>= 0
32
$SCRIPTS/gnt-os UNKNOWN
33
>>>/Usage:/
34
>>>2
23 35
>>>= 1
24 36
$SCRIPTS/gnt-os --version
25 37
>>>/^gnt-/
......
29 41
$SCRIPTS/gnt-group --help
30 42
>>>/Usage:/
31 43
>>>2
44
>>>= 0
45
$SCRIPTS/gnt-group UNKNOWN
46
>>>/Usage:/
47
>>>2
32 48
>>>= 1
33 49
$SCRIPTS/gnt-group --version
34 50
>>>/^gnt-/
......
38 54
$SCRIPTS/gnt-job --help
39 55
>>>/Usage:/
40 56
>>>2
57
>>>= 0
58
$SCRIPTS/gnt-job UNKNOWN
59
>>>/Usage:/
60
>>>2
41 61
>>>= 1
42 62
$SCRIPTS/gnt-job --version
43 63
>>>/^gnt-/
......
47 67
$SCRIPTS/gnt-cluster --help
48 68
>>>/Usage:/
49 69
>>>2
70
>>>= 0
71
$SCRIPTS/gnt-cluster UNKNOWN
72
>>>/Usage:/
73
>>>2
50 74
>>>= 1
51 75
$SCRIPTS/gnt-cluster --version
52 76
>>>/^gnt-/
......
56 80
$SCRIPTS/gnt-backup --help
57 81
>>>/Usage:/
58 82
>>>2
83
>>>= 0
84
$SCRIPTS/gnt-backup UNKNOWN
85
>>>/Usage:/
86
>>>2
59 87
>>>= 1
60 88
$SCRIPTS/gnt-backup --version
61 89
>>>/^gnt-/
......
65 93
$SCRIPTS/gnt-debug --help
66 94
>>>/Usage:/
67 95
>>>2
96
>>>= 0
97
$SCRIPTS/gnt-debug UNKNOWN
98
>>>/Usage:/
99
>>>2
68 100
>>>= 1
69 101
$SCRIPTS/gnt-debug --version
70 102
>>>/^gnt-/

Also available in: Unified diff