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

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

* add `shellexpand` as dependency
* expand environment variable in kdl parser's `parse_cwd()`

* Fix and enhance environment variable expansion.

* Return a proper `ConfigError` on failures.
* Replace raw cwd parsing with `parse_cwd()`.
* Add tests that verify correct expansions.

* Perform env var expansion in more contexts.

* feat(layout): Rewrite env var tests as snapshots.

* Update layout env var expansion test snapshot.
This commit is contained in:
Ran Shaham 2023-04-28 17:45:08 +03:00 committed by GitHub
parent b2ec105c76
commit b640270804
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 309 additions and 22 deletions

21
Cargo.lock generated
View file

@ -814,6 +814,15 @@ dependencies = [
"dirs-sys", "dirs-sys",
] ]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]] [[package]]
name = "dirs-sys" name = "dirs-sys"
version = "0.3.7" version = "0.3.7"
@ -2529,6 +2538,15 @@ dependencies = [
"opaque-debug 0.3.0", "opaque-debug 0.3.0",
] ]
[[package]]
name = "shellexpand"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
dependencies = [
"dirs 4.0.0",
]
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.1.17" version = "0.1.17"
@ -2880,7 +2898,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e" checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
dependencies = [ dependencies = [
"dirs", "dirs 2.0.2",
"fnv", "fnv",
"nom 5.1.2", "nom 5.1.2",
"phf 0.8.0", "phf 0.8.0",
@ -4057,6 +4075,7 @@ dependencies = [
"rmp-serde", "rmp-serde",
"serde", "serde",
"serde_json", "serde_json",
"shellexpand",
"signal-hook 0.3.14", "signal-hook 0.3.14",
"strip-ansi-escapes", "strip-ansi-escapes",
"strum", "strum",

View file

@ -38,6 +38,7 @@ miette = { version = "3.3.0", features = ["fancy"] }
regex = "1.5.5" regex = "1.5.5"
tempfile = "3.2.0" tempfile = "3.2.0"
kdl = { version = "4.5.0", features = ["span"] } kdl = { version = "4.5.0", features = ["span"] }
shellexpand = "3.0.0"
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
[target.'cfg(not(target_family = "wasm"))'.dependencies] [target.'cfg(not(target_family = "wasm"))'.dependencies]

View file

@ -2071,3 +2071,60 @@ fn run_plugin_location_parsing() {
}; };
assert_eq!(layout, expected_layout); assert_eq!(layout, expected_layout);
} }
#[test]
fn env_var_expansion() {
let raw_layout = r#"
layout {
// cwd tests + composition
cwd "$TEST_GLOBAL_CWD"
pane cwd="relative" // -> /abs/path/relative
pane cwd="/another/abs" // -> /another/abs
pane cwd="$TEST_LOCAL_CWD" // -> /another/abs
pane cwd="$TEST_RELATIVE" // -> /abs/path/relative
pane command="ls" cwd="$TEST_ABSOLUTE" // -> /somewhere
pane edit="file.rs" cwd="$TEST_ABSOLUTE" // -> /somewhere/file.rs
pane edit="file.rs" cwd="~/backup" // -> /home/aram/backup/file.rs
// other paths
pane command="~/backup/executable" // -> /home/aram/backup/executable
pane edit="~/backup/foo.txt" // -> /home/aram/backup/foo.txt
}
"#;
let env_vars = [
("TEST_GLOBAL_CWD", "/abs/path"),
("TEST_LOCAL_CWD", "/another/abs"),
("TEST_RELATIVE", "relative"),
("TEST_ABSOLUTE", "/somewhere"),
("HOME", "/home/aram"),
];
let mut old_vars = Vec::new();
// set environment variables for test, keeping track of existing values.
for (key, value) in env_vars {
old_vars.push((key, std::env::var(key).ok()));
std::env::set_var(key, value);
}
let layout = Layout::from_kdl(raw_layout, "layout_file_name".into(), None, None);
// restore environment.
for (key, opt) in old_vars {
match opt {
Some(value) => std::env::set_var(key, &value),
None => std::env::remove_var(key),
}
}
let layout = layout.unwrap();
assert_snapshot!(format!("{layout:#?}"));
}
#[test]
fn env_var_missing() {
std::env::remove_var("SOME_UNIQUE_VALUE");
let kdl_layout = r#"
layout {
cwd "$SOME_UNIQUE_VALUE"
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");
}

View file

@ -0,0 +1,212 @@
---
source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 2116
expression: "format!(\"{layout:#?}\")"
---
Layout {
tabs: [],
focused_tab_index: None,
template: Some(
(
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
Cwd(
"/abs/path/relative",
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
Cwd(
"/another/abs",
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
Cwd(
"/another/abs",
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
Cwd(
"/abs/path/relative",
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
Command(
RunCommand {
command: "ls",
args: [],
cwd: Some(
"/somewhere",
),
hold_on_close: true,
hold_on_start: false,
},
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
EditFile(
"/somewhere/file.rs",
None,
Some(
"/somewhere",
),
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
EditFile(
"/home/aram/backup/file.rs",
None,
Some(
"/home/aram/backup",
),
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
Command(
RunCommand {
command: "/home/aram/backup/executable",
args: [],
cwd: Some(
"/abs/path",
),
hold_on_close: true,
hold_on_start: false,
},
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
TiledPaneLayout {
children_split_direction: Horizontal,
name: None,
children: [],
split_size: None,
run: Some(
EditFile(
"/home/aram/backup/foo.txt",
None,
Some(
"/abs/path",
),
),
),
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
],
split_size: None,
run: None,
borderless: false,
focus: None,
external_children_index: None,
children_are_stacked: false,
is_expanded_in_stack: false,
exclude_from_sync: None,
},
[],
),
),
swap_layouts: [],
swap_tiled_layouts: [],
swap_floating_layouts: [],
}

View file

@ -333,22 +333,27 @@ impl<'a> KdlLayoutParser<'a> {
(None, None) => None, (None, None) => None,
}) })
} }
fn parse_cwd(&self, kdl_node: &KdlNode) -> Result<Option<PathBuf>, ConfigError> { fn parse_path(
Ok( &self,
kdl_get_string_property_or_child_value_with_error!(kdl_node, "cwd") kdl_node: &KdlNode,
.map(|cwd| PathBuf::from(cwd)), 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( fn parse_pane_command(
&self, &self,
pane_node: &KdlNode, pane_node: &KdlNode,
is_template: bool, is_template: bool,
) -> Result<Option<Run>, ConfigError> { ) -> Result<Option<Run>, ConfigError> {
let command = kdl_get_string_property_or_child_value_with_error!(pane_node, "command") let command = self.parse_path(pane_node, "command")?;
.map(|c| PathBuf::from(c)); let edit = self.parse_path(pane_node, "edit")?;
let edit = kdl_get_string_property_or_child_value_with_error!(pane_node, "edit") let cwd = self.parse_path(pane_node, "cwd")?;
.map(|c| PathBuf::from(c));
let cwd = self.parse_cwd(pane_node)?;
let args = self.parse_args(pane_node)?; let args = self.parse_args(pane_node)?;
let close_on_exit = let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit"); kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit");
@ -1047,8 +1052,7 @@ impl<'a> KdlLayoutParser<'a> {
self.assert_valid_tab_properties(kdl_node)?; self.assert_valid_tab_properties(kdl_node)?;
let tab_name = let tab_name =
kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string()); kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string());
let tab_cwd = let tab_cwd = self.parse_path(kdl_node, "cwd")?;
kdl_get_string_property_or_child_value!(kdl_node, "cwd").map(|c| PathBuf::from(c));
let is_focused = kdl_get_bool_property_or_child_value!(kdl_node, "focus").unwrap_or(false); 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 children_split_direction = self.parse_split_direction(kdl_node)?;
let mut child_floating_panes = vec![]; let mut child_floating_panes = vec![];
@ -1374,8 +1378,7 @@ impl<'a> KdlLayoutParser<'a> {
) -> Result<(), ConfigError> { ) -> Result<(), ConfigError> {
let has_borderless_prop = let has_borderless_prop =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless").is_some(); kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless").is_some();
let has_cwd_prop = let has_cwd_prop = self.parse_path(kdl_node, "cwd")?.is_some();
kdl_get_string_property_or_child_value_with_error!(kdl_node, "cwd").is_some();
let has_non_cwd_run_prop = self let has_non_cwd_run_prop = self
.parse_command_plugin_or_edit_block(kdl_node)? .parse_command_plugin_or_edit_block(kdl_node)?
.map(|r| match r { .map(|r| match r {
@ -1445,8 +1448,7 @@ impl<'a> KdlLayoutParser<'a> {
// (is_focused, Option<tab_name>, PaneLayout, Vec<FloatingPaneLayout>) // (is_focused, Option<tab_name>, PaneLayout, Vec<FloatingPaneLayout>)
let tab_name = let tab_name =
kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string()); kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string());
let tab_cwd = let tab_cwd = self.parse_path(kdl_node, "cwd")?;
kdl_get_string_property_or_child_value!(kdl_node, "cwd").map(|c| PathBuf::from(c));
let is_focused = kdl_get_bool_property_or_child_value!(kdl_node, "focus").unwrap_or(false); 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 children_split_direction = self.parse_split_direction(kdl_node)?;
match kdl_children_nodes!(kdl_node) { match kdl_children_nodes!(kdl_node) {
@ -1679,11 +1681,7 @@ impl<'a> KdlLayoutParser<'a> {
fn populate_global_cwd(&mut self, layout_node: &KdlNode) -> Result<(), ConfigError> { 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 // we only populate global cwd from the layout file if another wasn't explicitly passed to us
if self.global_cwd.is_none() { if self.global_cwd.is_none() {
if let Some(global_cwd) = self.global_cwd = self.parse_path(layout_node, "cwd")?;
kdl_get_string_property_or_child_value_with_error!(layout_node, "cwd")
{
self.global_cwd = Some(PathBuf::from(global_cwd));
}
} }
Ok(()) Ok(())
} }