Add path self command for getting absolute paths to files at parse time (#14303)

Alternative solution to:
- #12195 

The other approach:
- #14305

# Description
Adds ~`path const`~ `path self`, a parse-time only command for getting
the absolute path of the source file containing it, or any file relative
to the source file.

- Useful for any script or module that makes use of non nuscript files.
- Removes the need for `$env.CURRENT_FILE` and `$env.FILE_PWD`.
- Can be used in modules, sourced files or scripts.

# Examples

```nushell
# ~/.config/nushell/scripts/foo.nu
const paths = {
    self: (path self),
    dir: (path self .),
    sibling: (path self sibling),
    parent_dir: (path self ..),
    cousin: (path self ../cousin),
}

export def main [] {
    $paths
}
```

```nushell
> use foo.nu
> foo
╭────────────┬────────────────────────────────────────────╮
│ self       │ /home/user/.config/nushell/scripts/foo.nu  │
│ dir        │ /home/user/.config/nushell/scripts         │
│ sibling    │ /home/user/.config/nushell/scripts/sibling │
│ parent_dir │ /home/user/.config/nushell                 │
│ cousin     │ /home/user/.config/nushell/cousin          │
╰────────────┴────────────────────────────────────────────╯
```


Trying to run in a non-const context
```nushell
> path self
Error:   × this command can only run during parse-time
   ╭─[entry #1:1:1]
 1 │ path self 
   · ─────┬────
   ·      ╰── can't run after parse-time
   ╰────
  help: try assigning this command's output to a const variable
```

Trying to run in the REPL i.e. not in a file
```nushell
> const foo = path self
Error:   × Error: nu:🐚:file_not_found
  │ 
  │   × File not found
  │    ╭─[entry #3:1:13]
  │  1 │ const foo = path self
  │    ·             ─────┬────
  │    ·                  ╰── Couldn't find current file
  │    ╰────
  │ 
   ╭─[entry #3:1:13]
 1 │ const foo = path self
   ·             ─────┬────
   ·                  ╰── Encountered error during parse-time evaluation
   ╰────
```

# Comparison with #14305
## Pros
- Self contained implementation, does not require changes in the parser.
- More concise usage, especially with parent directories.

---------

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
Bahex 2024-12-06 17:19:08 +03:00 committed by GitHub
parent cda9ae1e42
commit 8771872d86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 132 additions and 0 deletions

View file

@ -106,6 +106,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
bind_command! {
Path,
PathBasename,
PathSelf,
PathDirname,
PathExists,
PathExpand,

View file

@ -6,6 +6,7 @@ mod join;
mod parse;
pub mod path_;
mod relative_to;
mod self_;
mod split;
mod r#type;
@ -18,6 +19,7 @@ pub use parse::SubCommand as PathParse;
pub use path_::PathCommand as Path;
pub use r#type::SubCommand as PathType;
pub use relative_to::SubCommand as PathRelativeTo;
pub use self_::SubCommand as PathSelf;
pub use split::SubCommand as PathSplit;
use nu_protocol::{ShellError, Span, Value};

View file

@ -0,0 +1,129 @@
use nu_engine::command_prelude::*;
use nu_path::expand_path_with;
use nu_protocol::engine::StateWorkingSet;
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path self"
}
fn signature(&self) -> Signature {
Signature::build("path self")
.input_output_type(Type::Nothing, Type::String)
.allow_variants_without_examples(true)
.optional(
"path",
SyntaxShape::Filepath,
"Path to get instead of the current file.",
)
.category(Category::Path)
}
fn description(&self) -> &str {
"Get the absolute path of the script or module containing this command at parse time."
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Err(ShellError::GenericError {
error: "this command can only run during parse-time".into(),
msg: "can't run after parse-time".into(),
span: Some(call.head),
help: Some("try assigning this command's output to a const variable".into()),
inner: vec![],
})
}
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let path: Option<String> = call.opt_const(working_set, 0)?;
let cwd = working_set.permanent_state.cwd(None)?;
let current_file =
working_set
.files
.top()
.ok_or_else(|| ShellError::FileNotFoundCustom {
msg: "Couldn't find current file".into(),
span: call.head,
})?;
let out = if let Some(path) = path {
let dir = expand_path_with(
current_file
.parent()
.ok_or_else(|| ShellError::FileNotFoundCustom {
msg: "Couldn't find current file's parent.".into(),
span: call.head,
})?,
&cwd,
true,
);
Value::string(
expand_path_with(path, dir, false).to_string_lossy(),
call.head,
)
} else {
Value::string(
expand_path_with(current_file, &cwd, true).to_string_lossy(),
call.head,
)
};
Ok(out.into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get the path of the current file",
example: r#"const current_file = path self"#,
result: None,
},
Example {
description: "Get the path of the directory containing the current file",
example: r#"const current_file = path self ."#,
result: None,
},
#[cfg(windows)]
Example {
description: "Get the absolute form of a path relative to the current file",
example: r#"const current_file = path self ..\foo"#,
result: None,
},
#[cfg(not(windows))]
Example {
description: "Get the absolute form of a path relative to the current file",
example: r#"const current_file = path self ../foo"#,
result: None,
},
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}