Hi,

looking at the log produced by running yast with Y2DEBUG set to 1 I
realized that it contained enough information to write a debugger that
parses it.

I saw the announcement of the ycp debugger and must admit that I
haven't looked at it. But working with the recorded trace allows some
fancy stuff like walking backwards and it's written in python so it is
easily hackable.

Well, here it is, it served me well and it might be useful for someone
else. Feel free to put it in the repository if you like.

Justus

~~~ snip ~~~

$ Y2DEBUG=1 /usr/lib/YaST2/bin/y2base -l - -c 
/usr/share/YaST2/data/testsuite/log.conf -I ../src -M ../src 
tests/Network_YaPI.ycp UI 2> log
$ ycppmd.py log
[...]
pmd> display Devs OriginalDevs
     1: 2011-08-30 10:54:26 <0> opensuse(21525) [libycp] 
pathsearch.cc(getPaths):68 getPaths /y2update /root/.yast2 /usr/share/YaST2

          Devs = UNDEFINED
  OriginalDevs = UNDEFINED
[...]
pmd> goto 38176
 38176: 2011-08-30 10:54:29 <0> opensuse(21525) [liby2] 
SymbolEntry.cc(setValue):107 SymbolEntry::setValue (Devs@0x1507900 = 
'$["eth":$["eth0":$["BOOTPROTO":"dhcp4", "STARTMODE":""], 
"eth1":$["BOOTPROTO":"static", "IPADDR":"1.2.3.4", "MTU":"1234", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth2":$["BOOTPROTO":"static", "IPADDR":"1.2.3.5", "NETMASK":"255.255.255.0", 
"PREFIXLEN":"24", "STARTMODE":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7/24", "STARTMODE":"auto"], "eth4":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":""], "eth5":$["BOOTPROTO":"static", "STARTMODE":""]], 
"vlan":$["eth5.23":$["BOOTPROTO":"static", "IPADDR":"1.2.3.8/24", 
"STARTMODE":"auto"]]]')

          Devs = $["eth":$["eth0":$["BOOTPROTO":"dhcp4", "STARTMODE":""], 
"eth1":$["BOOTPROTO":"static", "IPADDR":"1.2.3.4", "MTU":"1234", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth2":$["BOOTPROTO":"static", "IPADDR":"1.2.3.5", "NETMASK":"255.255.255.0", 
"PREFIXLEN":"24", "STARTMODE":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7/24", "STARTMODE":"auto"], "eth4":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":""], "eth5":$["BOOTPROTO":"static", "STARTMODE":""]], 
"vlan":$["eth5.23":$["BOOTPROTO":"static", "IPADDR":"1.2.3.8/24", 
"STARTMODE":"auto"]]]
  OriginalDevs = UNDEFINED
[...]
pmd> bt full
  0 - [main]
{'READ': '$["init":$["scripts":$["exists":false]], 
"network":$["section":$["eth0":$[], "eth1":$[], "eth2":$[], "eth3":$[], 
"eth4":$[], "eth5":$[], "eth5.23":$[]], 
"value":$["eth0":$["BOOTPROTO":"dhcp4"], "eth1":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.4/24", "MTU":"1234"], "eth2":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.5/24", "PREFIXLEN":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.6", "PREFIXLEN":"24"], "eth4":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7", "NETMASK":"255.255.255.0"], "eth5":$["BOOTPROTO":"static"], 
"eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", "IPADDR":"1.2.3.8/24", 
"VLAN_ID":"23"]]], "routes":[$["destination":"default", 
"gateway":"10.20.30.40"]], 
"sysconfig":$["network":$["config":$["NETCONFIG_DNS_STATIC_SEARCHLIST":"suse.cz 
suse.de", "NETCONFIG_DNS_STATIC_SERVERS":"208.67.222.222 208.67.220.220"], 
"dhcp":$["DHCLIENT_SET_HOSTNAME":"yes", "WRITE_HOSTNAME_TO_HOSTS":"no"]]], 
"target":$["bash_output":"laptop.suse.cz", "size":27, 
"string":"laptop.suse.cz"]]', 'dummy_log_string': '"LOGTHIS_SECRET_314 "', 
'EXEC': '$["target":$["bash_output":$["exit":0, "stdout":"laptop.suse.cz"]]]'}
  1 - TEST
{'FUNCTION': 'YaPI::NETWORK::Write 
($["interface":$["eth5.23":$["bootproto":"static", "ipaddr":"1.2.3.8/24", 
"vlan_etherdevice":"eth5", "vlan_id":"42"]]])', 'INPUT': 
'[$["init":$["scripts":$["exists":false]], "network":$["section":$["eth0":$[], 
"eth1":$[], "eth2":$[], "eth3":$[], "eth4":$[], "eth5":$[], "eth5.23":$[]], 
"value":$["eth0":$["BOOTPROTO":"dhcp4"], "eth1":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.4/24", "MTU":"1234"], "eth2":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.5/24", "PREFIXLEN":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.6", "PREFIXLEN":"24"], "eth4":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7", "NETMASK":"255.255.255.0"], "eth5":$["BOOTPROTO":"static"], 
"eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", "IPADDR":"1.2.3.8/24", 
"VLAN_ID":"23"]]], "routes":[$["destination":"default", 
"gateway":"10.20.30.40"]], 
"sysconfig":$["network":$["config":$["NETCONFIG_DNS_STATIC_SEARCHLIST":"suse.cz 
suse.de", "NETCONFIG_DNS_STATIC_SERVERS":"208.67.222.222 208.67.220.220"], 
"dhcp":$["DHCLIENT_SET_HOSTNAME":"yes", "WRITE_HOSTNAME_TO_HOSTS":"no"]]], 
"target":$["bash_output":"laptop.suse.cz", "size":27, 
"string":"laptop.suse.cz"]]]', 'DEFAULT': 'nil'}
  2 - Test
{'FUNCTION': 'YaPI::NETWORK::Write 
($["interface":$["eth5.23":$["bootproto":"static", "ipaddr":"1.2.3.8/24", 
"vlan_etherdevice":"eth5", "vlan_id":"42"]]])', 'INPUT': 
'[$["init":$["scripts":$["exists":false]], "network":$["section":$["eth0":$[], 
"eth1":$[], "eth2":$[], "eth3":$[], "eth4":$[], "eth5":$[], "eth5.23":$[]], 
"value":$["eth0":$["BOOTPROTO":"dhcp4"], "eth1":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.4/24", "MTU":"1234"], "eth2":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.5/24", "PREFIXLEN":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.6", "PREFIXLEN":"24"], "eth4":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7", "NETMASK":"255.255.255.0"], "eth5":$["BOOTPROTO":"static"], 
"eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", "IPADDR":"1.2.3.8/24", 
"VLAN_ID":"23"]]], "routes":[$["destination":"default", 
"gateway":"10.20.30.40"]], 
"sysconfig":$["network":$["config":$["NETCONFIG_DNS_STATIC_SEARCHLIST":"suse.cz 
suse.de", "NETCONFIG_DNS_STATIC_SERVERS":"208.67.222.222 208.67.220.220"], 
"dhcp":$["DHCLIENT_SET_HOSTNAME":"yes", "WRITE_HOSTNAME_TO_HOSTS":"no"]]], 
"target":$["bash_output":"laptop.suse.cz", "size":27, 
"string":"laptop.suse.cz"]]]', 'real_ret': '$["error":"", "exit":"0"]', 
'DEFAULT': 'nil'}
  3 - Write
{'Current': '$[]', 'Name': '""', 'OriginalDevs': 
'$["eth":$["eth0":$["BOOTPROTO":"dhcp4", "STARTMODE":""], 
"eth1":$["BOOTPROTO":"static", "IPADDR":"1.2.3.4", "MTU":"1234", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth2":$["BOOTPROTO":"static", "IPADDR":"1.2.3.5", "NETMASK":"255.255.255.0", 
"PREFIXLEN":"24", "STARTMODE":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.6", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":""], "eth4":$["BOOTPROTO":"static", "IPADDR":"1.2.3.7", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth5":$["BOOTPROTO":"static", "STARTMODE":""]], 
"vlan":$["eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", 
"IPADDR":"1.2.3.8", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":"", "VLAN_ID":"23"]]]', 'devregex': '""', 'chmod': '[]', 'Devs': 
'$["eth":$["eth0":$["BOOTPROTO":"dhcp4", "STARTMODE":""], 
"eth1":$["BOOTPROTO":"static", "IPADDR":"1.2.3.4", "MTU":"1234", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth2":$["BOOTPROTO":"static", "IPADDR":"1.2.3.5", "NETMASK":"255.255.255.0", 
"PREFIXLEN":"24", "STARTMODE":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7/24", "STARTMODE":"auto"], "eth4":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.7", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":""], "eth5":$["BOOTPROTO":"static", "STARTMODE":""]], 
"vlan":$["eth5.23":$["BOOTPROTO":"static", "IPADDR":"1.2.3.8/24", 
"STARTMODE":"auto"]]]', 'netmask': '"24"', 'has_key': 'false', 'file': 
'"/etc/sysconfig/network/ifcfg-eth5.23"', 'operation': 'nil', 'typ': '"vlan"', 
'nm': 
'"^((128|192|224|240|248|252|254|255).0.0.0|255.(128|192|224|240|248|252|254|255).0.0|255.255.(128|192|224|240|248|252|254|255).0|255.255.255.(128|192|224|240|248|252|254|255))$"',
 'devsmap': '$["eth5.23":$["BOOTPROTO":"static", "IPADDR":"1.2.3.8/24", 
"STARTMODE":"auto"]]', 'name': '"eth5.23"', 'service': '"network"', 's1': 
'"(128|192|224|240|248|252|254|255)"', 'devmap': '$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.8/24", "STARTMODE":"auto"]', 'p': 
'".network.value.\\"eth5.23\\"."', 'config': '"eth5.23"', 'k': 
'"WIRELESS_KEY_3"'}
  4 - ConcealSecrets
{'devs': '$["eth":$["eth0":$["BOOTPROTO":"dhcp4", "STARTMODE":""], 
"eth1":$["BOOTPROTO":"static", "IPADDR":"1.2.3.4", "MTU":"1234", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth2":$["BOOTPROTO":"static", "IPADDR":"1.2.3.5", "NETMASK":"255.255.255.0", 
"PREFIXLEN":"24", "STARTMODE":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.6", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":""], "eth4":$["BOOTPROTO":"static", "IPADDR":"1.2.3.7", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth5":$["BOOTPROTO":"static", "STARTMODE":""]], 
"vlan":$["eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", 
"IPADDR":"1.2.3.8", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":"", "VLAN_ID":"23"]]]', 'tout': 
'$["eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", "IPADDR":"1.2.3.8", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":"", "VLAN_ID":"23"]]', 
'tdevs': '$["eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", 
"IPADDR":"1.2.3.8", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":"", "VLAN_ID":"23"]]', 'out': 
'$["eth":$["eth0":$["BOOTPROTO":"dhcp4", "STARTMODE":""], 
"eth1":$["BOOTPROTO":"static", "IPADDR":"1.2.3.4", "MTU":"1234", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth2":$["BOOTPROTO":"static", "IPADDR":"1.2.3.5", "NETMASK":"255.255.255.0", 
"PREFIXLEN":"24", "STARTMODE":""], "eth3":$["BOOTPROTO":"static", 
"IPADDR":"1.2.3.6", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":""], "eth4":$["BOOTPROTO":"static", "IPADDR":"1.2.3.7", 
"NETMASK":"255.255.255.0", "PREFIXLEN":"24", "STARTMODE":""], 
"eth5":$["BOOTPROTO":"static", "STARTMODE":""]], 
"vlan":$["eth5.23":$["BOOTPROTO":"static", "ETHERDEVICE":"eth5", 
"IPADDR":"1.2.3.8", "NETMASK":"255.255.255.0", "PREFIXLEN":"24", 
"STARTMODE":"", "VLAN_ID":"23"]]]', 'ifcfg': '$["BOOTPROTO":"static", 
"ETHERDEVICE":"eth5", "IPADDR":"1.2.3.8", "NETMASK":"255.255.255.0", 
"PREFIXLEN":"24", "STARTMODE":"", "VLAN_ID":"23"]', 'id': '"eth5.23"', 't': 
'"vlan"'}
 38297: 2011-08-30 10:54:29 <0> opensuse(21525) [libycp] 
ExecutionEnvironment.cc(pushframe):105 Push frame ConcealSecrets1
-- 
Justus Winter                               [email protected]

PRESENSE Technologies GmbH            Sachsenstr. 5, D-20097 HH
                                         USt-IdNr.: DE263765024
Geschäftsführer/Managing Directors       AG Hamburg, HRB 107844
Till Dörges           Jürgen Sander              Axel Theilmann

#!/usr/bin/env python

# Copyright (c) 2011 Justus Winter (PRESENSE Technologies GmbH) <[email protected]>
# 
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import sys
import os
import re
import cmd
import atexit
import readline
import functools

class StackFrame(object):
    def __init__(self, parent, name, handle, lineno = 0, position = 0):
        self.parent = parent
        self.name = name
        self.handle = handle
        self.lineno = lineno
        self.position = position
        self.lines = []
        self.variables = {}

    def handle_push_frame(self, name = None):
        frame = StackFrame(self, name, self.handle, self.lineno, len(self.lines))
        self.lineno = frame.parse()
        return frame, True

    def handle_pop_frame(self, address = None):
        return None, False

    def handle_set_value(self, name = None, address = None, value = None):
        self.variables = self.variables.copy()
        self.variables[name] = value
        return None, True

    line_re = re.compile(r'''.*\[(?P<module>[^\]]*)\] (?P<c_source_file>.*)\((?P<c_function>.*)\):(?P<c_lineno>\d*) (?P<body>.*)''')
    
    handlers = {
        re.compile(r'''Push frame (?P<name>.*)'''): handle_push_frame,
        re.compile(r'''Pop frame (?P<address>.*)'''): handle_pop_frame,
        re.compile(r'''SymbolEntry::setValue \((?P<name>.*)@(?P<address>.*) = '(?P<value>.*)'\)'''): handle_set_value,
    }

    def parse(self):
        keep_going = True
        for line in self.handle:
            line = line.strip()
            self.lineno += 1
            lineno = self.lineno

            line_match = self.line_re.match(line)

            result = None
            if line_match:
                for handler_re, handler in self.handlers.items():
                    match = handler_re.match(line_match.group('body'))

                    if match:
                        result, keep_going = handler(self, **match.groupdict())
                        break

            self.lines.append((lineno, line, self.variables, result, line_match))

            if not keep_going:
                break

        return self.lineno

    def get_stack_trace(self):
        result = []
        
        pointer = self
        while pointer:
            result.insert(0, pointer)
            pointer = pointer.parent
            
        return result

def int_arg(fun):
    @functools.wraps(fun)
    def wrapper(self, args):
        if args:
            try:
                n = int(args)
            except ValueError as e:
                print 'Not a valid integer: %s' % args
            else:
                return fun(self, n)
        else:
            return fun(self)
    return wrapper

class PostMortemDebugger(cmd.Cmd):
    intro = '''
Welcome to the interactive post mortem ycp debugger!
'''
    prompt = 'pmd> '

    def __init__(self, trace, *args, **kwargs):
        cmd.Cmd.__init__(self, *args, **kwargs)
        self.trace = trace
        self.current_stack_frame = trace
        self.current_position = 0
        self.last_search = None
        self.display_variables = set()

    @property
    def current_line(self):
        return self.current_stack_frame.lines[self.current_position]

    def dump_variables(self, names):
        name_width = max(len(name) for name in names)
        for name in sorted(names):
            print '  %s = %s' % (name.rjust(name_width), self.current_line[2].get(name, 'UNDEFINED'))

    def postcmd(self, stop, line):
        print '% 6i: %s' % self.current_line[0:2]

        if self.display_variables:
            print
            self.dump_variables(self.display_variables)
            print

        return stop

    def do_EOF(self, args):
        print 'Bye!'
        return True

    def do_bt(self, args):
        'Displays a stack trace'
        for i, frame in enumerate(self.current_stack_frame.get_stack_trace()):
            print '% 3i - %s' % (i, frame.name)
            if args == 'full':
                print '%r' % frame.variables

    def do_up(self, args):
        'Return to the previous stack frame'
        try:
            self.up()
        except StopIteration as e:
            print 'Cannot go further up'

    @int_arg
    def do_next(self, n = 1):
        '''next [n]

Advance one [n] line[s] in the log treating function calls as one instruction'''
        try:
            for x in range(n):
                self.next()
        except StopIteration as e:
            print 'Reached end of trace'

    @int_arg
    def do_skip(self, n = 1):
        '''skip [n]

Advance one [n] line[s] in the log treating function calls as one instruction
ignoring any lines that do not match the line regexp (usually script code)'''
        try:
            for x in range(n):
                self.next()
                while not self.current_line[4]:
                    self.next()
        except StopIteration as e:
            print 'Reached end of trace'

    @int_arg
    def do_prev(self, n = 1):
        '''prev [n]

Move back [n] line[s] in the log treating function calls as one instruction'''
        try:
            for x in range(n):
                self.prev()
        except StopIteration as e:
            print 'Reached beginning of trace'

    @int_arg
    def do_step(self, n = 1):
        '''step [n]

Advance one [n] line[s] in the log descending into functions'''
        try:
            for x in range(n):
                self.step()
        except StopIteration as e:
            print 'Reached end of trace'

    def up(self):
        if self.current_stack_frame.parent:
            self.current_position = self.current_stack_frame.position + 1
            self.current_stack_frame = self.current_stack_frame.parent
        else:
            raise StopIteration()

    def step(self):
        if self.current_line[3]:
            self.current_stack_frame = self.current_line[3]
            self.current_position = 0
        else:
            self.next()

    def next(self):
        self.current_position += 1
        if self.current_position >= len(self.current_stack_frame.lines):
            self.up()

    def prev(self):
        self.current_position -= 1
        if self.current_position < 0:
            self.up()

    def do_reset(self, args):
        'Rewind to the beginning of the trace'
        self.current_position = 0
        self.current_stack_frame = self.trace

    def do_search(self, args, advance = None):
        '''[r]search

Search for the given regular expression from the current position. Rerun
without argument to repeat the last search.'''
        if args:
            try:
                self.last_search = re.compile(args)
            except re.error as e:
                print 'Invalid regular expression: %s' % e
                return
        elif not self.last_search:
            print 'No search pattern supplied'
            return

        advance = advance or self.step
        current = self.current_stack_frame, self.current_position
        try:
            advance()
            while not self.last_search.search(self.current_line[1]):
                sys.stdout.write('\r%i' % self.current_line[0])
                advance()
        except StopIteration as e:
            self.current_stack_frame, self.current_position = current
            print '\rPattern not found'
        else:
            sys.stdout.write('\r')

    @functools.wraps(do_search)
    def do_rsearch(self, args):
        self.do_search(args, advance = self.prev)

    @int_arg
    def do_goto(self, n = None):
        '''goto n

go to line n'''
        if n == None:
            print 'I need a line number to go to'
        else:
            try:
                while self.current_line[0] != n:
                    if self.current_line[0] > n:
                        self.prev()
                    else:
                        self.step()
            except StopIteration as e:
                print 'Reached %s trace' % ('beginning' if self.current_line[0] == 0 else 'end')

    def do_print(self, args):
        '''print <var> [<var> ...]

Print the value[s] of the given variable name[s]'''
        self.dump_variables(args.split())

    def complete_print(self, text, line, begidx, endidx):
        return [name for name in self.current_line[2].keys() if name.startswith(text)]

    def do_display(self, args):
        '''display <var>[-] [<var>[-] ...]

Add [remove] the given variable name[s] to the list of variables to display
after each command.'''
        for name in args.split():
            if name.endswith('-'):
                self.display_variables.discard(name[:-1])
            else:
                self.display_variables.add(name)

    complete_display = complete_print
    
def main(options, args):
    main = StackFrame(None, '[main]', open(args[0]))
    main.parse()
    debugger = PostMortemDebugger(main)
    debugger.cmdloop()

if __name__ == '__main__':
    histfile = os.environ.get('YCPPMDHISTORY',
                              os.path.join(os.environ.get('XDG_CONFIG_HOME',
                                                          os.path.join(os.environ['HOME'], '.config')),
                                           'ycppmd', 'history'))

    if not os.path.isdir(os.path.dirname(histfile)):
        os.mkdir(os.path.dirname(histfile))
        
    try:
        readline.read_history_file(histfile)
    except IOError as e:
        pass
    atexit.register(readline.write_history_file, histfile)

    main(None, sys.argv[1:])

Attachment: signature.asc
Description: PGP signature

Reply via email to