You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2021/02/03 23:46:11 UTC
[trafficserver] 16/20: Add command line utility to help convert
remap plugin usage to ATS9. (#7426)
This is an automated email from the ASF dual-hosted git repository.
zwoop pushed a commit to branch 9.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit ee755b5adeb06437d4a0d03887a9bbe167fe4aa2
Author: Walt Karas <wk...@verizonmedia.com>
AuthorDate: Tue Feb 2 11:49:57 2021 -0600
Add command line utility to help convert remap plugin usage to ATS9. (#7426)
---
doc/appendices/command-line/cvtremappi.en.rst | 69 ++++
tools/cvtremappi | 575 ++++++++++++++++++++++++++
tools/insnew | 67 +++
3 files changed, 711 insertions(+)
diff --git a/doc/appendices/command-line/cvtremappi.en.rst b/doc/appendices/command-line/cvtremappi.en.rst
new file mode 100644
index 0000000..264d474
--- /dev/null
+++ b/doc/appendices/command-line/cvtremappi.en.rst
@@ -0,0 +1,69 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+.. include:: ../../common.defs
+
+.. _cvtremappi:
+
+cvtremappi
+**********
+
+Description
+===========
+
+To help convert your remapping configuration from pre-ATS9 to ATS9 and later. It may be useful if you use any
+of the core plugins regex_remap.so, header_rewrite.so or gzip.so. (For this script to work, the python3
+command has to be in your path.) You can specify where your remap configuration file is with the option:
+
+--filepath FILEPATH
+
+If this parameter is omitted, it defaults to ``./remap.config`` . The script will make necessary modifications
+to this file, and any files it includes with ``.include`` . It will change `@plugin=gzip.so` to its new name,
+`@plugin=compress.so` . When regex_remap.so is invoked as the first remap plugin, it will add the parameter
+@pparam=pristine . (This makes it work the same as in pre-9 ATS, where the request URL is the pre-remapping
+URL for the first plugin for a remap rule.) When `header_rewrite.so` is used as a remap plugin, no changes
+are needed in the remap configuration line invoking it. However, changes may be necessary to the
+configuration files passed to it as parameters. If a header rewrite configuration file is used for both the
+invocation of header rewrite as the first plugin for remap rules, and for other invocations, it may be
+necessary to generate two new versions of it. In these cases, the prefix `1st-` is added to file's name,
+for the version used with header rewrite as the first plugin. If you prefer that a different prefix be added,
+you can specify it with this option:
+
+--prefix PREFIX
+
+If you are also using header rewrite as a global plugin, you should also provide the filepath of the global
+plugin configuration file with this option:
+
+--plugin PLUGIN
+
+(Note that, if the PLUGIN filepath is relative, it should be relative to the directory containing the remap
+configuration file, not relative to the directory the script is run from. Note also that, if relative paths
+for include files for header rewrite config files appear in the configuration files, they are assumed to be
+relative to the directory containing the remap configuration file.)
+
+Header rewrite previously had some logic that has been eliminated in ATS9. If a line in a header rewrite
+configuration file relies on this deprecated logic, an error message will be output to standard error. The
+text `ERROR:` will be prepended to the line in the configuration file causing the error.
+
+The script writes, one per line, a list of the files it is changing or creating to the standard output. But
+both new and changed files will be written into entirely new files with the suffix `.new` added to the filepath.
+For example, if `remap.config` is changed by the script, it will put the changed version of the file in
+`remap.config.new` . This gives you a chance to review the changes the script has made. You can then put the
+changed files into effect with the tool script `insnew`. This script reads a list of filepaths, one per line,
+from the standard input. For each filepath `FP`, if it specifies an existing file, it will rename it to
+`FP.old`. It will then rename the file `FP.new` to `FP`. This second script should be run from the same
+current directory as the first script was run from.
diff --git a/tools/cvtremappi b/tools/cvtremappi
new file mode 100755
index 0000000..290a3b5
--- /dev/null
+++ b/tools/cvtremappi
@@ -0,0 +1,575 @@
+#!/usr/bin/env python3
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Automatically convert remapping configuration for use of header_rewrite.so, regex_remap.so and gzip.so
+# plugins from pre-ATS9 to ATS9 and later. (See docs.trafficserver.apache.org, Command Line Utilities
+# Appendix.)
+
+import argparse
+
+BACKSLASH = "\\"
+
+parser = argparse.ArgumentParser(
+ prog="cvt7to9",
+ description= "Convert remap configuration from ATS7 to ATS9"
+)
+parser.add_argument("--filepath", default="remap.config", help="path specifier of remap config file")
+parser.add_argument("--prefix", default="1st-", help="prefix for new header_rewrite config files")
+parser.add_argument(
+ "--plugin", default=None, help="path specifier (relative to FILEPATH) of (global) plugins config file"
+)
+args = parser.parse_args()
+
+import sys
+
+def eprint(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+# Prefix to add to file relative pathspecs before opening file.
+#
+pathspec_prefix = None
+
+import os
+import copy
+
+# Remap or header rewrite config file.
+#
+class CfgFile:
+ def __init__(self, param):
+ if isinstance(param, CfgFile):
+ # Making copy of header rewrite config file to use when header rewrite is first plugin for remap
+ # rule.
+ #
+ self.changed = True
+ d = os.path.dirname(param.pathspec)
+ if (d != "") and (d != "/"):
+ d += "/"
+ self.pathspec = d + args.prefix + os.path.basename(param.pathspec)
+ self.lines = copy.copy(param.lines)
+
+ else:
+ # Opening existing config file, param is pathspec.
+ #
+ self.changed = False
+ self.pathspec = param
+ ps = param
+ if ps[:1] != "/":
+ ps = pathspec_prefix + ps
+ try:
+ fd = open(ps, "r")
+ except:
+ eprint(f"fatal error: failure opening {ps} for reading")
+ sys.exit(1)
+
+ try:
+ self.lines = fd.readlines()
+ except:
+ eprint(f"fatal error: failure reading {ps}")
+ sys.exit(1)
+
+ try:
+ fd.close()
+ except:
+ eprint(f"fatal error: failure closing {ps}")
+ sys.exit(1)
+
+ # Write out new version of file if it's contents are new. ".new" is appended for the pathspec to get the
+ # pathspec of the new file.
+ #
+ def close(self):
+ if self.changed:
+ ps = self.pathspec
+ if ps[:1] != "/":
+ ps = pathspec_prefix + ps
+ print(ps)
+ ps += ".new"
+ try:
+ fd = open(ps, "w")
+ except:
+ eprint(f"fatal error: failure opening {ps} for writing")
+ sys.exit(1)
+
+ for ln in self.lines:
+ if ln != None:
+ try:
+ fd.write(ln)
+ except:
+ eprint(f"fatal error: failure writing {ps}")
+ sys.exit(1)
+
+ try:
+ fd.close()
+ except:
+ eprint(f"fatal error: failure closing {ps}")
+ sys.exit(1)
+
+# A generic object.
+#
+class Obj:
+ def __init__(self):
+ pass
+
+# A dictionary that is a mapping from pathspecs for header rewrite config files to generic objects. If the
+# object has a "first" attribute, it is a list of 2-tuples. The first tuple entry is a reference to the
+# the CfgFile object for a remap config file. The second tuple entry is the index into the list of lines,
+# with the index of the line where header_rewrite.so is the first remap plugin, and the header rewrite
+# config file is a parameter to it. If "other" is an attribute of the generic object, the config file is
+# used as a parameter to calls to a global instance of header_rewrite.so, or instances of header_rewrite.so on
+# a remap rule as the second or later remap plugin. (The value of the "other" attribute is always None because
+# the value doesn't matter.)
+#
+header_rewrite_cfgs = dict()
+
+# Read in (global) header rewrite config file pathspecs from plugin.config file.
+#
+def handle_global(gbl_pathspec):
+
+ if gbl_pathspec[:1] != "/":
+ gbl_pathspec = pathspec_prefix + gbl_pathspec
+
+ try:
+ fd = open(gbl_pathspec, "r")
+ except:
+ eprint(f"fatal error: failure opening {gbl_pathspec} for reading")
+ sys.exit(1)
+
+ try:
+ lines = fd.readlines()
+ except:
+ eprint(f"fatal error: failure reading {gbl_pathspec}")
+ sys.exit(1)
+
+ try:
+ fd.close()
+ except:
+ eprint(f"fatal error: failure closing {gbl_pathspec}")
+ sys.exit(1)
+
+ ln_num = 0
+ while ln_num < len(lines):
+ ln = lines[ln_num]
+
+ # Join continuation lines to the first line.
+ #
+ num_ln_joined = 1
+ while ((ln_num + num_ln_joined) < len(lines)) and (ln[-2:] == (BACKSLASH + "\n")):
+ ln[:-2] += " " + lines[ln_num + num_ln_joined]
+ num_ln_joined += 1
+
+ ofst = ln.find("#")
+ if ofst >= 0:
+ # Remove comment.
+ #
+ ln = ln[:ofst]
+
+ ln = ln.split()
+
+ if (len(ln) > 0) and (ln[0] == "header_rewrite.so"):
+ for param in ln[1:]:
+ hr_obj = Obj()
+ hr_obj.other = None
+ header_rewrite_cfgs[param] = hr_obj
+ break
+
+ ln_num += num_ln_joined
+
+# A list of CfgFile instances for the main remap config file and any include files it contains, directly or
+# indirectly.
+#
+remap_cfgs = []
+
+# Handle the remap config file, and call this recursively to handle include files.
+#
+def handle_remap(filepath):
+
+ def skip_white(a_str):
+ if a_str[:1].isspace():
+ return 1
+ elif a_str.startswith(BACKSLASH + "\n"):
+ return 2
+ return 0
+
+ rc = CfgFile(filepath)
+
+ remap_cfgs.append(rc)
+
+ ln_num = 0
+ while ln_num < len(rc.lines):
+ ln = rc.lines[ln_num]
+
+ # Join continuation lines to the first line, and make them None in the rc.lines array.
+ #
+ num_ln_joined = 1
+ while ((ln_num + num_ln_joined) < len(rc.lines)) and (ln[-2:] == (BACKSLASH + "\n")):
+ ln += rc.lines[ln_num + num_ln_joined]
+ rc.lines[ln_num + num_ln_joined] = None
+ num_ln_joined += 1
+
+ rc.lines[ln_num] = ln
+
+ len_content = ln.find("#")
+ if len_content < 0:
+ len_content = len(ln) - 1
+
+ # Only process lines with content.
+ #
+ if (len_content > 0) and not ln[:len_content].isspace():
+ ofst = ln.find(".include")
+ if (ofst == 0) or ((ofst > 0) and ln[:ofst].isspace()):
+ ofst += len(".include")
+ start_ps = -1
+ while ofst < len_content:
+ sw = skip_white(ln[ofst:])
+ if sw == 0:
+ if start_ps < 0:
+ start_ps = ofst
+ ofst += 1
+ else:
+ if start_ps >= 0:
+ handle_remap(ln[start_ps:ofst])
+ start_ps = -1
+ ofst += sw
+
+ if start_ps >= 0:
+ handle_remap(ln[start_ps:len_content])
+ else:
+ # First, gzip.so -> compress.so
+ #
+ lnc = ln[:len_content]
+ lncr = lnc.replace("@plugin=gzip.so", "@plugin=compress.so")
+ if lncr != lnc:
+ ln = lncr + ln[len_content:]
+ len_content = len(lncr)
+ rc.lines[ln_num] = ln
+ rc.changed = True
+
+ ofst = ln[:len_content].find("@plugin=")
+ if ofst >= 0:
+ # Assuming it's some sort of remap line since it's got @plugin.
+ #
+ ofst += len("@plugin=")
+
+ new_ln = None
+
+ if ln[ofst:].startswith("header_rewrite.so"):
+ ofst += len("header_rewrite.so")
+
+ while ofst < len_content:
+ ofst2 = ln[ofst:len_content].find("@")
+ if ofst2 < 0:
+ break;
+
+ ofst += ofst2
+
+ if not ln[ofst:].startswith("@pparam="):
+ break
+
+ ofst += len("@pparam=")
+
+ ofst2 = ofst
+ while ofst2 < len_content:
+ sw = skip_white(ln[ofst2:])
+ if sw != 0:
+ break
+ ofst2 += 1
+
+ hr_pathspec = ln[ofst:ofst2]
+ ofst = ofst2
+
+ if hr_pathspec not in header_rewrite_cfgs:
+ hr_obj = Obj()
+ header_rewrite_cfgs[hr_pathspec] = hr_obj;
+ else:
+ hr_obj = header_rewrite_cfgs[hr_pathspec]
+
+ if not hasattr(hr_obj, "first"):
+ hr_obj.first = []
+
+ hr_obj.first.append((rc, ln_num))
+
+ elif ln[ofst:].startswith("regex_remap.so"):
+ ofst += len("regex_remap.so")
+
+ ofst2 = ln[ofst:len_content].find("@plugin=")
+
+ if ofst2 < 0:
+ ofst2 = len_content
+ else:
+ ofst2 += ofst
+
+ if ((ln[ofst:ofst2].find("@pparam=pristine") < 0) and
+ (ln[ofst:ofst2].find("@pparam=no-pristine") < 0)):
+
+ new_ln = ln[:ofst2]
+
+ if not new_ln[-1].isspace():
+ new_ln += " "
+
+ new_ln += "@pparam=pristine"
+
+ if not ln[ofst2].isspace():
+ new_ln += " "
+
+ ofst = len(new_ln)
+
+ new_ln += ln[ofst2:]
+
+ else:
+ ofst = ofst2
+
+ if new_ln:
+ rc.lines[ln_num] = new_ln
+ rc.changed = True
+ len_content += len(new_ln) - len(ln)
+ ln = new_ln
+
+ # Handle it if header rewrite is called as a second or later plugin.
+ #
+ while ofst < len_content:
+ ofst2 = ln[ofst:len_content].find("@plugin=header_rewrite.so")
+
+ if ofst2 < 0:
+ break
+
+ ofst += ofst2 + len("@plugin=header_rewrite.so")
+
+ while ofst < len_content:
+ ofst2 = ln[ofst:len_content].find("@")
+ if ofst2 < 0:
+ break
+
+ ofst += ofst2 + 1
+
+ if not ln[ofst:].startswith("pparam="):
+ ofst -= 1
+ break
+
+ ofst += len("pparam=")
+
+ ofst2 = ofst
+ while ofst2 < len_content:
+ sw = skip_white(ln[ofst2:])
+ if sw != 0:
+ break
+ ofst2 += 1
+
+ hr_pathspec = ln[ofst:ofst2]
+ ofst = ofst2
+
+ if hr_pathspec not in header_rewrite_cfgs:
+ hr_obj = Obj()
+ header_rewrite_cfgs[hr_pathspec] = hr_obj;
+ else:
+ hr_obj = header_rewrite_cfgs[hr_pathspec]
+
+ hr_obj.other = None # Only the existence of this attr matters, not its value.
+
+ ln_num += num_ln_joined
+
+def handle_header_rewrite(pathspec, obj):
+
+ # This class manages changes to a header rewrite config file.
+ #
+ class HRCfg:
+ # 'pathspec' is the key in header_rewrite_cfgs and 'obj' is the generic object value.
+ #
+ def __init__(self, pathspec, obj):
+ self._base = CfgFile(pathspec)
+ self._obj = obj
+
+ def has_first(self):
+ return hasattr(self._obj, "first")
+
+ def has_other(self):
+ return hasattr(self._obj, "other")
+
+ def get_lines(self):
+ return self._base.lines
+
+ # Assumes has_first is true.
+ #
+ def chg_ln_first(self, ln_num, ln):
+ if not hasattr(self, "_first"):
+ if self.has_other():
+ # Must make two versions of header rewrite config file.
+ #
+ self._first = CfgFile(self._base)
+ else:
+ self._first = self._base
+ self._first.changed = True
+
+ self._first.lines[ln_num] = ln
+
+ # Assumes has_other is true.
+ #
+ def chg_ln_cmn(self, ln_num, ln):
+ if hasattr(self, "_first") and not (self._first is self._base):
+ self._first.lines[ln_num] = ln
+
+ self._base.lines[ln_num] = ln
+ self._base.changed = True
+
+ def close(self):
+ self._base.close()
+ if hasattr(self, "_first") and not (self._first is self._base):
+ self._first.close()
+
+ old_pparam = "@pparam=" + self._base.pathspec
+
+ # Backpatch remap configuration files with pathspec of new header rewrite config file for call
+ # to header rewrite as first remap plugin.
+ #
+ for pair in self._obj.first:
+ rc = pair[0]
+ rc.changed = True
+ ln = rc.lines[pair[1]]
+ ofst = ln.find(old_pparam) + len("@pparam=")
+ rc.lines[pair[1]] = ln[:ofst] + self._first.pathspec + ln[ofst + len(self._base.pathspec):]
+
+ # Returns a pair: length of string before # comment, and a flag indicating if %< (beginning of a variable
+ # exapansion) was in any of the double-quoted strings.
+ #
+ def get_content_len(s):
+ # States.
+ #
+ COPYING = 0
+ ESCAPE_COPYING = 1
+ IN_QUOTED = 2
+ ESCAPE_IN_QUOTED = 3
+ PERCENT_IN_QUOTED = 4
+
+ state = COPYING
+ ofst = 0
+ variable_expansion = False
+
+ while ofst < (len(s) - 1):
+ if state == COPYING:
+ if s[ofst] == "#":
+ # Start of comment.
+ #
+ return (ofst, variable_expansion)
+ elif s[ofst] == '"':
+ state = IN_QUOTED
+ else:
+ if s[ofst] == BACKSLASH:
+ state = ESCAPE_COPYING
+
+ elif state == IN_QUOTED:
+ if s[ofst] == BACKSLASH:
+ state = ESCAPE_IN_QUOTED
+ elif s[ofst] == '"':
+ state = COPYING
+ elif s[ofst] == '%':
+ state = PERCENT_IN_QUOTED
+
+ elif state == ESCAPE_COPYING:
+ state = COPYING
+
+ elif state == ESCAPE_IN_QUOTED:
+ if s[ofst] == '%':
+ state = PERCENT_IN_QUOTED
+ else:
+ state = IN_QUOTED
+
+ elif state == PERCENT_IN_QUOTED:
+ if s[ofst] == '<':
+ variable_expansion = True
+ state = IN_QUOTED
+ elif s[ofst] == BACKSLASH:
+ state = ESCAPE_IN_QUOTED
+ elif s[ofst] == '"':
+ state = COPYING
+ elif s[ofst] == '%':
+ state = PERCENT_IN_QUOTED
+ else:
+ state = IN_QUOTED
+
+ ofst += 1
+
+ return (ofst, variable_expansion)
+
+ hrc = HRCfg(pathspec, obj)
+ lines = hrc.get_lines()
+ ln_num = 0
+ while ln_num < len(lines):
+ ln = lines[ln_num]
+ prepend_error = False
+
+ len_content, variable_expansion = get_content_len(ln)
+
+ if variable_expansion:
+ eprint(
+ f"error: {pathspec}, line {ln_num + 1}: variable expansions cannot be automatically converted"
+ )
+ prepend_error = True
+
+ lnc = ln[:len_content]
+ lnc = lnc.replace("%{CLIENT-IP}", "%{INBOUND:REMOTE-ADDR}")
+ lnc = lnc.replace("%{INCOMING-PORT}", "%{INBOUND:LOCAL-PORT}")
+ lnc = lnc.replace("%{PATH}", "%{URL:PATH}")
+ lnc = lnc.replace("%{QUERY}", "%{URL:QUERY}")
+
+ if hrc.has_other() and (prepend_error or (lnc != ln[:len_content])):
+ if prepend_error:
+ hrc.chg_ln_cmn(ln_num, "ERROR: " + lnc + ln[len_content:])
+ else:
+ hrc.chg_ln_cmn(ln_num, lnc + ln[len_content:])
+
+ if hrc.has_first():
+ lnc_save = lnc
+ ofst = lnc.find("set-destination")
+ if (ofst >= 0):
+ if (ofst == 0) or lnc[:ofst].isspace():
+ ofst += len("set-destination")
+ ofst2 = lnc[ofst:].find("PATH")
+ if (ofst2 > 0) and lnc[ofst:(ofst + ofst2)].isspace():
+ eprint(
+ f"error: {pathspec}, line {ln_num + 1}: the functionality of set-destination PATH" +
+ " for the first remap plugin does not exist in ATS9"
+ )
+ prepend_error = True
+
+ lnc = lnc.replace("%{URL:", "%{CLIENT-URL:")
+ if lnc != lnc_save or prepend_error:
+ if prepend_error:
+ hrc.chg_ln_first(ln_num, "ERROR: " + lnc + ln[len_content:])
+ else:
+ hrc.chg_ln_first(ln_num, lnc + ln[len_content:])
+
+ ln_num += 1
+
+ hrc.close()
+
+pathspec_prefix = os.path.dirname(args.filepath)
+if (pathspec_prefix != "") and (pathspec_prefix != "/"):
+ pathspec_prefix += "/"
+
+if not (args.plugin is None):
+ handle_global(args.plugin)
+
+handle_remap(os.path.basename(args.filepath))
+
+for pathspec in header_rewrite_cfgs:
+ handle_header_rewrite(pathspec, header_rewrite_cfgs[pathspec])
+
+# loop through remap_cfgs and close each one.
+for rc in remap_cfgs:
+ rc.close()
+
+sys.exit(0)
diff --git a/tools/insnew b/tools/insnew
new file mode 100755
index 0000000..94b1c8d
--- /dev/null
+++ b/tools/insnew
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# For each filespec FS given (one per line) on stdin, rename FS (if it exists) to FS.old, then rename FS.new to
+# FS.
+
+if [[ "$*" = "--help" ]] ; then
+ echo "For each filespec FS read from stdin, rename FS.new to FS. If FS already exists, rename to FS.old" >&2
+ exit 0
+fi
+
+while read F
+do
+python3 - "$F" << THE_END
+
+import sys
+import os
+
+def eprint(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+def rename_file(old_path, new_path):
+ try:
+ os.rename(old_path, new_path)
+ except:
+ eprint(f"fatal error: failure renaming {old_path} to {new_path}\n")
+ sys.exit(1)
+
+def rename_to_old(pathspec):
+ op = pathspec + ".old"
+
+ if os.access(op, os.F_OK):
+ last = 2
+ while os.access(op + ".{}".format(last), os.F_OK):
+ last += 1
+ while last > 2:
+ rename_file(op + ".{}".format(last - 1), op + ".{}".format(last))
+ last -= 1
+ rename_file(op, op + ".2")
+
+ rename_file(pathspec, op)
+
+filespec = sys.argv[1]
+if os.access(filespec, os.F_OK):
+ rename_to_old(filespec)
+rename_file(filespec + ".new", filespec)
+
+sys.exit(0)
+
+THE_END
+done