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

feat(layout): Support environment variables in cwd (#2288) #2291

Merged
merged 6 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
21 changes: 20 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zellij-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ miette = { version = "3.3.0", features = ["fancy"] }
regex = "1.5.5"
tempfile = "3.2.0"
kdl = { version = "4.5.0", features = ["span"] }
shellexpand = "3.0.0"

#[cfg(not(target_family = "wasm"))]
[target.'cfg(not(target_family = "wasm"))'.dependencies]
Expand Down
154 changes: 154 additions & 0 deletions zellij-utils/src/input/unit/layout_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2031,3 +2031,157 @@ fn run_plugin_location_parsing() {
};
assert_eq!(layout, expected_layout);
}

#[track_caller]
fn env_test_helper(layout_str: &str, env_vars: Vec<(&str, &str)>, expected_output: Vec<&str>) {
for (key, value) in &env_vars {
std::env::set_var(key, value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will set the environment variable globally for all the tests, no? I'm a little wary of such things. Ideally we can find a solution that does this locally only for one test, and if not maybe we can unset these after the assert?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's true, and this is why I used weird names for the vars I set (besides $HOME).

current version handles restoring the previous env after parsing.

}
let layout = Layout::from_kdl(layout_str, "layout_file_name".into(), None, None).unwrap();
let layout = format!("{layout:#?}",);
for (key, value) in &env_vars {
assert!(
!layout.contains(&format!("${key}")) && layout.contains(value),
"environment variable `{key}={value}` was not properly expanded",
);
}
for s in expected_output {
assert!(layout.contains(s), "expected string `{s}` was not found");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about asserting the cwd in the parsed structures (iirc they are a TildPaneLayout in this case) directly instead of parsing and stringifying? I think it'll be more accurate and clear, but maybe it's too much work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the snapshot test I added handles this request indirectly 😃

}
}

#[test]
fn env_valid_global_cwd() {
env_test_helper(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, but I personally find it a little hard to understand at a glance. Which I think is important when we're considering tests. How about we change it to something like (pseudocode, if you have a better idea then by all means!)

LayoutTesterWithEnvironment::new()
    .set_env_variables(/* ... */)
    .parse_layout(stringified_layout)
    .assert(/* .. */)
    .assert(/* .. */)

r#"
layout {
cwd "$Z_VALID_GLOBAL"
pane cwd="relative" // -> /abs/path/relative
pane cwd="/another/abs" // -> /another/abs
}
"#,
vec![("Z_VALID_GLOBAL", "/abs/path")],
vec!["/abs/path/relative", "/another/abs"],
);
}

#[test]
fn env_valid_local_abs_cwd() {
env_test_helper(
r#"
layout {
cwd "/abs/path"
pane cwd="relative" // -> /abs/path/relative
pane cwd="$Z_VALID_LOCAL" // -> /another/abs
}
"#,
vec![("Z_VALID_LOCAL", "/another/abs")],
vec!["/abs/path/relative", "/another/abs"],
);
}

#[test]
fn env_valid_local_relative_cwd() {
env_test_helper(
r#"
layout {
cwd "/abs/path"
pane cwd="relative" // -> /abs/path/relative
pane cwd="$Z_VALID_LOCAL_REL" // -> /abs/path/relative
}
"#,
vec![("Z_VALID_LOCAL_REL", "relative")],
vec!["/abs/path/relative"],
);
}

#[test]
fn env_command_cwd() {
env_test_helper(
r#"
layout {
pane command="ls" cwd="$Z_COMMAND" // -> some/path
}
"#,
vec![("Z_COMMAND", "some/path")],
vec!["some/path"],
);
}

#[test]
fn env_edit_cwd() {
env_test_helper(
r#"
layout {
pane edit="file.rs" cwd="$Z_EDIT" // -> some/path/file.rs
}
"#,
vec![("Z_EDIT", "some/path")],
vec!["some/path/file.rs"],
);
}

#[test]
fn env_tilde_cwd() {
env_test_helper(
r#"
layout {
pane edit="file.rs" cwd="~/my/folder" // -> /home/aram/my/folder/file.rs
}
"#,
vec![("HOME", "/home/aram")],
vec!["/home/aram/my/folder/file.rs"],
);
}

#[test]
fn env_command() {
env_test_helper(
r#"
layout {
pane command="~/backup/executable" // -> /home/aram/backup/executable
}
"#,
vec![("HOME", "/home/aram")],
vec!["/home/aram/backup/executable"],
);
}

#[test]
fn env_edit() {
env_test_helper(
r#"
layout {
pane edit="~/backup/foo.txt" // -> /home/aram/backup/foo.txt
}
"#,
vec![("HOME", "/home/aram")],
vec!["/home/aram/backup/foo.txt"],
);
}

#[test]
fn env_invalid_global_cwd() {
std::env::remove_var("Z_INVALID_GLOBAL");
let kdl_layout = r#"
layout {
cwd "$Z_INVALID_GLOBAL"
pane cwd="relative"
}
"#;
let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None);
assert!(layout.is_err(), "invalid env var lookup should fail");
}

#[test]
fn env_invalid_local_cwd() {
std::env::remove_var("Z_INVALID_LOCAL");
let kdl_layout = r#"
layout {
cwd "/abs/path"
pane cwd="$Z_INVALID_LOCAL"
}
"#;
let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None);
assert!(layout.is_err(), "invalid env var lookup should fail");
}
40 changes: 19 additions & 21 deletions zellij-utils/src/kdl/kdl_layout_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,22 +331,27 @@ impl<'a> KdlLayoutParser<'a> {
(None, None) => None,
})
}
fn parse_cwd(&self, kdl_node: &KdlNode) -> Result<Option<PathBuf>, ConfigError> {
Ok(
kdl_get_string_property_or_child_value_with_error!(kdl_node, "cwd")
.map(|cwd| PathBuf::from(cwd)),
)
fn parse_path(
&self,
kdl_node: &KdlNode,
name: &'static str,
) -> Result<Option<PathBuf>, ConfigError> {
match kdl_get_string_property_or_child_value_with_error!(kdl_node, name) {
Some(s) => match shellexpand::full(s) {
Ok(s) => Ok(Some(PathBuf::from(s.as_ref()))),
Err(e) => Err(kdl_parsing_error!(e.to_string(), kdl_node)),
},
None => Ok(None),
}
}
fn parse_pane_command(
&self,
pane_node: &KdlNode,
is_template: bool,
) -> Result<Option<Run>, ConfigError> {
let command = kdl_get_string_property_or_child_value_with_error!(pane_node, "command")
.map(|c| PathBuf::from(c));
let edit = kdl_get_string_property_or_child_value_with_error!(pane_node, "edit")
.map(|c| PathBuf::from(c));
let cwd = self.parse_cwd(pane_node)?;
let command = self.parse_path(pane_node, "command")?;
let edit = self.parse_path(pane_node, "edit")?;
let cwd = self.parse_path(pane_node, "cwd")?;
let args = self.parse_args(pane_node)?;
let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit");
Expand Down Expand Up @@ -1006,8 +1011,7 @@ impl<'a> KdlLayoutParser<'a> {
self.assert_valid_tab_properties(kdl_node)?;
let tab_name =
kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string());
let tab_cwd =
kdl_get_string_property_or_child_value!(kdl_node, "cwd").map(|c| PathBuf::from(c));
let tab_cwd = self.parse_path(kdl_node, "cwd")?;
let is_focused = kdl_get_bool_property_or_child_value!(kdl_node, "focus").unwrap_or(false);
let children_split_direction = self.parse_split_direction(kdl_node)?;
let mut child_floating_panes = vec![];
Expand Down Expand Up @@ -1333,8 +1337,7 @@ impl<'a> KdlLayoutParser<'a> {
) -> Result<(), ConfigError> {
let has_borderless_prop =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless").is_some();
let has_cwd_prop =
kdl_get_string_property_or_child_value_with_error!(kdl_node, "cwd").is_some();
let has_cwd_prop = self.parse_path(kdl_node, "cwd")?.is_some();
let has_non_cwd_run_prop = self
.parse_command_plugin_or_edit_block(kdl_node)?
.map(|r| match r {
Expand Down Expand Up @@ -1404,8 +1407,7 @@ impl<'a> KdlLayoutParser<'a> {
// (is_focused, Option<tab_name>, PaneLayout, Vec<FloatingPaneLayout>)
let tab_name =
kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string());
let tab_cwd =
kdl_get_string_property_or_child_value!(kdl_node, "cwd").map(|c| PathBuf::from(c));
let tab_cwd = self.parse_path(kdl_node, "cwd")?;
let is_focused = kdl_get_bool_property_or_child_value!(kdl_node, "focus").unwrap_or(false);
let children_split_direction = self.parse_split_direction(kdl_node)?;
match kdl_children_nodes!(kdl_node) {
Expand Down Expand Up @@ -1638,11 +1640,7 @@ impl<'a> KdlLayoutParser<'a> {
fn populate_global_cwd(&mut self, layout_node: &KdlNode) -> Result<(), ConfigError> {
// we only populate global cwd from the layout file if another wasn't explicitly passed to us
if self.global_cwd.is_none() {
if let Some(global_cwd) =
kdl_get_string_property_or_child_value_with_error!(layout_node, "cwd")
{
self.global_cwd = Some(PathBuf::from(global_cwd));
}
self.global_cwd = self.parse_path(layout_node, "cwd")?;
}
Ok(())
}
Expand Down