| # Conversion pipeline templates |
| # ============================= |
| |
| |
| # The problem: |
| # ------------ |
| # |
| # Suppose you have some data that you want to convert to another format |
| # (e.g. from GIF image format to PPM image format). Maybe the |
| # conversion involves several steps (e.g. piping it through compress or |
| # uuencode). Some of the conversion steps may require that their input |
| # is a disk file, others may be able to read standard input; similar for |
| # their output. The input to the entire conversion may also be read |
| # from a disk file or from an open file, and similar for its output. |
| # |
| # The module lets you construct a pipeline template by sticking one or |
| # more conversion steps together. It will take care of creating and |
| # removing temporary files if they are necessary to hold intermediate |
| # data. You can then use the template to do conversions from many |
| # different sources to many different destinations. The temporary |
| # file names used are different each time the template is used. |
| # |
| # The templates are objects so you can create templates for many |
| # different conversion steps and store them in a dictionary, for |
| # instance. |
| |
| |
| # Directions: |
| # ----------- |
| # |
| # To create a template: |
| # t = Template() |
| # |
| # To add a conversion step to a template: |
| # t.append(command, kind) |
| # where kind is a string of two characters: the first is '-' if the |
| # command reads its standard input or 'f' if it requires a file; the |
| # second likewise for the output. The command must be valid /bin/sh |
| # syntax. If input or output files are required, they are passed as |
| # $IN and $OUT; otherwise, it must be possible to use the command in |
| # a pipeline. |
| # |
| # To add a conversion step at the beginning: |
| # t.prepend(command, kind) |
| # |
| # To convert a file to another file using a template: |
| # sts = t.copy(infile, outfile) |
| # If infile or outfile are the empty string, standard input is read or |
| # standard output is written, respectively. The return value is the |
| # exit status of the conversion pipeline. |
| # |
| # To open a file for reading or writing through a conversion pipeline: |
| # fp = t.open(file, mode) |
| # where mode is 'r' to read the file, or 'w' to write it -- just like |
| # for the built-in function open() or for os.popen(). |
| # |
| # To create a new template object initialized to a given one: |
| # t2 = t.clone() |
| # |
| # For an example, see the function test() at the end of the file. |
| |
| |
| import sys |
| import re |
| |
| import os |
| import tempfile |
| import string |
| |
| |
| # Conversion step kinds |
| |
| FILEIN_FILEOUT = 'ff' # Must read & write real files |
| STDIN_FILEOUT = '-f' # Must write a real file |
| FILEIN_STDOUT = 'f-' # Must read a real file |
| STDIN_STDOUT = '--' # Normal pipeline element |
| SOURCE = '.-' # Must be first, writes stdout |
| SINK = '-.' # Must be last, reads stdin |
| |
| stepkinds = [FILEIN_FILEOUT, STDIN_FILEOUT, FILEIN_STDOUT, STDIN_STDOUT, \ |
| SOURCE, SINK] |
| |
| |
| # A pipeline template is a Template object: |
| |
| class Template: |
| |
| # Template() returns a fresh pipeline template |
| def __init__(self): |
| self.debugging = 0 |
| self.reset() |
| |
| # t.__repr__() implements `t` |
| def __repr__(self): |
| return '<Template instance, steps=' + `self.steps` + '>' |
| |
| # t.reset() restores a pipeline template to its initial state |
| def reset(self): |
| self.steps = [] |
| |
| # t.clone() returns a new pipeline template with identical |
| # initial state as the current one |
| def clone(self): |
| t = Template() |
| t.steps = self.steps[:] |
| t.debugging = self.debugging |
| return t |
| |
| # t.debug(flag) turns debugging on or off |
| def debug(self, flag): |
| self.debugging = flag |
| |
| # t.append(cmd, kind) adds a new step at the end |
| def append(self, cmd, kind): |
| if type(cmd) <> type(''): |
| raise TypeError, \ |
| 'Template.append: cmd must be a string' |
| if kind not in stepkinds: |
| raise ValueError, \ |
| 'Template.append: bad kind ' + `kind` |
| if kind == SOURCE: |
| raise ValueError, \ |
| 'Template.append: SOURCE can only be prepended' |
| if self.steps <> [] and self.steps[-1][1] == SINK: |
| raise ValueError, \ |
| 'Template.append: already ends with SINK' |
| if kind[0] == 'f' and not re.search('\$IN\b', cmd): |
| raise ValueError, \ |
| 'Template.append: missing $IN in cmd' |
| if kind[1] == 'f' and not re.search('\$OUT\b', cmd): |
| raise ValueError, \ |
| 'Template.append: missing $OUT in cmd' |
| self.steps.append((cmd, kind)) |
| |
| # t.prepend(cmd, kind) adds a new step at the front |
| def prepend(self, cmd, kind): |
| if type(cmd) <> type(''): |
| raise TypeError, \ |
| 'Template.prepend: cmd must be a string' |
| if kind not in stepkinds: |
| raise ValueError, \ |
| 'Template.prepend: bad kind ' + `kind` |
| if kind == SINK: |
| raise ValueError, \ |
| 'Template.prepend: SINK can only be appended' |
| if self.steps <> [] and self.steps[0][1] == SOURCE: |
| raise ValueError, \ |
| 'Template.prepend: already begins with SOURCE' |
| if kind[0] == 'f' and not re.search('\$IN\b', cmd): |
| raise ValueError, \ |
| 'Template.prepend: missing $IN in cmd' |
| if kind[1] == 'f' and not re.search('\$OUT\b', cmd): |
| raise ValueError, \ |
| 'Template.prepend: missing $OUT in cmd' |
| self.steps.insert(0, (cmd, kind)) |
| |
| # t.open(file, rw) returns a pipe or file object open for |
| # reading or writing; the file is the other end of the pipeline |
| def open(self, file, rw): |
| if rw == 'r': |
| return self.open_r(file) |
| if rw == 'w': |
| return self.open_w(file) |
| raise ValueError, \ |
| 'Template.open: rw must be \'r\' or \'w\', not ' + `rw` |
| |
| # t.open_r(file) and t.open_w(file) implement |
| # t.open(file, 'r') and t.open(file, 'w') respectively |
| |
| def open_r(self, file): |
| if self.steps == []: |
| return open(file, 'r') |
| if self.steps[-1][1] == SINK: |
| raise ValueError, \ |
| 'Template.open_r: pipeline ends width SINK' |
| cmd = self.makepipeline(file, '') |
| return os.popen(cmd, 'r') |
| |
| def open_w(self, file): |
| if self.steps == []: |
| return open(file, 'w') |
| if self.steps[0][1] == SOURCE: |
| raise ValueError, \ |
| 'Template.open_w: pipeline begins with SOURCE' |
| cmd = self.makepipeline('', file) |
| return os.popen(cmd, 'w') |
| |
| def copy(self, infile, outfile): |
| return os.system(self.makepipeline(infile, outfile)) |
| |
| def makepipeline(self, infile, outfile): |
| cmd = makepipeline(infile, self.steps, outfile) |
| if self.debugging: |
| print cmd |
| cmd = 'set -x; ' + cmd |
| return cmd |
| |
| |
| def makepipeline(infile, steps, outfile): |
| # Build a list with for each command: |
| # [input filename or '', command string, kind, output filename or ''] |
| |
| list = [] |
| for cmd, kind in steps: |
| list.append(['', cmd, kind, '']) |
| # |
| # Make sure there is at least one step |
| # |
| if list == []: |
| list.append(['', 'cat', '--', '']) |
| # |
| # Take care of the input and output ends |
| # |
| [cmd, kind] = list[0][1:3] |
| if kind[0] == 'f' and not infile: |
| list.insert(0, ['', 'cat', '--', '']) |
| list[0][0] = infile |
| # |
| [cmd, kind] = list[-1][1:3] |
| if kind[1] == 'f' and not outfile: |
| list.append(['', 'cat', '--', '']) |
| list[-1][-1] = outfile |
| # |
| # Invent temporary files to connect stages that need files |
| # |
| garbage = [] |
| for i in range(1, len(list)): |
| lkind = list[i-1][2] |
| rkind = list[i][2] |
| if lkind[1] == 'f' or rkind[0] == 'f': |
| temp = tempfile.mktemp() |
| garbage.append(temp) |
| list[i-1][-1] = list[i][0] = temp |
| # |
| for item in list: |
| [inf, cmd, kind, outf] = item |
| if kind[1] == 'f': |
| cmd = 'OUT=' + quote(outf) + '; ' + cmd |
| if kind[0] == 'f': |
| cmd = 'IN=' + quote(inf) + '; ' + cmd |
| if kind[0] == '-' and inf: |
| cmd = cmd + ' <' + quote(inf) |
| if kind[1] == '-' and outf: |
| cmd = cmd + ' >' + quote(outf) |
| item[1] = cmd |
| # |
| cmdlist = list[0][1] |
| for item in list[1:]: |
| [cmd, kind] = item[1:3] |
| if item[0] == '': |
| if 'f' in kind: |
| cmd = '{ ' + cmd + '; }' |
| cmdlist = cmdlist + ' |\n' + cmd |
| else: |
| cmdlist = cmdlist + '\n' + cmd |
| # |
| if garbage: |
| rmcmd = 'rm -f' |
| for file in garbage: |
| rmcmd = rmcmd + ' ' + quote(file) |
| trapcmd = 'trap ' + quote(rmcmd + '; exit') + ' 1 2 3 13 14 15' |
| cmdlist = trapcmd + '\n' + cmdlist + '\n' + rmcmd |
| # |
| return cmdlist |
| |
| |
| # Reliably quote a string as a single argument for /bin/sh |
| |
| _safechars = string.letters + string.digits + '!@%_-+=:,./' # Safe unquoted |
| _funnychars = '"`$\\' # Unsafe inside "double quotes" |
| |
| def quote(file): |
| for c in file: |
| if c not in _safechars: |
| break |
| else: |
| return file |
| if '\'' not in file: |
| return '\'' + file + '\'' |
| res = '' |
| for c in file: |
| if c in _funnychars: |
| c = '\\' + c |
| res = res + c |
| return '"' + res + '"' |
| |
| |
| # Small test program and example |
| |
| def test(): |
| print 'Testing...' |
| t = Template() |
| t.append('togif $IN $OUT', 'ff') |
| t.append('giftoppm', '--') |
| t.append('ppmtogif >$OUT', '-f') |
| t.append('fromgif $IN $OUT', 'ff') |
| t.debug(1) |
| FILE = '/usr/local/images/rgb/rogues/guido.rgb' |
| t.copy(FILE, '@temp') |
| print 'Done.' |