#! /usr/bin/env python

""" \
 This program serves for editing of objects stored in Oracle database.
 You need functional sqlplus binary in order for oraedit to work.
"""

# Copyright (c) 1998,1999,2000,2004 by Jaromir Dolecek <dolecek@ics.muni.cz>
#  Institute of Computer Science at Masaryk University, Brno, Czech Rep.
#	All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its 
# documentation for any purpose and without fee is hereby granted, 
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in 
# supporting documentation, and that the name of Jaromir Dolecek
# not be used in advertising or publicity pertaining to distribution 
# of the software without specific, written prior permission.
#
# JAROMIR DOLECEK DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT
# SHALL JAROMIR DOLECEK BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSSOF 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.

# $Id: oraedit,v 1.37 2004/10/02 08:56:03 dolecek Exp $

# version of program
version = "0.1.4"

# sqlplus executable
sqlplus = "sqlplus"

# timeout to wait for remote sqlplus to start coming up, in seconds
sqlplus_rcmd_timeout = 20

import os, sys, popen2, signal, errno
from fcntl import fcntl, F_SETFL, F_SETFD
import string, re, tempfile, getopt, time

# local exceptions
OE_Error = "OraEditException"
OE_Deploy = "OraEditDeployException"
OE_Locked = "OraEditLockException"

# class implementing connection to database
class OraConnect:
	def __init__(self, connect_str, rcmd=None):
		if (rcmd): cmd = rcmd
		else: cmd = sqlplus + ' ' + connect_str
		ein, eout, eerr = popen2.popen3(cmd)
		# set read pipe to NONBLOCK, so we won't block
		# on read() when there aren't any data available at the time
		fcntl(ein.fileno(), F_SETFL, os.O_NONBLOCK)
		fcntl(eerr.fileno(), F_SETFL, os.O_NONBLOCK)

		# for child -- close above opened file descriptors on exec()
		fcntl(ein.fileno(), F_SETFD, 1);
		fcntl(eout.fileno(), F_SETFD, 1);
		fcntl(eerr.fileno(), F_SETFD, 1);

		self.__sp = { 'fromchild' : ein, 'tochild' : eout, 'err' : eerr}
		self.__connect_str	= connect_str
		self.__sqlprompt	= 'ORAEDIT> '

		# consume all input up to SQL> prompt and inform/bail out
		# when an error occurs
		headinfo = ''
		errors = ''
		sqlplus_start = time.time()
		global sqlplus_rcmd_timeout
		while 1:
			try: headinfo = headinfo + ein.read()
			except IOError, (errnum, errmsg):
				if (errnum != errno.EAGAIN): raise
			if (headinfo[-5:] == 'SQL> '):
				break;
			errpos = string.find(headinfo, 'ERROR')
			if (errpos >= 0):
				# close pipe to subprocess, set pipe back
				# to blocking and suck up anything else
				# Sql*Plus writes
				self.__sp['tochild'].close()
				fcntl(self.__sp['fromchild'].fileno(),F_SETFL,0)
				headinfo = headinfo + ein.read()
				# trim the read text so only the errors message
				# remains
				cutpos = string.find(headinfo, 'Enter ')
				if (cutpos < 0): cutpos = len(headinfo)
				headinfo = string.strip(headinfo[errpos:cutpos])
				raise OE_Error, 'OraConnect: connecting as "' +\
					connect_str + '" unsuccesfull - ' + \
					headinfo + '\n	' + \
					'Failed command was: "%s"' % cmd

			# read error messages - if we won't manage to start
			# the sqlplus remotely in reasonable amount of time,
			# we will write it to user
			try:
				errors = errors + eerr.read()
			except IOError, (errnum, errmsg):
				if (errnum != errno.EAGAIN): raise

			# we must ensure remote login was successful --
			# if more then sqlplus_rcmd_timeout passes from
			# point when we started sqlplus and it still
			# didn't write it's startup banner, assume there
			# is some error and bail out
			if (rcmd and
			    time.time() - sqlplus_start > sqlplus_rcmd_timeout
			    and string.find(headinfo, 'SQL*Plus') < 0):
				if (errors):
				    if (errors[-1:] == '\n'):
						errors = errors[:-1]
				    errors = '\n	' + \
					'Error message was: "%s"' % errors
				raise OE_Error, 'OraConnect: starting ' + \
					sqlplus + ' remotely timed out\n' + \
					'Failed command was: "%s"' % rcmd + \
					errors

		# disable heading and feedback, disable paging, set prompt to
		# self.__sqlprompt
		self.run('set heading off\n' + 'set feedback off\n' +
			 'set pagesize 0\n' + 'set newpage none\n' +
			 'set trimout on\n' + 'set linesize 32767\n' +
			 'set long 2000000000\n' +
			 'set longchunksize 2000000000\n' +
			 'set define off\n' + # switch off &foo handling
			 "set sqlprompt '" + self.__sqlprompt + "'\n")

	def __del__(self):
		# explicitly close the writing pipe, so that suprocess
		# can end gracefully and won't sleep & wait for incoming data
		try:
			self.__sp['fromchild'].close()
			self.__sp['tochild'].close()
			self.__sp['err'].close()
		except AttributeError, errmsg: pass

	# execute the command and return the output - assume all the
	# output is read when we hit the sqlprompt again
	def run(self, input='', info=None):
		# automatically add newline on the end if the input
		# string doesn't end with one
		if (input[-1:] != '\n' and input != ''):
			input = input + '\n'

		output = ''
		sqlprompt_mlen = -len(self.__sqlprompt)

		# we can't write all the input at once and read output --
		# sqlplus writes a line number on output for every line
		# read and with many lines it would fill up it's output pipe
		# buffer pretty fast, which would lead to deadlock (sqlplus
		# would block waiting for other end to read it's output
		# and we would block waiting for sqlplus to read all data
		# we are feeding into it); so take care to only write maximum
		# 100 lines of input to sqlplus at once and read all it's
		# output before next iteration
		l100p = re.compile(r'(^.*$\n){1,100}', re.M)
		while 1:
		    if (input):
			end100 = l100p.match(input).end(0)
			self.__sp['tochild'].write(input[:end100])
			self.__sp['tochild'].flush()
			input = input[end100:]

		    # feedback to user to show something is happening
		    if (info):
			sys.stdout.write('.')
			sys.stdout.flush()

		    # wait for input and suck all what is available
		    somethingread = None
		    while 1:
			    try:
				output = output + self.__sp['fromchild'].read()
				somethingread = 1
			    except IOError, (errnum, errmsg):
				if (errnum != errno.EAGAIN): raise
				if (somethingread): break

		    # if we found prompt, strip it and end looping
		    if (output[sqlprompt_mlen:] == self.__sqlprompt):
			output = output[:sqlprompt_mlen-1]
			break
		if (info):
			sys.stdout.write(' done.')
		return output
# -- end of definition of object OraConnect

# implementation of object editor/viewer
class OraEdit:
	# constructor of OraEdit object
	def __init__(self, connect_str, obj_name, obj_type, usex=None,
		readonly=None, silent=None, rcmd=None):
		tobj_name = string.split(obj_name, '.')
		if len(tobj_name) > 1:
			#owner is the part before dot
			owner = tobj_name[0]
			obj_name = tobj_name[1]
		else:
			# connect string is something like
			# 'login/passwd@connect_to'
			tobj_name = string.split(connect_str, '/')
			if len(tobj_name) > 1:
				owner = tobj_name[0]
		if (not owner):
			raise OE_Error, "can't find out owner of the object"
		# find out machine we are connection on, if present
		# in connect string
		pos = string.find(connect_str, '@')
		if (pos >= 0):
			machine = connect_str[pos+1:]
		else:
			if (os.environ.has_key('TWO_TASK')):
				machine = os.environ['TWO_TASK']
			else:
				machine = 'local'

		owner = string.upper(owner)
		self.__obj_name = obj_name
		self.__obj_type = obj_type
		self.__owner	= owner
		self.__dirty	= None
		self.__modified	= None
		self.__useX	= usex and os.environ.has_key('DISPLAY')
		self.aborted	= None	# if true, exit ASAP
		self.__silent	= silent
		self.__rcmd	= rcmd
		self.__connect_str  = connect_str;
		# be optimistic -- assume we are not using buggy Linux sqlplus
		self.__buggysqlplus = None

		# filter any special characters in filename
		basename = string.translate(repr(self) + ':' + machine,
				string.maketrans(' /@()', '_____'))
		tmpdir = find_val('/tmp', 'TMP', 'TEMP')
		lockfilename =  tmpdir + '/' + basename + '.lock'
		tmpfilename  =  tmpdir + '/' + basename + '.sql'
		
		# make a lock and then verify tmpfile doesn't exists yet
		# don't check for lock if we don't want do actually edit
		# the object
		try:
			if (not readonly):
				# set this _before_ acquiring a lock so that
				# if we succesfully obtain lock and user
				# interrupts us right after the open(2) call,
				# the file would still get unlink(2)ed
				# in the destructor
				self.__lockfilename = lockfilename
				self.__lockfd = os.open(lockfilename,
					os.O_CREAT|os.O_EXCL|os.O_RDWR, 0644)
				f = os.fdopen(self.__lockfd, 'w')
				f.write(str(os.getpid()) + '\n')
				f.close() # closes the underlying descriptor too
		except os.error, (errnum, errmsg):
			self.__lockfilename = None
			# just re-raise the error if it's not EEXIST
			if (errnum <> errno.EEXIST): raise

			# read locker's pid from the lock file
			lckf = open(lockfilename, 'r')
			locktime = time.localtime(os.fstat(lckf.fileno())[9])
			try:
			    pid = string.atoi(lckf.readline())
			    lockmsg = 'process %d' % pid + \
				' edits the same object'
			except ValueError, msg:
			    lockmsg = "couldn't read locker info, "\
				'probably corrupt lock file ?'
			lckf.close()
			raise OE_Locked, lockmsg + '\n	' + \
			  'object locked at ' + \
				time.strftime('%Y/%m/%d %H:%M:%S', locktime) +\
				'\n'+ \
			  '	lock file is "%s"' % lockfilename 

		# check if the file can be opened for reading -- if yes,
		# something else is using it and we don't want to use it
		# create unique temporary filename if we want look
		# at the object source code only
		if (readonly):
			tempfile.template = basename
			tmpfilename = tempfile.mktemp('.view')
		else:
		    try:
			open(tmpfilename, 'r')
			raise OE_Error, \
				'edit file "%s" already exists' % tmpfilename
		    except: pass

		# set this after any checks, so that we won't unlink(2)
		# somebody's else file in __del__()
		self.__tmpfilename  = tmpfilename

		# read object source code
		self.__conn = self.connection()
		if (obj_type != "VIEW"):
			output = self.__conn.run(
			    "SELECT text FROM ALL_SOURCE WHERE "
				+ " name = '" + string.upper(obj_name) + "'"
				+ " AND type = " + "'" + obj_type + "'"
				+ " AND owner = '" + owner + "' ORDER BY line;")
		else:
			output = self.__conn.run(
			    "SELECT text FROM ALL_VIEWS WHERE "
				+ " view_name = '" + string.upper(obj_name)
				+ "';")
			if (output):
				output = 'VIEW ' + obj_name + ' AS ' + output
		newobj = (not output)
		if (newobj):
			if (obj_type != "VIEW"):
				body = 'AS\nBEGIN\nEND ' + obj_name + ';\n'
			else:
				body = ''
			output = obj_type + ' ' + owner + '.' + obj_name \
				+ '\n' + body
		if (output[-1:] != '\n'): output = output + '\n'
		if (not readonly):
			if (obj_type == 'TYPE'):
				if (newobj):
					prefix = 'CREATE '
				else:
					prefix = 'ALTER TYPE ' + obj_name + \
						' REPLACE AS OBJECT '
					patt = r'^\*TYPE\s*' + obj_name + \
						r'\s*AS\*OBJECT'
					declp = re.compile(r'^\s*TYPE\s*' +
						obj_name + r'\s*AS\s*OBJECT',
						re.I)
					t = declp.match(output)
					if (t):
						output = output[t.end(0):]
					else:
						prefix = 'CREATE OR REPLACE'
			else:
				prefix = 'CREATE OR REPLACE '
			output = prefix + output
		self.__source	= output

		# create execute command for editing/viewing
		if (readonly):
		    # find out which pager to use
		    binary = find_val("more", "ORAEDIT_PAGER", "PAGER")
		else:
		    # find out editor to use -- note vi is on any Unix
		    binary = find_val("vi", "ORAEDIT_EDITOR", "EDITOR")
		tuple = string.split(binary)
		if (self.__useX):
		    # set things up to run under xterm
		    tuple = ['xterm', 'xterm', '-T', repr(self), '-e'] + tuple
		else:
		    # set things up so that program would get proper argv[0]
		    tuple.insert(1, tuple[0])
		self.__exec_program = tuple[0]
		self.__exec_args = tuple[1:] + [self.__tmpfilename]

	# object destructor
	# ensure object has appropriate attributes before using them (we may
	# have aborted early in constructor)
	def __del__(self):
	    	try: os.unlink(self.__tmpfilename)
		except os.error, (errnum, errmsg):
			# ignore "No such file or directory"
			if (errnum != errno.ENOENT): raise
		except AttributeError, errmsg: pass

		# unlink the lockfile only when we succesfully obtained
		# the lock -- if we don't have the lock, the check for
		# self.__lockfd triggers AttributeError
		try:
			if (self.__lockfd): pass
			os.unlink(self.__lockfilename)
		except os.error, (errnum, errmsg):
			# ignore "No such file or directory"
			if (errnum != errno.ENOENT): raise
		except AttributeError, errmsg: pass

		try: del self.__conn
		except AttributeError, errmsg: pass

	# return printable representation of the object
	def __repr__(self):
		obj_type = type_name2abbr(self.__obj_type)
		return self.__owner + '.' + self.__obj_name + '[' \
			+ obj_type + ']'

	# print informational message to output
	def info(self, txt, wantnewline='yes'):
		if (not self.__silent): print repr(self) + ':', txt,
		if (wantnewline == 'yes'): print

	# print error message to stderr
	def errinfo(self, txt):
		sys.stderr.write(repr(self) + ': ' + txt + '\n')

	# execute editor or viewer; return child pid to parent
	def __exec(self, viewing=None):
		childpid = os.fork()
		if (childpid <> 0): return childpid
		try: 
		    if not viewing and self.__useX:
			# if we want to edit in separate xterm, create
			# separate process group so signals delivered
			# to parent won't be forwarded to us; it's not
			# a problem when the editor starts in same window
			# as parent, as the keyboard signals are delivered
			# to editor and we need full access to terminal anyway
			os.setpgrp()
		    os.execvp(self.__exec_program, self.__exec_args)
		except os.error, (errnum, errmsg):
			self.errinfo("%s: error %d (%s)"\
				% (self.__exec_program, errnum, errmsg))
			os.exit(-1)
		# not reached

	# edit object
	def edit(self):
		open(self.__tmpfilename, "w").write(self.__source)
		childpid = self.__exec()
		oldchtime = os.stat(self.__tmpfilename)[9]
		# if we get interrupted from keyboard, we would wait
		# for editor to finish before we exit, so that
		# changes entered after last save are not lost
		try:
		    while 1:
			# find out if the file has been updated
			# ignore error if file doesn't exist
			try: newchtime = os.stat(self.__tmpfilename)[9]
			except os.error, (errnum, errmsg):
				if (errnum != errno.ENOENT): raise
				newchtime = oldchtime

			# wait a while -- if the file changed,
			# wait  until editor finished saving, if it
			# didn't change, wait a while before next test
			time.sleep(0.2)

			# test if the child is still alive
			res = os.waitpid(childpid, os.WNOHANG)
			if (res[0] != 0): break

			if (newchtime != oldchtime):
				self.update()
				oldchtime = newchtime
				self.info(time.strftime('%Y/%m/%d %H:%M:%S',
					time.localtime(newchtime))
					+ ': editor save detected')
				self.deploy()
		except KeyboardInterrupt:
			self.info('iterrupted, waiting for editor to finish ..')
			self.aborted = "yes"

			# sqlplus received the SIGINT as well, it can
			# be confused, so create new connection
			self.__conn = self.connection()

			# wait for child to finish
			os.waitpid(childpid, 0)

		self.update()
				
	# opens OraConnect object with appropriate parameters and
	# returns it
	def connection(self):
		return OraConnect(self.__connect_str, self.__rcmd)

	# called after source change via editor to re-read
	# the text and act accordingly
	def update(self):
		newsrc = open(self.__tmpfilename, "r").read()
		if (not newsrc):
			raise OE_Error, 'no source written'
		self.__modified = (newsrc != self.__source)
		if (not self.__dirty and self.__modified):
			self.__dirty = "yes"
		# for views, canonify source code so it will be in form
		# expected by sqlplus
		if (self.__obj_type == "VIEW"):
			newsrc = string.rstrip(newsrc)
			if (newsrc[-1:] != ';'): newsrc = newsrc  + ";"
		self.__source = newsrc
		
	# check if there are any errors in source; raises OE_Error
	# with errors description if there are any
	def checkerrors(self, msg=''):
		if (msg and string.find(msg, 'with compilation errors.') < 0):
			err = string.find(string.lower(msg), 'error')
			if (err >= 0):
				msg = string.strip(msg[err:])
				raise OE_Deploy, msg

		showerrors_cmd = 'show errors ' \
			+ self.__obj_type + ' ' + self.__obj_name
		errors = self.__conn.run(showerrors_cmd)
		if (string.find(errors, 'ORA-01041') >= 0):
			self.__conn = self.connection()
			errors = self.__conn.run(showerrors_cmd)
		if (string.find(errors, 'No errors.') < 0):
			raise OE_Deploy, errors

	# deploy the source; returns errors found in source or None
	# if thre aren't any
	def __deploy(self):
		# do not deploy if not changed; if there are any errors
		# in source code, just view the errors and don't raise
		# OE_Deploy -- we might get stuck in edit/deploy loop then
		if (not self.__dirty): 
			try: self.checkerrors()
			except OE_Deploy, err:
				self.info('');
				self.errinfo(err)
			self.info('source not changed, not trying to deploy')
			return

		# work-around bug in Linux sqlplus 8.0.5.0.0
		# -- it returns  ORA-01041 when attempting to
		# CREATE OR REPLACE for second and every next
		# time in same sqlplus session
		self.info('deploying ', wantnewline='no')
		if (self.__source[-2:] != '/\n'): extra_s = '\n/\n'
		else: extra_s = ''
		msg = self.__conn.run(self.__source + extra_s,
			info=(not self.__silent))
		# add the newline (not written in above "deploying" message)
		if (not self.__silent): sys.stdout.write('\n');
		if (not self.__buggysqlplus 
			and string.find(msg, 'ORA-01041:') >= 0):
				self.__buggysqlplus = 'yes'
				self.errinfo('buggy sqlplus detected, ' +
					'upgrade it if possible')
				self.__conn = self.connection()
				return self.__deploy()
		self.checkerrors(msg)

		self.__dirty = None

	# wrapped __deploy(), returns 1 on success, 0 on failure and
	# prints all necessary messages to user
	def deploy(self):
		err_reason = None
		try: self.__deploy()
		except OE_Deploy, reason:
			err_reason = reason
		if (err_reason): self.errinfo(err_reason)
		return (err_reason is None)

	# view object (read-only, so no changes are possible or propagated
	# back to object, stored in database) 
	def view(self):
		self.info('viewing object via pager "' +
			self.__exec_program + '"')
		open(self.__tmpfilename, "w").write(self.__source)
		os.waitpid(self.__exec(viewing="yes"), 0)
# -- end of definition of object OraEdit

# print usage to stderr and exit
def usage(short=None):
	basic_usage = """Usage:
  oraedit [-?shxlrR] [-T{f|p|v|pi|pb|ti|tb}] login/pass[@machine] """ + \
	"[owner.]object"""
	if (short):
		sys.stderr.write(basic_usage + '\n')
		return
	sys.stderr.write(
	"OraEdit " + version + """ (c) 1998, 1999, 2004 Jaromir Dolecek
Simple oracle db object editor.

""" + basic_usage + """
	-?	print usage information (this screen)
	-s	silent mode (printing only errors)
	-h host	specify host for remote connection
	-x	run editor in separate xterm
	-l	loop - do not exit after succesfull deploy
	-r	read-only - display the object's code, no editing
	-R	use remote connection to run sqlplus
	-Txxx	type of object to be opened (defaults to package body):
		  f: function
		  p: procedure
		  v: view
		 pi: package interface/specification
		 pb: package body
		 ti: type interface/specification
		 tb: type body
"""
	)
# -- end of usage()

types_map = { 'f': 'FUNCTION', 'p': 'PROCEDURE', 'v': 'VIEW',
	'pi': 'PACKAGE', 'pb': 'PACKAGE BODY',
	'ti': 'TYPE', 'tb': 'TYPE BODY'}

# maps type identificator (see -T parameter) to object name
def type_id2name(type_id):
	global types_map
	if types_map.has_key(type_id):
		return types_map[type_id]
	else:
		raise OE_Error, 'invalid type id -- "' + type_id + '"'
# -- end of type_id2name()

types_abbr = {
	'PACKAGE BODY'	: 'PKG BODY',
	'PACKAGE'	: 'PKG',
	'PROCEDURE'	: 'PROC',
	'FUNCTION'	: 'FUNC',
	'TYPE BODY'	: 'T.BODY' }

# returns abbreviation of type name, used in OraConnect.__repr__()
def type_name2abbr(type_name):
	global types_abbr
	if (types_abbr.has_key(type_name)):
		return types_abbr[type_name]
	else:
		return type_name
# -- end of type_name2abbr()

# returns value of first environment variable found or the default
# value if no supplied environment variable is present
def find_val(def_value, *vars):
	for var in vars:
		if (os.environ.has_key(var)):
			return os.environ[var]
	return def_value

# program body
def main():
	try:
		opt, remain = getopt.getopt(['-Tpb'] + sys.argv[1:],
				'T:?lxsrh:R')
	except getopt.error, reason:
		raise OE_Error, reason

	obj_type = None
	loop = 0	# loop even when deploy is succesfull
	silent = 0	# shut up ?
	usex = 0	# run editor in separate xterm ?
	readonly = 0	# view only ?
	host = ''	# host on which we want to run sqlplus
	remote = None	# run sqlplus on remote system
	for i in range(len(opt)):
		opt_c = opt[i][0]
		if opt_c == '-s':
			silent = "yes"
		elif opt_c == '-?':
			# print usage and exit succesfully
			usage()
			return 0
		elif opt_c == '-l':
			loop = "yes"
		elif opt_c == '-x':
			usex = "yes"
		elif opt_c == '-r':
			readonly = "yes"
		elif opt_c == '-T':
			obj_type = type_id2name(opt[i][1])
		elif opt_c == '-h':
			remote = 'yes'
			host = opt[i][1]
		elif opt_c == '-R':
			remote = 'yes'

	# should be two more arguments - connect string and name of the object
	if len(remain) != 2:
		raise OE_Error, 'bad number of arguments (' + \
			str(len(remain)) + ') - should be two'

	obj_name = remain[1]
	if (not silent):
		if (not readonly):
			print 'Editing ' + obj_type + ' ' + obj_name + '.'
		else:
			print 'Viewing ' + obj_type + ' ' + obj_name + '.'

	# ensure sqlplus is available
	if (os.environ.has_key('PATH')): path = os.environ['PATH']
	else: path = ''
	found = 0
	for dir in string.split(path, ':'): # Unix only right now
		if os.path.isfile(os.path.join(dir, sqlplus)): found = 1
	if (not found):
		sys.stdout.write('oraedit: executable "' + sqlplus + '"' + 
			' not found in PATH' + '\n')
		return 1

	# update NLS_LANG so it would use AMERICAN_AMERICA language
	# setting - we rely on english messages in OraEdit.deploy()
	if (os.environ.has_key('NLS_LANG')):
		nls_lang = os.environ['NLS_LANG']
		dot = string.find(nls_lang, '.')
		if (dot >= 0):
			nls_lang = 'AMERICAN_AMERICA' + nls_lang[dot:]
			os.environ['NLS_LANG'] = nls_lang

	# construct command to run sqlplus remotely -- use
	# ORAEDIT_RCMD if set or default to ``rsh host sqlplus ...''
	conn_str = remain[0]
	if (remote):
		hostrcmdvar = 'ORAEDIT_RCMD_' + string.upper(host)
		if (host and os.environ.has_key(hostrcmdvar)):
			rcmdformat = os.environ[hostrcmdvar]
		elif (os.environ.has_key('ORAEDIT_RCMD')):
			rcmdformat = os.environ['ORAEDIT_RCMD']
		else:
			rcmdformat = "rsh %host% %sqlplus_cmd%"
		rcmd = string.replace(rcmdformat, '%host%', host)
		sqlplus_cmd = sqlplus + ' ' + conn_str
		if (string.find(rcmd, '%sqlplus_cmd%') >= 0):
			rcmd = string.replace(rcmd, '%sqlplus_cmd%',sqlplus_cmd)
		else:
			rcmd = rcmd + ' ' + sqlplus_cmd
	else:
		rcmd = None

	# okay, fun begins, create the OraEdit object
	oraed = OraEdit(remain[0], obj_name, obj_type,
		usex=usex, readonly=readonly, silent=silent,
		rcmd=rcmd)

	while (not readonly):
		try:
			oraed.edit()
			if (oraed.deploy() and not loop): break
		except OE_Error, reason:
			# fatal error, print message and exit
			oraed.errinfo(reason)
			return 1

		if (oraed.aborted):
			oraed.info('Interrupted, exiting NOW.')
			return -1
	else: oraed.view()

	oraed.info('exiting.')
	return 0
# -- end of main()

# don't bother doing anything if called without parameters -- just
# call usage() and exit
if (len(sys.argv) == 1):
	usage()
	sys.exit(0)

# call the main
try:
	sys.exit(main())
except KeyboardInterrupt:
	sys.stderr.write('oraedit: Interrupted, aborting ...' + '\n')
except OE_Locked, msg:
	sys.stderr.write('oraedit: ' + msg + '\n')
	sys.exit(2)
except OE_Error, reason:
	sys.stderr.write('oraedit: ' + reason + '\n')
	usage("short")
	sys.exit(1)
