yast/yast-yast2

View on GitHub
library/commandline/src/modules/CommandLine.rb

Summary

Maintainability
F
2 wks
Test Coverage
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File:  modules/CommandLine.ycp
# Package:  yast2
# Summary:  Command line interface for YaST2 modules
# Author:  Stanislav Visnovsky <visnov@suse.cz>
#
# $Id$
require "yast"

module Yast
  class CommandLineClass < Module
    include Yast::Logger

    def main
      Yast.import "Directory"
      Yast.import "Mode"
      Yast.import "Popup"
      Yast.import "Report"
      Yast.import "Stage"
      Yast.import "String"
      Yast.import "Integer"
      Yast.import "TypeRepository"
      Yast.import "XML"

      textdomain "base"

      @cmdlineprompt = "YaST2 > "

      # Map of commands for every module. ATM the list of commands this module handles internally.
      @systemcommands = {
        "actions"  => {
          "help"        => {
            # translators: help for 'help' option on command line
            "help"     => _(
              "Print the help for this module"
            ),
            "readonly" => true
          },
          "longhelp"    => {
            # translators: help for 'longhelp' option on command line
            "help"     => _(
              "Print a long version of help for this module"
            ),
            "readonly" => true
          },
          "xmlhelp"     => {
            # translators: help for 'xmlhelp' option on command line
            "help"     => _(
              "Print a long version of help for this module in XML format"
            ),
            "readonly" => true
          },
          "interactive" => {
            # translators: help for 'interactive' option on command line
            "help"     => _(
              "Start interactive shell to control the module"
            ),
            # interactive mode itself does not mean that write is needed.
            # The first action that is not readonly will switch flag.
            "readonly" => true
          },
          "exit"        => {
            # translators: help for 'exit' command line interactive mode
            "help"     => _(
              "Exit interactive mode and save the changes"
            ),
            "readonly" => true
          },
          "abort"       => {
            # translators: help for 'abort' command line interactive mode
            "help"     => _(
              "Abort interactive mode without saving the changes"
            ),
            "readonly" => true
          }
        },
        "options"  => {
          "help"    => {
            # translators:  command line "help" option
            "help" => _(
              "Print the help for this command"
            )
          },
          "verbose" => {
            # translators: command line "verbose" option
            "help" => _(
              "Show progress information"
            )
          },
          "xmlfile" => {
            # translators: command line "xmlfile" option
            "help" => _(
              "Where to store the XML output"
            ),
            "type" => "string"
          }
        },
        "mappings" => {
          "help"        => ["help", "verbose"],
          "xmlhelp"     => ["help", "verbose", "xmlfile"],
          "interactive" => ["help", "verbose"],
          "exit"        => ["help"],
          "abort"       => ["help"]
        }
      }

      # Map of commands defined by the YaST2 module.
      @modulecommands = {}

      # Merged map of commands - both defined by the YaST2 module and system commands. Used for lookup
      @allcommands = deep_copy(@systemcommands)

      # User asked for interactive session
      @interactive = false

      # All commands have been processed
      @done = false

      # User asked for quitting of interactive session, or there was an error
      @aborted = false

      # a cache for already parsed but not processed command
      @commandcache = {}

      # Verbose mode flag
      @verbose = false

      # Remember the command line specification for later use
      @cmdlinespec = {}

      # string: command line interface is not supported
      @nosupport = _(
        "This YaST2 module does not support the command line interface."
      )
    end

    #  Print a String
    #
    #  Print a string to /dev/tty in interactive mode, to stderr in non-interactive
    #  Suppress printing if there are no commands to be handled (starting GUI)
    #
    #  @param [String] string to be printed
    #  @param [Boolean] newline if newline character should be added or not
    def PrintInternal(string, newline)
      return if !Mode.commandline

      # avoid using of uninitialized value in .dev.tty perl agent
      if string.nil?
        Builtins.y2warning("CommandLine::Print: invalid argument (nil)")
        return
      end

      if @interactive
        if newline
          SCR.Write(path(".dev.tty"), string)
        else
          SCR.Write(path(".dev.tty.nocr"), string)
        end
      elsif newline
        SCR.Write(path(".dev.tty.stderr"), string)
      else
        SCR.Write(path(".dev.tty.stderr_nocr"), string)
      end

      nil
    end

    #  Print a String
    #
    #  Print a string to /dev/tty in interactive mode, to stderr in non-interactive
    #  Suppress printing if there are no commands to be handled (starting GUI)
    #
    #  @param [String] string to be printed
    def Print(string)
      PrintInternal(string, true)
    end

    #  Print a String, don't add a trailing newline character
    #
    #  Print a string to /dev/tty in interactive mode, to stderr in non-interactive
    #  Suppress printing if there are no commands to be handled (starting GUI)
    #
    #  @param [String] string to be printed
    def PrintNoCR(string)
      PrintInternal(string, false)
    end

    # Same as Print(), but the string is printed only when verbose command
    # line mode was activated
    # @param [String] string to print
    def PrintVerbose(string)
      Print(string) if @verbose

      nil
    end

    # Same as PrintNoCR(), but the string is printed only when verbose command
    # line mode was activated
    # @param [String] string to print
    def PrintVerboseNoCR(string)
      PrintNoCR(string) if @verbose

      nil
    end

    #  Print a Table
    #
    #  Print a table using Print(). Format of table is as libyui but not all features
    #  are supported, e.g. no icons.
    #
    #  @param [Yast::Term] header  header of table in libyui format
    #  @param [Array<Yast::Term>] content  content of table in libyui format
    def PrintTable(header, content)
      header = deep_copy(header)
      content = deep_copy(content)
      aligns = []
      widths = []

      process = lambda do |line|
        line = deep_copy(line)
        ret = []
        anys = Builtins.argsof(line)
        Builtins.foreach(anys) do |a|
          if Ops.is_string?(a)
            s = Convert.to_string(a)
            ret = Builtins.add(ret, s)
          elsif Ops.is_term?(a)
            t = Convert.to_term(a)
            ret = Builtins.add(ret, Ops.get_string(Builtins.argsof(t), 0, "")) if Builtins.contains([:Left, :Center, :Right], Builtins.symbolof(t))
          end
        end
        deep_copy(ret)
      end

      get_aligns = lambda do |header2|
        header2 = deep_copy(header2)
        anys = Builtins.argsof(header2)
        Builtins.foreach(Integer.Range(Builtins.size(anys))) do |i|
          a = Ops.get(anys, i)
          if Ops.is_term?(a)
            t = Convert.to_term(a)
            Ops.set(aligns, i, :right) if Builtins.symbolof(t) == :Right
          end
        end

        nil
      end

      update_widths = lambda do |columns|
        columns = deep_copy(columns)
        Builtins.foreach(Integer.Range(Builtins.size(columns))) do |i|
          Ops.set(
            widths,
            i,
            Integer.Max(
              [Ops.get(widths, i, 0), Builtins.size(Ops.get(columns, i, ""))]
            )
          )
        end

        nil
      end

      print_line = lambda do |line|
        line = deep_copy(line)
        columns = process.call(line)
        Builtins.foreach(Integer.Range(Builtins.size(columns))) do |i|
          Ops.set(
            columns,
            i,
            String.SuperPad(
              Ops.get(columns, i, ""),
              Ops.get(widths, i, 0),
              " ",
              Ops.get(aligns, i, :left)
            )
          )
        end
        Print(Builtins.mergestring(columns, " | "))

        nil
      end

      update_widths.call(process.call(header))
      Builtins.foreach(content) { |row| update_widths.call(process.call(row)) }

      print_line.call(header)

      get_aligns.call(header)

      Print(Builtins.mergestring(Builtins.maplist(widths) do |width|
        String.Repeat("-", width)
      end, "-+-"))

      Builtins.foreach(content) { |row| print_line.call(row) }

      nil
    end

    # Print an Error Message
    #
    # Print an error message and add the description how to get the help.
    # @param [String] message  error message to be printed. Use nil for no message
    def Error(message)
      Print(message) if !message.nil?

      if @interactive
        # translators: default error message for command line
        Print(_("Use 'help' for a complete list of available commands."))
      else
        # translators: default error message for command line
        Print(
          Builtins.sformat(
            _("Use 'yast2 %1 help' for a complete list of available commands."),
            Ops.get_string(@modulecommands, "id", "")
          )
        )
      end

      nil
    end

    #  Parse a list of arguments.
    #
    #  It checks the validity of the arguments, the type correctness
    #  and returns the command and its options in a map.
    #  @param [Array] arguments  the list of arguments to be parsed
    #  @return [Hash{String => Object}]  containing the command and it's option. In case of
    #        error it is an empty map.
    def Parse(arguments)
      args = deep_copy(arguments)
      return {} if Ops.less_than(Builtins.size(args), 1)

      # Parse command
      command = args.shift
      Builtins.y2debug("command=%1", command)
      Builtins.y2debug("args=%1", args)

      if command == ""
        Builtins.y2error(
          "CommandLine::Parse called with first parameter being empty. Arguments passed: %1",
          arguments
        )
        return {}
      end

      # Check command
      if !Builtins.haskey(Ops.get_map(@allcommands, "actions", {}), command)
        # translators: error message in command line interface
        Error(Builtins.sformat(_("Unknown Command: %1"), command))

        return {}
      end

      # build the list of options for the command
      opts = Ops.get_list(@allcommands, ["mappings", command], [])
      allopts = Ops.get_map(@allcommands, "options", {})
      cmdoptions = {}
      Builtins.maplist(opts) do |k|
        cmdoptions = Builtins.add(cmdoptions, k, Ops.get_map(allopts, k, {})) if Ops.is_string?(k)
      end

      ret = true

      # Parse options
      givenoptions = {}
      Builtins.maplist(args) do |aos|
        Builtins.y2debug("os=%1", aos)
        next if !Ops.is_string?(aos)

        os = Convert.to_string(aos)
        o = Builtins.regexptokenize(os, "([^=]+)=(.+)")
        Builtins.y2debug("o=%1", o)
        case Builtins.size(o)
        when 2
          givenoptions = Builtins.add(
            givenoptions,
            Ops.get(o, 0, ""),
            Ops.get(o, 1, "")
          )
        when 0
          # check, if the last character is "="
          # FIXME: consider whitespace
          if Builtins.substring(os, Ops.subtract(Builtins.size(os), 1)) == "="
            # translators: error message - user did not provide a value for option %1 on the command line
            Print(
              Builtins.sformat(
                _("Option '%1' is missing value."),
                Builtins.substring(os, 0, Ops.subtract(Builtins.size(os), 1))
              )
            )
            @aborted = true if !@interactive
            ret = false
            next {}
          else
            givenoptions = Builtins.add(givenoptions, os, "")
          end
        end
      end

      return {} if ret != true

      Builtins.y2debug("options=%1", givenoptions)

      # Check options

      # find out, if the action has a "non-strict" option set
      non_strict = Builtins.contains(
        Ops.get_list(@allcommands, ["actions", command, "options"], []),
        "non_strict"
      )
      Builtins.y2debug("Using non-strict check for %1", command) if non_strict

      # check (and convert data types)
      Builtins.maplist(givenoptions) do |o, val|
        v = Convert.to_string(val)
        next if ret != true

        if Ops.get(cmdoptions, o).nil?
          if !non_strict
            # translators: error message, %1 is a command, %2 is the wrong option given by the user
            Print(
              Builtins.sformat(
                _("Unknown option for command '%1': %2"),
                command,
                o
              )
            )
            @aborted = true if !@interactive
            ret = false
          end
        else
          # this option is valid, let's check the type

          opttype = Ops.get_string(cmdoptions, [o, "type"], "")

          if opttype != ""
            # need to check the type
            case opttype
            when "regex"
              opttypespec = Ops.get_string(cmdoptions, [o, "typespec"], "")
              ret = TypeRepository.regex_validator(opttypespec, v)
              if ret != true
                # translators: error message, %2 is the value given
                Print(
                  Builtins.sformat(_("Invalid value for option '%1': %2"), o, v)
                )
                @aborted = true if !@interactive
              end
            when "enum"
              ret = TypeRepository.enum_validator(
                Ops.get_list(cmdoptions, [o, "typespec"], []),
                v
              )
              if ret != true
                # translators: error message, %2 is the value given
                Print(
                  Builtins.sformat(_("Invalid value for option '%1': %2"), o, v)
                )
                @aborted = true if !@interactive
              end
            when "integer"
              i = Builtins.tointeger(v)
              ret = !i.nil?
              if ret == true
                # update value of the option to integer
                Ops.set(givenoptions, o, i)
              else
                # translators: error message, %2 is the value given
                Print(
                  Builtins.sformat(_("Invalid value for option '%1': %2"), o, v)
                )
                @aborted = true if !@interactive
              end
            else
              ret = (v == "") ? false : TypeRepository.is_a(v, opttype)

              if ret != true
                # translators: error message, %2 is expected type, %3 is the value given
                Print(
                  Builtins.sformat(
                    _(
                      "Invalid value for option '%1' -- expected '%2', received %3"
                    ),
                    o,
                    opttype,
                    v
                  )
                )
                @aborted = true if !@interactive
              end
            end
          # type is missing
          elsif v != ""
            Builtins.y2error(
              "Type specification for option '%1' is missing, cannot assign a value to the option",
              o
            )
            # translators: error message if option has a value, but cannot have one
            Print(
              Builtins.sformat(
                _("Option '%1' cannot have a value. Given value: %2"),
                o,
                v
              )
            )
            @aborted = true if !@interactive
            ret = false
          end
        end
      end

      # wrong, let's print the help message
      if ret != true
        if @interactive
          # translators: error message, how to get command line help for interactive mode
          # %1 is the module name, %2 is the action name
          Print(
            Builtins.sformat(
              _("Use '%1 %2 help' for a complete list of available options."),
              Ops.get_string(@modulecommands, "id", ""),
              command
            )
          )
        else
          # translators: error message, how to get command line help for non-interactive mode
          # %1 is the module name, %2 is the action name
          Print(
            Builtins.sformat(
              _(
                "Use 'yast2 %1 %2 help' for a complete list of available options."
              ),
              Ops.get_string(@modulecommands, "id", ""),
              command
            )
          )
        end
        return {}
      end

      { "command" => command, "options" => givenoptions }
    end

    # Print a nice heading for this module
    def PrintHead
      # translators: command line interface header, %1 is identification of the module
      head = Builtins.sformat(
        _("YaST Configuration Module %1\n"),
        Ops.get_string(@modulecommands, "id", "YaST")
      )
      head += "-" * (head.size - 1) # -1 to remove newline char from count
      head = Ops.add(Ops.add("\n", head), "\n")

      Print(head)
    end

    # Print a help text for a given action.
    #
    # @param [String] action the action for which the help should be printed
    def PrintActionHelp(action)
      # lookup action in actions
      command = Ops.get_map(@allcommands, ["actions", action], {})
      # translators: the command does not provide any help
      commandhelp = Ops.get(command, "help")
      commandhelp = _("No help available") if commandhelp.nil?
      has_string_option = false
      # Process <command> "help"
      # translators: %1 is the command name
      Print(Builtins.sformat(_("Command '%1'"), action))

      # print help
      if Ops.is_string?(commandhelp)
        Print(Builtins.sformat("    %1", commandhelp))
      elsif Ops.is(commandhelp, "list <string>")
        Builtins.foreach(
          Convert.convert(commandhelp, from: "any", to: "list <string>")
        ) { |e| Print(Builtins.sformat("    %1", e)) }
      end

      opts = Ops.get_list(@allcommands, ["mappings", action], [])

      # no options, skip the rest
      if Builtins.size(opts) == 0
        Print("")
        return
      end

      # translators: command line options
      Print(_("\n    Options:"))

      allopts = Ops.get_map(@allcommands, "options", {})

      longestopt = 0
      longestarg = 0

      Builtins.foreach(opts) do |opt|
        op = Ops.get_map(allopts, opt, {})
        t = Ops.get_string(op, "type", "")
        has_string_option = true if t == "string"
        if t != "regex" && t != "enum" && t != ""
          t = Ops.add(Ops.add("[", t), "]")
        elsif t == "enum"
          t = "[ "
          Builtins.foreach(Ops.get_list(op, "typespec", [])) do |s|
            t = Ops.add(Ops.add(t, s), " ")
          end
          t = Ops.add(t, "]")
        end
        longestarg = Builtins.size(t) if Ops.greater_than(Builtins.size(t), longestarg)
        if Ops.is_string?(opt) &&
            Ops.greater_than(Builtins.size(Convert.to_string(opt)), longestopt)
          longestopt = Builtins.size(Convert.to_string(opt))
        end
      end

      Builtins.foreach(opts) do |opt|
        op = Ops.get_map(allopts, opt, {})
        t = Ops.get_string(op, "type", "")
        if t != "regex" && t != "enum" && t != ""
          t = Ops.add(Ops.add("[", t), "]")
        elsif t == "enum"
          t = "[ "
          Builtins.foreach(Ops.get_list(op, "typespec", [])) do |s|
            t = Ops.add(Ops.add(t, s), " ")
          end
          t = Ops.add(t, "]")
        else
          t = "    "
        end
        if Ops.is_string?(opt)
          helptext = ""
          opthelp = Ops.get(op, "help")

          if Ops.is_string?(opthelp)
            helptext = Convert.to_string(opthelp)
          elsif Ops.is(opthelp, "map <string, string>")
            helptext = Ops.get(
              Convert.convert(
                opthelp,
                from: "any",
                to:   "map <string, string>"
              ),
              action,
              ""
            )
          elsif Ops.is(opthelp, "list <string>")
            delim = Builtins.sformat(
              "\n        %1  %2  ",
              String.Pad("", longestopt),
              String.Pad("", longestarg)
            )
            helptext = Builtins.mergestring(
              Convert.convert(opthelp, from: "any", to: "list <string>"),
              delim
            )
          else
            Builtins.y2error(
              "Invalid data type of help text, only 'string' or 'map<string,string>' types are allowed."
            )
          end

          Print(
            Builtins.sformat(
              "        %1  %2  %3",
              String.Pad(Convert.to_string(opt), longestopt),
              String.Pad(t, longestarg),
              helptext
            )
          )
        end
      end

      if has_string_option
        # additional help for using command line
        Print(
          _(
            "\n    Options of the [string] type must be written in the form 'option=value'."
          )
        )
      end
      if Builtins.haskey(command, "example")
        # translators: example title for command line
        Print(_("\n    Example:"))

        example = Ops.get(command, "example")

        if Ops.is_string?(example)
          Print(Builtins.sformat("        %1", example))
        elsif Ops.is(example, "list <string>")
          Builtins.foreach(
            Convert.convert(example, from: "any", to: "list <string>")
          ) { |e| Print(Builtins.sformat("        %1", e)) }
        else
          Builtins.y2error("Unsupported data type - value: %1", example)
        end
      end
      Print("")

      nil
    end

    # Print a general help - list of available command.
    def PrintGeneralHelp
      # display custom defined help instead of generic one
      if Builtins.haskey(@modulecommands, "customhelp")
        Print(Ops.get_string(@modulecommands, "customhelp", ""))
        return
      end

      # translators: default module description if none is provided by the module itself
      Print(
        Ops.add(
          Ops.get_locale(@modulecommands, "help", _("This is a YaST module.")),
          "\n"
        )
      )
      # translators: short help title for command line
      Print(_("Basic Syntax:"))

      if @interactive
        # translators: module command line help
        # translate <command> and [options] only!
        Print(_("    <command> [options]"))
        # translators: module command line help
        # translate <command> only!
        Print(_("    <command> help"))
        # translators: module command line help
        Print("    help")
        Print("    longhelp")
        Print("    xmlhelp")
        Print("")
        Print("    exit")
        Print("    abort")
      else
        # translators: module command line help, %1 is the module name
        Print(
          Builtins.sformat(
            "    yast2 %1 interactive",
            Ops.get_string(@modulecommands, "id", "")
          )
        )

        # translators: module command line help, %1 is the module name
        # translate <command> and [options] only!
        Print(
          Builtins.sformat(
            _("    yast2 %1 <command> [verbose] [options]"),
            Ops.get_string(@modulecommands, "id", "")
          )
        )
        # translators: module command line help, %1 is the module name
        Print(
          Builtins.sformat(
            "    yast2 %1 help",
            Ops.get_string(@modulecommands, "id", "")
          )
        )
        Print(
          Builtins.sformat(
            "    yast2 %1 longhelp",
            Ops.get_string(@modulecommands, "id", "")
          )
        )
        Print(
          Builtins.sformat(
            "    yast2 %1 xmlhelp",
            Ops.get_string(@modulecommands, "id", "")
          )
        )
        # translators: module command line help, %1 is the module name
        # translate <command> only!
        Print(
          Builtins.sformat(
            _("    yast2 %1 <command> help"),
            Ops.get_string(@modulecommands, "id", "")
          )
        )
      end

      Print("")
      # translators: command line title: list of available commands
      Print(_("Commands:"))

      longest = 0
      Builtins.foreach(Ops.get_map(@modulecommands, "actions", {})) do |action, _desc|
        longest = Builtins.size(action) if Ops.greater_than(Builtins.size(action), longest)
      end

      Builtins.maplist(Ops.get_map(@modulecommands, "actions", {})) do |cmd, desc|
        if !Builtins.haskey(desc, "help")
          # translators: error message: module does not provide any help messages
          Print(
            Builtins.sformat(
              "    %1  %2",
              String.Pad(cmd, longest),
              _("No help available.")
            )
          )
        end
        if Ops.is_string?(Ops.get(desc, "help"))
          Print(
            Builtins.sformat(
              "    %1  %2",
              String.Pad(cmd, longest),
              Ops.get_string(desc, "help", "")
            )
          )
        # multiline help text
        elsif Ops.is(Ops.get(desc, "help"), "list <string>")
          help = Ops.get_list(desc, "help", [])

          if Ops.greater_than(Builtins.size(help), 0)
            Print(
              Builtins.sformat(
                "    %1  %2",
                String.Pad(cmd, longest),
                Ops.get(help, 0, "")
              )
            )
            help = Builtins.remove(help, 0)
          end

          Builtins.foreach(help) do |h|
            Print(Builtins.sformat("    %1  %2", String.Pad("", longest), h))
          end
        else
          # fallback message - invalid help has been provided by the yast module
          Print(
            Builtins.sformat(
              "    %1  %2",
              String.Pad(cmd, longest),
              _("<Error: invalid help>")
            )
          )
        end
      end
      Print("")
      if !@interactive
        # translators: module command line help, %1 is the module name
        Print(
          Builtins.sformat(
            _("Run 'yast2 %1 <command> help' for a list of available options."),
            Ops.get_string(@modulecommands, "id", "")
          )
        )
        Print("")
      end

      nil
    end

    # Handle the system-wide commands, like help etc.
    #
    # @param [Hash] command  a map of the current command
    # @return    true, if the command was handled
    def ProcessSystemCommands(command)
      command = deep_copy(command)
      # handle help for specific command
      # this needs to be before general help, so "help help" works
      if Ops.get(command, ["options", "help"])
        PrintHead()
        PrintActionHelp(Ops.get_string(command, "command", ""))
        return true
      end

      # Process command "interactive"
      if Ops.get_string(command, "command", "") == "interactive"
        @interactive = true
        return true
      end

      # Process command "exit"
      if Ops.get_string(command, "command", "") == "exit"
        @done = true
        @aborted = false
        return true
      end

      # Process command "abort"
      if Ops.get_string(command, "command", "") == "abort"
        @done = true
        @aborted = true
        return true
      end

      if Ops.get_string(command, "command", "") == "help"
        # don't print header when custom help is defined
        PrintHead() if !Builtins.haskey(@modulecommands, "customhelp")
        PrintGeneralHelp()
        return true
      end

      if Ops.get_string(command, "command", "") == "longhelp"
        PrintHead()
        PrintGeneralHelp()
        Builtins.foreach(Ops.get_map(@allcommands, "actions", {})) do |action, _def|
          PrintActionHelp(action)
        end
        return true
      end

      if Ops.get_string(command, "command", "") == "xmlhelp"
        if Builtins.haskey(Ops.get_map(command, "options", {}), "xmlfile") == false
          # error message - command line option xmlfile is missing
          Print(
            _(
              "Target file name ('xmlfile' option) is missing. Use xmlfile=<target_XML_file> command line option."
            )
          )
          return false
        end

        xmlfilename = Ops.get_string(command, ["options", "xmlfile"], "")

        if xmlfilename.nil? || xmlfilename == ""
          # error message - command line option xmlfile is missing
          Print(
            _(
              "Target file name ('xmlfile' option) is empty. Use xmlfile=<target_XML_file> command line option."
            )
          )
          return false
        end

        doc = {}

        #      TODO: DTD specification
        Ops.set(
          doc,
          "listEntries",
          "commands" => "command",
          "options"  => "option",
          "examples" => "example"
        )
        #      doc["cdataSections"] = [];
        Ops.set(
          doc,
          "systemID",
          Ops.add(Directory.schemadir, "/commandline.dtd")
        )
        #      doc["nameSpace"] = "http://www.suse.com/1.0/yast2ns";
        Ops.set(doc, "typeNamespace", "http://www.suse.com/1.0/configns")

        Ops.set(doc, "rootElement", "commandline")
        XML.xmlCreateDoc(:xmlhelp, doc)

        exportmap = {}
        commands = []

        actions = Ops.get_map(@cmdlinespec, "actions", {})
        mappings = Ops.get_map(@cmdlinespec, "mappings", {})
        options = Ops.get_map(@cmdlinespec, "options", {})

        Builtins.y2debug("cmdlinespec: %1", @cmdlinespec)

        Builtins.foreach(actions) do |action, description|
          help = ""
          # help text might be a simple string or a multiline text (list<string>)
          help_value = Ops.get(description, "help")
          if Ops.is_string?(help_value)
            help = Convert.to_string(help_value)
          elsif Ops.is(help_value, "list <string>")
            help = Builtins.mergestring(
              Convert.convert(
                help_value,
                from: "any",
                to:   "list <string>"
              ),
              "\n"
            )
          else
            Builtins.y2error(
              "Unsupported data type for 'help' key: %1, use 'string' or 'list<string>' type!",
              help_value
            )
          end
          opts = []
          Builtins.foreach(Ops.get(mappings, action, [])) do |option|
            optn = {
              "name" => option,
              "help" => Ops.get_string(options, [option, "help"], "")
            }
            # add type specification if it's present
            if Ops.get_string(options, [option, "type"], "") != ""
              optn = Builtins.add(
                optn,
                "type",
                Ops.get_string(options, [option, "type"], "")
              )
            end
            opts = Builtins.add(opts, optn)
          end
          actiondescr = { "help" => help, "name" => action, "options" => opts }
          # add example if it's present
          if Builtins.haskey(Ops.get(actions, action, {}), "example")
            example = Ops.get(actions, [action, "example"])
            examples = Array(example)
            actiondescr = Builtins.add(actiondescr, "examples", examples)
          end
          commands = Builtins.add(commands, actiondescr)
        end

        Ops.set(exportmap, "commands", commands)
        Ops.set(exportmap, "module", Ops.get_string(@cmdlinespec, "id", ""))

        begin
          XML.YCPToXMLFile(:xmlhelp, exportmap, xmlfilename)
        rescue XMLSerializationError => e
          # error message - creation of xml failed
          Print(
            _("Failed to create XML file.")
          )
          Builtins.y2error("Failed to serialize xml help: #{e.inspect}")
          return false
        end

        Builtins.y2milestone("exported XML map: %1", exportmap)
        return true
      end

      false
    end

    #  Initialize Module
    #
    #  Initialize the module, setup the command line syntax and arguments passed on the command line.
    #
    #  @param [Hash] cmdlineinfo    the map describing the module command line
    #  @param [Array] args      arguments given by the user on the command line
    #  @return [Boolean]    true, if there are some commands to be processed (and cmdlineinfo passes sanity checks)
    #  @see #Command
    def Init(cmdlineinfo, args)
      cmdlineinfo = deep_copy(cmdlineinfo)
      args = deep_copy(args)
      # remember the command line specification
      # required later by xmlhelp command
      @cmdlinespec = deep_copy(cmdlineinfo)

      cmdline_supported = true

      # check whether the command line mode is really supported by the module
      if !Builtins.haskey(cmdlineinfo, "actions") ||
          Builtins.size(Ops.get_map(cmdlineinfo, "actions", {})) == 0
        cmdline_supported = false
      end

      # initialize verbose flag
      @verbose = Builtins.contains(WFM.Args, "verbose")

      id_string = Ops.get_string(cmdlineinfo, "id", "")
      # sanity checks on cmdlineinfo
      # check for id string , it must exist, and non-empty
      if cmdline_supported && (id_string == "" || !Ops.is_string?(id_string))
        Builtins.y2error("Command line specification does not define module id")

        # use 'unknown' as id
        cmdlineinfo = Builtins.remove(cmdlineinfo, "id") if Builtins.haskey(cmdlineinfo, "id")

        # translators: fallback name for a module at command line
        cmdlineinfo = Builtins.add(cmdlineinfo, "id", _("unknown"))

        # it's better to abort now
        @done = true
        @aborted = true
      end

      # check for helps, they are required everywhere
      # global help text
      if cmdline_supported && !Builtins.haskey(cmdlineinfo, "help")
        Builtins.y2error(
          "Command line specification does not define global help for the module"
        )

        # it's better to abort now
        @done = true
        @aborted = true
      end

      # help texts for actions
      if Builtins.haskey(cmdlineinfo, "actions")
        Builtins.foreach(Ops.get_map(cmdlineinfo, "actions", {})) do |action, def_|
          if !Builtins.haskey(def_, "help")
            Builtins.y2error(
              "Command line specification does not define help for action '%1'",
              action
            )

            # it's better to abort now
            @done = true
            @aborted = true
          end
        end
      end

      # help for options
      if Builtins.haskey(cmdlineinfo, "options")
        Builtins.foreach(Ops.get_map(cmdlineinfo, "options", {})) do |option, def_|
          if !Builtins.haskey(def_, "help")
            Builtins.y2error(
              "Command line specification does not define help for option '%1'",
              option
            )

            # it's better to abort now
            @done = true
            @aborted = true
          end
          # check that regex and enum have defined typespec
          if (Ops.get_string(def_, "type", "") == "regex" ||
              Ops.get_string(def_, "type", "") == "enum") &&
              !Builtins.haskey(def_, "typespec")
            Builtins.y2error(
              "Command line specification does not define typespec for option '%1'",
              option
            )

            # it's better to abort now
            @done = true
            @aborted = true
          end
        end
      end

      # mappings - check for existing actions and options
      if Builtins.haskey(cmdlineinfo, "mappings")
        Builtins.foreach(Ops.get_map(cmdlineinfo, "mappings", {})) do |mapaction, def_|
          # is this action defined?
          if !Builtins.haskey(
            Ops.get_map(cmdlineinfo, "actions", {}),
            mapaction
          )
            Builtins.y2error(
              "Command line specification maps undefined action '%1'",
              mapaction
            )

            # it's better to abort now
            @done = true
            @aborted = true
          end
          Builtins.foreach(def_) do |mapopt|
            next if !Ops.is_string?(mapopt)

            # is this option defined?
            if !Builtins.haskey(
              Ops.get_map(cmdlineinfo, "options", {}),
              Convert.to_string(mapopt)
            )
              Builtins.y2error(
                "Command line specification maps undefined option '%1' for action '%2'",
                mapopt,
                mapaction
              )

              # it's better to abort now
              @done = true
              @aborted = true
            end
          end
        end
      end

      return false if @done

      @modulecommands = deep_copy(cmdlineinfo)

      # build allcommands - help and verbose options are added specially
      @allcommands = {
        "actions"  => Builtins.union(
          Ops.get_map(@modulecommands, "actions", {}),
          Ops.get(@systemcommands, "actions", {})
        ),
        "options"  => Builtins.union(
          Ops.get_map(@modulecommands, "options", {}),
          Ops.get(@systemcommands, "options", {})
        ),
        "mappings" => Builtins.union(
          Builtins.mapmap(Ops.get_map(@modulecommands, "mappings", {})) do |act, opts|
            { act => Builtins.union(opts, ["help", "verbose"]) }
          end,
          Ops.get(@systemcommands, "mappings", {})
        )
      }

      if Ops.less_than(Builtins.size(args), 1) || Stage.stage != "normal" ||
          Stage.firstboot
        Mode.SetUI("dialog")
        # start GUI, module has some work to do :-)
        return true
      else
        Mode.SetUI("commandline")
      end

      if !cmdline_supported
        # command line is not supported
        Print(
          String.UnderlinedHeader(
            Ops.add("YaST2 ", Ops.get_string(cmdlineinfo, "id", "")),
            0
          )
        )
        Print("")

        help = Ops.get_string(cmdlineinfo, "help", "")
        if !help.nil? && help != ""
          Print(Ops.get_string(cmdlineinfo, "help", ""))
          Print("")
        end

        Print(@nosupport)
        Print("")
        return false
      end

      # setup prompt
      @cmdlineprompt = Ops.add(
        Ops.add("YaST2 ", Ops.get_string(cmdlineinfo, "id", "")),
        "> "
      )
      SCR.Write(path(".dev.tty.prompt"), @cmdlineprompt)

      # parse args
      @commandcache = Parse(args)

      # return true, if there is some work to do:
      # first, try to interpret system commands
      if ProcessSystemCommands(@commandcache)
        # it was system command, there is work only in interactive mode
        @commandcache = {}
        @done = !@interactive
        @aborted = false
        @interactive
      else
        # we cannot handle this on our own, return true if there is some command to be processed
        # i.e, there is no parsing error
        @done = Builtins.size(@commandcache) == 0
        @aborted = @done
        !@done
      end
    end

    # Scan a command line from stdin, return it split into a list
    #
    # @return [Array<String>] the list of command line parts, nil for end of file
    def Scan
      res = Convert.to_string(SCR.Read(path(".dev.tty")))
      return nil if res.nil?

      String.ParseOptions(res, "separator" => " ")
    end

    # Set prompt and read input from command line
    # @param [String] prompt Set prompt
    # @param [Symbol] type Type
    # @return [String] Entered string
    def GetInput(prompt, type)
      # set the required prompt
      SCR.Write(path(".dev.tty.prompt"), prompt)

      res = case type
      when :nohistory
        Convert.to_string(SCR.Read(path(".dev.tty.nohistory")))
      when :noecho
        Convert.to_string(SCR.Read(path(".dev.tty.noecho")))
      else
        Convert.to_string(SCR.Read(path(".dev.tty")))
      end

      # set the default prompt
      SCR.Write(path(".dev.tty.prompt"), @cmdlineprompt)

      res
    end

    # Read input from command line
    # @param [String] prompt Set prompt to this value
    # @return [String] Entered string
    def UserInput(prompt)
      GetInput(prompt, :nohistory)
    end

    # Read input from command line
    #
    # Read input from command line, input is not displayed and not stored in
    # the command line history. This function should be used for reading a password.
    # @param [String] prompt Set prompt to this value
    # @return [String] Entered string
    def PasswordInput(prompt)
      GetInput(prompt, :noecho)
    end

    #  Get next user-given command
    #
    #  Get next user-given command. If there is a command available, returns it, otherwise ask
    #  the user for a command (in interactive mode). Also processes system commands.
    #
    #  @return [Hash] of the new command. If there are no more commands, it returns exit or abort depending
    #  on the result user asked for.
    #
    #  @see #Parse
    def Command
      # if we are done already, return the result
      return { "command" => @aborted ? "abort" : "exit" } if @done

      # there is a command in the cache
      if Builtins.size(@commandcache) != 0
        result = deep_copy(@commandcache)
        @commandcache = {}
        @done = !@interactive
        deep_copy(result)
      # if in interactive mode, ask user for input
      elsif @interactive
        loop do
          newcommand = []
          newcommand = Scan() while Builtins.size(newcommand) == 0

          # EOF reached
          if newcommand.nil?
            @done = true
            return { "command" => "exit" }
          end

          @commandcache = Parse(newcommand)
          break if !ProcessSystemCommands(@commandcache)
          break if @done
        end

        return { "command" => @aborted ? "abort" : "exit" } if @done

        # we are not done, return the command asked back to module
        result = deep_copy(@commandcache)
        @commandcache = {}

        deep_copy(result)
      else
        # there is no further commands left
        @done = true
        { "command" => "exit" }
      end
    end

    #  Should module start UI?
    #
    #  @return [Boolean] true, if the user asked for standard UI (no parameter was passed by command line)
    def StartGUI
      !Mode.commandline
    end

    #  Is module started in interactive command-line mode?
    #
    #  @return [Boolean] true, if the user asked for interactive command-line mode
    def Interactive
      @interactive
    end

    #  User asked for abort (forgetting the changes)
    #
    #  @return [Boolean] true, if the user asked abort
    def Aborted
      @aborted
    end

    # Abort the command line handling
    def Abort
      @aborted = true
      @done = true

      nil
    end

    #  Are there some commands to be processed?
    #
    #  @return [Boolean] true, if there is no more commands to be processed, either because the user
    #  used command line, or the interactive mode was finished
    def Done
      @done
    end

    # Check uniqueness of an option
    #
    # Check uniqueness of an option. Simply pass the list of user-specified
    # options and a list of mutually exclusive options. In case of
    # error, Report::Error is used.
    #
    # @param [Hash{String => String}] options  options specified by the user on the command line to be checked
    # @param [Array] unique_options  list of mutually exclusive options to check against
    # @return  nil if there is a problem, otherwise the unique option found
    def UniqueOption(options, unique_options)
      return nil if options.nil? || unique_options.nil?

      # sanity check
      if unique_options.empty?
        log.error "Unique list of options required, but the list of the possible options is empty"
        return nil
      end

      # first do a filtering, then convert to a list of keys
      cmds = unique_options & options.keys

      # if it is OK, quickly return
      return cmds.first if cmds.size == 1

      msg = if cmds.empty?
        if unique_options.size == 1
          # translators: error message - missing unique command for command line execution
          Builtins.sformat(_("Specify the command '%1'."), unique_options.first)
        else
          # translators: error message - missing unique command for command line execution
          Builtins.sformat(_("Specify one of the commands: %1."), format_list(unique_options))
        end
      else
        Builtins.sformat(_("Specify only one of the commands: %1."), format_list(cmds))
      end

      Report.Error(msg)
      nil
    end

    # Parse the Command Line
    #
    # Function to parse the command line, start a GUI or handle interactive and
    # command line actions as supported by the {Yast::CommandLine} module.
    #
    # @param [Hash] commandline  a map used in the CommandLine module with information
    #                      about the handlers for GUI and commands.
    # @option commandline [String] "help" global help text.
    #   Help for options and actions are separated. Mandatory if module support command line.
    # @option commandline [String] "id" module id. Mandatory if module support command line.
    # @option commandline [Yast::FunRef("symbol ()")|Yast::FunRef("boolean ()")] "guihandler"
    #   function to be called when gui requested. Mandatory for modules with GUI.
    # @option commandline [Yast::FunRef("boolean ()")] "initialize" function that is called before
    #   any action handler is called. Usually module initialization happens there.
    # @option commandline [Yast::FunRef("boolean ()")] "finish" function that is called after
    #   all action handlers are called. Usually writing of changes happens there. NOTE: calling
    #   is skipped if all called handlers are readonly.
    # @option commandline [Hash<String, Object>] "actions" definition of actions. Hash has action
    #   name as key and value is hash with following keys:
    #
    #      - **"help"** _String|Array<String>_ mandatory action specific help text.
    #        Options help text is defined separately. If array is passed it will be
    #        printed indended on multiple lines.
    #      - **"handler"** _Yast::FunRef("boolean (map <string, string>)")_ handler when action is
    #        specified. Parameter is passed options. Mandatory.
    #      - **"example"** _String_ optional example of action invocation.
    #        By default no example is provided.
    #      - **"options"** _Array<String>_ optional list of flags. So far only `"non_strict"`
    #        supported. Useful when action arguments is not well defined, so unknown option does
    #        not finish with error. By default it is empty array.
    #      - **"readonly"** _Boolean_ optional flag that if it is set to true then
    #        invoking action is not reason to run finish handler, but if another
    #        action without readonly is called, it will run finish handler.
    #        Default value is `false`.
    #
    # @option commandline [Hash<String, Object>] "options" definition of options. Hash has action
    #   name as key and value is hash with following keys:
    #
    #      - **"help"** _String|Array<String>_ mandatory action specific help text.
    #        If array is passed it will be printed indended on multiple lines.
    #      - **"type"** _String_ optional type check for option parameter. By default no
    #        type checking is done. It aborts if no checking is done and a value is passed on CLI.
    #        Possible values are ycp types and additionally enum and regex. For enum additional
    #        key **"typespec"** with array of values have to be specified. For regex additional
    #        key **"typespec"** with string containing ycp regexp is required. For integer it
    #        does conversion of a string value to an integer value.
    #      - **"typespec"** _Object_ additional type specification. See **"type"** for details.
    #
    # @option commandline [Hash<String, Array<String>>] "mappings" defines connection between
    #   **"actions"** and its **"options"**. The key is action and the value is a list of options it
    #   supports.
    # @return [Object] false if there was an error or there are no changes to be written (for example "help").
    #      true if the changes should be written, or a value returned by the
    #      handler. Actions that are read-only return also true on success even if there is nothing to write.
    #
    # @example Complete CLI support. Methods definition are skipped for simplicity.
    #   Yast::CommandLine.Run(
    #     "help"       => _("Foo Configuration"),
    #     "id"         => "foo",
    #     "guihandler" => fun_ref(method(:FooSequence), "symbol ()"),
    #     "initialize" => fun_ref(Foo.method(:ReadNoGUI), "boolean ()"),
    #     "finish"     => fun_ref(Foo.method(:WriteNoGUI), "boolean ()"),
    #     "actions"    => {
    #       "list"   => {
    #         "help"     => _(
    #           "Display configuration summary"
    #           ),
    #         "example"  => "foo list configured",
    #         "readonly" => true,
    #         "handler"  => fun_ref(
    #           method(:ListHandler),
    #           "boolean (map <string, string>)"
    #         )
    #       },
    #       "edit"   => {
    #         "help"    => _("Change existing configuration"),
    #         "handler" => fun_ref(
    #           method(:EditHandler),
    #           "boolean (map <string, string>)"
    #         )
    #       },
    #     },
    #     "options"    => {
    #       "configured"   => {
    #         "help" => _("List only configured foo fighters")
    #       }
    #     },
    #     "mappings"   => {
    #       "list"   => ["configured"]
    #     }
    #   )
    def Run(commandline)
      commandline = deep_copy(commandline)
      # The main ()
      Builtins.y2milestone("----------------------------------------")
      Builtins.y2milestone("Command line interface started")

      # Initialize the arguments
      @done = false
      return !Aborted() if !Init(commandline, WFM.Args)

      ret = true
      # no action is readonly, but the first module without "readonly" will switch the flag to `false`
      read_only = true

      initialized = false
      if Ops.get(commandline, "initialize").nil?
        # no initialization routine
        # set initialized state to true => call finish handler at the end in command line mode
        initialized = true
      end

      # Start GUI
      if StartGUI()
        if !Builtins.haskey(commandline, "guihandler")
          Builtins.y2error(
            "Missing GUI handler for %1",
            Ops.get_string(commandline, "id", "<unknown>")
          )
          # translators: error message - the module does not provide command line interface
          Error(_("There is no user interface available for this module."))
          return false
        end

        if Ops.is(Ops.get(commandline, "guihandler"), "symbol ()")
          exec = Convert.convert(
            Ops.get(commandline, "guihandler"),
            from: "any",
            to:   "symbol ()"
          )
          symbol_ret = exec.call
          Builtins.y2debug("GUI handler ret=%1", symbol_ret)
          return symbol_ret
        else
          exec = Convert.convert(
            Ops.get(
              commandline,
              "guihandler",
              fun_ref(method(:fake_false), "boolean ()")
            ),
            from: "any",
            to:   "boolean ()"
          )
          ret = exec.call
          Builtins.y2debug("GUI handler ret=%1", ret)
          return ret
        end
      else
        # translators: progress message - command line interface ready
        PrintVerbose(_("Ready"))

        until Done()
          m = Command()
          command = Ops.get_string(m, "command", "exit")
          options = Ops.get_map(m, "options", {})

          # start initialization code if it wasn't already used
          if !initialized && (Builtins.haskey(Ops.get_map(commandline, "actions", {}), command) &&
                Ops.get(commandline, "initialize"))
            # non-GUI handling
            PrintVerbose(_("Initializing"))
            ret2 = commandline["initialize"].call
            if ret2
              initialized = true
            else
              Builtins.y2milestone("Module initialization failed")
              return false
            end
          end

          exec = Convert.convert(
            Ops.get(commandline, ["actions", command, "handler"]),
            from: "any",
            to:   "boolean (map <string, string>)"
          )

          # there is a handler, execute the action
          if !exec.nil?
            res = exec.call(options)
            # unless an action explicitly mentions that it is read-only it will run the finish handler
            read_only = false unless commandline["actions"][command]["readonly"]

            # if it is not interactive, abort on errors
            Abort() if !Interactive() && res == false
          elsif !Done()
            Builtins.y2error("Unknown command '%1' from CommandLine", command)
            next
          end
        end

        ret = !Aborted()
      end

      if ret && Ops.get(commandline, "finish") && initialized && !read_only
        # translators: Progress message - the command line interface is about to finish
        PrintVerbose(_("Finishing"))
        ret = commandline["finish"].call
        if !ret
          Builtins.y2milestone("Module finishing failed")
          return false
        end
        # translators: The command line interface is finished
        PrintVerbose(_("Done"))
      else
        # translators: The command line interface is finished without writing the changes
        PrintVerbose(_("Quitting (without changes)"))
      end

      Builtins.y2milestone("Commandline interface finished")
      Builtins.y2milestone("----------------------------------------")

      ret
    end

    # Ask user, commandline equivalent of Popup::YesNo()
    # @return [Boolean] true if user entered "yes"
    def YesNo
      # prompt message displayed in the commandline mode
      # when user is asked to replay "yes" or "no" (localized)
      prompt = _("yes or no?")

      ui = UserInput(prompt)

      # yes - used in the command line mode as input text for yes/no confirmation
      yes = _("yes")

      # no - used in the command line mode as input text for yes/no confirmation
      no = _("no")

      ui = UserInput(prompt) while ui != yes && ui != no

      ui == yes
    end

    # Return verbose flag
    # boolean verbose flag
    def Verbose
      @verbose
    end

    publish variable: :cmdlineprompt, type: "string", private: true
    publish variable: :systemcommands, type: "map <string, map <string, any>>", private: true
    publish variable: :modulecommands, type: "map", private: true
    publish variable: :allcommands, type: "map", private: true
    publish variable: :interactive, type: "boolean", private: true
    publish variable: :done, type: "boolean", private: true
    publish variable: :aborted, type: "boolean", private: true
    publish variable: :commandcache, type: "map <string, any>", private: true
    publish variable: :verbose, type: "boolean", private: true
    publish variable: :cmdlinespec, type: "map", private: true
    publish variable: :nosupport, type: "string", private: true
    publish function: :PrintInternal, type: "void (string, boolean)", private: true
    publish function: :Print, type: "void (string)"
    publish function: :PrintNoCR, type: "void (string)"
    publish function: :PrintVerbose, type: "void (string)"
    publish function: :PrintVerboseNoCR, type: "void (string)"
    publish function: :PrintTable, type: "void (term, list <term>)"
    publish function: :Error, type: "void (string)"
    publish function: :Parse, type: "map <string, any> (list)"
    publish function: :PrintHead, type: "void ()", private: true
    publish function: :PrintActionHelp, type: "void (string)", private: true
    publish function: :PrintGeneralHelp, type: "void ()", private: true
    publish function: :ProcessSystemCommands, type: "boolean (map)", private: true
    publish function: :Init, type: "boolean (map, list)"
    publish function: :Scan, type: "list <string> ()"
    publish function: :GetInput, type: "string (string, symbol)", private: true
    publish function: :UserInput, type: "string (string)"
    publish function: :PasswordInput, type: "string (string)"
    publish function: :Command, type: "map ()"
    publish function: :StartGUI, type: "boolean ()"
    publish function: :Interactive, type: "boolean ()"
    publish function: :Aborted, type: "boolean ()"
    publish function: :Abort, type: "void ()"
    publish function: :Done, type: "boolean ()"
    publish function: :UniqueOption, type: "string (map <string, string>, list)"
    publish function: :Run, type: "any (map)"
    publish function: :YesNo, type: "boolean ()"
    publish function: :Verbose, type: "boolean ()"

  private

    def format_list(list)
      # translators: the last entry in output of list
      list[0..-2].map { |l| "'#{l}'" }.join(", ") + " " +
        Builtins.sformat(_("or '%1'"), list[-1])
    end
  end

  CommandLine = CommandLineClass.new
  CommandLine.main
end