diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -586,16 +586,71 @@ A :class:`TarInfo` object also provides Return :const:`True` if it is a FIFO. .. method:: TarInfo.isdev() 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. + +You can create a new tar archive by passing one or more files as arguments that +follow the :option:`-c` option and the filename for the resultant tarfile:: + + $ 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 +:option:`-e` option:: + + $ python -m tarfile -e monty.tar + +For a list of the files in a tar archive, use :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 + -x + --extract + --extract [] + + Extract tarfile into the current directory if *output_dir* is not specified. + +.. cmdoption:: -t + --test + + Test whether the tarfile is valid or not. + + .. _tar-examples: Examples -------- How to extract an entire tar archive to the current working directory:: import tarfile diff --git a/Lib/tarfile.py b/Lib/tarfile.py --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2421,8 +2421,90 @@ def is_tarfile(name): t = open(name) t.close() return True except TarError: return False bltn_open = open open = TarFile.open + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='A simple command line ' + 'interface for tarfile module.') + parser.add_argument('-l', '--list', metavar='') + parser.add_argument('-x', '-e', '--extract', nargs='+', + metavar=('', '')) + parser.add_argument('-c', '--create', nargs='+', + metavar=('', '')) + parser.add_argument('-t', '--test', metavar='') + args = parser.parse_args() + + if args.test: + src = args.test + if is_tarfile(src): + print('{:s} is a tar archive.'.format(src)) + else: + print('{:s} is not a tar archive.'.format(src), file=sys.stderr) + sys.exit(1) + + elif args.list: + src = args.list + if is_tarfile(src): + with TarFile.open(src) as tf: + tf.list(verbose=False) + else: + print('{:s} is not a tar archive.'.format(src), file=sys.stderr) + sys.exit(1) + + 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.print_help() + if is_tarfile(src): + with TarFile.open(src) as tf: + tf.extractall(path=curdir) + if curdir == '.': + msg = '{:s} file is extracted.'.format(src) + else: + msg = '{:s} file is extracted into {:s} directory.'.format(src, curdir) + print(msg) + else: + print('{:s} is not a tar archive.'.format(src), file=sys.stderr) + sys.exit(1) + + elif args.create: + def determine_mode(mode): + try: + mode = mode.rpartition('.')[2] + except AttributeError: + return 'w' + if mode in ('gz', 'bz2', 'xz'): + mode = 'w:' + mode + elif mode == 'tgz': + mode = 'gz' + elif mode in ('tbz', 'tbz2', 'tb2'): + mode = 'bz2' + elif mode == 'txz': + mode = 'xz' + else: + mode = 'w' + return mode + + target_dir = args.create.pop(0) + mode = determine_mode(args.create) + with TarFile.open(target_dir, mode) as tf: + for file_name in args.create: + tf.add(file_name) + print('{:s} file created.'.format(target_dir)) + + else: + parser.print_help() + +if __name__ == '__main__': + main() diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4,17 +4,17 @@ import io import shutil import io from hashlib import md5 import errno import unittest import tarfile -from test import support +from test import support, script_helper # Check for our compression modules. try: import gzip gzip.GzipFile except (ImportError, AttributeError): gzip = None try: @@ -1639,16 +1639,78 @@ class MiscTest(unittest.TestCase): def test_number_field_limits(self): self.assertRaises(ValueError, tarfile.itn, -1, 8, tarfile.USTAR_FORMAT) self.assertRaises(ValueError, tarfile.itn, 0o10000000, 8, tarfile.USTAR_FORMAT) self.assertRaises(ValueError, tarfile.itn, -0x10000000001, 6, tarfile.GNU_FORMAT) self.assertRaises(ValueError, tarfile.itn, 0x10000000000, 6, tarfile.GNU_FORMAT) +class CommandLineTest(unittest.TestCase): + + def tarfilecmd(self, *args, expected_success=True): + if expected_success: + run_cmd = script_helper.assert_python_ok + else: + run_cmd = script_helper.assert_python_failure + rc, out, err = run_cmd('-m', 'tarfile', *args) + return out if not rc else err + + def test_test_command(self): + for tar_name in (tarname, gzipname, bz2name, xzname): + out = self.tarfilecmd('-t', tar_name) + self.assertIn(b'is a tar archive.\n', out) + + def test_test_command_invalid_file(self): + zipname = support.findfile('zipdir.zip') + out = self.tarfilecmd('-t', zipname, expected_success=False) + self.assertEqual(zipname.encode('ascii') + b' is not a tar archive.', + out) + + def test_list_command(self): + tardatas = (support.findfile('tokenize_tests.txt'), + support.findfile('tokenize_tests-no-coding-cookie-' + 'and-utf8-bom-sig-only.txt'),) + with support.captured_stdout() as t: + with tarfile.open(tmpname, 'w') as tf: + for tardata in tardatas: + tf.add(tardata, arcname=os.path.basename(tardata)) + with tarfile.open(tmpname, 'r') as tf: + tf.list(verbose=False) + out = self.tarfilecmd('-l', tmpname) + self.assertEqual(out, b''.join(map(lambda s: s.encode('ascii'), + t.getvalue()))) + + def test_list_command_invalid_file(self): + zipname = support.findfile('zipdir.zip') + out = self.tarfilecmd('-l', zipname, expected_success=False) + self.assertEqual(zipname.encode('ascii') + b' is not a tar archive.', + out) + + def test_create_command(self): + tardatas = (support.findfile('tokenize_tests.txt'), + support.findfile('tokenize_tests-no-coding-cookie-' + 'and-utf8-bom-sig-only.txt'),) + out = self.tarfilecmd('-c', tmpname, *tardatas) + self.assertEqual(tmpname.encode('ascii') + b' file created.\n', out) + + def test_extract_command(self): + with support.temp_cwd(TEMPDIR + 'ext'): + out = self.tarfilecmd('-e', tmpname) + self.assertEqual(tmpname.encode('ascii') + b' file is extracted.\n', + out) + + def test_extract_command_invalid_file(self): + zipname = support.findfile('zipdir.zip') + with support.temp_cwd(TEMPDIR + 'ext'): + out = self.tarfilecmd('-e', zipname, expected_success=False) + self.assertEqual(zipname.encode('ascii') + b' is not a tar archive.', + out) + + class ContextManagerTest(unittest.TestCase): def test_basic(self): with tarfile.open(tarname) as tar: self.assertFalse(tar.closed, "closed inside runtime context") self.assertTrue(tar.closed, "context manager failed") def test_closed(self): @@ -1836,16 +1898,17 @@ def test_main(): PaxWriteTest, UstarUnicodeTest, GNUUnicodeTest, PAXUnicodeTest, AppendTest, LimitsTest, MiscTest, ContextManagerTest, + CommandLineTest, ] if hasattr(os, "link"): tests.append(HardlinkTest) else: tests.append(LinkEmulationTest) with open(tarname, "rb") as fobj: