6
\$\begingroup\$

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 &)

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

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,
                                     epilog='''<your help string>''')
    
        # Next you can add arguments like this...
        parser.add_argument('source', type=str, help='source directory')
        parser.add_argument('destination', type=str, help='destination directory')
        parser.add_argument('-t', '--types', type=str, help='included types')
        ...
    
        # 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"""

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.