From 07c5414ce60facbc23fe5efb93f4b2939ea93817 Mon Sep 17 00:00:00 2001 From: Chris Lumens Date: Fri, 28 Jun 2013 14:54:38 -0400 Subject: [PATCH] Add an interactive kickstart shell command, ksshell. --- docs/ksshell.1 | 37 +++++++++ pykickstart.spec | 1 + setup.py | 5 +- tools/ksshell | 238 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 docs/ksshell.1 create mode 100755 tools/ksshell diff --git a/docs/ksshell.1 b/docs/ksshell.1 new file mode 100644 index 0000000..5cbd2de --- /dev/null +++ b/docs/ksshell.1 @@ -0,0 +1,37 @@ +.TH "KSSHELL" "1" +.SH "NAME" +ksshell \(em an interactive kickstart shell +.SH "SYNOPSIS" +.PP +\fBksflatten\fR [\fB\-i\fR | \fB\-\-input INFILE\fP] [\fB\-o\fR | \fB\-\-output OUTFILE\fP] [\fB\-v\fR | \fB\-\-version VERSION\fP] +.SH "DESCRIPTION" +.PP +\fBksshell\fR is an interactive kickstart shell. It optionally takes an input kickstart file as the basis, +allows the user to specify additional kickstart commands, and then writes out the finished kickstart file +to stdout or the given file name. This program supports all the usual readline niceties including tab +completion of kickstart commands and their options, though not the values those options can take. +.PP +In addition to understanding all the kickstart commands, \fBksshell\fR has some builtin commands of its +own to make working with kickstart files in the context of a shell easier: +.IP .clear +Clear the existing kickstart data, including any from INFILE. This essentially starts you over from a +blank state. +.IP .quit +Quit the interactive shell, either saving to the file given by OUTFILE or printing to stdout if none +was given. +.IP .show +Print the current kickstart file state. +.SH "EXIT STATUS" +.PP +\fBksflatten\fR returns 0 on success, and 1 if VERSION is incorrect. If INFILE does not exist, a warning +will be printed but the user will still be dumped to the interactive shell. +.SH "OPTIONS" +.IP "\fB\-i\fR, \fB\-\-input INFILE\fP" 10 +The name of the input kickstart file. +.IP "\fB\-o\fR, \fB\-\-output OUTFILE\fP" 10 +Write the flattened kickstart file to OUTFILE, or stdout if no filename is given. +.IP "\fB\-v\fR, \fB\-\-version VERSION\fP" 10 +Use this version of kickstart syntax when processing the file, or the latest if no version is given. +.SH "SEE ALSO" +.PP +ksvalidator (1), ksverdiff (1) diff --git a/pykickstart.spec b/pykickstart.spec index b13d6dc..e5c84ed 100644 --- a/pykickstart.spec +++ b/pykickstart.spec @@ -44,6 +44,7 @@ rm -rf %{buildroot} %{_bindir}/ksvalidator %{_bindir}/ksflatten %{_bindir}/ksverdiff +%{_bindir}/ksshell %{_mandir}/man1/* %changelog diff --git a/setup.py b/setup.py index fc0279c..df191c5 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ setup(name='pykickstart', version='1.99.33', description='Python module for manipulating kickstart files', author='Chris Lumens', author_email='clumens@redhat.com', url='http://fedoraproject.org/wiki/pykickstart', - scripts=['tools/ksvalidator', 'tools/ksflatten', 'tools/ksverdiff'], + scripts=['tools/ksvalidator', 'tools/ksflatten', 'tools/ksverdiff', 'tools/ksshell'], packages=['pykickstart', 'pykickstart.commands', 'pykickstart.handlers'], - data_files=[('share/man/man1', ['docs/ksvalidator.1', 'docs/ksflatten.1', 'docs/ksverdiff.1'])]) + data_files=[('share/man/man1', ['docs/ksvalidator.1', 'docs/ksflatten.1', 'docs/ksverdiff.1', + 'docs/ksshell.1'])]) diff --git a/tools/ksshell b/tools/ksshell new file mode 100755 index 0000000..e501fea --- /dev/null +++ b/tools/ksshell @@ -0,0 +1,238 @@ +#!/usr/bin/python +# +# Chris Lumens +# +# Copyright 2013 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, modify, +# copy, or redistribute it subject to the terms and conditions of the GNU +# General Public License v.2. This program is distributed in the hope that it +# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the +# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat +# trademarks that are incorporated in the source code or documentation are not +# subject to the GNU General Public License and may only be used or replicated +# with the express permission of Red Hat, Inc. + +# This script takes as input an optional input kickstart file and an optional +# kickstart syntax version (the latest is assumed, if none specified). It +# then provides an interactive shell for the user to manipulate the state of +# the input kickstart file and either prints the final results on stdout or to +# a designated output file when the program quits. + +# TODO: +# - error reporting always says you are on line 1 +# - handle sections like %packages +# - some meta-commands might be nice: +# - .delete (a previous line) +# - .help (requires moving help text into each optionparser object?) +# - .save + +import readline +from optparse import OptionParser +import os, sys + +from pykickstart.errors import KickstartError, KickstartVersionError +from pykickstart.parser import KickstartParser, preprocessKickstart +from pykickstart.version import DEVEL, makeVersion + +import gettext +gettext.textdomain("pykickstart") +_ = lambda x: gettext.ldgettext("pykickstart", x) + +## +## INTERNAL COMMANDS +## These are commands that control operation of the kickstart shell and are +## handled by this program. They are not recognized kickstart commands. +## + +class InternalCommand(object): + def __init__(self): + self.op = OptionParser() + + def execute(self, parser): + pass + +class ClearCommand(InternalCommand): + def execute(self, parser): + version = parser.version + parser.handler = makeVersion(version) + +class QuitCommand(InternalCommand): + def execute(self, parser): + raise EOFError + +class ShowCommand(InternalCommand): + def execute(self, parser): + print(parser.handler) + +## +## COMMAND COMPLETION +## + +class KickstartCompleter(object): + def __init__(self, handler, internalCommands): + # Build up a dict of kickstart commands and their valid options: + # { command_name: [options] } + self.commands = {} + + for (cStr, cObj) in handler.commands.iteritems(): + self._add_command(cStr, cObj) + + for (cStr, cObj) in internalCommands.iteritems(): + self._add_command(cStr, cObj) + + self.currentCandidates = [] + + def _add_command(self, cStr, cObj): + self.commands[cStr] = [] + + # Simple commands do not have any optparse crud. + if not hasattr(cObj, "op"): + return + + for opt in cObj.op.option_list: + self.commands[cStr] += opt._short_opts + opt._long_opts + + def complete(self, text, state): + response = None + + # This is the first time Tab has been pressed, so build up a list of matches. + if state == 0: + origline = readline.get_line_buffer() + begin = readline.get_begidx() + end = readline.get_endidx() + + beingCompleted = origline[begin:end] + words = origline.split() + + if not words: + # Everything's a match for an empty string. + self.currentCandidates = sorted(self.commands.keys()) + else: + try: + # Ignore leading whitespace when trying to figure out + # completions for a kickstart command. + if begin == 0 or origline[:begin].strip() == "": + # first word + candidates = self.commands.keys() + else: + # later word + candidates = self.commands[words[0]] + + if beingCompleted: + self.currentCandidates = [w for w in candidates if w.startswith(beingCompleted)] + else: + self.currentCandidates = candidates + except (KeyError, IndexError) as e: + self.currentCandidates = [] + + try: + response = self.currentCandidates[state] + except IndexError: + response = None + + return response + +## +## OPTION PROCESSING +## + +op = OptionParser(usage="usage: %prog [options]") +op.add_option("-i", "--input", dest="input", + help=_("a basis file to use for seeding the kickstart data (optional)")) +op.add_option("-o", "--output", dest="output", + help=_("the location to write the finished kickstart file, or stdout if not given")) +op.add_option("-v", "--version", dest="version", default=DEVEL, + help=_("version of kickstart syntax to validate against")) + +(opts, extra) = op.parse_args(sys.argv[1:]) +if extra: + op.print_help() + sys.exit(1) + +## +## SETTING UP PYKICKSTART +## + +try: + handler = makeVersion(opts.version) +except KickstartVersionError: + print(_("The version %s is not supported by pykickstart") % opts.version) + sys.exit(1) + +ksparser = KickstartParser(handler, followIncludes=True, errorsAreFatal=False) + +if opts.input: + try: + processedFile = preprocessKickstart(opts.input) + ksparser.readKickstart(processedFile) + os.remove(processedFile) + except KickstartError as e: + # Errors should just dump you to the prompt anyway. + print(_("Warning: The following error occurred when processing the input file:\n%s\n") % e) + +internalCommands = {".clear": ClearCommand(), + ".show": ShowCommand(), + ".quit": QuitCommand()} + +## +## SETTING UP READLINE +## + +readline.parse_and_bind("tab: complete") +readline.set_completer(KickstartCompleter(handler, internalCommands).complete) + +# Since everything in kickstart looks like a command line arg, we need to +# remove '-' from the delimiter string. +delims = readline.get_completer_delims() +readline.set_completer_delims(delims.replace('-', '')) + +## +## REPL +## + +print("Press ^D to exit.") + +while True: + try: + line = raw_input("ks> ") + except EOFError: + # ^D was hit, time to quit. + break + except KeyboardInterrupt: + # ^C was hit, time to quit. Don't be like other programs. + break + + # All internal commands start with a ., so if that's the beginning of the + # line, we need to dispatch ourselves. + if line.startswith("."): + words = line.split() + if words[0] in internalCommands: + try: + internalCommands[words[0]].execute(ksparser) + except EOFError: + # ".quit" was typed, time to quit. + break + else: + print(_("Internal command %s not recognized.") % words[0]) + + continue + + # Now process the line of input as if it were a kickstart file - just an + # extremely short one. + try: + ksparser.readKickstartFromString(line) + except KickstartError as e: + print(e) + +# And finally, print the output kickstart file. +if opts.output: + with open(opts.output, "w") as fd: + fd.write(str(ksparser.handler)) +else: + print("\n" + str(ksparser.handler)) -- 2.11.4.GIT