Skip to content

Commit ec4dc76

Browse files
authored
Merge pull request #186 from Univers42/develop
Develop
2 parents 1dd5ae5 + d72fb99 commit ec4dc76

41 files changed

Lines changed: 1229 additions & 214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Makefile‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,14 @@ clean: ## Remove object files, keep the binary
488488
# default set above does not, so a plain `make fclean` used to reach libft with
489489
# SAFE unset -- landing in its SAFE!=1 branch, running the LTO capability probe
490490
# and printing its warning just to delete files.
491+
#
492+
# The final `rm -rf build` cannot finish on NFS (the 42 machines' sgoinfre)
493+
# while a shell started from build/bin is still running: NFS keeps a deleted
494+
# file that is still open as build/bin/.nfsXXXX until its last user exits, and
495+
# refuses to remove it ("Device or resource busy"). That made `make re` stop
496+
# here with nothing rebuilt. Such a file is not ours to remove and does not
497+
# block the rebuild, so fclean succeeds when only those remain; anything else
498+
# left behind is retried and fails the target as before.
491499
fclean: clean ## Remove objects, the binary and libft's build trees
492500
@printf " \033[1;33m⚠\033[0m \033[1;37mRemoving binary\033[0m" >&2
493501
@rm -f $(BIN_DIR)/$(BAPTIZE_SHELL)
@@ -496,7 +504,14 @@ fclean: clean ## Remove objects, the binary and libft's build trees
496504
@$(MAKE) -C vendor/libft fclean SAFE=$(SAFE) BUILD_DIR=build-$(SAFE_TAG)
497505
@rm -rf vendor/libft/build-ft vendor/libft/build-libc vendor/libft/build
498506
@printf "\r\033[K \033[1;32m✓\033[0m \033[37mlibft cleaned\033[0m\n" >&2
499-
@rm -rf build
507+
@if ! rm -rf build 2>/dev/null; then \
508+
if find build ! -type d ! -name '.nfs*' | grep -q .; then \
509+
rm -rf build; \
510+
else \
511+
printf " \033[1;33m⚠\033[0m \033[37mkept %s: still open in a running process (NFS)\033[0m\n" \
512+
"$$(find build -name '.nfs*' | xargs)" >&2; \
513+
fi; \
514+
fi
500515
@printf "\n" >&2
501516

502517
# Run fclean THEN all, strictly in order. As plain prerequisites

‎RELEASE.md‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,65 @@
33
> 🔥 **hellish** is a fast, almost‑POSIX shell written from scratch in C —
44
> hackable, observable, and pleasant to live in.
55
6-
This file is what the welcome banner points you at. It tracks what's new and
7-
shows you how to drive the shell.
6+
This file tracks what's new in every release and shows you how to drive the
7+
shell. The welcome banner links to the guide for the latest one:
8+
[What's new](https://univers42.github.io/hellish/whats-new/).
9+
10+
---
11+
12+
## v3.2.0 — *Up finds what you typed, a hint is one key away, and `bind`*
13+
14+
**The guide: [What's new in 3.2](https://univers42.github.io/hellish/whats-new/)**.
15+
It covers every feature: the keys that reach it, and an example run on
16+
3.2. The welcome banner shows the same list once after you update.
17+
18+
- **Up and Down search by what you typed.** `git<Up>` steps through the
19+
history lines that start with `git`; on an empty line Up walks the whole
20+
history. On by default; `HELLISH_HISTORY_PREFIX=0` turns it off. The
21+
configs that ask for it now work too: bash's
22+
`bind '"\e[A": history-search-backward'` and zsh's
23+
`bindkey '^[[A' history-beginning-search-backward` (#136).
24+
- **Inline hints**, opt-in with `export HELLISH_AUTOSUGGEST=1`. The newest
25+
matching history line shows dim after the cursor. Right / End / Ctrl-F
26+
take all of it, Alt-F / Ctrl-Right take one word, and Alt-N / Alt-P
27+
show other matches (#140).
28+
- **`make <TAB>`** completes the targets of the Makefile in the current
29+
directory. The shell reads the file itself; make never runs (#140).
30+
- **`bind`**, bash's builtin: key bindings and readline settings from
31+
`~/.hellishrc`. `bind -x` runs a shell command on a key, and the command
32+
can edit the line through `READLINE_LINE` / `READLINE_POINT`.
33+
- **`bindkey`** in zsh mode reads zsh's `^X` notation and reaches the line
34+
editor's own widgets.
35+
- **`${var@P}`** renders a value through the PS1 renderer. **`$SRANDOM`**
36+
and **`$EPOCHREALTIME`** are new variables.
37+
38+
Fixed on the way to the release:
39+
40+
- **The hint and prefix keys did nothing on some builds.** The shell found
41+
readline's commands by comparing function addresses. On a non-PIE binary
42+
linked against a readline built with `-Bsymbolic-functions` (Ubuntu's),
43+
one function has two addresses, so nothing matched and every key kept
44+
its stock meaning. The shell now asks readline for each command by name
45+
(`rl_named_function`).
46+
- **Two leaks in readline 8.1's line editing**, one undo list per line:
47+
- after walking the history with Up or Ctrl-P;
48+
- after cancelling an Alt-P search.
49+
50+
LeakSanitizer reported both. `tests/rl_undo_leak_test.py` covers ten
51+
ways to reach them.
52+
- **`which` in zsh mode.** Esc Esc (oh-my-zsh's `sudo` plugin) printed
53+
`builtin: which: not a shell builtin`. zsh's `which [-ap]` now exists in
54+
zsh mode.
55+
- **`make re` on NFS** no longer stops in `fclean` on the `.nfsXXXX` file
56+
a running `build/bin/hellish` keeps open.
57+
- **The welcome banner** fits an 80-column terminal, and its frame stays
58+
aligned under `LANG=C`.
59+
60+
About two dozen smaller fixes landed as well, among them PS2 on every
61+
continuation line, `exec nosuch` no longer reading freed memory, `read`
62+
at end of input, and heredocs in functions, `eval` and `source`. The guide
63+
lists the ones you can see; the compare view has all of them:
64+
[v3.1.3...v3.2.0](https://github.com/Univers42/hellish/compare/v3.1.3...v3.2.0).
865

966
---
1067

‎incs/executor.h‎

Lines changed: 94 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,95 @@
1-
/* ************************************************************************** */
2-
/* */
3-
/* ::: :::::::: */
4-
/* executor.h :+: :+: :+: */
5-
/* +:+ +:+ +:+ */
6-
/* By: marvin <marvin@student.42.fr> +#+ +:+ +#+ */
7-
/* +#+#+#+#+#+ +#+ */
8-
/* Created: 2026/01/10 02:38:39 by marvin #+# #+# */
9-
/* Updated: 2026/01/10 02:38:39 by marvin ### ########.fr */
10-
/* */
11-
/* ************************************************************************** */
12-
13-
/* Executor public API: turning an AST into side-effects.
14-
The main entry is execute_top_level() which walks the tree returned by
15-
the parser. Every execute_* function returns t_execution_state so the
16-
exit code and any ctrl-c signal propagate up cleanly. */
17-
18-
#ifndef EXECUTOR_H
19-
# define EXECUTOR_H
20-
21-
# include "public/executor_types.h"
22-
# include "alias.h"
23-
24-
/* forward declarations to avoid heavy includes */
25-
typedef struct s_shell t_shell;
26-
typedef struct s_ast_node t_ast_node;
27-
28-
/* An expanded, ready-to-run simple command: pre-command assignments,
29-
the argv array, and the resolved executable path. pooled=true means
30-
argv.ctx comes from argv_pool (don't free it, just reset the vec). */
31-
typedef struct executable_cmd_s
32-
{
33-
t_vec_env pre_assigns; /* NAME=VALUE pairs before the command */
34-
t_vec argv; /* NULL-terminated argv word list */
35-
char *fname; /* resolved path (heap) or NULL for builtins */
36-
bool pooled; /* true if argv was borrowed from argv_pool */
37-
} t_executable_cmd;
38-
39-
/* argv backing pool (free_utils2.c): borrow on expand, return on teardown. */
40-
void argv_pool_acquire(t_shell *state, t_executable_cmd *cmd);
41-
void argv_pool_release(t_shell *state, t_executable_cmd *cmd);
42-
void free_argv_pool(t_shell *state);
43-
44-
/* Execution context for one node in the pipeline: which fds to hook up,
45-
which AST node to execute, and whether this command runs in the parent
46-
process (builtins, function calls) and is allowed to modify state. */
47-
typedef struct executable_node_s
48-
{
49-
int infd; /* read end of pipe from previous stage */
50-
int outfd; /* write end of pipe to next stage */
51-
int next_infd; /* saved next read end (pipeline setup) */
52-
t_ast_node *node; /* the AST node being executed */
53-
t_vec_int redirs; /* indices into state->redirects */
54-
bool modify_parent_ctx; /* true for builtins that modify state */
55-
} t_executable_node;
56-
57-
void execute_top_level(t_shell *state);
58-
int exec_string(t_shell *state, char *str);
59-
int exec_file_string(t_shell *state, char *str,
60-
const char *src);
61-
t_execution_state execute_pipeline(t_shell *state,
62-
t_executable_node *exe);
63-
void set_up_redirection(t_shell *state,
64-
t_executable_node *exe);
65-
t_execution_state execute_simple_command(t_shell *state,
66-
t_executable_node *exe);
67-
void forward_exit_status(t_execution_state res);
68-
t_execution_state execute_command(t_shell *state,
69-
t_executable_node *exe);
70-
t_execution_state execute_tree_node(t_shell *state,
71-
t_executable_node *exe);
72-
t_execution_state execute_simple_list(t_shell *state,
73-
t_executable_node *exe);
74-
t_string word_to_string(t_ast_node node);
75-
t_string word_to_hrdoc_string(t_ast_node node);
76-
void set_cmd_status(t_shell *state, t_execution_state res);
77-
t_execution_state res_status(int status);
78-
t_execution_state res_pid(int pid);
79-
void fire_debug_trap(t_shell *state);
80-
void fire_err_trap(t_shell *state, int code);
81-
void fire_return_trap(t_shell *state, int code);
82-
void trap_save_reset(t_shell *state, char **save);
83-
void pseudo_traps_quiet(t_shell *state);
84-
void trap_restore(t_shell *state, char **save);
85-
void exe_res_set_status(t_shell *st, t_execution_state *res);
86-
int find_cmd_path(t_shell *state, char *cmd_name,
87-
char **path_of_exe);
88-
int exec_external_argv(t_shell *state, t_vec *args);
89-
int run_external_sync(t_shell *state, t_vec *args);
90-
int expand_simple_command(t_shell *state, t_ast_node *node,
91-
t_executable_cmd *ret, t_vec_int *redirects);
92-
int (*builtin_func(char *name))(t_shell *state, t_vec argv);
93-
1+
/* ************************************************************************** */
2+
/* */
3+
/* ::: :::::::: */
4+
/* executor.h :+: :+: :+: */
5+
/* +:+ +:+ +:+ */
6+
/* By: marvin <marvin@student.42.fr> +#+ +:+ +#+ */
7+
/* +#+#+#+#+#+ +#+ */
8+
/* Created: 2026/01/10 02:38:39 by marvin #+# #+# */
9+
/* Updated: 2026/01/10 02:38:39 by marvin ### ########.fr */
10+
/* */
11+
/* ************************************************************************** */
12+
13+
/* Executor public API: turning an AST into side-effects.
14+
The main entry is execute_top_level() which walks the tree returned by
15+
the parser. Every execute_* function returns t_execution_state so the
16+
exit code and any ctrl-c signal propagate up cleanly. */
17+
18+
#ifndef EXECUTOR_H
19+
# define EXECUTOR_H
20+
21+
# include "public/executor_types.h"
22+
# include "alias.h"
23+
24+
/* forward declarations to avoid heavy includes */
25+
typedef struct s_shell t_shell;
26+
typedef struct s_ast_node t_ast_node;
27+
28+
/* An expanded, ready-to-run simple command: pre-command assignments,
29+
the argv array, and the resolved executable path. pooled=true means
30+
argv.ctx comes from argv_pool (don't free it, just reset the vec). */
31+
typedef struct executable_cmd_s
32+
{
33+
t_vec_env pre_assigns; /* NAME=VALUE pairs before the command */
34+
t_vec argv; /* NULL-terminated argv word list */
35+
char *fname; /* resolved path (heap) or NULL for builtins */
36+
bool pooled; /* true if argv was borrowed from argv_pool */
37+
} t_executable_cmd;
38+
39+
/* argv backing pool (free_utils2.c): borrow on expand, return on teardown. */
40+
void argv_pool_acquire(t_shell *state, t_executable_cmd *cmd);
41+
void argv_pool_release(t_shell *state, t_executable_cmd *cmd);
42+
void free_argv_pool(t_shell *state);
43+
44+
/* Execution context for one node in the pipeline: which fds to hook up,
45+
which AST node to execute, and whether this command runs in the parent
46+
process (builtins, function calls) and is allowed to modify state. */
47+
typedef struct executable_node_s
48+
{
49+
int infd; /* read end of pipe from previous stage */
50+
int outfd; /* write end of pipe to next stage */
51+
int next_infd; /* saved next read end (pipeline setup) */
52+
t_ast_node *node; /* the AST node being executed */
53+
t_vec_int redirs; /* indices into state->redirects */
54+
bool modify_parent_ctx; /* true for builtins that modify state */
55+
} t_executable_node;
56+
57+
void execute_top_level(t_shell *state);
58+
int exec_string(t_shell *state, char *str);
59+
int exec_file_string(t_shell *state, char *str,
60+
const char *src);
61+
t_execution_state execute_pipeline(t_shell *state,
62+
t_executable_node *exe);
63+
void set_up_redirection(t_shell *state,
64+
t_executable_node *exe);
65+
t_execution_state execute_simple_command(t_shell *state,
66+
t_executable_node *exe);
67+
void forward_exit_status(t_execution_state res);
68+
t_execution_state execute_command(t_shell *state,
69+
t_executable_node *exe);
70+
t_execution_state execute_tree_node(t_shell *state,
71+
t_executable_node *exe);
72+
t_execution_state execute_simple_list(t_shell *state,
73+
t_executable_node *exe);
74+
t_string word_to_string(t_ast_node node);
75+
t_string word_to_hrdoc_string(t_ast_node node);
76+
void set_cmd_status(t_shell *state, t_execution_state res);
77+
t_execution_state res_status(int status);
78+
t_execution_state res_pid(int pid);
79+
void fire_debug_trap(t_shell *state);
80+
void fire_err_trap(t_shell *state, int code);
81+
void fire_return_trap(t_shell *state, int code);
82+
void trap_save_reset(t_shell *state, char **save);
83+
void pseudo_traps_quiet(t_shell *state);
84+
void trap_restore(t_shell *state, char **save);
85+
void exe_res_set_status(t_shell *st, t_execution_state *res);
86+
int find_cmd_path(t_shell *state, char *cmd_name,
87+
char **path_of_exe);
88+
int exec_external_argv(t_shell *state, t_vec *args);
89+
int run_external_sync(t_shell *state, t_vec *args);
90+
int expand_simple_command(t_shell *state, t_ast_node *node,
91+
t_executable_cmd *ret, t_vec_int *redirects);
92+
int (*builtin_func(t_shell *state, char *name))(t_shell *s,
93+
t_vec argv);
94+
9495
#endif

‎incs/ft_builtins.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ bool zopt_apply(t_shell *state, const char *n, bool on);
148148
int builtin_let(t_shell *state, t_vec argv);
149149
int builtin_kill(t_shell *state, t_vec argv);
150150
int builtin_printf(t_shell *state, t_vec argv);
151-
int (*builtin_func(char *name))(t_shell *state,
151+
int (*builtin_func(t_shell *state, char *name))(t_shell *s,
152152
t_vec argv);
153+
int (*zsh_builtin_func(char *name))(t_shell *s, t_vec argv);
154+
int builtin_which(t_shell *state, t_vec argv);
153155

154156
#endif

‎incs/history.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ void hist_rehome(t_shell *state);
5454
keeps on an edited entry (history_rl.c). Never call the raw ones. */
5555
void hist_rl_drop(int idx);
5656
void hist_rl_clear(void);
57+
void hist_rl_reclaim_saved(void);
5758
void init_history(t_shell *state);
5859
void free_hist(t_shell *state);
5960
void parse_history_file(t_shell *state);

‎incs/version.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

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

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

3535
#endif

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ markdown_extensions:
5555

5656
nav:
5757
- Home: index.md
58+
- What's new in 3.2: whats-new.md
5859
- What it is + Install: product.md
5960
- The manual: manual.md
6061
- Builtins reference:

‎npm/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hellish-shell",
3-
"version": "3.1.3",
3+
"version": "3.2.0",
44
"description": "hellish — a fast, POSIX-compliant shell, with horns",
55
"bin": {
66
"hellish": "bin/hellish.js"

‎src/builtins/builtin_builtin.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ int builtin_builtin(t_shell *state, t_vec argv)
2929

3030
if (argv.len < 2)
3131
return (0);
32-
f = builtin_func(((char **)argv.ctx)[1]);
32+
f = builtin_func(state, ((char **)argv.ctx)[1]);
3333
if (!f)
3434
{
3535
ft_eprintf("%s: builtin: %s: not a shell builtin\n",

‎src/builtins/builtin_cmd2.c‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ static int command_v(t_shell *state, char *name)
5858

5959
if (alias_get(&state->aliases, name))
6060
return (ft_printf("alias "), alias_print_one(&state->aliases, name));
61-
if (type_is_keyword(name) || builtin_func(name) || func_lookup(state, name))
61+
if (type_is_keyword(name) || builtin_func(state, name)
62+
|| func_lookup(state, name))
6263
return (ft_printf("%s\n", name), 0);
6364
if (ft_strchr(name, '/'))
6465
{
@@ -120,8 +121,9 @@ static int command_run(t_shell *state, t_vec argv, size_t start)
120121
i = start;
121122
while (i < argv.len)
122123
vec_push(&sub, &av[i++]);
123-
if (builtin_func(av[start]))
124-
return (rc = builtin_func(av[start])(state, sub), xfree(sub.ctx), rc);
124+
if (builtin_func(state, av[start]))
125+
return (rc = builtin_func(state, av[start])(state, sub),
126+
xfree(sub.ctx), rc);
125127
rc = run_external_sync(state, &sub);
126128
return (xfree(sub.ctx), rc);
127129
}

0 commit comments

Comments
 (0)