#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 1998-2026 Stephane Galland # # This program is free library; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or any later version. # # This library is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; see the file COPYING. If not, # write to the Free Software Foundation, Inc., 59 Temple Place - Suite # 330, Boston, MA 02111-1307, USA. import logging import os import re import fnmatch from argparse import Namespace from typing import override from autolatex2.cli.abstract_actions import AbstractMakerAction import autolatex2.utils.utilfunctions as genutils from autolatex2.tex.utils import FileType from autolatex2.translator.translatorrepository import TranslatorRepository from autolatex2.translator.translatorrunner import TranslatorRunner from autolatex2.utils.i18n import T, TT class MakerAction(AbstractMakerAction): id : str = 'clean' help : str = T('Clean the current working directory by removing all LaTeX temp files and other temp files that are created during the processing of the document') CLEANABLE_FILE_EXTENSIONS : list[str] = [ '.aux', '.bcf', '.bbl', '.blg', '.bmt', '.brf', '.cb', '.fls', '.glg', '.glo', '.gls', '.idx', '.ilg', '.ind', '.lbl', '.loa', '.loc', '.loe', '.lof', '.log', '.lom', '.los', '.lot', '.maf', '.mtc', '.mtf', '.mtl', '.nav', '.out', '.run.xml', '.snm', '.soc', '.spl', '.thlodef', '.tmp', '.toc', '.vrb', '.xdy', ] CLEANABLE_FILE_PATTERNS : list[str] = [ '\\.goutputstream-.+', '\\.mtc[0-9]+', '\\.mtf[0-9]+', '\\.mtl[0-9]+', ] ANYWHERE_CLEANABLE_FILE_EXTENSIONS : list[str] = [ '~', '.back', '.backup', '.bak', ] def __init__(self): super().__init__() self.__nb_deletions = 0 @override def _add_command_cli_arguments(self, command_name : str, command_help : str | None, command_aliases : list[str] | None): """ Callback for creating the CLI arguments (positional and optional). :param command_name: The name of the command. :type command_name: str :param command_help: The help text for the command. :type command_help: str | None """ self.parse_cli.add_argument('--nochdir', action = 'store_true', help = T('Don\'t set the current directory of the application to document\'s root directory before the launch of the building process')) self.parse_cli.add_argument('--norecursive', action = 'store_true', help = T('Disable cleaning of the subfolders')) self.parse_cli.add_argument('--simulate', action = 'store_true', help = T('Simulate the removal of the files, i.e. the files are not removed from the disk')) if command_name == 'clean': self.parse_cli.add_argument('--all', action = 'store_true', help = T('If specified, the cleaning command behaves as the command \'cleanall\'')) @override def run(self, cli_arguments : Namespace) -> bool: """ Callback for running the command. :param cli_arguments: the successfully parsed CLI arguments. :type cli_arguments: Namespace :return: True if the process could continue. False if an error occurred and the process should stop. :rtype: bool """ self.__nb_deletions = 0 if not self.run_clean_command(cli_arguments): return False if cli_arguments.all: if not self.run_cleanall_command(cli_arguments): return False self._show_deletions_message(cli_arguments) return True def _delete_file(self, filename : str, simulate : bool): if simulate: msg = T("Selecting: %s") % filename else: msg = T("Deleting: %s") % filename logging.info(msg) if not simulate: genutils.unlink(filename) self.__nb_deletions = self.__nb_deletions + 1 def run_clean_command(self, cli_arguments : Namespace) -> bool: """ Run the command 'clean'. :param cli_arguments: the arguments. :return: True if the process could continue. False if an error occurred and the process should stop. """ old_dir = os.getcwd() try: ddir = self.configuration.document_directory if ddir and not cli_arguments.nochdir: os.chdir(ddir) maker = self._internal_create_maker() # Prepare used-defined list of deletable files clean_files = self.configuration.clean.clean_files abs_clean_files = list() bn_clean_files = list() for file in clean_files: if os.sep in file: abs_clean_files.append(file) else: bn_clean_files.append(file) for root_file in maker.root_files: root_dir = os.path.dirname(root_file) if cli_arguments.norecursive: root = os.path.dirname(root_file) for filename in os.listdir(root): abs_filename = os.path.normpath(os.path.join(root, filename)) if self._is_deletable_in_root_folder_only(root, root_file, abs_filename)\ or self._is_deletable_from_constants(abs_filename)\ or self._is_deletable_shell(abs_filename, filename, abs_clean_files, bn_clean_files): self._delete_file(abs_filename, cli_arguments.simulate) else: for root, dirs, files in os.walk(os.path.dirname(root_file)): for filename in files: abs_filename = os.path.normpath(os.path.join(root, filename)) if (root == root_dir and self._is_deletable_in_root_folder_only(root, root_file, abs_filename))\ or self._is_deletable_from_constants(abs_filename)\ or self._is_deletable_shell(abs_filename, filename, abs_clean_files, bn_clean_files): self._delete_file(abs_filename, cli_arguments.simulate) finally: os.chdir(old_dir) return True # noinspection PyMethodMayBeStatic def _is_deletable_anywhere(self, filename : str) -> bool: """ Replies if the given filename is for a deletable file anywhere. """ fnl = filename.lower() if os.name == 'nt' else filename for ext in MakerAction.ANYWHERE_CLEANABLE_FILE_EXTENSIONS: if fnl.endswith(ext): return True return False # noinspection PyMethodMayBeStatic def _is_deletable_in_root_folder_only(self, root_dir : str, tex_filename : str, filename : str) -> bool: """ Replies if the given filename is for a deletable file in the root folder. """ basename = genutils.basename2(os.path.join(root_dir, tex_filename), *FileType.tex_extensions()) candidates = [ os.path.normpath(os.path.join(root_dir, '.autolatex_stamp')), os.path.normpath(os.path.join(root_dir, 'autolatex_stamp')), os.path.normpath(os.path.join(root_dir, 'autolatex_exec_stderr.log')), # For old version of the program os.path.normpath(os.path.join(root_dir, 'autolatex_exec_stdout.log')), # For old version of the program os.path.normpath(os.path.join(root_dir, 'autolatex_exec_stdin.log')), # For old version of the program os.path.normpath(os.path.join(root_dir, 'autolatex_autogenerated.tex')), basename + ".pdf", basename + ".dvi", basename + ".xdvi", basename + ".xdv", basename + ".ps", basename + ".synctex.gz", basename + ".synctex", ] return filename in candidates # noinspection PyMethodMayBeStatic def _is_deletable_from_constants(self, filename : str) -> bool: """ Replies if the given filename is for a deletable file anywhere. """ fnl = filename.lower() if os.name == 'nt' else filename for ext in MakerAction.CLEANABLE_FILE_EXTENSIONS: if fnl.endswith(ext): return True for ext in MakerAction.CLEANABLE_FILE_EXTENSIONS: if re.match(ext + '$', fnl): return True return False # noinspection PyMethodMayBeStatic def _is_deletable_shell(self, filename : str, basename : str, absolute_shell_patterns : list[str], basename_shell_patterns : list[str]) -> bool: """ Replies if the given filename Unix Shell pattern is for a deletable file anywhere. """ for pattern in absolute_shell_patterns: if fnmatch.fnmatch(filename, pattern): return True for pattern in basename_shell_patterns: if fnmatch.fnmatch(basename, pattern): return True return False def _get_generated_images(self) -> list[str]: repository = TranslatorRepository(self.configuration) runner = TranslatorRunner(repository) runner.sync() images = runner.get_source_images() ddir = self.configuration.document_directory image_list : list[str] = list() for image in images: for target in runner.get_target_files(in_file=image): if ddir: abspath = genutils.abs_path(target, ddir) else: abspath = os.path.abspath(target) image_list.append(abspath) return image_list def run_cleanall_command(self, cli_arguments : Namespace) -> bool: """ Run the command 'cleanall' or '--all' optional argument. :param cli_arguments: the arguments. :return: True if the process could continue. False if an error occurred and the process should stop. """ # Remove additional files # noinspection DuplicatedCode maker = self._internal_create_maker() # Prepare used-defined list of deletable files clean_files = self.configuration.clean.cleanall_files abs_clean_files = list() bn_clean_files = list() for file in clean_files: if os.sep in file: abs_clean_files.append(file) else: bn_clean_files.append(file) for root_file in maker.root_files: root_dir = os.path.dirname(root_file) if cli_arguments.norecursive: root = os.path.dirname(root_file) for filename in os.listdir(root): abs_filename = os.path.normpath(os.path.join(root, filename)) if self._is_deletable_anywhere(abs_filename)\ or self._is_deletable_shell(abs_filename, filename, abs_clean_files, bn_clean_files): self._delete_file(abs_filename, cli_arguments.simulate) else: for root, dirs, files in os.walk(os.path.dirname(root_file)): for filename in files: abs_filename = os.path.normpath(os.path.join(root, filename)) if root == root_dir and self._is_deletable_anywhere(abs_filename)\ or self._is_deletable_shell(abs_filename, filename, abs_clean_files, bn_clean_files): self._delete_file(abs_filename, cli_arguments.simulate) for generated_image in self._get_generated_images(): self._delete_file(generated_image, cli_arguments.simulate) return True def _show_deletions_message(self, cli_arguments : Namespace): """ Show the conclusion message. :param cli_arguments: The CLI arguments. :type cli_arguments: argparse object """ if cli_arguments.simulate: msg = TT("%d file were selected as deletion candidates", "%d files were selected as deletion candidates", self.__nb_deletions) else: msg = TT("%d file was deleted", "%d files were deleted", self.__nb_deletions) % self.__nb_deletions logging.info(msg)