Skip to content

Instantly share code, notes, and snippets.

@igorvolnyi
Last active September 25, 2021 13:51
Show Gist options
  • Save igorvolnyi/c936662d4e3f435c71c513036e672bb2 to your computer and use it in GitHub Desktop.
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)
#! /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