• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yast / yast-yast2 / 13440235285

20 Feb 2025 04:40PM UTC coverage: 41.869% (-0.02%) from 41.889%
13440235285

push

github

web-flow
Merge pull request #1316 from yast/agama_kernel_conf

Respect Agama kernel parameters

2 of 4 new or added lines in 1 file covered. (50.0%)

265 existing lines in 40 files now uncovered.

12605 of 30106 relevant lines covered (41.87%)

10.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.33
/library/commandline/src/modules/CommandLine.rb
1
# ***************************************************************************
2
#
3
# Copyright (c) 2002 - 2012 Novell, Inc.
4
# All Rights Reserved.
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of version 2 of the GNU General Public License as
8
# published by the Free Software Foundation.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, contact Novell, Inc.
17
#
18
# To contact Novell about this file by physical or electronic mail,
19
# you may find current contact information at www.novell.com
20
#
21
# ***************************************************************************
22
# File:  modules/CommandLine.ycp
23
# Package:  yast2
24
# Summary:  Command line interface for YaST2 modules
25
# Author:  Stanislav Visnovsky <visnov@suse.cz>
26
#
27
# $Id$
28
require "yast"
1✔
29

30
module Yast
1✔
31
  class CommandLineClass < Module
1✔
32
    include Yast::Logger
1✔
33

34
    def main
1✔
35
      Yast.import "Directory"
12✔
36
      Yast.import "Mode"
12✔
37
      Yast.import "Popup"
12✔
38
      Yast.import "Report"
12✔
39
      Yast.import "Stage"
12✔
40
      Yast.import "String"
12✔
41
      Yast.import "Integer"
12✔
42
      Yast.import "TypeRepository"
12✔
43
      Yast.import "XML"
12✔
44

45
      textdomain "base"
12✔
46

47
      @cmdlineprompt = "YaST2 > "
12✔
48

49
      # Map of commands for every module. ATM the list of commands this module handles internally.
50
      @systemcommands = {
51
        "actions"  => {
12✔
52
          "help"        => {
53
            # translators: help for 'help' option on command line
54
            "help"     => _(
55
              "Print the help for this module"
56
            ),
57
            "readonly" => true
58
          },
59
          "longhelp"    => {
60
            # translators: help for 'longhelp' option on command line
61
            "help"     => _(
62
              "Print a long version of help for this module"
63
            ),
64
            "readonly" => true
65
          },
66
          "xmlhelp"     => {
67
            # translators: help for 'xmlhelp' option on command line
68
            "help"     => _(
69
              "Print a long version of help for this module in XML format"
70
            ),
71
            "readonly" => true
72
          },
73
          "interactive" => {
74
            # translators: help for 'interactive' option on command line
75
            "help"     => _(
76
              "Start interactive shell to control the module"
77
            ),
78
            # interactive mode itself does not mean that write is needed.
79
            # The first action that is not readonly will switch flag.
80
            "readonly" => true
81
          },
82
          "exit"        => {
83
            # translators: help for 'exit' command line interactive mode
84
            "help"     => _(
85
              "Exit interactive mode and save the changes"
86
            ),
87
            "readonly" => true
88
          },
89
          "abort"       => {
90
            # translators: help for 'abort' command line interactive mode
91
            "help"     => _(
92
              "Abort interactive mode without saving the changes"
93
            ),
94
            "readonly" => true
95
          }
96
        },
97
        "options"  => {
98
          "help"    => {
99
            # translators:  command line "help" option
100
            "help" => _(
101
              "Print the help for this command"
102
            )
103
          },
104
          "verbose" => {
105
            # translators: command line "verbose" option
106
            "help" => _(
107
              "Show progress information"
108
            )
109
          },
110
          "xmlfile" => {
111
            # translators: command line "xmlfile" option
112
            "help" => _(
113
              "Where to store the XML output"
114
            ),
115
            "type" => "string"
116
          }
117
        },
118
        "mappings" => {
119
          "help"        => ["help", "verbose"],
120
          "xmlhelp"     => ["help", "verbose", "xmlfile"],
121
          "interactive" => ["help", "verbose"],
122
          "exit"        => ["help"],
123
          "abort"       => ["help"]
124
        }
125
      }
126

127
      # Map of commands defined by the YaST2 module.
128
      @modulecommands = {}
12✔
129

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

133
      # User asked for interactive session
134
      @interactive = false
12✔
135

136
      # All commands have been processed
137
      @done = false
12✔
138

139
      # User asked for quitting of interactive session, or there was an error
140
      @aborted = false
12✔
141

142
      # a cache for already parsed but not processed command
143
      @commandcache = {}
12✔
144

145
      # Verbose mode flag
146
      @verbose = false
12✔
147

148
      # Remember the command line specification for later use
149
      @cmdlinespec = {}
12✔
150

151
      # string: command line interface is not supported
152
      @nosupport = _(
12✔
153
        "This YaST2 module does not support the command line interface."
154
      )
155
    end
156

157
    #  Print a String
158
    #
159
    #  Print a string to /dev/tty in interactive mode, to stderr in non-interactive
160
    #  Suppress printing if there are no commands to be handled (starting GUI)
161
    #
162
    #  @param [String] string to be printed
163
    #  @param [Boolean] newline if newline character should be added or not
164
    def PrintInternal(string, newline)
1✔
165
      return if !Mode.commandline
×
166

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

173
      if @interactive
×
174
        if newline
×
175
          SCR.Write(path(".dev.tty"), string)
×
176
        else
177
          SCR.Write(path(".dev.tty.nocr"), string)
×
178
        end
179
      elsif newline
×
180
        SCR.Write(path(".dev.tty.stderr"), string)
×
181
      else
182
        SCR.Write(path(".dev.tty.stderr_nocr"), string)
×
183
      end
184

UNCOV
185
      nil
×
186
    end
187

188
    #  Print a String
189
    #
190
    #  Print a string to /dev/tty in interactive mode, to stderr in non-interactive
191
    #  Suppress printing if there are no commands to be handled (starting GUI)
192
    #
193
    #  @param [String] string to be printed
194
    def Print(string)
1✔
195
      PrintInternal(string, true)
×
196
    end
197

198
    #  Print a String, don't add a trailing newline character
199
    #
200
    #  Print a string to /dev/tty in interactive mode, to stderr in non-interactive
201
    #  Suppress printing if there are no commands to be handled (starting GUI)
202
    #
203
    #  @param [String] string to be printed
204
    def PrintNoCR(string)
1✔
205
      PrintInternal(string, false)
×
206
    end
207

208
    # Same as Print(), but the string is printed only when verbose command
209
    # line mode was activated
210
    # @param [String] string to print
211
    def PrintVerbose(string)
1✔
212
      Print(string) if @verbose
5✔
213

214
      nil
5✔
215
    end
216

217
    # Same as PrintNoCR(), but the string is printed only when verbose command
218
    # line mode was activated
219
    # @param [String] string to print
220
    def PrintVerboseNoCR(string)
1✔
221
      PrintNoCR(string) if @verbose
×
222

UNCOV
223
      nil
×
224
    end
225

226
    #  Print a Table
227
    #
228
    #  Print a table using Print(). Format of table is as libyui but not all features
229
    #  are supported, e.g. no icons.
230
    #
231
    #  @param [Yast::Term] header  header of table in libyui format
232
    #  @param [Array<Yast::Term>] content  content of table in libyui format
233
    def PrintTable(header, content)
1✔
234
      header = deep_copy(header)
×
235
      content = deep_copy(content)
×
236
      aligns = []
×
237
      widths = []
×
238

239
      process = lambda do |line|
×
240
        line = deep_copy(line)
×
241
        ret = []
×
242
        anys = Builtins.argsof(line)
×
243
        Builtins.foreach(anys) do |a|
×
244
          if Ops.is_string?(a)
×
245
            s = Convert.to_string(a)
×
246
            ret = Builtins.add(ret, s)
×
247
          elsif Ops.is_term?(a)
×
248
            t = Convert.to_term(a)
×
249
            ret = Builtins.add(ret, Ops.get_string(Builtins.argsof(t), 0, "")) if Builtins.contains([:Left, :Center, :Right], Builtins.symbolof(t))
×
250
          end
251
        end
252
        deep_copy(ret)
×
253
      end
254

255
      get_aligns = lambda do |header2|
×
256
        header2 = deep_copy(header2)
×
257
        anys = Builtins.argsof(header2)
×
258
        Builtins.foreach(Integer.Range(Builtins.size(anys))) do |i|
×
259
          a = Ops.get(anys, i)
×
260
          if Ops.is_term?(a)
×
261
            t = Convert.to_term(a)
×
262
            Ops.set(aligns, i, :right) if Builtins.symbolof(t) == :Right
×
263
          end
264
        end
265

266
        nil
×
267
      end
268

269
      update_widths = lambda do |columns|
×
270
        columns = deep_copy(columns)
×
271
        Builtins.foreach(Integer.Range(Builtins.size(columns))) do |i|
×
272
          Ops.set(
×
273
            widths,
274
            i,
275
            Integer.Max(
276
              [Ops.get(widths, i, 0), Builtins.size(Ops.get(columns, i, ""))]
277
            )
278
          )
279
        end
280

281
        nil
×
282
      end
283

284
      print_line = lambda do |line|
×
285
        line = deep_copy(line)
×
286
        columns = process.call(line)
×
287
        Builtins.foreach(Integer.Range(Builtins.size(columns))) do |i|
×
288
          Ops.set(
×
289
            columns,
290
            i,
291
            String.SuperPad(
292
              Ops.get(columns, i, ""),
293
              Ops.get(widths, i, 0),
294
              " ",
295
              Ops.get(aligns, i, :left)
296
            )
297
          )
298
        end
299
        Print(Builtins.mergestring(columns, " | "))
×
300

301
        nil
×
302
      end
303

304
      update_widths.call(process.call(header))
×
305
      Builtins.foreach(content) { |row| update_widths.call(process.call(row)) }
×
306

307
      print_line.call(header)
×
308

309
      get_aligns.call(header)
×
310

311
      Print(Builtins.mergestring(Builtins.maplist(widths) do |width|
×
312
        String.Repeat("-", width)
×
313
      end, "-+-"))
314

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

UNCOV
317
      nil
×
318
    end
319

320
    # Print an Error Message
321
    #
322
    # Print an error message and add the description how to get the help.
323
    # @param [String] message  error message to be printed. Use nil for no message
324
    def Error(message)
1✔
325
      Print(message) if !message.nil?
1✔
326

327
      if @interactive
1✔
328
        # translators: default error message for command line
329
        Print(_("Use 'help' for a complete list of available commands."))
×
330
      else
331
        # translators: default error message for command line
332
        Print(
1✔
333
          Builtins.sformat(
334
            _("Use 'yast2 %1 help' for a complete list of available commands."),
335
            Ops.get_string(@modulecommands, "id", "")
336
          )
337
        )
338
      end
339

340
      nil
1✔
341
    end
342

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

354
      # Parse command
355
      command = args.shift
3✔
356
      Builtins.y2debug("command=%1", command)
3✔
357
      Builtins.y2debug("args=%1", args)
3✔
358

359
      if command == ""
3✔
360
        Builtins.y2error(
×
361
          "CommandLine::Parse called with first parameter being empty. Arguments passed: %1",
362
          arguments
363
        )
364
        return {}
×
365
      end
366

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

372
        return {}
1✔
373
      end
374

375
      # build the list of options for the command
376
      opts = Ops.get_list(@allcommands, ["mappings", command], [])
2✔
377
      allopts = Ops.get_map(@allcommands, "options", {})
2✔
378
      cmdoptions = {}
2✔
379
      Builtins.maplist(opts) do |k|
2✔
380
        cmdoptions = Builtins.add(cmdoptions, k, Ops.get_map(allopts, k, {})) if Ops.is_string?(k)
3✔
381
      end
382

383
      ret = true
2✔
384

385
      # Parse options
386
      givenoptions = {}
2✔
387
      Builtins.maplist(args) do |aos|
2✔
388
        Builtins.y2debug("os=%1", aos)
1✔
389
        next if !Ops.is_string?(aos)
1✔
390

391
        os = Convert.to_string(aos)
1✔
392
        o = Builtins.regexptokenize(os, "([^=]+)=(.+)")
1✔
393
        Builtins.y2debug("o=%1", o)
1✔
394
        case Builtins.size(o)
1✔
395
        when 2
396
          givenoptions = Builtins.add(
1✔
397
            givenoptions,
398
            Ops.get(o, 0, ""),
399
            Ops.get(o, 1, "")
400
          )
401
        when 0
402
          # check, if the last character is "="
403
          # FIXME: consider whitespace
404
          if Builtins.substring(os, Ops.subtract(Builtins.size(os), 1)) == "="
×
405
            # translators: error message - user did not provide a value for option %1 on the command line
406
            Print(
×
407
              Builtins.sformat(
408
                _("Option '%1' is missing value."),
409
                Builtins.substring(os, 0, Ops.subtract(Builtins.size(os), 1))
410
              )
411
            )
412
            @aborted = true if !@interactive
×
413
            ret = false
×
414
            next {}
×
415
          else
416
            givenoptions = Builtins.add(givenoptions, os, "")
×
417
          end
418
        end
419
      end
420

421
      return {} if ret != true
2✔
422

423
      Builtins.y2debug("options=%1", givenoptions)
2✔
424

425
      # Check options
426

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

434
      # check (and convert data types)
435
      Builtins.maplist(givenoptions) do |o, val|
2✔
436
        v = Convert.to_string(val)
1✔
437
        next if ret != true
1✔
438

439
        if Ops.get(cmdoptions, o).nil?
1✔
440
          if !non_strict
×
441
            # translators: error message, %1 is a command, %2 is the wrong option given by the user
442
            Print(
×
443
              Builtins.sformat(
444
                _("Unknown option for command '%1': %2"),
445
                command,
446
                o
447
              )
448
            )
449
            @aborted = true if !@interactive
×
450
            ret = false
×
451
          end
452
        else
453
          # this option is valid, let's check the type
454

455
          opttype = Ops.get_string(cmdoptions, [o, "type"], "")
1✔
456

457
          if opttype != ""
1✔
458
            # need to check the type
459
            case opttype
1✔
460
            when "regex"
461
              opttypespec = Ops.get_string(cmdoptions, [o, "typespec"], "")
×
462
              ret = TypeRepository.regex_validator(opttypespec, v)
×
463
              if ret != true
×
464
                # translators: error message, %2 is the value given
465
                Print(
×
466
                  Builtins.sformat(_("Invalid value for option '%1': %2"), o, v)
467
                )
468
                @aborted = true if !@interactive
×
469
              end
470
            when "enum"
471
              ret = TypeRepository.enum_validator(
×
472
                Ops.get_list(cmdoptions, [o, "typespec"], []),
473
                v
474
              )
475
              if ret != true
×
476
                # translators: error message, %2 is the value given
477
                Print(
×
478
                  Builtins.sformat(_("Invalid value for option '%1': %2"), o, v)
479
                )
480
                @aborted = true if !@interactive
×
481
              end
482
            when "integer"
483
              i = Builtins.tointeger(v)
×
484
              ret = !i.nil?
×
485
              if ret == true
×
486
                # update value of the option to integer
487
                Ops.set(givenoptions, o, i)
×
488
              else
489
                # translators: error message, %2 is the value given
490
                Print(
×
491
                  Builtins.sformat(_("Invalid value for option '%1': %2"), o, v)
492
                )
493
                @aborted = true if !@interactive
×
494
              end
495
            else
496
              ret = (v == "") ? false : TypeRepository.is_a(v, opttype)
1✔
497

498
              if ret != true
1✔
499
                # translators: error message, %2 is expected type, %3 is the value given
500
                Print(
×
501
                  Builtins.sformat(
502
                    _(
503
                      "Invalid value for option '%1' -- expected '%2', received %3"
504
                    ),
505
                    o,
506
                    opttype,
507
                    v
508
                  )
509
                )
510
                @aborted = true if !@interactive
×
511
              end
512
            end
513
          # type is missing
514
          elsif v != ""
×
515
            Builtins.y2error(
×
516
              "Type specification for option '%1' is missing, cannot assign a value to the option",
517
              o
518
            )
519
            # translators: error message if option has a value, but cannot have one
520
            Print(
×
521
              Builtins.sformat(
522
                _("Option '%1' cannot have a value. Given value: %2"),
523
                o,
524
                v
525
              )
526
            )
527
            @aborted = true if !@interactive
×
528
            ret = false
×
529
          end
530
        end
531
      end
532

533
      # wrong, let's print the help message
534
      if ret != true
2✔
535
        if @interactive
×
536
          # translators: error message, how to get command line help for interactive mode
537
          # %1 is the module name, %2 is the action name
538
          Print(
×
539
            Builtins.sformat(
540
              _("Use '%1 %2 help' for a complete list of available options."),
541
              Ops.get_string(@modulecommands, "id", ""),
542
              command
543
            )
544
          )
545
        else
546
          # translators: error message, how to get command line help for non-interactive mode
547
          # %1 is the module name, %2 is the action name
548
          Print(
×
549
            Builtins.sformat(
550
              _(
551
                "Use 'yast2 %1 %2 help' for a complete list of available options."
552
              ),
553
              Ops.get_string(@modulecommands, "id", ""),
554
              command
555
            )
556
          )
557
        end
558
        return {}
×
559
      end
560

561
      { "command" => command, "options" => givenoptions }
2✔
562
    end
563

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

574
      Print(head)
1✔
575
    end
576

577
    # Print a help text for a given action.
578
    #
579
    # @param [String] action the action for which the help should be printed
580
    def PrintActionHelp(action)
1✔
581
      # lookup action in actions
582
      command = Ops.get_map(@allcommands, ["actions", action], {})
×
583
      # translators: the command does not provide any help
584
      commandhelp = Ops.get(command, "help")
×
585
      commandhelp = _("No help available") if commandhelp.nil?
×
586
      has_string_option = false
×
587
      # Process <command> "help"
588
      # translators: %1 is the command name
589
      Print(Builtins.sformat(_("Command '%1'"), action))
×
590

591
      # print help
592
      if Ops.is_string?(commandhelp)
×
593
        Print(Builtins.sformat("    %1", commandhelp))
×
594
      elsif Ops.is(commandhelp, "list <string>")
×
595
        Builtins.foreach(
×
596
          Convert.convert(commandhelp, from: "any", to: "list <string>")
597
        ) { |e| Print(Builtins.sformat("    %1", e)) }
×
598
      end
599

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

602
      # no options, skip the rest
603
      if Builtins.size(opts) == 0
×
604
        Print("")
×
605
        return
×
606
      end
607

608
      # translators: command line options
609
      Print(_("\n    Options:"))
×
610

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

613
      longestopt = 0
×
614
      longestarg = 0
×
615

616
      Builtins.foreach(opts) do |opt|
×
617
        op = Ops.get_map(allopts, opt, {})
×
618
        t = Ops.get_string(op, "type", "")
×
619
        has_string_option = true if t == "string"
×
620
        if t != "regex" && t != "enum" && t != ""
×
621
          t = Ops.add(Ops.add("[", t), "]")
×
622
        elsif t == "enum"
×
623
          t = "[ "
×
624
          Builtins.foreach(Ops.get_list(op, "typespec", [])) do |s|
×
625
            t = Ops.add(Ops.add(t, s), " ")
×
626
          end
627
          t = Ops.add(t, "]")
×
628
        end
629
        longestarg = Builtins.size(t) if Ops.greater_than(Builtins.size(t), longestarg)
×
630
        if Ops.is_string?(opt) &&
×
631
            Ops.greater_than(Builtins.size(Convert.to_string(opt)), longestopt)
632
          longestopt = Builtins.size(Convert.to_string(opt))
×
633
        end
634
      end
635

636
      Builtins.foreach(opts) do |opt|
×
637
        op = Ops.get_map(allopts, opt, {})
×
638
        t = Ops.get_string(op, "type", "")
×
639
        if t != "regex" && t != "enum" && t != ""
×
640
          t = Ops.add(Ops.add("[", t), "]")
×
641
        elsif t == "enum"
×
642
          t = "[ "
×
643
          Builtins.foreach(Ops.get_list(op, "typespec", [])) do |s|
×
644
            t = Ops.add(Ops.add(t, s), " ")
×
645
          end
646
          t = Ops.add(t, "]")
×
647
        else
648
          t = "    "
×
649
        end
650
        if Ops.is_string?(opt)
×
651
          helptext = ""
×
652
          opthelp = Ops.get(op, "help")
×
653

654
          if Ops.is_string?(opthelp)
×
655
            helptext = Convert.to_string(opthelp)
×
656
          elsif Ops.is(opthelp, "map <string, string>")
×
657
            helptext = Ops.get(
×
658
              Convert.convert(
659
                opthelp,
660
                from: "any",
661
                to:   "map <string, string>"
662
              ),
663
              action,
664
              ""
665
            )
666
          elsif Ops.is(opthelp, "list <string>")
×
667
            delim = Builtins.sformat(
×
668
              "\n        %1  %2  ",
669
              String.Pad("", longestopt),
670
              String.Pad("", longestarg)
671
            )
672
            helptext = Builtins.mergestring(
×
673
              Convert.convert(opthelp, from: "any", to: "list <string>"),
674
              delim
675
            )
676
          else
677
            Builtins.y2error(
×
678
              "Invalid data type of help text, only 'string' or 'map<string,string>' types are allowed."
679
            )
680
          end
681

682
          Print(
×
683
            Builtins.sformat(
684
              "        %1  %2  %3",
685
              String.Pad(Convert.to_string(opt), longestopt),
686
              String.Pad(t, longestarg),
687
              helptext
688
            )
689
          )
690
        end
691
      end
692

693
      if has_string_option
×
694
        # additional help for using command line
695
        Print(
×
696
          _(
697
            "\n    Options of the [string] type must be written in the form 'option=value'."
698
          )
699
        )
700
      end
701
      if Builtins.haskey(command, "example")
×
702
        # translators: example title for command line
703
        Print(_("\n    Example:"))
×
704

705
        example = Ops.get(command, "example")
×
706

707
        if Ops.is_string?(example)
×
708
          Print(Builtins.sformat("        %1", example))
×
709
        elsif Ops.is(example, "list <string>")
×
710
          Builtins.foreach(
×
711
            Convert.convert(example, from: "any", to: "list <string>")
712
          ) { |e| Print(Builtins.sformat("        %1", e)) }
×
713
        else
714
          Builtins.y2error("Unsupported data type - value: %1", example)
×
715
        end
716
      end
717
      Print("")
×
718

UNCOV
719
      nil
×
720
    end
721

722
    # Print a general help - list of available command.
723
    def PrintGeneralHelp
1✔
724
      # display custom defined help instead of generic one
725
      if Builtins.haskey(@modulecommands, "customhelp")
×
726
        Print(Ops.get_string(@modulecommands, "customhelp", ""))
×
727
        return
×
728
      end
729

730
      # translators: default module description if none is provided by the module itself
731
      Print(
×
732
        Ops.add(
733
          Ops.get_locale(@modulecommands, "help", _("This is a YaST module.")),
734
          "\n"
735
        )
736
      )
737
      # translators: short help title for command line
738
      Print(_("Basic Syntax:"))
×
739

740
      if @interactive
×
741
        # translators: module command line help
742
        # translate <command> and [options] only!
743
        Print(_("    <command> [options]"))
×
744
        # translators: module command line help
745
        # translate <command> only!
746
        Print(_("    <command> help"))
×
747
        # translators: module command line help
748
        Print("    help")
×
749
        Print("    longhelp")
×
750
        Print("    xmlhelp")
×
751
        Print("")
×
752
        Print("    exit")
×
753
        Print("    abort")
×
754
      else
755
        # translators: module command line help, %1 is the module name
756
        Print(
×
757
          Builtins.sformat(
758
            "    yast2 %1 interactive",
759
            Ops.get_string(@modulecommands, "id", "")
760
          )
761
        )
762

763
        # translators: module command line help, %1 is the module name
764
        # translate <command> and [options] only!
765
        Print(
×
766
          Builtins.sformat(
767
            _("    yast2 %1 <command> [verbose] [options]"),
768
            Ops.get_string(@modulecommands, "id", "")
769
          )
770
        )
771
        # translators: module command line help, %1 is the module name
772
        Print(
×
773
          Builtins.sformat(
774
            "    yast2 %1 help",
775
            Ops.get_string(@modulecommands, "id", "")
776
          )
777
        )
778
        Print(
×
779
          Builtins.sformat(
780
            "    yast2 %1 longhelp",
781
            Ops.get_string(@modulecommands, "id", "")
782
          )
783
        )
784
        Print(
×
785
          Builtins.sformat(
786
            "    yast2 %1 xmlhelp",
787
            Ops.get_string(@modulecommands, "id", "")
788
          )
789
        )
790
        # translators: module command line help, %1 is the module name
791
        # translate <command> only!
792
        Print(
×
793
          Builtins.sformat(
794
            _("    yast2 %1 <command> help"),
795
            Ops.get_string(@modulecommands, "id", "")
796
          )
797
        )
798
      end
799

800
      Print("")
×
801
      # translators: command line title: list of available commands
802
      Print(_("Commands:"))
×
803

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

809
      Builtins.maplist(Ops.get_map(@modulecommands, "actions", {})) do |cmd, desc|
×
810
        if !Builtins.haskey(desc, "help")
×
811
          # translators: error message: module does not provide any help messages
812
          Print(
×
813
            Builtins.sformat(
814
              "    %1  %2",
815
              String.Pad(cmd, longest),
816
              _("No help available.")
817
            )
818
          )
819
        end
820
        if Ops.is_string?(Ops.get(desc, "help"))
×
821
          Print(
×
822
            Builtins.sformat(
823
              "    %1  %2",
824
              String.Pad(cmd, longest),
825
              Ops.get_string(desc, "help", "")
826
            )
827
          )
828
        # multiline help text
829
        elsif Ops.is(Ops.get(desc, "help"), "list <string>")
×
830
          help = Ops.get_list(desc, "help", [])
×
831

832
          if Ops.greater_than(Builtins.size(help), 0)
×
833
            Print(
×
834
              Builtins.sformat(
835
                "    %1  %2",
836
                String.Pad(cmd, longest),
837
                Ops.get(help, 0, "")
838
              )
839
            )
840
            help = Builtins.remove(help, 0)
×
841
          end
842

843
          Builtins.foreach(help) do |h|
×
844
            Print(Builtins.sformat("    %1  %2", String.Pad("", longest), h))
×
845
          end
846
        else
847
          # fallback message - invalid help has been provided by the yast module
848
          Print(
×
849
            Builtins.sformat(
850
              "    %1  %2",
851
              String.Pad(cmd, longest),
852
              _("<Error: invalid help>")
853
            )
854
          )
855
        end
856
      end
857
      Print("")
×
858
      if !@interactive
×
859
        # translators: module command line help, %1 is the module name
860
        Print(
×
861
          Builtins.sformat(
862
            _("Run 'yast2 %1 <command> help' for a list of available options."),
863
            Ops.get_string(@modulecommands, "id", "")
864
          )
865
        )
866
        Print("")
×
867
      end
868

UNCOV
869
      nil
×
870
    end
871

872
    # Handle the system-wide commands, like help etc.
873
    #
874
    # @param [Hash] command  a map of the current command
875
    # @return    true, if the command was handled
876
    def ProcessSystemCommands(command)
1✔
877
      command = deep_copy(command)
3✔
878
      # handle help for specific command
879
      # this needs to be before general help, so "help help" works
880
      if Ops.get(command, ["options", "help"])
3✔
881
        PrintHead()
×
882
        PrintActionHelp(Ops.get_string(command, "command", ""))
×
883
        return true
×
884
      end
885

886
      # Process command "interactive"
887
      if Ops.get_string(command, "command", "") == "interactive"
3✔
888
        @interactive = true
×
889
        return true
×
890
      end
891

892
      # Process command "exit"
893
      if Ops.get_string(command, "command", "") == "exit"
3✔
894
        @done = true
×
895
        @aborted = false
×
896
        return true
×
897
      end
898

899
      # Process command "abort"
900
      if Ops.get_string(command, "command", "") == "abort"
3✔
901
        @done = true
×
902
        @aborted = true
×
903
        return true
×
904
      end
905

906
      if Ops.get_string(command, "command", "") == "help"
3✔
907
        # don't print header when custom help is defined
908
        PrintHead() if !Builtins.haskey(@modulecommands, "customhelp")
×
909
        PrintGeneralHelp()
×
910
        return true
×
911
      end
912

913
      if Ops.get_string(command, "command", "") == "longhelp"
3✔
914
        PrintHead()
×
915
        PrintGeneralHelp()
×
916
        Builtins.foreach(Ops.get_map(@allcommands, "actions", {})) do |action, _def|
×
917
          PrintActionHelp(action)
×
918
        end
919
        return true
×
920
      end
921

922
      if Ops.get_string(command, "command", "") == "xmlhelp"
3✔
923
        if Builtins.haskey(Ops.get_map(command, "options", {}), "xmlfile") == false
×
924
          # error message - command line option xmlfile is missing
925
          Print(
×
926
            _(
927
              "Target file name ('xmlfile' option) is missing. Use xmlfile=<target_XML_file> command line option."
928
            )
929
          )
930
          return false
×
931
        end
932

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

935
        if xmlfilename.nil? || xmlfilename == ""
×
936
          # error message - command line option xmlfile is missing
937
          Print(
×
938
            _(
939
              "Target file name ('xmlfile' option) is empty. Use xmlfile=<target_XML_file> command line option."
940
            )
941
          )
942
          return false
×
943
        end
944

945
        doc = {}
×
946

947
        #      TODO: DTD specification
948
        Ops.set(
×
949
          doc,
950
          "listEntries",
951
          "commands" => "command",
952
          "options"  => "option",
953
          "examples" => "example"
954
        )
955
        #      doc["cdataSections"] = [];
956
        Ops.set(
×
957
          doc,
958
          "systemID",
959
          Ops.add(Directory.schemadir, "/commandline.dtd")
960
        )
961
        #      doc["nameSpace"] = "http://www.suse.com/1.0/yast2ns";
962
        Ops.set(doc, "typeNamespace", "http://www.suse.com/1.0/configns")
×
963

964
        Ops.set(doc, "rootElement", "commandline")
×
965
        XML.xmlCreateDoc(:xmlhelp, doc)
×
966

967
        exportmap = {}
×
968
        commands = []
×
969

970
        actions = Ops.get_map(@cmdlinespec, "actions", {})
×
971
        mappings = Ops.get_map(@cmdlinespec, "mappings", {})
×
972
        options = Ops.get_map(@cmdlinespec, "options", {})
×
973

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

976
        Builtins.foreach(actions) do |action, description|
×
977
          help = ""
×
978
          # help text might be a simple string or a multiline text (list<string>)
979
          help_value = Ops.get(description, "help")
×
980
          if Ops.is_string?(help_value)
×
981
            help = Convert.to_string(help_value)
×
982
          elsif Ops.is(help_value, "list <string>")
×
983
            help = Builtins.mergestring(
×
984
              Convert.convert(
985
                help_value,
986
                from: "any",
987
                to:   "list <string>"
988
              ),
989
              "\n"
990
            )
991
          else
992
            Builtins.y2error(
×
993
              "Unsupported data type for 'help' key: %1, use 'string' or 'list<string>' type!",
994
              help_value
995
            )
996
          end
997
          opts = []
×
998
          Builtins.foreach(Ops.get(mappings, action, [])) do |option|
×
999
            optn = {
1000
              "name" => option,
×
1001
              "help" => Ops.get_string(options, [option, "help"], "")
1002
            }
1003
            # add type specification if it's present
1004
            if Ops.get_string(options, [option, "type"], "") != ""
×
1005
              optn = Builtins.add(
×
1006
                optn,
1007
                "type",
1008
                Ops.get_string(options, [option, "type"], "")
1009
              )
1010
            end
1011
            opts = Builtins.add(opts, optn)
×
1012
          end
1013
          actiondescr = { "help" => help, "name" => action, "options" => opts }
×
1014
          # add example if it's present
1015
          if Builtins.haskey(Ops.get(actions, action, {}), "example")
×
1016
            example = Ops.get(actions, [action, "example"])
×
1017
            examples = Array(example)
×
1018
            actiondescr = Builtins.add(actiondescr, "examples", examples)
×
1019
          end
1020
          commands = Builtins.add(commands, actiondescr)
×
1021
        end
1022

1023
        Ops.set(exportmap, "commands", commands)
×
1024
        Ops.set(exportmap, "module", Ops.get_string(@cmdlinespec, "id", ""))
×
1025

1026
        begin
1027
          XML.YCPToXMLFile(:xmlhelp, exportmap, xmlfilename)
×
1028
        rescue XMLSerializationError => e
1029
          # error message - creation of xml failed
1030
          Print(
×
1031
            _("Failed to create XML file.")
1032
          )
1033
          Builtins.y2error("Failed to serialize xml help: #{e.inspect}")
×
1034
          return false
×
1035
        end
1036

1037
        Builtins.y2milestone("exported XML map: %1", exportmap)
×
1038
        return true
×
1039
      end
1040

1041
      false
3✔
1042
    end
1043

1044
    #  Initialize Module
1045
    #
1046
    #  Initialize the module, setup the command line syntax and arguments passed on the command line.
1047
    #
1048
    #  @param [Hash] cmdlineinfo    the map describing the module command line
1049
    #  @param [Array] args      arguments given by the user on the command line
1050
    #  @return [Boolean]    true, if there are some commands to be processed (and cmdlineinfo passes sanity checks)
1051
    #  @see #Command
1052
    def Init(cmdlineinfo, args)
1✔
1053
      cmdlineinfo = deep_copy(cmdlineinfo)
3✔
1054
      args = deep_copy(args)
3✔
1055
      # remember the command line specification
1056
      # required later by xmlhelp command
1057
      @cmdlinespec = deep_copy(cmdlineinfo)
3✔
1058

1059
      cmdline_supported = true
3✔
1060

1061
      # check whether the command line mode is really supported by the module
1062
      if !Builtins.haskey(cmdlineinfo, "actions") ||
3✔
1063
          Builtins.size(Ops.get_map(cmdlineinfo, "actions", {})) == 0
1064
        cmdline_supported = false
×
1065
      end
1066

1067
      # initialize verbose flag
1068
      @verbose = Builtins.contains(WFM.Args, "verbose")
3✔
1069

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

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

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

1082
        # it's better to abort now
1083
        @done = true
×
1084
        @aborted = true
×
1085
      end
1086

1087
      # check for helps, they are required everywhere
1088
      # global help text
1089
      if cmdline_supported && !Builtins.haskey(cmdlineinfo, "help")
3✔
1090
        Builtins.y2error(
×
1091
          "Command line specification does not define global help for the module"
1092
        )
1093

1094
        # it's better to abort now
1095
        @done = true
×
1096
        @aborted = true
×
1097
      end
1098

1099
      # help texts for actions
1100
      if Builtins.haskey(cmdlineinfo, "actions")
3✔
1101
        Builtins.foreach(Ops.get_map(cmdlineinfo, "actions", {})) do |action, def_|
3✔
1102
          if !Builtins.haskey(def_, "help")
6✔
1103
            Builtins.y2error(
×
1104
              "Command line specification does not define help for action '%1'",
1105
              action
1106
            )
1107

1108
            # it's better to abort now
1109
            @done = true
×
1110
            @aborted = true
×
1111
          end
1112
        end
1113
      end
1114

1115
      # help for options
1116
      if Builtins.haskey(cmdlineinfo, "options")
3✔
1117
        Builtins.foreach(Ops.get_map(cmdlineinfo, "options", {})) do |option, def_|
3✔
1118
          if !Builtins.haskey(def_, "help")
3✔
1119
            Builtins.y2error(
×
1120
              "Command line specification does not define help for option '%1'",
1121
              option
1122
            )
1123

1124
            # it's better to abort now
1125
            @done = true
×
1126
            @aborted = true
×
1127
          end
1128
          # check that regex and enum have defined typespec
1129
          if (Ops.get_string(def_, "type", "") == "regex" ||
3✔
1130
              Ops.get_string(def_, "type", "") == "enum") &&
1131
              !Builtins.haskey(def_, "typespec")
1132
            Builtins.y2error(
×
1133
              "Command line specification does not define typespec for option '%1'",
1134
              option
1135
            )
1136

1137
            # it's better to abort now
1138
            @done = true
×
1139
            @aborted = true
×
1140
          end
1141
        end
1142
      end
1143

1144
      # mappings - check for existing actions and options
1145
      if Builtins.haskey(cmdlineinfo, "mappings")
3✔
1146
        Builtins.foreach(Ops.get_map(cmdlineinfo, "mappings", {})) do |mapaction, def_|
3✔
1147
          # is this action defined?
1148
          if !Builtins.haskey(
3✔
1149
            Ops.get_map(cmdlineinfo, "actions", {}),
1150
            mapaction
1151
          )
1152
            Builtins.y2error(
×
1153
              "Command line specification maps undefined action '%1'",
1154
              mapaction
1155
            )
1156

1157
            # it's better to abort now
1158
            @done = true
×
1159
            @aborted = true
×
1160
          end
1161
          Builtins.foreach(def_) do |mapopt|
3✔
1162
            next if !Ops.is_string?(mapopt)
3✔
1163

1164
            # is this option defined?
1165
            if !Builtins.haskey(
3✔
1166
              Ops.get_map(cmdlineinfo, "options", {}),
1167
              Convert.to_string(mapopt)
1168
            )
1169
              Builtins.y2error(
×
1170
                "Command line specification maps undefined option '%1' for action '%2'",
1171
                mapopt,
1172
                mapaction
1173
              )
1174

1175
              # it's better to abort now
1176
              @done = true
×
1177
              @aborted = true
×
1178
            end
1179
          end
1180
        end
1181
      end
1182

1183
      return false if @done
3✔
1184

1185
      @modulecommands = deep_copy(cmdlineinfo)
3✔
1186

1187
      # build allcommands - help and verbose options are added specially
1188
      @allcommands = {
1189
        "actions"  => Builtins.union(
3✔
1190
          Ops.get_map(@modulecommands, "actions", {}),
1191
          Ops.get(@systemcommands, "actions", {})
1192
        ),
1193
        "options"  => Builtins.union(
1194
          Ops.get_map(@modulecommands, "options", {}),
1195
          Ops.get(@systemcommands, "options", {})
1196
        ),
1197
        "mappings" => Builtins.union(
1198
          Builtins.mapmap(Ops.get_map(@modulecommands, "mappings", {})) do |act, opts|
1199
            { act => Builtins.union(opts, ["help", "verbose"]) }
3✔
1200
          end,
1201
          Ops.get(@systemcommands, "mappings", {})
1202
        )
1203
      }
1204

1205
      if Ops.less_than(Builtins.size(args), 1) || Stage.stage != "normal" ||
3✔
1206
          Stage.firstboot
1207
        Mode.SetUI("dialog")
×
1208
        # start GUI, module has some work to do :-)
1209
        return true
×
1210
      else
1211
        Mode.SetUI("commandline")
3✔
1212
      end
1213

1214
      if !cmdline_supported
3✔
1215
        # command line is not supported
1216
        Print(
×
1217
          String.UnderlinedHeader(
1218
            Ops.add("YaST2 ", Ops.get_string(cmdlineinfo, "id", "")),
1219
            0
1220
          )
1221
        )
1222
        Print("")
×
1223

1224
        help = Ops.get_string(cmdlineinfo, "help", "")
×
1225
        if !help.nil? && help != ""
×
1226
          Print(Ops.get_string(cmdlineinfo, "help", ""))
×
1227
          Print("")
×
1228
        end
1229

1230
        Print(@nosupport)
×
1231
        Print("")
×
1232
        return false
×
1233
      end
1234

1235
      # setup prompt
1236
      @cmdlineprompt = Ops.add(
3✔
1237
        Ops.add("YaST2 ", Ops.get_string(cmdlineinfo, "id", "")),
1238
        "> "
1239
      )
1240
      SCR.Write(path(".dev.tty.prompt"), @cmdlineprompt)
3✔
1241

1242
      # parse args
1243
      @commandcache = Parse(args)
3✔
1244

1245
      # return true, if there is some work to do:
1246
      # first, try to interpret system commands
1247
      if ProcessSystemCommands(@commandcache)
3✔
1248
        # it was system command, there is work only in interactive mode
1249
        @commandcache = {}
×
1250
        @done = !@interactive
×
1251
        @aborted = false
×
1252
        @interactive
×
1253
      else
1254
        # we cannot handle this on our own, return true if there is some command to be processed
1255
        # i.e, there is no parsing error
1256
        @done = Builtins.size(@commandcache) == 0
3✔
1257
        @aborted = @done
3✔
1258
        !@done
3✔
1259
      end
1260
    end
1261

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

1269
      String.ParseOptions(res, "separator" => " ")
×
1270
    end
1271

1272
    # Set prompt and read input from command line
1273
    # @param [String] prompt Set prompt
1274
    # @param [Symbol] type Type
1275
    # @return [String] Entered string
1276
    def GetInput(prompt, type)
1✔
1277
      # set the required prompt
1278
      SCR.Write(path(".dev.tty.prompt"), prompt)
×
1279

1280
      res = case type
×
1281
      when :nohistory
1282
        Convert.to_string(SCR.Read(path(".dev.tty.nohistory")))
×
1283
      when :noecho
1284
        Convert.to_string(SCR.Read(path(".dev.tty.noecho")))
×
1285
      else
1286
        Convert.to_string(SCR.Read(path(".dev.tty")))
×
1287
      end
1288

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

1292
      res
×
1293
    end
1294

1295
    # Read input from command line
1296
    # @param [String] prompt Set prompt to this value
1297
    # @return [String] Entered string
1298
    def UserInput(prompt)
1✔
1299
      GetInput(prompt, :nohistory)
×
1300
    end
1301

1302
    # Read input from command line
1303
    #
1304
    # Read input from command line, input is not displayed and not stored in
1305
    # the command line history. This function should be used for reading a password.
1306
    # @param [String] prompt Set prompt to this value
1307
    # @return [String] Entered string
1308
    def PasswordInput(prompt)
1✔
1309
      GetInput(prompt, :noecho)
×
1310
    end
1311

1312
    #  Get next user-given command
1313
    #
1314
    #  Get next user-given command. If there is a command available, returns it, otherwise ask
1315
    #  the user for a command (in interactive mode). Also processes system commands.
1316
    #
1317
    #  @return [Hash] of the new command. If there are no more commands, it returns exit or abort depending
1318
    #  on the result user asked for.
1319
    #
1320
    #  @see #Parse
1321
    def Command
1✔
1322
      # if we are done already, return the result
1323
      return { "command" => @aborted ? "abort" : "exit" } if @done
2✔
1324

1325
      # there is a command in the cache
1326
      if Builtins.size(@commandcache) != 0
2✔
1327
        result = deep_copy(@commandcache)
2✔
1328
        @commandcache = {}
2✔
1329
        @done = !@interactive
2✔
1330
        deep_copy(result)
2✔
1331
      # if in interactive mode, ask user for input
1332
      elsif @interactive
×
1333
        loop do
×
1334
          newcommand = []
×
1335
          newcommand = Scan() while Builtins.size(newcommand) == 0
×
1336

1337
          # EOF reached
1338
          if newcommand.nil?
×
1339
            @done = true
×
1340
            return { "command" => "exit" }
×
1341
          end
1342

1343
          @commandcache = Parse(newcommand)
×
1344
          break if !ProcessSystemCommands(@commandcache)
×
1345
          break if @done
×
1346
        end
1347

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

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

1354
        deep_copy(result)
×
1355
      else
1356
        # there is no further commands left
1357
        @done = true
×
1358
        { "command" => "exit" }
×
1359
      end
1360
    end
1361

1362
    #  Should module start UI?
1363
    #
1364
    #  @return [Boolean] true, if the user asked for standard UI (no parameter was passed by command line)
1365
    def StartGUI
1✔
1366
      !Mode.commandline
2✔
1367
    end
1368

1369
    #  Is module started in interactive command-line mode?
1370
    #
1371
    #  @return [Boolean] true, if the user asked for interactive command-line mode
1372
    def Interactive
1✔
1373
      @interactive
1✔
1374
    end
1375

1376
    #  User asked for abort (forgetting the changes)
1377
    #
1378
    #  @return [Boolean] true, if the user asked abort
1379
    def Aborted
1✔
1380
      @aborted
2✔
1381
    end
1382

1383
    # Abort the command line handling
1384
    def Abort
1✔
1385
      @aborted = true
×
1386
      @done = true
×
1387

UNCOV
1388
      nil
×
1389
    end
1390

1391
    #  Are there some commands to be processed?
1392
    #
1393
    #  @return [Boolean] true, if there is no more commands to be processed, either because the user
1394
    #  used command line, or the interactive mode was finished
1395
    def Done
1✔
1396
      @done
3✔
1397
    end
1398

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

1411
      # sanity check
1412
      if unique_options.empty?
7✔
1413
        log.error "Unique list of options required, but the list of the possible options is empty"
1✔
1414
        return nil
1✔
1415
      end
1416

1417
      # first do a filtering, then convert to a list of keys
1418
      cmds = unique_options & options.keys
6✔
1419

1420
      # if it is OK, quickly return
1421
      return cmds.first if cmds.size == 1
6✔
1422

1423
      msg = if cmds.empty?
5✔
1424
        if unique_options.size == 1
3✔
1425
          # translators: error message - missing unique command for command line execution
1426
          Builtins.sformat(_("Specify the command '%1'."), unique_options.first)
1✔
1427
        else
1428
          # translators: error message - missing unique command for command line execution
1429
          Builtins.sformat(_("Specify one of the commands: %1."), format_list(unique_options))
2✔
1430
        end
1431
      else
1432
        Builtins.sformat(_("Specify only one of the commands: %1."), format_list(cmds))
2✔
1433
      end
1434

1435
      Report.Error(msg)
5✔
1436
      nil
5✔
1437
    end
1438

1439
    # Parse the Command Line
1440
    #
1441
    # Function to parse the command line, start a GUI or handle interactive and
1442
    # command line actions as supported by the {Yast::CommandLine} module.
1443
    #
1444
    # @param [Hash] commandline  a map used in the CommandLine module with information
1445
    #                      about the handlers for GUI and commands.
1446
    # @option commandline [String] "help" global help text.
1447
    #   Help for options and actions are separated. Mandatory if module support command line.
1448
    # @option commandline [String] "id" module id. Mandatory if module support command line.
1449
    # @option commandline [Yast::FunRef("symbol ()")|Yast::FunRef("boolean ()")] "guihandler"
1450
    #   function to be called when gui requested. Mandatory for modules with GUI.
1451
    # @option commandline [Yast::FunRef("boolean ()")] "initialize" function that is called before
1452
    #   any action handler is called. Usually module initialization happens there.
1453
    # @option commandline [Yast::FunRef("boolean ()")] "finish" function that is called after
1454
    #   all action handlers are called. Usually writing of changes happens there. NOTE: calling
1455
    #   is skipped if all called handlers are readonly.
1456
    # @option commandline [Hash<String, Object>] "actions" definition of actions. Hash has action
1457
    #   name as key and value is hash with following keys:
1458
    #
1459
    #      - **"help"** _String|Array<String>_ mandatory action specific help text.
1460
    #        Options help text is defined separately. If array is passed it will be
1461
    #        printed indended on multiple lines.
1462
    #      - **"handler"** _Yast::FunRef("boolean (map <string, string>)")_ handler when action is
1463
    #        specified. Parameter is passed options. Mandatory.
1464
    #      - **"example"** _String_ optional example of action invocation.
1465
    #        By default no example is provided.
1466
    #      - **"options"** _Array<String>_ optional list of flags. So far only `"non_strict"`
1467
    #        supported. Useful when action arguments is not well defined, so unknown option does
1468
    #        not finish with error. By default it is empty array.
1469
    #      - **"readonly"** _Boolean_ optional flag that if it is set to true then
1470
    #        invoking action is not reason to run finish handler, but if another
1471
    #        action without readonly is called, it will run finish handler.
1472
    #        Default value is `false`.
1473
    #
1474
    # @option commandline [Hash<String, Object>] "options" definition of options. Hash has action
1475
    #   name as key and value is hash with following keys:
1476
    #
1477
    #      - **"help"** _String|Array<String>_ mandatory action specific help text.
1478
    #        If array is passed it will be printed indended on multiple lines.
1479
    #      - **"type"** _String_ optional type check for option parameter. By default no
1480
    #        type checking is done. It aborts if no checking is done and a value is passed on CLI.
1481
    #        Possible values are ycp types and additionally enum and regex. For enum additional
1482
    #        key **"typespec"** with array of values have to be specified. For regex additional
1483
    #        key **"typespec"** with string containing ycp regexp is required. For integer it
1484
    #        does conversion of a string value to an integer value.
1485
    #      - **"typespec"** _Object_ additional type specification. See **"type"** for details.
1486
    #
1487
    # @option commandline [Hash<String, Array<String>>] "mappings" defines connection between
1488
    #   **"actions"** and its **"options"**. The key is action and the value is a list of options it
1489
    #   supports.
1490
    # @return [Object] false if there was an error or there are no changes to be written (for example "help").
1491
    #      true if the changes should be written, or a value returned by the
1492
    #      handler. Actions that are read-only return also true on success even if there is nothing to write.
1493
    #
1494
    # @example Complete CLI support. Methods definition are skipped for simplicity.
1495
    #   Yast::CommandLine.Run(
1496
    #     "help"       => _("Foo Configuration"),
1497
    #     "id"         => "foo",
1498
    #     "guihandler" => fun_ref(method(:FooSequence), "symbol ()"),
1499
    #     "initialize" => fun_ref(Foo.method(:ReadNoGUI), "boolean ()"),
1500
    #     "finish"     => fun_ref(Foo.method(:WriteNoGUI), "boolean ()"),
1501
    #     "actions"    => {
1502
    #       "list"   => {
1503
    #         "help"     => _(
1504
    #           "Display configuration summary"
1505
    #           ),
1506
    #         "example"  => "foo list configured",
1507
    #         "readonly" => true,
1508
    #         "handler"  => fun_ref(
1509
    #           method(:ListHandler),
1510
    #           "boolean (map <string, string>)"
1511
    #         )
1512
    #       },
1513
    #       "edit"   => {
1514
    #         "help"    => _("Change existing configuration"),
1515
    #         "handler" => fun_ref(
1516
    #           method(:EditHandler),
1517
    #           "boolean (map <string, string>)"
1518
    #         )
1519
    #       },
1520
    #     },
1521
    #     "options"    => {
1522
    #       "configured"   => {
1523
    #         "help" => _("List only configured foo fighters")
1524
    #       }
1525
    #     },
1526
    #     "mappings"   => {
1527
    #       "list"   => ["configured"]
1528
    #     }
1529
    #   )
1530
    def Run(commandline)
1✔
1531
      commandline = deep_copy(commandline)
3✔
1532
      # The main ()
1533
      Builtins.y2milestone("----------------------------------------")
3✔
1534
      Builtins.y2milestone("Command line interface started")
3✔
1535

1536
      # Initialize the arguments
1537
      @done = false
3✔
1538
      return !Aborted() if !Init(commandline, WFM.Args)
3✔
1539

1540
      ret = true
2✔
1541
      # no action is readonly, but the first module without "readonly" will switch the flag to `false`
1542
      read_only = true
2✔
1543

1544
      initialized = false
2✔
1545
      if Ops.get(commandline, "initialize").nil?
2✔
1546
        # no initialization routine
1547
        # set initialized state to true => call finish handler at the end in command line mode
1548
        initialized = true
×
1549
      end
1550

1551
      # Start GUI
1552
      if StartGUI()
2✔
1553
        if !Builtins.haskey(commandline, "guihandler")
×
1554
          Builtins.y2error(
×
1555
            "Missing GUI handler for %1",
1556
            Ops.get_string(commandline, "id", "<unknown>")
1557
          )
1558
          # translators: error message - the module does not provide command line interface
1559
          Error(_("There is no user interface available for this module."))
×
1560
          return false
×
1561
        end
1562

1563
        if Ops.is(Ops.get(commandline, "guihandler"), "symbol ()")
×
1564
          exec = Convert.convert(
×
1565
            Ops.get(commandline, "guihandler"),
1566
            from: "any",
1567
            to:   "symbol ()"
1568
          )
1569
          symbol_ret = exec.call
×
1570
          Builtins.y2debug("GUI handler ret=%1", symbol_ret)
×
1571
          return symbol_ret
×
1572
        else
1573
          exec = Convert.convert(
×
1574
            Ops.get(
1575
              commandline,
1576
              "guihandler",
1577
              fun_ref(method(:fake_false), "boolean ()")
1578
            ),
1579
            from: "any",
1580
            to:   "boolean ()"
1581
          )
1582
          ret = exec.call
×
1583
          Builtins.y2debug("GUI handler ret=%1", ret)
×
1584
          return ret
×
1585
        end
1586
      else
1587
        # translators: progress message - command line interface ready
1588
        PrintVerbose(_("Ready"))
2✔
1589

1590
        until Done()
2✔
1591
          m = Command()
2✔
1592
          command = Ops.get_string(m, "command", "exit")
2✔
1593
          options = Ops.get_map(m, "options", {})
2✔
1594

1595
          # start initialization code if it wasn't already used
1596
          if !initialized && (Builtins.haskey(Ops.get_map(commandline, "actions", {}), command) &&
2✔
1597
                Ops.get(commandline, "initialize"))
1598
            # non-GUI handling
1599
            PrintVerbose(_("Initializing"))
2✔
1600
            ret2 = commandline["initialize"].call
2✔
1601
            if ret2
2✔
1602
              initialized = true
2✔
1603
            else
1604
              Builtins.y2milestone("Module initialization failed")
×
1605
              return false
×
1606
            end
1607
          end
1608

1609
          exec = Convert.convert(
2✔
1610
            Ops.get(commandline, ["actions", command, "handler"]),
1611
            from: "any",
1612
            to:   "boolean (map <string, string>)"
1613
          )
1614

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

1621
            # if it is not interactive, abort on errors
1622
            Abort() if !Interactive() && res == false
1✔
1623
          elsif !Done()
×
1624
            Builtins.y2error("Unknown command '%1' from CommandLine", command)
×
1625
            next
×
1626
          end
1627
        end
1628

1629
        ret = !Aborted()
1✔
1630
      end
1631

1632
      if ret && Ops.get(commandline, "finish") && initialized && !read_only
1✔
1633
        # translators: Progress message - the command line interface is about to finish
1634
        PrintVerbose(_("Finishing"))
1✔
1635
        ret = commandline["finish"].call
1✔
1636
        if !ret
1✔
1637
          Builtins.y2milestone("Module finishing failed")
1✔
1638
          return false
1✔
1639
        end
1640
        # translators: The command line interface is finished
1641
        PrintVerbose(_("Done"))
×
1642
      else
1643
        # translators: The command line interface is finished without writing the changes
1644
        PrintVerbose(_("Quitting (without changes)"))
×
1645
      end
1646

1647
      Builtins.y2milestone("Commandline interface finished")
×
1648
      Builtins.y2milestone("----------------------------------------")
×
1649

1650
      ret
×
1651
    end
1652

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

1660
      ui = UserInput(prompt)
×
1661

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

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

1668
      ui = UserInput(prompt) while ui != yes && ui != no
×
1669

1670
      ui == yes
×
1671
    end
1672

1673
    # Return verbose flag
1674
    # boolean verbose flag
1675
    def Verbose
1✔
1676
      @verbose
×
1677
    end
1678

1679
    publish variable: :cmdlineprompt, type: "string", private: true
1✔
1680
    publish variable: :systemcommands, type: "map <string, map <string, any>>", private: true
1✔
1681
    publish variable: :modulecommands, type: "map", private: true
1✔
1682
    publish variable: :allcommands, type: "map", private: true
1✔
1683
    publish variable: :interactive, type: "boolean", private: true
1✔
1684
    publish variable: :done, type: "boolean", private: true
1✔
1685
    publish variable: :aborted, type: "boolean", private: true
1✔
1686
    publish variable: :commandcache, type: "map <string, any>", private: true
1✔
1687
    publish variable: :verbose, type: "boolean", private: true
1✔
1688
    publish variable: :cmdlinespec, type: "map", private: true
1✔
1689
    publish variable: :nosupport, type: "string", private: true
1✔
1690
    publish function: :PrintInternal, type: "void (string, boolean)", private: true
1✔
1691
    publish function: :Print, type: "void (string)"
1✔
1692
    publish function: :PrintNoCR, type: "void (string)"
1✔
1693
    publish function: :PrintVerbose, type: "void (string)"
1✔
1694
    publish function: :PrintVerboseNoCR, type: "void (string)"
1✔
1695
    publish function: :PrintTable, type: "void (term, list <term>)"
1✔
1696
    publish function: :Error, type: "void (string)"
1✔
1697
    publish function: :Parse, type: "map <string, any> (list)"
1✔
1698
    publish function: :PrintHead, type: "void ()", private: true
1✔
1699
    publish function: :PrintActionHelp, type: "void (string)", private: true
1✔
1700
    publish function: :PrintGeneralHelp, type: "void ()", private: true
1✔
1701
    publish function: :ProcessSystemCommands, type: "boolean (map)", private: true
1✔
1702
    publish function: :Init, type: "boolean (map, list)"
1✔
1703
    publish function: :Scan, type: "list <string> ()"
1✔
1704
    publish function: :GetInput, type: "string (string, symbol)", private: true
1✔
1705
    publish function: :UserInput, type: "string (string)"
1✔
1706
    publish function: :PasswordInput, type: "string (string)"
1✔
1707
    publish function: :Command, type: "map ()"
1✔
1708
    publish function: :StartGUI, type: "boolean ()"
1✔
1709
    publish function: :Interactive, type: "boolean ()"
1✔
1710
    publish function: :Aborted, type: "boolean ()"
1✔
1711
    publish function: :Abort, type: "void ()"
1✔
1712
    publish function: :Done, type: "boolean ()"
1✔
1713
    publish function: :UniqueOption, type: "string (map <string, string>, list)"
1✔
1714
    publish function: :Run, type: "any (map)"
1✔
1715
    publish function: :YesNo, type: "boolean ()"
1✔
1716
    publish function: :Verbose, type: "boolean ()"
1✔
1717

1718
  private
1✔
1719

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

1727
  CommandLine = CommandLineClass.new
1✔
1728
  CommandLine.main
1✔
1729
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc