diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 6a88738a9f..6ae3ca2866 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -225,6 +225,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { Input, InputList, InputListen, + IsTerminal, Kill, Sleep, TermSize, diff --git a/crates/nu-command/src/platform/is_terminal.rs b/crates/nu-command/src/platform/is_terminal.rs new file mode 100644 index 0000000000..e4be227bad --- /dev/null +++ b/crates/nu-command/src/platform/is_terminal.rs @@ -0,0 +1,78 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + span, Category, Example, PipelineData, ShellError, Signature, Type, Value, +}; +use std::io::IsTerminal as _; + +#[derive(Clone)] +pub struct IsTerminal; + +impl Command for IsTerminal { + fn name(&self) -> &str { + "is-terminal" + } + + fn signature(&self) -> Signature { + Signature::build("is-terminal") + .input_output_type(Type::Nothing, Type::Bool) + .switch("stdin", "Check if stdin is a terminal", Some('i')) + .switch("stdout", "Check if stdout is a terminal", Some('o')) + .switch("stderr", "Check if stderr is a terminal", Some('e')) + .category(Category::Platform) + } + + fn usage(&self) -> &str { + "Check if stdin, stdout, or stderr is a terminal" + } + + fn examples(&self) -> Vec { + vec![Example { + description: r#"Return "terminal attached" if standard input is attached to a terminal, and "no terminal" if not."#, + example: r#"if (is-terminal --stdin) { "terminal attached" } else { "no terminal" }"#, + result: Some(Value::test_string("terminal attached")), + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["input", "output", "stdin", "stdout", "stderr", "tty"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let stdin = call.has_flag("stdin"); + let stdout = call.has_flag("stdout"); + let stderr = call.has_flag("stderr"); + + let is_terminal = match (stdin, stdout, stderr) { + (true, false, false) => std::io::stdin().is_terminal(), + (false, true, false) => std::io::stdout().is_terminal(), + (false, false, true) => std::io::stderr().is_terminal(), + (false, false, false) => { + return Err(ShellError::MissingParameter { + param_name: "one of --stdin, --stdout, --stderr".into(), + span: call.head, + }); + } + _ => { + let spans: Vec<_> = call.arguments.iter().map(|arg| arg.span()).collect(); + let span = span(&spans); + + return Err(ShellError::IncompatibleParametersSingle { + msg: "Only one stream may be checked".into(), + span, + }); + } + }; + + Ok(PipelineData::Value( + Value::bool(is_terminal, call.head), + None, + )) + } +} diff --git a/crates/nu-command/src/platform/mod.rs b/crates/nu-command/src/platform/mod.rs index d6a6b3a5ad..63d3026bfa 100644 --- a/crates/nu-command/src/platform/mod.rs +++ b/crates/nu-command/src/platform/mod.rs @@ -3,6 +3,7 @@ mod clear; mod dir_info; mod du; mod input; +mod is_terminal; mod kill; mod sleep; mod term_size; @@ -15,6 +16,7 @@ pub use du::Du; pub use input::Input; pub use input::InputList; pub use input::InputListen; +pub use is_terminal::IsTerminal; pub use kill::Kill; pub use sleep::Sleep; pub use term_size::TermSize; diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 5f698ad087..9373801ef9 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -100,6 +100,7 @@ mod split_row; mod str_; mod table; mod take; +mod terminal; mod to_text; mod touch; mod transpose; diff --git a/crates/nu-command/tests/commands/terminal.rs b/crates/nu-command/tests/commands/terminal.rs new file mode 100644 index 0000000000..04275e6c54 --- /dev/null +++ b/crates/nu-command/tests/commands/terminal.rs @@ -0,0 +1,24 @@ +use nu_test_support::{nu, pipeline}; + +// Inside nu! stdout is piped so it won't be a terminal +#[test] +fn is_terminal_stdout_piped() { + let actual = nu!(pipeline( + r#" + is-terminal --stdout + "# + )); + + assert_eq!(actual.out, "false"); +} + +#[test] +fn is_terminal_two_streams() { + let actual = nu!(pipeline( + r#" + is-terminal --stdin --stderr + "# + )); + + assert!(actual.err.contains("Only one stream may be checked")); +}