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

neomutt / neomutt / 21812029969

07 Feb 2026 11:49AM UTC coverage: 42.162% (-0.007%) from 42.169%
21812029969

push

github

flatcap
unify mutt_safe_path(), buf_save_path() and mutt_save_path()

Discussion: https://github.com/neomutt/neomutt/discussions/4769

Amp-Thread-ID: https://ampcode.com/threads/T-019c0f8f-4e1f-704c-ba7b-c3278da5ba5e
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Richard Russon <rich@flatcap.org>

0 of 4 new or added lines in 2 files covered. (0.0%)

381 existing lines in 6 files now uncovered.

11858 of 28125 relevant lines covered (42.16%)

447.35 hits per line

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

92.93
/cli/parse.c
1
/**
2
 * @file
3
 * Parse the Command Line
4
 *
5
 * @authors
6
 * Copyright (C) 2025 Richard Russon <rich@flatcap.org>
7
 *
8
 * @copyright
9
 * This program is free software: you can redistribute it and/or modify it under
10
 * the terms of the GNU General Public License as published by the Free Software
11
 * Foundation, either version 2 of the License, or (at your option) any later
12
 * version.
13
 *
14
 * This program is distributed in the hope that it will be useful, but WITHOUT
15
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17
 * details.
18
 *
19
 * You should have received a copy of the GNU General Public License along with
20
 * this program.  If not, see <http://www.gnu.org/licenses/>.
21
 */
22

23
/**
24
 * @page cli_parse Parse the Command Line
25
 *
26
 * Parse the Command Line
27
 */
28

29
#include "config.h"
30
#include <getopt.h>
31
#include <stdbool.h>
32
#include <stddef.h>
33
#include <string.h>
34
#include <sys/param.h> // IWYU pragma: keep
35
#include <unistd.h>
36
#include "mutt/lib.h"
37
#include "objects.h"
38

39
/**
40
 * LongOptions - Long option definitions for getopt_long()
41
 *
42
 * All short options have corresponding long options for clarity.
43
 * The syntax is backwards compatible - all original short options work.
44
 */
45
static const struct option LongOptions[] = {
46
  // clang-format off
47
  // Shared options
48
  { "command",             required_argument, NULL, 'e' },
49
  { "config",              required_argument, NULL, 'F' },
50
  { "debug-file",          required_argument, NULL, 'l' },
51
  { "debug-level",         required_argument, NULL, 'd' },
52
  { "mbox-type",           required_argument, NULL, 'm' },
53
  { "no-system-config",    no_argument,       NULL, 'n' },
54

55
  // Help options
56
  { "help",                no_argument,       NULL, 'h' },
57
  { "license",             no_argument,       NULL, 'L' },
58
  { "version",             no_argument,       NULL, 'v' },
59

60
  // Info options
61
  { "alias",               required_argument, NULL, 'A' },
62
  { "dump-changed-config", no_argument,       NULL, 'X' },
63
  { "dump-config",         no_argument,       NULL, 'D' },
64
  { "hide-sensitive",      no_argument,       NULL, 'S' },
65
  { "query",               required_argument, NULL, 'Q' },
66
  { "with-docs",           no_argument,       NULL, 'O' },
67

68
  // Send options
69
  { "attach",              required_argument, NULL, 'a' },
70
  { "bcc",                 required_argument, NULL, 'b' },
71
  { "cc",                  required_argument, NULL, 'c' },
72
  { "crypto",              no_argument,       NULL, 'C' },
73
  { "draft",               required_argument, NULL, 'H' },
74
  { "edit-message",        no_argument,       NULL, 'E' },
75
  { "include",             required_argument, NULL, 'i' },
76
  { "subject",             required_argument, NULL, 's' },
77

78
  // TUI options
79
  { "browser",             no_argument,       NULL, 'y' },
80
  { "check-any-mail",      no_argument,       NULL, 'z' },
81
  { "check-new-mail",      no_argument,       NULL, 'Z' },
82
  { "folder",              required_argument, NULL, 'f' },
83
  { "nntp-browser",        no_argument,       NULL, 'G' },
84
  { "nntp-server",         required_argument, NULL, 'g' },
85
  { "postponed",           no_argument,       NULL, 'p' },
86
  { "read-only",           no_argument,       NULL, 'R' },
87

88
  { NULL, 0, NULL, 0 },
89
  // clang-format on
90
};
91

92
/**
93
 * check_help_mode - Check for help mode
94
 * @param mode String to check
95
 * @retval enum #HelpMode, e.g. HM_CONFIG
96
 */
97
int check_help_mode(const char *mode)
10✔
98
{
99
  if (mutt_istr_equal(mode, "shared"))
10✔
100
    return HM_SHARED;
101
  if (mutt_istr_equal(mode, "help"))
9✔
102
    return HM_HELP;
103
  if (mutt_istr_equal(mode, "info"))
8✔
104
    return HM_INFO;
105
  if (mutt_istr_equal(mode, "send"))
7✔
106
    return HM_SEND;
107
  if (mutt_istr_equal(mode, "tui"))
6✔
108
    return HM_TUI;
109
  if (mutt_istr_equal(mode, "all"))
5✔
110
    return HM_ALL;
2✔
111
  return 0;
112
}
113

114
/**
115
 * mop_up - Eat multiple arguments
116
 * @param[in]  argc  Size of argument array
117
 * @param[in]  argv  Array of argument strings
118
 * @param[in]  index Where to start eating
119
 * @param[out] sa    Array for the results
120
 * @retval num Number of arguments eaten
121
 *
122
 * Some options, like `-A`, can take multiple arguments.
123
 * e.g. `-A apple` or `-A apple banana cherry`
124
 *
125
 * Copy the arguments into an array.
126
 */
127
static int mop_up(int argc, char *const *argv, int index, struct StringArray *sa)
146✔
128
{
129
  int count = 0;
130
  for (int i = index; i < argc; i++, count++)
161✔
131
  {
132
    // Stop if we find '--' or '-X'
133
    if ((argv[i][0] == '-') && (argv[i][1] != '\0'))
144✔
134
      break;
135

136
    ARRAY_ADD(sa, mutt_str_dup(argv[i]));
15✔
137
  }
138

139
  return count;
146✔
140
}
141

142
/**
143
 * cli_parse - Parse the Command Line
144
 * @param[in]  argc Number of arguments
145
 * @param[in]  argv Arguments
146
 * @param[out] cli  Results
147
 * @retval true Success
148
 */
149
bool cli_parse(int argc, char *const *argv, struct CommandLine *cli)
132✔
150
{
151
  if ((argc < 1) || !argv || !cli)
132✔
152
    return false;
153

154
  // Check for unsupported '=' syntax in options
155
  for (int i = 1; i < argc; i++)
387✔
156
  {
157
    if ((argv[i][0] == '-') && (argv[i][1] != '\0'))
258✔
158
    {
159
      const char *equals = strchr(argv[i], '=');
161✔
160
      if (equals)
161✔
161
      {
162
        // L10N: Neomutt doesn't support `-X=VALUE` or `--OPTION=VALUE`
163
        //       Use `-X VALUE` or `--OPTION VALUE` instead
UNCOV
164
        mutt_warning(_("Invalid option syntax: %s (use space instead of '=')"), argv[i]);
×
UNCOV
165
        cli->help.help = true;
×
UNCOV
166
        cli->help.is_set = true;
×
UNCOV
167
        return false;
×
168
      }
169
    }
170
  }
171

172
  // Any leading non-options must be addresses
173
  int count = mop_up(argc, argv, 1, &cli->send.addresses);
129✔
174
  if (count > 0)
129✔
175
  {
176
    cli->send.is_set = true;
7✔
177
    argc -= count;
7✔
178
    argv += count;
7✔
179
  }
180

181
  bool rc = true;
182

183
  opterr = 0; // We'll handle the errors
129✔
184
// Always initialise getopt() or the tests will fail
185
#if defined(BSD) || defined(__APPLE__)
186
  optreset = 1;
187
  optind = 1;
188
#else
189
  optind = 0;
129✔
190
#endif
191

192
  while (rc && (argc > 1) && (optind < argc))
298✔
193
  {
194
    int opt = getopt_long(argc, argv, "+:A:a:b:Cc:Dd:Ee:F:f:Gg:H:hi:l:m:nOpQ:RSs:vyZz",
169✔
195
                          LongOptions, NULL);
196
    switch (opt)
169✔
197
    {
198
      // ------------------------------------------------------------
199
      // Shared
200
      case 'F': // user config file
11✔
201
      {
202
        ARRAY_ADD(&cli->shared.user_files, mutt_str_dup(optarg));
11✔
203
        cli->shared.is_set = true;
11✔
204
        break;
11✔
205
      }
206
      case 'n': // no system config file
6✔
207
      {
208
        cli->shared.disable_system = true;
6✔
209
        cli->shared.is_set = true;
6✔
210
        break;
6✔
211
      }
212
      case 'e': // enter commands
4✔
213
      {
214
        ARRAY_ADD(&cli->shared.commands, mutt_str_dup(optarg));
4✔
215
        cli->shared.is_set = true;
4✔
216
        break;
4✔
217
      }
218
      case 'm': // mbox type
4✔
219
      {
220
        buf_strcpy(&cli->shared.mbox_type, optarg);
4✔
221
        cli->shared.is_set = true;
4✔
222
        break;
4✔
223
      }
224
      case 'd': // log level
5✔
225
      {
226
        buf_strcpy(&cli->shared.log_level, optarg);
5✔
227
        cli->shared.is_set = true;
5✔
228
        break;
5✔
229
      }
230
      case 'l': // log file
5✔
231
      {
232
        buf_strcpy(&cli->shared.log_file, optarg);
5✔
233
        cli->shared.is_set = true;
5✔
234
        break;
5✔
235
      }
236

237
      // ------------------------------------------------------------
238
      // Help
239
      case 'h': // help
13✔
240
      {
241
        if (optind < argc)
13✔
242
        {
243
          cli->help.mode = check_help_mode(argv[optind]);
10✔
244
          if (cli->help.mode != HM_NONE)
10✔
245
            optind++;
7✔
246
        }
247
        cli->help.help = true;
13✔
248
        cli->help.is_set = true;
13✔
249
        break;
13✔
250
      }
UNCOV
251
      case 'L': // license
×
252
      {
UNCOV
253
        cli->help.license = true;
×
UNCOV
254
        cli->help.is_set = true;
×
UNCOV
255
        break;
×
256
      }
257
      case 'v': // version, license
11✔
258
      {
259
        if (cli->help.version)
11✔
260
          cli->help.license = true;
3✔
261
        else
262
          cli->help.version = true;
8✔
263

264
        cli->help.is_set = true;
11✔
265
        break;
11✔
266
      }
267

268
      // ------------------------------------------------------------
269
      // Info
270
      case 'A': // alias lookup
6✔
271
      {
272
        ARRAY_ADD(&cli->info.alias_queries, mutt_str_dup(optarg));
6✔
273
        // '-A' can take multiple arguments
274
        optind += mop_up(argc, argv, optind, &cli->info.alias_queries);
6✔
275
        cli->info.is_set = true;
6✔
276
        break;
6✔
277
      }
278
      case 'D': // dump config, dump changed
8✔
279
      {
280
        if (cli->info.dump_config)
8✔
281
          cli->info.dump_changed = true;
2✔
282
        else
283
          cli->info.dump_config = true;
6✔
284

285
        cli->info.is_set = true;
8✔
286
        break;
8✔
287
      }
288
      case 'O': // one-liner help
3✔
289
      {
290
        cli->info.show_help = true;
3✔
291
        cli->info.is_set = true;
3✔
292
        break;
3✔
293
      }
294
      case 'Q': // query config&cli->send.attach
6✔
295
      {
296
        ARRAY_ADD(&cli->info.queries, mutt_str_dup(optarg));
6✔
297
        // '-Q' can take multiple arguments
298
        optind += mop_up(argc, argv, optind, &cli->info.queries);
6✔
299
        cli->info.is_set = true;
6✔
300
        break;
6✔
301
      }
302
      case 'S': // hide sensitive
3✔
303
      {
304
        cli->info.hide_sensitive = true;
3✔
305
        cli->info.is_set = true;
3✔
306
        break;
3✔
307
      }
UNCOV
308
      case 'X': // dump changed
×
309
      {
UNCOV
310
        cli->info.dump_config = true;
×
UNCOV
311
        cli->info.dump_changed = true;
×
UNCOV
312
        cli->info.is_set = true;
×
UNCOV
313
        break;
×
314
      }
315

316
      // ------------------------------------------------------------
317
      // Send
318
      case 'a': // attach file
5✔
319
      {
320
        ARRAY_ADD(&cli->send.attach, mutt_str_dup(optarg));
5✔
321
        // `-a` can take multiple arguments
322
        optind += mop_up(argc, argv, optind, &cli->send.attach);
5✔
323
        cli->send.is_set = true;
5✔
324
        break;
5✔
325
      }
326
      case 'b': // bcc:
4✔
327
      {
328
        ARRAY_ADD(&cli->send.bcc_list, mutt_str_dup(optarg));
4✔
329
        cli->send.is_set = true;
4✔
330
        break;
4✔
331
      }
332
      case 'C': // crypto
3✔
333
      {
334
        cli->send.use_crypto = true;
3✔
335
        cli->send.is_set = true;
3✔
336
        break;
3✔
337
      }
338
      case 'c': // cc:
4✔
339
      {
340
        ARRAY_ADD(&cli->send.cc_list, mutt_str_dup(optarg));
4✔
341
        cli->send.is_set = true;
4✔
342
        break;
4✔
343
      }
344
      case 'E': // edit file
3✔
345
      {
346
        cli->send.edit_infile = true;
3✔
347
        cli->send.is_set = true;
3✔
348
        break;
3✔
349
      }
350
      case 'H': // draft file
4✔
351
      {
352
        buf_strcpy(&cli->send.draft_file, optarg);
4✔
353
        cli->send.is_set = true;
4✔
354
        break;
4✔
355
      }
356
      case 'i': // include file
4✔
357
      {
358
        buf_strcpy(&cli->send.include_file, optarg);
4✔
359
        cli->send.is_set = true;
4✔
360
        break;
4✔
361
      }
362
      case 's': // subject:
4✔
363
      {
364
        buf_strcpy(&cli->send.subject, optarg);
4✔
365
        cli->send.is_set = true;
4✔
366
        break;
4✔
367
      }
368

369
      // ------------------------------------------------------------
370
      // TUI
371
      case 'f': // start folder
4✔
372
      {
373
        buf_strcpy(&cli->tui.folder, optarg);
4✔
374
        cli->tui.is_set = true;
4✔
375
        break;
4✔
376
      }
377
      case 'G': // list newsgroups
5✔
378
      {
379
        cli->tui.start_nntp = true;
5✔
380
        cli->tui.is_set = true;
5✔
381
        break;
5✔
382
      }
383
      case 'g': // news server
4✔
384
      {
385
        cli->tui.start_nntp = true;
4✔
386
        buf_strcpy(&cli->tui.nntp_server, optarg);
4✔
387
        cli->tui.is_set = true;
4✔
388
        break;
4✔
389
      }
390
      case 'p': // postponed
4✔
391
      {
392
        cli->tui.start_postponed = true;
4✔
393
        cli->tui.is_set = true;
4✔
394
        break;
4✔
395
      }
396
      case 'R': // read-only
4✔
397
      {
398
        cli->tui.read_only = true;
4✔
399
        cli->tui.is_set = true;
4✔
400
        break;
4✔
401
      }
402
      case 'y': // browser
4✔
403
      {
404
        cli->tui.start_browser = true;
4✔
405
        cli->tui.is_set = true;
4✔
406
        break;
4✔
407
      }
408
      case 'Z': // new mail
4✔
409
      {
410
        cli->tui.start_new_mail = true;
4✔
411
        cli->tui.is_set = true;
4✔
412
        break;
4✔
413
      }
414
      case 'z': // any mail
3✔
415
      {
416
        cli->tui.start_any_mail = true;
3✔
417
        cli->tui.is_set = true;
3✔
418
        break;
3✔
419
      }
420

421
      // ------------------------------------------------------------
422
      case -1: // end of options
5✔
423
      {
424
        for (int i = optind; i < argc; i++)
10✔
425
        {
426
          ARRAY_ADD(&cli->send.addresses, mutt_str_dup(argv[i]));
5✔
427
        }
428
        cli->send.is_set = true;
5✔
429
        optind = argc; // finish parsing
5✔
430
        break;
5✔
431
      }
432
      default: // error
16✔
433
      {
434
        if (opt == '?')
16✔
435
        {
436
          // L10N: e.g. `neomutt -X`
437
          mutt_warning(_("Invalid option: %c"), optopt);
1✔
438
        }
439
        else if (opt == ':')
15✔
440
        {
441
          // L10N: e.g. `neomutt -F`
442
          mutt_warning(_("Option %c requires an argument"), optopt);
15✔
443
        }
444

445
        cli->help.help = true;
16✔
446
        cli->help.is_set = true;
16✔
447
        rc = false; // stop parsing
448
        break;
16✔
449
      }
450
    }
451
  }
452

453
  return rc;
454
}
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