From 84da9e78afb8159ceedad3472d70919da8a2f6a4 Mon Sep 17 00:00:00 2001 From: Viktor Kleen Date: Tue, 8 Aug 2023 11:55:03 +0000 Subject: [PATCH] Refactor the Nickel CLI (#1502) * Refactor the Nickel CLI structure * Add an `Error` enum for the CLI * Restore the previous user-facing interface * Make clippy happy * Restore `nickel format` CLI behaviour * Change command options naming * Properly respect feature flags * WithProgram -> ReportWithProgram * Remove the unecessary `Files` struct for now * ReportWithProgram -> ResultErrorExt * Suggestions from code review --- Cargo.lock | 10 ++ Cargo.toml | 3 +- cli/Cargo.toml | 3 +- cli/bin/nickel.rs | 248 ----------------------------------------- cli/src/cli.rs | 80 +++++++++++++ cli/src/completions.rs | 22 ++++ cli/src/doc.rs | 137 ++++++++++++++--------- cli/src/error.rs | 74 ++++++++++++ cli/src/eval.rs | 40 +++++++ cli/src/export.rs | 62 +++++++++++ cli/src/format.rs | 108 ++++++++---------- cli/src/lib.rs | 6 - cli/src/main.rs | 45 ++++++++ cli/src/pprint_ast.rs | 21 ++++ cli/src/query.rs | 67 +++++++++++ cli/src/repl.rs | 34 +++--- cli/src/typecheck.rs | 15 +++ core/src/program.rs | 2 +- 18 files changed, 593 insertions(+), 384 deletions(-) delete mode 100644 cli/bin/nickel.rs create mode 100644 cli/src/cli.rs create mode 100644 cli/src/completions.rs create mode 100644 cli/src/error.rs create mode 100644 cli/src/eval.rs create mode 100644 cli/src/export.rs delete mode 100644 cli/src/lib.rs create mode 100644 cli/src/main.rs create mode 100644 cli/src/pprint_ast.rs create mode 100644 cli/src/query.rs create mode 100644 cli/src/typecheck.rs diff --git a/Cargo.lock b/Cargo.lock index 887002ea46..a6e60ef304 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,15 @@ dependencies = [ "terminal_size", ] +[[package]] +name = "clap_complete" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" +dependencies = [ + "clap 4.3.0", +] + [[package]] name = "clap_derive" version = "4.3.0" @@ -1620,6 +1629,7 @@ name = "nickel-lang-cli" version = "1.1.1" dependencies = [ "clap 4.3.0", + "clap_complete", "directories", "git-version", "insta", diff --git a/Cargo.toml b/Cargo.toml index 71f5ed6196..6abf689767 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ assert_cmd = "2.0.11" assert_matches = "1.5.0" atty = "0.2.14" clap = "4.3" +clap_complete = "4.3.2" codespan = "0.11" codespan-lsp = "0.11" codespan-reporting = "0.11" @@ -47,6 +48,7 @@ csv = "1" derive_more = "0.99" directories = "4.0.1" env_logger = "0.10" +git-version = "0.3.5" indexmap = "1.9.3" indoc = "2" insta = "1.29.0" @@ -86,7 +88,6 @@ toml = "0.7.2" typed-arena = "2.0.2" unicode-segmentation = "1.10.1" void = "1" -git-version = "0.3.5" topiary = { version = "0.2.3", git = "https://github.com/tweag/topiary.git", rev = "refs/heads/main" } # This should be kept in sync with the revision in topiary diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6b5e70060c..2d629d7fb2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -12,7 +12,7 @@ readme.workspace = true [[bin]] name = "nickel" -path = "bin/nickel.rs" +path = "src/main.rs" bench = false [features] @@ -33,6 +33,7 @@ tree-sitter-nickel = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } git-version = { workspace = true } +clap_complete = { workspace = true } [dev-dependencies] nickel-lang-utils.workspace = true diff --git a/cli/bin/nickel.rs b/cli/bin/nickel.rs deleted file mode 100644 index 789b7a8848..0000000000 --- a/cli/bin/nickel.rs +++ /dev/null @@ -1,248 +0,0 @@ -//! Entry point of the program. -use core::fmt; -use git_version::git_version; -use nickel_lang_core::error::{Error, IOError}; -use nickel_lang_core::eval::cache::CacheImpl; -use nickel_lang_core::program::Program; -use nickel_lang_core::repl::query_print; -use nickel_lang_core::term::RichTerm; -use nickel_lang_core::{serialize, serialize::ExportFormat}; - -use std::path::PathBuf; -use std::{fs, io::Write, process}; - -/// Command-line options and subcommands. -#[derive(clap::Parser, Debug)] -/// The interpreter of the Nickel language. -struct Opt { - /// The input file. Standard input by default - #[arg(short, long, global = true)] - file: Option, - - #[cfg(debug_assertions)] - /// Skips the standard library import. For debugging only. This does not affect REPL - #[arg(long)] - nostdlib: bool, - - /// Coloring: auto, always, never. - #[arg(long, global = true, value_enum, default_value_t)] - color: clap::ColorChoice, - - #[command(subcommand)] - command: Option, - - #[arg(long, short = 'V')] - version: bool, -} - -/// Available subcommands. -#[derive(clap::Subcommand, Debug)] -enum Command { - /// Converts the parsed representation (AST) back to Nickel source code and prints it. Used for - /// debugging purpose - PprintAst { - /// Performs code transformations before printing - #[arg(long)] - transform: bool, - }, - /// Exports the result to a different format - Export { - #[arg(long, value_enum, default_value_t)] - format: ExportFormat, - - /// Output file. Standard output by default - #[arg(short, long)] - output: Option, - }, - /// Prints the metadata attached to an attribute, given as a path - Query { - path: Option, - #[arg(long)] - doc: bool, - #[arg(long)] - contract: bool, - #[arg(long = "type")] - typ: bool, - #[arg(long)] - default: bool, - #[arg(long)] - value: bool, - }, - /// Typechecks the program but do not run it - Typecheck, - /// Starts an REPL session - #[cfg(feature = "repl")] - Repl { - #[arg(long)] - history_file: Option, - }, - /// Generates the documentation files for the specified nickel file - #[cfg(feature = "doc")] - Doc { - /// The path of the generated documentation file. Default to - /// `~/.nickel/doc/.md` for input `.ncl`, or to - /// `~/.nickel/doc/out.md` if the input is read from stdin. - #[arg(short, long)] - output: Option, - /// Write documentation to stdout. Takes precedence over `output` - #[arg(long)] - stdout: bool, - /// The output format for the generated documentation. - #[arg(long, value_enum, default_value_t)] - format: nickel_lang_cli::doc::DocFormat, - }, - /// Format a nickel file - #[cfg(feature = "format")] - Format { - /// Output file. Standard output by default. - #[arg(short, long)] - output: Option, - /// Format in place, overwriting the input file. - #[arg(short, long, requires = "file")] - in_place: bool, - }, -} - -#[derive(Clone, Eq, PartialEq, Debug)] -pub struct ParseFormatError(String); - -impl fmt::Display for ParseFormatError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "unsupported export format {}", self.0) - } -} - -fn handle_eval_commands(opts: Opt) { - let mut program = opts - .file - .clone() - .map(|f| Program::new_from_file(f, std::io::stderr())) - .unwrap_or_else(|| Program::new_from_stdin(std::io::stderr())) - .unwrap_or_else(|err| { - eprintln!("Error when reading input: {err}"); - process::exit(1) - }); - - #[cfg(debug_assertions)] - if opts.nostdlib { - program.set_skip_stdlib(); - } - - program.set_color(opts.color.into()); - - let result = match opts.command { - Some(Command::PprintAst { transform }) => program.pprint_ast( - &mut std::io::BufWriter::new(Box::new(std::io::stdout())), - transform, - ), - Some(Command::Export { format, output }) => export(&mut program, format, output), - Some(Command::Query { - path, - doc, - contract, - typ: types, - default, - value, - }) => { - program.query(path).map(|term| { - // Print a default selection of attributes if no option is specified - let attrs = if !doc && !contract && !types && !default && !value { - query_print::Attributes::default() - } else { - query_print::Attributes { - doc, - contract, - typ: types, - default, - value, - } - }; - - query_print::write_query_result(&mut std::io::stdout(), &term, attrs).unwrap() - }) - } - Some(Command::Typecheck) => program.typecheck(), - #[cfg(feature = "doc")] - Some(Command::Doc { - output, - stdout, - format, - }) => nickel_lang_cli::doc::export_doc( - &mut program, - opts.file.as_ref(), - output, - stdout, - format, - ), - None => program.eval_full().map(|t| println!("{t}")), - - #[cfg(feature = "repl")] - Some(Command::Repl { .. }) => unreachable!(), - #[cfg(feature = "format")] - Some(Command::Format { .. }) => unreachable!(), - }; - - if let Err(err) = result { - program.report(err); - process::exit(1) - } -} - -fn main() { - use clap::Parser; - - let opts = Opt::parse(); - - if opts.version { - println!( - "{} {} (rev {})", - env!("CARGO_BIN_NAME"), - env!("CARGO_PKG_VERSION"), - git_version!(fallback = env!("NICKEL_NIX_BUILD_REV")) - ); - return; - } - - match opts.command { - #[cfg(feature = "repl")] - Some(Command::Repl { history_file }) => { - nickel_lang_cli::repl::repl(history_file, opts.color.into()) - } - #[cfg(feature = "format")] - Some(Command::Format { output, in_place }) => { - nickel_lang_cli::format::format(opts.file.as_deref(), output.as_deref(), in_place) - } - _ => handle_eval_commands(opts), - } -} - -fn export( - program: &mut Program, - format: ExportFormat, - output: Option, -) -> Result<(), Error> { - let rt = program.eval_full_for_export().map(RichTerm::from)?; - - // We only add a trailing newline for JSON exports. Both YAML and TOML - // exporters already append a trailing newline by default. - let trailing_newline = format == ExportFormat::Json; - - serialize::validate(format, &rt)?; - - if let Some(file) = output { - let mut file = fs::File::create(file).map_err(IOError::from)?; - serialize::to_writer(&mut file, format, &rt)?; - - if trailing_newline { - writeln!(file).map_err(IOError::from)?; - } - } else { - serialize::to_writer(std::io::stdout(), format, &rt)?; - - if trailing_newline { - println!(); - } - } - - Ok(()) -} diff --git a/cli/src/cli.rs b/cli/src/cli.rs new file mode 100644 index 0000000000..d1877a6e4e --- /dev/null +++ b/cli/src/cli.rs @@ -0,0 +1,80 @@ +//! Command-line options and subcommands. +use std::path::PathBuf; + +use git_version::git_version; + +use crate::{ + completions::GenCompletionsCommand, eval::EvalCommand, export::ExportCommand, + pprint_ast::PprintAstCommand, query::QueryCommand, typecheck::TypecheckCommand, +}; + +#[cfg(feature = "repl")] +use crate::repl::ReplCommand; + +#[cfg(feature = "doc")] +use crate::doc::DocCommand; + +#[cfg(feature = "format")] +use crate::format::FormatCommand; + +#[derive(clap::Parser, Debug)] +/// The interpreter of the Nickel language. +#[command( + author, + about, + long_about = None, + version = format!("{} {} (rev {})", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION"), git_version!(fallback = env!("NICKEL_NIX_BUILD_REV"))) +)] +pub struct Options { + #[command(flatten)] + pub global: GlobalOptions, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(clap::Parser, Debug)] +pub struct GlobalOptions { + #[cfg(debug_assertions)] + /// Skips the standard library import. For debugging only. This does not affect REPL + #[arg(long, global = true)] + pub nostdlib: bool, + + /// Configure when to output messages in color + #[arg(long, global = true, value_enum, default_value_t)] + pub color: clap::ColorChoice, + + /// Input file, omit to read from stdin + #[arg(long, short, global = true)] + pub file: Option, +} + +/// Available subcommands. +#[derive(clap::Subcommand, Debug)] +pub enum Command { + /// Evaluate a Nickel program and pretty-print the result. + #[command(hide = true)] + Eval(EvalCommand), + + /// Converts the parsed representation (AST) back to Nickel source code and prints it. Used for + /// debugging purpose + PprintAst(PprintAstCommand), + /// Exports the result to a different format + Export(ExportCommand), + /// Prints the metadata attached to an attribute, given as a path + Query(QueryCommand), + /// Typechecks the program but do not run it + Typecheck(TypecheckCommand), + /// Starts a REPL session + #[cfg(feature = "repl")] + Repl(ReplCommand), + /// Generates the documentation files for the specified nickel file + #[cfg(feature = "doc")] + Doc(DocCommand), + /// Format Nickel files + #[cfg(feature = "format")] + Format(FormatCommand), + + /// Generate shell completion files + GenCompletions(GenCompletionsCommand), +} diff --git a/cli/src/completions.rs b/cli/src/completions.rs new file mode 100644 index 0000000000..f741bae85c --- /dev/null +++ b/cli/src/completions.rs @@ -0,0 +1,22 @@ +use crate::{ + cli::{GlobalOptions, Options}, + error::CliResult, +}; + +#[derive(clap::Parser, Debug)] +pub struct GenCompletionsCommand { + #[arg(value_enum)] + pub shell: clap_complete::Shell, +} + +impl GenCompletionsCommand { + pub fn run(self, _: GlobalOptions) -> CliResult<()> { + clap_complete::generate( + self.shell, + &mut ::command(), + env!("CARGO_BIN_NAME"), + &mut std::io::stdout(), + ); + Ok(()) + } +} diff --git a/cli/src/doc.rs b/cli/src/doc.rs index 4be004e0c8..b454fd9603 100644 --- a/cli/src/doc.rs +++ b/cli/src/doc.rs @@ -9,6 +9,12 @@ use nickel_lang_core::{ program::Program, }; +use crate::{ + cli::GlobalOptions, + error::{CliResult, ResultErrorExt}, + eval, +}; + #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, clap::ValueEnum)] pub enum DocFormat { Json, @@ -34,66 +40,87 @@ impl fmt::Display for DocFormat { } } -pub fn export_doc( - program: &mut Program, - file: Option<&PathBuf>, - output: Option, - stdout: bool, - format: DocFormat, -) -> Result<(), Error> { - let doc = program.extract_doc()?; - let mut out: Box = if stdout { - Box::new(std::io::stdout()) - } else { - Box::new( - output - .as_ref() - .map(|output| { - fs::File::create(output.clone()).map_err(|e| { - Error::IOError(IOError(format!( - "when opening or creating output file `{}`: {}", - output.to_string_lossy(), - e - ))) +#[derive(clap::Parser, Debug)] +pub struct DocCommand { + /// The path of the generated documentation file. Default to + /// `~/.nickel/doc/.md` for input `.ncl`, or to + /// `~/.nickel/doc/out.md` if the input is read from stdin. + #[arg(short, long)] + pub output: Option, + /// Write documentation to stdout. Takes precedence over `output` + #[arg(long)] + pub stdout: bool, + /// The output format for the generated documentation. + #[arg(long, value_enum, default_value_t)] + pub format: crate::doc::DocFormat, +} + +impl DocCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let mut program = eval::prepare(&global)?; + self.export_doc(&mut program, &global) + .report_with_program(program) + } + + fn export_doc( + self, + program: &mut Program, + global: &GlobalOptions, + ) -> Result<(), Error> { + let doc = program.extract_doc()?; + let mut out: Box = if self.stdout { + Box::new(std::io::stdout()) + } else { + Box::new( + self.output + .as_ref() + .map(|output| { + fs::File::create(output.clone()).map_err(|e| { + Error::IOError(IOError(format!( + "when opening or creating output file `{}`: {}", + output.to_string_lossy(), + e + ))) + }) }) - }) - .unwrap_or_else(|| { - let docpath = Path::new(".nickel/doc/"); - fs::create_dir_all(docpath).map_err(|e| { - Error::IOError(IOError(format!( - "when creating output path `{}`: {}", - docpath.to_string_lossy(), - e - ))) - })?; - let mut output_file = docpath.to_path_buf(); + .unwrap_or_else(|| { + let docpath = Path::new(".nickel/doc/"); + fs::create_dir_all(docpath).map_err(|e| { + Error::IOError(IOError(format!( + "when creating output path `{}`: {}", + docpath.to_string_lossy(), + e + ))) + })?; + let mut output_file = docpath.to_path_buf(); - let mut has_file_name = false; + let mut has_file_name = false; - if let Some(path) = file { - if let Some(file_stem) = path.file_stem() { - output_file.push(file_stem); - has_file_name = true; + if let Some(path) = &global.file { + if let Some(file_stem) = path.file_stem() { + output_file.push(file_stem); + has_file_name = true; + } } - } - if !has_file_name { - output_file.push("out"); - } + if !has_file_name { + output_file.push("out"); + } - output_file.set_extension(format.extension()); - fs::File::create(output_file.clone().into_os_string()).map_err(|e| { - Error::IOError(IOError(format!( - "when opening or creating output file `{}`: {}", - output_file.to_string_lossy(), - e - ))) - }) - })?, - ) - }; - match format { - DocFormat::Json => doc.write_json(&mut out), - DocFormat::Markdown => doc.write_markdown(&mut out), + output_file.set_extension(self.format.extension()); + fs::File::create(output_file.clone().into_os_string()).map_err(|e| { + Error::IOError(IOError(format!( + "when opening or creating output file `{}`: {}", + output_file.to_string_lossy(), + e + ))) + }) + })?, + ) + }; + match self.format { + DocFormat::Json => doc.write_json(&mut out), + DocFormat::Markdown => doc.write_markdown(&mut out), + } } } diff --git a/cli/src/error.rs b/cli/src/error.rs new file mode 100644 index 0000000000..0c8fff1be9 --- /dev/null +++ b/cli/src/error.rs @@ -0,0 +1,74 @@ +use nickel_lang_core::{eval::cache::lazy::CBNCache, program::Program}; + +pub enum Error { + Program { + program: Program, + error: nickel_lang_core::error::Error, + }, + Io { + error: std::io::Error, + }, + #[cfg(feature = "repl")] + Repl { + error: nickel_lang_core::repl::InitError, + }, + #[cfg(feature = "format")] + Format { + error: crate::format::FormatError, + }, +} + +pub type CliResult = Result; + +impl From for Error { + fn from(error: std::io::Error) -> Self { + Error::Io { error } + } +} + +#[cfg(feature = "format")] +impl From for Error { + fn from(error: crate::format::FormatError) -> Self { + Error::Format { error } + } +} + +#[cfg(feature = "repl")] +impl From for Error { + fn from(error: nickel_lang_core::repl::InitError) -> Self { + Error::Repl { error } + } +} + +pub trait ResultErrorExt { + fn report_with_program(self, program: Program) -> CliResult; +} + +impl ResultErrorExt for Result { + fn report_with_program(self, program: Program) -> CliResult { + self.map_err(|error| Error::Program { program, error }) + } +} + +impl Error { + pub fn report(self) { + match self { + Error::Program { mut program, error } => program.report(error), + Error::Io { error } => { + eprintln!("{error}") + } + #[cfg(feature = "repl")] + Error::Repl { error } => { + use nickel_lang_core::repl::InitError; + match error { + InitError::Stdlib => eprintln!("Failed to load the Nickel standard library"), + InitError::ReadlineError(msg) => { + eprintln!("Readline intialization failed: {msg}") + } + } + } + #[cfg(feature = "format")] + Error::Format { error } => eprintln!("{error}"), + } + } +} diff --git a/cli/src/eval.rs b/cli/src/eval.rs new file mode 100644 index 0000000000..ff5e03ec49 --- /dev/null +++ b/cli/src/eval.rs @@ -0,0 +1,40 @@ +use nickel_lang_core::{eval::cache::lazy::CBNCache, program::Program}; + +use crate::{ + cli::GlobalOptions, + error::{CliResult, ResultErrorExt}, +}; + +#[derive(clap::Parser, Debug)] +pub struct EvalCommand {} + +impl EvalCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let mut program = prepare(&global)?; + program + .eval_full() + .map(|t| println!("{t}")) + .report_with_program(program) + } + + pub fn prepare(&self, global: &GlobalOptions) -> CliResult> { + prepare(global) + } +} + +pub fn prepare(global: &GlobalOptions) -> CliResult> { + let mut program = global + .file + .clone() + .map(|f| Program::new_from_file(f, std::io::stderr())) + .unwrap_or_else(|| Program::new_from_stdin(std::io::stderr()))?; + + #[cfg(debug_assertions)] + if global.nostdlib { + program.set_skip_stdlib(); + } + + program.set_color(global.color.into()); + + Ok(program) +} diff --git a/cli/src/export.rs b/cli/src/export.rs new file mode 100644 index 0000000000..95e304640e --- /dev/null +++ b/cli/src/export.rs @@ -0,0 +1,62 @@ +use std::io::Write; +use std::{fs, path::PathBuf}; + +use nickel_lang_core::error::Error; +use nickel_lang_core::{ + error::IOError, + eval::cache::lazy::CBNCache, + program::Program, + serialize::{self, ExportFormat}, + term::RichTerm, +}; + +use crate::error::{CliResult, ResultErrorExt}; +use crate::{cli::GlobalOptions, eval::EvalCommand}; + +#[derive(clap::Parser, Debug)] +pub struct ExportCommand { + #[arg(long, value_enum, default_value_t)] + pub format: ExportFormat, + + /// Output file. Standard output by default + #[arg(short, long)] + pub output: Option, + + #[command(flatten)] + pub evaluation: EvalCommand, +} + +impl ExportCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let mut program = self.evaluation.prepare(&global)?; + + self.export(&mut program).report_with_program(program) + } + + fn export(self, program: &mut Program) -> Result<(), Error> { + let rt = program.eval_full_for_export().map(RichTerm::from)?; + + // We only add a trailing newline for JSON exports. Both YAML and TOML + // exporters already append a trailing newline by default. + let trailing_newline = self.format == ExportFormat::Json; + + serialize::validate(self.format, &rt)?; + + if let Some(file) = self.output { + let mut file = fs::File::create(file).map_err(IOError::from)?; + serialize::to_writer(&mut file, self.format, &rt)?; + + if trailing_newline { + writeln!(file).map_err(IOError::from)?; + } + } else { + serialize::to_writer(std::io::stdout(), self.format, &rt)?; + + if trailing_newline { + println!(); + } + } + + Ok(()) + } +} diff --git a/cli/src/format.rs b/cli/src/format.rs index 39dbd0a1a2..8c38d034b9 100644 --- a/cli/src/format.rs +++ b/cli/src/format.rs @@ -1,25 +1,19 @@ use std::{ fmt::Display, fs::File, - io::{self, stdin, stdout, BufReader, Read, Write}, + io::{stdin, stdout, BufReader, Read, Write}, path::{Path, PathBuf}, - process, }; use tempfile::NamedTempFile; use topiary::TopiaryQuery; +use crate::{cli::GlobalOptions, error::CliResult}; + #[derive(Debug)] pub enum FormatError { NotAFile { path: PathBuf }, TopiaryError(topiary::FormatterError), - IOError(io::Error), -} - -impl From for FormatError { - fn from(e: io::Error) -> Self { - Self::IOError(e) - } } impl From for FormatError { @@ -39,7 +33,6 @@ impl Display for FormatError { ) } FormatError::TopiaryError(e) => write!(f, "{e}"), - FormatError::IOError(e) => write!(f, "{e}"), } } } @@ -54,21 +47,14 @@ pub enum Output { } impl Output { - pub fn new(path: Option<&Path>) -> Result { - match path { - None => Ok(Self::Stdout), - Some(path) => { - let path = nickel_lang_core::cache::normalize_path(path)?; - Ok(Self::Disk { - staged: NamedTempFile::new_in(path.parent().ok_or_else(|| { - FormatError::NotAFile { - path: path.to_owned(), - } - })?)?, - output: path.to_owned(), - }) - } - } + pub fn from_path(path: &Path) -> CliResult { + let path = nickel_lang_core::cache::normalize_path(path)?; + Ok(Self::Disk { + staged: NamedTempFile::new_in(path.parent().ok_or_else(|| FormatError::NotAFile { + path: path.to_owned(), + })?)?, + output: path.to_owned(), + }) } pub fn persist(self) { @@ -94,40 +80,44 @@ impl Write for Output { } } -pub fn format(input: Option<&Path>, output: Option<&Path>, in_place: bool) { - if let Err(e) = do_format(input, output, in_place) { - eprintln!("{e}"); - process::exit(1); - } -} +#[derive(clap::Parser, Debug)] +pub struct FormatCommand { + /// Output file. Standard output by default. + #[arg(short, long)] + output: Option, -fn do_format( - input: Option<&Path>, - output: Option<&Path>, + /// Format in place, overwriting the input file. + #[arg(short, long, requires = "file")] in_place: bool, -) -> Result<(), FormatError> { - let mut output: Output = match (output, input, in_place) { - (None, None, _) | (None, Some(_), false) => Output::new(None)?, - (None, Some(file), true) | (Some(file), _, _) => Output::new(Some(file))?, - }; - let mut input: Box = match input { - None => Box::new(stdin()), - Some(f) => Box::new(BufReader::new(File::open(f)?)), - }; - let topiary_config = topiary::Configuration::parse_default_configuration()?; - let language = topiary::SupportedLanguage::Nickel.to_language(&topiary_config); - let grammar = tree_sitter_nickel::language().into(); - topiary::formatter( - &mut input, - &mut output, - &TopiaryQuery::nickel(), - language, - &grammar, - topiary::Operation::Format { - skip_idempotence: true, - tolerate_parsing_errors: true, - }, - )?; - output.persist(); - Ok(()) +} + +impl FormatCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let mut output: Output = match (&self.output, &global.file, self.in_place) { + (None, None, _) | (None, Some(_), false) => Output::Stdout, + (None, Some(file), true) | (Some(file), _, _) => Output::from_path(file)?, + }; + let mut input: Box = match global.file { + None => Box::new(stdin()), + Some(f) => Box::new(BufReader::new(File::open(f)?)), + }; + let topiary_config = + topiary::Configuration::parse_default_configuration().map_err(FormatError::from)?; + let language = topiary::SupportedLanguage::Nickel.to_language(&topiary_config); + let grammar = tree_sitter_nickel::language().into(); + topiary::formatter( + &mut input, + &mut output, + &TopiaryQuery::nickel(), + language, + &grammar, + topiary::Operation::Format { + skip_idempotence: true, + tolerate_parsing_errors: true, + }, + ) + .map_err(FormatError::from)?; + output.persist(); + Ok(()) + } } diff --git a/cli/src/lib.rs b/cli/src/lib.rs deleted file mode 100644 index 027dc7c31f..0000000000 --- a/cli/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[cfg(feature = "doc")] -pub mod doc; -#[cfg(feature = "format")] -pub mod format; -#[cfg(feature = "repl")] -pub mod repl; diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000000..7d37663a8d --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,45 @@ +//! Entry point of the program. + +#[cfg(feature = "doc")] +mod doc; +#[cfg(feature = "format")] +mod format; +#[cfg(feature = "repl")] +mod repl; + +mod cli; +mod completions; +mod error; +mod eval; +mod export; +mod pprint_ast; +mod query; +mod typecheck; + +use eval::EvalCommand; + +use crate::cli::{Command, Options}; + +fn main() { + let opts = ::parse(); + + let result = match opts.command.unwrap_or(Command::Eval(EvalCommand {})) { + Command::Eval(eval) => eval.run(opts.global), + Command::PprintAst(pprint_ast) => pprint_ast.run(opts.global), + Command::Export(export) => export.run(opts.global), + Command::Query(query) => query.run(opts.global), + Command::Typecheck(typecheck) => typecheck.run(opts.global), + Command::GenCompletions(completions) => completions.run(opts.global), + + #[cfg(feature = "repl")] + Command::Repl(repl) => repl.run(opts.global), + + #[cfg(feature = "doc")] + Command::Doc(doc) => doc.run(opts.global), + + #[cfg(feature = "format")] + Command::Format(format) => format.run(opts.global), + }; + + result.unwrap_or_else(|e| e.report()) +} diff --git a/cli/src/pprint_ast.rs b/cli/src/pprint_ast.rs new file mode 100644 index 0000000000..bb2a439ce7 --- /dev/null +++ b/cli/src/pprint_ast.rs @@ -0,0 +1,21 @@ +use crate::{ + cli::GlobalOptions, + error::{CliResult, ResultErrorExt}, + eval, +}; + +#[derive(clap::Parser, Debug)] +pub struct PprintAstCommand { + /// Performs code transformations before printing + #[arg(long)] + pub transform: bool, +} + +impl PprintAstCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let mut program = eval::prepare(&global)?; + program + .pprint_ast(&mut std::io::stdout(), self.transform) + .report_with_program(program) + } +} diff --git a/cli/src/query.rs b/cli/src/query.rs new file mode 100644 index 0000000000..696bc229b4 --- /dev/null +++ b/cli/src/query.rs @@ -0,0 +1,67 @@ +use nickel_lang_core::repl::query_print; + +use crate::{ + cli::GlobalOptions, + error::{CliResult, ResultErrorExt}, + eval::EvalCommand, +}; + +#[derive(clap::Parser, Debug)] +pub struct QueryCommand { + pub path: Option, + + #[arg(long)] + pub doc: bool, + + #[arg(long)] + pub contract: bool, + + #[arg(long = "type")] + pub typ: bool, + + #[arg(long)] + pub default: bool, + + #[arg(long)] + pub value: bool, + + #[command(flatten)] + pub evaluation: EvalCommand, +} + +impl QueryCommand { + fn attributes_specified(&self) -> bool { + self.doc || self.contract || self.typ || self.default || self.value + } + + fn query_attributes(&self) -> query_print::Attributes { + // Use a default selection of attributes if no option is specified + if !self.attributes_specified() { + query_print::Attributes::default() + } else { + query_print::Attributes { + doc: self.doc, + contract: self.contract, + typ: self.typ, + default: self.default, + value: self.value, + } + } + } + + pub fn run(mut self, global: GlobalOptions) -> CliResult<()> { + let mut program = self.evaluation.prepare(&global)?; + + program + .query(std::mem::take(&mut self.path)) + .map(|term| { + query_print::write_query_result( + &mut std::io::stdout(), + &term, + self.query_attributes(), + ) + .unwrap() + }) + .report_with_program(program) + } +} diff --git a/cli/src/repl.rs b/cli/src/repl.rs index 4c176609f3..255e4effd7 100644 --- a/cli/src/repl.rs +++ b/cli/src/repl.rs @@ -1,18 +1,26 @@ -use std::{path::PathBuf, process}; +use std::path::PathBuf; use directories::BaseDirs; -use nickel_lang_core::{program::ColorOpt, repl::rustyline_frontend}; +use nickel_lang_core::repl::rustyline_frontend; -pub fn repl(history_file: Option, color: ColorOpt) { - let histfile = if let Some(h) = history_file { - h - } else { - BaseDirs::new() - .expect("Cannot retrieve home directory path") - .home_dir() - .join(".nickel_history") - }; - if rustyline_frontend::repl(histfile, color).is_err() { - process::exit(1); +use crate::{cli::GlobalOptions, error::CliResult}; + +#[derive(clap::Parser, Debug)] +pub struct ReplCommand { + #[arg(long)] + pub history_file: Option, +} + +impl ReplCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let histfile = if let Some(h) = self.history_file { + h + } else { + BaseDirs::new() + .expect("Cannot retrieve home directory path") + .home_dir() + .join(".nickel_history") + }; + Ok(rustyline_frontend::repl(histfile, global.color.into())?) } } diff --git a/cli/src/typecheck.rs b/cli/src/typecheck.rs new file mode 100644 index 0000000000..68c0b0162b --- /dev/null +++ b/cli/src/typecheck.rs @@ -0,0 +1,15 @@ +use crate::{ + cli::GlobalOptions, + error::{CliResult, ResultErrorExt}, + eval, +}; + +#[derive(clap::Parser, Debug)] +pub struct TypecheckCommand {} + +impl TypecheckCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let mut program = eval::prepare(&global)?; + program.typecheck().report_with_program(program) + } +} diff --git a/core/src/program.rs b/core/src/program.rs index c9b7ffdc8a..037d6a02c2 100644 --- a/core/src/program.rs +++ b/core/src/program.rs @@ -367,7 +367,7 @@ impl Program { pub fn pprint_ast( &mut self, - out: &mut std::io::BufWriter>, + out: &mut impl std::io::Write, apply_transforms: bool, ) -> Result<(), Error> { use crate::pretty::*;