Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file arg_parse.c
6 : * @brief Custom command-line argument parser for tg-cli.
7 : */
8 :
9 : #include "arg_parse.h"
10 :
11 : #include <stdio.h>
12 : #include <string.h>
13 : #include <stdlib.h>
14 :
15 : #define TG_CLI_VERSION "0.1.0"
16 :
17 : #ifndef TG_CLI_GIT_COMMIT
18 : # define TG_CLI_GIT_COMMIT "unknown"
19 : #endif
20 :
21 : /* ---- Internal helpers ---- */
22 :
23 : /* Sentinels returned only by try_global_flag — never exposed to callers. */
24 : #define TGF_HELP -10
25 : #define TGF_VERSION -11
26 :
27 680 : static int str_eq(const char *a, const char *b) {
28 680 : return strcmp(a, b) == 0;
29 : }
30 :
31 32 : static int parse_int(const char *s, int *out) {
32 32 : if (!s || !*s) return -1;
33 32 : char *end = NULL;
34 32 : long v = strtol(s, &end, 10);
35 32 : if (*end != '\0') return -1;
36 28 : *out = (int)v;
37 28 : return 0;
38 : }
39 :
40 : /* ---- Global flag parsing (returns number of argv entries consumed) ----
41 : *
42 : * Called for each positional slot; returns 0 if argv[i] is not a global
43 : * flag, 1 for a boolean flag, 2 for a flag that takes a value.
44 : */
45 2 : static int take_value_flag(int argc, char **argv, int i,
46 : const char *name, const char **out) {
47 2 : if (i + 1 >= argc) {
48 1 : fprintf(stderr, "tg-cli: %s requires a value\n", name);
49 1 : return -1;
50 : }
51 1 : *out = argv[i + 1];
52 1 : return 2;
53 : }
54 :
55 12 : static int try_global_flag(int argc, char **argv, int i, ArgResult *out) {
56 12 : const char *a = argv[i];
57 :
58 12 : if (str_eq(a, "--json")) { out->json = 1; return 1; }
59 7 : if (str_eq(a, "--quiet")) { out->quiet = 1; return 1; }
60 6 : if (str_eq(a, "--help") || str_eq(a, "-h")) return TGF_HELP;
61 4 : if (str_eq(a, "--version") || str_eq(a, "-v")) return TGF_VERSION;
62 :
63 2 : if (str_eq(a, "--config"))
64 2 : return take_value_flag(argc, argv, i, "--config", &out->config_path);
65 0 : if (str_eq(a, "--phone"))
66 0 : return take_value_flag(argc, argv, i, "--phone", &out->phone);
67 0 : if (str_eq(a, "--code"))
68 0 : return take_value_flag(argc, argv, i, "--code", &out->code);
69 0 : if (str_eq(a, "--password"))
70 0 : return take_value_flag(argc, argv, i, "--password", &out->password);
71 :
72 0 : return 0; /* not a global flag */
73 : }
74 :
75 : /* ---- Subcommand parsers ---- */
76 :
77 16 : static int parse_dialogs(int argc, char **argv, int i, ArgResult *out) {
78 16 : out->command = CMD_DIALOGS;
79 16 : out->limit = 20; /* default */
80 :
81 22 : while (i < argc) {
82 12 : if (str_eq(argv[i], "--limit")) {
83 9 : if (i + 1 >= argc) {
84 1 : fprintf(stderr, "tg-cli dialogs: --limit requires a number\n");
85 1 : return ARG_ERROR;
86 : }
87 8 : if (parse_int(argv[i + 1], &out->limit) != 0) {
88 1 : fprintf(stderr, "tg-cli dialogs: --limit value is not a number\n");
89 1 : return ARG_ERROR;
90 : }
91 7 : if (out->limit < 1 || out->limit > 1000) {
92 3 : fprintf(stderr,
93 : "tg-cli dialogs: --limit %d out of range [1, 1000]\n",
94 : out->limit);
95 3 : return ARG_ERROR;
96 : }
97 4 : i += 2;
98 3 : } else if (str_eq(argv[i], "--archived")) {
99 2 : out->archived = 1;
100 2 : i++;
101 : } else {
102 1 : fprintf(stderr, "tg-cli dialogs: unknown option: %s\n", argv[i]);
103 1 : return ARG_ERROR;
104 : }
105 : }
106 10 : return ARG_OK;
107 : }
108 :
109 18 : static int parse_history(int argc, char **argv, int i, ArgResult *out) {
110 18 : out->command = CMD_HISTORY;
111 18 : out->limit = 50; /* default */
112 18 : out->offset = 0;
113 :
114 18 : if (i >= argc) {
115 1 : fprintf(stderr, "tg-cli history: <peer> argument required\n");
116 1 : return ARG_ERROR;
117 : }
118 17 : if (argv[i][0] == '-') {
119 1 : fprintf(stderr, "tg-cli history: <peer> argument required before options\n");
120 1 : return ARG_ERROR;
121 : }
122 16 : out->peer = argv[i++];
123 :
124 21 : while (i < argc) {
125 13 : if (str_eq(argv[i], "--limit")) {
126 8 : if (i + 1 >= argc) {
127 1 : fprintf(stderr, "tg-cli history: --limit requires a number\n");
128 1 : return ARG_ERROR;
129 : }
130 7 : if (parse_int(argv[i + 1], &out->limit) != 0) {
131 1 : fprintf(stderr, "tg-cli history: --limit value is not a number\n");
132 1 : return ARG_ERROR;
133 : }
134 6 : if (out->limit < 1 || out->limit > 1000) {
135 3 : fprintf(stderr,
136 : "tg-cli history: --limit %d out of range [1, 1000]\n",
137 : out->limit);
138 3 : return ARG_ERROR;
139 : }
140 3 : i += 2;
141 5 : } else if (str_eq(argv[i], "--offset")) {
142 3 : if (i + 1 >= argc) {
143 1 : fprintf(stderr, "tg-cli history: --offset requires a number\n");
144 1 : return ARG_ERROR;
145 : }
146 2 : if (parse_int(argv[i + 1], &out->offset) != 0) {
147 1 : fprintf(stderr, "tg-cli history: --offset value is not a number\n");
148 1 : return ARG_ERROR;
149 : }
150 1 : i += 2;
151 2 : } else if (str_eq(argv[i], "--no-media")) {
152 1 : out->no_media = 1;
153 1 : i++;
154 : } else {
155 1 : fprintf(stderr, "tg-cli history: unknown option: %s\n", argv[i]);
156 1 : return ARG_ERROR;
157 : }
158 : }
159 8 : return ARG_OK;
160 : }
161 :
162 4 : static int parse_send(int argc, char **argv, int i, ArgResult *out) {
163 4 : out->command = CMD_SEND;
164 :
165 4 : if (i >= argc) {
166 1 : fprintf(stderr, "tg-cli send: <peer> argument required\n");
167 1 : return ARG_ERROR;
168 : }
169 3 : if (argv[i][0] == '-') {
170 1 : fprintf(stderr, "tg-cli send: <peer> argument required before options\n");
171 1 : return ARG_ERROR;
172 : }
173 2 : out->peer = argv[i++];
174 :
175 : /* Optional --reply <msg_id> before the message text. */
176 2 : while (i < argc && argv[i][0] == '-') {
177 0 : if (str_eq(argv[i], "--reply")) {
178 0 : if (i + 1 >= argc
179 0 : || parse_int(argv[i + 1], &out->reply_to) != 0
180 0 : || out->reply_to <= 0) {
181 0 : fprintf(stderr,
182 : "tg-cli send: --reply needs a positive message id\n");
183 0 : return ARG_ERROR;
184 : }
185 0 : i += 2;
186 0 : } else if (str_eq(argv[i], "--stdin")) {
187 : /* Explicit pipe opt-in; tg_cli will detect anyway, but this
188 : * is convenient in scripts that redirect stdin. */
189 0 : i++;
190 : } else {
191 0 : fprintf(stderr, "tg-cli send: unknown option: %s\n", argv[i]);
192 0 : return ARG_ERROR;
193 : }
194 : }
195 :
196 2 : if (i >= argc) {
197 : /* message may be provided via stdin; tg_cli checks isatty later. */
198 1 : return ARG_OK;
199 : }
200 1 : out->message = argv[i];
201 1 : return ARG_OK;
202 : }
203 :
204 13 : static int parse_search(int argc, char **argv, int i, ArgResult *out) {
205 13 : out->command = CMD_SEARCH;
206 13 : out->limit = 20; /* default */
207 :
208 13 : if (i >= argc) {
209 1 : fprintf(stderr, "tg-cli search: <query> argument required\n");
210 1 : return ARG_ERROR;
211 : }
212 :
213 : /* Optional peer before query: two consecutive non-flag positionals. */
214 12 : if (i + 1 < argc && argv[i][0] != '-' && argv[i + 1][0] != '-') {
215 2 : out->peer = argv[i++];
216 2 : out->query = argv[i++];
217 10 : } else if (argv[i][0] != '-') {
218 9 : out->query = argv[i++];
219 : } else {
220 1 : fprintf(stderr, "tg-cli search: <query> argument required\n");
221 1 : return ARG_ERROR;
222 : }
223 :
224 : /* Optional flags after the positional arguments. */
225 15 : while (i < argc) {
226 6 : if (str_eq(argv[i], "--limit")) {
227 6 : if (i + 1 >= argc) {
228 0 : fprintf(stderr, "tg-cli search: --limit requires a number\n");
229 2 : return ARG_ERROR;
230 : }
231 6 : int val = 0;
232 6 : if (parse_int(argv[i + 1], &val) != 0) {
233 0 : fprintf(stderr, "tg-cli search: --limit value is not a number\n");
234 0 : return ARG_ERROR;
235 : }
236 6 : if (val < 1 || val > 100) {
237 2 : fprintf(stderr,
238 : "tg-cli search: --limit %d out of range [1, 100]\n",
239 : val);
240 2 : return ARG_ERROR;
241 : }
242 4 : out->limit = val;
243 4 : i += 2;
244 : } else {
245 0 : fprintf(stderr, "tg-cli search: unknown option: %s\n", argv[i]);
246 0 : return ARG_ERROR;
247 : }
248 : }
249 9 : return ARG_OK;
250 : }
251 :
252 5 : static int parse_contacts(int argc, char **argv, int i, ArgResult *out) {
253 : (void)argc; (void)argv; (void)i;
254 5 : out->command = CMD_CONTACTS;
255 5 : return ARG_OK;
256 : }
257 :
258 8 : static int parse_me(int argc, char **argv, int i, ArgResult *out) {
259 : (void)argc; (void)argv; (void)i;
260 8 : out->command = CMD_ME;
261 8 : return ARG_OK;
262 : }
263 :
264 17 : static int parse_watch(int argc, char **argv, int i, ArgResult *out) {
265 17 : out->command = CMD_WATCH;
266 17 : out->watch_interval = 30; /* default 30 s */
267 24 : while (i < argc) {
268 12 : if (str_eq(argv[i], "--peers")) {
269 4 : if (i + 1 >= argc) {
270 1 : fprintf(stderr, "tg-cli watch: --peers requires a value\n");
271 1 : return ARG_ERROR;
272 : }
273 3 : out->watch_peers = argv[i + 1]; /* comma-separated list stored as-is */
274 3 : i += 2;
275 8 : } else if (str_eq(argv[i], "--interval")) {
276 8 : if (i + 1 >= argc) {
277 1 : fprintf(stderr, "tg-cli watch: --interval requires a number\n");
278 4 : return ARG_ERROR;
279 : }
280 7 : int val = 0;
281 7 : if (parse_int(argv[i + 1], &val) != 0) {
282 1 : fprintf(stderr, "tg-cli watch: --interval value is not a number\n");
283 1 : return ARG_ERROR;
284 : }
285 6 : if (val < 2 || val > 3600) {
286 2 : fprintf(stderr,
287 : "tg-cli watch: --interval %d out of range [2, 3600]\n",
288 : val);
289 2 : return ARG_ERROR;
290 : }
291 4 : out->watch_interval = val;
292 4 : i += 2;
293 : } else {
294 0 : fprintf(stderr, "tg-cli watch: unknown option: %s\n", argv[i]);
295 0 : return ARG_ERROR;
296 : }
297 : }
298 12 : return ARG_OK;
299 : }
300 :
301 4 : static int parse_user_info(int argc, char **argv, int i, ArgResult *out) {
302 4 : out->command = CMD_USER_INFO;
303 :
304 4 : if (i >= argc) {
305 1 : fprintf(stderr, "tg-cli user-info: <peer> argument required\n");
306 1 : return ARG_ERROR;
307 : }
308 3 : out->peer = argv[i];
309 3 : return ARG_OK;
310 : }
311 :
312 2 : static int parse_download(int argc, char **argv, int i, ArgResult *out) {
313 2 : out->command = CMD_DOWNLOAD;
314 :
315 2 : if (i >= argc || argv[i][0] == '-') {
316 0 : fprintf(stderr, "tg-cli download: <peer> argument required\n");
317 0 : return ARG_ERROR;
318 : }
319 2 : out->peer = argv[i++];
320 :
321 2 : if (i >= argc || argv[i][0] == '-') {
322 0 : fprintf(stderr, "tg-cli download: <msg_id> argument required\n");
323 0 : return ARG_ERROR;
324 : }
325 2 : if (parse_int(argv[i], &out->msg_id) != 0 || out->msg_id <= 0) {
326 0 : fprintf(stderr, "tg-cli download: <msg_id> must be a positive integer\n");
327 0 : return ARG_ERROR;
328 : }
329 2 : i++;
330 :
331 2 : while (i < argc) {
332 0 : if (str_eq(argv[i], "--out")) {
333 0 : if (i + 1 >= argc) {
334 0 : fprintf(stderr, "tg-cli download: --out requires a path\n");
335 0 : return ARG_ERROR;
336 : }
337 0 : out->out_path = argv[i + 1];
338 0 : i += 2;
339 : } else {
340 0 : fprintf(stderr, "tg-cli download: unknown option: %s\n", argv[i]);
341 0 : return ARG_ERROR;
342 : }
343 : }
344 2 : return ARG_OK;
345 : }
346 :
347 : /* ---- Public API ---- */
348 :
349 106 : int arg_parse(int argc, char **argv, ArgResult *out) {
350 106 : if (!argv || !out) return ARG_ERROR;
351 :
352 104 : memset(out, 0, sizeof(*out));
353 104 : out->limit = -1; /* -1 means "not set" (subcommand may supply default) */
354 :
355 104 : int i = 1; /* skip argv[0] (program name) */
356 :
357 : /* Collect global flags that may appear before the subcommand */
358 111 : while (i < argc && argv[i][0] == '-') {
359 12 : int r = try_global_flag(argc, argv, i, out);
360 12 : if (r == TGF_HELP) return ARG_HELP;
361 10 : if (r == TGF_VERSION) return ARG_VERSION;
362 8 : if (r < 0) return ARG_ERROR;
363 7 : if (r == 0) break; /* unknown flag — let subcommand handle */
364 7 : i += r;
365 : }
366 :
367 99 : if (i >= argc) {
368 : /* No subcommand given */
369 2 : if (out->json) {
370 1 : fprintf(stderr, "tg-cli: subcommand required\n");
371 1 : return ARG_ERROR;
372 : }
373 : /* Interactive mode — no subcommand is fine */
374 1 : return ARG_OK;
375 : }
376 :
377 97 : const char *subcmd = argv[i++];
378 :
379 97 : if (str_eq(subcmd, "dialogs")) return parse_dialogs (argc, argv, i, out);
380 81 : if (str_eq(subcmd, "history")) return parse_history (argc, argv, i, out);
381 63 : if (str_eq(subcmd, "send")) return parse_send (argc, argv, i, out);
382 59 : if (str_eq(subcmd, "search")) return parse_search (argc, argv, i, out);
383 46 : if (str_eq(subcmd, "contacts")) return parse_contacts (argc, argv, i, out);
384 41 : if (str_eq(subcmd, "user-info")) return parse_user_info(argc, argv, i, out);
385 37 : if (str_eq(subcmd, "me") || str_eq(subcmd, "self"))
386 8 : return parse_me (argc, argv, i, out);
387 29 : if (str_eq(subcmd, "watch")) return parse_watch (argc, argv, i, out);
388 12 : if (str_eq(subcmd, "download")) return parse_download (argc, argv, i, out);
389 10 : if (str_eq(subcmd, "edit")) {
390 0 : out->command = CMD_EDIT;
391 0 : if (i >= argc || argv[i][0] == '-') {
392 0 : fprintf(stderr, "tg-cli edit: <peer> required\n"); return ARG_ERROR;
393 : }
394 0 : out->peer = argv[i++];
395 0 : if (i >= argc || parse_int(argv[i], &out->msg_id) != 0
396 0 : || out->msg_id <= 0) {
397 0 : fprintf(stderr, "tg-cli edit: positive <msg_id> required\n");
398 0 : return ARG_ERROR;
399 : }
400 0 : i++;
401 0 : if (i >= argc) {
402 0 : fprintf(stderr, "tg-cli edit: <new-text> required\n");
403 0 : return ARG_ERROR;
404 : }
405 0 : out->message = argv[i];
406 0 : return ARG_OK;
407 : }
408 10 : if (str_eq(subcmd, "delete")) {
409 0 : out->command = CMD_DELETE;
410 0 : if (i >= argc || argv[i][0] == '-') {
411 0 : fprintf(stderr, "tg-cli delete: <peer> required\n"); return ARG_ERROR;
412 : }
413 0 : out->peer = argv[i++];
414 0 : if (i >= argc || parse_int(argv[i], &out->msg_id) != 0
415 0 : || out->msg_id <= 0) {
416 0 : fprintf(stderr, "tg-cli delete: positive <msg_id> required\n");
417 0 : return ARG_ERROR;
418 : }
419 0 : i++;
420 0 : while (i < argc) {
421 0 : if (str_eq(argv[i], "--revoke")) { out->revoke = 1; i++; }
422 : else {
423 0 : fprintf(stderr, "tg-cli delete: unknown option: %s\n", argv[i]);
424 0 : return ARG_ERROR;
425 : }
426 : }
427 0 : return ARG_OK;
428 : }
429 10 : if (str_eq(subcmd, "send-file") || str_eq(subcmd, "upload")) {
430 2 : out->command = CMD_SEND_FILE;
431 2 : if (i + 1 >= argc) {
432 0 : fprintf(stderr, "tg-cli send-file: <peer> <path> required\n");
433 0 : return ARG_ERROR;
434 : }
435 2 : out->peer = argv[i++];
436 2 : out->out_path = argv[i++]; /* reuse out_path as local file */
437 3 : while (i < argc) {
438 1 : if (str_eq(argv[i], "--caption")) {
439 1 : if (i + 1 >= argc) {
440 0 : fprintf(stderr, "tg-cli send-file: --caption needs text\n");
441 0 : return ARG_ERROR;
442 : }
443 1 : out->message = argv[i + 1];
444 1 : i += 2;
445 : } else {
446 0 : fprintf(stderr, "tg-cli send-file: unknown option: %s\n",
447 0 : argv[i]);
448 0 : return ARG_ERROR;
449 : }
450 : }
451 2 : return ARG_OK;
452 : }
453 8 : if (str_eq(subcmd, "forward")) {
454 0 : out->command = CMD_FORWARD;
455 0 : if (i + 2 >= argc) {
456 0 : fprintf(stderr,
457 : "tg-cli forward: <from_peer> <to_peer> <msg_id> required\n");
458 0 : return ARG_ERROR;
459 : }
460 0 : out->peer = argv[i++];
461 0 : out->peer2 = argv[i++];
462 0 : if (parse_int(argv[i], &out->msg_id) != 0 || out->msg_id <= 0) {
463 0 : fprintf(stderr, "tg-cli forward: positive <msg_id> required\n");
464 0 : return ARG_ERROR;
465 : }
466 0 : return ARG_OK;
467 : }
468 8 : if (str_eq(subcmd, "read")) {
469 0 : out->command = CMD_READ;
470 0 : if (i >= argc || argv[i][0] == '-') {
471 0 : fprintf(stderr, "tg-cli read: <peer> argument required\n");
472 0 : return ARG_ERROR;
473 : }
474 0 : out->peer = argv[i++];
475 0 : if (i < argc && str_eq(argv[i], "--max-id")) {
476 0 : if (i + 1 >= argc
477 0 : || parse_int(argv[i + 1], &out->msg_id) != 0) {
478 0 : fprintf(stderr, "tg-cli read: --max-id needs a number\n");
479 0 : return ARG_ERROR;
480 : }
481 : }
482 0 : return ARG_OK;
483 : }
484 8 : if (str_eq(subcmd, "login")) {
485 5 : out->command = CMD_LOGIN;
486 11 : while (i < argc) {
487 6 : if (str_eq(argv[i], "--api-id")) {
488 2 : if (i + 1 >= argc) {
489 0 : fprintf(stderr, "tg-cli login: --api-id requires a value\n");
490 0 : return ARG_ERROR;
491 : }
492 2 : out->api_id_str = argv[i + 1];
493 2 : i += 2;
494 4 : } else if (str_eq(argv[i], "--api-hash")) {
495 2 : if (i + 1 >= argc) {
496 0 : fprintf(stderr, "tg-cli login: --api-hash requires a value\n");
497 0 : return ARG_ERROR;
498 : }
499 2 : out->api_hash_str = argv[i + 1];
500 2 : i += 2;
501 2 : } else if (str_eq(argv[i], "--force")) {
502 2 : out->force = 1;
503 2 : i++;
504 : } else {
505 0 : fprintf(stderr, "tg-cli login: unknown option: %s\n", argv[i]);
506 0 : return ARG_ERROR;
507 : }
508 : }
509 5 : return ARG_OK;
510 : }
511 3 : if (str_eq(subcmd, "help")) return ARG_HELP;
512 2 : if (str_eq(subcmd, "version")) return ARG_VERSION;
513 :
514 1 : fprintf(stderr, "tg-cli: unknown subcommand: %s\n", subcmd);
515 1 : return ARG_ERROR;
516 : }
517 :
518 0 : void arg_print_version(void) {
519 0 : printf("tg-cli %s (git %s)\n", TG_CLI_VERSION, TG_CLI_GIT_COMMIT);
520 0 : }
|