diff --git a/CHANGELOG.md b/CHANGELOG.md index baf5ccb..658561a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added support for running scripts by passing absolute file paths in the CLI + - This does not have the restriction of scripts having to use the `.luau` or `.lua` extension, since it is presumed that if you pass an absolute path you know exactly what you are doing + +### Changed + +- Improved error messages for passing invalid file names / file paths substantially - they now include helpful formatting to make file names distinct from file extensions, and give suggestions on how to solve the problem + +### Fixed + +- Fixed the CLI being a bit too picky about file names when trying to run files in `lune` or `.lune` directories + ## `0.5.4` - March 7th, 2023 ### Added diff --git a/docs/pages/Getting Started - 3 Running Scripts.md b/docs/pages/Getting Started - 3 Running Scripts.md index fa220fd..edc27e5 100644 --- a/docs/pages/Getting Started - 3 Running Scripts.md +++ b/docs/pages/Getting Started - 3 Running Scripts.md @@ -46,4 +46,4 @@ Runs a script passed to Lune using stdin. Occasionally useful for running script --- -**_[1]_** _Lune also supports files with the `.lua` extension but using the `.luau` extension is highly recommended. Additionally, if you don't want Lune to look in sub-directories you can provide a full file path with the file extension included, instead of only the file name._ +**_[1]_** _Lune also supports files with the `.lua` extension but using the `.luau` extension is highly recommended. Additionally, if you don't want Lune to look in sub-directories or try to find files with `.lua` / `.luau` extensions at all, you can provide an absolute file path. This will disable all file path parsing and checks, and just run the file directly._ diff --git a/packages/cli/src/cli.rs b/packages/cli/src/cli.rs index cadeba9..b8a2892 100644 --- a/packages/cli/src/cli.rs +++ b/packages/cli/src/cli.rs @@ -15,7 +15,7 @@ use crate::{ generate_selene_defs_from_definitions, generate_wiki_dir_from_definitions, }, utils::{ - files::find_parse_file_path, + files::discover_script_file_path_including_lune_dirs, listing::{find_lune_scripts, print_lune_scripts, sort_lune_scripts}, }, }; @@ -168,7 +168,7 @@ impl Cli { .context("Failed to read script contents from stdin")?; ("stdin".to_string(), stdin_contents) } else { - let file_path = find_parse_file_path(&script_path)?; + let file_path = discover_script_file_path_including_lune_dirs(&script_path)?; let file_contents = read_to_vec(&file_path).await?; // NOTE: We skip the extension here to remove it from stack traces let file_display_name = file_path.with_extension("").display().to_string(); diff --git a/packages/cli/src/utils/files.rs b/packages/cli/src/utils/files.rs index 863e0b7..88f4796 100644 --- a/packages/cli/src/utils/files.rs +++ b/packages/cli/src/utils/files.rs @@ -1,43 +1,136 @@ -use std::path::{PathBuf, MAIN_SEPARATOR}; +use std::{ + fs::Metadata, + path::{PathBuf, MAIN_SEPARATOR}, +}; -use anyhow::{bail, Result}; +use anyhow::{anyhow, Result}; +use console::style; +use lazy_static::lazy_static; const LUNE_COMMENT_PREFIX: &str = "-->"; -pub fn find_luau_file_path(path: &str) -> Option { +lazy_static! { + static ref ERR_MESSAGE_HELP_NOTE: String = format!( + "To run this file, either:\n{}\n{}", + format_args!( + "{} rename it to use a {} or {} extension", + style("-").dim(), + style(".luau").blue(), + style(".lua").blue() + ), + format_args!( + "{} pass it as an absolute path instead of relative", + style("-").dim() + ), + ); +} + +/** + Discovers a script file path based on a given script name. + + Script discovery is done in several steps here for the best possible user experience: + + 1. If we got a file that definitely exists, make sure it is either + - using an absolute path + - has the lua or luau extension + 2. If we got a directory, let the user know + 3. If we got an absolute path, don't check any extensions, just let the user know it didn't exist + 4. If we got a relative path with no extension, also look for a file with a lua or luau extension + 5. No other options left, the file simply did not exist + + This behavior ensures that users can do pretty much whatever they want if they pass in an absolute + path, and that they then have control over script discovery behavior, whereas if they pass in + a relative path we will instead try to be as permissive as possible for user-friendliness +*/ +pub fn discover_script_file_path(path: &str) -> Result { let file_path = PathBuf::from(path); - if let Some(ext) = file_path.extension() { - match ext { - e if e == "lua" || e == "luau" && file_path.exists() => Some(file_path), - _ => None, + // NOTE: We use metadata directly here to try to + // avoid accessing the file path more than once + let file_meta = file_path.metadata(); + let is_file = file_meta.as_ref().map_or(false, Metadata::is_file); + let is_dir = file_meta.as_ref().map_or(false, Metadata::is_dir); + let is_abs = file_path.is_absolute(); + let ext = file_path.extension(); + if is_file { + if is_abs { + Ok(file_path) + } else if let Some(ext) = file_path.extension() { + match ext { + e if e == "lua" || e == "luau" => Ok(file_path), + _ => Err(anyhow!( + "A file was found at {} but it uses the '{}' file extension\n{}", + style(file_path.display()).green(), + style(ext.to_string_lossy()).blue(), + *ERR_MESSAGE_HELP_NOTE + )), + } + } else { + Err(anyhow!( + "A file was found at {} but it has no file extension\n{}", + style(file_path.display()).green(), + *ERR_MESSAGE_HELP_NOTE + )) + } + } else if is_dir { + Err(anyhow!( + "No file was found at {}, found a directory", + style(file_path.display()).yellow() + )) + } else if is_abs { + Err(anyhow!( + "No file was found at {}", + style(file_path.display()).yellow() + )) + } else if ext.is_none() { + let file_path_lua = file_path.with_extension("lua"); + let file_path_luau = file_path.with_extension("luau"); + if file_path_lua.is_file() { + Ok(file_path_lua) + } else if file_path_luau.is_file() { + Ok(file_path_luau) + } else { + Err(anyhow!( + "No file was found at {}", + style(file_path.display()).yellow() + )) } } else { - let file_path_lua = PathBuf::from(path).with_extension("lua"); - if file_path_lua.exists() { - Some(file_path_lua) - } else { - let file_path_luau = PathBuf::from(path).with_extension("luau"); - if file_path_luau.exists() { - Some(file_path_luau) - } else { - None - } - } + Err(anyhow!( + "No file was found at {}", + style(file_path.display()).yellow() + )) } } -pub fn find_parse_file_path(path: &str) -> Result { - let parsed_file_path = find_luau_file_path(path) - .or_else(|| find_luau_file_path(&format!("lune{MAIN_SEPARATOR}{path}"))) - .or_else(|| find_luau_file_path(&format!(".lune{MAIN_SEPARATOR}{path}"))); - if let Some(file_path) = parsed_file_path { - if file_path.exists() { - Ok(file_path) - } else { - bail!("File does not exist at path: '{}'", path) +/** + Discovers a script file path based on a given script name, and tries to + find scripts in `lune` and `.lune` folders if one was not directly found. + + Note that looking in `lune` and `.lune` folders is automatically + disabled if the given script name is an absolute path. + + Behavior is otherwise exactly the same as for `discover_script_file_path`. +*/ +pub fn discover_script_file_path_including_lune_dirs(path: &str) -> Result { + match discover_script_file_path(path) { + Ok(path) => Ok(path), + Err(e) => { + // If we got any absolute path it means the user has also + // told us to not look in any special relative directories + // so we should error right away with the first err message + if PathBuf::from(path).is_absolute() { + return Err(e); + } + // Otherwise we take a look in either relative lune or .lune directories + let res_lune = discover_script_file_path(&format!("lune{MAIN_SEPARATOR}{path}")); + let res_dotlune = discover_script_file_path(&format!(".lune{MAIN_SEPARATOR}{path}")); + match res_lune.or(res_dotlune) { + // NOTE: The first error message is generally more + // descriptive than the ones for the lune subfolders + Err(_) => Err(e), + Ok(path) => Ok(path), + } } - } else { - bail!("Invalid file path: '{}'", path) } }