Add tests for incorrect command usage detection
[pykickstart.git] / tools / ksshell
blobe501fea621ebbb9fbd90c387815003684ee3975b
1 #!/usr/bin/python
3 # Chris Lumens <clumens@redhat.com>
5 # Copyright 2013 Red Hat, Inc.
7 # This copyrighted material is made available to anyone wishing to use, modify,
8 # copy, or redistribute it subject to the terms and conditions of the GNU
9 # General Public License v.2. This program is distributed in the hope that it
10 # will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
11 # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12 # See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # this program; if not, write to the Free Software Foundation, Inc., 51
16 # Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat
17 # trademarks that are incorporated in the source code or documentation are not
18 # subject to the GNU General Public License and may only be used or replicated
19 # with the express permission of Red Hat, Inc.
21 # This script takes as input an optional input kickstart file and an optional
22 # kickstart syntax version (the latest is assumed, if none specified). It
23 # then provides an interactive shell for the user to manipulate the state of
24 # the input kickstart file and either prints the final results on stdout or to
25 # a designated output file when the program quits.
27 # TODO:
28 # - error reporting always says you are on line 1
29 # - handle sections like %packages
30 # - some meta-commands might be nice:
31 # - .delete (a previous line)
32 # - .help (requires moving help text into each optionparser object?)
33 # - .save
35 import readline
36 from optparse import OptionParser
37 import os, sys
39 from pykickstart.errors import KickstartError, KickstartVersionError
40 from pykickstart.parser import KickstartParser, preprocessKickstart
41 from pykickstart.version import DEVEL, makeVersion
43 import gettext
44 gettext.textdomain("pykickstart")
45 _ = lambda x: gettext.ldgettext("pykickstart", x)
48 ## INTERNAL COMMANDS
49 ## These are commands that control operation of the kickstart shell and are
50 ## handled by this program. They are not recognized kickstart commands.
53 class InternalCommand(object):
54 def __init__(self):
55 self.op = OptionParser()
57 def execute(self, parser):
58 pass
60 class ClearCommand(InternalCommand):
61 def execute(self, parser):
62 version = parser.version
63 parser.handler = makeVersion(version)
65 class QuitCommand(InternalCommand):
66 def execute(self, parser):
67 raise EOFError
69 class ShowCommand(InternalCommand):
70 def execute(self, parser):
71 print(parser.handler)
74 ## COMMAND COMPLETION
77 class KickstartCompleter(object):
78 def __init__(self, handler, internalCommands):
79 # Build up a dict of kickstart commands and their valid options:
80 # { command_name: [options] }
81 self.commands = {}
83 for (cStr, cObj) in handler.commands.iteritems():
84 self._add_command(cStr, cObj)
86 for (cStr, cObj) in internalCommands.iteritems():
87 self._add_command(cStr, cObj)
89 self.currentCandidates = []
91 def _add_command(self, cStr, cObj):
92 self.commands[cStr] = []
94 # Simple commands do not have any optparse crud.
95 if not hasattr(cObj, "op"):
96 return
98 for opt in cObj.op.option_list:
99 self.commands[cStr] += opt._short_opts + opt._long_opts
101 def complete(self, text, state):
102 response = None
104 # This is the first time Tab has been pressed, so build up a list of matches.
105 if state == 0:
106 origline = readline.get_line_buffer()
107 begin = readline.get_begidx()
108 end = readline.get_endidx()
110 beingCompleted = origline[begin:end]
111 words = origline.split()
113 if not words:
114 # Everything's a match for an empty string.
115 self.currentCandidates = sorted(self.commands.keys())
116 else:
117 try:
118 # Ignore leading whitespace when trying to figure out
119 # completions for a kickstart command.
120 if begin == 0 or origline[:begin].strip() == "":
121 # first word
122 candidates = self.commands.keys()
123 else:
124 # later word
125 candidates = self.commands[words[0]]
127 if beingCompleted:
128 self.currentCandidates = [w for w in candidates if w.startswith(beingCompleted)]
129 else:
130 self.currentCandidates = candidates
131 except (KeyError, IndexError) as e:
132 self.currentCandidates = []
134 try:
135 response = self.currentCandidates[state]
136 except IndexError:
137 response = None
139 return response
142 ## OPTION PROCESSING
145 op = OptionParser(usage="usage: %prog [options]")
146 op.add_option("-i", "--input", dest="input",
147 help=_("a basis file to use for seeding the kickstart data (optional)"))
148 op.add_option("-o", "--output", dest="output",
149 help=_("the location to write the finished kickstart file, or stdout if not given"))
150 op.add_option("-v", "--version", dest="version", default=DEVEL,
151 help=_("version of kickstart syntax to validate against"))
153 (opts, extra) = op.parse_args(sys.argv[1:])
154 if extra:
155 op.print_help()
156 sys.exit(1)
159 ## SETTING UP PYKICKSTART
162 try:
163 handler = makeVersion(opts.version)
164 except KickstartVersionError:
165 print(_("The version %s is not supported by pykickstart") % opts.version)
166 sys.exit(1)
168 ksparser = KickstartParser(handler, followIncludes=True, errorsAreFatal=False)
170 if opts.input:
171 try:
172 processedFile = preprocessKickstart(opts.input)
173 ksparser.readKickstart(processedFile)
174 os.remove(processedFile)
175 except KickstartError as e:
176 # Errors should just dump you to the prompt anyway.
177 print(_("Warning: The following error occurred when processing the input file:\n%s\n") % e)
179 internalCommands = {".clear": ClearCommand(),
180 ".show": ShowCommand(),
181 ".quit": QuitCommand()}
184 ## SETTING UP READLINE
187 readline.parse_and_bind("tab: complete")
188 readline.set_completer(KickstartCompleter(handler, internalCommands).complete)
190 # Since everything in kickstart looks like a command line arg, we need to
191 # remove '-' from the delimiter string.
192 delims = readline.get_completer_delims()
193 readline.set_completer_delims(delims.replace('-', ''))
196 ## REPL
199 print("Press ^D to exit.")
201 while True:
202 try:
203 line = raw_input("ks> ")
204 except EOFError:
205 # ^D was hit, time to quit.
206 break
207 except KeyboardInterrupt:
208 # ^C was hit, time to quit. Don't be like other programs.
209 break
211 # All internal commands start with a ., so if that's the beginning of the
212 # line, we need to dispatch ourselves.
213 if line.startswith("."):
214 words = line.split()
215 if words[0] in internalCommands:
216 try:
217 internalCommands[words[0]].execute(ksparser)
218 except EOFError:
219 # ".quit" was typed, time to quit.
220 break
221 else:
222 print(_("Internal command %s not recognized.") % words[0])
224 continue
226 # Now process the line of input as if it were a kickstart file - just an
227 # extremely short one.
228 try:
229 ksparser.readKickstartFromString(line)
230 except KickstartError as e:
231 print(e)
233 # And finally, print the output kickstart file.
234 if opts.output:
235 with open(opts.output, "w") as fd:
236 fd.write(str(ksparser.handler))
237 else:
238 print("\n" + str(ksparser.handler))