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
) {
108 hashmap_clear_with_destructor(h
, group_free
);
111 static void group_hashmap_free(Hashmap
*h
) {
112 group_hashmap_clear(h
);
116 static const char *maybe_format_bytes(char *buf
, size_t l
, bool is_valid
, uint64_t t
) {
120 snprintf(buf
, l
, "%" PRIu64
, t
);
123 return format_bytes(buf
, l
, t
);
127 const char *controller
,
141 all_unified
= cg_all_unified();
145 g
= hashmap_get(a
, path
);
147 g
= hashmap_get(b
, path
);
153 g
->path
= strdup(path
);
159 r
= hashmap_put(a
, g
->path
, g
);
165 r
= hashmap_move_one(a
, b
, path
);
169 g
->cpu_valid
= g
->memory_valid
= g
->io_valid
= g
->n_tasks_valid
= false;
173 if (streq(controller
, SYSTEMD_CGROUP_CONTROLLER
) && IN_SET(arg_count
, COUNT_ALL_PROCESSES
, COUNT_USERSPACE_PROCESSES
)) {
174 _cleanup_fclose_
FILE *f
= NULL
;
177 r
= cg_enumerate_processes(controller
, path
, &f
);
184 while (cg_read_pid(f
, &pid
) > 0) {
186 if (arg_count
== COUNT_USERSPACE_PROCESSES
&& is_kernel_thread(pid
) > 0)
193 g
->n_tasks_valid
= true;
195 } else if (streq(controller
, "pids") && arg_count
== COUNT_PIDS
) {
196 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
198 r
= cg_get_path(controller
, path
, "pids.current", &p
);
202 r
= read_one_line_file(p
, &v
);
208 r
= safe_atou64(v
, &g
->n_tasks
);
213 g
->n_tasks_valid
= true;
215 } else if (streq(controller
, "cpu") || streq(controller
, "cpuacct")) {
216 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
221 const char *keys
[] = { "usage_usec", NULL
};
222 _cleanup_free_
char *val
= NULL
;
224 if (!streq(controller
, "cpu"))
227 r
= cg_get_keyed_attribute("cpu", path
, "cpu.stat", keys
, &val
);
233 r
= safe_atou64(val
, &new_usage
);
237 new_usage
*= NSEC_PER_USEC
;
239 if (!streq(controller
, "cpuacct"))
242 r
= cg_get_path(controller
, path
, "cpuacct.usage", &p
);
246 r
= read_one_line_file(p
, &v
);
252 r
= safe_atou64(v
, &new_usage
);
257 timestamp
= now_nsec(CLOCK_MONOTONIC
);
259 if (g
->cpu_iteration
== iteration
- 1 &&
260 (nsec_t
) new_usage
> g
->cpu_usage
) {
264 x
= timestamp
- g
->cpu_timestamp
;
268 y
= (nsec_t
) new_usage
- g
->cpu_usage
;
269 g
->cpu_fraction
= (double) y
/ (double) x
;
273 g
->cpu_usage
= (nsec_t
) new_usage
;
274 g
->cpu_timestamp
= timestamp
;
275 g
->cpu_iteration
= iteration
;
277 } else if (streq(controller
, "memory")) {
278 _cleanup_free_
char *p
= NULL
, *v
= NULL
;
281 r
= cg_get_path(controller
, path
, "memory.current", &p
);
283 r
= cg_get_path(controller
, path
, "memory.usage_in_bytes", &p
);
287 r
= read_one_line_file(p
, &v
);
293 r
= safe_atou64(v
, &g
->memory
);
298 g
->memory_valid
= true;
300 } else if ((streq(controller
, "io") && all_unified
) ||
301 (streq(controller
, "blkio") && !all_unified
)) {
302 _cleanup_fclose_
FILE *f
= NULL
;
303 _cleanup_free_
char *p
= NULL
;
304 uint64_t wr
= 0, rd
= 0;
307 r
= cg_get_path(controller
, path
, all_unified
? "io.stat" : "blkio.io_service_bytes", &p
);
319 char line
[LINE_MAX
], *l
;
322 if (!fgets(line
, sizeof(line
), f
))
325 /* Trim and skip the device */
327 l
+= strcspn(l
, WHITESPACE
);
328 l
+= strspn(l
, WHITESPACE
);
331 while (!isempty(l
)) {
332 if (sscanf(l
, "rbytes=%" SCNu64
, &k
))
334 else if (sscanf(l
, "wbytes=%" SCNu64
, &k
))
337 l
+= strcspn(l
, WHITESPACE
);
338 l
+= strspn(l
, WHITESPACE
);
341 if (first_word(l
, "Read")) {
344 } else if (first_word(l
, "Write")) {
350 l
+= strspn(l
, WHITESPACE
);
351 r
= safe_atou64(l
, &k
);
359 timestamp
= now_nsec(CLOCK_MONOTONIC
);
361 if (g
->io_iteration
== iteration
- 1) {
364 x
= (uint64_t) (timestamp
- g
->io_timestamp
);
368 if (rd
> g
->io_input
)
369 yr
= rd
- g
->io_input
;
373 if (wr
> g
->io_output
)
374 yw
= wr
- g
->io_output
;
378 if (yr
> 0 || yw
> 0) {
379 g
->io_input_bps
= (yr
* 1000000000ULL) / x
;
380 g
->io_output_bps
= (yw
* 1000000000ULL) / x
;
387 g
->io_timestamp
= timestamp
;
388 g
->io_iteration
= iteration
;
397 static int refresh_one(
398 const char *controller
,
406 _cleanup_closedir_
DIR *d
= NULL
;
414 if (depth
> arg_depth
)
417 r
= process(controller
, path
, a
, b
, iteration
, &ours
);
421 r
= cg_enumerate_subgroups(controller
, path
, &d
);
428 _cleanup_free_
char *fn
= NULL
, *p
= NULL
;
431 r
= cg_read_subgroup(d
, &fn
);
437 p
= strjoin(path
, "/", fn
);
441 path_kill_slashes(p
);
443 r
= refresh_one(controller
, p
, a
, b
, iteration
, depth
+ 1, &child
);
448 IN_SET(arg_count
, COUNT_ALL_PROCESSES
, COUNT_USERSPACE_PROCESSES
) &&
450 child
->n_tasks_valid
&&
451 streq(controller
, SYSTEMD_CGROUP_CONTROLLER
)) {
453 /* Recursively sum up processes */
455 if (ours
->n_tasks_valid
)
456 ours
->n_tasks
+= child
->n_tasks
;
458 ours
->n_tasks
= child
->n_tasks
;
459 ours
->n_tasks_valid
= true;
470 static int refresh(const char *root
, Hashmap
*a
, Hashmap
*b
, unsigned iteration
) {
475 r
= refresh_one(SYSTEMD_CGROUP_CONTROLLER
, root
, a
, b
, iteration
, 0, NULL
);
478 r
= refresh_one("cpu", root
, a
, b
, iteration
, 0, NULL
);
481 r
= refresh_one("cpuacct", root
, a
, b
, iteration
, 0, NULL
);
484 r
= refresh_one("memory", root
, a
, b
, iteration
, 0, NULL
);
487 r
= refresh_one("io", root
, a
, b
, iteration
, 0, NULL
);
490 r
= refresh_one("blkio", root
, a
, b
, iteration
, 0, NULL
);
493 r
= refresh_one("pids", root
, a
, b
, iteration
, 0, NULL
);
500 static int group_compare(const void*a
, const void *b
) {
501 const Group
*x
= *(Group
**)a
, *y
= *(Group
**)b
;
503 if (arg_order
!= ORDER_TASKS
|| arg_recursive
) {
504 /* Let's make sure that the parent is always before
505 * the child. Except when ordering by tasks and
506 * recursive summing is off, since that is actually
507 * not accumulative for all children. */
509 if (path_startswith(y
->path
, x
->path
))
511 if (path_startswith(x
->path
, y
->path
))
521 if (arg_cpu_type
== CPU_PERCENT
) {
522 if (x
->cpu_valid
&& y
->cpu_valid
) {
523 if (x
->cpu_fraction
> y
->cpu_fraction
)
525 else if (x
->cpu_fraction
< y
->cpu_fraction
)
527 } else if (x
->cpu_valid
)
529 else if (y
->cpu_valid
)
532 if (x
->cpu_usage
> y
->cpu_usage
)
534 else if (x
->cpu_usage
< y
->cpu_usage
)
541 if (x
->n_tasks_valid
&& y
->n_tasks_valid
) {
542 if (x
->n_tasks
> y
->n_tasks
)
544 else if (x
->n_tasks
< y
->n_tasks
)
546 } else if (x
->n_tasks_valid
)
548 else if (y
->n_tasks_valid
)
554 if (x
->memory_valid
&& y
->memory_valid
) {
555 if (x
->memory
> y
->memory
)
557 else if (x
->memory
< y
->memory
)
559 } else if (x
->memory_valid
)
561 else if (y
->memory_valid
)
567 if (x
->io_valid
&& y
->io_valid
) {
568 if (x
->io_input_bps
+ x
->io_output_bps
> y
->io_input_bps
+ y
->io_output_bps
)
570 else if (x
->io_input_bps
+ x
->io_output_bps
< y
->io_input_bps
+ y
->io_output_bps
)
572 } else if (x
->io_valid
)
574 else if (y
->io_valid
)
578 return path_compare(x
->path
, y
->path
);
581 static void display(Hashmap
*a
) {
586 unsigned rows
, n
= 0, j
, maxtcpu
= 0, maxtpath
= 3; /* 3 for ellipsize() to work properly */
587 char buffer
[MAX3(21, FORMAT_BYTES_MAX
, FORMAT_TIMESPAN_MAX
)];
591 if (!terminal_is_dumb())
592 fputs(ANSI_HOME_CLEAR
, stdout
);
594 array
= alloca(sizeof(Group
*) * hashmap_size(a
));
596 HASHMAP_FOREACH(g
, a
, i
)
597 if (g
->n_tasks_valid
|| g
->cpu_valid
|| g
->memory_valid
|| g
->io_valid
)
600 qsort_safe(array
, n
, sizeof(Group
*), group_compare
);
602 /* Find the longest names in one run */
603 for (j
= 0; j
< n
; j
++) {
604 unsigned cputlen
, pathtlen
;
606 format_timespan(buffer
, sizeof(buffer
), (usec_t
) (array
[j
]->cpu_usage
/ NSEC_PER_USEC
), 0);
607 cputlen
= strlen(buffer
);
608 maxtcpu
= MAX(maxtcpu
, cputlen
);
610 pathtlen
= strlen(array
[j
]->path
);
611 maxtpath
= MAX(maxtpath
, pathtlen
);
614 if (arg_cpu_type
== CPU_PERCENT
)
615 xsprintf(buffer
, "%6s", "%CPU");
617 xsprintf(buffer
, "%*s", maxtcpu
, "CPU Time");
624 const char *on
, *off
;
626 path_columns
= columns() - 36 - strlen(buffer
);
627 if (path_columns
< 10)
630 on
= ansi_highlight_underline();
631 off
= ansi_underline();
633 printf("%s%s%-*s%s %s%7s%s %s%s%s %s%8s%s %s%8s%s %s%8s%s%s\n",
635 arg_order
== ORDER_PATH
? on
: "", path_columns
, "Control Group",
636 arg_order
== ORDER_PATH
? off
: "",
637 arg_order
== ORDER_TASKS
? on
: "", arg_count
== COUNT_PIDS
? "Tasks" : arg_count
== COUNT_USERSPACE_PROCESSES
? "Procs" : "Proc+",
638 arg_order
== ORDER_TASKS
? off
: "",
639 arg_order
== ORDER_CPU
? on
: "", buffer
,
640 arg_order
== ORDER_CPU
? off
: "",
641 arg_order
== ORDER_MEMORY
? on
: "", "Memory",
642 arg_order
== ORDER_MEMORY
? off
: "",
643 arg_order
== ORDER_IO
? on
: "", "Input/s",
644 arg_order
== ORDER_IO
? off
: "",
645 arg_order
== ORDER_IO
? on
: "", "Output/s",
646 arg_order
== ORDER_IO
? off
: "",
649 path_columns
= maxtpath
;
651 for (j
= 0; j
< n
; j
++) {
652 _cleanup_free_
char *ellipsized
= NULL
;
655 if (on_tty() && j
+ 6 > rows
)
660 path
= isempty(g
->path
) ? "/" : g
->path
;
661 ellipsized
= ellipsize(path
, path_columns
, 33);
662 printf("%-*s", path_columns
, ellipsized
?: path
);
664 if (g
->n_tasks_valid
)
665 printf(" %7" PRIu64
, g
->n_tasks
);
669 if (arg_cpu_type
== CPU_PERCENT
) {
671 printf(" %6.1f", g
->cpu_fraction
*100);
675 printf(" %*s", maxtcpu
, format_timespan(buffer
, sizeof(buffer
), (usec_t
) (g
->cpu_usage
/ NSEC_PER_USEC
), 0));
677 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->memory_valid
, g
->memory
));
678 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->io_valid
, g
->io_input_bps
));
679 printf(" %8s", maybe_format_bytes(buffer
, sizeof(buffer
), g
->io_valid
, g
->io_output_bps
));
685 static void help(void) {
686 printf("%s [OPTIONS...] [CGROUP]\n\n"
687 "Show top control groups by their resource usage.\n\n"
688 " -h --help Show this help\n"
689 " --version Show package version\n"
690 " -p --order=path Order by path\n"
691 " -t --order=tasks Order by number of tasks/processes\n"
692 " -c --order=cpu Order by CPU load (default)\n"
693 " -m --order=memory Order by memory load\n"
694 " -i --order=io Order by IO load\n"
695 " -r --raw Provide raw (not human-readable) numbers\n"
696 " --cpu=percentage Show CPU usage as percentage (default)\n"
697 " --cpu=time Show CPU usage as time\n"
698 " -P Count userspace processes instead of tasks (excl. kernel)\n"
699 " -k Count all processes instead of tasks (incl. kernel)\n"
700 " --recursive=BOOL Sum up process count recursively\n"
701 " -d --delay=DELAY Delay between updates\n"
702 " -n --iterations=N Run for N iterations before exiting\n"
703 " -b --batch Run in batch mode, accepting no input\n"
704 " --depth=DEPTH Maximum traversal depth (default: %u)\n"
705 " -M --machine= Show container\n"
706 , program_invocation_short_name
, arg_depth
);
709 static int parse_argv(int argc
, char *argv
[]) {
719 static const struct option options
[] = {
720 { "help", no_argument
, NULL
, 'h' },
721 { "version", no_argument
, NULL
, ARG_VERSION
},
722 { "delay", required_argument
, NULL
, 'd' },
723 { "iterations", required_argument
, NULL
, 'n' },
724 { "batch", no_argument
, NULL
, 'b' },
725 { "raw", no_argument
, NULL
, 'r' },
726 { "depth", required_argument
, NULL
, ARG_DEPTH
},
727 { "cpu", optional_argument
, NULL
, ARG_CPU_TYPE
},
728 { "order", required_argument
, NULL
, ARG_ORDER
},
729 { "recursive", required_argument
, NULL
, ARG_RECURSIVE
},
730 { "machine", required_argument
, NULL
, 'M' },
739 while ((c
= getopt_long(argc
, argv
, "hptcmin:brd:kPM:", options
, NULL
)) >= 0)
752 if (streq(optarg
, "time"))
753 arg_cpu_type
= CPU_TIME
;
754 else if (streq(optarg
, "percentage"))
755 arg_cpu_type
= CPU_PERCENT
;
757 log_error("Unknown argument to --cpu=: %s", optarg
);
761 arg_cpu_type
= CPU_TIME
;
766 r
= safe_atou(optarg
, &arg_depth
);
768 log_error("Failed to parse depth parameter.");
775 r
= parse_sec(optarg
, &arg_delay
);
776 if (r
< 0 || arg_delay
<= 0) {
777 log_error("Failed to parse delay parameter.");
784 r
= safe_atou(optarg
, &arg_iterations
);
786 log_error("Failed to parse iterations parameter.");
801 arg_order
= ORDER_PATH
;
805 arg_order
= ORDER_TASKS
;
809 arg_order
= ORDER_CPU
;
813 arg_order
= ORDER_MEMORY
;
817 arg_order
= ORDER_IO
;
821 if (streq(optarg
, "path"))
822 arg_order
= ORDER_PATH
;
823 else if (streq(optarg
, "tasks"))
824 arg_order
= ORDER_TASKS
;
825 else if (streq(optarg
, "cpu"))
826 arg_order
= ORDER_CPU
;
827 else if (streq(optarg
, "memory"))
828 arg_order
= ORDER_MEMORY
;
829 else if (streq(optarg
, "io"))
830 arg_order
= ORDER_IO
;
832 log_error("Invalid argument to --order=: %s", optarg
);
838 arg_count
= COUNT_ALL_PROCESSES
;
842 arg_count
= COUNT_USERSPACE_PROCESSES
;
846 r
= parse_boolean(optarg
);
848 log_error("Failed to parse --recursive= argument: %s", optarg
);
853 arg_recursive_unset
= r
== 0;
857 arg_machine
= optarg
;
864 assert_not_reached("Unhandled option");
867 if (optind
== argc
- 1)
868 arg_root
= argv
[optind
];
869 else if (optind
< argc
) {
870 log_error("Too many arguments.");
877 static const char* counting_what(void) {
878 if (arg_count
== COUNT_PIDS
)
880 else if (arg_count
== COUNT_ALL_PROCESSES
)
881 return "all processes (incl. kernel)";
883 return "userspace processes (excl. kernel)";
886 int main(int argc
, char *argv
[]) {
888 Hashmap
*a
= NULL
, *b
= NULL
;
889 unsigned iteration
= 0;
890 usec_t last_refresh
= 0;
891 bool quit
= false, immediate_refresh
= false;
892 _cleanup_free_
char *root
= NULL
;
895 log_parse_environment();
898 r
= parse_argv(argc
, argv
);
902 r
= cg_mask_supported(&mask
);
904 log_error_errno(r
, "Failed to determine supported controllers: %m");
908 arg_count
= (mask
& CGROUP_MASK_PIDS
) ? COUNT_PIDS
: COUNT_USERSPACE_PROCESSES
;
910 if (arg_recursive_unset
&& arg_count
== COUNT_PIDS
) {
911 log_error("Non-recursive counting is only supported when counting processes, not tasks. Use -P or -k.");
915 r
= show_cgroup_get_path_and_warn(arg_machine
, arg_root
, &root
);
917 log_error_errno(r
, "Failed to get root control group path: %m");
920 log_debug("Cgroup path: %s", root
);
922 a
= hashmap_new(&string_hash_ops
);
923 b
= hashmap_new(&string_hash_ops
);
929 signal(SIGWINCH
, columns_lines_cache_reset
);
931 if (arg_iterations
== (unsigned) -1)
932 arg_iterations
= on_tty() ? 0 : 1;
938 char h
[FORMAT_TIMESPAN_MAX
];
940 t
= now(CLOCK_MONOTONIC
);
942 if (t
>= last_refresh
+ arg_delay
|| immediate_refresh
) {
944 r
= refresh(root
, a
, b
, iteration
++);
946 log_error_errno(r
, "Failed to refresh: %m");
950 group_hashmap_clear(b
);
957 immediate_refresh
= false;
962 if (arg_iterations
&& iteration
>= arg_iterations
)
965 if (!on_tty()) /* non-TTY: Empty newline as delimiter between polls */
970 (void) usleep(last_refresh
+ arg_delay
- t
);
972 r
= read_one_char(stdin
, &key
, last_refresh
+ arg_delay
- t
, NULL
);
976 log_error_errno(r
, "Couldn't read key: %m");
981 if (on_tty()) { /* TTY: Clear any user keystroke */
982 fputs("\r \r", stdout
);
992 immediate_refresh
= true;
1000 arg_order
= ORDER_PATH
;
1004 arg_order
= ORDER_TASKS
;
1008 arg_order
= ORDER_CPU
;
1012 arg_order
= ORDER_MEMORY
;
1016 arg_order
= ORDER_IO
;
1020 arg_cpu_type
= arg_cpu_type
== CPU_TIME
? CPU_PERCENT
: CPU_TIME
;
1024 arg_count
= arg_count
!= COUNT_ALL_PROCESSES
? COUNT_ALL_PROCESSES
: COUNT_PIDS
;
1025 fprintf(stdout
, "\nCounting: %s.", counting_what());
1031 arg_count
= arg_count
!= COUNT_USERSPACE_PROCESSES
? COUNT_USERSPACE_PROCESSES
: COUNT_PIDS
;
1032 fprintf(stdout
, "\nCounting: %s.", counting_what());
1038 if (arg_count
== COUNT_PIDS
)
1039 fprintf(stdout
, "\n\aCannot toggle recursive counting, not available in task counting mode.");
1041 arg_recursive
= !arg_recursive
;
1042 fprintf(stdout
, "\nRecursive process counting: %s", yes_no(arg_recursive
));
1049 if (arg_delay
< USEC_PER_SEC
)
1050 arg_delay
+= USEC_PER_MSEC
*250;
1052 arg_delay
+= USEC_PER_SEC
;
1054 fprintf(stdout
, "\nIncreased delay to %s.", format_timespan(h
, sizeof(h
), arg_delay
, 0));
1060 if (arg_delay
<= USEC_PER_MSEC
*500)
1061 arg_delay
= USEC_PER_MSEC
*250;
1062 else if (arg_delay
< USEC_PER_MSEC
*1250)
1063 arg_delay
-= USEC_PER_MSEC
*250;
1065 arg_delay
-= USEC_PER_SEC
;
1067 fprintf(stdout
, "\nDecreased delay to %s.", format_timespan(h
, sizeof(h
), arg_delay
, 0));
1075 #define ON ANSI_HIGHLIGHT
1076 #define OFF ANSI_NORMAL
1079 "\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"
1080 "\t<" ON
"+" OFF
"> Inc. delay; <" ON
"-" OFF
"> Dec. delay; <" ON
"%%" OFF
"> Toggle time; <" ON
"SPACE" OFF
"> Refresh\n"
1081 "\t<" ON
"P" OFF
"> Toggle count userspace processes; <" ON
"k" OFF
"> Toggle count all processes\n"
1082 "\t<" ON
"r" OFF
"> Count processes recursively; <" ON
"q" OFF
"> Quit");
1089 fprintf(stdout
, "\nUnknown key '\\x%x'. Ignoring.", key
);
1091 fprintf(stdout
, "\nUnknown key '%c'. Ignoring.", key
);
1101 group_hashmap_free(a
);
1102 group_hashmap_free(b
);
1104 return r
< 0 ? EXIT_FAILURE
: EXIT_SUCCESS
;