/** * tcsgrep.c - match input against terminal control sequence patterns * * Copyright (C) 2020 CismonX * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H # include "config.h" #endif // HAVE_CONFIG_H #include #include #include #include #include #include #include #include #include #define TCSGREP_NINTH_ARG_(a1, a2, a3, a4, a5, a6, a7, a8, a9, ...) a9 #define TCSGREP_COUNT_ARGS_(...) TCSGREP_NINTH_ARG_(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0) #define TCSGREP_CONCAT_1_(a1) a1 #define TCSGREP_CONCAT_2_(a1, a2) a1 a2 #define TCSGREP_CONCAT_3_(a1, a2, a3) a1 a2 a3 #define TCSGREP_CONCAT_4_(a1, a2, a3, a4) a1 a2 a3 a4 #define TCSGREP_CONCAT_5_(a1, a2, a3, a4, a5) a1 a2 a3 a4 a5 #define TCSGREP_CONCAT_6_(a1, a2, a3, a4, a5, a6) a1 a2 a3 a4 a5 a6 #define TCSGREP_CONCAT_7_(a1, a2, a3, a4, a5, a6, a7) a1 a2 a3 a4 a5 a6 a7 #define TCSGREP_CONCAT_8_(a1, a2, a3, a4, a5, a6, a7, a8) a1 a2 a3 a4 a5 a6 a7 a8 #define TCSGREP_CONCAT_(nargs) TCSGREP_CONCAT_##nargs##_ #define TCSGREP_DEFARGS_(nargs, ...) TCSGREP_CONCAT_(nargs)(__VA_ARGS__) #define TCSGREP_DEFSEQ_NOARGS(name) { #name, CTLSEQS_##name(), "" } #define TCSGREP_DEFSEQ(name, ...) \ { #name, CTLSEQS_##name(__VA_ARGS__), TCSGREP_DEFARGS_(TCSGREP_COUNT_ARGS_(__VA_ARGS__), __VA_ARGS__) } struct tcsgrep_sequence { char const *name; char const *pattern; char const args[8]; }; struct tcsgrep_ctx { char const *prog_name; int timeout; int limit; bool purge_long_seqs; bool verbose; bool not_tty; }; static inline void print_error(struct tcsgrep_ctx const *ctx, char const *msg) { dprintf(STDERR_FILENO, "%s: [error] %s.\n", ctx->prog_name, msg); } static inline bool parse_int(char const *str, int *dest) { errno = 0; unsigned long result = strtoul(str, NULL, 10); if (errno || result > 4096) { return false; } *dest = result; return true; } static inline void print_char(int ch) { static char const *ascii_table[] = { "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US" }; if (ch == ' ') { printf(" SP"); } else if (isprint(ch)) { printf(" %c", ch); } else if (ch == 0x7f) { printf(" DEL"); } else if (!iscntrl(ch)) { printf(" \\x%02x", ch); } else { printf(" %s", ascii_table[ch]); } } static void print_generic_seq(char const *header, union ctlseqs_value *result, bool newline) { size_t length = result[0].len; char const *seq = result[1].str; printf("%s %zu", header, length); for (size_t idx = 0; idx < length; ++idx) { print_char((unsigned)seq[idx]); } if (newline) { printf("\n"); } } static void print_matching_seq(struct tcsgrep_sequence *seq, union ctlseqs_value *result, bool verbose) { if (verbose) { print_generic_seq("OK", result, false); result += 2; } else { printf("OK"); } printf(" %s", seq->name); for (int idx = 0; idx < 8; ++idx) { char placeholder = seq->args[idx]; switch (placeholder) { case 0x0e: // CTLSEQS_PH_NUM printf(" %lu", result[idx].num); break; case 0x10: // CTLSEQS_PH_STR printf(" %.*s", (int)result[idx].len, result[idx + 1].str); break; case 0x0f: // CTLSEQS_PH_NUMS for (size_t i = 1; i <= result[idx].len; ++i) { printf(" %lu", result[idx + i].num); } break; } } printf("\n"); } int main(int argc, char **argv) { struct tcsgrep_ctx ctx = { .prog_name = argv[0], .timeout = -1, .limit = 4096, }; int opt; while (-1 != (opt = getopt(argc, argv, "t:l:pv"))) { switch (opt) { case 't': if (!parse_int(optarg, &ctx.timeout)) { print_error(&ctx, "invalid timeout option"); return 1; } break; case 'l': if (!parse_int(optarg, &ctx.limit)) { print_error(&ctx, "invalid limit option"); return 1; } break; case 'p': ctx.purge_long_seqs = true; break; case 'v': ctx.verbose = true; break; case '?': default: print_error(&ctx, "invalid arguments"); return 1; } } int flags = fcntl(STDIN_FILENO, F_GETFL); if (flags == -1) { print_error(&ctx, "failed to get file status flags"); return 1; } if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) { print_error(&ctx, "failed to set file status flags"); return 1; } struct ctlseqs_matcher *matcher = ctlseqs_matcher_init(); if (matcher == NULL) { print_error(&ctx, "failed to initialize matcher"); return 1; } static struct tcsgrep_sequence seqs[] = { TCSGREP_DEFSEQ_NOARGS(S7C1T), TCSGREP_DEFSEQ_NOARGS(S8C1T), TCSGREP_DEFSEQ_NOARGS(DECDHL_TOP), TCSGREP_DEFSEQ_NOARGS(DECDHL_BOTOM), TCSGREP_DEFSEQ_NOARGS(DECSWL), TCSGREP_DEFSEQ_NOARGS(DECDWL), TCSGREP_DEFSEQ_NOARGS(DECALN), TCSGREP_DEFSEQ_NOARGS(DECBI), TCSGREP_DEFSEQ_NOARGS(DECSC), TCSGREP_DEFSEQ_NOARGS(DECRC), TCSGREP_DEFSEQ_NOARGS(DECFI), TCSGREP_DEFSEQ_NOARGS(DECKPAM), TCSGREP_DEFSEQ_NOARGS(DECKPNM), TCSGREP_DEFSEQ_NOARGS(RIS), TCSGREP_DEFSEQ(DECUDK, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(DECRQSS, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(DECRSPS, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(XTGETXRES, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(XTSETTCAP, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(XTGETTCAP, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(ICH, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(SL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CUU, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CUD, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CUF, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CUB, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CNL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CPL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CHA, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CUP, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CHT, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(ED, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSED, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(EL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSEL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(IL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DCH, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(SU, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTSMGRAPHICS, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(SD, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTHIMOUSE, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTRMTITLE, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(ECH, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(CBT, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(HPA, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(HPR, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(REP, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(PRIMARY_DA), TCSGREP_DEFSEQ_NOARGS(TERTIARY_DA), TCSGREP_DEFSEQ_NOARGS(SECONDARY_DA), TCSGREP_DEFSEQ(VPA, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(VPR, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(HVP, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(TBC, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(SM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSET, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(MC, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(MC_DEC, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(RM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECRST, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(SGR, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTMODKEYS, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(DSR, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTMODKEYS_DISABLE, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DSR_DEC, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTSMPOINTER, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(DECSTR), TCSGREP_DEFSEQ(DECSCL, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECRQM_ANSI, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECRQM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(XTVERSION), TCSGREP_DEFSEQ(DECLL, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSCUSR, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSCA, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSTBM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTRESTORE, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(DECCARA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(SCOSC), TCSGREP_DEFSEQ(DECSLRM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTSAVE, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(XTWINOPS, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(XTSMTITLE, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(DECSWBV, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECRARA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(SCORC), TCSGREP_DEFSEQ(DECSMBV, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECCRA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECRQPSR, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECEFR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECREQTPARM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECEFR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECREQTPARM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSACE, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECFRA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTCHECKSUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECRQCRA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECELR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECERA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSLE, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(XTPUSHSGR, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(DECSERA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(XTREPORTSGR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECSCPP, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(DECRQLP), TCSGREP_DEFSEQ(DECSNLS, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(XTPOPSGR), TCSGREP_DEFSEQ(DECIC, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(DECDC, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(OSC_SEQ, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_DECRQSS, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_XTGETXRES, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_XTGETTCAP, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_PRIMARY_DA, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(RESP_SECONDARY_DA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(RESP_DECXCPR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(RESP_DSR, CTLSEQS_PH_NUMS), TCSGREP_DEFSEQ(RESP_DECRQM_ANSI, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(RESP_DECRQM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ(RESP_XTVERSION, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_DECCIR, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_DECTABSR, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_DECRQCRA, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), TCSGREP_DEFSEQ(RESP_DECRQLP, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), TCSGREP_DEFSEQ_NOARGS(KEY_UP), TCSGREP_DEFSEQ_NOARGS(KEY_DOWN), TCSGREP_DEFSEQ_NOARGS(KEY_RIGHT), TCSGREP_DEFSEQ_NOARGS(KEY_LEFT), TCSGREP_DEFSEQ_NOARGS(KEY_HOME), TCSGREP_DEFSEQ_NOARGS(KEY_END), TCSGREP_DEFSEQ_NOARGS(KEY_INSERT), TCSGREP_DEFSEQ_NOARGS(KEY_DELETE), TCSGREP_DEFSEQ_NOARGS(KEY_PGUP), TCSGREP_DEFSEQ_NOARGS(KEY_PGDN), TCSGREP_DEFSEQ_NOARGS(KEY_F1), TCSGREP_DEFSEQ_NOARGS(KEY_F2), TCSGREP_DEFSEQ_NOARGS(KEY_F3), TCSGREP_DEFSEQ_NOARGS(KEY_F4), TCSGREP_DEFSEQ_NOARGS(KEY_F5), TCSGREP_DEFSEQ_NOARGS(KEY_F6), TCSGREP_DEFSEQ_NOARGS(KEY_F7), TCSGREP_DEFSEQ_NOARGS(KEY_F8), TCSGREP_DEFSEQ_NOARGS(KEY_F9), TCSGREP_DEFSEQ_NOARGS(KEY_F10), TCSGREP_DEFSEQ_NOARGS(KEY_F11), TCSGREP_DEFSEQ_NOARGS(KEY_F12), }; size_t npatterns = sizeof(seqs) / sizeof(struct tcsgrep_sequence); char const **patterns = malloc(sizeof(char const *) * npatterns); if (patterns == NULL) { print_error(&ctx, "failed to allocate memory"); return 1; } for (size_t idx = 0; idx < npatterns; ++idx) { patterns[idx] = seqs[idx].pattern; } struct ctlseqs_matcher_options matcher_options = { .patterns = patterns, .npatterns = npatterns, }; if (ctlseqs_matcher_config(matcher, &matcher_options) != CTLSEQS_OK) { print_error(&ctx, "matcher setopt failed"); return 1; } struct ctlseqs_reader *reader = ctlseqs_reader_init(); if (reader == NULL) { print_error(&ctx, "failed to initialize reader"); } static union ctlseqs_value result[4096]; struct ctlseqs_reader_options reader_options = { .fd = STDIN_FILENO, .maxlen = ctx.limit, .result = result, .flags = ctx.verbose ? CTLSEQS_READER_SAVE_MATCHED_SEQS : 0, }; if (ctlseqs_reader_config(reader, &reader_options) != CTLSEQS_OK) { print_error(&ctx, "reader setopt failed"); return 1; } struct termios old_termios; if (tcgetattr(STDIN_FILENO, &old_termios) != 0) { ctx.not_tty = true; } else { struct termios new_termios = old_termios; new_termios.c_cc[VMIN] = 0; new_termios.c_cc[VTIME] = 0; new_termios.c_lflag &= ~(ICANON | ISIG | ECHO); if (tcsetattr(STDIN_FILENO, TCSANOW, &new_termios) != 0) { print_error(&ctx, "failed to set terminal attributes"); return 1; } } int status = 0; while (true) { ssize_t retval = ctlseqs_read(reader, matcher, ctx.timeout); switch (retval) { case CTLSEQS_ERROR: print_error(&ctx, "unexpected error"); status = 1; goto terminate; case CTLSEQS_TIMEOUT: printf("TIMEOUT\n"); status = 1; goto terminate; case CTLSEQS_INTR: printf("INTR\n"); break; case CTLSEQS_EOF: printf("EOF\n"); goto terminate; case CTLSEQS_PARTIAL: if (ctx.verbose) { print_generic_seq("PARTIAL", result, true); } break; case CTLSEQS_NOMATCH: print_generic_seq("NOMATCH", result, true); break; case CTLSEQS_NOMEM: print_generic_seq("NOMEM", result, true); if (ctx.purge_long_seqs) { ctlseqs_purge(reader, result[0].len); break; } else { status = 1; goto terminate; } case CTLSEQS_NOSEQ: print_generic_seq("NOSEQ", result, true); if (result[1].str[0] == 0x04) { goto terminate; } break; default: print_matching_seq(&seqs[retval], result, ctx.verbose); break; } } terminate: ctlseqs_matcher_free(matcher); ctlseqs_reader_free(reader); if (!ctx.not_tty) { tcsetattr(STDIN_FILENO, TCSANOW, &old_termios); } return status; }