Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,14 @@ clean: ## Remove object files, keep the binary
# default set above does not, so a plain `make fclean` used to reach libft with
# SAFE unset -- landing in its SAFE!=1 branch, running the LTO capability probe
# and printing its warning just to delete files.
#
# The final `rm -rf build` cannot finish on NFS (the 42 machines' sgoinfre)
# while a shell started from build/bin is still running: NFS keeps a deleted
# file that is still open as build/bin/.nfsXXXX until its last user exits, and
# refuses to remove it ("Device or resource busy"). That made `make re` stop
# here with nothing rebuilt. Such a file is not ours to remove and does not
# block the rebuild, so fclean succeeds when only those remain; anything else
# left behind is retried and fails the target as before.
fclean: clean ## Remove objects, the binary and libft's build trees
@printf " \033[1;33m⚠\033[0m \033[1;37mRemoving binary\033[0m" >&2
@rm -f $(BIN_DIR)/$(BAPTIZE_SHELL)
Expand All @@ -496,7 +504,14 @@ fclean: clean ## Remove objects, the binary and libft's build trees
@$(MAKE) -C vendor/libft fclean SAFE=$(SAFE) BUILD_DIR=build-$(SAFE_TAG)
@rm -rf vendor/libft/build-ft vendor/libft/build-libc vendor/libft/build
@printf "\r\033[K \033[1;32m✓\033[0m \033[37mlibft cleaned\033[0m\n" >&2
@rm -rf build
@if ! rm -rf build 2>/dev/null; then \
if find build ! -type d ! -name '.nfs*' | grep -q .; then \
rm -rf build; \
else \
printf " \033[1;33m⚠\033[0m \033[37mkept %s: still open in a running process (NFS)\033[0m\n" \
"$$(find build -name '.nfs*' | xargs)" >&2; \
fi; \
fi
@printf "\n" >&2

# Run fclean THEN all, strictly in order. As plain prerequisites
Expand Down
61 changes: 59 additions & 2 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,65 @@
> 🔥 **hellish** is a fast, almost‑POSIX shell written from scratch in C —
> hackable, observable, and pleasant to live in.

This file is what the welcome banner points you at. It tracks what's new and
shows you how to drive the shell.
This file tracks what's new in every release and shows you how to drive the
shell. The welcome banner links to the guide for the latest one:
[What's new](https://univers42.github.io/hellish/whats-new/).

---

## v3.2.0 — *Up finds what you typed, a hint is one key away, and `bind`*

**The guide: [What's new in 3.2](https://univers42.github.io/hellish/whats-new/)**.
It covers every feature: the keys that reach it, and an example run on
3.2. The welcome banner shows the same list once after you update.

- **Up and Down search by what you typed.** `git<Up>` steps through the
history lines that start with `git`; on an empty line Up walks the whole
history. On by default; `HELLISH_HISTORY_PREFIX=0` turns it off. The
configs that ask for it now work too: bash's
`bind '"\e[A": history-search-backward'` and zsh's
`bindkey '^[[A' history-beginning-search-backward` (#136).
- **Inline hints**, opt-in with `export HELLISH_AUTOSUGGEST=1`. The newest
matching history line shows dim after the cursor. Right / End / Ctrl-F
take all of it, Alt-F / Ctrl-Right take one word, and Alt-N / Alt-P
show other matches (#140).
- **`make <TAB>`** completes the targets of the Makefile in the current
directory. The shell reads the file itself; make never runs (#140).
- **`bind`**, bash's builtin: key bindings and readline settings from
`~/.hellishrc`. `bind -x` runs a shell command on a key, and the command
can edit the line through `READLINE_LINE` / `READLINE_POINT`.
- **`bindkey`** in zsh mode reads zsh's `^X` notation and reaches the line
editor's own widgets.
- **`${var@P}`** renders a value through the PS1 renderer. **`$SRANDOM`**
and **`$EPOCHREALTIME`** are new variables.

Fixed on the way to the release:

- **The hint and prefix keys did nothing on some builds.** The shell found
readline's commands by comparing function addresses. On a non-PIE binary
linked against a readline built with `-Bsymbolic-functions` (Ubuntu's),
one function has two addresses, so nothing matched and every key kept
its stock meaning. The shell now asks readline for each command by name
(`rl_named_function`).
- **Two leaks in readline 8.1's line editing**, one undo list per line:
- after walking the history with Up or Ctrl-P;
- after cancelling an Alt-P search.

LeakSanitizer reported both. `tests/rl_undo_leak_test.py` covers ten
ways to reach them.
- **`which` in zsh mode.** Esc Esc (oh-my-zsh's `sudo` plugin) printed
`builtin: which: not a shell builtin`. zsh's `which [-ap]` now exists in
zsh mode.
- **`make re` on NFS** no longer stops in `fclean` on the `.nfsXXXX` file
a running `build/bin/hellish` keeps open.
- **The welcome banner** fits an 80-column terminal, and its frame stays
aligned under `LANG=C`.

About two dozen smaller fixes landed as well, among them PS2 on every
continuation line, `exec nosuch` no longer reading freed memory, `read`
at end of input, and heredocs in functions, `eval` and `source`. The guide
lists the ones you can see; the compare view has all of them:
[v3.1.3...v3.2.0](https://github.com/Univers42/hellish/compare/v3.1.3...v3.2.0).

---

Expand Down
187 changes: 94 additions & 93 deletions incs/executor.h
Original file line number Diff line number Diff line change
@@ -1,94 +1,95 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* executor.h :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: marvin <marvin@student.42.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/01/10 02:38:39 by marvin #+# #+# */
/* Updated: 2026/01/10 02:38:39 by marvin ### ########.fr */
/* */
/* ************************************************************************** */

/* Executor public API: turning an AST into side-effects.
The main entry is execute_top_level() which walks the tree returned by
the parser. Every execute_* function returns t_execution_state so the
exit code and any ctrl-c signal propagate up cleanly. */

#ifndef EXECUTOR_H
# define EXECUTOR_H

# include "public/executor_types.h"
# include "alias.h"

/* forward declarations to avoid heavy includes */
typedef struct s_shell t_shell;
typedef struct s_ast_node t_ast_node;

/* An expanded, ready-to-run simple command: pre-command assignments,
the argv array, and the resolved executable path. pooled=true means
argv.ctx comes from argv_pool (don't free it, just reset the vec). */
typedef struct executable_cmd_s
{
t_vec_env pre_assigns; /* NAME=VALUE pairs before the command */
t_vec argv; /* NULL-terminated argv word list */
char *fname; /* resolved path (heap) or NULL for builtins */
bool pooled; /* true if argv was borrowed from argv_pool */
} t_executable_cmd;

/* argv backing pool (free_utils2.c): borrow on expand, return on teardown. */
void argv_pool_acquire(t_shell *state, t_executable_cmd *cmd);
void argv_pool_release(t_shell *state, t_executable_cmd *cmd);
void free_argv_pool(t_shell *state);

/* Execution context for one node in the pipeline: which fds to hook up,
which AST node to execute, and whether this command runs in the parent
process (builtins, function calls) and is allowed to modify state. */
typedef struct executable_node_s
{
int infd; /* read end of pipe from previous stage */
int outfd; /* write end of pipe to next stage */
int next_infd; /* saved next read end (pipeline setup) */
t_ast_node *node; /* the AST node being executed */
t_vec_int redirs; /* indices into state->redirects */
bool modify_parent_ctx; /* true for builtins that modify state */
} t_executable_node;

void execute_top_level(t_shell *state);
int exec_string(t_shell *state, char *str);
int exec_file_string(t_shell *state, char *str,
const char *src);
t_execution_state execute_pipeline(t_shell *state,
t_executable_node *exe);
void set_up_redirection(t_shell *state,
t_executable_node *exe);
t_execution_state execute_simple_command(t_shell *state,
t_executable_node *exe);
void forward_exit_status(t_execution_state res);
t_execution_state execute_command(t_shell *state,
t_executable_node *exe);
t_execution_state execute_tree_node(t_shell *state,
t_executable_node *exe);
t_execution_state execute_simple_list(t_shell *state,
t_executable_node *exe);
t_string word_to_string(t_ast_node node);
t_string word_to_hrdoc_string(t_ast_node node);
void set_cmd_status(t_shell *state, t_execution_state res);
t_execution_state res_status(int status);
t_execution_state res_pid(int pid);
void fire_debug_trap(t_shell *state);
void fire_err_trap(t_shell *state, int code);
void fire_return_trap(t_shell *state, int code);
void trap_save_reset(t_shell *state, char **save);
void pseudo_traps_quiet(t_shell *state);
void trap_restore(t_shell *state, char **save);
void exe_res_set_status(t_shell *st, t_execution_state *res);
int find_cmd_path(t_shell *state, char *cmd_name,
char **path_of_exe);
int exec_external_argv(t_shell *state, t_vec *args);
int run_external_sync(t_shell *state, t_vec *args);
int expand_simple_command(t_shell *state, t_ast_node *node,
t_executable_cmd *ret, t_vec_int *redirects);
int (*builtin_func(char *name))(t_shell *state, t_vec argv);

/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* executor.h :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: marvin <marvin@student.42.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/01/10 02:38:39 by marvin #+# #+# */
/* Updated: 2026/01/10 02:38:39 by marvin ### ########.fr */
/* */
/* ************************************************************************** */

/* Executor public API: turning an AST into side-effects.
The main entry is execute_top_level() which walks the tree returned by
the parser. Every execute_* function returns t_execution_state so the
exit code and any ctrl-c signal propagate up cleanly. */

#ifndef EXECUTOR_H
# define EXECUTOR_H

# include "public/executor_types.h"
# include "alias.h"

/* forward declarations to avoid heavy includes */
typedef struct s_shell t_shell;
typedef struct s_ast_node t_ast_node;

/* An expanded, ready-to-run simple command: pre-command assignments,
the argv array, and the resolved executable path. pooled=true means
argv.ctx comes from argv_pool (don't free it, just reset the vec). */
typedef struct executable_cmd_s
{
t_vec_env pre_assigns; /* NAME=VALUE pairs before the command */
t_vec argv; /* NULL-terminated argv word list */
char *fname; /* resolved path (heap) or NULL for builtins */
bool pooled; /* true if argv was borrowed from argv_pool */
} t_executable_cmd;

/* argv backing pool (free_utils2.c): borrow on expand, return on teardown. */
void argv_pool_acquire(t_shell *state, t_executable_cmd *cmd);
void argv_pool_release(t_shell *state, t_executable_cmd *cmd);
void free_argv_pool(t_shell *state);

/* Execution context for one node in the pipeline: which fds to hook up,
which AST node to execute, and whether this command runs in the parent
process (builtins, function calls) and is allowed to modify state. */
typedef struct executable_node_s
{
int infd; /* read end of pipe from previous stage */
int outfd; /* write end of pipe to next stage */
int next_infd; /* saved next read end (pipeline setup) */
t_ast_node *node; /* the AST node being executed */
t_vec_int redirs; /* indices into state->redirects */
bool modify_parent_ctx; /* true for builtins that modify state */
} t_executable_node;

void execute_top_level(t_shell *state);
int exec_string(t_shell *state, char *str);
int exec_file_string(t_shell *state, char *str,
const char *src);
t_execution_state execute_pipeline(t_shell *state,
t_executable_node *exe);
void set_up_redirection(t_shell *state,
t_executable_node *exe);
t_execution_state execute_simple_command(t_shell *state,
t_executable_node *exe);
void forward_exit_status(t_execution_state res);
t_execution_state execute_command(t_shell *state,
t_executable_node *exe);
t_execution_state execute_tree_node(t_shell *state,
t_executable_node *exe);
t_execution_state execute_simple_list(t_shell *state,
t_executable_node *exe);
t_string word_to_string(t_ast_node node);
t_string word_to_hrdoc_string(t_ast_node node);
void set_cmd_status(t_shell *state, t_execution_state res);
t_execution_state res_status(int status);
t_execution_state res_pid(int pid);
void fire_debug_trap(t_shell *state);
void fire_err_trap(t_shell *state, int code);
void fire_return_trap(t_shell *state, int code);
void trap_save_reset(t_shell *state, char **save);
void pseudo_traps_quiet(t_shell *state);
void trap_restore(t_shell *state, char **save);
void exe_res_set_status(t_shell *st, t_execution_state *res);
int find_cmd_path(t_shell *state, char *cmd_name,
char **path_of_exe);
int exec_external_argv(t_shell *state, t_vec *args);
int run_external_sync(t_shell *state, t_vec *args);
int expand_simple_command(t_shell *state, t_ast_node *node,
t_executable_cmd *ret, t_vec_int *redirects);
int (*builtin_func(t_shell *state, char *name))(t_shell *s,
t_vec argv);

#endif
4 changes: 3 additions & 1 deletion incs/ft_builtins.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ bool zopt_apply(t_shell *state, const char *n, bool on);
int builtin_let(t_shell *state, t_vec argv);
int builtin_kill(t_shell *state, t_vec argv);
int builtin_printf(t_shell *state, t_vec argv);
int (*builtin_func(char *name))(t_shell *state,
int (*builtin_func(t_shell *state, char *name))(t_shell *s,
t_vec argv);
int (*zsh_builtin_func(char *name))(t_shell *s, t_vec argv);
int builtin_which(t_shell *state, t_vec argv);

#endif
1 change: 1 addition & 0 deletions incs/history.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ void hist_rehome(t_shell *state);
keeps on an edited entry (history_rl.c). Never call the raw ones. */
void hist_rl_drop(int idx);
void hist_rl_clear(void);
void hist_rl_reclaim_saved(void);
void init_history(t_shell *state);
void free_hist(t_shell *state);
void parse_history_file(t_shell *state);
Expand Down
4 changes: 2 additions & 2 deletions incs/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

/* The single source of truth for the running shell's version. Bumped in step
with the git tag / GitHub release / npm package / docker image. */
# define HELLISH_VERSION "3.1.3"
# define HELLISH_VERSION "3.2.0"

/* Where releases live; used by the `update` builtin and the daily check. */
# define HELLISH_REPO "Univers42/hellish"
Expand All @@ -30,6 +30,6 @@
/* Bumped whenever the welcome header itself changes what it says. The
header is shown once a day, but a new revision is new information, so
it is shown again immediately rather than waiting for tomorrow. */
# define HELLISH_HEADER_REV 3
# define HELLISH_HEADER_REV 4

#endif
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ markdown_extensions:

nav:
- Home: index.md
- What's new in 3.2: whats-new.md
- What it is + Install: product.md
- The manual: manual.md
- Builtins reference:
Expand Down
2 changes: 1 addition & 1 deletion npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hellish-shell",
"version": "3.1.3",
"version": "3.2.0",
"description": "hellish — a fast, POSIX-compliant shell, with horns",
"bin": {
"hellish": "bin/hellish.js"
Expand Down
2 changes: 1 addition & 1 deletion src/builtins/builtin_builtin.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ int builtin_builtin(t_shell *state, t_vec argv)

if (argv.len < 2)
return (0);
f = builtin_func(((char **)argv.ctx)[1]);
f = builtin_func(state, ((char **)argv.ctx)[1]);
if (!f)
{
ft_eprintf("%s: builtin: %s: not a shell builtin\n",
Expand Down
8 changes: 5 additions & 3 deletions src/builtins/builtin_cmd2.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ static int command_v(t_shell *state, char *name)

if (alias_get(&state->aliases, name))
return (ft_printf("alias "), alias_print_one(&state->aliases, name));
if (type_is_keyword(name) || builtin_func(name) || func_lookup(state, name))
if (type_is_keyword(name) || builtin_func(state, name)
|| func_lookup(state, name))
return (ft_printf("%s\n", name), 0);
if (ft_strchr(name, '/'))
{
Expand Down Expand Up @@ -120,8 +121,9 @@ static int command_run(t_shell *state, t_vec argv, size_t start)
i = start;
while (i < argv.len)
vec_push(&sub, &av[i++]);
if (builtin_func(av[start]))
return (rc = builtin_func(av[start])(state, sub), xfree(sub.ctx), rc);
if (builtin_func(state, av[start]))
return (rc = builtin_func(state, av[start])(state, sub),
xfree(sub.ctx), rc);
rc = run_external_sync(state, &sub);
return (xfree(sub.ctx), rc);
}
Expand Down
Loading
Loading