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 107 : static int str_eq(const char *a, const char *b) {
28 107 : return strcmp(a, b) == 0;
29 : }
30 :
31 1 : static int parse_int(const char *s, int *out) {
32 1 : if (!s || !*s) return -1;
33 1 : char *end = NULL;
34 1 : long v = strtol(s, &end, 10);
35 1 : if (*end != '\0') return -1;
36 1 : *out = (int)v;
37 1 : 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 0 : static int take_value_flag(int argc, char **argv, int i,
46 : const char *name, const char **out) {
47 0 : if (i + 1 >= argc) {
48 0 : fprintf(stderr, "tg-cli: %s requires a value\n", name);
49 0 : return -1;
50 : }
51 0 : *out = argv[i + 1];
52 0 : return 2;
53 : }
54 :
55 0 : static int try_global_flag(int argc, char **argv, int i, ArgResult *out) {
56 0 : const char *a = argv[i];
57 :
58 0 : if (str_eq(a, "--json")) { out->json = 1; return 1; }
59 0 : if (str_eq(a, "--quiet")) { out->quiet = 1; return 1; }
60 0 : if (str_eq(a, "--help") || str_eq(a, "-h")) return TGF_HELP;
61 0 : if (str_eq(a, "--version") || str_eq(a, "-v")) return TGF_VERSION;
62 :
63 0 : if (str_eq(a, "--config"))
64 0 : 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 1 : static int parse_dialogs(int argc, char **argv, int i, ArgResult *out) {
78 1 : out->command = CMD_DIALOGS;
79 1 : out->limit = 20; /* default */
80 :
81 1 : while (i < argc) {
82 0 : if (str_eq(argv[i], "--limit")) {
83 0 : if (i + 1 >= argc) {
84 0 : fprintf(stderr, "tg-cli dialogs: --limit requires a number\n");
85 0 : return ARG_ERROR;
86 : }
87 0 : if (parse_int(argv[i + 1], &out->limit) != 0) {
88 0 : fprintf(stderr, "tg-cli dialogs: --limit value is not a number\n");
89 0 : return ARG_ERROR;
90 : }
91 0 : if (out->limit < 1 || out->limit > 1000) {
92 0 : fprintf(stderr,
93 : "tg-cli dialogs: --limit %d out of range [1, 1000]\n",
94 : out->limit);
95 0 : return ARG_ERROR;
96 : }
97 0 : i += 2;
98 0 : } else if (str_eq(argv[i], "--archived")) {
99 0 : out->archived = 1;
100 0 : i++;
101 : } else {
102 0 : fprintf(stderr, "tg-cli dialogs: unknown option: %s\n", argv[i]);
103 0 : return ARG_ERROR;
104 : }
105 : }
106 1 : return ARG_OK;
107 : }
108 :
109 1 : static int parse_history(int argc, char **argv, int i, ArgResult *out) {
110 1 : out->command = CMD_HISTORY;
111 1 : out->limit = 50; /* default */
112 1 : out->offset = 0;
113 :
114 1 : if (i >= argc) {
115 0 : fprintf(stderr, "tg-cli history: <peer> argument required\n");
116 0 : return ARG_ERROR;
117 : }
118 1 : if (argv[i][0] == '-') {
119 0 : fprintf(stderr, "tg-cli history: <peer> argument required before options\n");
120 0 : return ARG_ERROR;
121 : }
122 1 : out->peer = argv[i++];
123 :
124 1 : while (i < argc) {
125 0 : if (str_eq(argv[i], "--limit")) {
126 0 : if (i + 1 >= argc) {
127 0 : fprintf(stderr, "tg-cli history: --limit requires a number\n");
128 0 : return ARG_ERROR;
129 : }
130 0 : if (parse_int(argv[i + 1], &out->limit) != 0) {
131 0 : fprintf(stderr, "tg-cli history: --limit value is not a number\n");
132 0 : return ARG_ERROR;
133 : }
134 0 : if (out->limit < 1 || out->limit > 1000) {
135 0 : fprintf(stderr,
136 : "tg-cli history: --limit %d out of range [1, 1000]\n",
137 : out->limit);
138 0 : return ARG_ERROR;
139 : }
140 0 : i += 2;
141 0 : } else if (str_eq(argv[i], "--offset")) {
142 0 : if (i + 1 >= argc) {
143 0 : fprintf(stderr, "tg-cli history: --offset requires a number\n");
144 0 : return ARG_ERROR;
145 : }
146 0 : if (parse_int(argv[i + 1], &out->offset) != 0) {
147 0 : fprintf(stderr, "tg-cli history: --offset value is not a number\n");
148 0 : return ARG_ERROR;
149 : }
150 0 : i += 2;
151 0 : } else if (str_eq(argv[i], "--no-media")) {
152 0 : out->no_media = 1;
153 0 : i++;
154 : } else {
155 0 : fprintf(stderr, "tg-cli history: unknown option: %s\n", argv[i]);
156 0 : return ARG_ERROR;
157 : }
158 : }
159 1 : return ARG_OK;
160 : }
161 :
162 0 : static int parse_send(int argc, char **argv, int i, ArgResult *out) {
163 0 : out->command = CMD_SEND;
164 :
165 0 : if (i >= argc) {
166 0 : fprintf(stderr, "tg-cli send: <peer> argument required\n");
167 0 : return ARG_ERROR;
168 : }
169 0 : if (argv[i][0] == '-') {
170 0 : fprintf(stderr, "tg-cli send: <peer> argument required before options\n");
171 0 : return ARG_ERROR;
172 : }
173 0 : out->peer = argv[i++];
174 :
175 : /* Optional --reply <msg_id> before the message text. */
176 0 : 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 0 : if (i >= argc) {
197 : /* message may be provided via stdin; tg_cli checks isatty later. */
198 0 : return ARG_OK;
199 : }
200 0 : out->message = argv[i];
201 0 : return ARG_OK;
202 : }
203 :
204 1 : static int parse_search(int argc, char **argv, int i, ArgResult *out) {
205 1 : out->command = CMD_SEARCH;
206 1 : out->limit = 20; /* default */
207 :
208 1 : if (i >= argc) {
209 0 : fprintf(stderr, "tg-cli search: <query> argument required\n");
210 0 : return ARG_ERROR;
211 : }
212 :
213 : /* Optional peer before query: two consecutive non-flag positionals. */
214 1 : if (i + 1 < argc && argv[i][0] != '-' && argv[i + 1][0] != '-') {
215 0 : out->peer = argv[i++];
216 0 : out->query = argv[i++];
217 1 : } else if (argv[i][0] != '-') {
218 1 : out->query = argv[i++];
219 : } else {
220 0 : fprintf(stderr, "tg-cli search: <query> argument required\n");
221 0 : return ARG_ERROR;
222 : }
223 :
224 : /* Optional flags after the positional arguments. */
225 1 : while (i < argc) {
226 0 : if (str_eq(argv[i], "--limit")) {
227 0 : if (i + 1 >= argc) {
228 0 : fprintf(stderr, "tg-cli search: --limit requires a number\n");
229 0 : return ARG_ERROR;
230 : }
231 0 : int val = 0;
232 0 : 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 0 : if (val < 1 || val > 100) {
237 0 : fprintf(stderr,
238 : "tg-cli search: --limit %d out of range [1, 100]\n",
239 : val);
240 0 : return ARG_ERROR;
241 : }
242 0 : out->limit = val;
243 0 : 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 1 : return ARG_OK;
250 : }
251 :
252 1 : static int parse_contacts(int argc, char **argv, int i, ArgResult *out) {
253 : (void)argc; (void)argv; (void)i;
254 1 : out->command = CMD_CONTACTS;
255 1 : return ARG_OK;
256 : }
257 :
258 4 : static int parse_me(int argc, char **argv, int i, ArgResult *out) {
259 : (void)argc; (void)argv; (void)i;
260 4 : out->command = CMD_ME;
261 4 : return ARG_OK;
262 : }
263 :
264 1 : static int parse_watch(int argc, char **argv, int i, ArgResult *out) {
265 1 : out->command = CMD_WATCH;
266 1 : out->watch_interval = 30; /* default 30 s */
267 1 : while (i < argc) {
268 0 : if (str_eq(argv[i], "--peers")) {
269 0 : if (i + 1 >= argc) {
270 0 : fprintf(stderr, "tg-cli watch: --peers requires a value\n");
271 0 : return ARG_ERROR;
272 : }
273 0 : out->watch_peers = argv[i + 1]; /* comma-separated list stored as-is */
274 0 : i += 2;
275 0 : } else if (str_eq(argv[i], "--interval")) {
276 0 : if (i + 1 >= argc) {
277 0 : fprintf(stderr, "tg-cli watch: --interval requires a number\n");
278 0 : return ARG_ERROR;
279 : }
280 0 : int val = 0;
281 0 : if (parse_int(argv[i + 1], &val) != 0) {
282 0 : fprintf(stderr, "tg-cli watch: --interval value is not a number\n");
283 0 : return ARG_ERROR;
284 : }
285 0 : if (val < 2 || val > 3600) {
286 0 : fprintf(stderr,
287 : "tg-cli watch: --interval %d out of range [2, 3600]\n",
288 : val);
289 0 : return ARG_ERROR;
290 : }
291 0 : out->watch_interval = val;
292 0 : 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 1 : return ARG_OK;
299 : }
300 :
301 1 : static int parse_user_info(int argc, char **argv, int i, ArgResult *out) {
302 1 : out->command = CMD_USER_INFO;
303 :
304 1 : if (i >= argc) {
305 0 : fprintf(stderr, "tg-cli user-info: <peer> argument required\n");
306 0 : return ARG_ERROR;
307 : }
308 1 : out->peer = argv[i];
309 1 : return ARG_OK;
310 : }
311 :
312 1 : static int parse_download(int argc, char **argv, int i, ArgResult *out) {
313 1 : out->command = CMD_DOWNLOAD;
314 :
315 1 : if (i >= argc || argv[i][0] == '-') {
316 0 : fprintf(stderr, "tg-cli download: <peer> argument required\n");
317 0 : return ARG_ERROR;
318 : }
319 1 : out->peer = argv[i++];
320 :
321 1 : 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 1 : 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 1 : i++;
330 :
331 1 : 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 1 : return ARG_OK;
345 : }
346 :
347 : /* ---- Public API ---- */
348 :
349 13 : int arg_parse(int argc, char **argv, ArgResult *out) {
350 13 : if (!argv || !out) return ARG_ERROR;
351 :
352 13 : memset(out, 0, sizeof(*out));
353 13 : out->limit = -1; /* -1 means "not set" (subcommand may supply default) */
354 :
355 13 : int i = 1; /* skip argv[0] (program name) */
356 :
357 : /* Collect global flags that may appear before the subcommand */
358 13 : while (i < argc && argv[i][0] == '-') {
359 0 : int r = try_global_flag(argc, argv, i, out);
360 0 : if (r == TGF_HELP) return ARG_HELP;
361 0 : if (r == TGF_VERSION) return ARG_VERSION;
362 0 : if (r < 0) return ARG_ERROR;
363 0 : if (r == 0) break; /* unknown flag — let subcommand handle */
364 0 : i += r;
365 : }
366 :
367 13 : if (i >= argc) {
368 : /* No subcommand given */
369 0 : if (out->json) {
370 0 : fprintf(stderr, "tg-cli: subcommand required\n");
371 0 : return ARG_ERROR;
372 : }
373 : /* Interactive mode — no subcommand is fine */
374 0 : return ARG_OK;
375 : }
376 :
377 13 : const char *subcmd = argv[i++];
378 :
379 13 : if (str_eq(subcmd, "dialogs")) return parse_dialogs (argc, argv, i, out);
380 12 : if (str_eq(subcmd, "history")) return parse_history (argc, argv, i, out);
381 11 : if (str_eq(subcmd, "send")) return parse_send (argc, argv, i, out);
382 11 : if (str_eq(subcmd, "search")) return parse_search (argc, argv, i, out);
383 10 : if (str_eq(subcmd, "contacts")) return parse_contacts (argc, argv, i, out);
384 9 : if (str_eq(subcmd, "user-info")) return parse_user_info(argc, argv, i, out);
385 8 : if (str_eq(subcmd, "me") || str_eq(subcmd, "self"))
386 4 : return parse_me (argc, argv, i, out);
387 4 : if (str_eq(subcmd, "watch")) return parse_watch (argc, argv, i, out);
388 3 : if (str_eq(subcmd, "download")) return parse_download (argc, argv, i, out);
389 2 : 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 2 : 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 2 : if (str_eq(subcmd, "send-file") || str_eq(subcmd, "upload")) {
430 0 : out->command = CMD_SEND_FILE;
431 0 : if (i + 1 >= argc) {
432 0 : fprintf(stderr, "tg-cli send-file: <peer> <path> required\n");
433 0 : return ARG_ERROR;
434 : }
435 0 : out->peer = argv[i++];
436 0 : out->out_path = argv[i++]; /* reuse out_path as local file */
437 0 : while (i < argc) {
438 0 : if (str_eq(argv[i], "--caption")) {
439 0 : if (i + 1 >= argc) {
440 0 : fprintf(stderr, "tg-cli send-file: --caption needs text\n");
441 0 : return ARG_ERROR;
442 : }
443 0 : out->message = argv[i + 1];
444 0 : 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 0 : return ARG_OK;
452 : }
453 2 : 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 2 : 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 2 : if (str_eq(subcmd, "login")) {
485 2 : out->command = CMD_LOGIN;
486 5 : while (i < argc) {
487 3 : if (str_eq(argv[i], "--api-id")) {
488 1 : if (i + 1 >= argc) {
489 0 : fprintf(stderr, "tg-cli login: --api-id requires a value\n");
490 0 : return ARG_ERROR;
491 : }
492 1 : out->api_id_str = argv[i + 1];
493 1 : i += 2;
494 2 : } else if (str_eq(argv[i], "--api-hash")) {
495 1 : if (i + 1 >= argc) {
496 0 : fprintf(stderr, "tg-cli login: --api-hash requires a value\n");
497 0 : return ARG_ERROR;
498 : }
499 1 : out->api_hash_str = argv[i + 1];
500 1 : i += 2;
501 1 : } else if (str_eq(argv[i], "--force")) {
502 1 : out->force = 1;
503 1 : i++;
504 : } else {
505 0 : fprintf(stderr, "tg-cli login: unknown option: %s\n", argv[i]);
506 0 : return ARG_ERROR;
507 : }
508 : }
509 2 : return ARG_OK;
510 : }
511 0 : if (str_eq(subcmd, "help")) return ARG_HELP;
512 0 : if (str_eq(subcmd, "version")) return ARG_VERSION;
513 :
514 0 : fprintf(stderr, "tg-cli: unknown subcommand: %s\n", subcmd);
515 0 : 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 : }
|