diff --git a/docs/rbtools/index.rst b/docs/rbtools/index.rst
index c12b4d1331e8dd6ed65fdd262cbdc98dc294c781..670947bac9a74c2247265f7ccf9c0db4baea7089 100644
--- a/docs/rbtools/index.rst
+++ b/docs/rbtools/index.rst
@@ -60,6 +60,7 @@ There's a whole suite of additional commands that might also be useful:
 * :ref:`rbt-login` - Create a Review Board login session for RBTools
 * :ref:`rbt-logout` - Log RBTools out of Review Board
 * :ref:`rbt-publish` - Publish a review request
+* :ref:`rbt-review` - Create and publish a review
 * :ref:`rbt-setup-completion` - Set up shell integration/auto-completion
 * :ref:`rbt-stamp` - Stamp a local commit with a review request URL
 * :ref:`rbt-status-update` - Register or update a "status update" on a review
diff --git a/rbtools/commands/__init__.py b/rbtools/commands/__init__.py
index bce2ed0c36472d74ba494c5cedd92f07af914b6a..6a3f4fe9b499fc5ab22f51985ae70b00700b2b5c 100644
--- a/rbtools/commands/__init__.py
+++ b/rbtools/commands/__init__.py
@@ -118,6 +118,7 @@ class JSONOutput(object):
         self.output_stream.write(json.dumps(self.output, indent=4))
         self.output_stream.write('\n')
 
+
 class SmartHelpFormatter(argparse.HelpFormatter):
     """Smartly formats help text, preserving paragraphs."""
 
@@ -258,6 +259,7 @@ class Command(object):
                help='Displays debug output.',
                extended_help='This information can be valuable when debugging '
                              'problems running the command.'),
+
         Option('--json',
                action='store_true',
                dest='json_output',
@@ -848,6 +850,9 @@ class Command(object):
             parser.error('Invalid number of arguments provided')
             sys.exit(1)
 
+        if self.options.json_output:
+            sys.stdout = open(os.devnull, 'w')
+            sys.stderr = open(os.devnull, 'w')
         self.init_logging()
         log_command_line('Command line: %s', argv)
 
@@ -860,7 +865,7 @@ class Command(object):
                 raise
 
             logging.error(e)
-            self.json.add_error(e)
+            self.json.add_error(str(e))
             exit_code = 1
         except CommandExit as e:
             exit_code = e.exit_code
@@ -884,6 +889,8 @@ class Command(object):
                 self.json.add('status', 'success')
 
             self.json.print_to_stream()
+            sys.stdout = sys.__stdout__
+            sys.stderr = sys.__stderr__
 
         sys.exit(exit_code)
 
diff --git a/rbtools/commands/review.py b/rbtools/commands/review.py
new file mode 100644
index 0000000000000000000000000000000000000000..2d20f775017efe1d28dfee40b84065971241d145
--- /dev/null
+++ b/rbtools/commands/review.py
@@ -0,0 +1,927 @@
+import argparse
+import logging
+import sys
+from rbtools.api.errors import APIError
+from rbtools.commands import Command, CommandError, JSONOutput,\
+    SmartHelpFormatter
+
+
+class Subcommand(object):
+    """An abstract subcommand."""
+
+    #: Subommand line usage information for the command's help output.
+    usage = '%(prog)s <review-request-id> [<options>]'
+
+    #: Help text for the subcommand.
+    help_text = None
+
+    #: A description of the subcommand, when displaying the subcommand's own
+    # help.
+    description_text = None
+
+    #: Formatter class for help output.
+    help_formatter_cls = SmartHelpFormatter
+
+    json = JSONOutput(sys.stdout)
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+        pass
+
+    def run(self, api_root, review_request, options):
+        """Run the command.
+
+        Args:
+            api_root:
+                The site to operate on.
+
+            review_request:
+                The review_request for which a review is being created or
+                modified.
+
+            options (argparse.Namespace):
+                The parsed options for the subcommand.
+        """
+        raise NotImplementedError
+
+
+class DiscardReview(Subcommand):
+    """Discard a pending review draft for an existing review request.
+    """
+    name = ('discard')
+
+    help_text = (
+        'Discards a review draft for the specified review request'
+    )
+
+    description_text = (
+        'This will discard a pending review draft if one exists for the '
+        'specified review request. If one does not exist, it will display '
+        'an error message to the user informing them of this.'
+    )
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+
+    def discard_review(self, api_root, options):
+        """Discard a review draft.
+
+        Args:
+            api_root (rbtools.api.transport.Transport):
+                Representation of the root level of the Review Board API for
+                doing further requests to the Review Board API.
+
+            options
+                The options parsed for the subcommand.
+
+        Raises:
+            rbtools.commands.CommandError:
+                Error with the execution of the command.
+        """
+
+        # Fetch a review draft for the specified review request.
+        # If one does not exist, raise an error to inform the user.
+
+        review_exists = False
+        try:
+            review_draft = api_root.get_review_draft(
+                review_request_id=options.review_request_id)
+            if review_draft:
+                review_exists = True
+            else:
+                raise CommandError('No review draft exists for the specified '
+                                   'review request %s: %s' %
+                                   (options.review_request_id))
+        except APIError as e:
+            raise CommandError('Error discarding review draft for the '
+                               'specified review request %s: %s' %
+                               (options.review_request_id, e))
+
+        # Delete the review draft if one exists.
+
+        try:
+            if review_exists:
+                self.json.add('review_draft_id', review_draft.id)
+                review_draft.delete()
+                logging.info('Successfully discarded pending review with ID '
+                             '%s for review request %s' %
+                             (review_draft.id, options.review_request_id))
+        except APIError as e:
+            raise CommandError('Error deleting review draft for '
+                               'review request %s: %s' %
+                               (options.review_request_id, e))
+
+    def run(self, api_root, review_request, options):
+        try:
+            self.discard_review(api_root, options)
+            return self.json
+        except Exception as e:
+            raise CommandError(e)
+
+
+class PublishReview(Subcommand):
+    """Publish a pending review draft for an existing review request.
+    """
+    name = ('publish')
+
+    help_text = (
+        'Publishes a pending review draft for the specified review request.'
+    )
+
+    description_text = (
+        'This will publish a pending review draft if one exists for the '
+        'specified review request. If one does not exist or if the review, '
+        'draft is empty,it will display an error message to the user '
+        'informing them of this.'
+    )
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+
+    def empty_review(self, review_draft):
+        """Check if a pending review draft is empty.
+
+        Args:
+            review_draft:
+                The review draft that we are checking is empty or not.
+        Returns:
+            bool:
+            ``True`` if the review is completely empty.
+            ``False`` otherwise.
+        """
+        if (review_draft.get_diff_comments().total_results == 0
+            and review_draft.get_file_attachment_comments().total_results == 0
+            and review_draft.get_general_comments().total_results == 0
+            and not review_draft.body_top and not review_draft.body_bottom
+            and not review_draft.ship_it):
+            return True
+        return False
+
+    def publish_review(self, api_root, options):
+        """Publish a pending review draft for the specified review request.
+
+        Args:
+            api_root (rbtools.api.transport.Transport):
+                Representation of the root level of the Review Board API for
+                doing further requests to the Review Board API.
+
+            options
+                The options parsed for the subcommand.
+
+        Raises:
+            rbtools.commands.CommandError:
+                Error with the execution of the command.
+        """
+
+        # Fetch a review draft for the specified review request.
+        # If one does not exist, raise an error to inform the user.
+
+        review_exists = False
+        try:
+            review_draft = api_root.get_review_draft(
+                review_request_id=options.review_request_id)
+            if review_draft:
+                review_exists = True
+                self.json.add('review_draft_id', review_draft.id)
+        except APIError as e:
+            raise CommandError('Error publishing review draft for '
+                               'review request %s: %s' %
+                               (options.review_request_id, e))
+
+        # Publish the review draft if not empty
+        try:
+            if review_exists:
+                if self.empty_review(review_draft):
+                    raise CommandError("Cannot publish empty review with ID %s"
+                                       " for review request %s" %
+                                       (review_draft.id,
+                                        options.review_request_id))
+
+                review_draft.update(public=True)
+                logging.info('Successfully published a review with ID '
+                             '%s for review request %s' %
+                             (review_draft.id, options.review_request_id))
+        except APIError as e:
+            raise CommandError('Error publishing review draft for '
+                               'review request %s: %s' %
+                               (options.review_request_id, e))
+
+    def run(self, api_root, review_request, options):
+        try:
+            self.publish_review(api_root, options)
+            return self.json
+        except Exception as e:
+            raise CommandError(e)
+
+
+class ShipIt(Subcommand):
+    """Add a ship-it label for a review for a specified review request
+
+    This will create a ship-it only review if no review draft exists for the
+    review request id. If there is a pending review, it will be updated with
+    a ship-it label.
+    """
+    name = ('ship_it')
+
+    help_text = (
+        'Adds a ship-it label to a review draft.'
+    )
+
+    description_text = (
+        'This will create a ship-it only review if no review draft exists for '
+        'the review request id. If there is a pending review, it will be '
+        'updated with a ship-it label.'
+    )
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+
+    def ship_it(self, api_root, review_request, options):
+        """Add a ship-it only review for a specified review request
+
+        Args:
+            api_root (rbtools.api.transport.Transport):
+                Representation of the root level of the Review Board API for
+                doing further requests to the Review Board API.
+
+            review_request
+                The review request that the user would like to mark as ship-it.
+
+            options
+                The options parsed for the subcommand.
+        Raises:
+            rbtools.commands.CommandError:
+                Error with the execution of the command.
+        """
+
+        # Fetch a review and add a ship_it. If a review does not exist, we
+        # create a new one and add a ship_it attribute to it.
+
+        try:
+            review_draft = review_request.get_reviews(
+                review_request_id=options.review_request_id)\
+                .create(ship_it=True)
+
+            self.json.add('review_draft_id', review_draft.id)
+
+            logging.info('Successfully added a ship-it label to a review '
+                         'for review request %s' % (options.review_request_id))
+        except APIError as e:
+            raise CommandError('RBTools was unable to add a ship-it label '
+                               'to a review for the specified review request '
+                               '%s: %s' % (options.review_request_id, e))
+
+    def run(self, api_root, review_request, options):
+        try:
+            self.ship_it(api_root, review_request, options)
+            return self.json
+        except Exception as e:
+            raise CommandError(e)
+
+
+class AddFileAttachmentComment(Subcommand):
+    """Add a file attachment comment to a review draft.
+    """
+    name = ('add_file_attachment_comment')
+
+    help_text = (
+        'Add a file attachment comment to a pending review draft.'
+    )
+
+    description_text = (
+        'Add a file attachment comment to a pending review draft '
+        'for the specified review request. If a review draft does not '
+        'exist, then a review draft is created and the comment is added '
+        'to it.'
+    )
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+        parser.add_argument(
+            '--file-attachment-id',
+            dest='fid',
+            metavar='fid',
+            type=int,
+            required=True,
+            help='The file attachment ID identifying the file attachment that '
+            'a comment should be added to.'
+        )
+        parser.add_argument(
+            '--open-issue',
+            dest='open_issue',
+            action='store_true',
+            default=False,
+            help='Open an issue with the comment.')
+        parser.add_argument(
+            '-t', '--text',
+            dest='text',
+            metavar='text',
+            type=str,
+            required=True,
+            help='Set content for the comment text field.')
+
+    def add_file_attachment_comment(self, api_root, options):
+        """Add a file attachment comment to a review draft for a specified review
+        request
+
+        Args:
+            api_root (rbtools.api.transport.Transport):
+                Representation of the root level of the Review Board API for
+                doing further requests to the Review Board API.
+
+            options
+                The options parsed for the subcommand.
+        Raises:
+            rbtools.commands.CommandError:
+                Error with the execution of the command.
+        """
+
+        # If the comment has no text component, then we raise an error to
+        # inform the user
+        try:
+            if (not options.text or options.text.isspace()):
+                raise CommandError('All comments must have a text component.')
+
+            # Get a pending review draft or create a new one for the specified
+            # review request
+            review = api_root.get_reviews(
+                review_request_id=options.review_request_id).create()
+            self.json.add('review_draft_id', review.id)
+        except APIError as e:
+            raise CommandError('No review exists for the specified '
+                               'review request  %s: %s' %
+                               (options.review_request_id, e))
+        try:
+            # Create a file attachment comment for the specified file
+            file_attachment_comment = review.get_file_attachment_comments().\
+                create(text_type='plain', text=options.text,
+                       issue_opened=options.open_issue,
+                       file_attachment_id=options.fid)
+
+            self.json.add('file_attachment_id', options.fid)
+            self.json.add('file_attachment_comment_id',
+                          file_attachment_comment.id)
+            self.json.add('file_attachment_comment_text',
+                          file_attachment_comment.text)
+            self.json.add('issue_opened',
+                          file_attachment_comment.issue_opened)
+
+            # Inform the user if adding the comment was successful
+            logging.info('A file attachment comment was created for '
+                         'review request %s' % (options.review_request_id))
+        except APIError as e:
+            raise CommandError('Could not add a file attachment comment '
+                               'for review request %s: %s' %
+                               (options.review_request_id, e))
+
+    def run(self, api_root, review_request, options):
+        try:
+            self.add_file_attachment_comment(api_root, options)
+            return self.json
+        except Exception as e:
+            raise CommandError(e)
+
+
+class AddDiffComment(Subcommand):
+    """Add a diff comment to a review draft for an existing review request.
+    """
+    name = ('add_diff_comment')
+
+    help_text = (
+        'Add a diff comment to a pending review draft.'
+    )
+
+    description_text = (
+        'Add a diff comment to a pending review draft for the specified '
+        'review request. If a review draft does not exist, then a '
+        'review draft is created and the comment is added to it.'
+    )
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+        parser.add_argument(
+            '-t', '--text',
+            dest='text',
+            metavar='text',
+            type=str,
+            required=True,
+            help='Set content for the comment text field.'),
+        parser.add_argument(
+            '--filename',
+            dest='filename',
+            metavar='file-name',
+            type=str,
+            required=True,
+            help='The name of the file a diff comment should be added to.'),
+        parser.add_argument(
+            '--num-lines',
+            dest='num_lines',
+            metavar='num-lines',
+            type=int,
+            required=False,
+            help='Number of lines. If not specified, the comment will '
+            'span one line.'),
+        parser.add_argument(
+            '-l', '--line',
+            dest='line',
+            metavar='line',
+            type=int,
+            required=True,
+            help='Specify the line number on which a diff comment should '
+            'be added.'),
+        parser.add_argument(
+            '--open-issue',
+            dest='open_issue',
+            action='store_true',
+            default=False,
+            help='Open an issue with the comment.'),
+        parser.add_argument(
+            '--diff-version',
+            dest='diff_version',
+            metavar='diff_version',
+            type=int,
+            required=False,
+            help='The diff version of the file that a diff comment '
+            'should be added to.')
+
+    def add_diff_comment(self, api_root, review_request, options):
+        """Discard a review draft.
+
+        Args:
+            api_root (rbtools.api.transport.Transport):
+                Representation of the root level of the Review Board API for
+                doing further requests to the Review Board API.
+
+            review_request
+                The review request that the user would like to mark as ship-it.
+
+            options
+                The options parsed for the subcommand.
+
+        Raises:
+            rbtools.commands.CommandError:
+                Error with the execution of the command.
+        """
+
+        # Fetch a review draft for the specified review request.
+        # If one does not exist, raise an error to inform the user.
+        try:
+            # Check that the comment has text in it
+            if (not options.text or options.text.isspace()):
+                raise CommandError('All comments must have a text component.')
+
+            # Get review draft or create one for the specified review request
+            review = api_root.get_reviews(
+                review_request_id=options.review_request_id).create()
+            self.json.add('review_draft_id', review.id)
+        except APIError as e:
+            raise CommandError('No review exists for the specified '
+                               'review request  %s: %s' %
+                               (options.review_request_id, e))
+
+        try:
+            # Set num_lines to 1 if not specified or invalid
+            if options.num_lines is None:
+                options.num_lines = 1
+            elif options.num_lines <= 0:
+                options.num_lines = 1
+
+            # If the user has not set a diff version, then we
+            # set it to the latest version.
+
+            if options.diff_version is None:
+                diff_revision = review_request.get_diffs(
+                    review_request_id=options.review_request_id)
+                diff_revision_len = len(diff_revision)
+                options.diff_version = diff_revision_len
+
+            # Get the specified diff.
+
+            diff_set = api_root.get_diff(
+                review_request_id=options.review_request_id,
+                diff_revision=options.diff_version)
+
+            # Get all the files in the fetched diff.
+
+            files = diff_set.get_files()
+
+            # Loop through all the files in the fetched diff
+            # and try to find a file matching the name - maybe do path instead?
+
+            filediff_id = None
+            file_total_line_count = 0
+            num_matches = 0
+            for file in files:
+                if file.dest_file.endswith(options.filename):
+                    num_matches = num_matches + 1
+                    if num_matches == 1:
+                        filediff_id = file.id
+                        file_total_line_count = \
+                            file.extra_data.total_line_count
+
+            # Inform the user if we cannot find a matching file
+
+            if filediff_id is None:
+                raise CommandError('Could not find a file in the diff with '
+                                   'the specified file name.')
+
+            # Inform the user if we find more than one matching filename
+
+            elif num_matches > 1:
+                raise CommandError('RBTools found more than 1 filediff with '
+                                   'the passed filename: %s. To ensure '
+                                   'RBTools selects the correct file, '
+                                   'please pass the subdirectory/filename '
+                                   'instead.' % (options.filename))
+            else:
+                # Check and inform the user if the line number they passed
+                # is invalid
+
+                if options.line < 1:
+                    raise CommandError('Please choose a valid line number. ')
+                if options.line > file_total_line_count:
+                    raise CommandError('Please choose a valid line number. ')
+
+            # Create a diff comment with all the attributes that are
+            # passed by the users via options
+
+            diff_comment = review.get_diff_comments().create(
+                text_type='plain', text=options.text,
+                issue_opened=options.open_issue,
+                first_line=options.line,
+                num_lines=options.num_lines,
+                filediff_id=filediff_id)
+
+            self.json.add('diff_comment_id', diff_comment.id)
+            self.json.add('diff_comment_file', options.filename)
+            self.json.add('diff_comment_line', diff_comment.first_line)
+            self.json.add('diff_comment_text', diff_comment.text)
+            self.json.add('issue_opened', diff_comment.issue_opened)
+
+            # Inform the user if a diff comment was created successfully
+            logging.info('A diff comment was created for review request %s' %
+                         (options.review_request_id))
+        except APIError as e:
+            raise CommandError('Could not create a diff comment for '
+                               'review request %s: %s' %
+                               (options.review_request_id, e))
+
+    def run(self, api_root, review_request, options):
+        try:
+            self.add_diff_comment(api_root, review_request, options)
+            return self.json
+        except Exception as e:
+            raise CommandError(e)
+
+
+class AddGeneralComment(Subcommand):
+    """Add a general comment to a pending review draft for an existing
+        review request.
+    """
+    name = ('add_general_comment')
+
+    help_text = (
+        'Add a general comment to a pending review draft.'
+    )
+
+    description_text = (
+        'Add a general comment to a pending review draft for the specified '
+        'review request. If a review draft does not exist, then a '
+        'review draft is created and the comment is added to it.'
+    )
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+        parser.add_argument(
+            '-t', '--text',
+            dest='text',
+            metavar='text',
+            type=str,
+            required=True,
+            help='Set content for the comment text field.')
+        parser.add_argument(
+            '--open-issue',
+            dest='open_issue',
+            action='store_true',
+            default=False,
+            help='Open an issue with the comment.')
+
+    def add_general_comment(self, api_root, options):
+        """Add a general comment to a review.
+
+        Args:
+            api_root (rbtools.api.transport.Transport):
+                Representation of the root level of the Review Board API for
+                doing further requests to the Review Board API.
+
+            options
+                The parsed options for this subcommand.
+
+        Raises:
+            rbtools.commands.CommandError:
+                Error with the execution of the command.
+        """
+
+        # Fetch a review draft for the specified review request.
+        # If one does not exist, raise an error to inform the user.
+        try:
+            # Check that the comment has text in it
+            if (not options.text or options.text.isspace()):
+                raise CommandError('All comments must have a text component.')
+            # Get review draft or create one for the specified review request
+            review = api_root.get_reviews(
+                review_request_id=options.review_request_id).create()
+            self.json.add('review_draft_id', review.id)
+        except APIError as e:
+            raise CommandError('No review exists for the specified '
+                               'review request  %s: %s' %
+                               (options.review_request_id, e))
+
+        # Add a general comment to a review and inform the user if it was done
+        # successfully or not
+        try:
+            general_comment = review.get_general_comments().create(
+                text_type='plain', text=options.text,
+                issue_opened=options.open_issue)
+
+            self.json.add('general_comment_id', general_comment.id)
+            self.json.add('general_comment_text', general_comment.text)
+            self.json.add('issue_opened', general_comment.issue_opened)
+
+            logging.info('A general comment was created for review request %s'
+                         % (options.review_request_id))
+        except APIError as e:
+            raise CommandError('Could not add a general comment '
+                               'to review request %s: %s' %
+                               (options.review_request_id, e))
+
+    def run(self, api_root, review_request, options):
+        try:
+            self.add_general_comment(api_root, options)
+            return self.json
+        except Exception as e:
+            raise CommandError(e)
+
+
+class CreateReview(Subcommand):
+    """Creates a review for an existing review request.
+
+    This will create a new review for the specified review request
+    if one does not previously exist. If one does exist from before,
+    we will update it and return it instead of creating a new one.
+    """
+    name = ('create')
+
+    help_text = (
+        'Creates a review for the specified review request'
+    )
+
+    description_text = (
+        'This will create a new review for the specified review request '
+        'if one does not previously exist. If one does exist from before, '
+        'we will update it and return it instead of creating a new one.'
+    )
+
+    def add_options(self, parser):
+        """Add any command-specific options to the parser.
+
+        Args:
+            parser (argparse.ArgumentParser):
+                The argument parser for this subcommand.
+        """
+        parser.add_argument(
+            '--review-footer',
+            dest='review_footer',
+            metavar='review_footer',
+            type=str,
+            required=False,
+            default='',
+            help='Set content for the review footer field.')
+        parser.add_argument(
+            '--review-header',
+            dest='review_header',
+            type=str,
+            required=False,
+            default='',
+            help='Set content for the review header field.')
+        parser.add_argument(
+            '--ship-it',
+            dest='ship_it',
+            action='store_true',
+            required=False,
+            default=False,
+            help='Add a ship-it label to a review.')
+
+    def create_review(self, api_root, review_request, options):
+        """Create a review for a specified review request.
+
+        Args:
+            api_root (rbtools.api.transport.Transport):
+                Representation of the root level of the Review Board API for
+                doing further requests to the Review Board API.
+
+            review_request
+                The review request that the user would like to create
+                a review for.
+
+            options
+                The options parsed for this subcommand.
+
+        Raises:
+            rbtools.commands.CommandError:
+                Error with the execution of the command.
+        """
+
+        # We check if a review draft exists for the specified review request.
+        # If a review already exists, we use its existing attributes unless
+        # changes are specified. Additionally, we inform the user that a
+        # review already exists.
+
+        review_unchanged = False
+        review_draft = None
+        review_draft_existed = False
+
+        try:
+            review_draft = api_root.get_review_draft(
+                review_request_id=options.review_request_id)
+            if review_draft:
+                review_draft_existed = True
+                if (not options.review_header and not
+                    options.review_footer and not options.ship_it):
+                    review_unchanged = True
+                if not options.review_header:
+                    options.review_header = review_draft.body_top
+                if not options.review_footer:
+                    options.review_footer = review_draft.body_bottom
+                if not options.ship_it:
+                    options.ship_it = review_draft.ship_it
+                logging.info('RBTools found a pre-existing review draft for '
+                             'the specified review request %s.' %
+                             (review_request.id))
+        except Exception:
+            pass
+
+        # If a review does not already exist from before, we will create a new
+        # review with any attributes specified through the options passed. On
+        # the successful creation of a review, we inform the user of such and
+        # if there is an error, we inform the user of that as well.
+
+        try:
+            if review_unchanged is False:
+                review_draft = review_request.get_reviews(
+                    review_request_id=options.review_request_id).create(
+                        body_top=options.review_header,
+                        body_bottom=options.review_footer,
+                        ship_it=options.ship_it)
+
+                if review_draft_existed:
+                    logging.info('RBTools successfully updated a review for '
+                                 'review request %s' % (review_request.id))
+                else:
+                    logging.info('RBTools successfully created a review for '
+                                 'review request %s' % (review_request.id))
+            self.json.add('review_draft_id', review_draft.id)
+        except APIError as e:
+            raise CommandError('Error creating review for review request \
+                                %s: %s' % (review_request.id, e))
+
+    def run(self, api_root, review_request, options):
+        try:
+            self.create_review(api_root, review_request, options)
+            return self.json
+        except Exception as e:
+            raise CommandError(e)
+
+
+class Review(Command):
+    name = 'review'
+    author = 'The Review Board Project'
+    option_list = [
+        Command.server_options,
+        Command.repository_options]
+
+    def create_parser(self, config, argv=[]):
+        """Create and return the argument parser for this command."""
+        parser = argparse.ArgumentParser(
+            prog='rbt review <subcommand> [options]',
+            usage='rbt review <subcommand> [options]',
+            add_help=False,
+            formatter_class=SmartHelpFormatter)
+
+        # mapping of user-passed subcommands to subcommand programs
+        COMMANDS = {
+            "create": CreateReview(),
+            "add-diff-comment": AddDiffComment(),
+            "add-file-attachment-comment": AddFileAttachmentComment(),
+            "add-general-comment": AddGeneralComment(),
+            "ship-it": ShipIt(),
+            "discard": DiscardReview(),
+            "publish": PublishReview()
+        }
+
+        sorted_commands = list(COMMANDS.keys())
+        sorted_commands.sort()
+
+        subparsers = parser.add_subparsers(
+            help='the review command to run.',
+            description='To get additional help for these commands, run: '
+            'rbt review <command> --help')
+
+        # loop through each command and create a subparser for it
+        # by adding all common arguments, and adding subcommand
+        # specific arguments by calling command.add_options(subparser)
+        for cmd_name in sorted_commands:
+            command = COMMANDS[cmd_name]
+            subparser = subparsers.add_parser(
+                cmd_name,
+                formatter_class=Subcommand.help_formatter_cls,
+                prog='%s %s' % (parser.prog, cmd_name),
+                description=command.description_text,
+                help=command.help_text)
+
+            subparser.add_argument(
+                '-r', '--review-request-id',
+                dest='review_request_id',
+                metavar='review-request-id',
+                type=int,
+                required=True,
+                help='Specifies which review request.')
+
+            for option in self.option_list:
+                option.add_to(subparser, config, argv)
+
+            for option in self._global_options:
+                option.add_to(subparser, config, argv)
+
+            subparser.set_defaults(command=command)
+
+            command.add_options(subparser)
+
+        return parser
+
+    def main(self):
+        """Main application handler.
+
+        This will set up rbt review for operation on the command line,
+        parse any command line options, and invoke the handler for the
+        requested subcommand.
+        """
+        # Run the command.
+        repository_info, tool = self.initialize_scm_tool(
+            client_name=self.options.repository_type)
+        server_url = self.get_server_url(repository_info, tool)
+        api_root = self.get_api(server_url)[1]
+
+        # Check that the user has passed a valid review request in
+        # their options.
+        try:
+            review_request = api_root.get_review_request(
+                review_request_id=self.options.review_request_id)
+        except APIError as e:
+            raise CommandError('Error getting review request %s: %s'
+                               % (self.options.review_request_id, e))
+        # Run the subcommand.
+        try:
+            self.json.add('review_request_id', self.options.review_request_id)
+            self.json.add('review_subcommand', str(self.options.command.name))
+            result = self.options.command.run(api_root, review_request,
+                                              self.options)
+            for key in result.output.keys():
+                self.json.add(key, result.output[key])
+        except CommandError as e:
+            raise CommandError(e)
diff --git a/rbtools/commands/tests/test_review.py b/rbtools/commands/tests/test_review.py
new file mode 100644
index 0000000000000000000000000000000000000000..97e248a8d45c6e532ff7a64f142fa8ac88757920
--- /dev/null
+++ b/rbtools/commands/tests/test_review.py
@@ -0,0 +1,96 @@
+"""Test for RBTools review command."""
+
+from __future__ import unicode_literals
+
+from rbtools.commands.review import Review
+from rbtools.utils.testbase import RBTestBase
+
+
+class ReveiwCommandTests(RBTestBase):
+    """Tests for rbt review command."""
+
+    def test_add_file_attachment_comment(self):
+        """Testing adding a file attachment comment for an existing
+        review request with --review-request-id=12345
+        """
+        comment = self._create_review_command(
+            args=['add-file-attachment-comment',
+                  '--review-request-id', '12345',
+                  '--text', 'test123',
+                  '--file-attachment-id', '5'])
+        self.assertEqual(comment.options.review_request_id, 12345)
+        self.assertEqual(comment.options.text, 'test123')
+        self.assertEqual(comment.options.fid, 5)
+
+    def test_add_diff_comment(self):
+        """Testing adding a diff comment for a review for an existing
+        review request with --review-request-id=12345
+        """
+        comment = self._create_review_command(
+            args=['add-diff-comment',
+                  '--review-request-id', '12345',
+                  '--text', 'test123',
+                  '--line', '23',
+                  '--filename', 'test.py'])
+        self.assertEqual(comment.options.review_request_id, 12345)
+        self.assertEqual(comment.options.text, 'test123')
+        self.assertEqual(comment.options.line, 23)
+        self.assertEqual(comment.options.filename, 'test.py')
+
+    def test_add_general_comment(self):
+        """Testing adding a general comment for a review for an existing
+        review request with --review-request-id=12345
+        """
+        comment = self._create_review_command(
+            args=['add-general-comment',
+                  '--review-request-id', '12345',
+                  '--text', 'test123'])
+        self.assertEqual(comment.options.review_request_id, 12345)
+        self.assertEqual(comment.options.text, 'test123')
+
+    def test_create_review(self):
+        """Testing creating a review for an existing
+        review request with --review-request-id=12345
+        """
+        review = self._create_review_command(
+            args=['create',
+                  '--review-request-id', '12345',
+                  '--review-header', 'header_test',
+                  '--review-footer', 'footer_test',
+                  '--ship-it'])
+        self.assertEqual(review.options.review_request_id, 12345)
+        self.assertEqual(review.options.review_header, 'header_test')
+        self.assertEqual(review.options.review_footer, 'footer_test')
+        self.assertEqual(review.options.ship_it, True)
+
+    def _create_review_command(self, fields=None, args=None):
+        """Create an argument parser with the given extra fields.
+
+        Args:
+            fields (list of unicode):
+                A list of key-value pairs for the field argument.
+
+                Each pair should be of the form key=value.
+
+            args (list of unicode):
+                A list of command line arguments to be passed to the parser.
+
+                The command line will receive each item in the list.
+
+        Returns:
+            rbtools.commands.review.REVIEW:
+            A REVIEW instance for communicating with the rbt server
+        """
+        review = Review()
+        argv = ['rbt', 'review']
+
+        if args is not None:
+            argv.extend(args)
+
+        parser = review.create_arg_parser(argv)
+        review.options = parser.parse_args(argv[2:])
+
+        if fields is not None:
+            review.options.fields = fields
+
+        return review
diff --git a/setup.py b/setup.py
index fbff0add17f6bf4b212dd12c3228730baa0cd9d4..649750600d2c831653d885f7dc5ccf667c8f18c6 100755
--- a/setup.py
+++ b/setup.py
@@ -76,6 +76,7 @@ rb_commands = [
     'patch = rbtools.commands.patch:Patch',
     'post = rbtools.commands.post:Post',
     'publish = rbtools.commands.publish:Publish',
+    'review = rbtools.commands.review:Review',
     'setup-completion = rbtools.commands.setup_completion:SetupCompletion',
     'setup-repo = rbtools.commands.setup_repo:SetupRepo',
     'stamp = rbtools.commands.stamp:Stamp',
