Created
July 5, 2025 06:19
-
-
Save cumulus13/232de4dee81b6d450fcd81339eb629d8 to your computer and use it in GitHub Desktop.
copy cli with more options, this my tool, use '-h' for usage
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/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