Last active
September 25, 2021 13:51
-
-
Save igorvolnyi/c936662d4e3f435c71c513036e672bb2 to your computer and use it in GitHub Desktop.
Move or copy files from subdirectories into current directory and remove empty subdirectories (in case of moving)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/python | |
''' | |
Move or copy files from subdirectories of specified directories into them or into current directory | |
and optionally remove the empty subdirectories (in case of moving). | |
TODO: add exception handling. | |
''' | |
import sys | |
# No point to check if major version of python is less than 3. | |
if sys.version_info.major == 3 and sys.version_info.minor < 7: | |
print('At least version 3.7 of python is required to run this program.') | |
sys.exit(4) | |
import os, shutil, getopt | |
def usage(): | |
return f'''Move or copy files from subdirectories into current directory. | |
{sys.argv[0]} [OPTIONS] [PATH [PATH ...]] | |
OPTIONS: | |
-h, --help - show this help and exit. | |
-c, --copy - copy files. | |
-m, --move - move files. | |
-k, --keep-subdirs, --dkeep - keep empty subdirectories. Ignored if -c or --copy is specified or no other options. | |
-x PATTERN, --exclude-dir=PATTERN - do not touch subdirectories matching PATTERN. Can be specified multiple times. | |
If both copy and move options pecified, last one is used. | |
If PATHs are not specified, current directory is used. | |
Without any options the program performs a dry run.''' | |
def main(): | |
for_real = False | |
move = False # True = move files and remove subdirs, False = copy files and leave subdirs alone. | |
keep_subdirs = False | |
exclude_dirs = [] | |
paths = [] | |
this_dir = os.getcwd() | |
try: | |
opts, args = getopt.getopt(sys.argv[1:], 'hcmkx:', ['help', 'move', 'copy', 'keep-subdirs', 'dkeep', 'exclude-dir=']) | |
except getopt.GetoptError as err: | |
print(err) | |
print(usage()) | |
return 2 | |
if len(opts) == 0: | |
print("\nThis is a dry run. To actually perform changes, add '-m' (move) or '-c' (copy) after the program name.\n") | |
if len(args) > 0: | |
for path in args: | |
if os.path.islink(path) or not os.path.isdir(path): | |
# User may specify paths as a glob pattern. So skip regular files and symlinks. | |
print(f"{path} is not a directory. Skipping") | |
continue | |
else: | |
paths.append(path) | |
else: | |
paths.append(os.getcwd()) | |
for opt, arg in opts: | |
if opt in ['-h', '--help']: | |
print(usage()) | |
return 0 | |
if opt in ['-m', '--move', '-c', '--copy']: | |
for_real = True | |
if opt in ['-m', '--move']: | |
move = True | |
if opt in ['-c', '--copy']: | |
move = False | |
if opt in ['-k', '--keep-subdirs', '--dkeep']: | |
keep_subdirs = True | |
if opt in ['-x', '--exclude-dir']: | |
exclude_dirs.append(arg) | |
for path in paths: | |
os.chdir(path) | |
cwd = os.getcwd() | |
dlist = os.walk(cwd) | |
for cur_dir, dirs, files in dlist: | |
if os.path.basename(cur_dir) in exclude_dirs: | |
print(f"Skipping {cur_dir}") | |
continue | |
if cur_dir != cwd: | |
for f in files: | |
to_move = os.path.join(cur_dir, f) | |
print(to_move) | |
dest_file_name = os.path.join(cwd, f) | |
# In case destination file exists we need to provide a new name. | |
dest_file, dest_ext = os.path.splitext(dest_file_name) | |
rename_number = 0 # zero means no renaming. | |
while os.path.exists(dest_file+(('_'+str(rename_number)) if rename_number > 0 else '')+dest_ext): | |
rename_number += 1 | |
if rename_number > 0: | |
new_file, ext = os.path.splitext(dest_file_name) | |
dest_file_name = new_file+'_'+str(rename_number)+ext | |
if for_real: | |
if move: | |
os.rename(to_move, dest_file_name) | |
else: | |
shutil.copy(to_move, dest_file_name) | |
if move and not keep_subdirs: | |
# Remove empty directories recursively | |
entries = os.scandir() | |
for e in entries: | |
if e.is_dir() and os.path.basename(e.path) not in exclude_dirs: | |
print("Removing empty folder:", e.path) | |
shutil.rmtree(e.path) | |
os.chdir(this_dir) | |
if not for_real: | |
print("\nThis was a test run. To actually perform changes, add '-m' (move) or '-c' (copy) after the program name.") | |
return 0 | |
if __name__ == '__main__': | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment