bin/tahoe: clean up global-vs-subcommand arguments like --node-directory
authorBrian Warner <warner@lothar.com>
Mon, 18 Jun 2012 17:43:49 +0000 (10:43 -0700)
committerBrian Warner <warner@lothar.com>
Tue, 9 Apr 2013 18:11:32 +0000 (19:11 +0100)
The new rules for "bin/tahoe ARG1.. SUBCOMMAND ARG2.." arg:

* --node-directory is only accepted in ARG1, not ARG2
* create-*/start/stop/restart accept --basedir in ARG2, or an explicit
  basedir argument
* only one of --node-directory/--basedir/explicit-basedir is accepted
* --quiet/--version is only accepted in ARG1, not ARG2

Closes #166

13 files changed:
src/allmydata/scripts/admin.py
src/allmydata/scripts/cli.py
src/allmydata/scripts/common.py
src/allmydata/scripts/create_node.py
src/allmydata/scripts/debug.py
src/allmydata/scripts/keygen.py
src/allmydata/scripts/runner.py
src/allmydata/scripts/startstop_node.py
src/allmydata/scripts/stats_gatherer.py
src/allmydata/test/test_cli.py
src/allmydata/test/test_deepcheck.py
src/allmydata/test/test_runner.py
src/allmydata/test/test_system.py

index 581224d69c93165348279f2083002cee7d78304f..1a6c9854f93ed32161d95b1ee7db47c618d7bc95 100644 (file)
@@ -1,12 +1,13 @@
 
 from twisted.python import usage
+from allmydata.scripts.common import BaseOptions
 
-class GenerateKeypairOptions(usage.Options):
+class GenerateKeypairOptions(BaseOptions):
     def getSynopsis(self):
         return "Usage: tahoe admin generate-keypair"
 
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Generate a public/private keypair, dumped to stdout as two lines of ASCII..
 
@@ -20,7 +21,7 @@ def print_keypair(options):
     print >>out, "private:", privkey_vs
     print >>out, "public:", pubkey_vs
 
-class DerivePubkeyOptions(usage.Options):
+class DerivePubkeyOptions(BaseOptions):
     def parseArgs(self, privkey):
         self.privkey = privkey
 
@@ -28,7 +29,7 @@ class DerivePubkeyOptions(usage.Options):
         return "Usage: tahoe admin derive-pubkey PRIVKEY"
 
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Given a private (signing) key that was previously generated with
 generate-keypair, derive the public key and print it to stdout.
@@ -45,7 +46,7 @@ def derive_pubkey(options):
     print >>out, "public:", pubkey_vs
     return 0
 
-class AdminCommand(usage.Options):
+class AdminCommand(BaseOptions):
     subCommands = [
         ("generate-keypair", None, GenerateKeypairOptions,
          "Generate a public/private keypair, write to stdout."),
@@ -58,7 +59,7 @@ class AdminCommand(usage.Options):
     def getSynopsis(self):
         return "Usage: tahoe admin SUBCOMMAND"
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Please run e.g. 'tahoe admin generate-keypair --help' for more details on
 each subcommand.
index 343a572807b3d7997167aa04199891e7e65dd4bb..5082685cfc568293a2329335f35a4b5fff66575c 100644 (file)
@@ -1,6 +1,7 @@
 import os.path, re, fnmatch
 from twisted.python import usage
-from allmydata.scripts.common import BaseOptions, get_aliases, get_default_nodedir, DEFAULT_ALIAS
+from allmydata.scripts.common import get_aliases, get_default_nodedir, \
+     DEFAULT_ALIAS, BaseOptions
 from allmydata.util.encodingutil import argv_to_unicode, argv_to_abspath, quote_output
 
 NODEURL_RE=re.compile("http(s?)://([^:]*)(:([1-9][0-9]*))?")
@@ -9,23 +10,18 @@ _default_nodedir = get_default_nodedir()
 
 class VDriveOptions(BaseOptions):
     optParameters = [
-        ["node-directory", "d", None,
-         "Specify which Tahoe node directory should be used. The directory "
-         "should either contain a full Tahoe node, or a file named node.url "
-         "that points to some other Tahoe node. It should also contain a file "
-         "named '" + os.path.join('private', 'aliases') + "' which contains the "
-         "mapping from alias name to root dirnode URI." + (
-            _default_nodedir and (" [default: " + quote_output(_default_nodedir) + "]") or "")],
         ["node-url", "u", None,
-         "Specify the URL of the Tahoe gateway node, such as 'http://127.0.0.1:3456'. "
+         "Specify the URL of the Tahoe gateway node, such as "
+         "'http://127.0.0.1:3456'. "
          "This overrides the URL found in the --node-directory ."],
         ["dir-cap", None, None,
          "Specify which dirnode URI should be used as the 'tahoe' alias."]
         ]
 
     def postOptions(self):
-        if self['node-directory']:
-            self['node-directory'] = argv_to_abspath(self['node-directory'])
+        self["quiet"] = self.parent["quiet"]
+        if self.parent['node-directory']:
+            self['node-directory'] = argv_to_abspath(self.parent['node-directory'])
         else:
             self['node-directory'] = _default_nodedir
 
index cd545c3c09cce0fd2b7823eeb06b9120a9d3608d..27ea9af1668c93bbbdae0dc44ad816c7e230c146 100644 (file)
@@ -25,58 +25,42 @@ def get_default_nodedir():
 
 
 class BaseOptions(usage.Options):
-    # unit tests can override these to point at StringIO instances
-    stdin = sys.stdin
-    stdout = sys.stdout
-    stderr = sys.stderr
-
-    optFlags = [
-        ["quiet", "q", "Operate silently."],
-        ["version", "V", "Display version numbers."],
-        ["version-and-path", None, "Display version numbers and paths to their locations."],
-    ]
-    optParameters = [
-        ["node-directory", "d", None, "Specify which Tahoe node directory should be used." + (
-            _default_nodedir and (" [default for most commands: " + quote_output(_default_nodedir) + "]") or "")],
-    ]
-
     def __init__(self):
         super(BaseOptions, self).__init__()
         self.command_name = os.path.basename(sys.argv[0])
         if self.command_name == 'trial':
             self.command_name = 'tahoe'
 
+    # Only allow "tahoe --version", not e.g. "tahoe start --version"
     def opt_version(self):
-        import allmydata
-        print >>self.stdout, allmydata.get_package_versions_string(debug=True)
-        self.no_command_needed = True
+        raise usage.UsageError("--version not allowed on subcommands")
 
-    def opt_version_and_path(self):
-        import allmydata
-        print >>self.stdout, allmydata.get_package_versions_string(show_paths=True, debug=True)
-        self.no_command_needed = True
-
-
-class BasedirMixin:
+class BasedirOptions(BaseOptions):
     default_nodedir = _default_nodedir
 
     optParameters = [
-        ["basedir", "C", None, "Same as --node-directory."],
+        ["basedir", "C", None, "Same as --node-directory (default %s)."
+         % get_default_nodedir()],
     ]
 
     def parseArgs(self, basedir=None):
-        if self['node-directory'] and self['basedir']:
-            raise usage.UsageError("The --node-directory (or -d) and --basedir (or -C) "
-                                   "options cannot both be used.")
+        if self.parent['node-directory'] and self['basedir']:
+            raise usage.UsageError("The --node-directory (or -d) and --basedir (or -C) options cannot both be used.")
+        if self.parent['node-directory'] and basedir:
+            raise usage.UsageError("The --node-directory (or -d) option and a basedir argument cannot both be used.")
+        if self['basedir'] and basedir:
+            raise usage.UsageError("The --basedir (or -C) option and a basedir argument cannot both be used.")
 
         if basedir:
             b = argv_to_abspath(basedir)
         elif self['basedir']:
             b = argv_to_abspath(self['basedir'])
-        elif self['node-directory']:
-            b = argv_to_abspath(self['node-directory'])
-        else:
+        elif self.parent['node-directory']:
+            b = argv_to_abspath(self.parent['node-directory'])
+        elif self.default_nodedir:
             b = self.default_nodedir
+        else:
+            raise usage.UsageError("No default basedir available, you must provide one with --node-directory, --basedir, or a basedir argument")
         self['basedir'] = b
 
     def postOptions(self):
index 7e9dcf7912a50279d673c9702ebd3db4517e6790..893bf08c2b0ec62f3946a398dda7ce6d6747bee4 100644 (file)
@@ -1,11 +1,11 @@
 
 import os, sys
-from allmydata.scripts.common import BasedirMixin, BaseOptions
+from allmydata.scripts.common import BasedirOptions
 from allmydata.util.assertutil import precondition
 from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_output
 import allmydata
 
-class CreateClientOptions(BasedirMixin, BaseOptions):
+class CreateClientOptions(BasedirOptions):
     optParameters = [
         # we provide 'create-node'-time options for the most common
         # configuration knobs. The rest can be controlled by editing
@@ -29,13 +29,9 @@ class CreateNodeOptions(CreateClientOptions):
         return "Usage:  %s create-node [options] [NODEDIR]" % (self.command_name,)
 
 
-class CreateIntroducerOptions(BasedirMixin, BaseOptions):
+class CreateIntroducerOptions(BasedirOptions):
     default_nodedir = None
 
-    optParameters = [
-        ["node-directory", "d", None, "Specify which directory the introducer should be created in. [no default]"],
-    ]
-
     def getSynopsis(self):
         return "Usage:  %s create-introducer [options] NODEDIR" % (self.command_name,)
 
index f7749cd45894f23d19b8fc53b4ce4c3589b19496..cb8e986a7136552469450f51e131d7c436aeb1e1 100644 (file)
@@ -6,9 +6,10 @@ from twisted.python import usage, failure
 from twisted.internet import defer
 from twisted.scripts import trial as twisted_trial
 from foolscap.logging import cli as foolscap_cli
+from allmydata.scripts.common import BaseOptions
 
 
-class DumpOptions(usage.Options):
+class DumpOptions(BaseOptions):
     def getSynopsis(self):
         return "Usage: tahoe debug dump-share SHARE_FILENAME"
 
@@ -18,7 +19,7 @@ class DumpOptions(usage.Options):
         ]
 
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Print lots of information about the given share, by parsing the share's
 contents. This includes share type, lease information, encoding parameters,
@@ -405,7 +406,7 @@ def dump_MDMF_share(m, length, options):
 
 
 
-class DumpCapOptions(usage.Options):
+class DumpCapOptions(BaseOptions):
     def getSynopsis(self):
         return "Usage: tahoe debug dump-cap [options] FILECAP"
     optParameters = [
@@ -420,7 +421,7 @@ class DumpCapOptions(usage.Options):
         self.cap = cap
 
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Print information about the given cap-string (aka: URI, file-cap, dir-cap,
 read-cap, write-cap). The URI string is parsed and unpacked. This prints the
@@ -607,7 +608,7 @@ def dump_uri_instance(u, nodeid, secret, out, show_header=True):
     else:
         print >>out, "unknown cap type"
 
-class FindSharesOptions(usage.Options):
+class FindSharesOptions(BaseOptions):
     def getSynopsis(self):
         return "Usage: tahoe debug find-shares STORAGE_INDEX NODEDIRS.."
 
@@ -617,7 +618,7 @@ class FindSharesOptions(usage.Options):
         self.nodedirs = map(argv_to_abspath, nodedirs)
 
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Locate all shares for the given storage index. This command looks through one
 or more node directories to find the shares. It returns a list of filenames,
@@ -657,7 +658,7 @@ def find_shares(options):
     return 0
 
 
-class CatalogSharesOptions(usage.Options):
+class CatalogSharesOptions(BaseOptions):
     """
 
     """
@@ -671,7 +672,7 @@ class CatalogSharesOptions(usage.Options):
         return "Usage: tahoe debug catalog-shares NODEDIRS.."
 
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Locate all shares in the given node directories, and emit a one-line summary
 of each share. Run it like this:
@@ -879,7 +880,7 @@ def catalog_shares_one_abbrevdir(si_s, si_dir, now, out, err):
         print >>err, "Error processing %s" % quote_output(si_dir)
         failure.Failure().printTraceback(err)
 
-class CorruptShareOptions(usage.Options):
+class CorruptShareOptions(BaseOptions):
     def getSynopsis(self):
         return "Usage: tahoe debug corrupt-share SHARE_FILENAME"
 
@@ -888,7 +889,7 @@ class CorruptShareOptions(usage.Options):
         ]
 
     def getUsage(self, width=None):
-        t = usage.Options.getUsage(self, width)
+        t = BaseOptions.getUsage(self, width)
         t += """
 Corrupt the given share by flipping a bit. This will cause a
 verifying/downloading client to log an integrity-check failure incident, and
@@ -959,7 +960,7 @@ def corrupt_share(options):
 
 
 
-class ReplOptions(usage.Options):
+class ReplOptions(BaseOptions):
     def getSynopsis(self):
         return "Usage: tahoe debug repl"
 
@@ -1042,7 +1043,7 @@ def flogtool(config):
     return foolscap_cli.run_flogtool()
 
 
-class DebugCommand(usage.Options):
+class DebugCommand(BaseOptions):
     subCommands = [
         ["dump-share", None, DumpOptions,
          "Unpack and display the contents of a share (uri_extension and leases)."],
@@ -1060,7 +1061,7 @@ class DebugCommand(usage.Options):
     def getSynopsis(self):
         return ""
     def getUsage(self, width=None):
-        #t = usage.Options.getUsage(self, width)
+        #t = BaseOptions.getUsage(self, width)
         t = """Usage: tahoe debug SUBCOMMAND
 Subcommands:
     tahoe debug dump-share      Unpack and display the contents of a share.
index 1f5c30f5287d150e60a4850ef83e60bd349ff308..5b1c9ab5981de0baa1278d4df98c13b494a1c1dd 100644 (file)
@@ -1,16 +1,12 @@
 
 import os, sys
-from allmydata.scripts.common import BasedirMixin, BaseOptions
+from allmydata.scripts.common import BasedirOptions
 from allmydata.util.assertutil import precondition
 from allmydata.util.encodingutil import listdir_unicode, quote_output
 
-class CreateKeyGeneratorOptions(BasedirMixin, BaseOptions):
+class CreateKeyGeneratorOptions(BasedirOptions):
     default_nodedir = None
 
-    optParameters = [
-        ["node-directory", "d", None, "Specify which directory the key-generator should be created in. [no default]"],
-    ]
-
     def getSynopsis(self):
         return "Usage:  %s create-key-generator [options] NODEDIR" % (self.command_name,)
 
index 210efb6546468aef3839cca9970ecca7ef7961c6..c89a2f45cde5dedebb1b93d6b9c8c9d7fdbb2e8c 100644 (file)
@@ -1,10 +1,10 @@
 
-import sys
+import os, sys
 from cStringIO import StringIO
 
 from twisted.python import usage
 
-from allmydata.scripts.common import BaseOptions
+from allmydata.scripts.common import get_default_nodedir
 from allmydata.scripts import debug, create_node, startstop_node, cli, keygen, stats_gatherer, admin
 from allmydata.util.encodingutil import quote_output, get_io_encoding
 
@@ -15,7 +15,24 @@ def GROUP(s):
     return [("\n" + s, None, None, None)]
 
 
-class Options(BaseOptions, usage.Options):
+_default_nodedir = get_default_nodedir()
+
+NODEDIR_HELP = ("Specify which Tahoe node directory should be used. The "
+                "directory should either contain a full Tahoe node, or a "
+                "file named node.url that points to some other Tahoe node. "
+                "It should also contain a file named '"
+                + os.path.join('private', 'aliases') +
+                "' which contains the mapping from alias name to root "
+                "dirnode URI.")
+if _default_nodedir:
+    NODEDIR_HELP += " [default for most commands: " + quote_output(_default_nodedir) + "]"
+
+class Options(usage.Options):
+    # unit tests can override these to point at StringIO instances
+    stdin = sys.stdin
+    stdout = sys.stdout
+    stderr = sys.stderr
+
     synopsis = "\nUsage:  tahoe <command> [command options]"
     subCommands = ( GROUP("Administration")
                     +   create_node.subCommands
@@ -30,6 +47,28 @@ class Options(BaseOptions, usage.Options):
                     +   cli.subCommands
                     )
 
+    optFlags = [
+        ["quiet", "q", "Operate silently."],
+        ["version", "V", "Display version numbers."],
+        ["version-and-path", None, "Display version numbers and paths to their locations."],
+    ]
+    optParameters = [
+        ["node-directory", "d", None, NODEDIR_HELP],
+    ]
+
+    def opt_version(self):
+        import allmydata
+        print >>self.stdout, allmydata.get_package_versions_string(debug=True)
+        self.no_command_needed = True
+
+    def opt_version_and_path(self):
+        import allmydata
+        print >>self.stdout, allmydata.get_package_versions_string(show_paths=True, debug=True)
+        self.no_command_needed = True
+
+    def getSynopsis(self):
+        return "\nUsage: tahoe [global-options] <command> [command-options]"
+
     def getUsage(self, **kwargs):
         t = usage.Options.getUsage(self, **kwargs)
         return t + "\nPlease run 'tahoe <command> --help' for more details on each command.\n"
index 5045bd6136d58f698a4b7312436c098ebde58ec2..36138e00995949cb16e091c3f6de9aed6eac577f 100644 (file)
@@ -1,12 +1,12 @@
 
 import os, sys, signal, time
-from allmydata.scripts.common import BasedirMixin, BaseOptions
+from allmydata.scripts.common import BasedirOptions
 from allmydata.util import fileutil
 from allmydata.util.assertutil import precondition
 from allmydata.util.encodingutil import listdir_unicode, quote_output
 
 
-class StartOptions(BasedirMixin, BaseOptions):
+class StartOptions(BasedirOptions):
     optFlags = [
         ["profile", "p", "Run under the Python profiler, putting results in 'profiling_results.prof'."],
         ["syslog", None, "Tell the node to log to syslog, not a file."],
@@ -16,12 +16,12 @@ class StartOptions(BasedirMixin, BaseOptions):
         return "Usage:  %s start [options] [NODEDIR]" % (self.command_name,)
 
 
-class StopOptions(BasedirMixin, BaseOptions):
+class StopOptions(BasedirOptions):
     def getSynopsis(self):
         return "Usage:  %s stop [options] [NODEDIR]" % (self.command_name,)
 
 
-class RestartOptions(BasedirMixin, BaseOptions):
+class RestartOptions(BasedirOptions):
     optFlags = [
         ["profile", "p", "Run under the Python profiler, putting results in 'profiling_results.prof'."],
         ["syslog", None, "Tell the node to log to syslog, not a file."],
@@ -31,13 +31,9 @@ class RestartOptions(BasedirMixin, BaseOptions):
         return "Usage:  %s restart [options] [NODEDIR]" % (self.command_name,)
 
 
-class RunOptions(BasedirMixin, BaseOptions):
+class RunOptions(BasedirOptions):
     default_nodedir = u"."
 
-    optParameters = [
-        ["node-directory", "d", None, "Specify the directory of the node to be run. [default, for 'tahoe run' only: current directory]"],
-    ]
-
     def getSynopsis(self):
         return "Usage:  %s run [options] [NODEDIR]" % (self.command_name,)
 
index 230d4a9073fb41d186b07450705ec59021f08b39..bdb357660043307b61d0cdba6aeeaf1cb772f1bd 100644 (file)
@@ -1,16 +1,12 @@
 
 import os, sys
-from allmydata.scripts.common import BasedirMixin, BaseOptions
+from allmydata.scripts.common import BasedirOptions
 from allmydata.util.assertutil import precondition
 from allmydata.util.encodingutil import listdir_unicode, quote_output
 
-class CreateStatsGathererOptions(BasedirMixin, BaseOptions):
+class CreateStatsGathererOptions(BasedirOptions):
     default_nodedir = None
 
-    optParameters = [
-        ["node-directory", "d", None, "Specify which directory the stats-gatherer should be created in. [no default]"],
-    ]
-
     def getSynopsis(self):
         return "Usage:  %s create-stats-gatherer [options] NODEDIR" % (self.command_name,)
 
index 222bbba552929e708c27a5eab36ad9465e7ed708..22886d0a202dc24fe2bc007c05f1b8c13b0615d1 100644 (file)
@@ -44,13 +44,19 @@ from allmydata.util.fileutil import abspath_expanduser_unicode
 
 timeout = 480 # deep_check takes 360s on Zandr's linksys box, others take > 240s
 
+def parse_options(basedir, command, args):
+    o = runner.Options()
+    o.parseOptions(["--node-directory", basedir, command] + args)
+    while hasattr(o, "subOptions"):
+        o = o.subOptions
+    return o
 
 class CLITestMixin(ReallyEqualMixin):
     def do_cli(self, verb, *args, **kwargs):
         nodeargs = [
             "--node-directory", self.get_clientdir(),
             ]
-        argv = [verb] + nodeargs + list(args)
+        argv = nodeargs + [verb] + list(args)
         stdin = kwargs.get("stdin", "")
         stdout, stderr = StringIO(), StringIO()
         d = threads.deferToThread(runner.runner, argv, run_by_human=False,
@@ -73,69 +79,6 @@ class CLITestMixin(ReallyEqualMixin):
 
 
 class CLI(CLITestMixin, unittest.TestCase):
-    # this test case only looks at argument-processing and simple stuff.
-    def test_options(self):
-        fileutil.rm_dir("cli/test_options")
-        fileutil.make_dirs("cli/test_options")
-        fileutil.make_dirs("cli/test_options/private")
-        fileutil.write("cli/test_options/node.url", "http://localhost:8080/\n")
-        filenode_uri = uri.WriteableSSKFileURI(writekey="\x00"*16,
-                                               fingerprint="\x00"*32)
-        private_uri = uri.DirectoryURI(filenode_uri).to_string()
-        fileutil.write("cli/test_options/private/root_dir.cap", private_uri + "\n")
-        o = cli.ListOptions()
-        o.parseOptions(["--node-directory", "cli/test_options"])
-        self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
-        self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], private_uri)
-        self.failUnlessReallyEqual(o.where, u"")
-
-        o = cli.ListOptions()
-        o.parseOptions(["--node-directory", "cli/test_options",
-                        "--node-url", "http://example.org:8111/"])
-        self.failUnlessReallyEqual(o['node-url'], "http://example.org:8111/")
-        self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], private_uri)
-        self.failUnlessReallyEqual(o.where, u"")
-
-        o = cli.ListOptions()
-        o.parseOptions(["--node-directory", "cli/test_options",
-                        "--dir-cap", "root"])
-        self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
-        self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], "root")
-        self.failUnlessReallyEqual(o.where, u"")
-
-        o = cli.ListOptions()
-        other_filenode_uri = uri.WriteableSSKFileURI(writekey="\x11"*16,
-                                                     fingerprint="\x11"*32)
-        other_uri = uri.DirectoryURI(other_filenode_uri).to_string()
-        o.parseOptions(["--node-directory", "cli/test_options",
-                        "--dir-cap", other_uri])
-        self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
-        self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], other_uri)
-        self.failUnlessReallyEqual(o.where, u"")
-
-        o = cli.ListOptions()
-        o.parseOptions(["--node-directory", "cli/test_options",
-                        "--dir-cap", other_uri, "subdir"])
-        self.failUnlessReallyEqual(o['node-url'], "http://localhost:8080/")
-        self.failUnlessReallyEqual(o.aliases[DEFAULT_ALIAS], other_uri)
-        self.failUnlessReallyEqual(o.where, u"subdir")
-
-        o = cli.ListOptions()
-        self.failUnlessRaises(usage.UsageError,
-                              o.parseOptions,
-                              ["--node-directory", "cli/test_options",
-                               "--node-url", "NOT-A-URL"])
-
-        o = cli.ListOptions()
-        o.parseOptions(["--node-directory", "cli/test_options",
-                        "--node-url", "http://localhost:8080"])
-        self.failUnlessReallyEqual(o["node-url"], "http://localhost:8080/")
-
-        o = cli.ListOptions()
-        o.parseOptions(["--node-directory", "cli/test_options",
-                        "--node-url", "https://localhost/"])
-        self.failUnlessReallyEqual(o["node-url"], "https://localhost/")
-
     def _dump_cap(self, *args):
         config = debug.DumpCapOptions()
         config.stdout,config.stderr = StringIO(), StringIO()
@@ -730,11 +673,11 @@ class Help(unittest.TestCase):
 class CreateAlias(GridTestMixin, CLITestMixin, unittest.TestCase):
 
     def _test_webopen(self, args, expected_url):
-        woo = cli.WebopenOptions()
-        all_args = ["--node-directory", self.get_clientdir()] + list(args)
-        woo.parseOptions(all_args)
+        o = runner.Options()
+        o.parseOptions(["--node-directory", self.get_clientdir(), "webopen"]
+                       + list(args))
         urls = []
-        rc = cli.webopen(woo, urls.append)
+        rc = cli.webopen(o, urls.append)
         self.failUnlessReallyEqual(rc, 0)
         self.failUnlessReallyEqual(len(urls), 1)
         self.failUnlessReallyEqual(urls[0], expected_url)
@@ -2747,25 +2690,20 @@ class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
         fileutil.make_dirs(basedir)
         nodeurl_path = os.path.join(basedir, 'node.url')
         fileutil.write(nodeurl_path, 'http://example.net:2357/')
+        def parse(args): return parse_options(basedir, "backup", args)
 
         # test simple exclude
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude', '*lyx', '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude', '*lyx', 'from', 'to'])
         filtered = list(backup_options.filter_listdir(root_listdir))
         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
                               (u'nice_doc.lyx',))
         # multiple exclude
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude', '*lyx', '--exclude', 'lib.?', '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude', '*lyx', '--exclude', 'lib.?', 'from', 'to'])
         filtered = list(backup_options.filter_listdir(root_listdir))
         self._check_filtering(filtered, root_listdir, (u'_darcs', u'subdir'),
                               (u'nice_doc.lyx', u'lib.a'))
         # vcs metadata exclusion
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude-vcs', '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude-vcs', 'from', 'to'])
         filtered = list(backup_options.filter_listdir(subdir_listdir))
         self._check_filtering(filtered, subdir_listdir, (u'another_doc.lyx', u'run_snake_run.py',),
                               (u'CVS', u'.svn', u'_darcs'))
@@ -2773,22 +2711,17 @@ class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
         exclusion_string = "_darcs\n*py\n.svn"
         excl_filepath = os.path.join(basedir, 'exclusion')
         fileutil.write(excl_filepath, exclusion_string)
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude-from', excl_filepath, '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude-from', excl_filepath, 'from', 'to'])
         filtered = list(backup_options.filter_listdir(subdir_listdir))
         self._check_filtering(filtered, subdir_listdir, (u'another_doc.lyx', u'CVS'),
                               (u'.svn', u'_darcs', u'run_snake_run.py'))
         # test BackupConfigurationError
         self.failUnlessRaises(cli.BackupConfigurationError,
-                              backup_options.parseOptions,
-                              ['--exclude-from', excl_filepath + '.no', '--node-directory',
-                               basedir, 'from', 'to'])
+                              parse,
+                              ['--exclude-from', excl_filepath + '.no', 'from', 'to'])
 
         # test that an iterator works too
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude', '*lyx', '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude', '*lyx', 'from', 'to'])
         filtered = list(backup_options.filter_listdir(iter(root_listdir)))
         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
                               (u'nice_doc.lyx',))
@@ -2805,18 +2738,15 @@ class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
         fileutil.make_dirs(basedir)
         nodeurl_path = os.path.join(basedir, 'node.url')
         fileutil.write(nodeurl_path, 'http://example.net:2357/')
+        def parse(args): return parse_options(basedir, "backup", args)
 
         # test simple exclude
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude', doc_pattern_arg, '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude', doc_pattern_arg, 'from', 'to'])
         filtered = list(backup_options.filter_listdir(root_listdir))
         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
                               (nice_doc,))
         # multiple exclude
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude', doc_pattern_arg, '--exclude', 'lib.?', '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude', doc_pattern_arg, '--exclude', 'lib.?', 'from', 'to'])
         filtered = list(backup_options.filter_listdir(root_listdir))
         self._check_filtering(filtered, root_listdir, (u'_darcs', u'subdir'),
                              (nice_doc, u'lib.a'))
@@ -2824,17 +2754,13 @@ class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
         exclusion_string = doc_pattern_arg + "\nlib.?"
         excl_filepath = os.path.join(basedir, 'exclusion')
         fileutil.write(excl_filepath, exclusion_string)
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude-from', excl_filepath, '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude-from', excl_filepath, 'from', 'to'])
         filtered = list(backup_options.filter_listdir(root_listdir))
         self._check_filtering(filtered, root_listdir, (u'_darcs', u'subdir'),
                              (nice_doc, u'lib.a'))
 
         # test that an iterator works too
-        backup_options = cli.BackupOptions()
-        backup_options.parseOptions(['--exclude', doc_pattern_arg, '--node-directory',
-                                     basedir, 'from', 'to'])
+        backup_options = parse(['--exclude', doc_pattern_arg, 'from', 'to'])
         filtered = list(backup_options.filter_listdir(iter(root_listdir)))
         self._check_filtering(filtered, root_listdir, (u'lib.a', u'_darcs', u'subdir'),
                               (nice_doc,))
@@ -2845,14 +2771,13 @@ class Backup(GridTestMixin, CLITestMixin, StallMixin, unittest.TestCase):
         fileutil.make_dirs(basedir)
         nodeurl_path = os.path.join(basedir, 'node.url')
         fileutil.write(nodeurl_path, 'http://example.net:2357/')
+        def parse(args): return parse_options(basedir, "backup", args)
 
         # ensure that tilde expansion is performed on exclude-from argument
         exclude_file = u'~/.tahoe/excludes.dummy'
-        backup_options = cli.BackupOptions()
 
         mock.return_value = StringIO()
-        backup_options.parseOptions(['--exclude-from', unicode_to_argv(exclude_file),
-                                     '--node-directory', basedir, 'from', 'to'])
+        parse(['--exclude-from', unicode_to_argv(exclude_file), 'from', 'to'])
         self.failUnlessIn(((abspath_expanduser_unicode(exclude_file),), {}), mock.call_args_list)
 
     def test_ignore_symlinks(self):
@@ -3765,3 +3690,111 @@ class Webopen(GridTestMixin, CLITestMixin, unittest.TestCase):
             _cleanup(None)
             raise
         return d
+
+class Options(unittest.TestCase):
+    # this test case only looks at argument-processing and simple stuff.
+
+    def parse(self, args, stdout=None):
+        o = runner.Options()
+        if stdout is not None:
+            o.stdout = stdout
+        o.parseOptions(args)
+        while hasattr(o, "subOptions"):
+            o = o.subOptions
+        return o
+
+    def test_list(self):
+        fileutil.rm_dir("cli/test_options")
+        fileutil.make_dirs("cli/test_options")
+        fileutil.make_dirs("cli/test_options/private")
+        fileutil.write("cli/test_options/node.url", "http://localhost:8080/\n")
+        filenode_uri = uri.WriteableSSKFileURI(writekey="\x00"*16,
+                                               fingerprint="\x00"*32)
+        private_uri = uri.DirectoryURI(filenode_uri).to_string()
+        fileutil.write("cli/test_options/private/root_dir.cap", private_uri + "\n")
+        def parse2(args): return parse_options("cli/test_options", "ls", args)
+        o = parse2([])
+        self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
+        self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
+        self.failUnlessEqual(o.where, u"")
+
+        o = parse2(["--node-url", "http://example.org:8111/"])
+        self.failUnlessEqual(o['node-url'], "http://example.org:8111/")
+        self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
+        self.failUnlessEqual(o.where, u"")
+
+        o = parse2(["--dir-cap", "root"])
+        self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
+        self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], "root")
+        self.failUnlessEqual(o.where, u"")
+
+        other_filenode_uri = uri.WriteableSSKFileURI(writekey="\x11"*16,
+                                                     fingerprint="\x11"*32)
+        other_uri = uri.DirectoryURI(other_filenode_uri).to_string()
+        o = parse2(["--dir-cap", other_uri])
+        self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
+        self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
+        self.failUnlessEqual(o.where, u"")
+
+        o = parse2(["--dir-cap", other_uri, "subdir"])
+        self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
+        self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
+        self.failUnlessEqual(o.where, u"subdir")
+
+        self.failUnlessRaises(usage.UsageError, parse2,
+                              ["--node-url", "NOT-A-URL"])
+
+        o = parse2(["--node-url", "http://localhost:8080"])
+        self.failUnlessEqual(o["node-url"], "http://localhost:8080/")
+
+        o = parse2(["--node-url", "https://localhost/"])
+        self.failUnlessEqual(o["node-url"], "https://localhost/")
+
+    def test_version(self):
+        # "tahoe --version" dumps text to stdout and exits
+        stdout = StringIO()
+        self.failUnlessRaises(SystemExit, self.parse, ["--version"], stdout)
+        self.failUnlessIn("allmydata-tahoe", stdout.getvalue())
+        # but "tahoe SUBCOMMAND --version" should be rejected
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["start", "--version"])
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["start", "--version-and-path"])
+
+    def test_quiet(self):
+        # accepted as an overall option, but not on subcommands
+        o = self.parse(["--quiet", "start"])
+        self.failUnless(o.parent["quiet"])
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["start", "--quiet"])
+
+    def test_basedir(self):
+        # accept a --node-directory option before the verb, or a --basedir
+        # option after, or a basedir argument after, but none in the wrong
+        # place, and not more than one of the three.
+        o = self.parse(["start"])
+        self.failUnlessEqual(o["basedir"], os.path.expanduser("~/.tahoe"))
+        o = self.parse(["start", "here"])
+        self.failUnlessEqual(o["basedir"], os.path.abspath("here"))
+        o = self.parse(["start", "--basedir", "there"])
+        self.failUnlessEqual(o["basedir"], os.path.abspath("there"))
+        o = self.parse(["--node-directory", "there", "start"])
+        self.failUnlessEqual(o["basedir"], os.path.abspath("there"))
+
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["--basedir", "there", "start"])
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["start", "--node-directory", "there"])
+
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["--node-directory=there",
+                               "start", "--basedir=here"])
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["start", "--basedir=here", "anywhere"])
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["--node-directory=there",
+                               "start", "anywhere"])
+        self.failUnlessRaises(usage.UsageError, self.parse,
+                              ["--node-directory=there",
+                               "start", "--basedir=here", "anywhere"])
+
index 045f5c0e3900e6b6cc952ed19eb16ee249ab7194..98f2a528850d5212d8a89f326260c799c99dcb55 100644 (file)
@@ -759,8 +759,8 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
 
     def do_cli_manifest_stream1(self):
         basedir = self.get_clientdir(0)
-        d = self._run_cli(["manifest",
-                           "--node-directory", basedir,
+        d = self._run_cli(["--node-directory", basedir,
+                           "manifest",
                            self.root_uri])
         def _check((out,err)):
             self.failUnlessEqual(err, "")
@@ -787,8 +787,8 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
 
     def do_cli_manifest_stream2(self):
         basedir = self.get_clientdir(0)
-        d = self._run_cli(["manifest",
-                           "--node-directory", basedir,
+        d = self._run_cli(["--node-directory", basedir,
+                           "manifest",
                            "--raw",
                            self.root_uri])
         def _check((out,err)):
@@ -800,8 +800,8 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
 
     def do_cli_manifest_stream3(self):
         basedir = self.get_clientdir(0)
-        d = self._run_cli(["manifest",
-                           "--node-directory", basedir,
+        d = self._run_cli(["--node-directory", basedir,
+                           "manifest",
                            "--storage-index",
                            self.root_uri])
         def _check((out,err)):
@@ -812,8 +812,8 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
 
     def do_cli_manifest_stream4(self):
         basedir = self.get_clientdir(0)
-        d = self._run_cli(["manifest",
-                           "--node-directory", basedir,
+        d = self._run_cli(["--node-directory", basedir,
+                           "manifest",
                            "--verify-cap",
                            self.root_uri])
         def _check((out,err)):
@@ -828,8 +828,8 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
 
     def do_cli_manifest_stream5(self):
         basedir = self.get_clientdir(0)
-        d = self._run_cli(["manifest",
-                           "--node-directory", basedir,
+        d = self._run_cli(["--node-directory", basedir,
+                           "manifest",
                            "--repair-cap",
                            self.root_uri])
         def _check((out,err)):
@@ -844,8 +844,8 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
 
     def do_cli_stats1(self):
         basedir = self.get_clientdir(0)
-        d = self._run_cli(["stats",
-                           "--node-directory", basedir,
+        d = self._run_cli(["--node-directory", basedir,
+                           "stats",
                            self.root_uri])
         def _check3((out,err)):
             lines = [l.strip() for l in out.split("\n") if l]
@@ -864,8 +864,8 @@ class DeepCheckWebGood(DeepCheckBase, unittest.TestCase):
 
     def do_cli_stats2(self):
         basedir = self.get_clientdir(0)
-        d = self._run_cli(["stats",
-                           "--node-directory", basedir,
+        d = self._run_cli(["--node-directory", basedir,
+                           "stats",
                            "--raw",
                            self.root_uri])
         def _check4((out,err)):
index aed220e4b5fa326bb99a65a3279e9bb5085e99af..33974e2eb0390ef18002a1e47a6b9b2df58bc3c0 100644 (file)
@@ -284,7 +284,7 @@ class CreateNode(unittest.TestCase):
 
         # test the --node-directory form
         n3 = os.path.join(basedir, command + "-n3")
-        argv = ["--quiet", command, "--node-directory", n3]
+        argv = ["--quiet", "--node-directory", n3, command]
         rc, out, err = self.run_tahoe(argv)
         self.failUnlessEqual(err, "")
         self.failUnlessEqual(out, "")
index 6ef94b30842ddfa9ab679647dc21b229a93b3d6f..472f106df8cd3d0a3832f45e19ef0aaeb32f8627 100644 (file)
@@ -1440,7 +1440,7 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase):
 
         def run(ignored, verb, *args, **kwargs):
             stdin = kwargs.get("stdin", "")
-            newargs = [verb] + nodeargs + list(args)
+            newargs = nodeargs + [verb] + list(args)
             return self._run_cli(newargs, stdin=stdin)
 
         def _check_ls((out,err), expected_children, unexpected_children=[]):
@@ -1745,7 +1745,7 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase):
         # tahoe_ls doesn't currently handle the error correctly: it tries to
         # JSON-parse a traceback.
 ##         def _ls_missing(res):
-##             argv = ["ls"] + nodeargs + ["bogus"]
+##             argv = nodeargs + ["ls", "bogus"]
 ##             return self._run_cli(argv)
 ##         d.addCallback(_ls_missing)
 ##         def _check_ls_missing((out,err)):
@@ -1769,7 +1769,7 @@ class SystemTest(SystemTestMixin, RunBinTahoeMixin, unittest.TestCase):
         def _run_in_subprocess(ignored, verb, *args, **kwargs):
             stdin = kwargs.get("stdin")
             env = kwargs.get("env")
-            newargs = [verb, "--node-directory", self.getdir("client0")] + list(args)
+            newargs = ["--node-directory", self.getdir("client0"), verb] + list(args)
             return self.run_bintahoe(newargs, stdin=stdin, env=env)
 
         def _check_succeeded(res, check_stderr=True):