| Index: third_party/logilab/common/shellutils.py
|
| diff --git a/third_party/logilab/common/shellutils.py b/third_party/logilab/common/shellutils.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c713913502d145d1ad4858d62118443f7d80e1bc
|
| --- /dev/null
|
| +++ b/third_party/logilab/common/shellutils.py
|
| @@ -0,0 +1,443 @@
|
| +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
|
| +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
| +#
|
| +# This file is part of logilab-common.
|
| +#
|
| +# logilab-common is free software: you can redistribute it and/or modify it under
|
| +# the terms of the GNU Lesser General Public License as published by the Free
|
| +# Software Foundation, either version 2.1 of the License, or (at your option) any
|
| +# later version.
|
| +#
|
| +# logilab-common is distributed in the hope that it will be useful, but WITHOUT
|
| +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
| +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
| +# details.
|
| +#
|
| +# You should have received a copy of the GNU Lesser General Public License along
|
| +# with logilab-common. If not, see <http://www.gnu.org/licenses/>.
|
| +"""shell/term utilities, useful to write some python scripts instead of shell
|
| +scripts.
|
| +"""
|
| +__docformat__ = "restructuredtext en"
|
| +
|
| +import os
|
| +import glob
|
| +import shutil
|
| +import stat
|
| +import sys
|
| +import tempfile
|
| +import time
|
| +import fnmatch
|
| +import errno
|
| +import string
|
| +import random
|
| +from os.path import exists, isdir, islink, basename, join
|
| +
|
| +from logilab.common import STD_BLACKLIST, _handle_blacklist
|
| +from logilab.common.compat import raw_input
|
| +from logilab.common.compat import str_to_bytes
|
| +
|
| +try:
|
| + from logilab.common.proc import ProcInfo, NoSuchProcess
|
| +except ImportError:
|
| + # windows platform
|
| + class NoSuchProcess(Exception): pass
|
| +
|
| + def ProcInfo(pid):
|
| + raise NoSuchProcess()
|
| +
|
| +
|
| +class tempdir(object):
|
| +
|
| + def __enter__(self):
|
| + self.path = tempfile.mkdtemp()
|
| + return self.path
|
| +
|
| + def __exit__(self, exctype, value, traceback):
|
| + # rmtree in all cases
|
| + shutil.rmtree(self.path)
|
| + return traceback is None
|
| +
|
| +
|
| +class pushd(object):
|
| + def __init__(self, directory):
|
| + self.directory = directory
|
| +
|
| + def __enter__(self):
|
| + self.cwd = os.getcwd()
|
| + os.chdir(self.directory)
|
| + return self.directory
|
| +
|
| + def __exit__(self, exctype, value, traceback):
|
| + os.chdir(self.cwd)
|
| +
|
| +
|
| +def chown(path, login=None, group=None):
|
| + """Same as `os.chown` function but accepting user login or group name as
|
| + argument. If login or group is omitted, it's left unchanged.
|
| +
|
| + Note: you must own the file to chown it (or be root). Otherwise OSError is raised.
|
| + """
|
| + if login is None:
|
| + uid = -1
|
| + else:
|
| + try:
|
| + uid = int(login)
|
| + except ValueError:
|
| + import pwd # Platforms: Unix
|
| + uid = pwd.getpwnam(login).pw_uid
|
| + if group is None:
|
| + gid = -1
|
| + else:
|
| + try:
|
| + gid = int(group)
|
| + except ValueError:
|
| + import grp
|
| + gid = grp.getgrnam(group).gr_gid
|
| + os.chown(path, uid, gid)
|
| +
|
| +def mv(source, destination, _action=shutil.move):
|
| + """A shell-like mv, supporting wildcards.
|
| + """
|
| + sources = glob.glob(source)
|
| + if len(sources) > 1:
|
| + assert isdir(destination)
|
| + for filename in sources:
|
| + _action(filename, join(destination, basename(filename)))
|
| + else:
|
| + try:
|
| + source = sources[0]
|
| + except IndexError:
|
| + raise OSError('No file matching %s' % source)
|
| + if isdir(destination) and exists(destination):
|
| + destination = join(destination, basename(source))
|
| + try:
|
| + _action(source, destination)
|
| + except OSError, ex:
|
| + raise OSError('Unable to move %r to %r (%s)' % (
|
| + source, destination, ex))
|
| +
|
| +def rm(*files):
|
| + """A shell-like rm, supporting wildcards.
|
| + """
|
| + for wfile in files:
|
| + for filename in glob.glob(wfile):
|
| + if islink(filename):
|
| + os.remove(filename)
|
| + elif isdir(filename):
|
| + shutil.rmtree(filename)
|
| + else:
|
| + os.remove(filename)
|
| +
|
| +def cp(source, destination):
|
| + """A shell-like cp, supporting wildcards.
|
| + """
|
| + mv(source, destination, _action=shutil.copy)
|
| +
|
| +def find(directory, exts, exclude=False, blacklist=STD_BLACKLIST):
|
| + """Recursively find files ending with the given extensions from the directory.
|
| +
|
| + :type directory: str
|
| + :param directory:
|
| + directory where the search should start
|
| +
|
| + :type exts: basestring or list or tuple
|
| + :param exts:
|
| + extensions or lists or extensions to search
|
| +
|
| + :type exclude: boolean
|
| + :param exts:
|
| + if this argument is True, returning files NOT ending with the given
|
| + extensions
|
| +
|
| + :type blacklist: list or tuple
|
| + :param blacklist:
|
| + optional list of files or directory to ignore, default to the value of
|
| + `logilab.common.STD_BLACKLIST`
|
| +
|
| + :rtype: list
|
| + :return:
|
| + the list of all matching files
|
| + """
|
| + if isinstance(exts, basestring):
|
| + exts = (exts,)
|
| + if exclude:
|
| + def match(filename, exts):
|
| + for ext in exts:
|
| + if filename.endswith(ext):
|
| + return False
|
| + return True
|
| + else:
|
| + def match(filename, exts):
|
| + for ext in exts:
|
| + if filename.endswith(ext):
|
| + return True
|
| + return False
|
| + files = []
|
| + for dirpath, dirnames, filenames in os.walk(directory):
|
| + _handle_blacklist(blacklist, dirnames, filenames)
|
| + # don't append files if the directory is blacklisted
|
| + dirname = basename(dirpath)
|
| + if dirname in blacklist:
|
| + continue
|
| + files.extend([join(dirpath, f) for f in filenames if match(f, exts)])
|
| + return files
|
| +
|
| +
|
| +def globfind(directory, pattern, blacklist=STD_BLACKLIST):
|
| + """Recursively finds files matching glob `pattern` under `directory`.
|
| +
|
| + This is an alternative to `logilab.common.shellutils.find`.
|
| +
|
| + :type directory: str
|
| + :param directory:
|
| + directory where the search should start
|
| +
|
| + :type pattern: basestring
|
| + :param pattern:
|
| + the glob pattern (e.g *.py, foo*.py, etc.)
|
| +
|
| + :type blacklist: list or tuple
|
| + :param blacklist:
|
| + optional list of files or directory to ignore, default to the value of
|
| + `logilab.common.STD_BLACKLIST`
|
| +
|
| + :rtype: iterator
|
| + :return:
|
| + iterator over the list of all matching files
|
| + """
|
| + for curdir, dirnames, filenames in os.walk(directory):
|
| + _handle_blacklist(blacklist, dirnames, filenames)
|
| + for fname in fnmatch.filter(filenames, pattern):
|
| + yield join(curdir, fname)
|
| +
|
| +def unzip(archive, destdir):
|
| + import zipfile
|
| + if not exists(destdir):
|
| + os.mkdir(destdir)
|
| + zfobj = zipfile.ZipFile(archive)
|
| + for name in zfobj.namelist():
|
| + if name.endswith('/'):
|
| + os.mkdir(join(destdir, name))
|
| + else:
|
| + outfile = open(join(destdir, name), 'wb')
|
| + outfile.write(zfobj.read(name))
|
| + outfile.close()
|
| +
|
| +class Execute:
|
| + """This is a deadlock safe version of popen2 (no stdin), that returns
|
| + an object with errorlevel, out and err.
|
| + """
|
| +
|
| + def __init__(self, command):
|
| + outfile = tempfile.mktemp()
|
| + errfile = tempfile.mktemp()
|
| + self.status = os.system("( %s ) >%s 2>%s" %
|
| + (command, outfile, errfile)) >> 8
|
| + self.out = open(outfile, "r").read()
|
| + self.err = open(errfile, "r").read()
|
| + os.remove(outfile)
|
| + os.remove(errfile)
|
| +
|
| +def acquire_lock(lock_file, max_try=10, delay=10, max_delay=3600):
|
| + """Acquire a lock represented by a file on the file system
|
| +
|
| + If the process written in lock file doesn't exist anymore, we remove the
|
| + lock file immediately
|
| + If age of the lock_file is greater than max_delay, then we raise a UserWarning
|
| + """
|
| + count = abs(max_try)
|
| + while count:
|
| + try:
|
| + fd = os.open(lock_file, os.O_EXCL | os.O_RDWR | os.O_CREAT)
|
| + os.write(fd, str_to_bytes(str(os.getpid())) )
|
| + os.close(fd)
|
| + return True
|
| + except OSError, e:
|
| + if e.errno == errno.EEXIST:
|
| + try:
|
| + fd = open(lock_file, "r")
|
| + pid = int(fd.readline())
|
| + pi = ProcInfo(pid)
|
| + age = (time.time() - os.stat(lock_file)[stat.ST_MTIME])
|
| + if age / max_delay > 1 :
|
| + raise UserWarning("Command '%s' (pid %s) has locked the "
|
| + "file '%s' for %s minutes"
|
| + % (pi.name(), pid, lock_file, age/60))
|
| + except UserWarning:
|
| + raise
|
| + except NoSuchProcess:
|
| + os.remove(lock_file)
|
| + except Exception:
|
| + # The try block is not essential. can be skipped.
|
| + # Note: ProcInfo object is only available for linux
|
| + # process information are not accessible...
|
| + # or lock_file is no more present...
|
| + pass
|
| + else:
|
| + raise
|
| + count -= 1
|
| + time.sleep(delay)
|
| + else:
|
| + raise Exception('Unable to acquire %s' % lock_file)
|
| +
|
| +def release_lock(lock_file):
|
| + """Release a lock represented by a file on the file system."""
|
| + os.remove(lock_file)
|
| +
|
| +
|
| +class ProgressBar(object):
|
| + """A simple text progression bar."""
|
| +
|
| + def __init__(self, nbops, size=20, stream=sys.stdout, title=''):
|
| + if title:
|
| + self._fstr = '\r%s [%%-%ss]' % (title, int(size))
|
| + else:
|
| + self._fstr = '\r[%%-%ss]' % int(size)
|
| + self._stream = stream
|
| + self._total = nbops
|
| + self._size = size
|
| + self._current = 0
|
| + self._progress = 0
|
| + self._current_text = None
|
| + self._last_text_write_size = 0
|
| +
|
| + def _get_text(self):
|
| + return self._current_text
|
| +
|
| + def _set_text(self, text=None):
|
| + if text != self._current_text:
|
| + self._current_text = text
|
| + self.refresh()
|
| +
|
| + def _del_text(self):
|
| + self.text = None
|
| +
|
| + text = property(_get_text, _set_text, _del_text)
|
| +
|
| + def update(self):
|
| + """Update the progression bar."""
|
| + self._current += 1
|
| + progress = int((float(self._current)/float(self._total))*self._size)
|
| + if progress > self._progress:
|
| + self._progress = progress
|
| + self.refresh()
|
| +
|
| + def refresh(self):
|
| + """Refresh the progression bar display."""
|
| + self._stream.write(self._fstr % ('.' * min(self._progress, self._size)) )
|
| + if self._last_text_write_size or self._current_text:
|
| + template = ' %%-%is' % (self._last_text_write_size)
|
| + text = self._current_text
|
| + if text is None:
|
| + text = ''
|
| + self._stream.write(template % text)
|
| + self._last_text_write_size = len(text.rstrip())
|
| + self._stream.flush()
|
| +
|
| + def finish(self):
|
| + self._stream.write('\n')
|
| + self._stream.flush()
|
| +
|
| +
|
| +class DummyProgressBar(object):
|
| + __slot__ = ('text',)
|
| +
|
| + def refresh(self):
|
| + pass
|
| + def update(self):
|
| + pass
|
| + def finish(self):
|
| + pass
|
| +
|
| +
|
| +_MARKER = object()
|
| +class progress(object):
|
| +
|
| + def __init__(self, nbops=_MARKER, size=_MARKER, stream=_MARKER, title=_MARKER, enabled=True):
|
| + self.nbops = nbops
|
| + self.size = size
|
| + self.stream = stream
|
| + self.title = title
|
| + self.enabled = enabled
|
| +
|
| + def __enter__(self):
|
| + if self.enabled:
|
| + kwargs = {}
|
| + for attr in ('nbops', 'size', 'stream', 'title'):
|
| + value = getattr(self, attr)
|
| + if value is not _MARKER:
|
| + kwargs[attr] = value
|
| + self.pb = ProgressBar(**kwargs)
|
| + else:
|
| + self.pb = DummyProgressBar()
|
| + return self.pb
|
| +
|
| + def __exit__(self, exc_type, exc_val, exc_tb):
|
| + self.pb.finish()
|
| +
|
| +class RawInput(object):
|
| +
|
| + def __init__(self, input=None, printer=None):
|
| + self._input = input or raw_input
|
| + self._print = printer
|
| +
|
| + def ask(self, question, options, default):
|
| + assert default in options
|
| + choices = []
|
| + for option in options:
|
| + if option == default:
|
| + label = option[0].upper()
|
| + else:
|
| + label = option[0].lower()
|
| + if len(option) > 1:
|
| + label += '(%s)' % option[1:].lower()
|
| + choices.append((option, label))
|
| + prompt = "%s [%s]: " % (question,
|
| + '/'.join([opt[1] for opt in choices]))
|
| + tries = 3
|
| + while tries > 0:
|
| + answer = self._input(prompt).strip().lower()
|
| + if not answer:
|
| + return default
|
| + possible = [option for option, label in choices
|
| + if option.lower().startswith(answer)]
|
| + if len(possible) == 1:
|
| + return possible[0]
|
| + elif len(possible) == 0:
|
| + msg = '%s is not an option.' % answer
|
| + else:
|
| + msg = ('%s is an ambiguous answer, do you mean %s ?' % (
|
| + answer, ' or '.join(possible)))
|
| + if self._print:
|
| + self._print(msg)
|
| + else:
|
| + print msg
|
| + tries -= 1
|
| + raise Exception('unable to get a sensible answer')
|
| +
|
| + def confirm(self, question, default_is_yes=True):
|
| + default = default_is_yes and 'y' or 'n'
|
| + answer = self.ask(question, ('y', 'n'), default)
|
| + return answer == 'y'
|
| +
|
| +ASK = RawInput()
|
| +
|
| +
|
| +def getlogin():
|
| + """avoid using os.getlogin() because of strange tty / stdin problems
|
| + (man 3 getlogin)
|
| + Another solution would be to use $LOGNAME, $USER or $USERNAME
|
| + """
|
| + if sys.platform != 'win32':
|
| + import pwd # Platforms: Unix
|
| + return pwd.getpwuid(os.getuid())[0]
|
| + else:
|
| + return os.environ['USERNAME']
|
| +
|
| +def generate_password(length=8, vocab=string.ascii_letters + string.digits):
|
| + """dumb password generation function"""
|
| + pwd = ''
|
| + for i in xrange(length):
|
| + pwd += random.choice(vocab)
|
| + return pwd
|
|
|