xref: /netbsd/external/gpl3/gdb/dist/gdb/cli/cli-setshow.c (revision 1424dfb3)
1 /* Handle set and show GDB commands.
2 
3    Copyright (C) 2000-2020 Free Software Foundation, Inc.
4 
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 3 of the License, or
8    (at your option) any later version.
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, see <http://www.gnu.org/licenses/>.  */
17 
18 #include "defs.h"
19 #include "readline/tilde.h"
20 #include "value.h"
21 #include <ctype.h>
22 #include "arch-utils.h"
23 #include "observable.h"
24 
25 #include "ui-out.h"
26 
27 #include "cli/cli-decode.h"
28 #include "cli/cli-cmds.h"
29 #include "cli/cli-setshow.h"
30 #include "cli/cli-utils.h"
31 
32 /* Return true if the change of command parameter should be notified.  */
33 
34 static int
notify_command_param_changed_p(int param_changed,struct cmd_list_element * c)35 notify_command_param_changed_p (int param_changed, struct cmd_list_element *c)
36 {
37   if (param_changed == 0)
38     return 0;
39 
40   if (c->theclass == class_maintenance || c->theclass == class_deprecated
41       || c->theclass == class_obscure)
42     return 0;
43 
44   return 1;
45 }
46 
47 
48 static enum auto_boolean
parse_auto_binary_operation(const char * arg)49 parse_auto_binary_operation (const char *arg)
50 {
51   if (arg != NULL && *arg != '\0')
52     {
53       int length = strlen (arg);
54 
55       while (isspace (arg[length - 1]) && length > 0)
56 	length--;
57 
58       /* Note that "o" is ambiguous.  */
59 
60       if ((length == 2 && strncmp (arg, "on", length) == 0)
61 	  || strncmp (arg, "1", length) == 0
62 	  || strncmp (arg, "yes", length) == 0
63 	  || strncmp (arg, "enable", length) == 0)
64 	return AUTO_BOOLEAN_TRUE;
65       else if ((length >= 2 && strncmp (arg, "off", length) == 0)
66 	       || strncmp (arg, "0", length) == 0
67 	       || strncmp (arg, "no", length) == 0
68 	       || strncmp (arg, "disable", length) == 0)
69 	return AUTO_BOOLEAN_FALSE;
70       else if (strncmp (arg, "auto", length) == 0
71 	       || (length > 1 && strncmp (arg, "-1", length) == 0))
72 	return AUTO_BOOLEAN_AUTO;
73     }
74   error (_("\"on\", \"off\" or \"auto\" expected."));
75   return AUTO_BOOLEAN_AUTO; /* Pacify GCC.  */
76 }
77 
78 /* See cli-setshow.h.  */
79 
80 int
parse_cli_boolean_value(const char ** arg)81 parse_cli_boolean_value (const char **arg)
82 {
83   const char *p = skip_to_space (*arg);
84   size_t length = p - *arg;
85 
86   /* Note that "o" is ambiguous.  */
87 
88   if ((length == 2 && strncmp (*arg, "on", length) == 0)
89       || strncmp (*arg, "1", length) == 0
90       || strncmp (*arg, "yes", length) == 0
91       || strncmp (*arg, "enable", length) == 0)
92     {
93       *arg = skip_spaces (*arg + length);
94       return 1;
95     }
96   else if ((length >= 2 && strncmp (*arg, "off", length) == 0)
97 	   || strncmp (*arg, "0", length) == 0
98 	   || strncmp (*arg, "no", length) == 0
99 	   || strncmp (*arg, "disable", length) == 0)
100     {
101       *arg = skip_spaces (*arg + length);
102       return 0;
103     }
104   else
105     return -1;
106 }
107 
108 /* See cli-setshow.h.  */
109 
110 int
parse_cli_boolean_value(const char * arg)111 parse_cli_boolean_value (const char *arg)
112 {
113   if (!arg || !*arg)
114     return 1;
115 
116   int b = parse_cli_boolean_value (&arg);
117   if (b >= 0 && *arg != '\0')
118     return -1;
119 
120   return b;
121 }
122 
123 
124 void
deprecated_show_value_hack(struct ui_file * ignore_file,int ignore_from_tty,struct cmd_list_element * c,const char * value)125 deprecated_show_value_hack (struct ui_file *ignore_file,
126 			    int ignore_from_tty,
127 			    struct cmd_list_element *c,
128 			    const char *value)
129 {
130   /* If there's no command or value, don't try to print it out.  */
131   if (c == NULL || value == NULL)
132     return;
133   /* Print doc minus "Show " at start.  Tell print_doc_line that
134      this is for a 'show value' prefix.  */
135   print_doc_line (gdb_stdout, c->doc + 5, true);
136   switch (c->var_type)
137     {
138     case var_string:
139     case var_string_noescape:
140     case var_optional_filename:
141     case var_filename:
142     case var_enum:
143       printf_filtered ((" is \"%s\".\n"), value);
144       break;
145     default:
146       printf_filtered ((" is %s.\n"), value);
147       break;
148     }
149 }
150 
151 /* Returns true if ARG is "unlimited".  */
152 
153 static bool
is_unlimited_literal(const char ** arg,bool expression)154 is_unlimited_literal (const char **arg, bool expression)
155 {
156   *arg = skip_spaces (*arg);
157 
158   const char *unl_start = *arg;
159 
160   const char *p = skip_to_space (*arg);
161 
162   size_t len = p - *arg;
163 
164   if (len > 0 && strncmp ("unlimited", *arg, len) == 0)
165     {
166       *arg += len;
167 
168       /* If parsing an expression (i.e., parsing for a "set" command),
169 	 anything after "unlimited" is junk.  For options, anything
170 	 after "unlimited" might be a command argument or another
171 	 option.  */
172       if (expression)
173 	{
174 	  const char *after = skip_spaces (*arg);
175 	  if (*after != '\0')
176 	    error (_("Junk after \"%.*s\": %s"),
177 		   (int) len, unl_start, after);
178 	}
179 
180       return true;
181     }
182 
183   return false;
184 }
185 
186 /* See cli-setshow.h.  */
187 
188 unsigned int
parse_cli_var_uinteger(var_types var_type,const char ** arg,bool expression)189 parse_cli_var_uinteger (var_types var_type, const char **arg,
190 			bool expression)
191 {
192   LONGEST val;
193 
194   if (*arg == nullptr || **arg == '\0')
195     {
196       if (var_type == var_uinteger)
197 	error_no_arg (_("integer to set it to, or \"unlimited\"."));
198       else
199 	error_no_arg (_("integer to set it to."));
200     }
201 
202   if (var_type == var_uinteger && is_unlimited_literal (arg, expression))
203     val = 0;
204   else if (expression)
205     val = parse_and_eval_long (*arg);
206   else
207     val = get_ulongest (arg);
208 
209   if (var_type == var_uinteger && val == 0)
210     val = UINT_MAX;
211   else if (val < 0
212 	   /* For var_uinteger, don't let the user set the value
213 	      to UINT_MAX directly, as that exposes an
214 	      implementation detail to the user interface.  */
215 	   || (var_type == var_uinteger && val >= UINT_MAX)
216 	   || (var_type == var_zuinteger && val > UINT_MAX))
217     error (_("integer %s out of range"), plongest (val));
218 
219   return val;
220 }
221 
222 /* See cli-setshow.h.  */
223 
224 int
parse_cli_var_zuinteger_unlimited(const char ** arg,bool expression)225 parse_cli_var_zuinteger_unlimited (const char **arg, bool expression)
226 {
227   LONGEST val;
228 
229   if (*arg == nullptr || **arg == '\0')
230     error_no_arg (_("integer to set it to, or \"unlimited\"."));
231 
232   if (is_unlimited_literal (arg, expression))
233     val = -1;
234   else if (expression)
235     val = parse_and_eval_long (*arg);
236   else
237     val = get_ulongest (arg);
238 
239   if (val > INT_MAX)
240     error (_("integer %s out of range"), plongest (val));
241   else if (val < -1)
242     error (_("only -1 is allowed to set as unlimited"));
243 
244   return val;
245 }
246 
247 /* See cli-setshow.h.  */
248 
249 const char *
parse_cli_var_enum(const char ** args,const char * const * enums)250 parse_cli_var_enum (const char **args, const char *const *enums)
251 {
252   /* If no argument was supplied, print an informative error
253      message.  */
254   if (args == NULL || *args == NULL || **args == '\0')
255     {
256       std::string msg;
257 
258       for (size_t i = 0; enums[i]; i++)
259 	{
260 	  if (i != 0)
261 	    msg += ", ";
262 	  msg += enums[i];
263 	}
264       error (_("Requires an argument. Valid arguments are %s."),
265 	     msg.c_str ());
266     }
267 
268   const char *p = skip_to_space (*args);
269   size_t len = p - *args;
270 
271   int nmatches = 0;
272   const char *match = NULL;
273   for (size_t i = 0; enums[i]; i++)
274     if (strncmp (*args, enums[i], len) == 0)
275       {
276 	if (enums[i][len] == '\0')
277 	  {
278 	    match = enums[i];
279 	    nmatches = 1;
280 	    break; /* Exact match.  */
281 	  }
282 	else
283 	  {
284 	    match = enums[i];
285 	    nmatches++;
286 	  }
287       }
288 
289   if (nmatches == 0)
290     error (_("Undefined item: \"%.*s\"."), (int) len, *args);
291 
292   if (nmatches > 1)
293     error (_("Ambiguous item \"%.*s\"."), (int) len, *args);
294 
295   *args += len;
296   return match;
297 }
298 
299 /* Do a "set" command.  ARG is NULL if no argument, or the
300    text of the argument, and FROM_TTY is nonzero if this command is
301    being entered directly by the user (i.e. these are just like any
302    other command).  C is the command list element for the command.  */
303 
304 void
do_set_command(const char * arg,int from_tty,struct cmd_list_element * c)305 do_set_command (const char *arg, int from_tty, struct cmd_list_element *c)
306 {
307   /* A flag to indicate the option is changed or not.  */
308   int option_changed = 0;
309 
310   gdb_assert (c->type == set_cmd);
311 
312   if (arg == NULL)
313     arg = "";
314 
315   switch (c->var_type)
316     {
317     case var_string:
318       {
319 	char *newobj;
320 	const char *p;
321 	char *q;
322 	int ch;
323 
324 	newobj = (char *) xmalloc (strlen (arg) + 2);
325 	p = arg;
326 	q = newobj;
327 	while ((ch = *p++) != '\000')
328 	  {
329 	    if (ch == '\\')
330 	      {
331 		/* \ at end of argument is used after spaces
332 		   so they won't be lost.  */
333 		/* This is obsolete now that we no longer strip
334 		   trailing whitespace and actually, the backslash
335 		   didn't get here in my test, readline or
336 		   something did something funky with a backslash
337 		   right before a newline.  */
338 		if (*p == 0)
339 		  break;
340 		ch = parse_escape (get_current_arch (), &p);
341 		if (ch == 0)
342 		  break;	/* C loses */
343 		else if (ch > 0)
344 		  *q++ = ch;
345 	      }
346 	    else
347 	      *q++ = ch;
348 	  }
349 #if 0
350 	if (*(p - 1) != '\\')
351 	  *q++ = ' ';
352 #endif
353 	*q++ = '\0';
354 	newobj = (char *) xrealloc (newobj, q - newobj);
355 
356 	if (*(char **) c->var == NULL
357 	    || strcmp (*(char **) c->var, newobj) != 0)
358 	  {
359 	    xfree (*(char **) c->var);
360 	    *(char **) c->var = newobj;
361 
362 	    option_changed = 1;
363 	  }
364 	else
365 	  xfree (newobj);
366       }
367       break;
368     case var_string_noescape:
369       if (*(char **) c->var == NULL || strcmp (*(char **) c->var, arg) != 0)
370 	{
371 	  xfree (*(char **) c->var);
372 	  *(char **) c->var = xstrdup (arg);
373 
374 	  option_changed = 1;
375 	}
376       break;
377     case var_filename:
378       if (*arg == '\0')
379 	error_no_arg (_("filename to set it to."));
380       /* FALLTHROUGH */
381     case var_optional_filename:
382       {
383 	char *val = NULL;
384 
385 	if (*arg != '\0')
386 	  {
387 	    /* Clear trailing whitespace of filename.  */
388 	    const char *ptr = arg + strlen (arg) - 1;
389 	    char *copy;
390 
391 	    while (ptr >= arg && (*ptr == ' ' || *ptr == '\t'))
392 	      ptr--;
393 	    copy = xstrndup (arg, ptr + 1 - arg);
394 
395 	    val = tilde_expand (copy);
396 	    xfree (copy);
397 	  }
398 	else
399 	  val = xstrdup ("");
400 
401 	if (*(char **) c->var == NULL
402 	    || strcmp (*(char **) c->var, val) != 0)
403 	  {
404 	    xfree (*(char **) c->var);
405 	    *(char **) c->var = val;
406 
407 	    option_changed = 1;
408 	  }
409 	else
410 	  xfree (val);
411       }
412       break;
413     case var_boolean:
414       {
415 	int val = parse_cli_boolean_value (arg);
416 
417 	if (val < 0)
418 	  error (_("\"on\" or \"off\" expected."));
419 	if (val != *(bool *) c->var)
420 	  {
421 	    *(bool *) c->var = val;
422 
423 	    option_changed = 1;
424 	  }
425       }
426       break;
427     case var_auto_boolean:
428       {
429 	enum auto_boolean val = parse_auto_binary_operation (arg);
430 
431 	if (*(enum auto_boolean *) c->var != val)
432 	  {
433 	    *(enum auto_boolean *) c->var = val;
434 
435 	    option_changed = 1;
436 	  }
437       }
438       break;
439     case var_uinteger:
440     case var_zuinteger:
441       {
442 	unsigned int val = parse_cli_var_uinteger (c->var_type, &arg, true);
443 
444 	if (*(unsigned int *) c->var != val)
445 	  {
446 	    *(unsigned int *) c->var = val;
447 
448 	    option_changed = 1;
449 	  }
450       }
451       break;
452     case var_integer:
453     case var_zinteger:
454       {
455 	LONGEST val;
456 
457 	if (*arg == '\0')
458 	  {
459 	    if (c->var_type == var_integer)
460 	      error_no_arg (_("integer to set it to, or \"unlimited\"."));
461 	    else
462 	      error_no_arg (_("integer to set it to."));
463 	  }
464 
465 	if (c->var_type == var_integer && is_unlimited_literal (&arg, true))
466 	  val = 0;
467 	else
468 	  val = parse_and_eval_long (arg);
469 
470 	if (val == 0 && c->var_type == var_integer)
471 	  val = INT_MAX;
472 	else if (val < INT_MIN
473 		 /* For var_integer, don't let the user set the value
474 		    to INT_MAX directly, as that exposes an
475 		    implementation detail to the user interface.  */
476 		 || (c->var_type == var_integer && val >= INT_MAX)
477 		 || (c->var_type == var_zinteger && val > INT_MAX))
478 	  error (_("integer %s out of range"), plongest (val));
479 
480 	if (*(int *) c->var != val)
481 	  {
482 	    *(int *) c->var = val;
483 
484 	    option_changed = 1;
485 	  }
486 	break;
487       }
488     case var_enum:
489       {
490 	const char *end_arg = arg;
491 	const char *match = parse_cli_var_enum (&end_arg, c->enums);
492 
493 	int len = end_arg - arg;
494 	const char *after = skip_spaces (end_arg);
495 	if (*after != '\0')
496 	  error (_("Junk after item \"%.*s\": %s"), len, arg, after);
497 
498 	if (*(const char **) c->var != match)
499 	  {
500 	    *(const char **) c->var = match;
501 
502 	    option_changed = 1;
503 	  }
504       }
505       break;
506     case var_zuinteger_unlimited:
507       {
508 	int val = parse_cli_var_zuinteger_unlimited (&arg, true);
509 
510 	if (*(int *) c->var != val)
511 	  {
512 	    *(int *) c->var = val;
513 	    option_changed = 1;
514 	  }
515       }
516       break;
517     default:
518       error (_("gdb internal error: bad var_type in do_setshow_command"));
519     }
520   c->func (c, NULL, from_tty);
521 
522   if (notify_command_param_changed_p (option_changed, c))
523     {
524       char *name, *cp;
525       struct cmd_list_element **cmds;
526       struct cmd_list_element *p;
527       int i;
528       int length = 0;
529 
530       /* Compute the whole multi-word command options.  If user types command
531 	 'set foo bar baz on', c->name is 'baz', and GDB can't pass "bar" to
532 	 command option change notification, because it is confusing.  We can
533 	 trace back through field 'prefix' to compute the whole options,
534 	 and pass "foo bar baz" to notification.  */
535 
536       for (i = 0, p = c; p != NULL; i++)
537 	{
538 	  length += strlen (p->name);
539 	  length++;
540 
541 	  p = p->prefix;
542 	}
543       cp = name = (char *) xmalloc (length);
544       cmds = XNEWVEC (struct cmd_list_element *, i);
545 
546       /* Track back through filed 'prefix' and cache them in CMDS.  */
547       for (i = 0, p = c; p != NULL; i++)
548 	{
549 	  cmds[i] = p;
550 	  p = p->prefix;
551 	}
552 
553       /* Don't trigger any observer notification if prefixlist is not
554 	 setlist.  */
555       i--;
556       if (cmds[i]->prefixlist != &setlist)
557 	{
558 	  xfree (cmds);
559 	  xfree (name);
560 
561 	  return;
562 	}
563       /* Traverse them in the reversed order, and copy their names into
564 	 NAME.  */
565       for (i--; i >= 0; i--)
566 	{
567 	  memcpy (cp, cmds[i]->name, strlen (cmds[i]->name));
568 	  cp += strlen (cmds[i]->name);
569 
570 	  if (i != 0)
571 	    {
572 	      cp[0] = ' ';
573 	      cp++;
574 	    }
575 	}
576       cp[0] = 0;
577 
578       xfree (cmds);
579 
580       switch (c->var_type)
581 	{
582 	case var_string:
583 	case var_string_noescape:
584 	case var_filename:
585 	case var_optional_filename:
586 	case var_enum:
587 	  gdb::observers::command_param_changed.notify (name, *(char **) c->var);
588 	  break;
589 	case var_boolean:
590 	  {
591 	    const char *opt = *(bool *) c->var ? "on" : "off";
592 
593 	    gdb::observers::command_param_changed.notify (name, opt);
594 	  }
595 	  break;
596 	case var_auto_boolean:
597 	  {
598 	    const char *s = auto_boolean_enums[*(enum auto_boolean *) c->var];
599 
600 	    gdb::observers::command_param_changed.notify (name, s);
601 	  }
602 	  break;
603 	case var_uinteger:
604 	case var_zuinteger:
605 	  {
606 	    char s[64];
607 
608 	    xsnprintf (s, sizeof s, "%u", *(unsigned int *) c->var);
609 	    gdb::observers::command_param_changed.notify (name, s);
610 	  }
611 	  break;
612 	case var_integer:
613 	case var_zinteger:
614 	case var_zuinteger_unlimited:
615 	  {
616 	    char s[64];
617 
618 	    xsnprintf (s, sizeof s, "%d", *(int *) c->var);
619 	    gdb::observers::command_param_changed.notify (name, s);
620 	  }
621 	  break;
622 	}
623       xfree (name);
624     }
625 }
626 
627 /* See cli/cli-setshow.h.  */
628 
629 std::string
get_setshow_command_value_string(const cmd_list_element * c)630 get_setshow_command_value_string (const cmd_list_element *c)
631 {
632   string_file stb;
633 
634   switch (c->var_type)
635     {
636     case var_string:
637       if (*(char **) c->var)
638 	stb.putstr (*(char **) c->var, '"');
639       break;
640     case var_string_noescape:
641     case var_optional_filename:
642     case var_filename:
643     case var_enum:
644       if (*(char **) c->var)
645 	stb.puts (*(char **) c->var);
646       break;
647     case var_boolean:
648       stb.puts (*(bool *) c->var ? "on" : "off");
649       break;
650     case var_auto_boolean:
651       switch (*(enum auto_boolean*) c->var)
652 	{
653 	case AUTO_BOOLEAN_TRUE:
654 	  stb.puts ("on");
655 	  break;
656 	case AUTO_BOOLEAN_FALSE:
657 	  stb.puts ("off");
658 	  break;
659 	case AUTO_BOOLEAN_AUTO:
660 	  stb.puts ("auto");
661 	  break;
662 	default:
663 	  gdb_assert_not_reached ("invalid var_auto_boolean");
664 	  break;
665 	}
666       break;
667     case var_uinteger:
668     case var_zuinteger:
669       if (c->var_type == var_uinteger
670 	  && *(unsigned int *) c->var == UINT_MAX)
671 	stb.puts ("unlimited");
672       else
673 	stb.printf ("%u", *(unsigned int *) c->var);
674       break;
675     case var_integer:
676     case var_zinteger:
677       if (c->var_type == var_integer
678 	  && *(int *) c->var == INT_MAX)
679 	stb.puts ("unlimited");
680       else
681 	stb.printf ("%d", *(int *) c->var);
682       break;
683     case var_zuinteger_unlimited:
684       {
685 	if (*(int *) c->var == -1)
686 	  stb.puts ("unlimited");
687 	else
688 	  stb.printf ("%d", *(int *) c->var);
689       }
690       break;
691     default:
692       gdb_assert_not_reached ("bad var_type");
693     }
694 
695   return std::move (stb.string ());
696 }
697 
698 
699 /* Do a "show" command.  ARG is NULL if no argument, or the
700    text of the argument, and FROM_TTY is nonzero if this command is
701    being entered directly by the user (i.e. these are just like any
702    other command).  C is the command list element for the command.  */
703 
704 void
do_show_command(const char * arg,int from_tty,struct cmd_list_element * c)705 do_show_command (const char *arg, int from_tty, struct cmd_list_element *c)
706 {
707   struct ui_out *uiout = current_uiout;
708 
709   gdb_assert (c->type == show_cmd);
710 
711   /* Possibly call the pre hook.  */
712   if (c->pre_show_hook)
713     (c->pre_show_hook) (c);
714 
715   std::string val = get_setshow_command_value_string (c);
716 
717   /* FIXME: cagney/2005-02-10: There should be MI and CLI specific
718      versions of code to print the value out.  */
719 
720   if (uiout->is_mi_like_p ())
721     uiout->field_string ("value", val.c_str ());
722   else
723     {
724       if (c->show_value_func != NULL)
725 	c->show_value_func (gdb_stdout, from_tty, c, val.c_str ());
726       else
727 	deprecated_show_value_hack (gdb_stdout, from_tty, c, val.c_str ());
728     }
729 
730   c->func (c, NULL, from_tty);
731 }
732 
733 /* Show all the settings in a list of show commands.  */
734 
735 void
cmd_show_list(struct cmd_list_element * list,int from_tty)736 cmd_show_list (struct cmd_list_element *list, int from_tty)
737 {
738   struct ui_out *uiout = current_uiout;
739 
740   ui_out_emit_tuple tuple_emitter (uiout, "showlist");
741   for (; list != NULL; list = list->next)
742     {
743       /* We skip show command aliases to avoid showing duplicated values.  */
744 
745       /* If we find a prefix, run its list, prefixing our output by its
746          prefix (with "show " skipped).  */
747       if (list->prefixlist && list->cmd_pointer == nullptr)
748 	{
749 	  ui_out_emit_tuple optionlist_emitter (uiout, "optionlist");
750 	  const char *new_prefix = strstr (list->prefixname, "show ") + 5;
751 
752 	  if (uiout->is_mi_like_p ())
753 	    uiout->field_string ("prefix", new_prefix);
754 	  cmd_show_list (*list->prefixlist, from_tty);
755 	}
756       else if (list->theclass != no_set_class && list->cmd_pointer == nullptr)
757 	{
758 	  ui_out_emit_tuple option_emitter (uiout, "option");
759 
760 	  {
761 	    /* If we find a prefix, output it (with "show " skipped).  */
762 	    const char *prefixname
763 	      = (list->prefix == nullptr ? ""
764 		 : strstr (list->prefix->prefixname, "show ") + 5);
765 	    uiout->text (prefixname);
766 	  }
767 	  uiout->field_string ("name", list->name);
768 	  uiout->text (":  ");
769 	  if (list->type == show_cmd)
770 	    do_show_command (NULL, from_tty, list);
771 	  else
772 	    cmd_func (list, NULL, from_tty);
773 	}
774     }
775 }
776 
777 
778