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
2 changes: 2 additions & 0 deletions src/builtins/builtin_declare.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ static int declare_print_one(t_shell *state, const char *name)
return (ft_eprintf("%s: declare: %s: not found\n",
state->ctx, name), 1);
attr_letters(state, e, attrs);
if (!e->value)
return (ft_printf("declare -%s %s\n", attrs, e->key), 0);
if (assoc_is(e->value))
fmt = assoc_format(e->value);
else if (arr_is(e->value))
Expand Down
18 changes: 7 additions & 11 deletions src/builtins/builtin_set2.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
#include "builtins_private.h"
#include "sh_input.h"

/* Print all environment entries in `name=value` form (or just `name` when a
variable has no value). This is the output of bare `set` — it differs from
/* Print all set variables in `name=value` form; a name exported before it
has a value is not set, and bash's `set` leaves it out too. This is the
output of bare `set` — it differs from
`env` in that it includes all variables, not just the exported ones, and
it does not quote the values. */
/* Arrays list in bash's display form, name=([0]="a" ...), so the
Expand All @@ -38,17 +39,12 @@ void set_print_env(t_shell *state)
while (i < state->env.len)
{
e = &((t_env *)state->env.ctx)[i];
if (e->key)
if (e->key && e->value)
{
if (e->value)
{
if (arr_is(e->value))
print_env_array(e);
else
ft_printf("%s=%s\n", e->key, e->value);
}
if (arr_is(e->value))
print_env_array(e);
else
ft_printf("%s\n", e->key);
ft_printf("%s=%s\n", e->key, e->value);
}
i++;
}
Expand Down
26 changes: 19 additions & 7 deletions src/builtins/collect_and_print_exported.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,29 @@

#include "builtins_private.h"

/* Walk the env table and format each exported entry as `export KEY="value"`.
Variables without a value (exported but never assigned) use an empty
string. All strings are heap-allocated — print_and_free_list owns them. */
/* One exported entry as `export KEY="value"`. A variable exported but not
yet assigned is listed bare, `export KEY`, as bash lists it: printing
KEY="" would re-create it SET when the list is fed back to the shell. */
static char *export_line(t_env *e)
{
char *s;
char *tmp;

if (!e->value)
return (ft_asprintf("export %s", e->key));
tmp = dquote_str(e->value);
s = ft_asprintf("export %s=\"%s\"", e->key, tmp);
xfree(tmp);
return (s);
}

/* Walk the env table and format each exported entry. All strings are
heap-allocated — print_and_free_list owns them. */
static void collect_exported_list(t_shell *st, t_vec *list)
{
size_t j;
t_env *e;
char *s;
char *tmp;

vec_init(list);
list->elem_size = sizeof(char *);
Expand All @@ -30,9 +44,7 @@ static void collect_exported_list(t_shell *st, t_vec *list)
e = &((t_env *)st->env.ctx)[j];
if (e->exported)
{
tmp = dquote_str(e->value);
s = ft_asprintf("export %s=\"%s\"", e->key, tmp);
xfree(tmp);
s = export_line(e);
vec_push(list, &s);
}
j++;
Expand Down
8 changes: 6 additions & 2 deletions src/builtins/declare_subscript.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ char *declare_assign_eq(const char *word)
return (NULL);
}

/* One NAME or NAME=VALUE operand: NAME=VALUE assigns (export flag from -x),
a bare NAME with no '=' is a no-op that just accepts the name.
/* One NAME or NAME=VALUE operand: NAME=VALUE assigns (export flag from -x).
A bare NAME with -x marks it exported, or, when it is not set, leaves a
record with no value that holds the attribute until it is, exactly as
`export NAME` does. Without -x a bare NAME just accepts the name.
subscript_assign() rewrites key and value in place when the key carries
a [subscript], so a plain name falls through it untouched and the two
spellings cannot drift apart. */
Expand All @@ -68,6 +70,8 @@ void declare_assign(t_shell *state, const char *word, int exprt)
{
if (exprt && env_get(&state->env, (char *)word))
env_get(&state->env, (char *)word)->exported = true;
else if (exprt && ft_is_valid_ident((char *)word))
env_set(&state->env, env_create(ft_strdup(word), NULL, true));
return ;
}
ev = env_create(ft_strndup(word, eq - word), ft_strdup(eq + 1),
Expand Down
26 changes: 19 additions & 7 deletions src/builtins/export_helpers2.c
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,32 @@ static char *export_apply_append(t_shell *st, char *id, char *val)
return (joined);
}

int handle_identifier(t_shell *st, char *id, char *val, const char *argv0)
/* `export NAME` with no value. An existing variable is marked; a name
that is not set yet gets a record with no value, so the attribute
waits for it: `export X; X=1` exports X (POSIX), and until then X is
still unset -- ${X+set} is empty, no child sees it, and `export -p`
lists it bare, as bash does. */
static void export_name_only(t_shell *st, char *id)
{
t_env *e;

e = env_get(&st->env, id);
if (e)
{
e->exported = true;
xfree(id);
return ;
}
env_set(&st->env, env_create(id, NULL, true));
}

int handle_identifier(t_shell *st, char *id, char *val, const char *argv0)
{
val = export_apply_append(st, id, val);
if (ft_is_valid_ident(id))
{
if (!val)
{
e = env_get(&st->env, id);
if (e)
e->exported = true;
xfree(id);
}
export_name_only(st, id);
else
env_set_shaped(st, (t_env){true, id, val});
return (0);
Expand Down
7 changes: 4 additions & 3 deletions src/environment/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ char *env_to_str(t_env *e)

/* Build the NULL-terminated envp[] for execve, including only entries
where exported==true. Shell-local variables (exported=false) are
intentionally hidden from child processes. Caller owns the array and
each element. */
intentionally hidden from child processes, and so is a name exported
before it has a value (`export X` alone): bash passes nothing for it,
not "X=". Caller owns the array and each element. */
char **get_envp(t_shell *state, char *exe_path)
{
char **ret;
Expand All @@ -123,7 +124,7 @@ char **get_envp(t_shell *state, char *exe_path)
while (++i < state->env.len)
{
e = &((t_env *)state->env.ctx)[i];
if (e->exported && !arr_is(e->value))
if (e->exported && e->value && !arr_is(e->value))
ret[j++] = env_to_str(e);
}
return (ret);
Expand Down
3 changes: 3 additions & 0 deletions src/environment/utils2.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ void update_pwd_vars(t_shell *state)
pwd = env_get(&state->env, PWD_NAME);
if (pwd == NULL)
try_unset(state, OLDPWD_NAME);
else if (!pwd->value)
env_set(&state->env, env_create(ft_strdup(OLDPWD_NAME), NULL,
pwd->exported));
else
env_set(&state->env, env_create(ft_strdup(OLDPWD_NAME),
ft_strdup(pwd->value), pwd->exported));
Expand Down
88 changes: 88 additions & 0 deletions tests/scripts/62_export_unset_attribute.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# `export NAME` before NAME has a value. POSIX: the export attribute is
# set, and "subsequent" assignments are exported. Until one comes, NAME
# is still unset: no child sees it, ${NAME+x} is empty, `set` leaves it
# out, and `export -p` / `declare -p` list it without a value.

# The idiom itself.
export A
A=1
sh -c 'echo "A in child: [$A]"'

# Before it is assigned: unset everywhere, listed bare.
export B
echo "B set: [${B+yes}]"
env | grep -c '^B=' || true
export -p | grep -E '^export B($|=)'
declare -p B
set | grep -c '^B=' || true
( set -u; : "$B" ) 2>/dev/null || echo "B unbound under set -u"

# Any way of assigning it exports it.
export C D E F
read -r C <<EOF
from-read
EOF
for D in from-for; do :; done
printf -v E '%s' from-printf 2>/dev/null || E=from-printf
F+=appended
sh -c 'echo "C=[$C] D=[$D] E=[$E] F=[$F]"'

# declare -x does the same.
declare -x G
G=2
sh -c 'echo "G in child: [$G]"'
declare -p G

# unset takes the attribute away with the variable.
export H
H=3
unset H
H=4
sh -c 'echo "H after unset: [${H-unset}]"'

# export -n and declare +x take it away and leave the name unset.
export I
export -n I
I=5
sh -c 'echo "I after export -n: [${I-unset}]"'
export J
declare +x J
J=6
sh -c 'echo "J after declare +x: [${J-unset}]"'

# export -p output can be fed back to a shell and means the same thing.
export K
L=val
export L
export -p | grep -E '^export (K|L)($|=)' > "${TMPDIR:-/tmp}/exp_$$"
unset K L
. "${TMPDIR:-/tmp}/exp_$$"
rm -f "${TMPDIR:-/tmp}/exp_$$"
echo "K set: [${K+yes}] L=[$L]"
K=k
sh -c 'echo "K=[$K] L=[$L]"'

# Exporting a name that is already set changes nothing but the attribute.
M=keep
export M
sh -c 'echo "M=[$M]"'

# cd with PWD exported but unset: OLDPWD takes PWD's state, attribute
# included, and PWD is set again.
(
unset PWD
export PWD
cd /
echo "OLDPWD set: [${OLDPWD+yes}]"
export -p | grep -E '^export OLDPWD($|=)'
echo "PWD=[$PWD]"
)

# A readonly name exported before it has a value stays readonly.
(
export N
readonly N
declare -p N
N=1
) 2>/dev/null
echo "readonly N: status $?"
Loading