Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement unexporting variables #2098

Merged
merged 16 commits into from
Jun 5, 2024
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,23 @@ a $A $B=`echo $A`:
When [export](#export) is set, all `just` variables are exported as environment
variables.

#### Unexporting Environment Variables<sup>master</sup>

Environment variables can be unexported with the `unexport keyword`:

```just
unexport FOO

@foo:
echo $FOO
```

```
$ export FOO=bar
$ just foo
sh: FOO: unbound variable
```

#### Getting Environment Variables from the environment

Environment variables from the environment are passed automatically to the
Expand Down
26 changes: 19 additions & 7 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ impl<'src> Analyzer<'src> {

let mut modules: Table<Justfile> = Table::new();

let mut unexports: HashSet<String> = HashSet::new();

let mut definitions: HashMap<&str, (&'static str, Name)> = HashMap::new();

let mut define = |name: Name<'src>,
Expand Down Expand Up @@ -98,6 +100,13 @@ impl<'src> Analyzer<'src> {
self.analyze_set(set)?;
self.sets.insert(set.clone());
}
Item::Unexport { name } => {
if !unexports.insert(name.lexeme().to_string()) {
return Err(name.token.error(DuplicateUnexport {
variable: name.lexeme(),
}));
}
}
}
}

Expand All @@ -109,21 +118,23 @@ impl<'src> Analyzer<'src> {
let mut recipe_table: Table<'src, UnresolvedRecipe<'src>> = Table::default();

for assignment in assignments {
if !settings.allow_duplicate_variables
&& self.assignments.contains_key(assignment.name.lexeme())
{
return Err(assignment.name.token.error(DuplicateVariable {
variable: assignment.name.lexeme(),
}));
let variable = assignment.name.lexeme();

if !settings.allow_duplicate_variables && self.assignments.contains_key(variable) {
return Err(assignment.name.token.error(DuplicateVariable { variable }));
}

if self
.assignments
.get(assignment.name.lexeme())
.get(variable)
.map_or(true, |original| assignment.depth <= original.depth)
{
self.assignments.insert(assignment.clone());
}

if unexports.contains(variable) {
return Err(assignment.name.token.error(ExportUnexported { variable }));
}
}

AssignmentResolver::resolve_assignments(&self.assignments)?;
Expand Down Expand Up @@ -167,6 +178,7 @@ impl<'src> Analyzer<'src> {
recipes,
settings,
source: root.into(),
unexports,
warnings,
})
}
Expand Down
28 changes: 22 additions & 6 deletions src/command_ext.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
use super::*;

pub(crate) trait CommandExt {
fn export(&mut self, settings: &Settings, dotenv: &BTreeMap<String, String>, scope: &Scope);
fn export(
&mut self,
settings: &Settings,
dotenv: &BTreeMap<String, String>,
scope: &Scope,
unexports: &HashSet<String>,
);

fn export_scope(&mut self, settings: &Settings, scope: &Scope);
fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>);
}

impl CommandExt for Command {
fn export(&mut self, settings: &Settings, dotenv: &BTreeMap<String, String>, scope: &Scope) {
fn export(
&mut self,
settings: &Settings,
dotenv: &BTreeMap<String, String>,
scope: &Scope,
unexports: &HashSet<String>,
) {
for (name, value) in dotenv {
self.env(name, value);
}

if let Some(parent) = scope.parent() {
self.export_scope(settings, parent);
self.export_scope(settings, parent, unexports);
}
}

fn export_scope(&mut self, settings: &Settings, scope: &Scope) {
fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>) {
if let Some(parent) = scope.parent() {
self.export_scope(settings, parent);
self.export_scope(settings, parent, unexports);
}

for unexport in unexports {
self.env_remove(unexport);
}

for binding in scope.bindings() {
Expand Down
6 changes: 6 additions & 0 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ impl Display for CompileError<'_> {
DuplicateVariable { variable } => {
write!(f, "Variable `{variable}` has multiple definitions")
}
DuplicateUnexport { variable } => {
write!(f, "Variable `{variable}` is unexported multiple times")
}
ExpectedKeyword { expected, found } => {
let expected = List::or_ticked(expected);
if found.kind == TokenKind::Identifier {
Expand All @@ -143,6 +146,9 @@ impl Display for CompileError<'_> {
write!(f, "Expected keyword {expected} but found `{}`", found.kind)
}
}
ExportUnexported { variable } => {
write!(f, "Variable {variable} is both exported and unexported")
}
ExtraLeadingWhitespace => write!(f, "Recipe line has extra leading whitespace"),
FunctionArgumentCountMismatch {
function,
Expand Down
6 changes: 6 additions & 0 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ pub(crate) enum CompileErrorKind<'src> {
DuplicateVariable {
variable: &'src str,
},
DuplicateUnexport {
variable: &'src str,
},
ExpectedKeyword {
expected: Vec<Keyword>,
found: Token<'src>,
},
ExportUnexported {
variable: &'src str,
},
ExtraLeadingWhitespace,
FunctionArgumentCountMismatch {
function: &'src str,
Expand Down
9 changes: 8 additions & 1 deletion src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) struct Evaluator<'src: 'run, 'run> {
pub(crate) scope: Scope<'src, 'run>,
pub(crate) search: &'run Search,
pub(crate) settings: &'run Settings<'run>,
unsets: &'run HashSet<String>,
}

impl<'src, 'run> Evaluator<'src, 'run> {
Expand All @@ -19,6 +20,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
scope: Scope<'src, 'run>,
search: &'run Search,
settings: &'run Settings<'run>,
unsets: &'run HashSet<String>,
) -> RunResult<'src, Scope<'src, 'run>> {
let mut evaluator = Self {
assignments: Some(assignments),
Expand All @@ -28,6 +30,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
scope,
search,
settings,
unsets,
};

for assignment in assignments.values() {
Expand Down Expand Up @@ -217,7 +220,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
cmd.arg(command);
cmd.args(args);
cmd.current_dir(&self.search.working_directory);
cmd.export(self.settings, self.dotenv, &self.scope);
cmd.export(self.settings, self.dotenv, &self.scope, self.unsets);
cmd.stdin(Stdio::inherit());
cmd.stderr(if self.config.verbosity.quiet() {
Stdio::null()
Expand Down Expand Up @@ -261,6 +264,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
scope: &'run Scope<'src, 'run>,
search: &'run Search,
settings: &'run Settings,
unsets: &'run HashSet<String>,
) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
let mut evaluator = Self {
assignments: None,
Expand All @@ -270,6 +274,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
scope: scope.child(),
search,
settings,
unsets,
};

let mut scope = scope.child();
Expand Down Expand Up @@ -316,6 +321,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
scope: &'run Scope<'src, 'run>,
search: &'run Search,
settings: &'run Settings,
unsets: &'run HashSet<String>,
) -> Self {
Self {
assignments: None,
Expand All @@ -325,6 +331,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
scope: Scope::child(scope),
search,
settings,
unsets,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub(crate) enum Item<'src> {
},
Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>),
Unexport {
name: Name<'src>,
},
}

impl<'src> Display for Item<'src> {
Expand Down Expand Up @@ -61,6 +64,7 @@ impl<'src> Display for Item<'src> {
}
Self::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
Self::Set(set) => write!(f, "{set}"),
Self::Unexport { name } => write!(f, "unexport {name}"),
}
}
}
7 changes: 6 additions & 1 deletion src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub(crate) struct Justfile<'src> {
pub(crate) settings: Settings<'src>,
#[serde(skip)]
pub(crate) source: PathBuf,
pub(crate) unexports: HashSet<String>,
pub(crate) warnings: Vec<Warning>,
}

Expand Down Expand Up @@ -113,6 +114,7 @@ impl<'src> Justfile<'src> {
scope,
search,
&self.settings,
&self.unexports,
)
}

Expand Down Expand Up @@ -163,7 +165,7 @@ impl<'src> Justfile<'src> {

let scope = scope.child();

command.export(&self.settings, &dotenv, &scope);
command.export(&self.settings, &dotenv, &scope, &self.unexports);

let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
Error::CommandInvoke {
Expand Down Expand Up @@ -286,6 +288,7 @@ impl<'src> Justfile<'src> {
scope: invocation.scope,
search,
settings: invocation.settings,
unexports: &self.unexports,
};

Self::run_recipe(
Expand Down Expand Up @@ -441,6 +444,7 @@ impl<'src> Justfile<'src> {
context.scope,
search,
context.settings,
context.unexports,
)?;

let scope = outer.child();
Expand All @@ -452,6 +456,7 @@ impl<'src> Justfile<'src> {
&scope,
search,
context.settings,
context.unexports,
);

if !context.config.no_dependencies {
Expand Down
1 change: 1 addition & 0 deletions src/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) enum Keyword {
Shell,
Tempdir,
True,
Unexport,
WindowsPowershell,
WindowsShell,
X,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub(crate) use {
std::{
borrow::Cow,
cmp,
collections::{BTreeMap, BTreeSet, HashMap},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
env,
ffi::OsString,
fmt::{self, Debug, Display, Formatter},
Expand Down
5 changes: 5 additions & 0 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ impl<'src> Node<'src> for Item<'src> {
}
Self::Recipe(recipe) => recipe.tree(),
Self::Set(set) => set.tree(),
Self::Unexport { name } => {
let mut unexport = Tree::atom(Keyword::Unexport.lexeme());
unexport.push_mut(name.lexeme().replace('-', "_"));
unexport
}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ impl<'run, 'src> Parser<'run, 'src> {
self.presume_keyword(Keyword::Export)?;
items.push(Item::Assignment(self.parse_assignment(true)?));
}
Some(Keyword::Unexport) => {
self.presume_keyword(Keyword::Unexport)?;
let name = self.parse_name()?;
items.push(Item::Unexport { name });
}
Some(Keyword::Import)
if self.next_are(&[Identifier, StringToken])
|| self.next_are(&[Identifier, Identifier, StringToken])
Expand Down
5 changes: 3 additions & 2 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl<'src, D> Recipe<'src, D> {
scope,
context.search,
context.settings,
context.unexports,
);

if self.shebang {
Expand Down Expand Up @@ -279,7 +280,7 @@ impl<'src, D> Recipe<'src, D> {
cmd.stdout(Stdio::null());
}

cmd.export(context.settings, context.dotenv, scope);
cmd.export(context.settings, context.dotenv, scope, context.unexports);

match InterruptHandler::guard(|| cmd.status()) {
Ok(exit_status) => {
Expand Down Expand Up @@ -425,7 +426,7 @@ impl<'src, D> Recipe<'src, D> {
command.args(positional);
}

command.export(context.settings, context.dotenv, scope);
command.export(context.settings, context.dotenv, scope, context.unexports);

// run it!
match InterruptHandler::guard(|| command.status()) {
Expand Down
1 change: 1 addition & 0 deletions src/recipe_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ pub(crate) struct RecipeContext<'src: 'run, 'run> {
pub(crate) scope: &'run Scope<'src, 'run>,
pub(crate) search: &'run Search,
pub(crate) settings: &'run Settings<'src>,
pub(crate) unexports: &'run HashSet<String>,
}
6 changes: 3 additions & 3 deletions tests/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ fn bash() {
#[test]
fn replacements() {
for shell in ["bash", "elvish", "fish", "powershell", "zsh"] {
let status = Command::new(executable_path("just"))
let output = Command::new(executable_path("just"))
.args(["--completions", shell])
.status()
.output()
.unwrap();
assert!(status.success());
assert!(output.status.success());
}
}
Loading