Skip to content

Instantly share code, notes, and snippets.

@cumulus13
Created July 5, 2025 06:19
Show Gist options
  • Save cumulus13/232de4dee81b6d450fcd81339eb629d8 to your computer and use it in GitHub Desktop.
Save cumulus13/232de4dee81b6d450fcd81339eb629d8 to your computer and use it in GitHub Desktop.
copy cli with more options, this my tool, use '-h' for usage
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import re
import traceback
import argparse
import glob
import shutil
from pathlib import Path
__version__ = "2.0"
__filename__ = os.path.basename(sys.argv[0])
__author__ = "licface"
__url__ = "[email protected]"
__target__ = "all"
__build__ = "3.x"
def usage():
"""Display help information"""
parser = argparse.ArgumentParser(
description="File/Directory Copying Tool",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s file.txt newname.txt # Simple copy
%(prog)s "*.txt" "*.bak" # Copy all .txt to .bak
%(prog)s "*.txt" "*.bak" -d # Recursive copy
%(prog)s -U file.txt # Copy with uppercase name
%(prog)s -l file.txt # Copy with lowercase name
%(prog)s -T file.txt # Copy with title case
%(prog)s file.txt /dest/folder/ # Copy to different folder
"""
)
parser.add_argument('source', nargs='?', help='Source file/pattern')
parser.add_argument('destination', nargs='?', help='Destination file/pattern/folder')
parser.add_argument('-e', '--with-extension',
help='Copy File/Dirs with extension',
action='store_true')
parser.add_argument('-n', '--no-extension',
help='No copy extension, name file/dirs only',
action='store_true')
parser.add_argument('-c', '--count',
help='Copy with add count (000-*) if file exists',
action='store_true')
parser.add_argument('-o', '--overwrite',
help='Copy file and overwrite file exists only',
action='store_true')
parser.add_argument('-s', '--start-count',
help='Start count, with end unlimited',
type=int, default=1)
parser.add_argument('-U', '--upper',
help='Copy file to UpperCase name',
action='store_true')
parser.add_argument('-l', '--lower',
help='Copy file to LowerCase name',
action='store_true')
parser.add_argument('-T', '--title',
help='Copy file to title case (first letter of each word)',
action='store_true')
parser.add_argument('-r', '--recursive',
help='Copy file with/and subdirectory',
action='store_true')
parser.add_argument('-d', '--depth',
help='Recursive depth search',
action='store_true')
parser.add_argument('-f', '--find',
help='Find and copy file with pattern',
action='store_true')
parser.add_argument('-p', '--pattern',
help='Pattern for find and copy function')
parser.add_argument('--preserve-structure',
help='Preserve directory structure when copying recursively',
action='store_true')
parser.add_argument('--dry-run',
help='Show what would be copied without actually copying',
action='store_true')
parser.add_argument('--move',
help='Move files instead of copy (like cut-paste)',
action='store_true')
if len(sys.argv) == 1:
parser.print_help()
return None
return parser.parse_args()
def get_files_by_pattern(pattern, recursive=False):
"""Get files matching pattern"""
files = []
if recursive:
# Use pathlib for recursive search
if os.path.dirname(pattern):
base_dir = Path(os.path.dirname(pattern))
file_pattern = os.path.basename(pattern)
else:
base_dir = Path('.')
file_pattern = pattern
files = list(base_dir.rglob(file_pattern))
else:
files = [Path(f) for f in glob.glob(pattern)]
return files
def transform_filename(filename, args):
"""Apply filename transformations"""
name, ext = os.path.splitext(filename)
# Apply case transformations
if args.upper:
if args.no_extension:
name = name.upper()
else:
filename = filename.upper()
elif args.lower:
if args.no_extension:
name = name.lower()
else:
filename = filename.lower()
elif args.title:
if args.no_extension:
name = name.title()
else:
filename = filename.title()
# Reconstruct filename if only name was changed
if args.no_extension and (args.upper or args.lower or args.title):
filename = name + ext
return filename
def get_new_extension(old_ext, dest_pattern):
"""Extract new extension from destination pattern"""
if '*.' in dest_pattern:
return dest_pattern.split('*.')[-1]
return old_ext
def generate_unique_filename(filepath, args):
"""Generate unique filename if file exists"""
if not args.count:
return filepath
directory = os.path.dirname(filepath)
filename = os.path.basename(filepath)
name, ext = os.path.splitext(filename)
counter = args.start_count
while os.path.exists(filepath):
new_name = f"{name}_{counter:03d}{ext}"
filepath = os.path.join(directory, new_name)
counter += 1
return filepath
def copy_file(source_path, dest_path, args):
"""Copy a single file"""
try:
if args.dry_run:
action = "MOVE" if args.move else "COPY"
print(f"[DRY RUN] {action}: {source_path} --> {dest_path}")
return True
# Create destination directory if it doesn't exist
dest_dir = os.path.dirname(dest_path)
if dest_dir and not os.path.exists(dest_dir):
os.makedirs(dest_dir)
if os.path.exists(dest_path) and not args.overwrite:
if args.count:
dest_path = generate_unique_filename(dest_path, args)
else:
print(f"WARNING: {dest_path} already exists. Use -o to overwrite or -c to add counter.")
return False
if args.move:
shutil.move(source_path, dest_path)
print(f"MOVE: {source_path} --> {dest_path}")
else:
# Copy file with metadata
shutil.copy2(source_path, dest_path)
print(f"COPY: {source_path} --> {dest_path}")
return True
except Exception as e:
action = "moving" if args.move else "copying"
print(f"ERROR {action} {source_path}: {e}")
return False
def process_pattern_copy(source_pattern, dest_pattern, args):
"""Process pattern-based copying"""
files = get_files_by_pattern(source_pattern, args.recursive or args.depth)
if not files:
print(f"No files found matching pattern: {source_pattern}")
return
success_count = 0
# Check if destination is a directory
is_dest_dir = os.path.isdir(dest_pattern) or dest_pattern.endswith(('/', '\\'))
for file_path in files:
source_str = str(file_path)
if is_dest_dir:
# Copy to directory, preserve filename
filename = os.path.basename(source_str)
new_filename = transform_filename(filename, args)
dest_path = os.path.join(dest_pattern, new_filename)
else:
# Get directory and filename
if args.preserve_structure and args.recursive:
# Preserve directory structure
rel_path = os.path.relpath(source_str, os.path.dirname(source_pattern) or '.')
dest_path = os.path.join(os.path.dirname(dest_pattern) or '.', rel_path)
# Transform just the filename part
dest_dir = os.path.dirname(dest_path)
filename = os.path.basename(dest_path)
new_filename = transform_filename(filename, args)
dest_path = os.path.join(dest_dir, new_filename)
else:
directory = os.path.dirname(dest_pattern) if os.path.dirname(dest_pattern) else '.'
filename = os.path.basename(source_str)
# Apply transformations
new_filename = transform_filename(filename, args)
# Handle extension change from pattern
if '*.' in dest_pattern:
name, old_ext = os.path.splitext(new_filename)
new_ext = get_new_extension(old_ext, dest_pattern)
new_filename = name + '.' + new_ext
dest_path = os.path.join(directory, new_filename)
# Skip if source and destination are the same
if os.path.abspath(source_str) == os.path.abspath(dest_path):
continue
if copy_file(source_str, dest_path, args):
success_count += 1
action = "moved" if args.move else "copied"
print(f"\nCompleted: {success_count} files {action} successfully.")
def process_simple_copy(source, destination, args):
"""Process simple file copy"""
if not os.path.exists(source):
print(f"ERROR: Source file '{source}' not found!")
return
# Handle relative paths
if not os.path.isabs(source):
source = os.path.abspath(source)
# Check if destination is a directory
if os.path.isdir(destination):
# Copy to directory with transformed filename
filename = os.path.basename(source)
new_filename = transform_filename(filename, args)
destination = os.path.join(destination, new_filename)
else:
# Determine destination path
if not os.path.isabs(destination):
dest_dir = os.path.dirname(source)
destination = os.path.join(dest_dir, destination)
# If destination doesn't have extension but source does, preserve source extension
source_name, source_ext = os.path.splitext(os.path.basename(source))
dest_name, dest_ext = os.path.splitext(os.path.basename(destination))
if not dest_ext and source_ext and not args.no_extension:
# Preserve original extension
dest_filename = dest_name + source_ext
destination = os.path.join(os.path.dirname(destination), dest_filename)
# Apply transformations
dest_filename = os.path.basename(destination)
dest_filename = transform_filename(dest_filename, args)
destination = os.path.join(os.path.dirname(destination), dest_filename)
copy_file(source, destination, args)
def process_directory_copy(source, destination, args):
"""Process directory copying"""
if not os.path.isdir(source):
print(f"ERROR: Source directory '{source}' not found!")
return
try:
if args.dry_run:
action = "MOVE DIR" if args.move else "COPY DIR"
print(f"[DRY RUN] {action}: {source} --> {destination}")
return
if args.move:
shutil.move(source, destination)
print(f"MOVE DIR: {source} --> {destination}")
else:
shutil.copytree(source, destination, dirs_exist_ok=args.overwrite)
print(f"COPY DIR: {source} --> {destination}")
except Exception as e:
action = "moving" if args.move else "copying"
print(f"ERROR {action} directory {source}: {e}")
def main():
"""Main function"""
try:
args = usage()
if args is None:
return
# Handle case where only one argument is provided with transformation flags
if args.source and not args.destination:
if args.upper or args.lower or args.title:
# Copy the file in same directory with transformed name
source_path = args.source
if not os.path.exists(source_path):
print(f"ERROR: Source file '{source_path}' not found!")
return
directory = os.path.dirname(source_path) if os.path.dirname(source_path) else '.'
filename = os.path.basename(source_path)
new_filename = transform_filename(filename, args)
if filename != new_filename:
dest_path = os.path.join(directory, new_filename)
copy_file(source_path, dest_path, args)
else:
print("No changes needed.")
return
# Require both source and destination for other operations
if not args.source or not args.destination:
print("ERROR: Both source and destination are required!")
usage()
return
# Handle directory copying
if os.path.isdir(args.source) and not ('*' in args.source or '?' in args.source):
process_directory_copy(args.source, args.destination, args)
return
# Check if this is pattern-based copying
if ('*' in args.source or '?' in args.source or
'*' in args.destination or '?' in args.destination):
process_pattern_copy(args.source, args.destination, args)
else:
process_simple_copy(args.source, args.destination, args)
except KeyboardInterrupt:
print("\nOperation cancelled by user.")
except Exception as e:
print(f"\nUnexpected error: {e}")
if '--debug' in sys.argv:
traceback.print_exc()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment