# Copy directories while excluding or isolating certain extensions

I often find myself wanting to copy directories around with either:

1. having to copy only specific extensions, or
2. having to exclude specific extensions.

For this task, I wrote some Python which I am calling from the cmd.

The Python script is humbly called copy_master.py and I have also set an environment variable for it called %Pcopy%.

copy_master.py

def classify(dirs):
return {(1, 0): dirs, (0, 1): dirs[::-1], (0, 0): [None, None], (1, 1): [1, 1]}[tuple(map(os.path.isdir, dirs))]

def include(d, files):
return [f for f in files if not any(f.endswith('.' + x) for x in sys.argv[4].split(',')) and not os.path.isdir(os.path.join(d, f))]

def refuse(d, files):
return [f for f in files if any(f.endswith('.' + x) for x in sys.argv[4].split(',')) and not os.path.isdir(os.path.join(d, f))]

if __name__ == '__main__':
import shutil
import os
import sys

if len(sys.argv) < 3 or sys.argv[1].lower() == 'help':
print('\nParameters:: \n\t > Source and Destination directories: order does not matter; one of the specified '
'directories MUST NOT exist and this is taken to be the destination. \n\t > Mode: (+) for copying specific'
' file extensions only OR (-) for skipping specific extensions. \n\t > Extensions: comma delimited list of'
' file extensions to be copied or ignored.\n\nExample Usage:\n\t{0:} {1:} + bdf  -> Copies only bdf files '
'from {1:} to {0:}\n\t{0:} {1:} - f06,xdb  -> Copies everything but f06 & xbd files from {1:} to {0:}'.format(r'I:\do\not\exist', r'I:\have\the\files\you\want'))
elif len(sys.argv) > 2:
src, dst = classify(sys.argv[1:3])
if src is None:
raise IOError('The source directory should exist!')
elif src == 1:
raise IOError('The destination directory should not exist!')

if len(sys.argv) == 5:
mode = sys.argv[3]
print('\rCopying...', end='')
shutil.copytree(src, dst, ignore={'+': include, '-': refuse}[mode])
print('\rCopying completed')
else:
q = input('Extensions and execution mode have not been specified. Everything is going to be copied! Proceed (Y/N)?\n::')
if q.lower() in ['n', 'no']:
exit()
else:
print('Copying...')
shutil.copytree(src, dst)


Example usages (with the command prompt open, type):

• copy only specific extension:

%Pcopy% I:\do\not\exist I:\have\the\files\you\want + txt
#                                                  ^

• exclude specific extensions from the copying:

%Pcopy% I:\do\not\exist I:\have\the\files\you\want - xlsx,bat
#                                                  ^


There are a couple of things I am not so happy\sure about:

1. Have I sacrificed too much readability in the classify() function? Are dicts meant to be (ab)used like that? What should I be returning in the (1, 1) case (if both directories exist)?
2. Is the use of global sys.argv in include() and refuse() ok?
3. How can I handle the different ways a user might use the script in the best possible way? Not quite happy about the if statements in the __main__.

Apart from the points discussed above, feel free to suggest other changes or even an entirely different approach. And by all means, use the script if you like it ;)

Bonus Points: Currently, directories that have to be quoted to be recognized by the cmd are not handled (e.g., C:\users\Example\This&That, note the &)

• Not 100% sure it fits, but have you considered the glob module with the "glob" patterns to match files and directories? – alecxe Dec 7 '17 at 14:24
• @alecxe I know about it but somehow did not consider it for the include() and refuse(). Will look into it. – Ev. Kounis Dec 7 '17 at 14:28

Is the use of global sys.argv in include() and refuse() ok?

• At the moment it's not really readable, since sys.argv[4] doesn't say much... This could be solved by using argparse or naming your sys.argv to variables

How can I handle the different ways a user might use the script in the best possible way? Not quite happy about the if statements in the __main__.

• Use argeparse that will relieve the clutter from the if __name__ == '__main__' and make it easier to add optional parameters in your script.

def parse_arguments():
"""Arguments parser."""

parser = argparse.ArgumentParser(usage='%(prog)s [optional] <source> <destination>',
description='Copy master',
formatter_class=argparse.RawDescriptionHelpFormatter,

# Next you can add arguments like this...
...

# Parse the arguments to the object
args = parser.parse_args()

# Check if all args are set or something
if not args.types:
parser.error('No typyes are specified')
return args

# Now you can call this from the main
if __name__ == '__main__':
import argparse
args = parse_arguments()
# And call your arguments like...
# args.source, etc...


# Bonus

• imports should be at the top of the file!

At the moment when this script is imported to another module, it will not import all dependencies.

Note that you can import argparse in __main__, since this will on be executed from the cli

• (Subjective style) You might consider to chop lines for better readability

return [f for f in files if any(f.endswith('.' + x) for x in sys.argv[4].split(',')) and not os.path.isdir(os.path.join(d, f))]


Could be this:

return [file
for file in files if any(file.endswith('.' + x)
for x in sys.argv[4].split(','))
and not os.path.isdir(os.path.join(d, f))]

• At your help string consider using a heredoc string

"""Heredoc string""" Or consider setting it as the docstring __doc__ = """Help"""