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 |