source: ietf @ 33

Revision 33, 43.1 KB checked in by paul.hoffman@vpnc.org, 10 months ago (diff)

1.15: fixed a problem with the RFC Editor files, and forced arguments to lowercase

  • Property svn:executable set to *
Line 
1#!/usr/bin/python
2from __future__ import print_function
3from cmd import Cmd
4from fnmatch import filter as fnfilter
5from glob import glob
6from json import dump as jsondump, load as jsonload
7from optparse import OptionParser
8from os import chdir, environ, link, listdir, mkdir, system, unlink
9from os.path import basename, exists as pathexists, expanduser, isdir, join as pathjoin
10from pdb import set_trace as trace
11from re import compile, search, sub
12from subprocess import Popen, PIPE
13from sys import argv, excepthook
14from time import strftime
15from xml.etree import ElementTree as ElTree
16
17'''
18Program to help command-line users have better access to IETF-related documents
19   See help text below.
20'''
21__version__ = "1.15"
22__license__ = "https://en.wikipedia.org/wiki/WTFPL"
23
24# Version history:
25#  1.0
26#    Initial release
27#  1.1
28#    Added bcp
29#    Split out the command-calling in the CLI to make it clearer
30#  1.2
31#    Added more places to look for the config file
32#    Fixed help text for "charter" to make wildcard use clearer
33#    Added auth48
34#  1.3
35#    Added the WTFPL license
36#    Added "rfcextra " which opens RFCs that replace the requested RFC and the errata page for the
37#       requested RFC if the database says there is errata
38#    Added "rfcstatus" which lists RFC status from the RFC Editor's database
39#    Added "draftstatus" which lists the I-D status from the Datatracer
40#    Added "author" command which lists all drafts and RFCs by an author
41#    Added parsing the Datatracker's I-D database during mirror; file is saved as a local JSON database
42#    Added parsing the RFC Editor's database during mirror; file is saved as a local JSON database
43#    Added the --quiet command line option
44#    Made "tracker" also work for RFCs
45#    Made the help a bit more helpful
46#    Fixed bug that found too many drafts in in-notes/authors
47#    Fixed bug so that the help text fit in 80 column windows
48#    Fixed bug for finding the configuration file
49#  1.4
50#    Added "iesg docs" and "iesg agenda"
51#    Fixed bug with displaying multiple drafts that have the same name beginnings
52#  1.5
53#          Made changes to get new-style charters with "charter-new"
54#  1.6
55#    Added "charternew"
56#    Added "conflict"
57#  1.7
58#    Got rid of "charter" because new WGs don't work with old charter
59#  1.8
60#    Added .encode('utf-8') to author and AD names in draftstatus because those might be UTF-8
61#       Clearly, this needs to be dealt with better in a future version
62#  1.9
63#    Added "rfcinfo", and changed "tools" to only go to the tools.ietf.org site
64#  1.10
65#    Changed the rsync targets from www.ietf.org to rsync.ietf.org
66#  1.11
67#    Mirror conflict reviews and status changes when updating, but don't expose them in the UI
68#  1.12
69#    Changed all http: URIs to https:
70#  1.13
71#    Fixed ietf-rfc-status.json generation, and fixed a minor typo
72#  1.14
73#    Changed the location of the IESG directory, which had been broken for a while
74#  1.15
75#    Lower-cased the inputs to match arguments better
76#    Caused a bug in the AUTH48 HTMLs to not cause the program to die
77
78##########
79# Utility functions and definitions
80##########
81
82KnownCmds = ("auth48", "author", "bcp", "charter", "conflict", "diff", "draft", "draftstatus", "iesg", "mirror", \
83        "rfc", "rfcextra", "rfcinfo", "rfcstatus", "tools", "tracker", "foo")
84ConfigPlaces = ("~/bin/ietf.config", "/usr/local/bin/ietf.config", "~/.ietf/ietf.config")
85RFCZerosPat = compile(r'^0+(.*)')
86
87# Make a block of text that can be executed in the CLI
88CLICmdCode = ""
89for ThisCmd in KnownCmds:
90        ThisCode = '''
91def do_REPLTHISCMD(self, RestOfArgs):
92  Cmd_REPLTHISCMD(RestOfArgs.split(' '))
93def help_REPLTHISCMD(self):
94  CheckHelp('REPLTHISCMD', '__helptext__')
95'''
96        CLICmdCode += ThisCode.replace("REPLTHISCMD", ThisCmd)
97
98# Find a draft in the in-notes/authors directory, return "rfc1234" or ""
99def FindDraftInAuth48(basename):
100        TheDiffs = glob(pathjoin(FullRFCDir, "authors", "*-diff.html"))
101        for ThisDiff in TheDiffs:
102                try:
103                        InTextLines = open(ThisDiff, mode="r").readlines()
104                except:
105                        exit("Weird: could not read '" + ThisDiff + "' even though it exists. Exit.")
106                for InText in InTextLines[0:40]:
107                        ### The following try/except is needed due to erroneous non-ASCII text in some of these HTML files
108                        try:
109                                if InText.find("<strike><font color='red'>" + basename) > -1:
110                                        return(ThisDiff.replace(pathjoin(FullRFCDir, "authors", ""), "").replace("-diff.html", ""))
111                        except:
112                                pass
113        return("")  # Only here if there was no file in AUTH48
114
115# Open a URL in the browser, but give a warning in the terminal if the command is "less"
116def WebDisplay(TheURL, TheArg):
117        TheRet = system(DisplayWebCommand + TheURL + TheArg)
118        if TheRet > 0:
119                print("The command used to display web content, '" + DisplayWebCommand \
120                 + TheURL + TheArg + "', had an error.'")
121        if DisplayWebCommand == "less ":
122                print("The reason that this HTML was displayed on your console is that you do not have\n" \
123                        "'DisplayWebCommand' defined in the file '" + ConfigFile + "'.")
124
125# Create a command-line processor for our commands
126class OurCLI(Cmd):
127        intro = "Command line processor for ietf commands; try 'help' for more info."
128        prompt = "ietf: "
129        # Make just pressing Return not do anything
130        def emptyline(self):
131                pass
132        # Make it easy to exit
133        def do_exit(self, RestOfArgs):
134                return True
135        do_quit = do_q = do_exit
136        def do_EOF(self, RestOfArgs):
137                print()
138                return True
139        def default(self, RestOfArgs):
140                print("Unknown command '" + RestOfArgs + "'. Try 'help' for a list of commands.")
141        # Let them do shell commands
142        def do_shell(self, RestOfArgs):
143                print("Execuiting shell command: '" + RestOfArgs + "'")
144                system(RestOfArgs)
145        # Fill in the needed definitions for all the known commands
146        #   This was created as CLICmdCode above
147        exec(CLICmdCode)
148        # Do our own help
149        def do_help(self, RestOfArgs):
150                if RestOfArgs in KnownCmds:
151                        CheckHelp(RestOfArgs, "__helptext__")
152                else:
153                        CheckHelp("allclicmds", "__helptext__")
154        # Allow to change commandline settings
155        def do_tombstones(self, RestOfArgs):
156                global DisplayTombstones
157                DisplayTombstones = True
158        def do_maxdrafts(self, RestOfArgs):
159                try:
160                        global MaxDrafts
161                        MaxDrafts = int(RestOfArgs)
162                except:
163                        exit("The argument to 'maxdrafts' must be a positive integer. Exiting.")
164        def do_usedraftnumbers(self, RestOfArgs):
165                global UseDraftNumbers
166                UseDraftNumbers = True
167        def do_quiet(self, RestOfArgs):
168                global QuietDraft
169                QuietDraft = True
170
171# Print help text if this is called with no args or with a single arg of "__helptext__"
172#   All commands other than "mirror" need args.
173def CheckHelp(TheCaller, InArgs):
174        if ((InArgs == "__helptext__") or ((InArgs == []) and (TheCaller != "mirror"))):
175                if HelpText.get(TheCaller, "") != "":
176                        print(HelpText[TheCaller])
177                else:
178                        print("No help text available for '" + TheCaller + "'.")
179                return True
180        else:
181                return False
182
183HelpText = {
184        "auth48": '''auth48:
185    Takes a list of RFC numbers or draft names, determines if there are AUTH48
186    files associated with them, and displays the various files.''',
187        "bcp": '''bcp:
188    Takes a list of BCP numbers. Displays the BCP RFCs found using the text
189    dispay program. You can also give 'index' as an argument to see
190    bcp-index.txt.''',
191        "charter": '''charter:
192    Takes a list of WG names. Displays the charter for each WG using the text
193    dispay program. Wildcards are appended to the beginning and end of the
194    charter name given, and can also be given in the name. The charters are
195    gotten from the new-style charters in the "charter" directory, which was
196    begun in June 2012.''',
197  "conflict": '''conflict:
198    Takes a draft name (with or without the '-nn' version number or '.txt''
199    and displays the HTML conflict review, if it exists.''',
200        "diff": '''diff:
201    Takes a draft name (with or without the '-nn' version number or '.txt'
202    and displays the HTML diff between it and the preceding version on the
203    IETF Tools page using your web display program.''',
204        "draft": '''draft:
205    Takes a list of draft file names. Displays the drafts found using the text
206    dispay program. Substrings can be used instead of full names. There are
207    command-line options to change the way this shows tombstones (where a
208    draft has expired or been replaced with an RFC). You can also give
209    'abstracts' as an argument to see 1id-abstracts.txt.''',
210        "draftstatus": '''draftstatus:
211    Takes a list of draft names or substrings and reports the status from the
212    Datatracker database for each one''',
213  "iesg": '''iesg:
214    Displays the next agenda (when given the "agenda" argument") or the list
215    of documents under consideration (when given the "docs" argument) in the
216    web display program''',
217        "mirror": '''mirror:
218    Updates your local mirror of IETF directories, such as all drafts, RFCs,
219    and WG charters.''',
220        "rfc": '''rfc:
221    Takes a list of RFC file names. Displays the RFCs found using the text
222    dispay program. You do not need to give 'rfc' or '.txt' in the file
223    names. You can also give 'index' as an argument to see rfc-index.txt.
224    This command searches both the main RFC directory and the pre-publication
225    (AUTH48) directory. It will automatically open RFCs that obsolete and
226    update the one given, and will open errata in the browser if the RFC
227    Editor's database indicates that such errata exists.''',
228        "rfcextra": '''rfcextra:
229    Similar to 'rfc' but opens additional files. It will automatically open
230    RFCs that obsolete and update the one given, and will open errata in the
231    browser if the RFC Editor's database indicates that such errata exists.''',
232        "rfcinfo": '''rfcinfo:
233    Takes a list of RFC numbers and opens the info pages from the RFC Editor's
234    web site''',
235        "rfcstatus": '''rfcstatus:
236    Takes a list of RFC numbers and reports the status from the RFC Editor's
237    database for each one''',
238        "tools": '''tools:
239    Takes a list of draft file names, RFC names, and/or WG names. Displays the
240    result from the IETF Tools pages in the web dispay program. Draft names
241    can be either complete or be missing the '-nn' version number and '.txt'.
242    RFC names can be given as 'rfc1234' or '1234'. WG names are matched
243    exactly.''',
244        "tracker": '''tracker:
245    Takes a list of draft file names and/or WG names. Displays the
246    result from the IETF Datatracker pages in the web dispay program. Draft
247    names and WG names are matched exactly.''',
248}
249AllHelp = "Command-line interface for displaying IETF-related information. Version " \
250        + __version__ + ".\nCommands are:\n"
251for ThisHelp in sorted(HelpText.keys()):
252        AllHelp += " " + HelpText[ThisHelp] + "\n"
253ArgsCLIHelp = "You can cause tombstone drafts to be displayed in the 'draft' command\n" \
254        + "    by giving the 'tombstones' command by itself.\n" \
255        + "You can increase the number of drafts that will be opened by the 'draft'\n" \
256        + "    command by giving the 'maxdrafts' command followed by an integer.\n" \
257        + "You can require that the 'draft' command only use full draft names\n" \
258        + "    (including draft numbers and '.txt') by giving the 'usedraftnumbers'\n" \
259        + "    command by itself.\n" \
260        + "You can make the 'draft' command not tell you about tombstones by giving\n" \
261        + "    the 'quiet' command by itself.\n" 
262AllCLIHelp = AllHelp + ArgsCLIHelp \
263        + "There is also a 'shell' command to give shell commands from within\n" \
264        + "    this processor.\n" \
265        + "Use 'q' or 'quit' or 'exit' to leave the program."
266ArgsShellHelp = "You can cause tombstone drafts to be displayed in the 'draft' command\n" \
267        + "    with the --tombstones argument.\n" \
268        + "You can increase the number of drafts that will be opened by the 'draft'\n" \
269        + "    command with the --maxdrafts= argument followed by an integer.\n" \
270        + "You can require that the 'draft' command only use full draft names\n" \
271        + "    (including draft numbers and '.txt') with the --usedraftnumbers'\n" \
272        + "    argument.\n" \
273        + "You can make the 'draft' command not tell you about tombstones with the\n" \
274        + "    --quiet argument.\n" 
275AllShellHelp = AllHelp + ArgsShellHelp
276HelpText["allclicmds"] = AllCLIHelp
277HelpText["allshellcmds"] = AllShellHelp
278
279##########
280# The commands themselves
281##########
282
283### auth48 -- Open all appropriate files for a doc in AUTH48
284def Cmd_auth48(Args):
285        if CheckHelp("auth48", Args): return
286        if Args[0] == "":
287                print("Must give at least one draft name or RFC name; skipping.")
288                return
289        def ShowAuth48s(RFCfile):
290                # Incoming file is in format "rfc1234"
291                # Open the text file
292                system(DisplayTextCommand + pathjoin(FullRFCDir, "authors", RFCfile + ".txt"))
293                # Open the local diff in the browser
294                WebDisplay("file:///", pathjoin(FullRFCDir, "authors", RFCfile + "-diff.html"))
295                # Show the status on the RFC Editor's site
296                WebDisplay("https://www.rfc-editor.org/auth48/", RFCfile)
297        for ThisArg in Args:
298                # If it is just a number, check for the RFC
299                if ThisArg.isdigit():
300                        if pathexists(pathjoin(FullRFCDir, "authors", "rfc" + ThisArg + ".txt")):
301                                ShowAuth48s("rfc" + ThisArg)
302                        else:
303                                print("You specified an all-digit argument, '" + ThisArg + "', but a corresponding RFC doesn't " \
304                                        + "exist in the AUTH48 directory. Skipping.")
305                elif ((ThisArg[0:3] == "rfc") and (ThisArg[3:7].isdigit())):
306                        if pathexists(pathjoin(FullRFCDir, "authors", "rfc" + ThisArg[3:7] + ".txt")):
307                                ShowAuth48s("rfc" + ThisArg[3:7])
308                        else:
309                                print("You specified 'rfc' and some digits, but a corresponding RFC doesn't " \
310                                        + "exist in the AUTH48 directory. Skipping.")
311                elif ThisArg.startswith("draft-"):
312                        ThisBaseName = basename(ThisArg)
313                        ThisAuth48 = FindDraftInAuth48(ThisBaseName)
314                        if ThisAuth48 != "":
315                                ShowAuth48s(ThisAuth48)
316                        else:
317                                print("You gave a draft name, but that draft doesn't have an AUTH48 RFC associated with it. Skipping.")
318                else:
319                        print("Didn't recognize the argument '" + ThisArg + "'. Skipping.")
320
321### author -- Search for drafts and RFCs with a particular author
322def Cmd_author(Args):
323        if CheckHelp("author", Args): return
324        if Args[0] == "":
325                print("Must give at least one string to search for; skipping.")
326                return
327        # Get the drafts status and RFC status databases
328        try:
329                with open(IDStatusFileLoc, mode="r") as statusf:
330                        IDStatusDB = jsonload(statusf)
331        except:
332                exit("Weird: could not get data from the ID status database, '" + IDStatusFileLoc + "'. Exiting.")
333        try:
334                with open(RFCStatusFileLoc, mode="r") as statusf:
335                        RFCStatusDB = jsonload(statusf)
336        except:
337                exit("Weird: could not get data from the RFC status database, '" + RFCStatusFileLoc + "'. Exiting.")
338        for ThisArg in Args:
339                FoundRFCs = []
340                FoundIDs = []
341                for ThisRFC in sorted(RFCStatusDB.keys()):
342                        if search(".*" + ThisArg + ".*", str(RFCStatusDB[ThisRFC]["authors"])):
343                                FoundRFCs.append(ThisRFC)
344                if FoundRFCs:
345                        print("Found '" + ThisArg + "' as author in RFCs:")
346                        for ThisFoundRFC in FoundRFCs:
347                                print("  RFC " + ThisFoundRFC + "  " + RFCStatusDB[ThisFoundRFC]["title"])
348                for ThisID in sorted(IDStatusDB.keys()):
349                        if search(".*" + ThisArg + ".*", repr(IDStatusDB[ThisID]["authors"])):
350                                FoundIDs.append(ThisID)
351                if FoundIDs:
352                        print("Found '" + ThisArg + "' as author in IDs:")
353                        for ThisFoundID in FoundIDs:
354                                print("  " + ThisFoundID + "  " + IDStatusDB[ThisFoundID]["title"])
355                       
356### bcp -- Open BCPs locally
357def Cmd_bcp(Args):
358        if CheckHelp("bcp", Args): return
359        if Args[0] == "":
360                print("Must give at least one BCP number; skipping.")
361                return
362        for ThisArg in Args:
363                # Special case: 'index' returns the bcp-index.txt file
364                if ThisArg == "index":
365                        system(DisplayTextCommand + pathjoin(FullRFCDir, "bcp-index.txt"))
366                else:
367                        for ThisBCPNum in Args:
368                                ThisBCPFile = pathjoin(FullRFCDir, "bcp", "bcp"+ ThisBCPNum + ".txt")
369                                if pathexists(ThisBCPFile):
370                                        system(DisplayTextCommand + ThisBCPFile)
371                                else:
372                                        print("Could not find the BCP " + ThisBCPNum + " as '" + ThisBCPFile + "'; skipping.")
373
374### FillAllWGsInIETF -- Helper function for speeding up lookup of new-style charters
375def FillAllWGsInIETF():
376        # Get this list once to optimize if there are many WGs to look up
377        try:
378                chdir(expanduser(CharterDir))
379        except:
380                exit("Weird: could not chdir to " + CharterDir)
381        global AllWGsInIETF
382        AllWGsInIETF = {}
383        AllCharterFiles = glob(pathjoin(CharterDir, "charter-ietf-*"))
384        for ThisCharterFile in sorted(glob("charter-ietf-*")):
385                CharterParts = (ThisCharterFile[13:-4]).split("-")
386                # There's always a special case for the Security Area <grumble>
387                if CharterParts[0:2] == ["krb", "wg"]:
388                        CharterParts = [ "krb-wg", CharterParts[2:] ]
389                AllWGsInIETF[CharterParts[0]] = ThisCharterFile
390
391### charter -- Open 2012-style charter files locally
392def Cmd_charter(Args):
393        if CheckHelp("charternew", Args): return
394        if Args[0] == "":
395                print("Must give at least one WG name; skipping.")
396                return
397        FillAllWGsInIETF()
398        for ThisArg in Args:
399                ThisArg = ThisArg.lower()
400                MatchingWGs = fnfilter(sorted(AllWGsInIETF.keys()), "*" + ThisArg + "*")
401                if len(MatchingWGs) > 10:
402                        AllMatched = ", ".join(MatchingWGs)
403                        print("More than 10 WGs match '*" + ThisArg + "*' in the IETF directory. Skipping.\n" + AllMatched)
404                elif len(MatchingWGs) == 0:
405                        print("Did not find the WG that matches '*" + ThisArg + "*' in the IETF directory.")
406                        print("Possibly try the 'tracker' command to see if the Datatracker has the desired data. Skipping.")
407                else:
408                        for ThisWG in MatchingWGs:
409                                CharterTextFile = pathjoin(expanduser(CharterDir), AllWGsInIETF[ThisWG])
410                                if pathexists(CharterTextFile):
411                                        system(DisplayTextCommand + CharterTextFile)
412                                else:
413                                        print("Weird: when looking for the charter file for " + ThisWG + ", I should have found " \
414                                                + CharterTextFile + ", but didn't. Skipping.")
415
416### conflict -- Show the conflict review for a draft
417def Cmd_conflict(Args):
418        if CheckHelp("conflict", Args): return
419        if Args[0] == "":
420                print("Must give at least one draft name; skipping.")
421                return
422        for ThisArg in Args:
423                if ThisArg.startswith("draft-"):
424                        # Strip any ".txt" and "-nn" from the arugment so we can match the database
425                        ShorterArg = sub(r'(\.txt)$', "", ThisArg)
426                        ShorterArg = sub(r'-\d\d$', "", ShorterArg)
427                        # Remove "draft-" from the beginning
428                        ShorterArg = ShorterArg[6:]
429                        WebDisplay("https://datatracker.ietf.org/doc/conflict-review-", ShorterArg)
430                else:
431                        print("The argument to this command must begin with 'draft-'.\n")
432
433### diff -- Show the diff between a draft and the previous one on the IETF Tools site
434def Cmd_diff(Args):
435        if CheckHelp("diff", Args): return
436        if Args[0] == "":
437                print("Must give at least one draft name; skipping.")
438                return
439        for ThisArg in Args:
440                if ThisArg.startswith("draft-"):
441                        WebDisplay("https://tools.ietf.org/rfcdiff?url2=", ThisArg)
442                else:
443                        print("The argument to this command must begin with 'draft-'.\n")
444
445### draft -- Open drafts locally
446def Cmd_draft(Args):
447        if CheckHelp("draft", Args): return
448        if Args[0] == "":
449                print("Must give at least one draft name; skipping.")
450                return
451        # Get the drafts status database
452        try:
453                with open(IDStatusFileLoc, mode="r") as statusf:
454                        IDStatusDB = jsonload(statusf)
455        except:
456                exit("Weird: could not get data from the ID status database, '" + IDStatusFileLoc + "'. Exiting.")
457        for ThisArg in Args:
458                # Special case: 'abstracts" returns the 1id-abstracts.txt file
459                if ThisArg == "abstracts":
460                        system(DisplayTextCommand + pathjoin(FullIDDir, "1id-abstracts.txt"))
461                        continue
462                # Pay attention only to expired, became-an-rfc, was replaced, and active drafts
463                MatchedDraftsByStatus = { "Expired": [], "RFC": [], "Replaced": [], "Active": [] }
464                # Strip any ".txt" and "-nn" from the arugment so we can match the database
465                ShorterArg = sub(r'(\.txt)$', "", ThisArg)
466                ShorterArg = sub(r'-\d\d$', "", ShorterArg)
467                # Find all the drafts in the database that match the argument given
468                for ThisDraftFromDraftsDB in IDStatusDB.keys():
469                        if search(".*" + ShorterArg + ".*", ThisDraftFromDraftsDB):
470                                ThisStatus = IDStatusDB[ThisDraftFromDraftsDB]["status"]
471                                if ThisStatus in MatchedDraftsByStatus.keys():
472                                        MatchedDraftsByStatus[ThisStatus].append(ThisDraftFromDraftsDB)
473                # Report on the drafts found for expired, became an RFC, and replaced
474                if not(QuietDraft):
475                        if MatchedDraftsByStatus["Expired"]:
476                                print("Matching drafts that have expired:")
477                                for ThisExpired in sorted(MatchedDraftsByStatus["Expired"]):
478                                        print("  " + ThisExpired + " (last revised " + IDStatusDB[ThisExpired]["last-revised"] + ")")
479                                print()
480                        if MatchedDraftsByStatus["RFC"]:
481                                print("Matching drafts that became RFCs:")
482                                for ThisBecameRFC in sorted(MatchedDraftsByStatus["RFC"]):
483                                        print("  " + ThisBecameRFC + " (became RFC " + IDStatusDB[ThisBecameRFC]["became-rfc"] + ")")
484                                print()
485                        if MatchedDraftsByStatus["Replaced"]:
486                                print("Matching drafts that were replaced:")
487                                for ThisWasReplaced in sorted(MatchedDraftsByStatus["Replaced"]):
488                                        print("  " + ThisWasReplaced + " (replaced by " + IDStatusDB[ThisWasReplaced]["replaced-by"] + ")")
489                                print()
490                # If there are no active drafts that match this argument, say something and go to the next argument
491                if not(MatchedDraftsByStatus.get("Active")):
492                        print("No active drafts matched the substring '" + ThisArg + "'.")
493                        continue
494                # If there are too many matched active drafts, list them and go to the next argument
495                if len(MatchedDraftsByStatus["Active"]) > MaxDrafts:
496                        print("There are more than " + str(MaxDrafts) + " active drafts that match the string '" \
497                                + ThisArg + "'; not displaying.\nYou can raise this count with ", end="")
498                        if FromCommandLine:
499                                print(" the '--maxdrafts' command-line argument,\nsuch as '--maxdrafts=40'.")
500                        else:
501                                print(" the 'maxdrafts' command,\nsuch as 'maxdrafts 40'.")
502                        for ThisOverMax in MatchedDraftsByStatus["Active"]:
503                                print("  " + ThisOverMax)
504                        continue
505                # Display the active drafts that match this argument
506                for ThisActiveDraft in sorted(MatchedDraftsByStatus["Active"]):
507                        # If it is in Auth48, display it from that directory only
508                        ThisAuth48 = FindDraftInAuth48(ThisActiveDraft)
509                        if ThisAuth48 != "":
510                                print("This Internet-Draft is in AUTH48 state; displaying " + ThisAuth48)
511                                WebDisplay("file:///", pathjoin(FullRFCDir, "authors", ThisAuth48 + "-diff.html"))
512                                continue
513                        # Display the draft from the numbered or unnumbered mirror directory, based on their preference
514                        if UseDraftNumbers:
515                                TargetDir = FullIDDir
516                        else:
517                                TargetDir = FullShortIDDir
518                        # Make sure there is only one that matches
519                        TheseNumberedDrafts = glob(pathjoin(TargetDir, ThisActiveDraft + "*"))
520                        if len(TheseNumberedDrafts) == 0:
521                                print("Weird: could not find a draft matching '" + ThisActiveDraft \
522                                        + "' in '" + TargetDir + "'; skipping.")
523                        else:
524                                for ThisToDisplay in TheseNumberedDrafts:
525                                        system(DisplayTextCommand + pathjoin(TargetDir, ThisToDisplay))
526
527### draftstatus -- Show I-D status from the database without opening the file
528def Cmd_draftstatus(Args):
529        if CheckHelp("draftstatus", Args): return
530        if Args[0] == "":
531                print("Must give at least one draft name or substring; skipping.")
532                return
533        # Open the status database before going through the arguments
534        try:
535                with open(IDStatusFileLoc, mode="r") as statusf:
536                        IDStatusDB = jsonload(statusf)
537        except:
538                exit("Weird: could not get data from the ID status database, '" + IDStatusFileLoc + "'. Exiting.")
539        for ThisArg in Args:
540                FoundThisArg = False
541                # Find all drafts matching this string
542                for ThisDraftFromDraftsDB in IDStatusDB.keys():
543                        if search(".*" + ThisArg + ".*", ThisDraftFromDraftsDB):
544                                FoundThisArg = True
545                                ThisIDStatus = IDStatusDB.get(ThisDraftFromDraftsDB)
546                                print("Draft " + ThisDraftFromDraftsDB + ":\n  Status: " + ThisIDStatus["status"])
547                                if ThisIDStatus.get("title"):
548                                        print("  Draft title: " + ThisIDStatus.get("title"))
549                                if ThisIDStatus.get("authors"):
550                                        print("  Authors: " + ThisIDStatus.get("authors").encode('utf-8'))
551                                if ThisIDStatus.get("last-revised"):
552                                        print("  Last revision: " + ThisIDStatus.get("last-revised"))
553                                if ThisIDStatus.get("iesg-state"):
554                                        print("  IESG state: " + ThisIDStatus.get("iesg-state"))
555                                if ThisIDStatus.get("intended-level"):
556                                        print("  Intended level: " + ThisIDStatus.get("intended-level"))
557                                if ThisIDStatus.get("last-call-ends"):
558                                        print("  Last call ends: " + ThisIDStatus.get("last-call-ends"))
559                                if ThisIDStatus.get("became-rfc"):
560                                        print("  Became RFC: " + ThisIDStatus.get("became-rfc"))
561                                if ThisIDStatus.get("replaced-by"):
562                                        print("  Replaced by: " + ThisIDStatus.get("replaced-by"))
563                                if ThisIDStatus.get("wg-name"):
564                                        print("  WG: " + ThisIDStatus.get("wg-name"))
565                                if ThisIDStatus.get("area-name"):
566                                        print("  Area: " + ThisIDStatus.get("area-name"))
567                                if ThisIDStatus.get("ad-name"):
568                                        print("  Area Director: " + ThisIDStatus.get("ad-name").encode('utf-8'))
569                                if ThisIDStatus.get("file-types"):
570                                        # No need to show just .txt
571                                        if ThisIDStatus.get("file-types") != ".txt":
572                                                print("  File types available: " + ThisIDStatus.get("file-types"))
573                if FoundThisArg == False:
574                        print("Did not find any records in the database matching " + ThisArg + "; skipping.")
575
576### iesg -- Show IESG pages on the Datatracker
577def Cmd_iesg(Args):
578        if CheckHelp("iesg", Args): return
579        if Args[0] == "":
580                print("Must give at least one of 'agenda' or 'docs' as an argument; skipping.")
581                return
582        for ThisArg in Args:
583                # If it is just a number, check for the RFC
584                if ThisArg.lower() == "agenda":
585                        WebDisplay("https://datatracker.ietf.org/iesg/agenda", "")
586                if ThisArg.lower() == "docs":
587                        WebDisplay("https://datatracker.ietf.org/iesg/agenda/documents", "")
588
589### mirror -- Update the local mirror
590def Cmd_mirror(Args):
591        if CheckHelp("mirror", Args): return
592        # See if the main directory exists; if not, try to create it
593        if pathexists(expanduser(MirrorDir)) == False:
594                try:
595                        mkdir(expanduser(MirrorDir))
596                except:
597                        exit("The mirror directory '" + MirrorDir + "' does not exist, and could not be created. Exiting.")
598        if pathexists(expanduser(IDDir)) == False:
599                print("This appears to be the first time you are running this; it may take a long")
600                print("  time. Each mirror section will be named, but the files being mirrored will")
601                print("  only appear when the full directory has been mirrored; this can take hours,")
602                print("  depending on network speed. You can check the progress by looking in the")
603                print("  created directories.")
604        # Set up the log file
605        LogFile = expanduser(MirrorDir + "/mirror-log.txt")
606        try:
607                logf = open(LogFile, "a")
608        except:
609                exit("Could not open " + LogFile + " for appending. Exiting.\n")
610        # Print out to both the console and log file
611        def PrintLog(String):
612                print(String)
613                print(String, file=logf)
614        PrintLog("\nMirror began at " + strftime("%Y-%m-%d %H:%M:%S") + "\n")
615        # AllActions is the set of actions to be performed
616        # First see if it was already defined in the config file
617        if "AllActions" in globals():
618                AllActions = globals()["AllActions"]
619        else:
620                AllActions = [
621                        [ "Internet Drafts", "rsync -avz --exclude='*.xml' --exclude='*.pdf' --exclude='*.p7s' " +
622                                " --exclude='*.ps' --delete-after  rsync.ietf.org::internet-drafts " + IDDir ],
623                        [ "IANA", "rsync -avz --delete-after  rsync.ietf.org::everything-ftp/iana/ " + IANADir ],
624                        [ "IESG", "rsync -avz --delete-after  rsync.ietf.org::iesg-minutes/ " + IESGDir ],
625                        [ "IETF", "rsync -avz --delete-after  --exclude='ipr/' " +
626                                "ietf.org::everything-ftp/ietf/ " + IETFDir ],
627                        [ "charters", "rsync -avz --delete-after  rsync.ietf.org::everything-ftp/charter/ " + CharterDir ],
628                        [ "conflict reviews", "rsync -avz --delete-after  rsync.ietf.org::everything-ftp/conflict-reviews/ " + ConflictDir ],
629                        [ "status changes", "rsync -avz --delete-after  rsync.ietf.org::everything-ftp/status-changes/ " + StatusDir ],
630                        [ "RFCs", "rsync -avz --delete-after " +
631                                " --exclude='tar*' --exclude='search*' --exclude='PDF-RFC*' " +
632                                " --exclude='tst/' --exclude='pdfrfc/' --exclude='internet-drafts/' " +
633                                " --exclude='ien/' ftp.rfc-editor.org::everything-ftp/in-notes/ " + RFCDir ]
634                        ]
635        for DoThis in AllActions:
636                PrintLog("Starting " + DoThis[0])
637                FullOut = []
638                p = Popen(DoThis[1], bufsize=-1, shell=True, stdout=PIPE)
639                while p.poll() is None:
640                        FullOut.append(p.stdout.readline())
641                TheOut = ""
642                for ThisLine in FullOut:
643                        # Need the following to prevent printing and parsing problems later
644                        ThisLine = ThisLine.decode("ascii")
645                        if ThisLine.startswith("receiving "): continue
646                        if ThisLine.startswith("sent "): continue
647                        if ThisLine.startswith("total "): continue
648                        if ThisLine.startswith("skipping non-regular file "): continue
649                        if ThisLine.endswith('.listing" [1]\n'): continue
650                        if ThisLine == "\n": continue
651                        TheOut += ThisLine
652                PrintLog(TheOut)
653
654        # Do the filling of the short-name directory
655        PrintLog("Filling short-name directory")
656        FullIDDir = expanduser(IDDir)
657        FullShortIDDir = expanduser(ShortIDDir)
658        # See if the directory mirrorded from the IETF exists and get the list of drafts
659        if pathexists(FullIDDir) == False:
660                exit("The directory with the drafts, " + IDDir + ", does not exist. Exiting.")
661        elif isdir(FullIDDir) == False:
662                exit(IDDir + "is not a directory. Exiting.")
663        try:
664                chdir(FullIDDir)
665        except:
666                exit("Weird: could not chdir to " + IDDir + ". Exiting.")
667        # Note that this is only making short names for .txt files, not any of the others
668        TheIDs = sorted(glob("draft-*.txt"))
669        # See if the directory to be copied to exists; if so, delete all the files there
670        if pathexists(FullShortIDDir) == False:
671                try:
672                        mkdir(FullShortIDDir)
673                except:
674                        exit("The directory where the shorter-named drafts will go, " + ShortIDDir + ", could not be created. Exiting.")
675        elif isdir(FullShortIDDir) == False:
676                exit(ShortIDDir + "is not a directory. Exiting.")
677        try:
678                chdir(FullShortIDDir)
679        except:
680                exit("Weird: could not chdir to " + ShortIDDir + ". Exiting.")
681        for ToDel in glob("*"):
682                if isdir(ToDel):
683                        exit("Found a directory in " + ShortIDDir + ". Exiting.")
684                unlink(ToDel)
685        # Determine the shorter name and link the file with the destination
686        for ThisDraftName in TheIDs:
687                # Strip off "-nn.txt"
688                ShorterName = ThisDraftName[:-7]
689                # Test if the shorter name already exists; if so, nuke it
690                #   This is based on the the assumption that there are two drafts where the version numbers
691                #   are different, and because this is sorted, the higher ones should come later.
692                if pathexists(pathjoin(FullShortIDDir, ShorterName)):
693                        unlink(pathjoin(FullShortIDDir, ShorterName))
694                try:
695                        link(pathjoin(FullIDDir, ThisDraftName), pathjoin(FullShortIDDir, ShorterName))
696                except OSError as e:
697                        print("For '" + ThisDraftName + "', got error: " + str(e) + ". Skipping.")
698
699        # Make the RFC status database to make rfc status searching faster
700        PrintLog("Making the RFC status index")
701        TagBase = "{http://www.rfc-editor.org/rfc-index}"
702        try:
703                ParsedRFCDB = ElTree.parse(pathjoin(FullRFCDir, "rfc-index.xml"))
704        except:
705                exit("Weird: could not find '" + pathjoin(FullRFCDir, "rfc-index.xml") + "' when building the status index. Exiting.")
706        TreeRoot = ParsedRFCDB.getroot()
707        RFCStatus = {}
708        def StripLeadingZeros(InStr):
709                return(sub(RFCZerosPat, "\\1", InStr))
710        LookForFields = ("obsoleted-by", "updated-by", "obsoletes", "updates", "is-also")
711        for ThisTopNode in TreeRoot:
712                # Just get the RFCs, not (yet) BCPs, STDs, and so on; maybe add them later
713                if ThisTopNode.tag == TagBase + "rfc-entry":
714                        ThisRFCNum = StripLeadingZeros(ThisTopNode.find(TagBase + "doc-id").text.replace("RFC", ""))
715                        RFCStatus[ThisRFCNum] = {}
716                        for ThisLookedFor in LookForFields:
717                                ### if ((ThisRFCNum == "2822") and (ThisLookedFor == "updated-by")): trace()
718                                if ThisTopNode.findall(TagBase + ThisLookedFor):
719                                        RFCStatus[ThisRFCNum][ThisLookedFor] = []
720                                        for ThisFoundOuterElement in ThisTopNode.findall(TagBase + ThisLookedFor):
721                                                for ThisFoundInnerElement in ThisFoundOuterElement.findall(TagBase + "doc-id"):
722                                                        RFCStatus[ThisRFCNum][ThisLookedFor].append(StripLeadingZeros(ThisFoundInnerElement.text.replace("RFC", "")))
723                        if ThisTopNode.findall(TagBase + "errata-url"):
724                                RFCStatus[ThisRFCNum]["errata"] = True
725                        ThisTitle = ThisTopNode.find(TagBase + "title").text
726                        if ThisTitle:
727                                RFCStatus[ThisRFCNum]["title"] = ThisTitle
728                        CurrStat = ThisTopNode.find(TagBase + "current-status").text
729                        if (CurrStat and CurrStat != "UNKNOWN"):
730                                RFCStatus[ThisRFCNum]["current-status"] = CurrStat
731                        RFCStatus[ThisRFCNum]["authors"] = []
732                        for ThisFoundOuterAuthor in ThisTopNode.findall(TagBase + "author"):
733                                for ThisFoundInnerAuthor in ThisFoundOuterAuthor.findall(TagBase + "name"):
734                                        RFCStatus[ThisRFCNum]["authors"].append(ThisFoundInnerAuthor.text)
735        try:
736                with open(RFCStatusFileLoc, mode="wb") as statusf:
737                        jsondump(RFCStatus, statusf)
738        except:
739                exit("Could not dump status info to '" + RFCStatusFileLoc + "'. Exiting.")
740
741        # Make the I-D status database to make rfc status searching faster
742        PrintLog("Making the ID status index")
743        try:
744                AllIDStatusLines = open(FullIDDir + "/all_id2.txt", mode="r").readlines()
745        except:
746                exit("Weird: could not read all_id2.txt to make the I-D status database. Exiting.")
747        IDStatus = {}
748        for ThisLine in AllIDStatusLines:
749                if ThisLine[0] == "#": continue
750                TheFields = ThisLine.split("\t")
751                # The key is the draft name minus the "-nn"
752                IDStatus[TheFields[0][0:-3]] = { \
753                        "status": TheFields[2], \
754                        "iesg-state": TheFields[3], \
755                        "became-rfc": TheFields[4], \
756                        "replaced-by": TheFields[5], \
757                        "last-revised": TheFields[6], \
758                        "wg-name": TheFields[7], \
759                        "area-name": TheFields[8], \
760                        "ad-name": TheFields[9], \
761                        "intended-level": TheFields[10], \
762                        "last-call-ends": TheFields[11], \
763                        "file-types": TheFields[12], \
764                        "title": TheFields[13], \
765                        "authors": TheFields[14].rstrip() }
766        try:
767                with open(IDStatusFileLoc, mode="wb") as statusf:
768                        jsondump(IDStatus, statusf)
769        except:
770                exit("Could not dump status info to '" + IDStatusFileLoc + "'. Exiting.")       
771
772        # Finish up
773        PrintLog("\nMirror ended at " + strftime("%Y-%m-%d %H:%M:%S"))
774        logf.close()
775
776### rfc -- Open RFCs locally
777def Cmd_rfc(Args):
778        if CheckHelp("rfc", Args): return
779        if Args[0] == "":
780                print("Must give at least one RFC name or number; skipping.")
781                return
782        for ThisArg in Args:
783                # Special case: 'index' returns the rfc-index.txt file
784                if ThisArg == "index":
785                        system(DisplayTextCommand + pathjoin(FullRFCDir, "rfc-index.txt"))
786                        continue
787                # Look for different ways they may have specified it
788                RFCTests = [ ThisArg, ThisArg + ".txt", "rfc" + ThisArg, "rfc" + ThisArg + ".txt" ]
789                FoundRFC = False
790                for ThisTest in RFCTests:
791                        # Also check in the AUTH48 directory
792                        for WhichDir in (FullRFCDir, FullRFCDir + "/authors"):
793                                if pathexists(pathjoin(WhichDir, ThisTest)):
794                                        FoundRFC = True
795                                        system(DisplayTextCommand + pathjoin(WhichDir, ThisTest))
796                                        break
797                if FoundRFC == False:
798                        print("Could not find an RFC for '" + ThisArg + "' in '" + FullRFCDir + "'; skipping.")
799
800### rfcextra -- Open RFCs locally and also open related RFCs (updates, obsoleted, errata...)
801def Cmd_rfcextra(Args):
802        if CheckHelp("rfcextra", Args): return
803        if Args[0] == "":
804                print("Must give at least one RFC name or number; skipping.")
805                return
806        # Open the status database before going through the arguments
807        try:
808                with open(RFCStatusFileLoc, mode="r") as statusf:
809                        RFCStatusDB = jsonload(statusf)
810        except:
811                exit("Weird: could not get data from the RFC status database, '" + RFCStatusFileLoc + "'. Exiting.")
812        for ThisArg in Args:
813                # First try to open the RFC itself
814                Cmd_rfc([ThisArg])
815                # Then get the status of the RFC and open RFCs and errata that happened later
816                ThisRFCStatus = RFCStatusDB.get(ThisArg)
817                # If the status exists for this RFC, display additional information and open what was found
818                if ThisRFCStatus:
819                        if ThisRFCStatus.get("obsoleted-by"):
820                                for ThisObsoleted in ThisRFCStatus.get("obsoleted-by"):
821                                        print("RFC " + ThisArg + " was obsoleted by RFC " + ThisObsoleted)
822                                        Cmd_rfcextra([ThisObsoleted])
823                        if ThisRFCStatus.get("updated-by"):
824                                for ThisUpdated in ThisRFCStatus.get("updated-by"):
825                                        print("RFC " + ThisArg + " was updated by RFC " + ThisUpdated)
826                                        Cmd_rfcextra([ThisUpdated])
827                        if ThisRFCStatus.get("errata") == True:
828                                print("RFC " + ThisArg + " has errata")
829                                WebDisplay("https://www.rfc-editor.org/errata_search.php?rfc=", ThisArg)
830
831### rfcinfo -- Show RFC information on the RFC Editor site
832def Cmd_rfcinfo(Args):
833        if CheckHelp("rfcinfo", Args): return
834        if Args[0] == "":
835                print("Must give at least one RFC number; skipping.")
836                return
837        for ThisArg in Args:
838                # If it is just a number, check for the RFC
839                if ThisArg.isdigit():
840                        WebDisplay("https://www.rfc-editor.org/info/rfc", ThisArg)
841                # If it starts with "rfc" and rest are digits, it is also an RFC
842                elif (ThisArg.startswith("rfc") and  ThisArg[3:].isdigit()):
843                        WebDisplay("https://www.rfc-editor.org/info/", ThisArg)
844                else:
845                        print("This command is for finding RFCs on the RFC Editor's site web site.\n")
846
847### rfcstatus -- Show RFC status from the database without opening the file
848def Cmd_rfcstatus(Args):
849        if CheckHelp("rfcstatus", Args): return
850        if Args[0] == "":
851                print("Must give at least one RFC name or number; skipping.")
852                return
853        # Open the status database before going through the arguments
854        try:
855                with open(RFCStatusFileLoc, mode="r") as statusf:
856                        RFCStatusDB = jsonload(statusf)
857        except:
858                exit("Weird: could not get data from the RFC status database, '" + RFCStatusFileLoc + "'. Exiting.")
859        for ThisArg in Args:
860                # Get the status of the RFC
861                ShorterArg = sub("^rfc", "", ThisArg)
862                ShorterArg = sub(".txt%", "", ShorterArg)
863                ThisRFCStatus = RFCStatusDB.get(ShorterArg)
864                # Be sure the status exists
865                if ThisRFCStatus:
866                        print("RFC " + ShorterArg + ":")
867                        if ThisRFCStatus.get("is-also"):
868                                print("  Is also " + " ".join(ThisRFCStatus.get("is-also")))
869                        if ThisRFCStatus.get("obsoleted-by"):
870                                print("  Obsoleted by " + " ".join(ThisRFCStatus.get("obsoleted-by")))
871                        if ThisRFCStatus.get("obsoletes"):
872                                print("  Obsoletes " + " ".join(ThisRFCStatus.get("obsoletes")))
873                        if ThisRFCStatus.get("updated-by"):
874                                print("  Updated by " + " ".join(sorted(ThisRFCStatus.get("updated-by"))))
875                        if ThisRFCStatus.get("updates"):
876                                print("  Updates " + " ".join(sorted(ThisRFCStatus.get("updates"))))
877                        if ThisRFCStatus.get("errata") == True:
878                                print("  Has errata")
879                else:
880                        print("Weird: did not find status in the database for RFC " + ThisArg + "; skipping.")
881
882### tools -- Show RFCs, WGs, and drafts on the IETF Tools site
883def Cmd_tools(Args):
884        if CheckHelp("tools", Args): return
885        if Args[0] == "":
886                print("Must give at least one RFC, WG, or draft name; skipping.")
887                return
888        for ThisArg in Args:
889                ThisArg = ThisArg.lower()
890                # If it is just a number, check for the RFC
891                if ThisArg.isdigit():
892                        WebDisplay("https://tools.ietf.org/html/rfc", ThisArg)
893                # If it starts with "rfc" and rest are digits, it is also an RFC
894                elif (ThisArg.startswith("rfc") and  ThisArg[3:].isdigit()):
895                        WebDisplay("https:tools.ietf.org/html/", ThisArg)
896                # If it isn't an RFC and it has no hyphens, assume it is a WG
897                elif ThisArg.find("-") == -1:
898                        WebDisplay("https:tools.ietf.org/wg/", ThisArg)
899                # Otherwise, assume it is a draft; this might get a 404
900                elif ThisArg.startswith("draft-"):
901                        WebDisplay("https:tools.ietf.org/html/", ThisArg)
902                else:
903                        print("This command is for finding RFCs, WGs (with no hypens) or drafts\n(that start with 'draft-')" \
904                                + " on the IETF Tools web site.\n")
905
906### tracker -- Show WGs and draft statuses on the Datatracker
907def Cmd_tracker(Args):
908        if CheckHelp("tracker", Args): return
909        if Args[0] == "":
910                print("Must give at least one WG or draft name; skipping.")
911                return
912        for ThisArg in Args:
913                ThisArg = ThisArg.lower()
914                # If it is just a number, check for the RFC
915                if ThisArg.isdigit():
916                        WebDisplay("https://datatracker.ietf.org/doc/rfc", ThisArg)
917                # If it starts with "rfc" and rest are digits, it is also an RFC
918                elif (ThisArg.startswith("rfc") and  ThisArg[3:].isdigit()):
919                        WebDisplay("https://datatracker.ietf.org/doc/", ThisArg)
920                # If it isn't an RFC and it has no hyphens, assume it is a WG
921                elif ThisArg.find("-") == -1:
922                        WebDisplay("https://datatracker.ietf.org/wg/", ThisArg)
923                # If not, assume it is a draft
924                elif ThisArg.startswith("draft-"):  # This might get a 404
925                        WebDisplay("https://datatracker.ietf.org/doc/", ThisArg)
926                else:
927                        print("This command is for finding WGs (with no hypens) or drafts (that start with 'draft-')" \
928                                + " on the IETF Datatracker.\n")
929
930# For showing help when --help or -h is given on the command line
931def ShowCommandLineHelp(ignore1, ignore2, ignore3, ignore4):
932        CheckHelp("allshellcmds", "__helptext__")
933        exit()
934
935##########
936# The real program starts here
937##########
938
939Parse = OptionParser(add_help_option=False, usage="Something here")
940# Don't display tombstones unless option is given
941Parse.add_option("--tombstones", action="store_true", dest="DisplayTombstones", default=False)
942# Maximum number of drafts to display
943Parse.add_option("--maxdrafts", action="store", type="int", dest="MaxDrafts", default=10)
944# Only open drafts from directory with full draft names (including version numbers)
945Parse.add_option("--usedraftnumbers", action="store_true", dest="UseDraftNumbers", default=False)
946# Normally have the "draft" and "rfc" commands be verbose
947Parse.add_option("--quiet", action="store_true", dest="QuietDraft", default=False)
948# Set up the help
949Parse.add_option("--help", "-h", action="callback", callback=ShowCommandLineHelp)
950(Opts, RestOfArgs) = Parse.parse_args()
951# Define these top-level variables to make it easier to change them from the config file
952DisplayTombstones = Opts.DisplayTombstones
953MaxDrafts = Opts.MaxDrafts
954UseDraftNumbers = Opts.UseDraftNumbers
955QuietDraft = Opts.QuietDraft
956
957ConfigFile = ""
958for ThisPlace in ConfigPlaces:
959        if pathexists(expanduser(ThisPlace)):
960                ConfigFile = ThisPlace
961                break
962if ConfigFile == "":
963        exit("Could not find a configuration file in " + " or ".join(ConfigPlaces) + "\nExiting.")
964
965# Get the variable names for the directories and display mechanisms
966try:
967        Configs = open(expanduser(ConfigFile), mode="r").read()
968except:
969        exit("Could not open '" + expanduser(ConfigFile) + "' for input. Exiting.")
970try:
971        exec(Configs)
972except:
973        exit("Failed during exec of " + ConfigFile + ". Exiting.")
974
975# All the variables from the config file must be defined, and the named directories must exist.
976TheDirectories = ( "MirrorDir", "IDDir", "ShortIDDir", "IANADir", "IESGDir", "IETFDir", "RFCDir" )
977for ThisDir in TheDirectories:
978        if not ThisDir in dir():
979                exit("The variable '" + ThisDir + "' was not defined in " + ConfigFile + ". Exiting.")
980        globals()["Full" + ThisDir] = expanduser(globals()[ThisDir])
981        if not(pathexists(globals()["Full" + ThisDir])):
982                print("The directory '" + globals()["Full" + ThisDir] + "' does not exist.\n" \
983                        + "You need to run the 'ietf mirror' command before running any other command.\n")
984# The display mechanisms can be blank
985# Set defaults for the desplay commands if they are not set
986if DisplayTextCommand == "":
987        # If DisplayTextCommand is not set but the EDITOR environment variable is, use EDITOR instead
988        if environ.get("EDITOR", "") != "":
989                DisplayTextCommand = environ["EDITOR"] + " "
990        else:
991                DisplayTextCommand = "less "
992if DisplayWebCommand == "":
993        DisplayWebCommand = "less "  # This is a terrible fallback, of course
994
995# Location of the RFC and JSON files (which could not be complete until we got the config)
996RFCStatusFileLoc = pathjoin(FullRFCDir, "ietf-rfc-status.json")
997IDStatusFileLoc = pathjoin(FullIDDir, "ietf-id-status.json")
998
999# The "ietf" command can be called with no arguments to go to the internal command processor
1000#    It is often called as "ietf" with arguments from the KnownCommand list.
1001if RestOfArgs == []:
1002        FromCommandLine = False
1003        try:
1004                OurCLI().cmdloop()
1005        except KeyboardInterrupt:
1006                exit("\n^C caught. Exiting.")
1007else:
1008        FromCommandLine = True
1009        GivenCmd = RestOfArgs[0]
1010        if GivenCmd in KnownCmds:
1011                globals()["Cmd_" + GivenCmd](RestOfArgs[1:])
1012        else:
1013                exit("Found a bad command: " + GivenCmd + ". Exiting.")
Note: See TracBrowser for help on using the repository browser.