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. + +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 files in a tarfile. + +.. cmdoption:: -c + + Create tarfile from source files. + +.. cmdoption:: -e + -e [] + + Extract tarfile into the current directory if *output_dir* is not specified. + +.. cmdoption:: -t + + 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 @@ -2399,8 +2399,89 @@ 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 + + description = 'A simple command line interface for tarfile module.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('-l', metavar='') + parser.add_argument('-e', nargs='+', metavar=('', '')) + parser.add_argument('-c', nargs='+', metavar=('', '')) + parser.add_argument('-t', metavar='') + args = parser.parse_args() + + if args.t: # test + src = args.t + if is_tarfile(src): + print('{:s} is a tar archive.'.format(src)) + else: + parser.exit(1, '{:s} is not a tar archive.\n'.format(src)) + + elif args.l: # list + src = args.l + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.list(verbose=False) + else: + parser.exit(1, '{:s} is not a tar archive.\n'.format(src)) + + elif args.e: # extract + if len(args.e) == 1: + src = args.e[0] + curdir = os.curdir + elif len(args.e) == 2: + src, curdir = args.e + else: + parser.exit(1, parser.format_help()) + + if is_tarfile(src): + with TarFile.open(src, 'r:*') as tf: + tf.extractall(path=curdir) + else: + parser.exit(1, '{:s} is not a tar archive.\n'.format(src)) + + elif args.c: # create + def determine_mode(mode): + try: + mode = mode.rpartition('.')[2] + except AttributeError: + return 'w' + + compressions = { + # gz + 'gz': 'gz', + 'tgz': 'gz', + # xz + 'xz': 'xz', + 'txz': 'xz', + # bz2 + 'bz2': 'bz2', + 'tbz': 'bz2', + 'tbz2': 'bz2', + 'tb2': 'bz2', + } + if mode in compressions: + return 'w:' + compressions[mode] + return 'w' + + tar_name = args.c.pop(0) + tar_mode = determine_mode(tar_name) + tar_files = args.c + + with TarFile.open(tar_name, tar_mode) as tf: + for file_name in tar_files: + tf.add(file_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 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -2,17 +2,17 @@ import sys import os import io import shutil from hashlib import md5 import unittest import tarfile -from test import support +from test import support, script_helper # Check for our compression modules. try: import gzip except ImportError: gzip = None try: import bz2 @@ -22,21 +22,23 @@ try: import lzma except ImportError: lzma = None 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" class TarTest: tarname = tarname suffix = '' @@ -1719,16 +1721,100 @@ class MiscTest(unittest.TestCase): with self.assertRaises(ValueError): tarfile.itn(0o10000000, 8, tarfile.USTAR_FORMAT) with self.assertRaises(ValueError): tarfile.itn(-0x10000000001, 6, tarfile.GNU_FORMAT) with 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, rc + + def test_test_command(self): + for tar_name in (tarname, gzipname, bz2name, xzname): + with self.subTest(name=tar_name): + out, rc = self.tarfilecmd('-t', tar_name) + self.assertIn(b'is a tar archive.\n', out) + self.assertEqual(rc, 0) + + def test_test_command_invalid_file(self): + zipname = support.findfile('zipdir.zip') + out, rc = self.tarfilecmd('-t', zipname, expected_success=False) + self.assertEqual(out, + zipname.encode('ascii') + b' is not a tar archive.') + self.assertEqual(rc, 1) + + 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, rc = self.tarfilecmd('-l', tmpname) + self.assertEqual(b''.join(map(lambda s: s.encode('ascii'), + t.getvalue())), + out) + self.assertEqual(rc, 0) + + def test_list_command_invalid_file(self): + zipname = support.findfile('zipdir.zip') + out, rc = self.tarfilecmd('-l', zipname, expected_success=False) + self.assertEqual(out, + zipname.encode('ascii') + b' is not a tar archive.') + self.assertEqual(rc, 1) + + 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, rc = self.tarfilecmd('-c', tmpname, *tardatas) + self.assertEqual(out, b'') + self.assertEqual(rc, 0) + + def test_create_command_dotless_filename(self): + out, rc = self.tarfilecmd('-c', dotlessname, + support.findfile('tokenize_tests.txt')) + self.assertEqual(out, b'') + self.assertEqual(rc, 0) + out, rc = self.tarfilecmd('-t', dotlessname) + self.assertIn(b'is a tar archive.\n', out) + self.assertEqual(rc, 0) + + def test_extract_command(self): + with support.temp_cwd(tarextdir): + out, rc = self.tarfilecmd('-e', tmpname) + self.assertEqual(out, b'') + self.assertEqual(rc, 0) + + def test_extract_command_different_directory(self): + with support.temp_cwd(tarextdir): + out, rc = self.tarfilecmd('-e', tmpname, 'spamdir') + self.assertEqual(out, b'') + self.assertEqual(rc, 0) + + def test_extract_command_invalid_file(self): + zipname = support.findfile('zipdir.zip') + with support.temp_cwd(tarextdir): + out, rc = self.tarfilecmd('-e', zipname, expected_success=False) + expected = zipname.encode('ascii') + b' is not a tar archive.' + self.assertEqual(out, expected) + + 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):