#! /usr/bin/env python3 __version__ = '0.1' import fnmatch import os import re import sys import tarfile def compile_patterns(patterns): if not patterns: return None return re.compile('|'.join(map(fnmatch.translate, patterns))).match def yes(msg): print(msg, '(y/N)', end=' ', file=sys.stderr) return input().lstrip()[:1].lower() == 'y' def filter_member(args, op, tarinfo): if (args.newer_mtime is not None and tarinfo.mtime < args.newer_mtime and (args.newer is None or getattr(tarinfo, 'ctime', 0) < args.newer)): return None if args.confirmation and not yes('%s %r' % (op, tarinfo.name)): return None if args.numeric_owner: tarinfo.uname = '' tarinfo.gname = '' return tarinfo def filtered_members(args, included, excluded, members): for tarinfo in members: if excluded is not None and excluded(tarinfo.name): continue if included is not None and not included(tarinfo.name): continue tarinfo = filter_member(args, 'extract', tarinfo) if tarinfo is None: continue yield tarinfo def tar_open(args, tar_mode, **kwargs): fileobj = None if args.file == '-': if tar_mode == 'r': fileobj = sys.stdin.buffer else: fileobj = sys.stdout.buffer if args.compression is not None: tar_mode += ':' + args.compression return tarfile.TarFile.open(args.file, tar_mode, fileobj, bufsize=args.blocking_factor * tarfile.BLOCKSIZE, **kwargs) def do_list(parser, args): included = compile_patterns(args.files) excluded = compile_patterns(args.exclude) args.confirmation = False with tar_open(args, 'r') as tf: members = filtered_members(args, included, excluded, tf) tf.list(verbose=args.verbose, members=members) def do_extract(parser, args): included = compile_patterns(args.files) excluded = compile_patterns(args.exclude) curdir = os.curdir if args.directory is None else args.directory with tar_open(args, 'r') as tf: if args.same_owner: tf.chown = lambda tarinfo, targetpath: None if args.same_permissions: tf.chmod = lambda tarinfo, targetpath: None if args.touch: tf.utime = lambda tarinfo, targetpath: None members = filtered_members(args, included, excluded, tf) tf.extractall(path=curdir, members=members) def do_create(parser, args): do_write(parser, args, 'w') def do_append(parser, args): do_write(parser, args, 'a') def do_write(parser, args, tar_mode): excluded = compile_patterns(args.exclude) with tar_open(args, tar_mode, format=formats[args.format]) as tf: if args.directory is not None: oldcwd = os.getcwd() os.chdir(args.directory) try: def filter(tarinfo): return filter_member(args, 'add', tarinfo) for file_name in args.files: if excluded is not None and excluded(file_name): continue tf.add(file_name, recursive=args.recursion, filter=filter) finally: if args.directory is not None: os.chdir(oldcwd) def do_update(parser, args): # TODO pass formats = { 'gnu': tarfile.GNU_FORMAT, 'pax': tarfile.PAX_FORMAT, 'posix': tarfile.PAX_FORMAT, 'ustar': tarfile.USTAR_FORMAT, } date_formats = [ '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M', '%Y-%m-%d', '%a %b %d %H:%M:%S %Y', '%b %d %H:%M:%S %Y', '%a %b %d %H:%M %Y', '%b %d %H:%M %Y', '%a %b %d %H:%M %Y', '%b %d %H:%M %Y', '%a %b %d %Y', '%b %d %Y', ] def parse_datetime(value): if value[:1] == '.' or os.path.isabs(value): return os.stat(value).st_mtime value = ' '.join(value.split()) for format in date_formats: try: return time.mktime(time.strptime(value, format)) except ValueError: pass return time.mktime(time.strptime(value)) def main(): is_root = hasattr(os, 'geteuid') and os.geteuid() == 0 import argparse description = 'The Python implementation of the tar archiving utility.' parser = argparse.ArgumentParser(description=description, add_help=False) group = parser.add_mutually_exclusive_group() group.add_argument('-c', '--create', dest='operation', action='store_const', const=do_create, help='create a new archive') group.add_argument('-r', '--append', dest='operation', action='store_const', const=do_append, help='append files to the end of an archive') group.add_argument('-t', '--list', dest='operation', action='store_const', const=do_list, help='list the contents of an archive') # group.add_argument('-u', '--update', # dest='operation', action='store_const', const=do_update, # help='only append files newer than copy in archive') group.add_argument('-x', '--extract', dest='operation', action='store_const', const=do_extract, help='extract files from an archive') compr_group = parser.add_mutually_exclusive_group() parser.add_argument('-b', '--blocking-factor', metavar='BLOCKS', type=int, default=20, help='BLOCKS x 512 bytes per record') parser.add_argument('-C', '--directory', metavar='DIR', help='change to directory DIR') parser.add_argument('--exclude', metavar='PATTERN', action='append', default=[], help='exclude files, given as a PATTERN') parser.add_argument('--help', action='help', help='show this help message and exit') parser.add_argument('-f', '--file', metavar='ARCHIVE', default='-', help='use archive file ARCHIVE') parser.add_argument('--format', metavar='FORMAT', choices=sorted(formats), default='gnu', help='create archive of the given format (%s)' % ', '.join(sorted(formats))) # parser.add_argument('-h', '--dereference', action='store_true', # help='follow symlinks; archive and dump the files they ' # 'point to') compr_group.add_argument('-j', '--bzip2', dest='compression', action='store_const', const='bz2') compr_group.add_argument('-J', '--xz', dest='compression', action='store_const', const='xz') # parser.add_argument('-k', '--keep-old-files', action='store_true', # help="don't replace existing files when extracting") # parser.add_argument('--keep-newer-files', action='store_true', # help="don't replace existing files that are newer than " # "their archive copies") # parser.add_argument('-l', '--check-links', action='store_true', # help='print a message if not all links are dumped') parser.add_argument('-m', '--touch', action='store_true', help="don't extract file modified time") parser.add_argument('--newer', metavar='DATE-OR-FILE', type=parse_datetime, help='only store files newer than DATE-OR-FILE') parser.add_argument('--newer-mtime', metavar='DATA', type=parse_datetime, help='compare date and time when data changed only') parser.add_argument('--null', action='store_true', help='-T reads null-terminated names') parser.add_argument('--numeric-owner', action='store_true', help='always use numbers for user/group names') parser.add_argument('-O', '--to-stdout', action='store_true', help='extract files to standard output') # parser.add_argument('--one-file-system', action='store_true', # help='stay in local file system when creating archive') parser.add_argument('-p', '--preserve-permissions', '--same-permissions', dest='same_permissions', action='store_true', default=is_root, help='extract information about file permissions ' '(default for superuser)') parser.add_argument('--no-same-permissions', dest='same_permissions', action='store_const', const=False, help="apply the user's umask when extracting " "permissions from the archive (default for " "ordinary users)") # parser.add_argument('-P', '--absolute-names', '--absolute-paths', # action='store_true', # help="don't strip leading `/'s from file names") parser.add_argument('--posix', dest='format', action='store_const', const='posix', help='same as --format=posix') parser.add_argument('--recursion', action='store_true', default=True, help='recurse into directories (default)') parser.add_argument('--no-recursion', dest='recursion', action='store_const', const=False, help='avoid descending automatically in directories') # parser.add_argument('-S', '--sparse', action='store_true', # help='handle sparse files efficiently') parser.add_argument('--same-owner', action='store_true', default=is_root, help='try extracting files with the same ownership as ' 'exists in the archive (default for superuser)') parser.add_argument('--no-same-owner', dest='same_owner', action='store_const', const=False, help='extract files as yourself (default for ordinary ' 'users)') # parser.add_argument('--strip-components', metavar='NUMBER', type=int, # help='strip NUMBER leading components from file names ' # 'on extraction') parser.add_argument('-T', '--files-from', metavar='FILE', action='append', default=[], help='get names to extract or create from FILE') # parser.add_argument('--totals', action='store_true', # help='print total bytes after processing the archive;') # parser.add_argument('-U', '--unlink-first', action='store_true', # help='remove each file prior to extracting over it') parser.add_argument('-v', '--verbose', action='count', help='verbosely list files processed') parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('-w', '--confirmation', '--interactive', action='store_true', help='ask for confirmation for every action') parser.add_argument('-X', '--exclude-from', metavar='FILE', action='append', default=[], help='exclude patterns listed in FILE') compr_group.add_argument('-z', '--gunzip', '--gzip', dest='compression', action='store_const', const='gz') parser.add_argument('files', nargs='*') if len(sys.argv) >= 2 and sys.argv[1] and sys.argv[1][0] in 'crtux': sys.argv[1] = '-' + sys.argv[1] args = parser.parse_args() if args.operation is None: parser.exit(1, parser.format_help()) args.newer_mtime = args.newer or args.newer_mtime for path in args.files_from: f = sys.stdin if path == '-' else open(path, 'r') with f: if args.null: args.files += f.read().split('\0') else: args.files += f.read().splitlines() for path in args.exclude_from: with open(path, 'r') as f: args.exclude += f.read().splitlines() try: args.operation(parser, args) except (tarfile.TarError, OSError) as err: parser.exit(1, str(err)) if __name__ == '__main__': main()