Repository navigation
fix(export): export X before X is set keeps the attribute - #161
Merged
Merged
Conversation
export A
A=1
sh -c 'echo "[$A]"' # bash: [1] hellish: []
POSIX: `export name` sets the export attribute, and later assignments
are exported. hellish only marked a variable that already existed. For
a name not yet set, handle_identifier found no record and stored
nothing, so the attribute was lost. `declare -x G` had the same gap in
declare_assign. `export -p` and `declare -p B` then said nothing about
the name either ("declare: B: not found").
The table can already hold a record with no value, and it already
means "declared, not set": ${X+x}, [[ -v ]], set -u, ${!p*} and
compgen -v all read a NULL value as unset. env_set keeps the export
flag on assignment, and unset drops the record with its attribute. So
the fix is to create that record in the two places that should, and
to teach the rest to show it the way bash does:
- export NAME / declare -x NAME on an unset name: a record with no
value, exported;
- child environment: skipped, since bash passes nothing, not "X=";
- export -p: `export X` without a value, so the list reads back to
the same thing (it printed X="" before, which re-created X set);
- declare -p X: `declare -x X`;
- set / declare with no names: not listed, as in bash;
- cd with PWD exported but unset: OLDPWD takes PWD's state,
attribute included, where ft_strdup(NULL) would have crashed.
tests/scripts/62_export_unset_attribute.sh, graded against
bash --posix, covers:
- the idiom;
- the unset state everywhere (env, ${B+}, set, set -u, the listings);
- assignment by =, read, for, printf -v and +=, and declare -x;
- unset, export -n and declare +x taking the attribute away;
- export -p output fed back to a shell;
- cd with an exported unset PWD;
- a readonly name exported before it has a value.
develop diverges on 9 lines; this branch matches bash byte for byte,
with no sanitizer report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
POSIX says
export namesets the export attribute, and every later assignment to that name is exported. hellish only marked a variable that already existed:handle_identifierfound no record and stored nothing, so the attribute was lost.declare -x Ghad the same gap, indeclare_assign.export -panddeclare -p Bthen said nothing about the name (declare: B: not found).Root cause and fix. The variable table can already hold a record with no value, and it already means "declared, not set":
${X+x},[[ -v ]],set -u,${!p*}andcompgen -vall read a NULL value as unset.env_setkeeps the export flag on assignment, andunsetdrops the record together with its attribute. So the fix creates that record in the two places that should create it, and teaches the other places to show it the way bash does:export NAME/declare -x NAMEon an unset nameX=export -pexport Xwithout a value, so the list reads back as the same state. It used to printX="", which re-created X as set.declare -p Xdeclare -x Xset/declarewith no namescdwith PWD exported but unsetft_strdup(NULL)would have crashed.How I verified it
Reproduced first.
tests/scripts/62_export_unset_attribute.shis graded againstbash --posix5.3.9. It covers:${B+},set,set -u, and the listings;=,read,for,printf -vand+=, anddeclare -x;unset,export -nanddeclare +xremoving the attribute;export -poutput fed back into a shell;cdwith an exported but unset PWD;develop diverges on 9 lines; this branch matches bash byte for byte, with no sanitizer report.
Local gates on this commit (ASan debug build):
tests/tester: 5363/5363;tests/run_scripts.shagainstbash --posix: 126/126;verify_alloc.sh: identical output on both heaps;alloc_stress.sh: all clean;tests/pty_suite.sh: 111 ok, 6 skipped, 4 failed. None of the four failures is this change:prompt_compat_matrix,prompt_drift_matrixandprompt_jobs_badgeexpect the non-root%/$prompt and get#, because the container runs as root. They fail the same way on develop, and CI runs them as a normal user.hxp_framework_testhit the 420 s per-file limit on this 4-core container. develop's binary takes 440 s for it on the same machine, and CI's runners finish it inside the limit.norminetteis OK on every touched file.Notes / trade-offs
declare -ax A=(x y)still drops-x, andexport -pstill prints an exported array as an encoded scalar. Both were found while writing this test, and they are a separate change in how arrays are exported.🤖 Generated with Claude Code
https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG
Generated by Claude Code