diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a367a..91340bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [//]: # (- **Security** in case of vulnerabilities.) +## [1.1.0] - 2024-11-15 +### Added +- Dry-run mode and recursive symlink handling. +- add --update flag to check for new version. + +### Changed +- Changed logo + +### Removed +- Removed debug argument option, it wasn't even implemented + ## [1.0.0] - 2024-10-07 - Released CrossRename +[1.1.0]: https://github.com/Jemeni11/CrossRename/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/Jemeni11/CrossRename/releases/tag/v1.0.0 diff --git a/CrossRename/main.py b/CrossRename/main.py index 5d943a0..0617739 100644 --- a/CrossRename/main.py +++ b/CrossRename/main.py @@ -4,8 +4,10 @@ from pathlib import Path import argparse import logging +from .utils import check_for_update + +__version__ = "1.1.0" -__version__ = "1.0.0" logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s » %(message)s') @@ -61,26 +63,44 @@ def sanitize_filename(filename: str) -> str: return sanitized -def rename_file(file_path: str) -> None: +def rename_file(file_path: str, dry_run: bool = False) -> None: directory, filename = os.path.split(file_path) new_filename = sanitize_filename(filename) if new_filename != filename: new_file_path = os.path.join(directory, new_filename) - try: - os.rename(file_path, new_file_path) - logger.info(f"Renamed: {filename} -> {new_filename}") - except Exception as e: - logger.error(f"Error renaming {filename}: {str(e)}") + if dry_run: + logger.info(f"[Dry-run] Would rename: {filename} -> {new_filename}") + else: + try: + os.rename(file_path, new_file_path) + logger.info(f"Renamed: {filename} -> {new_filename}") + except Exception as e: + logger.error(f"Error renaming {filename}: {str(e)}") else: logger.info(f"No change needed: {filename}") def file_search(directory: str) -> list[str]: file_list = [] - for root, _, files in os.walk(directory): + visited_paths = set() + + for root, _, files in os.walk(directory, followlinks=False): + real_root = os.path.realpath(root) + + if real_root in visited_paths: + logger.warning(f"Skipping recursive symlink in {root}") + continue + + visited_paths.add(real_root) + for file in files: - file_list.append(os.path.join(root, file)) + file_path = os.path.join(root, file) + if os.path.islink(file_path): + logger.info(f"Skipping symlink: {file_path}") + continue + file_list.append(file_path) + return file_list @@ -88,7 +108,7 @@ def main() -> None: try: parser = argparse.ArgumentParser( description="CrossRename: Harmonize file names for Linux and Windows.") - parser.add_argument("-p", "--path", help="The path to the file or directory to rename.", required=True) + parser.add_argument("-p", "--path", help="The path to the file or directory to rename.") parser.add_argument( "-v", "--version", @@ -96,32 +116,44 @@ def main() -> None: action='version', version=f"CrossRename Version {__version__}" ) + parser.add_argument( + "-u", "--update", + help="Check if a new version is available.", + action="store_true" + ) parser.add_argument( "-r", "--recursive", help="Rename all files in the directory path given and its subdirectories.", action="store_true" ) - args = parser.parse_args() + parser.add_argument("-d", "--dry-run", help="Perform a dry run, logging changes without renaming.", + action="store_true") + args = parser.parse_args() path = args.path recursive = args.recursive + dry_run = args.dry_run + + if args.update: + check_for_update(__version__) + sys.exit() if path is None: - sys.exit("Please provide a path to a file or directory using the --path argument.") + sys.exit("Error: Please provide a path to a file or directory using the --path argument.") if os.path.isfile(path): - rename_file(path) + rename_file(path, dry_run) elif os.path.isdir(path): if recursive: file_list = file_search(path) for file_path in file_list: - rename_file(file_path) + rename_file(file_path, dry_run) else: for item in os.listdir(path): item_path = os.path.join(path, item) if os.path.isfile(item_path): - rename_file(item_path) + rename_file(item_path, dry_run) else: sys.exit(f"Error: {path} is not a valid file or directory") except Exception as e: diff --git a/CrossRename/utils.py b/CrossRename/utils.py new file mode 100644 index 0000000..06ad4f5 --- /dev/null +++ b/CrossRename/utils.py @@ -0,0 +1,25 @@ +from urllib import request, error +import json + + +def parse_version(version: str): + """Converts a version string (e.g., '1.2.3') into a tuple of integers (1, 2, 3).""" + return tuple([int(part) for part in version.split(".")]) + + +def check_for_update(current_version: str): + """Checks if a new version of CrossRename is available on PyPI.""" + try: + url = "https://pypi.org/pypi/CrossRename/json" + with request.urlopen(url, timeout=5) as response: + data = json.load(response) + latest_version = data["info"]["version"] + + if parse_version(latest_version) > parse_version(current_version): + print(f"Update available: v{latest_version}. You're on v{current_version}.") + print(f"Run `pip install --upgrade CrossRename` to update.") + else: + print(f"You're on the latest version: v{current_version}.") + + except error.URLError as e: + print(f"Unable to check for updates: {e}") diff --git a/PYPI_README.rst b/PYPI_README.rst index e3f0122..2200756 100644 --- a/PYPI_README.rst +++ b/PYPI_README.rst @@ -33,7 +33,9 @@ Features - Handles both individual files and entire directories - Supports recursive renaming of files in subdirectories - Preserves file extensions, including compound extensions like .tar.gz -- Provides informative logging with optional debug mode +- Provides informative logging +- Provides a dry-run mode to preview renaming changes without executing them +- Skips recursive symlinks to avoid infinite loops Installation ============ @@ -50,16 +52,17 @@ Usage :: - usage: crossrename [-h] -p PATH [-d] [-v] [-r] + usage: crossrename [-h] [-p PATH] [-v] [-u] [-r] [-d] CrossRename: Harmonize file names for Linux and Windows. options: -h, --help show this help message and exit -p PATH, --path PATH The path to the file or directory to rename. - -d, --debug Enable debug mode. -v, --version Prints out the current version and quits. + -u, --update Check if a new version is available. -r, --recursive Rename all files in the directory path given and its subdirectories. + -d, --dry-run Perform a dry run, logging changes without renaming. Examples -------- @@ -76,16 +79,30 @@ Rename all files in a directory (and its subdirectories): crossrename -p /path/to/directory -r +Perform a dry run to preview renaming changes without executing them: + +:: + + crossrename -p /path/to/directory -r -d + + +Check for an update: + +:: + + crossrename -u + + Why did I build this? ===================== .. warning:: - I’m no longer dual booting. I’m only using Windows 10 now. I do have - WSL2 and that’s what I use for testing. I don’t know if there’ll be + I'm no longer dual booting. I'm only using Windows 10 now. I do have + WSL2 and that's what I use for testing. I don't know if there'll be any difference in the way the tool works on a native Linux system. -I’m a dual-booter running Windows 10 and Lubuntu 22.04. One day +I'm a dual-booter running Windows 10 and Lubuntu 22.04. One day (literally yesterday lol), while transferring a folder between the two systems, I hit a naming roadblock. Five stubborn files refused to budge, thanks to the quirky differences in file naming rules between Linux and @@ -104,14 +121,14 @@ smooth, worry-free file management. Contributing ============ -Contributions are welcome! If you’d like to improve CrossRename or add +Contributions are welcome! If you'd like to improve CrossRename or add support for other operating systems (like macOS), please feel free to submit a pull request. Wait a minute, who are you? =========================== -Hello there! I’m Emmanuel Jemeni, and while I primarily work as a +Hello there! I'm Emmanuel Jemeni, and while I primarily work as a Frontend Developer, Python holds a special place as my first programming language. You can find me on various platforms: diff --git a/README.md b/README.md index 1c62418..5128fa8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ when transferring files between different environments. - Handles both individual files and entire directories - Supports recursive renaming of files in subdirectories - Preserves file extensions, including compound extensions like .tar.gz -- Provides informative logging with optional debug mode +- Provides informative logging +- Provides a dry-run mode to preview renaming changes without executing them +- Skips recursive symlinks to avoid infinite loops
@@ -58,16 +60,18 @@ pip install CrossRename ## Usage ``` -usage: crossrename [-h] -p PATH [-d] [-v] [-r] +usage: crossrename [-h] [-p PATH] [-v] [-u] [-r] [-d] CrossRename: Harmonize file names for Linux and Windows. options: -h, --help show this help message and exit -p PATH, --path PATH The path to the file or directory to rename. - -d, --debug Enable debug mode. -v, --version Prints out the current version and quits. + -u, --update Check if a new version is available. -r, --recursive Rename all files in the directory path given and its subdirectories. + -d, --dry-run Perform a dry run, logging changes without renaming. + ``` @@ -87,6 +91,18 @@ Rename all files in a directory (and its subdirectories ): crossrename -p /path/to/directory -r ``` +Perform a dry run to preview renaming changes without executing them: + +``` +crossrename -p /path/to/directory -r -d +``` + +Check for an update: + +``` +crossrename -u +``` + ## Why did I build this? diff --git a/logo.png b/logo.png index d40c2cd..55904b2 100644 Binary files a/logo.png and b/logo.png differ diff --git a/pyproject.toml b/pyproject.toml index f59cdee..237b61e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "CrossRename" -version = "1.0.0" +version = "1.1.0" authors = [ { name="Emmanuel C. Jemeni", email="jemenichinonso11@gmail.com" } ]