1 /* SPDX-License-Identifier: LGPL-2.1+ */
3 This file is part of systemd.
5 Copyright 2012 Lennart Poettering
7 systemd is free software; you can redistribute it and/or modify it
8 under the terms of the GNU Lesser General Public License as published by
9 the Free Software Foundation; either version 2.1 of the License, or
10 (at your option) any later version.
12 systemd is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public License
18 along with systemd; If not, see <http://www.gnu.org/licenses/>.
32 #include "alloc-util.h"
33 #include "bus-error.h"
35 #include "cgroup-show.h"
36 #include "cgroup-util.h"
40 #include "parse-util.h"
41 #include "path-util.h"
42 #include "process-util.h"
43 #include "stdio-util.h"
44 #include "terminal-util.h"
45 #include "unit-name.h"
48 typedef struct Group
{
58 unsigned cpu_iteration
;
65 unsigned io_iteration
;
66 uint64_t io_input
, io_output
;
68 uint64_t io_input_bps
, io_output_bps
;
71 static unsigned arg_depth
= 3;
72 static unsigned arg_iterations
= (unsigned) -1;
73 static bool arg_batch
= false;
74 static bool arg_raw
= false;
75 static usec_t arg_delay
= 1*USEC_PER_SEC
;
76 static char* arg_machine
= NULL
;
77 static char* arg_root
= NULL
;
78 static bool arg_recursive
= true;
79 static bool arg_recursive_unset
= false;
83 COUNT_USERSPACE_PROCESSES
,
85 } arg_count
= COUNT_PIDS
;
93 } arg_order
= ORDER_CPU
;
98 } arg_cpu_type
= CPU_PERCENT
;
100 static void group_free(Group
*g
) {
107 static void group_hashmap_clear(Hashmap
*h
) {
110 while ((g
= hashmap_steal_first(h
)))
114 static void group_hashmap_free(Hashmap
*h
) {
115 group_hashmap_clear(h
);
119 static const char *maybe_format_bytes(char *buf
, size_t l
, bool is_valid
, uint64_t t
) {
123 snprintf(buf
, l
, "%" PRIu64
, t
);
126 return format_bytes(buf
, l
, t
);
130 const char *controller
,
144 all_unified
= cg_all_unified();
148 g
= hashmap_get(a
, path
);
150 g
= hashmap_get(b
, path
);
156 g
->path
= strdup(path
);
162 r
= hashmap_put(a
, g
->path
, g
);
168 r
= hashmap_move_one(a
, b
, path
);
172 g
->cpu_valid
= g
->memory_valid
= g
->io_valid
= g
->n_tasks_valid
= false;
176 if (streq(controller
, SYSTEMD_CGROUP_CONTROLLER
) && IN_SET(arg_count
, COUNT_ALL_PROCESSES
, COUNT_USERSPACE_PROCESSES
)) {
177 _cleanup_fclose_
FILE *f
= NULL
;
180 r
= cg_enumerate_processes(controller
, path
, &f
);
187 while (cg_read_pid(f
, &pid
) > 0) {
189 if (arg_count
== COUNT_USERSPACE_PROCESSES
&& is_kernel_thread(pid
) > 0)
196 g
->n_tasks_valid
= true;
198 } else if (streq(controller
, "pids") && arg_count
== COUNT_PIDS
) {
199 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
201 r
= cg_get_path(controller
, path
, "pids.current", &p
);
205 r
= read_one_line_file(p
, &v
);
211 r
= safe_atou64(v
, &g
->n_tasks
);
216 g
->n_tasks_valid
= true;
218 } else if (streq(controller
, "cpu") || streq(controller
, "cpuacct")) {
219 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
224 const char *keys
[] = { "usage_usec", NULL
};
225 _cleanup_free_
char *val
= NULL
;
227 if (!streq(controller
, "cpu"))
230 r
= cg_get_keyed_attribute("cpu", path
, "cpu.stat", keys
, &val
);
236 r
= safe_atou64(val
, &new_usage
);
240 new_usage
*= NSEC_PER_USEC
;
242 if (!streq(controller
, "cpuacct"))
245 r
= cg_get_path(controller
, path
, "cpuacct.usage", &p
);
249 r
= read_one_line_file(p
, &v
);
255 r
= safe_atou64(v
, &new_usage
);
260 timestamp
= now_nsec(CLOCK_MONOTONIC
);
262 if (g
->cpu_iteration
== iteration
- 1 &&
263 (nsec_t
) new_usage
> g
->cpu_usage
) {
267 x
= timestamp
- g
->cpu_timestamp
;
271 y
= (nsec_t
) new_usage
- g
->cpu_usage
;
272 g
->cpu_fraction
= (double) y
/ (double) x
;
276 g
->cpu_usage
= (nsec_t
) new_usage
;
277 g
->cpu_timestamp
= timestamp
;
278 g
->cpu_iteration
= iteration
;
280 } else if (streq(controller
, "memory")) {
281 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
284 r
= cg_get_path(controller
, path
, "memory.current", &p
);
286 r
= cg_get_path(controller
, path
, "memory.usage_in_bytes", &p
);
290 r
= read_one_line_file(p
, &v
);
296 r
= safe_atou64(v
, &g
->memory
);
301 g
->memory_valid
= true;
303 } else if ((streq(controller
, "io") && all_unified
) ||
304 (streq(controller
, "blkio") && !all_unified
)) {
305 _cleanup_fclose_
FILE *f
= NULL
;
306 _cleanup_free_
char *p
= NULL
;
307 uint64_t wr
= 0, rd
= 0;
310 r
= cg_get_path(controller
, path
, all_unified
? "io.stat" : "blkio.io_service_bytes", &p
);
322 char line
[LINE_MAX
], *l
;
325 if (!fgets(line
, sizeof(line
), f
))
328 /* Trim and skip the device */
330 l
+= strcspn(l
, WHITESPACE
);
331 l
+= strspn(l
, WHITESPACE
);
334 while (!isempty(l
)) {
335 if (sscanf(l
, "rbytes=%" SCNu64
, &k
))
337 else if (sscanf(l
, "wbytes=%" SCNu64
, &k
))
340 l
+= strcspn(l
, WHITESPACE
);
341 l
+= strspn(l
, WHITESPACE
);
344 if (first_word(l
, "Read")) {
347 } else if (first_word(l
, "Write")) {
353 l
+= strspn(l
, WHITESPACE
);
354 r
= safe_atou64(l
, &k
);
362 timestamp
= now_nsec(CLOCK_MONOTONIC
);
364 if (g
->io_iteration
== iteration
- 1) {
367 x
= (uint64_t) (timestamp
- g
->io_timestamp
);
371 if (rd
> g
->io_input
)
372 yr
= rd
- g
->io_input
;
376 if (wr
> g
->io_output
)
377 yw
= wr
- g
->io_output
;
381 if (yr
> 0 || yw
> 0) {
382 g
->io_input_bps
= (yr
* 1000000000ULL) / x
;
383 g
->io_output_bps
= (yw
* 1000000000ULL) / x
;
390 g
->io_timestamp
= timestamp
;
391 g
->io_iteration
= iteration
;
400 static int refresh_one(
401 const char *controller
,
409 _cleanup_closedir_
DIR *d
= NULL
;
417 if (depth
> arg_depth
)
420 r
= process(controller
, path
, a
, b
, iteration
, &ours
);
424 r
= cg_enumerate_subgroups(controller
, path
, &d
);
431 _cleanup_free_
char *fn
= NULL
, *p
= NULL
;
434 r
= cg_read_subgroup(d
, &fn
);
440 p
= strjoin(path
, "/", fn
);
444 path_kill_slashes(p
);
446 r
= refresh_one(controller
, p
, a
, b
, iteration
, depth
+ 1, &child
);
451 IN_SET(arg_count
, COUNT_ALL_PROCESSES
, COUNT_USERSPACE_PROCESSES
) &&
453 child
->n_tasks_valid
&&
454 streq(controller
, SYSTEMD_CGROUP_CONTROLLER
)) {
456 /* Recursively sum up processes */
458 if (ours
->n_tasks_valid
)
459 ours
->n_tasks
+= child
->n_tasks
;
461 ours
->n_tasks
= child
->n_tasks
;
462 ours
->n_tasks_valid
= true;
473 static int refresh(const char *root
, Hashmap
*a
, Hashmap
*b
, unsigned iteration
) {
478 r
= refresh_one(SYSTEMD_CGROUP_CONTROLLER
, root
, a
, b
, iteration
, 0, NULL
);
481 r
= refresh_one("cpu", root
, a
, b
, iteration
, 0, NULL
);
484 r
= refresh_one("cpuacct", root
, a
, b
, iteration
, 0, NULL
);
487 r
= refresh_one("memory", root
, a
, b
, iteration
, 0, NULL
);
490 r
= refresh_one("io", root
, a
, b
, iteration
, 0, NULL
);
493 r
= refresh_one("blkio", root
, a
, b
, iteration
, 0, NULL
);
496 r
= refresh_one("pids", root
, a
, b
, iteration
, 0, NULL
);
503 static int group_compare(const void*a
, const void *b
) {
504 const Group
*x
= *(Group
**)a
, *y
= *(Group
**)b
;
506 if (arg_order
!= ORDER_TASKS
|| arg_recursive
) {
507 /* Let's make sure that the parent is always before
508 * the child. Except when ordering by tasks and
509 * recursive summing is off, since that is actually
510 * not accumulative for all children. */
512 if (path_startswith(y
->path
, x
->path
))
514 if (path_startswith(x
->path
, y
->path
))
524 if (arg_cpu_type
== CPU_PERCENT
) {
525 if (x
->cpu_valid
&& y
->cpu_valid
) {
526 if (x
->cpu_fraction
> y
->cpu_fraction
)
528 else if (x
->cpu_fraction
< y
->cpu_fraction
)
530 } else if (x
->cpu_valid
)
532 else if (y
->cpu_valid
)
535 if (x
->cpu_usage
> y
->cpu_usage
)
537 else if (x
->cpu_usage
< y
->cpu_usage
)
544 if (x
->n_tasks_valid
&& y
->n_tasks_valid
) {
545 if (x
->n_tasks
> y
->n_tasks
)
547 else if (x
->n_tasks
< y
->n_tasks
)
549 } else if (x
->n_tasks_valid
)
551 else if (y
->n_tasks_valid
)
557 if (x
->memory_valid
&& y
->memory_valid
) {
558 if (x
->memory
> y
->memory
)
560 else if (x
->memory
< y
->memory
)
562 } else if (x
->memory_valid
)
564 else if (y
->memory_valid
)
570 if (x
->io_valid
&& y
->io_valid
) {
571 if (x
->io_input_bps
+ x
->io_output_bps
> y
->io_input_bps
+ y
->io_output_bps
)
573 else if (x
->io_input_bps
+ x
->io_output_bps
< y
->io_input_bps
+ y
->io_output_bps
)
575 } else if (x
->io_valid
)
577 else if (y
->io_valid
)
581 return path_compare(x
->path
, y
->path
);
584 static void display(Hashmap
*a
) {
589 unsigned rows
, n
= 0, j
, maxtcpu
= 0, maxtpath
= 3; /* 3 for ellipsize() to work properly */
590 char buffer
[MAX3(21, FORMAT_BYTES_MAX
, FORMAT_TIMESPAN_MAX
)];
594 if (!terminal_is_dumb())
595 fputs(ANSI_HOME_CLEAR
, stdout
);
597 array
= alloca(sizeof(Group
*) * hashmap_size(a
));
599 HASHMAP_FOREACH(g
, a
, i
)
600 if (g
->n_tasks_valid
|| g
->cpu_valid
|| g
->memory_valid
|| g
->io_valid
)
603 qsort_safe(array
, n
, sizeof(Group
*), group_compare
);
605 /* Find the longest names in one run */
606 for (j
= 0; j
< n
; j
++) {
607 unsigned cputlen
, pathtlen
;
609 format_timespan(buffer
, sizeof(buffer
), (usec_t
) (array
[j
]->cpu_usage
/ NSEC_PER_USEC
), 0);
610 cputlen
= strlen(buffer
);
611 maxtcpu
= MAX(maxtcpu
, cputlen
);
613 pathtlen
= strlen(array
[j
]->path
);
614 maxtpath
= MAX(maxtpath
, pathtlen
);
617 if (arg_cpu_type
== CPU_PERCENT
)
618 xsprintf(buffer
, "%6s", "%CPU");
620 xsprintf(buffer
, "%*s", maxtcpu
, "CPU Time");
627 const char *on
, *off
;
629 path_columns
= columns() - 36 - strlen(buffer
);
630 if (path_columns
< 10)
633 on
= ansi_highlight_underline();
634 off
= ansi_underline();
636 printf("%s%s%-*s%s %s%7s%s %s%s%s %s%8s%s %s%8s%s %s%8s%s%s\n",
638 arg_order
== ORDER_PATH
? on
: "", path_columns
, "Control Group",
639 arg_order
== ORDER_PATH
? off
: "",
640 arg_order
== ORDER_TASKS
? on
: "", arg_count
== COUNT_PIDS
? "Tasks" : arg_count
== COUNT_USERSPACE_PROCESSES
? "Procs" : "Proc+",
641 arg_order
== ORDER_TASKS
? off
: "",
642 arg_order
== ORDER_CPU
? on
: "", buffer
,
643 arg_order
== ORDER_CPU
? off
: "",
644 arg_order
== ORDER_MEMORY
? on
: "", "Memory",
645 arg_order
== ORDER_MEMORY
? off
: "",
646 arg_order
== ORDER_IO
? on
: "", "Input/s",
647 arg_order
== ORDER_IO
? off
: "",
648 arg_order
== ORDER_IO
? on
: "", "Output/s",
649 arg_order
== ORDER_IO
? off
: "",
652 path_columns
= maxtpath
;
654 for (j
= 0; j
< n
; j
++) {
655 _cleanup_free_
char *ellipsized
= NULL
;
658 if (on_tty() && j
+ 6 > rows
)
663 path
= isempty(g
->path
) ? "/" : g
->path
;
664 ellipsized
= ellipsize(path
, path_columns
, 33);
665 printf("%-*s", path_columns
, ellipsized
?: path
);
667 if (g
->n_tasks_valid
)
668 printf(" %7" PRIu64
, g
->n_tasks
);
672 if (arg_cpu_type
== CPU_PERCENT
) {
674 printf(" %6.1f", g
->cpu_fraction
*100);
678 printf(" %*s", maxtcpu
, format_timespan(buffer
, sizeof(buffer
), (usec_t
) (g
->cpu_usage
/ NSEC_PER_USEC
), 0));
680 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->memory_valid
, g
->memory
));
681 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->io_valid
, g
->io_input_bps
));
682 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->io_valid
, g
->io_output_bps
));
688 static void help(void) {
689 printf("%s [OPTIONS...] [CGROUP]\n\n"
690 "Show top control groups by their resource usage.\n\n"
691 " -h --help Show this help\n"
692 " --version Show package version\n"
693 " -p --order=path Order by path\n"
694 " -t --order=tasks Order by number of tasks/processes\n"
695 " -c --order=cpu Order by CPU load (default)\n"
696 " -m --order=memory Order by memory load\n"
697 " -i --order=io Order by IO load\n"
698 " -r --raw Provide raw (not human-readable) numbers\n"
699 " --cpu=percentage Show CPU usage as percentage (default)\n"
700 " --cpu=time Show CPU usage as time\n"
701 " -P Count userspace processes instead of tasks (excl. kernel)\n"
702 " -k Count all processes instead of tasks (incl. kernel)\n"
703 " --recursive=BOOL Sum up process count recursively\n"
704 " -d --delay=DELAY Delay between updates\n"
705 " -n --iterations=N Run for N iterations before exiting\n"
706 " -b --batch Run in batch mode, accepting no input\n"
707 " --depth=DEPTH Maximum traversal depth (default: %u)\n"
708 " -M --machine= Show container\n"
709 , program_invocation_short_name
, arg_depth
);
712 static int parse_argv(int argc
, char *argv
[]) {
722 static const struct option options
[] = {
723 { "help", no_argument
, NULL
, 'h' },
724 { "version", no_argument
, NULL
, ARG_VERSION
},
725 { "delay", required_argument
, NULL
, 'd' },
726 { "iterations", required_argument
, NULL
, 'n' },
727 { "batch", no_argument
, NULL
, 'b' },
728 { "raw", no_argument
, NULL
, 'r' },
729 { "depth", required_argument
, NULL
, ARG_DEPTH
},
730 { "cpu", optional_argument
, NULL
, ARG_CPU_TYPE
},
731 { "order", required_argument
, NULL
, ARG_ORDER
},
732 { "recursive", required_argument
, NULL
, ARG_RECURSIVE
},
733 { "machine", required_argument
, NULL
, 'M' },
742 while ((c
= getopt_long(argc
, argv
, "hptcmin:brd:kPM:", options
, NULL
)) >= 0)
755 if (streq(optarg
, "time"))
756 arg_cpu_type
= CPU_TIME
;
757 else if (streq(optarg
, "percentage"))
758 arg_cpu_type
= CPU_PERCENT
;
760 log_error("Unknown argument to --cpu=: %s", optarg
);
764 arg_cpu_type
= CPU_TIME
;
769 r
= safe_atou(optarg
, &arg_depth
);
771 log_error("Failed to parse depth parameter.");
778 r
= parse_sec(optarg
, &arg_delay
);
779 if (r
< 0 || arg_delay
<= 0) {
780 log_error("Failed to parse delay parameter.");
787 r
= safe_atou(optarg
, &arg_iterations
);
789 log_error("Failed to parse iterations parameter.");
804 arg_order
= ORDER_PATH
;
808 arg_order
= ORDER_TASKS
;
812 arg_order
= ORDER_CPU
;
816 arg_order
= ORDER_MEMORY
;
820 arg_order
= ORDER_IO
;
824 if (streq(optarg
, "path"))
825 arg_order
= ORDER_PATH
;
826 else if (streq(optarg
, "tasks"))
827 arg_order
= ORDER_TASKS
;
828 else if (streq(optarg
, "cpu"))
829 arg_order
= ORDER_CPU
;
830 else if (streq(optarg
, "memory"))
831 arg_order
= ORDER_MEMORY
;
832 else if (streq(optarg
, "io"))
833 arg_order
= ORDER_IO
;
835 log_error("Invalid argument to --order=: %s", optarg
);
841 arg_count
= COUNT_ALL_PROCESSES
;
845 arg_count
= COUNT_USERSPACE_PROCESSES
;
849 r
= parse_boolean(optarg
);
851 log_error("Failed to parse --recursive= argument: %s", optarg
);
856 arg_recursive_unset
= r
== 0;
860 arg_machine
= optarg
;
867 assert_not_reached("Unhandled option");
870 if (optind
== argc
- 1)
871 arg_root
= argv
[optind
];
872 else if (optind
< argc
) {
873 log_error("Too many arguments.");
880 static const char* counting_what(void) {
881 if (arg_count
== COUNT_PIDS
)
883 else if (arg_count
== COUNT_ALL_PROCESSES
)
884 return "all processes (incl. kernel)";
886 return "userspace processes (excl. kernel)";
889 int main(int argc
, char *argv
[]) {
891 Hashmap
*a
= NULL
, *b
= NULL
;
892 unsigned iteration
= 0;
893 usec_t last_refresh
= 0;
894 bool quit
= false, immediate_refresh
= false;
895 _cleanup_free_
char *root
= NULL
;
898 log_parse_environment();
901 r
= parse_argv(argc
, argv
);
905 r
= cg_mask_supported(&mask
);
907 log_error_errno(r
, "Failed to determine supported controllers: %m");
911 arg_count
= (mask
& CGROUP_MASK_PIDS
) ? COUNT_PIDS
: COUNT_USERSPACE_PROCESSES
;
913 if (arg_recursive_unset
&& arg_count
== COUNT_PIDS
) {
914 log_error("Non-recursive counting is only supported when counting processes, not tasks. Use -P or -k.");
918 r
= show_cgroup_get_path_and_warn(arg_machine
, arg_root
, &root
);
920 log_error_errno(r
, "Failed to get root control group path: %m");
923 log_debug("Cgroup path: %s", root
);
925 a
= hashmap_new(&string_hash_ops
);
926 b
= hashmap_new(&string_hash_ops
);
932 signal(SIGWINCH
, columns_lines_cache_reset
);
934 if (arg_iterations
== (unsigned) -1)
935 arg_iterations
= on_tty() ? 0 : 1;
941 char h
[FORMAT_TIMESPAN_MAX
];
943 t
= now(CLOCK_MONOTONIC
);
945 if (t
>= last_refresh
+ arg_delay
|| immediate_refresh
) {
947 r
= refresh(root
, a
, b
, iteration
++);
949 log_error_errno(r
, "Failed to refresh: %m");
953 group_hashmap_clear(b
);
960 immediate_refresh
= false;
965 if (arg_iterations
&& iteration
>= arg_iterations
)
968 if (!on_tty()) /* non-TTY: Empty newline as delimiter between polls */
973 (void) usleep(last_refresh
+ arg_delay
- t
);
975 r
= read_one_char(stdin
, &key
, last_refresh
+ arg_delay
- t
, NULL
);
979 log_error_errno(r
, "Couldn't read key: %m");
984 if (on_tty()) { /* TTY: Clear any user keystroke */
985 fputs("\r \r", stdout
);
995 immediate_refresh
= true;
1003 arg_order
= ORDER_PATH
;
1007 arg_order
= ORDER_TASKS
;
1011 arg_order
= ORDER_CPU
;
1015 arg_order
= ORDER_MEMORY
;
1019 arg_order
= ORDER_IO
;
1023 arg_cpu_type
= arg_cpu_type
== CPU_TIME
? CPU_PERCENT
: CPU_TIME
;
1027 arg_count
= arg_count
!= COUNT_ALL_PROCESSES
? COUNT_ALL_PROCESSES
: COUNT_PIDS
;
1028 fprintf(stdout
, "\nCounting: %s.", counting_what());
1034 arg_count
= arg_count
!= COUNT_USERSPACE_PROCESSES
? COUNT_USERSPACE_PROCESSES
: COUNT_PIDS
;
1035 fprintf(stdout
, "\nCounting: %s.", counting_what());
1041 if (arg_count
== COUNT_PIDS
)
1042 fprintf(stdout
, "\n\aCannot toggle recursive counting, not available in task counting mode.");
1044 arg_recursive
= !arg_recursive
;
1045 fprintf(stdout
, "\nRecursive process counting: %s", yes_no(arg_recursive
));
1052 if (arg_delay
< USEC_PER_SEC
)
1053 arg_delay
+= USEC_PER_MSEC
*250;
1055 arg_delay
+= USEC_PER_SEC
;
1057 fprintf(stdout
, "\nIncreased delay to %s.", format_timespan(h
, sizeof(h
), arg_delay
, 0));
1063 if (arg_delay
<= USEC_PER_MSEC
*500)
1064 arg_delay
= USEC_PER_MSEC
*250;
1065 else if (arg_delay
< USEC_PER_MSEC
*1250)
1066 arg_delay
-= USEC_PER_MSEC
*250;
1068 arg_delay
-= USEC_PER_SEC
;
1070 fprintf(stdout
, "\nDecreased delay to %s.", format_timespan(h
, sizeof(h
), arg_delay
, 0));
1078 #define ON ANSI_HIGHLIGHT
1079 #define OFF ANSI_NORMAL
1082 "\t<" ON
"p" OFF
"> By path; <" ON
"t" OFF
"> By tasks/procs; <" ON
"c" OFF
"> By CPU; <" ON
"m" OFF
"> By memory; <" ON
"i" OFF
"> By I/O\n"
1083 "\t<" ON
"+" OFF
"> Inc. delay; <" ON
"-" OFF
"> Dec. delay; <" ON
"%%" OFF
"> Toggle time; <" ON
"SPACE" OFF
"> Refresh\n"
1084 "\t<" ON
"P" OFF
"> Toggle count userspace processes; <" ON
"k" OFF
"> Toggle count all processes\n"
1085 "\t<" ON
"r" OFF
"> Count processes recursively; <" ON
"q" OFF
"> Quit");
1092 fprintf(stdout
, "\nUnknown key '\\x%x'. Ignoring.", key
);
1094 fprintf(stdout
, "\nUnknown key '%c'. Ignoring.", key
);
1104 group_hashmap_free(a
);
1105 group_hashmap_free(b
);
1107 return r
< 0 ? EXIT_FAILURE
: EXIT_SUCCESS
;