Issue #13477: Added command line interface to the tarfile module.
Original patch by Berker Peksag.
diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst
index b8b65b9..d7e3b5f 100644
--- a/Doc/library/tarfile.rst
+++ b/Doc/library/tarfile.rst
@@ -591,6 +591,67 @@
    Return :const:`True` if it is one of character device, block device or FIFO.
 
 
+.. _tarfile-commandline:
+
+Command Line Interface
+----------------------
+
+.. versionadded:: 3.4
+
+The :mod:`tarfile` module provides a simple command line interface to interact
+with tar archives.
+
+If you want to create a new tar archive, specify its name after the :option:`-c`
+option and then list the filename(s) that should be included::
+
+    $ python -m tarfile -c monty.tar  spam.txt eggs.txt
+
+Passing a directory is also acceptable::
+
+    $ python -m tarfile -c monty.tar life-of-brian_1979/
+
+If you want to extract a tar archive into the current directory, use
+the :option:`-e` option::
+
+    $ python -m tarfile -e monty.tar
+
+You can also extract a tar archive into a different directory by passing the
+directory's name::
+
+    $ python -m tarfile -e monty.tar  other-dir/
+
+For a list of the files in a tar archive, use the :option:`-l` option::
+
+    $ python -m tarfile -l monty.tar
+
+
+Command line options
+~~~~~~~~~~~~~~~~~~~~
+
+.. cmdoption:: -l <tarfile>
+               --list <tarfile>
+
+   List files in a tarfile.
+
+.. cmdoption:: -c <tarfile> <source1> <sourceN>
+               --create <tarfile> <source1> <sourceN>
+
+   Create tarfile from source files.
+
+.. cmdoption:: -e <tarfile> [<output_dir>]
+               --extract <tarfile> [<output_dir>]
+
+   Extract tarfile into the current directory if *output_dir* is not specified.
+
+.. cmdoption:: -t <tarfile>
+               --test <tarfile>
+
+   Test whether the tarfile is valid or not.
+
+.. cmdoption:: -v, --verbose
+
+   Verbose output
+
 .. _tar-examples:
 
 Examples
diff --git a/Lib/tarfile.py b/Lib/tarfile.py
index 94bd75d..f4df6c7 100644
--- a/Lib/tarfile.py
+++ b/Lib/tarfile.py
@@ -2404,3 +2404,97 @@
 
 bltn_open = open
 open = TarFile.open
+
+
+def main():
+    import argparse
+
+    description = 'A simple command line interface for tarfile module.'
+    parser = argparse.ArgumentParser(description=description)
+    parser.add_argument('-v', '--verbose', action='store_true', default=False,
+                        help='Verbose output')
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument('-l', '--list', metavar='<tarfile>',
+                       help='Show listing of a tarfile')
+    group.add_argument('-e', '--extract', nargs='+',
+                       metavar=('<tarfile>', '<output_dir>'),
+                       help='Extract tarfile into target dir')
+    group.add_argument('-c', '--create', nargs='+',
+                       metavar=('<name>', '<file>'),
+                       help='Create tarfile from sources')
+    group.add_argument('-t', '--test', metavar='<tarfile>',
+                       help='Test if a tarfile is valid')
+    args = parser.parse_args()
+
+    if args.test:
+        src = args.test
+        if is_tarfile(src):
+            with open(src, 'r') as tar:
+                tar.getmembers()
+                print(tar.getmembers(), file=sys.stderr)
+            if args.verbose:
+                print('{!r} is a tar archive.'.format(src))
+        else:
+            parser.exit(1, '{!r} is not a tar archive.\n'.format(src))
+
+    elif args.list:
+        src = args.list
+        if is_tarfile(src):
+            with TarFile.open(src, 'r:*') as tf:
+                tf.list(verbose=args.verbose)
+        else:
+            parser.exit(1, '{!r} is not a tar archive.\n'.format(src))
+
+    elif args.extract:
+        if len(args.extract) == 1:
+            src = args.extract[0]
+            curdir = os.curdir
+        elif len(args.extract) == 2:
+            src, curdir = args.extract
+        else:
+            parser.exit(1, parser.format_help())
+
+        if is_tarfile(src):
+            with TarFile.open(src, 'r:*') as tf:
+                tf.extractall(path=curdir)
+            if args.verbose:
+                if curdir == '.':
+                    msg = '{!r} file is extracted.'.format(src)
+                else:
+                    msg = ('{!r} file is extracted '
+                           'into {!r} directory.').format(src, curdir)
+                print(msg)
+        else:
+            parser.exit(1, '{!r} is not a tar archive.\n'.format(src))
+
+    elif args.create:
+        tar_name = args.create.pop(0)
+        _, ext = os.path.splitext(tar_name)
+        compressions = {
+            # gz
+            'gz': 'gz',
+            'tgz': 'gz',
+            # xz
+            'xz': 'xz',
+            'txz': 'xz',
+            # bz2
+            'bz2': 'bz2',
+            'tbz': 'bz2',
+            'tbz2': 'bz2',
+            'tb2': 'bz2',
+        }
+        tar_mode = 'w:' + compressions[ext] if ext in compressions else 'w'
+        tar_files = args.create
+
+        with TarFile.open(tar_name, tar_mode) as tf:
+            for file_name in tar_files:
+                tf.add(file_name)
+
+        if args.verbose:
+            print('{!r} file created.'.format(tar_name))
+
+    else:
+        parser.exit(1, parser.format_help())
+
+if __name__ == '__main__':
+    main()
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
index 2f2bf6b..886a80e 100644
--- a/Lib/test/test_tarfile.py
+++ b/Lib/test/test_tarfile.py
@@ -7,7 +7,7 @@
 import unittest
 import tarfile
 
-from test import support
+from test import support, script_helper
 
 # Check for our compression modules.
 try:
@@ -27,11 +27,13 @@
     return md5(data).hexdigest()
 
 TEMPDIR = os.path.abspath(support.TESTFN) + "-tardir"
+tarextdir = TEMPDIR + '-extract-test'
 tarname = support.findfile("testtar.tar")
 gzipname = os.path.join(TEMPDIR, "testtar.tar.gz")
 bz2name = os.path.join(TEMPDIR, "testtar.tar.bz2")
 xzname = os.path.join(TEMPDIR, "testtar.tar.xz")
 tmpname = os.path.join(TEMPDIR, "tmp.tar")
+dotlessname = os.path.join(TEMPDIR, "testtar")
 
 md5_regtype = "65f477c818ad9e15f7feab0c6d37742f"
 md5_sparse = "a54fbc4ca4f4399a90e1b27164012fc6"
@@ -1724,6 +1726,168 @@
             tarfile.itn(0x10000000000, 6, tarfile.GNU_FORMAT)
 
 
+class CommandLineTest(unittest.TestCase):
+
+    def tarfilecmd(self, *args):
+        rc, out, err = script_helper.assert_python_ok('-m', 'tarfile', *args)
+        return out
+
+    def tarfilecmd_failure(self, *args):
+        return script_helper.assert_python_failure('-m', 'tarfile', *args)
+
+    def make_simple_tarfile(self, tar_name):
+        files = [support.findfile('tokenize_tests.txt'),
+                 support.findfile('tokenize_tests-no-coding-cookie-'
+                                  'and-utf8-bom-sig-only.txt')]
+        self.addCleanup(support.unlink, tar_name)
+        with tarfile.open(tar_name, 'w') as tf:
+            for tardata in files:
+                tf.add(tardata, arcname=os.path.basename(tardata))
+
+    def test_test_command(self):
+        for tar_name in (tarname, gzipname, bz2name, xzname):
+            for opt in '-t', '--test':
+                out = self.tarfilecmd(opt, tar_name)
+                self.assertEqual(out, b'')
+
+    def test_test_command_verbose(self):
+        for tar_name in (tarname, gzipname, bz2name, xzname):
+            for opt in '-v', '--verbose':
+                out = self.tarfilecmd(opt, '-t', tar_name)
+                self.assertIn(b'is a tar archive.\n', out)
+
+    def test_test_command_invalid_file(self):
+        zipname = support.findfile('zipdir.zip')
+        rc, out, err = self.tarfilecmd_failure('-t', zipname)
+        self.assertIn(b' is not a tar archive.', err)
+        self.assertEqual(out, b'')
+        self.assertEqual(rc, 1)
+
+        for tar_name in (tarname, gzipname, bz2name, xzname):
+            with self.subTest(tar_name=tar_name):
+                with open(tar_name, 'rb') as f:
+                    data = f.read()
+                try:
+                    with open(tmpname, 'wb') as f:
+                        f.write(data[:511])
+                    rc, out, err = self.tarfilecmd_failure('-t', tmpname)
+                    self.assertEqual(out, b'')
+                    self.assertEqual(rc, 1)
+                finally:
+                    support.unlink(tmpname)
+
+    def test_list_command(self):
+        self.make_simple_tarfile(tmpname)
+        with support.captured_stdout() as t:
+            with tarfile.open(tmpname, 'r') as tf:
+                tf.list(verbose=False)
+        expected = t.getvalue().encode(sys.getfilesystemencoding())
+        for opt in '-l', '--list':
+            out = self.tarfilecmd(opt, tmpname)
+            self.assertEqual(out, expected)
+
+    def test_list_command_verbose(self):
+        self.make_simple_tarfile(tmpname)
+        with support.captured_stdout() as t:
+            with tarfile.open(tmpname, 'r') as tf:
+                tf.list(verbose=True)
+        expected = t.getvalue().encode(sys.getfilesystemencoding())
+        for opt in '-v', '--verbose':
+            out = self.tarfilecmd(opt, '-l', tmpname)
+            self.assertEqual(out, expected)
+
+    def test_list_command_invalid_file(self):
+        zipname = support.findfile('zipdir.zip')
+        rc, out, err = self.tarfilecmd_failure('-l', zipname)
+        self.assertIn(b' is not a tar archive.', err)
+        self.assertEqual(out, b'')
+        self.assertEqual(rc, 1)
+
+    def test_create_command(self):
+        files = [support.findfile('tokenize_tests.txt'),
+                 support.findfile('tokenize_tests-no-coding-cookie-'
+                                  'and-utf8-bom-sig-only.txt')]
+        for opt in '-c', '--create':
+            try:
+                out = self.tarfilecmd(opt, tmpname, *files)
+                self.assertEqual(out, b'')
+                with tarfile.open(tmpname) as tar:
+                    tar.getmembers()
+            finally:
+                support.unlink(tmpname)
+
+    def test_create_command_verbose(self):
+        files = [support.findfile('tokenize_tests.txt'),
+                 support.findfile('tokenize_tests-no-coding-cookie-'
+                                  'and-utf8-bom-sig-only.txt')]
+        for opt in '-v', '--verbose':
+            try:
+                out = self.tarfilecmd(opt, '-c', tmpname, *files)
+                self.assertIn(b' file created.', out)
+                with tarfile.open(tmpname) as tar:
+                    tar.getmembers()
+            finally:
+                support.unlink(tmpname)
+
+    def test_create_command_dotless_filename(self):
+        files = [support.findfile('tokenize_tests.txt')]
+        try:
+            out = self.tarfilecmd('-c', dotlessname, *files)
+            self.assertEqual(out, b'')
+            with tarfile.open(dotlessname) as tar:
+                tar.getmembers()
+        finally:
+            support.unlink(dotlessname)
+
+    def test_create_command_dot_started_filename(self):
+        tar_name = os.path.join(TEMPDIR, ".testtar")
+        files = [support.findfile('tokenize_tests.txt')]
+        try:
+            out = self.tarfilecmd('-c', tar_name, *files)
+            self.assertEqual(out, b'')
+            with tarfile.open(tar_name) as tar:
+                tar.getmembers()
+        finally:
+            support.unlink(tar_name)
+
+    def test_extract_command(self):
+        self.make_simple_tarfile(tmpname)
+        for opt in '-e', '--extract':
+            try:
+                with support.temp_cwd(tarextdir):
+                    out = self.tarfilecmd(opt, tmpname)
+                self.assertEqual(out, b'')
+            finally:
+                support.rmtree(tarextdir)
+
+    def test_extract_command_verbose(self):
+        self.make_simple_tarfile(tmpname)
+        for opt in '-v', '--verbose':
+            try:
+                with support.temp_cwd(tarextdir):
+                    out = self.tarfilecmd(opt, '-e', tmpname)
+                self.assertIn(b' file is extracted.', out)
+            finally:
+                support.rmtree(tarextdir)
+
+    def test_extract_command_different_directory(self):
+        self.make_simple_tarfile(tmpname)
+        try:
+            with support.temp_cwd(tarextdir):
+                out = self.tarfilecmd('-e', tmpname, 'spamdir')
+            self.assertEqual(out, b'')
+        finally:
+            support.rmtree(tarextdir)
+
+    def test_extract_command_invalid_file(self):
+        zipname = support.findfile('zipdir.zip')
+        with support.temp_cwd(tarextdir):
+            rc, out, err = self.tarfilecmd_failure('-e', zipname)
+        self.assertIn(b' is not a tar archive.', err)
+        self.assertEqual(out, b'')
+        self.assertEqual(rc, 1)
+
+
 class ContextManagerTest(unittest.TestCase):
 
     def test_basic(self):
diff --git a/Misc/NEWS b/Misc/NEWS
index cf6cc0e..ddd9cf6 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -68,6 +68,9 @@
 Library
 -------
 
+- Issue #13477: Added command line interface to the tarfile module.
+  Original patch by Berker Peksag.
+
 - Issue #19674: inspect.signature() now produces a correct signature
   for some builtins.