def associate(committish, quiet=False): """Associate the current branch with a commit-ish. :param str or unicode committish: the commit-ish to associate the current branch with :param bool quiet: suppress non-error output """ if not directories.is_git_repository(): messages.error('{0!r} not a git repository'.format(os.getcwd())) elif git.is_empty_repository(): messages.error('cannot associate while empty') elif git.is_detached(): messages.error('cannot associate while HEAD is detached') # is it a ref? if git.is_ref(committish): if not git.is_ref_ambiguous(committish, limit=('heads', 'tags')): committish = git.symbolic_full_name(committish) else: _ambiguous_ref(committish) else: resolved_committish = git.resolve_sha1(committish) if not resolved_committish: messages.error('{} is not a valid revision'.format(committish)) committish = resolved_committish current_branch = git.current_branch() subprocess.call(['git', 'config', '--local', 'git-changes.associations.' + current_branch + '.with', committish]) messages.info('{} has been associated with {}'.format(current_branch, committish), quiet)
def unassociate(branch=None, cleanup=None, quiet=False, dry_run=False): """Unassociate a branch. :param str or unicode branch: branch to unassociate :param str or unicode cleanup: cleanup action (one of: all, prune) :param bool quiet: suppress non-error output :param bool dry_run: show the association(s) that would be remove but do nothing """ assert not cleanup or cleanup in ('all', 'prune'), 'cleanup must be one of ' + str(['all', 'prune']) if not directories.is_git_repository(): messages.error('{0!r} not a git repository'.format(os.getcwd())) elif git.is_empty_repository(): return if cleanup: _prune_associations(cleanup, quiet, dry_run) else: branch = branch if branch else git.current_branch() current_association = get_association(branch) if current_association: if dry_run: messages.info('Would unassociate {0!r} from {1!r}'.format(branch, current_association)) else: subprocess.call(['git', 'config', '--local', '--remove-section', 'git-changes.associations.' + branch])
def changes(committish, details=None, color_when=None): """Print the changes between a given branch and HEAD. :param str or unicode committish: commit-ish to view changes from :param str or unicode details: the level of details to show (diff, stat, or None) :param str or unicode color_when: when to color output """ assert not details or details in _DETAIL_OPTIONS, 'details must be one of ' + str(_DETAIL_OPTIONS) assert not color_when or color_when in _COLOR_OPTIONS, 'color_when must be one of ' + str(_COLOR_OPTIONS) if not directories.is_git_repository(): messages.error('{0!r} not a git repository'.format(os.getcwd())) elif not git.is_commit(committish): messages.error('{0!r} is not a valid commit'.format(committish)) elif git.is_ref(committish) and git.is_ref_ambiguous(committish, limit=('heads', 'tags')): _ambiguous_ref(committish) color_when = git.resolve_coloring(color_when) if details == 'diff': subprocess.call(['git', 'diff', '--color={}'.format(color_when), committish + '...HEAD']) elif details == 'stat': subprocess.call(['git', 'diff', '--color={}'.format(color_when), '--stat', committish + '...HEAD']) else: command = ['git', 'log', '--no-decorate', '--oneline', '{}..HEAD'.format(committish)] if details == 'count': log = subprocess.check_output(command) log = log.splitlines() messages.info(str(len(log))) else: command += ['--color={}'.format(color_when)] subprocess.call(command)
def _prune_associations(cleanup, quiet, dry_run=False): """Remove associations for branches that no longer exist.""" # get branches and associations current_branches = [ref.split()[1][11:] for ref in subprocess.check_output(('git', 'show-ref', '--heads')).splitlines()] current_associations = _get_associated_branches() branches_to_prune = current_associations if cleanup == 'prune': # remove only stale associations branches_to_prune = list(set(current_associations) - set(current_branches)) for to_prune in branches_to_prune: if dry_run: messages.info('Would remove association {0!r}'.format(to_prune), quiet) else: unassociate(to_prune) messages.info('Removed association {0!r}'.format(to_prune), quiet)
def restash(stash='stash@{0}', quiet=False): """Restash a stash reference. :param str or unicode stash: stash reference to reverse apply :param bool quiet: suppress all output """ if not subprocess.check_output('git stash list'.split()): messages.error('no stashes exist') if not _is_valid_stash(stash): messages.error('{} is not a valid stash reference'.format(stash)) _reverse_modifications(stash) _remove_untracked_files(stash) stash_sha = subprocess.check_output(['git', 'rev-parse', stash]).splitlines()[0] messages.info('Restashed {} ({})'.format(stash, stash_sha), quiet)
def snapshot(message=None, quiet=False, files=None): """Create a snapshot of the working directory and index. :param str or unicode message: the message to use when creating the underlying stash :param bool quiet: suppress all output :param list files: a list of pathspecs to specific files to use when creating the snapshot """ status_command = ['git', 'status', '--porcelain'] status_output = subprocess.check_output(status_command).splitlines() # if there aren't any changes then we don't have anything to do if not status_output: messages.info('No local changes to save. No snapshot created.', quiet) return stash_command = ['git', 'stash', 'push', '--include-untracked', '--quiet'] stash_command = stash_command if message is None else stash_command + ['--message', message] stash_command = stash_command if not files else stash_command + ['--'] + files _stash_buffer(quiet) subprocess.call(stash_command) # apply isn't completely quiet when the stash only contains untracked files so swallow all output execute.swallow(['git', 'stash', 'apply', '--quiet', '--index'])
def makeConfig(name, version, cmdline_args): """ """ # Read in PCBmodE's configuration file. Look for it in the # calling directory, and then where the script is msg.info("Processing PCBmodE's configuration file") paths = [os.path.join(os.getcwdu()), # project dir os.path.join(os.path.dirname(os.path.realpath(__file__)))] # script dir filenames = '' for path in paths: filename = os.path.join(path, cmdline_args.config_file) filenames += " %s \n" % filename if os.path.isfile(filename): config.cfg = utils.dictFromJsonFile(filename) break if config.cfg == {}: msg.error("Couldn't open PCBmodE's configuration file %s. Looked for it here:\n%s" % (cmdline_args.config_file, filenames)) # add stuff config.cfg['name'] = name config.cfg['version'] = version config.cfg['base-dir'] = os.path.join(config.cfg['locations']['boards'], name) config.cfg['digest-digits'] = 10 # Read in the board's configuration data msg.info("Processing board's configuration file") filename = os.path.join(config.cfg['locations']['boards'], config.cfg['name'], config.cfg['name'] + '.json') config.brd = utils.dictFromJsonFile(filename) tmp_dict = config.brd.get('config') if tmp_dict != None: config.brd['config']['units'] = tmp_dict.get('units', 'mm') or 'mm' config.brd['config']['style-layout'] = tmp_dict.get('style-layout', 'default') or 'default' else: config.brd['config'] = {} config.brd['config']['units'] = 'mm' config.brd['config']['style-layout'] = 'default' # Get style file; search for it in the project directory and # where the script it layout_style = config.brd['config']['style-layout'] layout_style_filename = 'layout.json' paths = [os.path.join(config.cfg['base-dir']), # project dir os.path.join(os.path.dirname(os.path.realpath(__file__)))] # script dir filenames = '' for path in paths: filename = os.path.join(path, config.cfg['locations']['styles'], layout_style, layout_style_filename) filenames += " %s \n" % filename if os.path.isfile(filename): config.stl['layout'] = utils.dictFromJsonFile(filename) break if config.stl['layout'] == {}: msg.error("Couldn't find style file %s. Looked for it here:\n%s" % (layout_style_filename, filenames)) #================================= # Path database #================================= filename = os.path.join(config.cfg['locations']['boards'], config.cfg['name'], config.cfg['locations']['build'], 'paths_db.json') # Open database file. If it doesn't exist, leave the database in # ots initial state of {} if os.path.isfile(filename): config.pth = utils.dictFromJsonFile(filename) #================================= # Routing #================================= filename = os.path.join(config.cfg['base-dir'], config.brd['files'].get('routing-json') or config.cfg['name'] + '_routing.json') # Open database file. If it doesn't exist, leave the database in # ots initial state of {} if os.path.isfile(filename): config.rte = utils.dictFromJsonFile(filename) else: config.rte = {} # namespace URLs config.cfg['ns'] = { None : "http://www.w3.org/2000/svg", "dc" : "http://purl.org/dc/elements/1.1/", "cc" : "http://creativecommons.org/ns#", "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "svg" : "http://www.w3.org/2000/svg", "sodipodi" : "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", "inkscape" : "http://www.inkscape.org/namespaces/inkscape", # Namespace URI are strings; they don't need to be URLs. See: # http://en.wikipedia.org/wiki/XML_namespace "pcbmode" : "pcbmode" } config.cfg['namespace'] = config.cfg['ns'] # significant digits to use for floats config.cfg['significant-digits'] = config.cfg.get('significant-digits', 8) # buffer from board outline to display block edge config.cfg['display-frame-buffer'] = config.cfg.get('display_frame_buffer', 1.0) # the style for masks used for copper pours config.cfg['mask-style'] = "fill:#000;stroke:#000;stroke-linejoin:round;stroke-width:%s;" # Sort out distances distances = { "from-pour-to": { "outline": 0.5, "drill": 0.3, "pad": 0.2, "route": 0.25 } } config.brd['distances'] = (config.brd.get('distances') or distances) config.brd['distances']['from-pour-to'] = (config.brd['distances'].get('from-pour-to') or distances['from-pour-to']) dcfg = config.brd['distances']['from-pour-to'] for key in distances['from-pour-to'].keys(): dcfg[key] = (dcfg[key] or distances[key]) # Commandline overrides. These are stored in a temporary dictionary # so that they are not written to the config file when the board's # configuration is dumped, with extraction, for example config.tmp = {} config.tmp['no-layer-index'] = (cmdline_args.no_layer_index or config.brd['config'].get('no-layer-index') or False) config.tmp['no-flashes'] = (cmdline_args.no_flashes or config.brd['config'].get('no-flashes') or False) config.tmp['no-docs'] = (cmdline_args.no_docs or config.brd['config'].get('no-docs') or False) config.tmp['no-drill-index'] = (cmdline_args.no_drill_index or config.brd['config'].get('no-drill-index') or False) # Define Gerber setting from board's config or defaults try: tmp = config.brd['gerber'] except: config.brd['gerber'] = {} gd = config.brd['gerber'] gd['decimals'] = config.brd['gerber'].get('decimals') or 6 gd['digits'] = config.brd['gerber'].get('digits') or 6 gd['steps-per-segment'] = config.brd['gerber'].get('steps-per-segment') or 100 gd['min-segment-length'] = config.brd['gerber'].get('min-segment-length') or 0.05 # Inkscape inverts the 'y' axis for some historical reasons. # This means that we need to invert it as well. This should # be the only place this inversion happens so it's easy to # control if things change. config.cfg['invert-y'] = -1 # Applying a scale factor to a rectanle can look bad if the height # and width are different. For paths, since they are typically # irregular, we apply a scale, but for rectangles and circles we # apply a buffer # Soldemask scales and buffers soldermask_dict = { "path-scale": 1.05, "rect-buffer": 0.05, "circle-buffer": 0.05 } config.brd['soldermask'] = config.brd.get('soldermask') or {} for key in soldermask_dict: value = config.brd['soldermask'].get(key) if value == None: config.brd['soldermask'][key] = soldermask_dict[key] # Solderpaste scale solderpaste_dict = { "path-scale": 0.9, "rect-buffer": -0.1, "circle-buffer": -0.1 } config.brd['solderpaste'] = config.brd.get('solderpaste') or {} for key in solderpaste_dict: value = config.brd['solderpaste'].get(key) if value == None: config.brd['solderpaste'][key] = solderpaste_dict[key] return
def main(): # get PCBmodE version version = utils.get_git_revision() # setup and parse commandline arguments argp = cmdArgSetup(version) cmdline_args = argp.parse_args() # Might support running multiple boards in the future, # for now get the first onw board_name = cmdline_args.boards[0] makeConfig(board_name, version, cmdline_args) # check if build directory exists; if not, create build_dir = os.path.join(config.cfg['base-dir'], config.cfg['locations']['build']) utils.create_dir(build_dir) # renumber refdefs and dump board config file if cmdline_args.renumber is not False: msg.info("Renumbering refdefs") if cmdline_args.renumber is None: order = 'top-to-bottom' else: order = cmdline_args.renumber.lower() utils.renumberRefdefs(order) # Extract routing from input SVG file elif cmdline_args.extract is True: extract.extract() # Create a BoM elif cmdline_args.make_bom is not False: bom.make_bom(cmdline_args.make_bom) else: # make the board if cmdline_args.make is True: msg.info("Creating board") board = Board() # Create production files (Gerbers, Excellon, etc.) if cmdline_args.fab is not False: if cmdline_args.fab is None: manufacturer = 'default' else: manufacturer = cmdline_args.fab.lower() msg.info("Creating Gerbers") gerber.gerberise(manufacturer) msg.info("Creating excellon drill file") excellon.makeExcellon(manufacturer) if cmdline_args.pngs is True: msg.info("Creating PNGs") utils.makePngs() filename = os.path.join(config.cfg['locations']['boards'], config.cfg['name'], config.cfg['locations']['build'], 'paths_db.json') try: f = open(filename, 'wb') except IOError as e: print "I/O error({0}): {1}".format(e.errno, e.strerror) f.write(json.dumps(config.pth, sort_keys=True, indent=2)) f.close() msg.info("Done!")
def _run(start, end, quiet): start_stash = 'stash@{{{}}}'.format(start) for i in range(start, end): stash_sha = subprocess.check_output(['git', 'rev-parse', start_stash]).splitlines()[0] subprocess.call(['git', 'stash', 'drop', '--quiet', start_stash]) messages.info('Dropped refs/stash@{{{}}} ({})'.format(i, stash_sha), quiet)
def _dry_run(start, end): for i in range(start, end): stash = 'stash@{{{}}}'.format(i) stash_sha = subprocess.check_output(['git', 'rev-parse', stash]).splitlines()[0] messages.info('Would drop refs/{} ({})'.format(stash, stash_sha))
def makeConfig(name, version, cmdline_args): """ """ # Read in PCBmodE's configuration file. Look for it in the # calling directory, and then where the script is msg.info("Processing PCBmodE's configuration file") paths = [os.path.join(os.getcwdu()), # project dir os.path.join(os.path.dirname(os.path.realpath(__file__)))] # script dir filenames = '' for path in paths: filename = os.path.join(path, cmdline_args.config_file) filenames += " %s \n" % filename if os.path.isfile(filename): config.cfg = utils.dictFromJsonFile(filename) break if config.cfg == {}: msg.error("Couldn't open PCBmodE's configuration file %s. Looked for it here:\n%s" % (cmdline_args.config_file, filenames)) # add stuff config.cfg['name'] = name config.cfg['version'] = version config.cfg['base-dir'] = os.path.join(config.cfg['locations']['boards'], name) config.cfg['digest-digits'] = 10 # Read in the board's configuration data msg.info("Processing board's configuration file") filename = os.path.join(config.cfg['locations']['boards'], config.cfg['name'], config.cfg['name'] + '.json') config.brd = utils.dictFromJsonFile(filename) tmp_dict = config.brd.get('config') if tmp_dict != None: config.brd['config']['units'] = tmp_dict.get('units', 'mm') or 'mm' config.brd['config']['style-layout'] = tmp_dict.get('style-layout', 'default') or 'default' else: config.brd['config'] = {} config.brd['config']['units'] = 'mm' config.brd['config']['style-layout'] = 'default' #================================= # Style #================================= # Get style file; search for it in the project directory and # where the script it layout_style = config.brd['config']['style-layout'] layout_style_filename = 'layout.json' paths = [os.path.join(config.cfg['base-dir']), # project dir os.path.join(os.path.dirname(os.path.realpath(__file__)))] # script dir filenames = '' for path in paths: filename = os.path.join(path, config.cfg['locations']['styles'], layout_style, layout_style_filename) filenames += " %s \n" % filename if os.path.isfile(filename): config.stl['layout'] = utils.dictFromJsonFile(filename) break if config.stl['layout'] == {}: msg.error("Couldn't find style file %s. Looked for it here:\n%s" % (layout_style_filename, filenames)) #------------------------------------------------------------- # Stackup #------------------------------------------------------------- try: stackup_filename = config.brd['stackup']['name'] + '.json' except: stackup_filename = 'two-layer.json' paths = [os.path.join(config.cfg['base-dir']), # project dir os.path.join(os.path.dirname(os.path.realpath(__file__)))] # script dir filenames = '' for path in paths: filename = os.path.join(path, config.cfg['locations']['stackups'], stackup_filename) filenames += " %s \n" % filename if os.path.isfile(filename): config.stk = utils.dictFromJsonFile(filename) break if config.stk == {}: msg.error("Couldn't find stackup file %s. Looked for it here:\n%s" % (stackup_filename, filenames)) config.stk['layers-dict'], config.stk['layer-names'] = utils.getLayerList() config.stk['surface-layers'] = [config.stk['layers-dict'][0], config.stk['layers-dict'][-1]] config.stk['internal-layers'] = config.stk['layers-dict'][1:-1] config.stk['surface-layer-names'] = [config.stk['layer-names'][0], config.stk['layer-names'][-1]] config.stk['internal-layer-names'] = config.stk['layer-names'][1:-1] #--------------------------------------------------------------- # Path database #--------------------------------------------------------------- filename = os.path.join(config.cfg['locations']['boards'], config.cfg['name'], config.cfg['locations']['build'], 'paths_db.json') # Open database file. If it doesn't exist, leave the database in # ots initial state of {} if os.path.isfile(filename): config.pth = utils.dictFromJsonFile(filename) #---------------------------------------------------------------- # Routing #---------------------------------------------------------------- filename = os.path.join(config.cfg['base-dir'], config.brd['files'].get('routing-json') or config.cfg['name'] + '_routing.json') # Open database file. If it doesn't exist, leave the database in # ots initial state of {} if os.path.isfile(filename): config.rte = utils.dictFromJsonFile(filename) else: config.rte = {} # namespace URLs config.cfg['ns'] = { None : "http://www.w3.org/2000/svg", "dc" : "http://purl.org/dc/elements/1.1/", "cc" : "http://creativecommons.org/ns#", "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "svg" : "http://www.w3.org/2000/svg", "sodipodi" : "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", "inkscape" : "http://www.inkscape.org/namespaces/inkscape", # Namespace URI are strings; they don't need to be URLs. See: # http://en.wikipedia.org/wiki/XML_namespace "pcbmode" : "pcbmode" } config.cfg['namespace'] = config.cfg['ns'] # significant digits to use for floats config.cfg['significant-digits'] = config.cfg.get('significant-digits', 8) # buffer from board outline to display block edge config.cfg['display-frame-buffer'] = config.cfg.get('display_frame_buffer', 1.0) # the style for masks used for copper pours config.cfg['mask-style'] = "fill:#000;stroke:#000;stroke-linejoin:round;stroke-width:%s;" #------------------------------------------------------------------ # Distances #------------------------------------------------------------------ # If any of the distance definitions are missing from the board's # configuration file, use PCBmodE's defaults #------------------------------------------------------------------ config_distances_dict = config.cfg['distances'] try: board_distances_dict = config.brd.get('distances') except: board_distances_dict = {} distance_keys = ['from-pour-to', 'soldermask', 'solderpaste'] for dk in distance_keys: config_dict = config_distances_dict[dk] try: board_dict = board_distances_dict[dk] except: board_distances_dict[dk] = {} board_dict = board_distances_dict[dk] for k in config_dict.keys(): board_dict[k] = (board_dict.get(k) or config_dict[k]) #----------------------------------------------------------------- # Commandline overrides #----------------------------------------------------------------- # These are stored in a temporary dictionary so that they are not # written to the config file when the board's configuration is # dumped, with extraction, for example #----------------------------------------------------------------- config.tmp = {} config.tmp['no-layer-index'] = (cmdline_args.no_layer_index or config.brd['config'].get('no-layer-index') or False) config.tmp['no-flashes'] = (cmdline_args.no_flashes or config.brd['config'].get('no-flashes') or False) config.tmp['no-docs'] = (cmdline_args.no_docs or config.brd['config'].get('no-docs') or False) config.tmp['no-drill-index'] = (cmdline_args.no_drill_index or config.brd['config'].get('no-drill-index') or False) # Define Gerber setting from board's config or defaults try: tmp = config.brd['gerber'] except: config.brd['gerber'] = {} gd = config.brd['gerber'] gd['decimals'] = config.brd['gerber'].get('decimals') or 6 gd['digits'] = config.brd['gerber'].get('digits') or 6 gd['steps-per-segment'] = config.brd['gerber'].get('steps-per-segment') or 100 gd['min-segment-length'] = config.brd['gerber'].get('min-segment-length') or 0.05 # Inkscape inverts the 'y' axis for some historical reasons. # This means that we need to invert it as well. This should # be the only place this inversion happens so it's easy to # control if things change. config.cfg['invert-y'] = -1 return
def state(**kwargs): """Print the state of the working tree. :keyword str show_color: color when (always, never, or auto) :keyword str format: format for output (compact or pretty) :keyword bool show_status: show status :keyword list ignore_extensions: extensions to hide even if the configuration is to show :keyword list show_extensions: extensions to show even if the configuration is to hide :keyword dict options: dictionary of extension to option list :keyword bool show_empty: show empty sections :keyword list order: order to print sections in :keyword bool clear: clear terminal before printing :keyword bool page: page output if too long """ if not directories.is_git_repository(): messages.error('{0!r} not a git repository'.format(os.getcwd())) show_color = git.resolve_coloring(kwargs.get('show_color').lower()) colorama.init(strip=(show_color == 'never')) kwargs['show_color'] = show_color kwargs['show_clean_message'] = settings.get( 'git-state.status.show-clean-message', default=True, as_type=parse_string.as_bool ) format_ = kwargs.get('format_') sections = OrderedDict() if git.is_empty_repository(): if kwargs.get('show_status'): status_output = status.get(new_repository=True, **kwargs) status_title = status.title() status_accent = status.accent(new_repository=True, **kwargs) sections[status_title] = _print_section(status_title, status_accent, status_output, format_, color=show_color) else: if kwargs.get('show_status'): status_output = status.get(**kwargs) status_title = status.title() status_accent = status.accent(show_color=show_color) # TODO: remove restriction that status is always shown sections[status_title] = _print_section(status_title, status_accent, status_output, format_, show_empty=True, color=show_color) # show any user defined sections extensions = settings.list_( section='git-state.extensions', config=None, count=False, keys=True, format_=None, file_=None ).splitlines() extensions = list(set(extensions) - set(kwargs.get('ignore_extensions'))) show_extensions = kwargs.get('show_extensions', []) options = kwargs.get('options') for extension in extensions or []: # skip if we should ignore this extension if extension not in show_extensions and not settings.get('git-state.extensions.' + extension + '.show', default=True, as_type=parse_string.as_bool): continue extension_command = settings.get('git-state.extensions.' + extension) extension_name = settings.get('git-state.extensions.' + extension + '.name', default=extension) # merge config and command line options extension_options = settings.get( 'git-state.extensions.' + extension + '.options', default=[], as_type=(lambda value: [value]) # pragma: no cover since this call is mocked and the lambda never fires ) extension_options += options[extension_name] if extension_name in options else [] extension_options = [o for sub in [shlex.split(line) for line in extension_options] for o in sub] extension_command = shlex.split(extension_command) + extension_options if settings.get('git-state.extensions.' + extension + '.color', default=True, as_type=parse_string.as_bool): extension_command += ['--color={}'.format(show_color)] extension_proc = subprocess.Popen(extension_command, stdout=PIPE, stderr=PIPE) extension_out, extension_error = extension_proc.communicate() sections[extension_name] = _print_section( title=extension_name, text=extension_out if not extension_proc.returncode else extension_error, format_=format_, show_empty=kwargs.get('show_empty'), color=show_color ) state_result = '' # print sections with a predefined order order = kwargs.get('order', settings.get('git-state.order', default=[], as_type=parse_string.as_delimited_list('|'))) for section in order: if section in sections: state_result += sections.pop(section) # print any remaining sections in the order they were defined for section_info in sections: state_result += sections[section_info] if state_result: state_result = state_result[:-1] # strip the extra trailing newline state_lines = len(state_result.splitlines()) terminal_lines = literal_eval(subprocess.check_output(['tput', 'lines'])) if not kwargs.get('page', True) or terminal_lines >= state_lines + 2: # one for the newline and one for the prompt if kwargs.get('clear') and sys.stdout.isatty(): subprocess.call('clear') messages.info(state_result) else: echo = subprocess.Popen(['echo', state_result], stdout=PIPE) subprocess.call(['less', '-r'], stdin=echo.stdout) echo.wait()