diff --git a/rattail_fabric2/__init__.py b/rattail_fabric2/__init__.py index c0bd1af..8a15349 100644 --- a/rattail_fabric2/__init__.py +++ b/rattail_fabric2/__init__.py @@ -37,4 +37,4 @@ from .core import ( set_timezone, UNSPECIFIED, ) -from .util import exists, contains, append +from .util import exists, contains, append, sed diff --git a/rattail_fabric2/util.py b/rattail_fabric2/util.py index acd776c..b15b86b 100644 --- a/rattail_fabric2/util.py +++ b/rattail_fabric2/util.py @@ -24,6 +24,8 @@ Misc. Utilities """ +import hashlib + def exists(c, path, use_sudo=False): """ @@ -192,3 +194,77 @@ def get_home_path(c, user=None): home = c.run('getent passwd {} | cut -d: -f6'.format(user)).stdout.strip() home = home.rstrip('/') return home + + +def sed(c, filename, before, after, limit='', use_sudo=False, backup='.bak', + flags='', + # shell=False, +): + """ + NOTE: This was copied from the upstream ``fabric.contrib.files`` (v1) module. + + Run a search-and-replace on ``filename`` with given regex patterns. + + Equivalent to ``sed -i -r -e "// s///g" + ``. Setting ``backup`` to an empty string will, disable backup + file creation. + + For convenience, ``before`` and ``after`` will automatically escape forward + slashes, single quotes and parentheses for you, so you don't need to + specify e.g. ``http:\/\/foo\.com``, instead just using ``http://foo\.com`` + is fine. + + If ``use_sudo`` is True, will use `sudo` instead of `run`. + + .. + The ``shell`` argument will be eventually passed to `run`/`sudo`. It + defaults to False in order to avoid problems with many nested levels of + quotes and backslashes. However, setting it to True may help when using + ``~fabric.operations.cd`` to wrap explicit or implicit ``sudo`` calls. + (``cd`` by it's nature is a shell built-in, not a standalone command, so it + should be called within a shell.) + + Other options may be specified with sed-compatible regex flags -- for + example, to make the search and replace case insensitive, specify + ``flags="i"``. The ``g`` flag is always specified regardless, so you do not + need to remember to include it when overriding this parameter. + """ + func = use_sudo and c.sudo or c.run + # Characters to be escaped in both + for char in "/'": + before = before.replace(char, r'\%s' % char) + after = after.replace(char, r'\%s' % char) + # Characters to be escaped in replacement only (they're useful in regexen + # in the 'before' part) + for char in "()": + after = after.replace(char, r'\%s' % char) + if limit: + limit = r'/%s/ ' % limit + context = { + 'script': r"'%ss/%s/%s/%sg'" % (limit, before, after, flags), + 'filename': _expand_path(c, filename), + 'backup': backup + } + # Test the OS because of differences between sed versions + + # with hide('running', 'stdout'): + # platform = run("uname", shell=False, pty=False) + platform = c.run("uname", pty=False, echo=False, hide=True) + if platform in ('NetBSD', 'OpenBSD', 'QNX'): + # Attempt to protect against failures/collisions + hasher = hashlib.sha1() + hasher.update(c.host_string) # TODO: what did env.host_string become? + hasher.update(filename) + context['tmp'] = "/tmp/%s" % hasher.hexdigest() + # Use temp file to work around lack of -i + expr = r"""cp -p %(filename)s %(tmp)s \ +&& sed -r -e %(script)s %(filename)s > %(tmp)s \ +&& cp -p %(filename)s %(filename)s%(backup)s \ +&& mv %(tmp)s %(filename)s""" + else: + context['extended_regex'] = '-E' if platform == 'Darwin' else '-r' + expr = r"sed -i%(backup)s %(extended_regex)s -e %(script)s %(filename)s" + command = expr % context + return func(command, + # shell=shell + )