• mini_repl.c

    From Michael Sanders@3:633/10 to All on Mon Nov 10 12:59:38 2025
    /*
    * mini_repl.c ? Mini REPL with persistent history
    * Michael Sanders - 2025 use as you see fit
    *
    * repl commands:
    * .q quit
    * .h show short help
    * .c clear the screen
    *
    * Full persistent history (~/.mini_repl_history)
    * via up/down keys, no history duplicates.
    *
    * Build: cc mini_repl.c -o mini_repl
    *
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <ctype.h>
    #include <unistd.h>
    #include <termios.h>
    #include <limits.h>

    #define MAXLINE 1024
    #define HISTORY_MAX 128
    #define HISTFILE ".mini_repl_history"

    static char *history[HISTORY_MAX];
    static int hist_count = 0;
    static int hist_index = -1;
    static char hist_path[PATH_MAX];

    /* -------- Helpers -------- */
    static void clear_screen(void) {
    fputs("\033[2J\033[H", stdout);
    fflush(stdout);
    }

    static void load_history(void) {
    const char *home = getenv("HOME");
    if (home) snprintf(hist_path, sizeof(hist_path), "%s/%s", home, HISTFILE);
    else strncpy(hist_path, HISTFILE, sizeof(hist_path) - 1);

    FILE *fp = fopen(hist_path, "r");
    if (!fp) return;

    char line[MAXLINE];
    while (fgets(line, sizeof(line), fp) && hist_count < HISTORY_MAX) {
    line[strcspn(line, "\r\n")] = '\0';
    if (*line) history[hist_count++] = strdup(line);
    }
    fclose(fp);
    }

    static void save_history(void) {
    FILE *fp = fopen(hist_path, "w");
    if (!fp) return;
    for (int i = 0; i < hist_count; i++) fprintf(fp, "%s\n", history[i]);
    fclose(fp);
    }

    static void add_history(const char *line) {
    if (!line || !*line) return;
    for (int i = 0; i < hist_count; i++) {
    if (strcmp(history[i], line) == 0) {
    free(history[i]);
    for (int j = i; j < hist_count - 1; j++) history[j] = history[j + 1];
    hist_count--;
    break;
    }
    }
    if (hist_count == HISTORY_MAX) {
    free(history[0]);
    memmove(&history[0], &history[1], sizeof(char*) * (HISTORY_MAX - 1));
    hist_count--;
    }
    history[hist_count++] = strdup(line);
    }

    /* -------- REPL -------- */
    int main(void) {
    load_history();

    struct termios orig, raw;
    tcgetattr(STDIN_FILENO, &orig);
    raw = orig;
    raw.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &raw);

    printf("Mini REPL (.h help, .c clear, .q quit)\n> ");
    fflush(stdout);

    char line[MAXLINE];
    int pos = 0;

    for (;;) {
    unsigned char c;
    if (read(STDIN_FILENO, &c, 1) != 1) break;

    if (c == '\n' || c == '\r') {
    line[pos] = '\0';

    /* BLANK: erase current line and re-prompt */
    if (pos == 0) {
    printf("\033[2K\r> ");
    fflush(stdout);
    continue;
    }

    /* ERASE input line before any output */
    printf("\033[2K\r");
    fflush(stdout);

    if (strcmp(line, ".q") == 0) {
    add_history(line);
    break;
    } else if (strcmp(line, ".c") == 0) {
    add_history(line);
    clear_screen();
    printf("> ");
    } else if (strcmp(line, ".h") == 0) {
    add_history(line);
    printf("Commands:\n .q Quit\n .c Clear screen\n .h Show help\n\n> ");
    } else {
    add_history(line);
    printf("You typed: %s\n> ", line);
    }

    fflush(stdout);
    pos = 0;
    hist_index = hist_count;
    continue;
    }

    if (c == 4) break; /* Ctrl+D */

    if (c == 127 || c == '\b') { /* backspace */
    if (pos > 0) {
    pos--;
    printf("\b \b");
    fflush(stdout);
    }
    continue;
    }

    if (c == 27) { /* arrows */
    unsigned char seq[2];
    if (read(STDIN_FILENO, seq, 2) == 2 && seq[0] == '[') {
    if (seq[1] == 'A') { /* up */
    if (hist_index > 0) hist_index--;
    if (hist_index >= 0 && hist_index < hist_count) {
    printf("\033[2K\r> %s", history[hist_index]);
    fflush(stdout);
    strcpy(line, history[hist_index]);
    pos = (int)strlen(line);
    }
    } else if (seq[1] == 'B') { /* down */
    if (hist_index < hist_count - 1) hist_index++;
    else hist_index = hist_count;
    if (hist_index < hist_count) {
    printf("\033[2K\r> %s", history[hist_index]);
    strcpy(line, history[hist_index]);
    pos = (int)strlen(line);
    } else {
    printf("\033[2K\r> ");
    pos = 0;
    line[0] = '\0';
    }
    fflush(stdout);
    }
    }
    continue;
    }

    if (isprint(c) && pos < MAXLINE - 1) {
    line[pos++] = c;
    putchar(c);
    fflush(stdout);
    }
    }

    tcsetattr(STDIN_FILENO, TCSANOW, &orig);
    save_history();
    for (int i = 0; i < hist_count; i++) free(history[i]);
    return 0;
    }

    --
    :wq
    Mike Sanders

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Scott Lurndal@3:633/10 to All on Mon Nov 10 15:19:08 2025
    Michael Sanders <porkchop@invalid.foo> writes:
    /*
    * mini_repl.c ? Mini REPL with persistent history
    * Michael Sanders - 2025 use as you see fit
    *
    * repl commands:
    * .q quit
    * .h show short help
    * .c clear the screen
    *
    * Full persistent history (~/.mini_repl_history)
    * via up/down keys, no history duplicates.
    *
    * Build: cc mini_repl.c -o mini_repl
    *
    */

    <code elided>

    So, what is REPL? What does it actually do?

    If you are using linux, I'd recommend using libreadline
    or libedit rather than manually dealing with 'command lines'.

    https://en.wikipedia.org/wiki/GNU_Readline

    I'd also recommend using ncurses rather than hardcoding
    escape sequences for a specific terminal.

    https://en.wikipedia.org/wiki/Ncurses

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Michael Sanders@3:633/10 to All on Mon Nov 10 15:31:33 2025
    On Mon, 10 Nov 2025 15:19:08 GMT, Scott Lurndal wrote:

    If you are using linux, I'd recommend using libreadline
    or libedit rather than manually dealing with 'command lines'.

    Not all who wander are lost (translation: Many thanks but
    I recommend teaching myself & building it myself).
    I like dealing with things it means less 3rd party
    stuff & extra libs. =)

    Folks who want it will know what to do.

    --
    :wq
    Mike Sanders

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Kaz Kylheku@3:633/10 to All on Mon Nov 10 23:24:53 2025
    On 2025-11-10, Michael Sanders <porkchop@invalid.foo> wrote:
    On Mon, 10 Nov 2025 15:19:08 GMT, Scott Lurndal wrote:

    If you are using linux, I'd recommend using libreadline
    or libedit rather than manually dealing with 'command lines'.

    Not all who wander are lost (translation: Many thanks but
    I recommend teaching myself & building it myself).
    I like dealing with things it means less 3rd party
    stuff & extra libs. =)

    Antirez's "Linenoise" library is one .h and one .c file you can
    put into your C project; then you have no external dependency
    on libedit or libreadline.

    (The latter is poison because it's GPLed, by the way).

    --
    TXR Programming Language: http://nongnu.org/txr
    Cygnal: Cygwin Native Application Library: http://kylheku.com/cygnal
    Mastodon: @Kazinator@mstdn.ca

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Michael Sanders@3:633/10 to All on Tue Nov 11 00:16:32 2025
    On Mon, 10 Nov 2025 23:24:53 -0000 (UTC), Kaz Kylheku wrote:

    Antirez's "Linenoise" library is one .h and one .c file you can
    put into your C project; then you have no external dependency
    on libedit or libreadline.

    (The latter is poison because it's GPLed, by the way).

    Thanks Kaz, really i want to figure it out. Messed around
    with tab completion too, probably overkill but learned some
    nifty things. GPLed/BSD clauses - shudder. Man I just want
    to learn...

    --
    :wq
    Mike Sanders

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Keith Thompson@3:633/10 to All on Mon Nov 10 17:14:24 2025
    Michael Sanders <porkchop@invalid.foo> writes:
    /*
    * mini_repl.c ? Mini REPL with persistent history
    * Michael Sanders - 2025 use as you see fit
    *
    * repl commands:
    * .q quit
    * .h show short help
    * .c clear the screen
    *
    * Full persistent history (~/.mini_repl_history)
    * via up/down keys, no history duplicates.
    *
    * Build: cc mini_repl.c -o mini_repl
    *
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <ctype.h>
    #include <unistd.h>
    #include <termios.h>
    #include <limits.h>

    #define MAXLINE 1024
    #define HISTORY_MAX 128
    #define HISTFILE ".mini_repl_history"

    static char *history[HISTORY_MAX];
    static int hist_count = 0;
    static int hist_index = -1;
    static char hist_path[PATH_MAX];

    /* -------- Helpers -------- */
    static void clear_screen(void) {
    fputs("\033[2J\033[H", stdout);
    fflush(stdout);
    }

    I think you call this only when the user requests it, which is fine.
    (I've seen too many programs that gratuitiously clear the screen,
    possibly erasing the user's information from previous commands.)

    You have several string literals containing terminal control sequences.
    Your code would be clearer if you had named "constants" for them.

    static void load_history(void) {
    const char *home = getenv("HOME");
    if (home) snprintf(hist_path, sizeof(hist_path), "%s/%s", home, HISTFILE);
    else strncpy(hist_path, HISTFILE, sizeof(hist_path) - 1);

    strncpy is not just a "safer" version of strcpy. It can leave the
    target array unterminated (not containing a string). I've written about
    it here:

    https://the-flat-trantor-society.blogspot.com/2012/03/no-strncpy-is-not-safer-strcpy.html

    If you want something like strcpy that truncates safely, set
    hist_path[0] to '\0' and use strncat. But consider whether truncation
    is really what you want. Imagine that a command containing a command
    like "rm -rf /home/yourname/temporary_directory" is truncated to "rm -rf /home/yourname/". Consider treating an overly long input as an error,
    at least for now. Later you can consider updating your program to
    handle arbitrarily long lines gracefully.

    PATH_MAX is typically 4096, so you're not likely to run into it -- but
    that makes it difficult to test your program's behavior with long lines.

    FILE *fp = fopen(hist_path, "r");
    if (!fp) return;

    char line[MAXLINE];
    while (fgets(line, sizeof(line), fp) && hist_count < HISTORY_MAX) {
    line[strcspn(line, "\r\n")] = '\0';

    It's unlikely that line will contain a '\r' character, even on Windows.
    In text mode, line endings are translated to a single '\n' on input, and
    vice versa on output. (On Unix-like systems, this translation is trivial.)

    If line doesn't contain a '\r' or '\n' character, you have undefined
    behavior. That can happen if the input is very long.

    if (*line) history[hist_count++] = strdup(line);

    It's worth being aware that strdup() is not specified by standard C
    prior to C23. It is specified by POSIX. You're already relying on
    nonstandard headers, so that's not really an issue.

    [...]

    I haven't read the entire program.

    --
    Keith Thompson (The_Other_Keith) Keith.S.Thompson+u@gmail.com
    void Void(void) { Void(); } /* The recursive call of the void */

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Keith Thompson@3:633/10 to All on Mon Nov 10 17:17:45 2025
    scott@slp53.sl.home (Scott Lurndal) writes:
    [...]
    I'd also recommend using ncurses rather than hardcoding
    escape sequences for a specific terminal.

    https://en.wikipedia.org/wiki/Ncurses

    One problem with ncurses is that it takes over the entire screen. It's
    good for a full-screen editor. If you want to, for example, handle
    arrow keys on input or edit a single input line, I don't know of a good
    way to do that with ncurses.

    --
    Keith Thompson (The_Other_Keith) Keith.S.Thompson+u@gmail.com
    void Void(void) { Void(); } /* The recursive call of the void */

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Michael Sanders@3:633/10 to All on Tue Nov 11 01:22:29 2025
    On Mon, 10 Nov 2025 17:14:24 -0800, Keith Thompson wrote:

    [...]

    Thanks Keith. Yeah needs more work.

    It's unlikely that line will contain a '\r' character, even on Windows.

    In 'cooked' mode I bet you're right, it raw mode? I'm not so sure now...

    --
    :wq
    Mike Sanders

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Michael S@3:633/10 to All on Tue Nov 11 12:19:45 2025
    On Mon, 10 Nov 2025 23:24:53 -0000 (UTC)
    Kaz Kylheku <643-408-1753@kylheku.com> wrote:

    On 2025-11-10, Michael Sanders <porkchop@invalid.foo> wrote:
    On Mon, 10 Nov 2025 15:19:08 GMT, Scott Lurndal wrote:

    If you are using linux, I'd recommend using libreadline
    or libedit rather than manually dealing with 'command lines'.

    Not all who wander are lost (translation: Many thanks but
    I recommend teaching myself & building it myself).
    I like dealing with things it means less 3rd party
    stuff & extra libs. =)

    Antirez's "Linenoise" library is one .h and one .c file you can
    put into your C project; then you have no external dependency
    on libedit or libreadline.


    Interesting.
    I'd look at it for my embedded work.
    Today, in embedded projects that feature command interpreter, I either
    use my own line edit/history code or one I stall from Microsoft ~20
    years ago (Microsoft's license from 20 years ago encourages stealing,
    as long as result isn't GPLed).
    I both cases I am not fully satisfied with behavior of command history.

    (The latter is poison because it's GPLed, by the way).


    I wouldn't use libreadline in OS-less or lite-OSed embedded software
    even if it had most anarchistic license in the World. It's too huge and convoluted and probably assumes things that I don't want assumed.



    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From candycanearter07@3:633/10 to All on Tue Nov 11 22:20:04 2025
    Michael Sanders <porkchop@invalid.foo> wrote at 00:16 this Tuesday (GMT):
    On Mon, 10 Nov 2025 23:24:53 -0000 (UTC), Kaz Kylheku wrote:

    Antirez's "Linenoise" library is one .h and one .c file you can
    put into your C project; then you have no external dependency
    on libedit or libreadline.

    (The latter is poison because it's GPLed, by the way).

    Thanks Kaz, really i want to figure it out. Messed around
    with tab completion too, probably overkill but learned some
    nifty things. GPLed/BSD clauses - shudder. Man I just want
    to learn...


    What's wrong with GPL licenses?
    --
    user <candycane> is generated from /dev/urandom

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)
  • From Michael Sanders@3:633/10 to All on Tue Nov 11 23:24:48 2025
    On Tue, 11 Nov 2025 22:20:04 -0000 (UTC), candycanearter07 wrote:

    Thanks Kaz, really i want to figure it out. Messed around
    with tab completion too, probably overkill but learned some
    nifty things. GPLed/BSD clauses - shudder. Man I just want
    to learn...

    What's wrong with GPL licenses?

    I think the whole licensing debate is silly. Speaking only
    for myself, too much time is wasted on just exactly how
    licensing details are to be applied. When i say 'shudder'
    I'm referring to legalese in the abstract. In other words
    you all figure it out, I'm just going to code & use my own
    licensing terms - its either free or you've got to pay.
    In the 2nd case, no sources are given. Makes the issue
    simple for me.

    --
    :wq
    Mike Sanders

    --- PyGate Linux v1.5
    * Origin: Dragon's Lair, PyGate NNTP<>Fido Gate (3:633/10)