Merge branch 'globalopts2'

Improves CLI rendering of --help for global options. Closes ticket:2233.
This commit is contained in:
Brian Warner 2015-05-26 13:23:31 -07:00
commit 9d6003357d
9 changed files with 218 additions and 226 deletions

View File

@ -3,8 +3,6 @@ from twisted.python import usage
from allmydata.scripts.common import BaseOptions from allmydata.scripts.common import BaseOptions
class GenerateKeypairOptions(BaseOptions): class GenerateKeypairOptions(BaseOptions):
def getSynopsis(self):
return "Usage: tahoe [global-opts] admin generate-keypair"
def getUsage(self, width=None): def getUsage(self, width=None):
t = BaseOptions.getUsage(self, width) t = BaseOptions.getUsage(self, width)
@ -26,7 +24,7 @@ class DerivePubkeyOptions(BaseOptions):
self.privkey = privkey self.privkey = privkey
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] admin derive-pubkey PRIVKEY" return "Usage: tahoe [global-options] admin derive-pubkey PRIVKEY"
def getUsage(self, width=None): def getUsage(self, width=None):
t = BaseOptions.getUsage(self, width) t = BaseOptions.getUsage(self, width)
@ -57,7 +55,7 @@ class AdminCommand(BaseOptions):
if not hasattr(self, 'subOptions'): if not hasattr(self, 'subOptions'):
raise usage.UsageError("must specify a subcommand") raise usage.UsageError("must specify a subcommand")
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] admin SUBCOMMAND" return "Usage: tahoe [global-options] admin SUBCOMMAND"
def getUsage(self, width=None): def getUsage(self, width=None):
t = BaseOptions.getUsage(self, width) t = BaseOptions.getUsage(self, width)
t += """ t += """

View File

@ -57,10 +57,8 @@ class MakeDirectoryOptions(FilesystemOptions):
if self['format'].upper() not in ("SDMF", "MDMF"): if self['format'].upper() not in ("SDMF", "MDMF"):
raise usage.UsageError("%s is an invalid format" % self['format']) raise usage.UsageError("%s is an invalid format" % self['format'])
def getSynopsis(self): synopsis = "[options] [REMOTE_DIR]"
return "Usage: %s [global-opts] mkdir [options] [REMOTE_DIR]" % (self.command_name,) description = """Create a new directory, either unlinked or as a subdirectory."""
longdesc = """Create a new directory, either unlinked or as a subdirectory."""
class AddAliasOptions(FilesystemOptions): class AddAliasOptions(FilesystemOptions):
def parseArgs(self, alias, cap): def parseArgs(self, alias, cap):
@ -69,10 +67,8 @@ class AddAliasOptions(FilesystemOptions):
self.alias = self.alias[:-1] self.alias = self.alias[:-1]
self.cap = cap self.cap = cap
def getSynopsis(self): synopsis = "[options] ALIAS[:] DIRCAP"
return "Usage: %s [global-opts] add-alias [options] ALIAS[:] DIRCAP" % (self.command_name,) description = """Add a new alias for an existing directory."""
longdesc = """Add a new alias for an existing directory."""
class CreateAliasOptions(FilesystemOptions): class CreateAliasOptions(FilesystemOptions):
def parseArgs(self, alias): def parseArgs(self, alias):
@ -80,16 +76,12 @@ class CreateAliasOptions(FilesystemOptions):
if self.alias.endswith(u':'): if self.alias.endswith(u':'):
self.alias = self.alias[:-1] self.alias = self.alias[:-1]
def getSynopsis(self): synopsis = "[options] ALIAS[:]"
return "Usage: %s [global-opts] create-alias [options] ALIAS[:]" % (self.command_name,) description = """Create a new directory and add an alias for it."""
longdesc = """Create a new directory and add an alias for it."""
class ListAliasesOptions(FilesystemOptions): class ListAliasesOptions(FilesystemOptions):
def getSynopsis(self): synopsis = "[options]"
return "Usage: %s [global-opts] list-aliases [options]" % (self.command_name,) description = """Display a table of all configured aliases."""
longdesc = """Display a table of all configured aliases."""
class ListOptions(FilesystemOptions): class ListOptions(FilesystemOptions):
optFlags = [ optFlags = [
@ -102,10 +94,9 @@ class ListOptions(FilesystemOptions):
def parseArgs(self, where=""): def parseArgs(self, where=""):
self.where = argv_to_unicode(where) self.where = argv_to_unicode(where)
def getSynopsis(self): synopsis = "[options] [PATH]"
return "Usage: %s [global-opts] ls [options] [PATH]" % (self.command_name,)
longdesc = """ description = """
List the contents of some portion of the grid. List the contents of some portion of the grid.
If PATH is omitted, "tahoe:" is assumed. If PATH is omitted, "tahoe:" is assumed.
@ -113,7 +104,7 @@ class ListOptions(FilesystemOptions):
When the -l or --long option is used, each line is shown in the When the -l or --long option is used, each line is shown in the
following format: following format:
drwx <size> <date/time> <name in this directory> drwx <size> <date/time> <name in this directory>
where each of the letters on the left may be replaced by '-'. where each of the letters on the left may be replaced by '-'.
If 'd' is present, it indicates that the object is a directory. If 'd' is present, it indicates that the object is a directory.
@ -146,24 +137,20 @@ class GetOptions(FilesystemOptions):
self.from_file = argv_to_unicode(arg1) self.from_file = argv_to_unicode(arg1)
self.to_file = None if arg2 is None else argv_to_abspath(arg2) self.to_file = None if arg2 is None else argv_to_abspath(arg2)
def getSynopsis(self): synopsis = "[options] REMOTE_FILE LOCAL_FILE"
return "Usage: %s [global-opts] get [options] REMOTE_FILE LOCAL_FILE" % (self.command_name,)
longdesc = """ description = """
Retrieve a file from the grid and write it to the local filesystem. If Retrieve a file from the grid and write it to the local filesystem. If
LOCAL_FILE is omitted or '-', the contents of the file will be written to LOCAL_FILE is omitted or '-', the contents of the file will be written to
stdout.""" stdout."""
def getUsage(self, width=None): description_unwrapped = """
t = FilesystemOptions.getUsage(self, width) Examples:
t += """ % tahoe get FOO |less # write to stdout
Examples: % tahoe get tahoe:FOO |less # same
% tahoe get FOO |less # write to stdout % tahoe get FOO bar # write to local file
% tahoe get tahoe:FOO |less # same % tahoe get tahoe:FOO bar # same
% tahoe get FOO bar # write to local file """
% tahoe get tahoe:FOO bar # same
"""
return t
class PutOptions(FilesystemOptions): class PutOptions(FilesystemOptions):
optFlags = [ optFlags = [
@ -186,33 +173,30 @@ class PutOptions(FilesystemOptions):
if self['format'].upper() not in ("SDMF", "MDMF", "CHK"): if self['format'].upper() not in ("SDMF", "MDMF", "CHK"):
raise usage.UsageError("%s is an invalid format" % self['format']) raise usage.UsageError("%s is an invalid format" % self['format'])
def getSynopsis(self): synopsis = "[options] LOCAL_FILE REMOTE_FILE"
return "Usage: %s [global-opts] put [options] LOCAL_FILE REMOTE_FILE" % (self.command_name,)
longdesc = """ description = """
Put a file into the grid, copying its contents from the local filesystem. Put a file into the grid, copying its contents from the local filesystem.
If REMOTE_FILE is missing, upload the file but do not link it into a If REMOTE_FILE is missing, upload the file but do not link it into a
directory; also print the new filecap to stdout. If LOCAL_FILE is missing directory; also print the new filecap to stdout. If LOCAL_FILE is missing
or '-', data will be copied from stdin. REMOTE_FILE is assumed to start or '-', data will be copied from stdin. REMOTE_FILE is assumed to start
with tahoe: unless otherwise specified. with tahoe: unless otherwise specified.
If the destination file already exists and is mutable, it will be modified If the destination file already exists and is mutable, it will be
in-place, whether or not --mutable is specified. (--mutable only affects modified in-place, whether or not --mutable is specified. (--mutable only
creation of new files.)""" affects creation of new files.)
"""
def getUsage(self, width=None): description_unwrapped = """
t = FilesystemOptions.getUsage(self, width) Examples:
t += """ % cat FILE | tahoe put # create unlinked file from stdin
Examples: % cat FILE | tahoe put - # same
% cat FILE | tahoe put # create unlinked file from stdin % tahoe put bar # create unlinked file from local 'bar'
% cat FILE | tahoe put - # same % cat FILE | tahoe put - FOO # create tahoe:FOO from stdin
% tahoe put bar # create unlinked file from local 'bar' % tahoe put bar FOO # copy local 'bar' to tahoe:FOO
% cat FILE | tahoe put - FOO # create tahoe:FOO from stdin % tahoe put bar tahoe:FOO # same
% tahoe put bar FOO # copy local 'bar' to tahoe:FOO % tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
% tahoe put bar tahoe:FOO # same """
% tahoe put bar MUTABLE-FILE-WRITECAP # modify the mutable file in-place
"""
return t
class CpOptions(FilesystemOptions): class CpOptions(FilesystemOptions):
optFlags = [ optFlags = [
@ -229,10 +213,9 @@ class CpOptions(FilesystemOptions):
self.sources = map(argv_to_unicode, args[:-1]) self.sources = map(argv_to_unicode, args[:-1])
self.destination = argv_to_unicode(args[-1]) self.destination = argv_to_unicode(args[-1])
def getSynopsis(self): synopsis = "[options] FROM.. TO"
return "Usage: %s [global-opts] cp [options] FROM.. TO" % (self.command_name,)
longdesc = """ description = """
Use 'tahoe cp' to copy files between a local filesystem and a Tahoe grid. Use 'tahoe cp' to copy files between a local filesystem and a Tahoe grid.
Any FROM/TO arguments that begin with an alias indicate Tahoe-side Any FROM/TO arguments that begin with an alias indicate Tahoe-side
files or non-file arguments. Directories will be copied recursively. files or non-file arguments. Directories will be copied recursively.
@ -240,15 +223,15 @@ class CpOptions(FilesystemOptions):
you have previously set up an alias 'home' with 'tahoe create-alias home', you have previously set up an alias 'home' with 'tahoe create-alias home',
here are some examples: here are some examples:
tahoe cp ~/foo.txt home: # creates tahoe-side home:foo.txt tahoe cp ~/foo.txt home: # creates tahoe-side home:foo.txt
tahoe cp ~/foo.txt /tmp/bar.txt home: # copies two files to home: tahoe cp ~/foo.txt /tmp/bar.txt home: # copies two files to home:
tahoe cp ~/Pictures home:stuff/my-pictures # copies directory recursively tahoe cp ~/Pictures home:stuff/my-pictures # copies directory recursively
You can also use a dircap as either FROM or TO target: You can also use a dircap as either FROM or TO target:
tahoe cp URI:DIR2-RO:ixqhc4kdbjxc7o65xjnveoewym:5x6lwoxghrd5rxhwunzavft2qygfkt27oj3fbxlq4c6p45z5uneq/blog.html ./ # copy Zooko's wiki page to a local file tahoe cp URI:DIR2-RO:ixqhc4kdbjxc7o65xjnveoewym:5x6lwoxghrd5rxhwunzavft2qygfkt27oj3fbxlq4c6p45z5uneq/blog.html ./ # copy Zooko's wiki page to a local file
This command still has some limitations: symlinks and special files This command still has some limitations: symlinks and special files
(device nodes, named pipes) are not handled very well. Arguments should (device nodes, named pipes) are not handled very well. Arguments should
@ -266,22 +249,21 @@ class UnlinkOptions(FilesystemOptions):
def parseArgs(self, where): def parseArgs(self, where):
self.where = argv_to_unicode(where) self.where = argv_to_unicode(where)
def getSynopsis(self): synopsis = "[options] REMOTE_FILE"
return "Usage: %s [global-opts] unlink [options] REMOTE_FILE" % (self.command_name,) description = "Remove a named file from its parent directory."
class RmOptions(UnlinkOptions): class RmOptions(UnlinkOptions):
def getSynopsis(self): synopsis = "[options] REMOTE_FILE"
return "Usage: %s [global-opts] rm [options] REMOTE_FILE" % (self.command_name,) description = "Remove a named file from its parent directory."
class MvOptions(FilesystemOptions): class MvOptions(FilesystemOptions):
def parseArgs(self, frompath, topath): def parseArgs(self, frompath, topath):
self.from_file = argv_to_unicode(frompath) self.from_file = argv_to_unicode(frompath)
self.to_file = argv_to_unicode(topath) self.to_file = argv_to_unicode(topath)
def getSynopsis(self): synopsis = "[options] FROM TO"
return "Usage: %s [global-opts] mv [options] FROM TO" % (self.command_name,)
longdesc = """ description = """
Use 'tahoe mv' to move files that are already on the grid elsewhere on Use 'tahoe mv' to move files that are already on the grid elsewhere on
the grid, e.g., 'tahoe mv alias:some_file alias:new_file'. the grid, e.g., 'tahoe mv alias:some_file alias:new_file'.
@ -298,10 +280,9 @@ class LnOptions(FilesystemOptions):
self.from_file = argv_to_unicode(frompath) self.from_file = argv_to_unicode(frompath)
self.to_file = argv_to_unicode(topath) self.to_file = argv_to_unicode(topath)
def getSynopsis(self): synopsis = "[options] FROM_LINK TO_LINK"
return "Usage: %s [global-opts] ln [options] FROM_LINK TO_LINK" % (self.command_name,)
longdesc = """ description = """
Use 'tahoe ln' to duplicate a link (directory entry) already on the grid Use 'tahoe ln' to duplicate a link (directory entry) already on the grid
to elsewhere on the grid. For example 'tahoe ln alias:some_file to elsewhere on the grid. For example 'tahoe ln alias:some_file
alias:new_file'. causes 'alias:new_file' to point to the same object that alias:new_file'. causes 'alias:new_file' to point to the same object that
@ -345,8 +326,7 @@ class BackupOptions(FilesystemOptions):
self.from_dir = argv_to_abspath(localdir) self.from_dir = argv_to_abspath(localdir)
self.to_dir = argv_to_unicode(topath) self.to_dir = argv_to_unicode(topath)
def getSynopsis(self): synopsis = "[options] FROM ALIAS:TO"
return "Usage: %s [global-opts] backup [options] FROM ALIAS:TO" % (self.command_name,)
def opt_exclude(self, pattern): def opt_exclude(self, pattern):
"""Ignore files matching a glob pattern. You may give multiple """Ignore files matching a glob pattern. You may give multiple
@ -388,7 +368,7 @@ class BackupOptions(FilesystemOptions):
else: else:
yield filename yield filename
longdesc = """ description = """
Add a versioned backup of the local FROM directory to a timestamped Add a versioned backup of the local FROM directory to a timestamped
subdirectory of the TO/Archives directory on the grid, sharing as many subdirectory of the TO/Archives directory on the grid, sharing as many
files and directories as possible with earlier backups. Create TO/Latest files and directories as possible with earlier backups. Create TO/Latest
@ -403,10 +383,10 @@ class WebopenOptions(FilesystemOptions):
def parseArgs(self, where=''): def parseArgs(self, where=''):
self.where = argv_to_unicode(where) self.where = argv_to_unicode(where)
def getSynopsis(self): synopsis = "[options] [ALIAS:PATH]"
return "Usage: %s [global-opts] webopen [options] [ALIAS:PATH]" % (self.command_name,)
longdesc = """Open a web browser to the contents of some file or description = """
Open a web browser to the contents of some file or
directory on the grid. When run without arguments, open the Welcome directory on the grid. When run without arguments, open the Welcome
page.""" page."""
@ -420,11 +400,10 @@ class ManifestOptions(FilesystemOptions):
def parseArgs(self, where=''): def parseArgs(self, where=''):
self.where = argv_to_unicode(where) self.where = argv_to_unicode(where)
def getSynopsis(self): synopsis = "[options] [ALIAS:PATH]"
return "Usage: %s [global-opts] manifest [options] [ALIAS:PATH]" % (self.command_name,) description = """
Print a list of all files and directories reachable from the given
longdesc = """Print a list of all files and directories reachable from starting point."""
the given starting point."""
class StatsOptions(FilesystemOptions): class StatsOptions(FilesystemOptions):
optFlags = [ optFlags = [
@ -433,11 +412,10 @@ class StatsOptions(FilesystemOptions):
def parseArgs(self, where=''): def parseArgs(self, where=''):
self.where = argv_to_unicode(where) self.where = argv_to_unicode(where)
def getSynopsis(self): synopsis = "[options] [ALIAS:PATH]"
return "Usage: %s [global-opts] stats [options] [ALIAS:PATH]" % (self.command_name,) description = """
Print statistics about of all files and directories reachable from the
longdesc = """Print statistics about of all files and directories given starting point."""
reachable from the given starting point."""
class CheckOptions(FilesystemOptions): class CheckOptions(FilesystemOptions):
optFlags = [ optFlags = [
@ -449,10 +427,8 @@ class CheckOptions(FilesystemOptions):
def parseArgs(self, *locations): def parseArgs(self, *locations):
self.locations = map(argv_to_unicode, locations) self.locations = map(argv_to_unicode, locations)
def getSynopsis(self): synopsis = "[options] [ALIAS:PATH]"
return "Usage: %s [global-opts] check [options] [ALIAS:PATH]" % (self.command_name,) description = """
longdesc = """
Check a single file or directory: count how many shares are available and Check a single file or directory: count how many shares are available and
verify their hashes. Optionally repair the file if any problems were verify their hashes. Optionally repair the file if any problems were
found.""" found."""
@ -468,10 +444,8 @@ class DeepCheckOptions(FilesystemOptions):
def parseArgs(self, *locations): def parseArgs(self, *locations):
self.locations = map(argv_to_unicode, locations) self.locations = map(argv_to_unicode, locations)
def getSynopsis(self): synopsis = "[options] [ALIAS:PATH]"
return "Usage: %s [global-opts] deep-check [options] [ALIAS:PATH]" % (self.command_name,) description = """
longdesc = """
Check all files and directories reachable from the given starting point Check all files and directories reachable from the given starting point
(which must be a directory), like 'tahoe check' but for multiple files. (which must be a directory), like 'tahoe check' but for multiple files.
Optionally repair any problems found.""" Optionally repair any problems found."""

View File

@ -1,29 +1,23 @@
import os, sys, urllib import os, sys, urllib, textwrap
import codecs import codecs
from twisted.python import usage from twisted.python import usage
from allmydata.util.assertutil import precondition from allmydata.util.assertutil import precondition
from allmydata.util.encodingutil import unicode_to_url, quote_output, \ from allmydata.util.encodingutil import unicode_to_url, quote_output, \
quote_local_unicode_path, argv_to_abspath quote_local_unicode_path, argv_to_abspath
from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.scripts.default_nodedir import _default_nodedir
_default_nodedir = None
if sys.platform == 'win32':
from allmydata.windows import registry
path = registry.get_base_dir_path()
if path:
precondition(isinstance(path, unicode), path)
_default_nodedir = abspath_expanduser_unicode(path)
if _default_nodedir is None:
path = abspath_expanduser_unicode(u"~/.tahoe")
precondition(isinstance(path, unicode), path)
_default_nodedir = path
def get_default_nodedir(): def get_default_nodedir():
return _default_nodedir return _default_nodedir
def wrap_paragraphs(text, width):
# like textwrap.wrap(), but preserve paragraphs (delimited by double
# newlines) and leading whitespace, and remove internal whitespace.
text = textwrap.dedent(text)
if text.startswith("\n"):
text = text[1:]
return "\n\n".join([textwrap.fill(paragraph, width=width)
for paragraph in text.split("\n\n")])
class BaseOptions(usage.Options): class BaseOptions(usage.Options):
def __init__(self): def __init__(self):
@ -36,6 +30,24 @@ class BaseOptions(usage.Options):
def opt_version(self): def opt_version(self):
raise usage.UsageError("--version not allowed on subcommands") raise usage.UsageError("--version not allowed on subcommands")
description = None
description_unwrapped = None
def __str__(self):
width = int(os.environ.get('COLUMNS', '80'))
s = (self.getSynopsis() + '\n' +
"(use 'tahoe --help' to view global options)\n" +
'\n' +
self.getUsage())
if self.description:
s += '\n' + wrap_paragraphs(self.description, width) + '\n'
if self.description_unwrapped:
du = textwrap.dedent(self.description_unwrapped)
if du.startswith("\n"):
du = du[1:]
s += '\n' + du + '\n'
return s
class BasedirOptions(BaseOptions): class BasedirOptions(BaseOptions):
default_nodedir = _default_nodedir default_nodedir = _default_nodedir
@ -81,7 +93,7 @@ class NoDefaultBasedirOptions(BasedirOptions):
BasedirOptions.parseArgs(self, basedir) BasedirOptions.parseArgs(self, basedir)
def getSynopsis(self): def getSynopsis(self):
return "Usage: %s [global-opts] %s [options] NODEDIR" % (self.command_name, self.subcommand_name) return "Usage: %s [global-options] %s [options] NODEDIR" % (self.command_name, self.subcommand_name)
DEFAULT_ALIAS = u"tahoe" DEFAULT_ALIAS = u"tahoe"

View File

@ -1,11 +1,12 @@
import os, sys import os, sys
from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions
from allmydata.scripts.default_nodedir import _default_nodedir
from allmydata.util.assertutil import precondition from allmydata.util.assertutil import precondition
from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_output from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_output, quote_local_unicode_path
import allmydata import allmydata
class CreateClientOptions(BasedirOptions): class _CreateBaseOptions(BasedirOptions):
optParameters = [ optParameters = [
# we provide 'create-node'-time options for the most common # we provide 'create-node'-time options for the most common
# configuration knobs. The rest can be controlled by editing # configuration knobs. The rest can be controlled by editing
@ -14,28 +15,30 @@ class CreateClientOptions(BasedirOptions):
("introducer", "i", None, "Specify the introducer FURL to use."), ("introducer", "i", None, "Specify the introducer FURL to use."),
("webport", "p", "tcp:3456:interface=127.0.0.1", ("webport", "p", "tcp:3456:interface=127.0.0.1",
"Specify which TCP port to run the HTTP interface on. Use 'none' to disable."), "Specify which TCP port to run the HTTP interface on. Use 'none' to disable."),
("basedir", "C", None, "Specify which Tahoe base directory should be used. This has the same effect as the global --node-directory option. [default: %s]"
% quote_local_unicode_path(_default_nodedir)),
] ]
def getSynopsis(self): # This is overridden in order to ensure we get a "Wrong number of
return "Usage: %s [global-opts] create-client [options] [NODEDIR]" % (self.command_name,) # arguments." error when more than one argument is given.
# This is overridden in order to ensure we get a "Wrong number of arguments."
# error when more than one argument is given.
def parseArgs(self, basedir=None): def parseArgs(self, basedir=None):
BasedirOptions.parseArgs(self, basedir) BasedirOptions.parseArgs(self, basedir)
class CreateClientOptions(_CreateBaseOptions):
synopsis = "[options] [NODEDIR]"
description = "Create a client-only Tahoe-LAFS node (no storage server)."
class CreateNodeOptions(CreateClientOptions): class CreateNodeOptions(CreateClientOptions):
optFlags = [ optFlags = [
("no-storage", None, "Do not offer storage service to other nodes."), ("no-storage", None, "Do not offer storage service to other nodes."),
] ]
synopsis = "[options] [NODEDIR]"
def getSynopsis(self): description = "Create a full Tahoe-LAFS node (client+server)."
return "Usage: %s [global-opts] create-node [options] [NODEDIR]" % (self.command_name,)
class CreateIntroducerOptions(NoDefaultBasedirOptions): class CreateIntroducerOptions(NoDefaultBasedirOptions):
subcommand_name = "create-introducer" subcommand_name = "create-introducer"
description = "Create a Tahoe-LAFS introducer."
client_tac = """ client_tac = """

View File

@ -11,25 +11,21 @@ from allmydata.scripts.common import BaseOptions
class DumpOptions(BaseOptions): class DumpOptions(BaseOptions):
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug dump-share SHARE_FILENAME" return "Usage: tahoe [global-options] debug dump-share SHARE_FILENAME"
optFlags = [ optFlags = [
["offsets", None, "Display a table of section offsets."], ["offsets", None, "Display a table of section offsets."],
["leases-only", None, "Dump leases but not CHK contents."], ["leases-only", None, "Dump leases but not CHK contents."],
] ]
def getUsage(self, width=None): description = """
t = BaseOptions.getUsage(self, width)
t += """
Print lots of information about the given share, by parsing the share's Print lots of information about the given share, by parsing the share's
contents. This includes share type, lease information, encoding parameters, contents. This includes share type, lease information, encoding parameters,
hash-tree roots, public keys, and segment sizes. This command also emits a hash-tree roots, public keys, and segment sizes. This command also emits a
verify-cap for the file that uses the share. verify-cap for the file that uses the share.
tahoe debug dump-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0 tahoe debug dump-share testgrid/node-3/storage/shares/4v/4vozh77tsrw7mdhnj7qvp5ky74/0
""" """
return t
def parseArgs(self, filename): def parseArgs(self, filename):
from allmydata.util.encodingutil import argv_to_abspath from allmydata.util.encodingutil import argv_to_abspath
@ -408,7 +404,7 @@ def dump_MDMF_share(m, length, options):
class DumpCapOptions(BaseOptions): class DumpCapOptions(BaseOptions):
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug dump-cap [options] FILECAP" return "Usage: tahoe [global-options] debug dump-cap [options] FILECAP"
optParameters = [ optParameters = [
["nodeid", "n", ["nodeid", "n",
None, "Specify the storage server nodeid (ASCII), to construct WE and secrets."], None, "Specify the storage server nodeid (ASCII), to construct WE and secrets."],
@ -420,9 +416,7 @@ class DumpCapOptions(BaseOptions):
def parseArgs(self, cap): def parseArgs(self, cap):
self.cap = cap self.cap = cap
def getUsage(self, width=None): description = """
t = BaseOptions.getUsage(self, width)
t += """
Print information about the given cap-string (aka: URI, file-cap, dir-cap, 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 read-cap, write-cap). The URI string is parsed and unpacked. This prints the
type of the cap, its storage index, and any derived keys. type of the cap, its storage index, and any derived keys.
@ -437,7 +431,6 @@ If additional information is provided (storage server nodeid and/or client
base secret), this command will compute the shared secrets used for the base secret), this command will compute the shared secrets used for the
write-enabler and for lease-renewal. write-enabler and for lease-renewal.
""" """
return t
def dump_cap(options): def dump_cap(options):
@ -610,16 +603,14 @@ def dump_uri_instance(u, nodeid, secret, out, show_header=True):
class FindSharesOptions(BaseOptions): class FindSharesOptions(BaseOptions):
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug find-shares STORAGE_INDEX NODEDIRS.." return "Usage: tahoe [global-options] debug find-shares STORAGE_INDEX NODEDIRS.."
def parseArgs(self, storage_index_s, *nodedirs): def parseArgs(self, storage_index_s, *nodedirs):
from allmydata.util.encodingutil import argv_to_abspath from allmydata.util.encodingutil import argv_to_abspath
self.si_s = storage_index_s self.si_s = storage_index_s
self.nodedirs = map(argv_to_abspath, nodedirs) self.nodedirs = map(argv_to_abspath, nodedirs)
def getUsage(self, width=None): description = """
t = BaseOptions.getUsage(self, width)
t += """
Locate all shares for the given storage index. This command looks through one 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, or more node directories to find the shares. It returns a list of filenames,
one per line, for each share file found. one per line, for each share file found.
@ -630,7 +621,6 @@ It may be useful during testing, when running a test grid in which all the
nodes are on a local disk. The share files thus located can be counted, nodes are on a local disk. The share files thus located can be counted,
examined (with dump-share), or corrupted/deleted to test checker/repairer. examined (with dump-share), or corrupted/deleted to test checker/repairer.
""" """
return t
def find_shares(options): def find_shares(options):
"""Given a storage index and a list of node directories, emit a list of """Given a storage index and a list of node directories, emit a list of
@ -659,9 +649,6 @@ def find_shares(options):
class CatalogSharesOptions(BaseOptions): class CatalogSharesOptions(BaseOptions):
"""
"""
def parseArgs(self, *nodedirs): def parseArgs(self, *nodedirs):
from allmydata.util.encodingutil import argv_to_abspath from allmydata.util.encodingutil import argv_to_abspath
self.nodedirs = map(argv_to_abspath, nodedirs) self.nodedirs = map(argv_to_abspath, nodedirs)
@ -669,11 +656,9 @@ class CatalogSharesOptions(BaseOptions):
raise usage.UsageError("must specify at least one node directory") raise usage.UsageError("must specify at least one node directory")
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug catalog-shares NODEDIRS.." return "Usage: tahoe [global-options] debug catalog-shares NODEDIRS.."
def getUsage(self, width=None): description = """
t = BaseOptions.getUsage(self, width)
t += """
Locate all shares in the given node directories, and emit a one-line summary Locate all shares in the given node directories, and emit a one-line summary
of each share. Run it like this: of each share. Run it like this:
@ -691,7 +676,6 @@ you see shares with the same SI but different parameters/filesize/UEB_hash,
then something is wrong. The misc/find-share/anomalies.py script may be then something is wrong. The misc/find-share/anomalies.py script may be
useful for purpose. useful for purpose.
""" """
return t
def call(c, *args, **kwargs): def call(c, *args, **kwargs):
# take advantage of the fact that ImmediateReadBucketProxy returns # take advantage of the fact that ImmediateReadBucketProxy returns
@ -882,15 +866,13 @@ def catalog_shares_one_abbrevdir(si_s, si_dir, now, out, err):
class CorruptShareOptions(BaseOptions): class CorruptShareOptions(BaseOptions):
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug corrupt-share SHARE_FILENAME" return "Usage: tahoe [global-options] debug corrupt-share SHARE_FILENAME"
optParameters = [ optParameters = [
["offset", "o", "block-random", "Specify which bit to flip."], ["offset", "o", "block-random", "Specify which bit to flip."],
] ]
def getUsage(self, width=None): description = """
t = BaseOptions.getUsage(self, width)
t += """
Corrupt the given share by flipping a bit. This will cause a Corrupt the given share by flipping a bit. This will cause a
verifying/downloading client to log an integrity-check failure incident, and verifying/downloading client to log an integrity-check failure incident, and
downloads will proceed with a different share. downloads will proceed with a different share.
@ -902,7 +884,6 @@ to flip a single random bit of the block data.
Obviously, this command should not be used in normal operation. Obviously, this command should not be used in normal operation.
""" """
return t
def parseArgs(self, filename): def parseArgs(self, filename):
self['filename'] = filename self['filename'] = filename
@ -962,7 +943,7 @@ def corrupt_share(options):
class ReplOptions(BaseOptions): class ReplOptions(BaseOptions):
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug repl" return "Usage: tahoe [global-options] debug repl"
def repl(options): def repl(options):
import code import code
@ -973,7 +954,7 @@ DEFAULT_TESTSUITE = 'allmydata'
class TrialOptions(twisted_trial.Options): class TrialOptions(twisted_trial.Options):
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug trial [options] [[file|package|module|TestCase|testmethod]...]" return "Usage: tahoe [global-options] debug trial [options] [[file|package|module|TestCase|testmethod]...]"
def parseOptions(self, all_subargs, *a, **kw): def parseOptions(self, all_subargs, *a, **kw):
self.trial_args = list(all_subargs) self.trial_args = list(all_subargs)
@ -985,13 +966,10 @@ class TrialOptions(twisted_trial.Options):
if not nonoption_args: if not nonoption_args:
self.trial_args.append(DEFAULT_TESTSUITE) self.trial_args.append(DEFAULT_TESTSUITE)
def getUsage(self, width=None): longdesc = twisted_trial.Options.longdesc + "\n\n" + (
t = twisted_trial.Options.getUsage(self, width) "The 'tahoe debug trial' command uses the correct imports for this "
t += """ "instance of Tahoe-LAFS. The default test suite is '%s'."
The 'tahoe debug trial' command uses the correct imports for this instance of % DEFAULT_TESTSUITE)
Tahoe-LAFS. The default test suite is '%s'.
""" % (DEFAULT_TESTSUITE,)
return t
def trial(config): def trial(config):
sys.argv = ['trial'] + config.trial_args sys.argv = ['trial'] + config.trial_args
@ -1014,9 +992,9 @@ def fixOptionsClass( (subcmd, shortcut, OptionsClass, desc) ):
t = OptionsClass.getSynopsis(self) t = OptionsClass.getSynopsis(self)
i = t.find("Usage: flogtool ") i = t.find("Usage: flogtool ")
if i >= 0: if i >= 0:
return "Usage: tahoe [global-opts] debug flogtool " + t[i+len("Usage: flogtool "):] return "Usage: tahoe [global-options] debug flogtool " + t[i+len("Usage: flogtool "):]
else: else:
return "Usage: tahoe [global-opts] debug flogtool %s [options]" % (subcmd,) return "Usage: tahoe [global-options] debug flogtool %s [options]" % (subcmd,)
return (subcmd, shortcut, FixedOptionsClass, desc) return (subcmd, shortcut, FixedOptionsClass, desc)
class FlogtoolOptions(foolscap_cli.Options): class FlogtoolOptions(foolscap_cli.Options):
@ -1025,7 +1003,7 @@ class FlogtoolOptions(foolscap_cli.Options):
self.subCommands = map(fixOptionsClass, self.subCommands) self.subCommands = map(fixOptionsClass, self.subCommands)
def getSynopsis(self): def getSynopsis(self):
return "Usage: tahoe [global-opts] debug flogtool (%s) [command options]" % ("|".join([x[0] for x in self.subCommands])) return "Usage: tahoe [global-options] debug flogtool COMMAND [flogtool-options]"
def parseOptions(self, all_subargs, *a, **kw): def parseOptions(self, all_subargs, *a, **kw):
self.flogtool_args = list(all_subargs) self.flogtool_args = list(all_subargs)
@ -1037,7 +1015,7 @@ class FlogtoolOptions(foolscap_cli.Options):
The 'tahoe debug flogtool' command uses the correct imports for this instance The 'tahoe debug flogtool' command uses the correct imports for this instance
of Tahoe-LAFS. of Tahoe-LAFS.
Please run 'tahoe debug flogtool SUBCOMMAND --help' for more details on each Please run 'tahoe debug flogtool COMMAND --help' for more details on each
subcommand. subcommand.
""" """
return t return t
@ -1066,20 +1044,11 @@ class DebugCommand(BaseOptions):
def postOptions(self): def postOptions(self):
if not hasattr(self, 'subOptions'): if not hasattr(self, 'subOptions'):
raise usage.UsageError("must specify a subcommand") raise usage.UsageError("must specify a subcommand")
def getSynopsis(self): synopsis = "COMMAND"
return ""
def getUsage(self, width=None): def getUsage(self, width=None):
#t = BaseOptions.getUsage(self, width) t = BaseOptions.getUsage(self, width)
t = """Usage: tahoe debug SUBCOMMAND t += """\
Subcommands:
tahoe debug dump-share Unpack and display the contents of a share.
tahoe debug dump-cap Unpack a read-cap or write-cap.
tahoe debug find-shares Locate sharefiles in node directories.
tahoe debug catalog-shares Describe all shares in node dirs.
tahoe debug corrupt-share Corrupt a share by flipping a bit.
tahoe debug repl Open a Python interpreter.
tahoe debug trial Run tests using Twisted Trial with the right imports.
tahoe debug flogtool Utilities to access log files.
Please run e.g. 'tahoe debug dump-share --help' for more details on each Please run e.g. 'tahoe debug dump-share --help' for more details on each
subcommand. subcommand.

View File

@ -0,0 +1,17 @@
import sys
from allmydata.util.assertutil import precondition
from allmydata.util.fileutil import abspath_expanduser_unicode
_default_nodedir = None
if sys.platform == 'win32':
from allmydata.windows import registry
path = registry.get_base_dir_path()
if path:
precondition(isinstance(path, unicode), path)
_default_nodedir = abspath_expanduser_unicode(path)
if _default_nodedir is None:
path = abspath_expanduser_unicode(u"~/.tahoe")
precondition(isinstance(path, unicode), path)
_default_nodedir = path

View File

@ -12,7 +12,7 @@ def GROUP(s):
# Usage.parseOptions compares argv[1] against command[0], so it will # Usage.parseOptions compares argv[1] against command[0], so it will
# effectively ignore any "subcommand" that starts with a newline. We use # effectively ignore any "subcommand" that starts with a newline. We use
# these to insert section headers into the --help output. # these to insert section headers into the --help output.
return [("\n" + s, None, None, None)] return [("\n(%s)" % s, None, None, None)]
_default_nodedir = get_default_nodedir() _default_nodedir = get_default_nodedir()
@ -66,11 +66,15 @@ class Options(usage.Options):
print >>self.stdout, allmydata.get_package_versions_string(show_paths=True, debug=True) print >>self.stdout, allmydata.get_package_versions_string(show_paths=True, debug=True)
self.no_command_needed = True self.no_command_needed = True
def getSynopsis(self): def __str__(self):
return "\nUsage: tahoe [global-opts] <command> [command-options]" return ("\nUsage: tahoe [global-options] <command> [command-options]\n"
+ self.getUsage())
synopsis = "\nUsage: tahoe [global-options]" # used only for subcommands
def getUsage(self, **kwargs): def getUsage(self, **kwargs):
t = usage.Options.getUsage(self, **kwargs) t = usage.Options.getUsage(self, **kwargs)
t = t.replace("Options:", "\nGlobal options:", 1)
return t + "\nPlease run 'tahoe <command> --help' for more details on each command.\n" return t + "\nPlease run 'tahoe <command> --help' for more details on each command.\n"
def postOptions(self): def postOptions(self):

View File

@ -3,25 +3,34 @@ import os, sys, signal, time
from allmydata.scripts.common import BasedirOptions from allmydata.scripts.common import BasedirOptions
from twisted.scripts import twistd from twisted.scripts import twistd
from twisted.python import usage from twisted.python import usage
from allmydata.scripts.default_nodedir import _default_nodedir
from allmydata.util import fileutil from allmydata.util import fileutil
from allmydata.util.encodingutil import listdir_unicode, quote_local_unicode_path from allmydata.util.encodingutil import listdir_unicode, quote_local_unicode_path
class StartOptions(BasedirOptions): class StartOptions(BasedirOptions):
subcommand_name = "start" subcommand_name = "start"
optParameters = [
("basedir", "C", None,
"Specify which Tahoe base directory should be used."
" This has the same effect as the global --node-directory option."
" [default: %s]" % quote_local_unicode_path(_default_nodedir)),
]
def parseArgs(self, basedir=None, *twistd_args): def parseArgs(self, basedir=None, *twistd_args):
# This can't handle e.g. 'tahoe start --nodaemon', since '--nodaemon' # This can't handle e.g. 'tahoe start --nodaemon', since '--nodaemon'
# looks like an option to the tahoe subcommand, not to twistd. # looks like an option to the tahoe subcommand, not to twistd. So you
# So you can either use 'tahoe start' or 'tahoe start NODEDIR --TWISTD-OPTIONS'. # can either use 'tahoe start' or 'tahoe start NODEDIR
# Note that 'tahoe --node-directory=NODEDIR start --TWISTD-OPTIONS' also # --TWISTD-OPTIONS'. Note that 'tahoe --node-directory=NODEDIR start
# isn't allowed, unfortunately. # --TWISTD-OPTIONS' also isn't allowed, unfortunately.
BasedirOptions.parseArgs(self, basedir) BasedirOptions.parseArgs(self, basedir)
self.twistd_args = twistd_args self.twistd_args = twistd_args
def getSynopsis(self): def getSynopsis(self):
return "Usage: %s [global-opts] %s [options] [NODEDIR [twistd-options]]" % (self.command_name, self.subcommand_name) return ("Usage: %s [global-options] %s [options]"
" [NODEDIR [twistd-options]]"
% (self.command_name, self.subcommand_name))
def getUsage(self, width=None): def getUsage(self, width=None):
t = BasedirOptions.getUsage(self, width) + "\n" t = BasedirOptions.getUsage(self, width) + "\n"
@ -40,7 +49,8 @@ class StopOptions(BasedirOptions):
BasedirOptions.parseArgs(self, basedir) BasedirOptions.parseArgs(self, basedir)
def getSynopsis(self): def getSynopsis(self):
return "Usage: %s [global-opts] stop [options] [NODEDIR]" % (self.command_name,) return ("Usage: %s [global-options] stop [options] [NODEDIR]"
% (self.command_name,))
class RestartOptions(StartOptions): class RestartOptions(StartOptions):
subcommand_name = "restart" subcommand_name = "restart"
@ -230,9 +240,9 @@ def restart(config, stdout, stderr):
def run(config, stdout, stderr): def run(config, stdout, stderr):
config.twistd_args = config.twistd_args + ("--nodaemon",) config.twistd_args = config.twistd_args + ("--nodaemon",)
# Previously we would do the equivalent of adding ("--logfile", "tahoesvc.log"), # Previously we would do the equivalent of adding ("--logfile",
# but that redirects stdout/stderr which is often unhelpful, and the user can # "tahoesvc.log"), but that redirects stdout/stderr which is often
# add that option explicitly if they want. # unhelpful, and the user can add that option explicitly if they want.
return start(config, stdout, stderr) return start(config, stdout, stderr)

View File

@ -561,126 +561,131 @@ class CLI(CLITestMixin, unittest.TestCase):
class Help(unittest.TestCase): class Help(unittest.TestCase):
def failUnlessInNormalized(self, x, y):
# helper function to deal with the --help output being wrapped to
# various widths, depending on the $COLUMNS environment variable
self.failUnlessIn(x.replace("\n", " "), y.replace("\n", " "))
def test_get(self): def test_get(self):
help = str(cli.GetOptions()) help = str(cli.GetOptions())
self.failUnlessIn(" [global-opts] get [options] REMOTE_FILE LOCAL_FILE", help) self.failUnlessIn("[options] REMOTE_FILE LOCAL_FILE", help)
self.failUnlessIn("% tahoe get FOO |less", help) self.failUnlessIn("% tahoe get FOO |less", help)
def test_put(self): def test_put(self):
help = str(cli.PutOptions()) help = str(cli.PutOptions())
self.failUnlessIn(" [global-opts] put [options] LOCAL_FILE REMOTE_FILE", help) self.failUnlessIn("[options] LOCAL_FILE REMOTE_FILE", help)
self.failUnlessIn("% cat FILE | tahoe put", help) self.failUnlessIn("% cat FILE | tahoe put", help)
def test_ls(self): def test_ls(self):
help = str(cli.ListOptions()) help = str(cli.ListOptions())
self.failUnlessIn(" [global-opts] ls [options] [PATH]", help) self.failUnlessIn("[options] [PATH]", help)
def test_unlink(self): def test_unlink(self):
help = str(cli.UnlinkOptions()) help = str(cli.UnlinkOptions())
self.failUnlessIn(" [global-opts] unlink [options] REMOTE_FILE", help) self.failUnlessIn("[options] REMOTE_FILE", help)
def test_rm(self): def test_rm(self):
help = str(cli.RmOptions()) help = str(cli.RmOptions())
self.failUnlessIn(" [global-opts] rm [options] REMOTE_FILE", help) self.failUnlessIn("[options] REMOTE_FILE", help)
def test_mv(self): def test_mv(self):
help = str(cli.MvOptions()) help = str(cli.MvOptions())
self.failUnlessIn(" [global-opts] mv [options] FROM TO", help) self.failUnlessIn("[options] FROM TO", help)
self.failUnlessIn("Use 'tahoe mv' to move files", help) self.failUnlessInNormalized("Use 'tahoe mv' to move files", help)
def test_cp(self): def test_cp(self):
help = str(cli.CpOptions()) help = str(cli.CpOptions())
self.failUnlessIn(" [global-opts] cp [options] FROM.. TO", help) self.failUnlessIn("[options] FROM.. TO", help)
self.failUnlessIn("Use 'tahoe cp' to copy files", help) self.failUnlessInNormalized("Use 'tahoe cp' to copy files", help)
def test_ln(self): def test_ln(self):
help = str(cli.LnOptions()) help = str(cli.LnOptions())
self.failUnlessIn(" [global-opts] ln [options] FROM_LINK TO_LINK", help) self.failUnlessIn("[options] FROM_LINK TO_LINK", help)
self.failUnlessIn("Use 'tahoe ln' to duplicate a link", help) self.failUnlessInNormalized("Use 'tahoe ln' to duplicate a link", help)
def test_mkdir(self): def test_mkdir(self):
help = str(cli.MakeDirectoryOptions()) help = str(cli.MakeDirectoryOptions())
self.failUnlessIn(" [global-opts] mkdir [options] [REMOTE_DIR]", help) self.failUnlessIn("[options] [REMOTE_DIR]", help)
self.failUnlessIn("Create a new directory", help) self.failUnlessInNormalized("Create a new directory", help)
def test_backup(self): def test_backup(self):
help = str(cli.BackupOptions()) help = str(cli.BackupOptions())
self.failUnlessIn(" [global-opts] backup [options] FROM ALIAS:TO", help) self.failUnlessIn("[options] FROM ALIAS:TO", help)
def test_webopen(self): def test_webopen(self):
help = str(cli.WebopenOptions()) help = str(cli.WebopenOptions())
self.failUnlessIn(" [global-opts] webopen [options] [ALIAS:PATH]", help) self.failUnlessIn("[options] [ALIAS:PATH]", help)
def test_manifest(self): def test_manifest(self):
help = str(cli.ManifestOptions()) help = str(cli.ManifestOptions())
self.failUnlessIn(" [global-opts] manifest [options] [ALIAS:PATH]", help) self.failUnlessIn("[options] [ALIAS:PATH]", help)
def test_stats(self): def test_stats(self):
help = str(cli.StatsOptions()) help = str(cli.StatsOptions())
self.failUnlessIn(" [global-opts] stats [options] [ALIAS:PATH]", help) self.failUnlessIn("[options] [ALIAS:PATH]", help)
def test_check(self): def test_check(self):
help = str(cli.CheckOptions()) help = str(cli.CheckOptions())
self.failUnlessIn(" [global-opts] check [options] [ALIAS:PATH]", help) self.failUnlessIn("[options] [ALIAS:PATH]", help)
def test_deep_check(self): def test_deep_check(self):
help = str(cli.DeepCheckOptions()) help = str(cli.DeepCheckOptions())
self.failUnlessIn(" [global-opts] deep-check [options] [ALIAS:PATH]", help) self.failUnlessIn("[options] [ALIAS:PATH]", help)
def test_create_alias(self): def test_create_alias(self):
help = str(cli.CreateAliasOptions()) help = str(cli.CreateAliasOptions())
self.failUnlessIn(" [global-opts] create-alias [options] ALIAS[:]", help) self.failUnlessIn("[options] ALIAS[:]", help)
def test_add_alias(self): def test_add_alias(self):
help = str(cli.AddAliasOptions()) help = str(cli.AddAliasOptions())
self.failUnlessIn(" [global-opts] add-alias [options] ALIAS[:] DIRCAP", help) self.failUnlessIn("[options] ALIAS[:] DIRCAP", help)
def test_list_aliases(self): def test_list_aliases(self):
help = str(cli.ListAliasesOptions()) help = str(cli.ListAliasesOptions())
self.failUnlessIn(" [global-opts] list-aliases [options]", help) self.failUnlessIn("[options]", help)
def test_start(self): def test_start(self):
help = str(startstop_node.StartOptions()) help = str(startstop_node.StartOptions())
self.failUnlessIn(" [global-opts] start [options] [NODEDIR [twistd-options]]", help) self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
def test_stop(self): def test_stop(self):
help = str(startstop_node.StopOptions()) help = str(startstop_node.StopOptions())
self.failUnlessIn(" [global-opts] stop [options] [NODEDIR]", help) self.failUnlessIn("[options] [NODEDIR]", help)
def test_restart(self): def test_restart(self):
help = str(startstop_node.RestartOptions()) help = str(startstop_node.RestartOptions())
self.failUnlessIn(" [global-opts] restart [options] [NODEDIR [twistd-options]]", help) self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
def test_run(self): def test_run(self):
help = str(startstop_node.RunOptions()) help = str(startstop_node.RunOptions())
self.failUnlessIn(" [global-opts] run [options] [NODEDIR [twistd-options]]", help) self.failUnlessIn("[options] [NODEDIR [twistd-options]]", help)
def test_create_client(self): def test_create_client(self):
help = str(create_node.CreateClientOptions()) help = str(create_node.CreateClientOptions())
self.failUnlessIn(" [global-opts] create-client [options] [NODEDIR]", help) self.failUnlessIn("[options] [NODEDIR]", help)
def test_create_node(self): def test_create_node(self):
help = str(create_node.CreateNodeOptions()) help = str(create_node.CreateNodeOptions())
self.failUnlessIn(" [global-opts] create-node [options] [NODEDIR]", help) self.failUnlessIn("[options] [NODEDIR]", help)
def test_create_introducer(self): def test_create_introducer(self):
help = str(create_node.CreateIntroducerOptions()) help = str(create_node.CreateIntroducerOptions())
self.failUnlessIn(" [global-opts] create-introducer [options] NODEDIR", help) self.failUnlessIn("[options] NODEDIR", help)
def test_debug_trial(self): def test_debug_trial(self):
help = str(debug.TrialOptions()) help = str(debug.TrialOptions())
self.failUnlessIn(" [global-opts] debug trial [options] [[file|package|module|TestCase|testmethod]...]", help) self.failUnlessIn(" [global-options] debug trial [options] [[file|package|module|TestCase|testmethod]...]", help)
self.failUnlessIn("The 'tahoe debug trial' command uses the correct imports", help) self.failUnlessInNormalized("The 'tahoe debug trial' command uses the correct imports", help)
def test_debug_flogtool(self): def test_debug_flogtool(self):
options = debug.FlogtoolOptions() options = debug.FlogtoolOptions()
help = str(options) help = str(options)
self.failUnlessIn(" [global-opts] debug flogtool ", help) self.failUnlessIn(" [global-options] debug flogtool ", help)
self.failUnlessIn("The 'tahoe debug flogtool' command uses the correct imports", help) self.failUnlessInNormalized("The 'tahoe debug flogtool' command uses the correct imports", help)
for (option, shortcut, oClass, desc) in options.subCommands: for (option, shortcut, oClass, desc) in options.subCommands:
subhelp = str(oClass()) subhelp = str(oClass())
self.failUnlessIn(" [global-opts] debug flogtool %s " % (option,), subhelp) self.failUnlessIn(" [global-options] debug flogtool %s " % (option,), subhelp)
class Ln(GridTestMixin, CLITestMixin, unittest.TestCase): class Ln(GridTestMixin, CLITestMixin, unittest.TestCase):