forked from xot/ssst
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ssst-replacetag
executable file
·224 lines (197 loc) · 6.82 KB
/
ssst-replacetag
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/local/bin/python3
#
# SSST-REPLACETAG
#
# Manage tags in a site maintained with SSST
#
# Author: Jaap-henk Hoepman ([email protected])
import argparse, sys, shutil, yaml
from pathlib import Path
# ---
# CONSTANTS / PARAMETERS
# ---
# Path to the root of the source files tree
src_root = Path('./src')
# Keep backup
backup = False
# Verbosity
loglevel = 1
# logfile name
logfile = None
# Abort after warning
pedantic = False
# Only report found matches
dry = False
# Suppress warning messages
silent = False
# ---
# ERRORS / LOGGING
# ---
# Outpur the error message, and log it when required. Then exit
# - msg: the error message
def error(msg: str):
m = 'ssst-replacetag: error: %s\n' % str(msg)
sys.stderr.write(m)
if logfile != None:
logfile.write(m)
sys.exit (1)
# Output the warning message, and write it to the log file if it was specified.
# Exit when pedantic processing was specified.
# - msg: the warning message
def warning(msg: str):
m = 'ssst-replacetag: warning: %s\n' % str(msg)
if not silent:
sys.stderr.write(m)
if logfile != None:
logfile.write(m)
if pedantic:
sys.exit (1)
# Output a log message when its level is lower than the specifief verbosity
# on the command line. Always write the message to the log file if it was
# specified
# - level: the log-level for this message
# - msg: the log message
def log(level: int, msg: str):
if level == 0:
m = '# '
else:
m = '-' * level + ' '
m = m + str(msg) + '\n'
if (level <= loglevel):
sys.stdout.write(m)
if logfile != None:
logfile.write(m)
# ---
# Utility
# ---
# Return an iterator that iterates over all files in the tree rooted at
# src_root with the specified suffix
# - suffix: suffix of files to return, including "."
def src_files_with_suffix(suffix: str):
return Path(src_root).glob('**/*' + suffix)
# ---
# YAML
# ---
def get_yaml_dict(src: Path):
try:
with open(src,'r') as f:
yamls = yaml.load_all(f,Loader=yaml.SafeLoader)
for y in yamls:
return y
except:
warning('Invalid YAML block in ' + str(src))
return {}
# ---
# SUBROUTINES
# ---
# Replace the header
# - item
# - yaml_header: new header
def replace_yaml_header(item: Path, yaml_header):
# copy the post to its destination, replacing the yaml
# block with the one with the correct keywords
if backup:
shutil.copy2(item,str(item) + '.bak')
with open(item,'r') as inf:
s = inf.read()
# the content is after the second occurance of the
# YAML separator '---'
content = s.split('---',maxsplit=2)[2]
with open(str(item),'w') as outf:
yaml.dump(yaml_header,outf,explicit_start=True)
outf.write('---\n')
outf.write(content)
# Process all posts in the tree rooted at stc_root, and replace keyword
# with replacement
def process_post (item: Path, keyword: str, replacement: str):
log(2,'Processing post: ' + str(item))
yaml_header = get_yaml_dict(item)
# warn for missing or unknown keywords/tags
if not 'keywords' in yaml_header:
warning(f'Post {item} is missing a keyword/tag')
return False
else:
keywords = yaml_header['keywords']
# careful: keywords may be a single string
if type(keywords) is str:
keywords = [ keywords ]
# check if keyword in keywords, and if so, replace with replacement
if keyword in keywords:
foundmsg = f"Keyword '{keyword}' found in {item}."
if not dry:
new_keywords = [ k for k in keywords if not (k == keyword)]
if len(replacement) > 0: # replace or remove keyword
log(1,f"{foundmsg} Replacing with '{replacement}'.")
replacement_list = replacement.split(',')
for e in replacement_list:
new_keywords.append(e)
else:
log(1,f"{foundmsg} Removing it.")
yaml_header['keywords'] = new_keywords
replace_yaml_header(item,yaml_header)
else:
log(1,foundmsg)
return True
else:
return False
# Process all posts in the tree rooted at stc_root, and replace keyword
# with replacement
def process_posts (keyword,replacement):
log(0,'Processing posts.')
if dry:
log(0,f"Looking for all occurances of '{keyword}'.")
else:
if len(replacement) == 0:
log(0,f"Removing, not replacing, all occurances of '{keyword}'.")
else:
log(0,f"Replacing all occurances of '{keyword}' with '{replacement}'.")
# Find all posts (they have suffix .md) and process their metadata
count = 0
for item in src_files_with_suffix('.md'):
if process_post(item,keyword,replacement):
count += 1
log(0,f'{count} matches found.')
# ---
# MAIN
# ---
# Set up arguments parsing
parser = argparse.ArgumentParser(description='SSST-replacetag. Replace a tag in a post.')
parser.add_argument('-s', '--source', default=src_root,
help='Path to the root of the source files tree')
parser.add_argument('-v', '--verbosity', default=loglevel,
help='Verbosity (1, the default, reports which replacements took place)')
parser.add_argument('-l', '--logfile', default='ssst-replacetag.log',
help='Name of file to store all log messages in')
parser.add_argument('-S', '--silent', default = False, action='store_true',
help='Suppress warning messages.')
parser.add_argument('-p', '--pedantic', default = False, action='store_true',
help='Abort after warning')
parser.add_argument('-d', '--dry', default = False, action='store_true',
help='Only report found matches but don\'t change anything')
parser.add_argument('-b', '--backup', default = False, action='store_true',
help='Keep bakcup of source file when changed.')
parser.add_argument('keyword',
help='Keyword')
parser.add_argument('replacement', nargs = '?', default = '',
help='Replacement keywords, separated by comma. If empty, keyword is removed')
args = parser.parse_args()
# Apply arguments and check validity
src_root = Path(args.source)
if not src_root.exists():
error('Source tree %s does not exist.' % src_root)
loglevel = int(args.verbosity)
logname = args.logfile
backup = args.backup
dry = args.dry
# make sure logmessages with found keywords are actually displayed
if dry and (loglevel < 1):
loglevel = 1
silent = args.silent
pedantic = args.pedantic
# open logfile
if logname != '':
logfile = open(logname,'w')
process_posts(args.keyword,args.replacement)
# close the logfile if necessary
if logfile != None:
logfile.close()