summaryrefslogtreecommitdiff
path: root/macros/generic/memoize/memoize-clean.py
blob: 1833931bf031424aec7c941a294bc11b04107494 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python

# This file is a part of Memoize, a TeX package for externalization of
# graphics and memoization of compilation results in general, available at
# https://ctan.org/pkg/memoize and https://github.com/sasozivanovic/memoize.
#
# Copyright (c) 2020- Saso Zivanovic <saso.zivanovic@guest.arnes.si>
#
# This work may be distributed and/or modified under the conditions of the
# LaTeX Project Public License, either version 1.3c of this license or (at
# your option) any later version.  The latest version of this license is in
# https://www.latex-project.org/lppl.txt and version 1.3c or later is part of
# all distributions of LaTeX version 2008 or later.
#
# This work has the LPPL maintenance status `maintained'.
# The Current Maintainer of this work is Saso Zivanovic.
# 
# The files belonging to this work and covered by LPPL are listed in
# <texmf>/doc/generic/memoize/FILES.

__version__ = '2024/01/21 v1.1.2'

import argparse, re, sys, pathlib, os

parser = argparse.ArgumentParser(
    description="Remove (stale) memo and extern files.",
    epilog = "For details, see the man page or the Memoize documentation "
             "(https://ctan.org/pkg/memoize)."
)
parser.add_argument('--yes', '-y', action = 'store_true',
                    help = 'Do not ask for confirmation.')
parser.add_argument('--all', '-a', action = 'store_true',
                    help = 'Remove *all* memos and externs.')
parser.add_argument('--quiet', '-q', action = 'store_true')
parser.add_argument('--prefix', '-p', action = 'append', default = [],
    help = 'A path prefix to clean; this option can be specified multiple times.')
parser.add_argument('mmz', nargs= '*', help='.mmz record files')
parser.add_argument('--version', '-V', action = 'version',
                    version = f"%(prog)s of Memoize " + __version__)
args = parser.parse_args()

re_prefix = re.compile(r'\\mmzPrefix *{(.*?)}')
re_memo = re.compile(r'%? *\\mmz(?:New|Used)(?:CC?Memo|Extern) *{(.*?)}')
re_endinput = re.compile(r' *\\endinput *$')

prefixes = set(pathlib.Path(prefix).resolve() for prefix in args.prefix)
keep = set()

# We loop through the given .mmz files, adding prefixes to whatever manually
# specified by the user, and collecting the files to keep.
for mmz_fn in args.mmz:
    mmz = pathlib.Path(mmz_fn)
    mmz_parent = mmz.parent.resolve()
    try:
        with open(mmz) as mmz_fh:
            prefix = ''
            endinput = False
            empty = None
            for line in mmz_fh:
                line = line.strip()
                
                if not line:
                    pass
                
                elif endinput:
                    raise RuntimeError(
                        rf'Bailing out, '
                        rf'\endinput is not the last line of file {mmz_fn}.')
                
                elif m := re_prefix.match(line):
                    prefix = m[1]
                    prefixes.add( (mmz_parent/prefix).resolve() )
                    if empty is None:
                        empty = True

                elif m := re_memo.match(line):
                    if not prefix:
                        raise RuntimeError(
                            f'Bailing out, no prefix announced before file "{m[1]}".')
                    if not m[1].startswith(prefix):
                        raise RuntimeError(
                            f'Bailing out, prefix of file "{m[1]}" does not match '
                            f'the last announced prefix ({prefix}).')
                    keep.add((mmz_parent / m[1]))
                    empty = False

                elif re_endinput.match(line):
                    endinput = True
                    continue

                else:
                    raise RuntimeError(fr"Bailing out, "
                        fr"file {mmz_fn} contains an unrecognized line: {line}")
            
        if empty and not args.all:
            raise RuntimeError(fr'Bailing out, file {mmz_fn} is empty.')

        if not endinput and empty is not None and not args.all:
            raise RuntimeError(
                fr'Bailing out, file {mmz_fn} does not end with \endinput; '
                fr'this could mean that the compilation did not finish properly. '
                fr'You can only clean with --all.'
            )
        
    # It is not an error if the file doesn't exist.
    # Otherwise, cleaning from scripts would be cumbersome.
    except FileNotFoundError:
        pass

tbdeleted = []
def populate_tbdeleted(folder, basename_prefix):
    re_aux = re.compile(
        re.escape(basename_prefix) + 
        '[0-9A-F]{32}(?:-[0-9A-F]{32})?'
        '(?:-[0-9]+)?(?:\.memo|(?:-[0-9]+)?\.pdf|\.log)$')
    try:
        for f in folder.iterdir():
            if re_aux.match(f.name) and (args.all or f not in keep):
                tbdeleted.append(f)
    except FileNotFoundError:
        pass

for prefix in prefixes:
    # "prefix" is interpreted both as a directory (if it exists) and a basename prefix.
    if prefix.is_dir():
        populate_tbdeleted(prefix, '')
    populate_tbdeleted(prefix.parent, prefix.name)

allowed_dirs = [pathlib.Path().absolute()] # todo: output directory
deletion_not_allowed = [f for f in tbdeleted if not f.is_relative_to(*allowed_dirs)]
if deletion_not_allowed:
    raise RuntimeError("Bailing out, "
        "I was asked to delete these files outside the current directory:\n" +
        "\n".join(str(f) for f in deletion_not_allowed))
        
_cwd_absolute = pathlib.Path().absolute()
def relativize(path):
    try:
        return path.relative_to(_cwd_absolute)
    except ValueError:
        return path

if tbdeleted:
    tbdeleted.sort()
    if not args.yes:
        print('I will delete the following files:')
        for f in tbdeleted:
            print(relativize(f))
        print("Proceed (y/n)? ")
        a = input()
    if args.yes or a == 'y' or a == 'yes':
        for f in tbdeleted:
            if not args.quiet:
                print("Deleting", relativize(f))
            try:
                f.unlink()
            except FileNotFoundError:
                print(f"Cannot delete {f}")
    else:
        print("Bailing out.")
elif not args.quiet:
    print('Nothing to do, the directory seems clean.')

# Local Variables:
# fill-column: 79
# after-save-hook: py2dtx
# End: