diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index b8b65b98e83..d7e3b5ff2b3 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -591,6 +591,67 @@ A :class:`TarInfo` object also provides some convenient query methods: 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 + --list + + List files in a tarfile. + +.. cmdoption:: -c + --create + + Create tarfile from source files. + +.. cmdoption:: -e [] + --extract [] + + Extract tarfile into the current directory if *output_dir* is not specified. + +.. cmdoption:: -t + --test + + 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 94bd75d10ef..f4df6c7b0ac 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2404,3 +2404,97 @@ def is_tarfile(name): 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='', + help='Show listing of a tarfile') + group.add_argument('-e', '--extract', nargs='+', + metavar=('', ''), + help='Extract tarfile into target dir') + group.add_argument('-c', '--create', nargs='+', + metavar=('', ''), + help='Create tarfile from sources') + group.add_argument('-t', '--test', metavar='', + 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 2f2bf6b8bec..886a80e2275 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -7,7 +7,7 @@ from hashlib import md5 import unittest import tarfile -from test import support +from test import support, script_helper # Check for our compression modules. try: @@ -27,11 +27,13 @@ def md5sum(data): 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 @@ class MiscTest(unittest.TestCase): 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 cf6cc0e57e5..ddd9cf6926f 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -68,6 +68,9 @@ Core and Builtins 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.