From b32f7e839bb63e619b44db190f48d28580d75e4b Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 2 Jan 2026 20:12:01 +0100 Subject: [PATCH] Added input_hidden() to ask for passwords in the CLI without echoing the chars. (0.37.37) --- CMakeLists.txt | 2 +- examples/input_hidden_example.fun | 29 ++++++++++++ examples/input_hidden_pam_auth.fun | 45 +++++++++++++++++++ lib/io/console.fun | 9 ++++ src/parser.c | 14 ++++++ src/vm.c | 6 +++ src/vm/io/input_line.c | 71 ++++++++++++++++++++++++++++-- 7 files changed, 172 insertions(+), 4 deletions(-) create mode 100755 examples/input_hidden_example.fun create mode 100755 examples/input_hidden_pam_auth.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index c5d90af..c841589 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.37.36 LANGUAGES C) +project(fun VERSION 0.37.37 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/examples/input_hidden_example.fun b/examples/input_hidden_example.fun new file mode 100755 index 0000000..770f69f --- /dev/null +++ b/examples/input_hidden_example.fun @@ -0,0 +1,29 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Example: Demonstrate hidden input (no echo) for passwords. + * + * Added: 2026-01-02 + */ + +#include + +print("=== input_hidden() example ===") + +c = Console() + +username = c.ask("Username") +password = c.ask_hidden("Password") + +// Do not print the password; just show the username +print(join(["Hello, ", username, "!"], "")) + +/* Expected output: +Username: hanez +Password: +=== input_hidden() example === +Hello, hanez! +*/ diff --git a/examples/input_hidden_pam_auth.fun b/examples/input_hidden_pam_auth.fun new file mode 100755 index 0000000..56d5fb7 --- /dev/null +++ b/examples/input_hidden_pam_auth.fun @@ -0,0 +1,45 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Example: Demonstrate hidden input (no echo) for passwords using + * /usr/bin/chkpwd from https://git.xw3.org/hanez/chkusr for PAM authetication. + * + * Added: 2026-01-02 + */ + +#include + +print("=== input_hidden() example with PAM authentication ===") + +c = Console() + +username = c.ask("Username") +password = c.ask_hidden("Password") + +// Do not print the password; just show the username +print(join(["Hello, ", username, "!"], "")) + +res = proc_run("chkpwd -u " + username + " -p " + password) +content = res["out"] +if (res["code"] == 0) + print("Login success!") +else + print("Login failed!") + +/* Possible output: +Username: hanez +Password: +=== input_hidden() example with PAM authentication === +Hello, hanez! +Login success! + +Or: +Username: hanez +Password: +=== input_hidden() example with PAM authentication === +Hello, hanez! +Login failed! +*/ diff --git a/lib/io/console.fun b/lib/io/console.fun index 52c89ad..e2a0cd0 100644 --- a/lib/io/console.fun +++ b/lib/io/console.fun @@ -17,6 +17,15 @@ class Console() else return input(join([q, ": "], "")) + // Ask for a secret string (e.g., password) without echoing input + // Returns the entered string. A trailing newline is not echoed back. + fun ask_hidden(this, question) + q = to_string(question) + if (len(q) == 0) + return input_hidden("") + else + return input_hidden(join([q, ": "], "")) + // Ask a yes/no question; returns 1 for yes, 0 for no // Accepts: y, yes, n, no (case-insensitive). Keeps asking until valid. fun ask_yes_no(this, question) diff --git a/src/parser.c b/src/parser.c index f5b69d9..67a4599 100644 --- a/src/parser.c +++ b/src/parser.c @@ -709,6 +709,20 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + if (strcmp(name, "input_hidden") == 0) { + (*pos)++; /* '(' */ + int hasPrompt = 0; + skip_spaces(src, len, pos); + if (*pos < len && src[*pos] != ')') { + if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "input_hidden expects 0 or 1 argument"); free(name); return 0; } + hasPrompt = 1; + } + if (!consume_char(src, len, pos, ')')) { parser_fail(*pos, "Expected ')' after input_hidden arg(s)"); free(name); return 0; } + /* operand bit0 = hasPrompt, bit1 = hidden */ + bytecode_add_instruction(bc, OP_INPUT_LINE, (hasPrompt ? 1 : 0) | 2); + free(name); + return 1; + } if (strcmp(name, "proc_run") == 0) { (*pos)++; /* '(' */ if (!emit_expression(bc, src, len, pos)) { parser_fail(*pos, "proc_run expects 1 argument (command string)"); free(name); return 0; } diff --git a/src/vm.c b/src/vm.c index f2a4470..d349967 100644 --- a/src/vm.c +++ b/src/vm.c @@ -29,9 +29,15 @@ #include #include #include +/* For hidden input (password) handling in OP_INPUT_LINE */ +#include //#include #endif +#ifdef _WIN32 +#include +#endif + /* Bring in split-out built-ins without changing the build system yet */ #include "iter.c" #include "map.c" diff --git a/src/vm/io/input_line.c b/src/vm/io/input_line.c index 8c624d2..938dda9 100644 --- a/src/vm/io/input_line.c +++ b/src/vm/io/input_line.c @@ -1,6 +1,10 @@ case OP_INPUT_LINE: { - /* operand: 0 = no prompt; 1 = has prompt (string or any value convertible to string) */ - int has_prompt = inst.operand ? 1 : 0; + /* operand bit flags: + * bit0 (1): has prompt (string or any value convertible to string) — top of stack holds prompt when set + * bit1 (2): hidden input (do not echo typed characters) + */ + int has_prompt = (inst.operand & 1) ? 1 : 0; + int hidden = (inst.operand & 2) ? 1 : 0; if (has_prompt) { /* pop prompt value and print without newline */ Value pv = pop_value(vm); @@ -13,6 +17,35 @@ case OP_INPUT_LINE: { free_value(pv); } + /* For hidden input, temporarily disable terminal echo if possible */ + int echo_disabled = 0; +#ifdef _WIN32 + if (hidden) { + HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); + if (hStdin != INVALID_HANDLE_VALUE) { + DWORD mode; + if (GetConsoleMode(hStdin, &mode)) { + DWORD newMode = mode & ~(ENABLE_ECHO_INPUT); + if (SetConsoleMode(hStdin, newMode)) { + echo_disabled = 1; + } + } + } + } +#else + if (hidden) { + /* POSIX termios */ + struct termios oldt; + if (tcgetattr(STDIN_FILENO, &oldt) == 0) { + struct termios newt = oldt; + newt.c_lflag &= ~(ECHO); + if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) == 0) { + echo_disabled = 1; + } + } + } +#endif + /* read a line from stdin, dynamically grow buffer */ size_t cap = 128; size_t len = 0; @@ -20,7 +53,8 @@ case OP_INPUT_LINE: { if (!buf) { fprintf(stderr, "Runtime error: out of memory reading input"); push_value(vm, make_string("")); - break; + /* On early exit, try to restore echo if we turned it off */ + goto restore_echo_and_break; } int ch; @@ -68,5 +102,36 @@ case OP_INPUT_LINE: { free(buf); push_done: + /* If we disabled echo, restore terminal settings and print a newline for UX */ +#ifdef _WIN32 + if (echo_disabled) { + HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); + if (hStdin != INVALID_HANDLE_VALUE) { + DWORD mode; + if (GetConsoleMode(hStdin, &mode)) { + /* Re-enable ECHO flag */ + mode |= ENABLE_ECHO_INPUT; + SetConsoleMode(hStdin, mode); + } + } + if (has_prompt) { + fputc('\n', stdout); + fflush(stdout); + } + } +#else + if (echo_disabled) { + struct termios t; + if (tcgetattr(STDIN_FILENO, &t) == 0) { + t.c_lflag |= ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &t); + } + if (has_prompt) { + fputc('\n', stdout); + fflush(stdout); + } + } +#endif +restore_echo_and_break: break; }