Begin directory contrib docs and split commands (#3650)

* Begin directory contrib docs and split commands

* Fix unused import warning
This commit is contained in:
JT
2021-06-19 12:06:44 +12:00
committed by GitHub
parent 4140834e4c
commit a74d05061d
261 changed files with 794 additions and 808 deletions

View File

@ -0,0 +1,72 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_engine::shell::CdArgs;
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape};
pub struct Cd;
impl WholeStreamCommand for Cd {
fn name(&self) -> &str {
"cd"
}
fn signature(&self) -> Signature {
Signature::build("cd").optional(
"directory",
SyntaxShape::FilePath,
"the directory to change to",
)
}
fn usage(&self) -> &str {
"Change to a new path."
}
fn run_with_actions(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
let name = args.call_info.name_tag.clone();
let shell_manager = args.shell_manager();
let args = CdArgs { path: args.opt(0)? };
shell_manager.cd(args, name)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Change to a new directory called 'dirname'",
example: "cd dirname",
result: None,
},
Example {
description: "Change to your home directory",
example: "cd",
result: None,
},
Example {
description: "Change to your home directory (alternate version)",
example: "cd ~",
result: None,
},
Example {
description: "Change to the previous directory",
example: "cd -",
result: None,
},
]
}
}
#[cfg(test)]
mod tests {
use super::Cd;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Cd {})
}
}

View File

@ -0,0 +1,67 @@
use crate::prelude::*;
use nu_engine::{shell::CopyArgs, WholeStreamCommand};
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape};
pub struct Cpy;
impl WholeStreamCommand for Cpy {
fn name(&self) -> &str {
"cp"
}
fn signature(&self) -> Signature {
Signature::build("cp")
.required("src", SyntaxShape::GlobPattern, "the place to copy from")
.required("dst", SyntaxShape::FilePath, "the place to copy to")
.switch(
"recursive",
"copy recursively through subdirectories",
Some('r'),
)
}
fn usage(&self) -> &str {
"Copy files."
}
fn run_with_actions(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
let shell_manager = args.shell_manager();
let name = args.call_info.name_tag.clone();
let args = CopyArgs {
src: args.req(0)?,
dst: args.req(1)?,
recursive: args.has_flag("recursive"),
};
shell_manager.cp(args, name)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Copy myfile to dir_b",
example: "cp myfile dir_b",
result: None,
},
Example {
description: "Recursively copy dir_a to dir_b",
example: "cp -r dir_a dir_b",
result: None,
},
]
}
}
#[cfg(test)]
mod tests {
use super::Cpy;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Cpy {})
}
}

View File

@ -0,0 +1,90 @@
use crate::prelude::*;
use nu_engine::{shell::LsArgs, WholeStreamCommand};
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape};
pub struct Ls;
impl WholeStreamCommand for Ls {
fn name(&self) -> &str {
"ls"
}
fn signature(&self) -> Signature {
Signature::build("ls")
.optional(
"path",
SyntaxShape::GlobPattern,
"a path to get the directory contents from",
)
.switch("all", "Show hidden files", Some('a'))
.switch(
"long",
"List all available columns for each entry",
Some('l'),
)
.switch(
"short-names",
"Only print the file names and not the path",
Some('s'),
)
.switch(
"du",
"Display the apparent directory size in place of the directory metadata size",
Some('d'),
)
}
fn usage(&self) -> &str {
"View the contents of the current or given path."
}
fn run_with_actions(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
let name = args.call_info.name_tag.clone();
let ctrl_c = args.ctrl_c();
let shell_manager = args.shell_manager();
let args = LsArgs {
path: args.opt(0)?,
all: args.has_flag("all"),
long: args.has_flag("long"),
short_names: args.has_flag("short-names"),
du: args.has_flag("du"),
};
shell_manager.ls(args, name, ctrl_c)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "List all files in the current directory",
example: "ls",
result: None,
},
Example {
description: "List all files in a subdirectory",
example: "ls subdir",
result: None,
},
Example {
description: "List all rust files",
example: "ls *.rs",
result: None,
},
]
}
}
#[cfg(test)]
mod tests {
use super::Ls;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Ls {})
}
}

View File

@ -0,0 +1,61 @@
use crate::prelude::*;
use nu_engine::{shell::MkdirArgs, WholeStreamCommand};
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape};
pub struct Mkdir;
impl WholeStreamCommand for Mkdir {
fn name(&self) -> &str {
"mkdir"
}
fn signature(&self) -> Signature {
Signature::build("mkdir")
.rest(
SyntaxShape::FilePath,
"the name(s) of the path(s) to create",
)
.switch("show-created-paths", "show the path(s) created.", Some('s'))
}
fn usage(&self) -> &str {
"Make directories, creates intermediary directories as required."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
mkdir(args)
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Make a directory named foo",
example: "mkdir foo",
result: None,
}]
}
}
fn mkdir(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let shell_manager = args.shell_manager();
let args = MkdirArgs {
rest: args.rest(0)?,
show_created_paths: args.has_flag("show-created-paths"),
};
shell_manager.mkdir(args, name)
}
#[cfg(test)]
mod tests {
use super::Mkdir;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Mkdir {})
}
}

View File

@ -0,0 +1,19 @@
mod cd;
mod cp;
mod ls;
mod mkdir;
mod mv;
pub(crate) mod open;
mod rm;
mod save;
mod touch;
pub use cd::Cd;
pub use cp::Cpy;
pub use ls::Ls;
pub use mkdir::Mkdir;
pub use mv::Mv;
pub use open::Open;
pub use rm::Remove;
pub use save::Save;
pub use touch::Touch;

View File

@ -0,0 +1,79 @@
use crate::prelude::*;
use nu_engine::{shell::MvArgs, WholeStreamCommand};
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape};
pub struct Mv;
impl WholeStreamCommand for Mv {
fn name(&self) -> &str {
"mv"
}
fn signature(&self) -> Signature {
Signature::build("mv")
.required(
"source",
SyntaxShape::GlobPattern,
"the location to move files/directories from",
)
.required(
"destination",
SyntaxShape::FilePath,
"the location to move files/directories to",
)
}
fn usage(&self) -> &str {
"Move files or directories."
}
fn run_with_actions(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
mv(args)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Rename a file",
example: "mv before.txt after.txt",
result: None,
},
Example {
description: "Move a file into a directory",
example: "mv test.txt my/subdirectory",
result: None,
},
Example {
description: "Move many files into a directory",
example: "mv *.txt my/subdirectory",
result: None,
},
]
}
}
fn mv(args: CommandArgs) -> Result<ActionStream, ShellError> {
let name = args.call_info.name_tag.clone();
let shell_manager = args.shell_manager();
let args = MvArgs {
src: args.req(0)?,
dst: args.req(1)?,
};
shell_manager.mv(args, name)
}
#[cfg(test)]
mod tests {
use super::Mv;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Mv {})
}
}

View File

@ -0,0 +1,264 @@
use crate::commands::viewers::BAT_LANGUAGES;
use crate::prelude::*;
use encoding_rs::{Encoding, UTF_8};
use log::debug;
use nu_engine::StringOrBinary;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
use nu_source::{AnchorLocation, Span, Tagged};
use std::path::{Path, PathBuf};
pub struct Open;
impl WholeStreamCommand for Open {
fn name(&self) -> &str {
"open"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.required(
"path",
SyntaxShape::FilePath,
"the file path to load values from",
)
.switch(
"raw",
"load content as a string instead of a table",
Some('r'),
)
.named(
"encoding",
SyntaxShape::String,
"encoding to use to open file",
Some('e'),
)
}
fn usage(&self) -> &str {
"Load a file into a cell, convert to table if possible (avoid by appending '--raw')."
}
fn extra_usage(&self) -> &str {
r#"Multiple encodings are supported for reading text files by using
the '--encoding <encoding>' parameter. Here is an example of a few:
big5, euc-jp, euc-kr, gbk, iso-8859-1, utf-16, cp1252, latin5
For a more complete list of encodings please refer to the encoding_rs
documentation link at https://docs.rs/encoding_rs/0.8.28/encoding_rs/#statics"#
}
fn run_with_actions(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
open(args)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Opens \"users.csv\" and creates a table from the data",
example: "open users.csv",
result: None,
},
Example {
description: "Opens file with iso-8859-1 encoding",
example: "open file.csv --encoding iso-8859-1 | from csv",
result: None,
},
Example {
description: "Lists the contents of a directory (identical to `ls ../projectB`)",
example: "open ../projectB",
result: None,
},
]
}
}
pub fn get_encoding(opt: Option<Tagged<String>>) -> Result<&'static Encoding, ShellError> {
match opt {
None => Ok(UTF_8),
Some(label) => match Encoding::for_label((&label.item).as_bytes()) {
None => Err(ShellError::labeled_error(
format!(
r#"{} is not a valid encoding, refer to https://docs.rs/encoding_rs/0.8.23/encoding_rs/#statics for a valid list of encodings"#,
label.item
),
"invalid encoding",
label.span(),
)),
Some(encoding) => Ok(encoding),
},
}
}
fn open(args: CommandArgs) -> Result<ActionStream, ShellError> {
let scope = args.scope().clone();
let shell_manager = args.shell_manager();
let cwd = PathBuf::from(shell_manager.path());
let name = args.call_info.name_tag.clone();
let ctrl_c = args.ctrl_c();
let path: Tagged<PathBuf> = args.req(0)?;
let raw = args.has_flag("raw");
let encoding: Option<Tagged<String>> = args.get_flag("encoding")?;
if path.is_dir() {
let args = nu_engine::shell::LsArgs {
path: Some(path),
all: false,
long: false,
short_names: false,
du: false,
};
return shell_manager.ls(args, name, ctrl_c);
}
// TODO: Remove once Streams are supported everywhere!
// As a short term workaround for getting AutoConvert and Bat functionality (Those don't currently support Streams)
// Check if the extension has a "from *" command OR "bat" supports syntax highlighting
// AND the user doesn't want the raw output
// In these cases, we will collect the Stream
let ext = if raw {
None
} else {
path.extension()
.map(|name| name.to_string_lossy().to_string())
};
if let Some(ext) = ext {
// Check if we have a conversion command
if let Some(_command) = scope.get_command(&format!("from {}", ext)) {
let (_, tagged_contents) = crate::commands::open::fetch(
&cwd,
&PathBuf::from(&path.item),
path.tag.span,
encoding,
)?;
return Ok(ActionStream::one(ReturnSuccess::action(
CommandAction::AutoConvert(tagged_contents, ext),
)));
}
// Check if bat does syntax highlighting
if BAT_LANGUAGES.contains(&ext.as_ref()) {
let (_, tagged_contents) = crate::commands::open::fetch(
&cwd,
&PathBuf::from(&path.item),
path.tag.span,
encoding,
)?;
return Ok(ActionStream::one(ReturnSuccess::value(tagged_contents)));
}
}
// Normal Streaming operation
let with_encoding = if encoding.is_none() {
None
} else {
Some(get_encoding(encoding)?)
};
let sob_stream = shell_manager.open(&path.item, path.tag.span, with_encoding)?;
let final_stream = sob_stream.map(move |x| {
// The tag that will used when returning a Value
let file_tag = Tag {
span: path.tag.span,
anchor: Some(AnchorLocation::File(path.to_string_lossy().to_string())),
};
match x {
Ok(StringOrBinary::String(s)) => {
ReturnSuccess::value(UntaggedValue::string(s).into_value(file_tag))
}
Ok(StringOrBinary::Binary(b)) => ReturnSuccess::value(
UntaggedValue::binary(b.into_iter().collect()).into_value(file_tag),
),
Err(se) => Err(se),
}
});
Ok(ActionStream::new(final_stream))
}
// Note that we do not output a Stream in "fetch" since it is only used by "enter" command
// Which we expect to use a concrete Value a not a Stream
pub fn fetch(
cwd: &Path,
location: &Path,
span: Span,
encoding_choice: Option<Tagged<String>>,
) -> Result<(Option<String>, Value), ShellError> {
// TODO: I don't understand the point of this? Maybe for better error reporting
let mut cwd = PathBuf::from(cwd);
cwd.push(location);
let nice_location = dunce::canonicalize(&cwd).map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => ShellError::labeled_error(
format!("Cannot find file {:?}", cwd),
"cannot find file",
span,
),
std::io::ErrorKind::PermissionDenied => {
ShellError::labeled_error("Permission denied", "permission denied", span)
}
_ => ShellError::labeled_error(
format!("Cannot open file {:?} because {:?}", &cwd, e),
"Cannot open",
span,
),
})?;
// The extension may be used in AutoConvert later on
let ext = location
.extension()
.map(|name| name.to_string_lossy().to_string());
// The tag that will used when returning a Value
let file_tag = Tag {
span,
anchor: Some(AnchorLocation::File(
nice_location.to_string_lossy().to_string(),
)),
};
let res = std::fs::read(location)
.map_err(|_| ShellError::labeled_error("Can't open filename given", "can't open", span))?;
// If no encoding is provided we try to guess the encoding to read the file with
let encoding = if encoding_choice.is_none() {
UTF_8
} else {
get_encoding(encoding_choice.clone())?
};
// If the user specified an encoding, then do not do BOM sniffing
let decoded_res = if encoding_choice.is_some() {
let (cow_res, _replacements) = encoding.decode_with_bom_removal(&res);
cow_res
} else {
// Otherwise, use the default UTF-8 encoder with BOM sniffing
let (cow_res, actual_encoding, replacements) = encoding.decode(&res);
// If we had to use replacement characters then fallback to binary
if replacements {
return Ok((ext, UntaggedValue::binary(res).into_value(file_tag)));
}
debug!("Decoded using {:?}", actual_encoding);
cow_res
};
let v = UntaggedValue::string(decoded_res.to_string()).into_value(file_tag);
Ok((ext, v))
}
#[cfg(test)]
mod tests {
use super::Open;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Open {})
}
}

View File

@ -0,0 +1,99 @@
use crate::prelude::*;
use nu_engine::shell::RemoveArgs;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape};
pub struct Remove;
impl WholeStreamCommand for Remove {
fn name(&self) -> &str {
"rm"
}
fn signature(&self) -> Signature {
Signature::build("rm")
.switch(
"trash",
"use the platform's recycle bin instead of permanently deleting",
Some('t'),
)
.switch(
"permanent",
"don't use recycle bin, delete permanently",
Some('p'),
)
.switch("recursive", "delete subdirectories recursively", Some('r'))
.switch("force", "suppress error when no file", Some('f'))
.rest(SyntaxShape::GlobPattern, "the file path(s) to remove")
}
fn usage(&self) -> &str {
"Remove file(s)."
}
fn run_with_actions(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
rm(args)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Delete or move a file to the system trash (depending on 'rm_always_trash' config option)",
example: "rm file.txt",
result: None,
},
Example {
description: "Move a file to the system trash",
example: "rm --trash file.txt",
result: None,
},
Example {
description: "Delete a file permanently",
example: "rm --permanent file.txt",
result: None,
},
Example {
description: "Delete a file, and suppress errors if no file is found",
example: "rm --force file.txt",
result: None,
}
]
}
}
fn rm(args: CommandArgs) -> Result<ActionStream, ShellError> {
let name = args.call_info.name_tag.clone();
let shell_manager = args.shell_manager();
let args = RemoveArgs {
rest: args.rest(0)?,
recursive: args.has_flag("recursive"),
trash: args.has_flag("trash"),
permanent: args.has_flag("permanent"),
force: args.has_flag("force"),
};
if args.trash && args.permanent {
return Ok(ActionStream::one(Err(ShellError::labeled_error(
"only one of --permanent and --trash can be used",
"conflicting flags",
name,
))));
}
shell_manager.rm(args, name)
}
#[cfg(test)]
mod tests {
use super::Remove;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Remove {})
}
}

View File

@ -0,0 +1,268 @@
use crate::prelude::*;
use nu_engine::{UnevaluatedCallInfo, WholeStreamCommand};
use nu_errors::ShellError;
use nu_protocol::{
hir::ExternalRedirection, Primitive, Signature, SyntaxShape, UntaggedValue, Value,
};
use nu_source::Tagged;
use std::path::{Path, PathBuf};
pub struct Save;
macro_rules! process_unknown {
($scope:tt, $input:ident, $name_tag:ident) => {{
if $input.len() > 0 {
match $input[0] {
Value {
value: UntaggedValue::Primitive(Primitive::Binary(_)),
..
} => process_binary!($scope, $input, $name_tag),
_ => process_string!($scope, $input, $name_tag),
}
} else {
process_string!($scope, $input, $name_tag)
}
}};
}
macro_rules! process_string {
($scope:tt, $input:ident, $name_tag:ident) => {{
let mut result_string = String::new();
for res in $input {
match res {
Value {
value: UntaggedValue::Primitive(Primitive::String(s)),
..
} => {
result_string.push_str(&s);
}
_ => {
break $scope Err(ShellError::labeled_error(
"Save requires string data",
"consider converting data to string (see `help commands`)",
$name_tag,
));
}
}
}
Ok(result_string.into_bytes())
}};
}
macro_rules! process_binary {
($scope:tt, $input:ident, $name_tag:ident) => {{
let mut result_binary: Vec<u8> = Vec::new();
for res in $input {
match res {
Value {
value: UntaggedValue::Primitive(Primitive::Binary(b)),
..
} => {
for u in b.into_iter() {
result_binary.push(u);
}
}
_ => {
break $scope Err(ShellError::labeled_error(
"Save could not successfully save",
"unexpected data during binary save",
$name_tag,
));
}
}
}
Ok(result_binary)
}};
}
macro_rules! process_string_return_success {
($scope:tt, $result_vec:ident, $name_tag:ident) => {{
let mut result_string = String::new();
for res in $result_vec {
match res {
Value {
value: UntaggedValue::Primitive(Primitive::String(s)),
..
} => {
result_string.push_str(&s);
}
_ => {
break $scope Err(ShellError::labeled_error(
"Save could not successfully save",
"unexpected data during text save",
$name_tag,
));
}
}
}
Ok(result_string.into_bytes())
}};
}
macro_rules! process_binary_return_success {
($scope:tt, $result_vec:ident, $name_tag:ident) => {{
let mut result_binary: Vec<u8> = Vec::new();
for res in $result_vec {
match res {
Value {
value: UntaggedValue::Primitive(Primitive::Binary(b)),
..
} => {
for u in b.into_iter() {
result_binary.push(u);
}
}
_ => {
break $scope Err(ShellError::labeled_error(
"Save could not successfully save",
"unexpected data during binary save",
$name_tag,
));
}
}
}
Ok(result_binary)
}};
}
impl WholeStreamCommand for Save {
fn name(&self) -> &str {
"save"
}
fn signature(&self) -> Signature {
Signature::build("save")
.optional(
"path",
SyntaxShape::FilePath,
"the path to save contents to",
)
.switch(
"raw",
"treat values as-is rather than auto-converting based on file extension",
Some('r'),
)
}
fn usage(&self) -> &str {
"Save the contents of the pipeline to a file."
}
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
save(args)
}
}
fn save(args: CommandArgs) -> Result<OutputStream, ShellError> {
let shell_manager = args.shell_manager();
let mut full_path = PathBuf::from(shell_manager.path());
let name_tag = args.call_info.name_tag.clone();
let name = args.call_info.name_tag.clone();
let context = args.context.clone();
let scope = args.scope().clone();
let head = args.call_info.args.head.clone();
let path: Option<Tagged<PathBuf>> = args.opt(0)?;
let save_raw = args.has_flag("raw");
let input: Vec<Value> = args.input.collect();
if path.is_none() {
let mut should_return_file_path_error = true;
// If there is no filename, check the metadata for the anchor filename
if !input.is_empty() {
let anchor = input[0].tag.anchor();
if let Some(AnchorLocation::File(file)) = anchor {
should_return_file_path_error = false;
full_path.push(Path::new(&file));
}
}
if should_return_file_path_error {
return Err(ShellError::labeled_error(
"Save requires a filepath",
"needs path",
name_tag,
));
}
} else if let Some(file) = path {
full_path.push(file.item());
}
// TODO use label_break_value once it is stable:
// https://github.com/rust-lang/rust/issues/48594
#[allow(clippy::never_loop)]
let content: Result<Vec<u8>, ShellError> = 'scope: loop {
break if !save_raw {
if let Some(extension) = full_path.extension() {
let command_name = format!("to {}", extension.to_string_lossy());
if let Some(converter) = scope.get_command(&command_name) {
let new_args = CommandArgs {
context,
call_info: UnevaluatedCallInfo {
args: nu_protocol::hir::Call {
head,
positional: None,
named: None,
span: Span::unknown(),
external_redirection: ExternalRedirection::Stdout,
},
name_tag: name_tag.clone(),
},
input: InputStream::from_stream(input.into_iter()),
};
let mut result = converter.run(new_args)?;
let result_vec: Vec<Value> = result.drain_vec();
if converter.is_binary() {
process_binary_return_success!('scope, result_vec, name_tag)
} else {
process_string_return_success!('scope, result_vec, name_tag)
}
} else {
process_unknown!('scope, input, name_tag)
}
} else {
process_unknown!('scope, input, name_tag)
}
} else {
Ok(string_from(&input).into_bytes())
};
};
shell_manager.save(&full_path, &content?, name.span)
}
fn string_from(input: &[Value]) -> String {
let mut save_data = String::new();
if !input.is_empty() {
let mut first = true;
for i in input.iter() {
if !first {
save_data.push('\n');
} else {
first = false;
}
if let Ok(data) = &i.as_string() {
save_data.push_str(data);
}
}
}
save_data
}
#[cfg(test)]
mod tests {
use super::Save;
use super::ShellError;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Save {})
}
}

View File

@ -0,0 +1,78 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape};
use nu_source::Tagged;
use std::fs::OpenOptions;
use std::path::PathBuf;
pub struct Touch;
impl WholeStreamCommand for Touch {
fn name(&self) -> &str {
"touch"
}
fn signature(&self) -> Signature {
Signature::build("touch")
.required(
"filename",
SyntaxShape::FilePath,
"the path of the file you want to create",
)
.rest(SyntaxShape::FilePath, "additional files to create")
}
fn usage(&self) -> &str {
"Creates one or more files."
}
fn run_with_actions(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
touch(args)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Creates \"fixture.json\"",
example: "touch fixture.json",
result: None,
},
Example {
description: "Creates files a, b and c",
example: "touch a b c",
result: None,
},
]
}
}
fn touch(args: CommandArgs) -> Result<ActionStream, ShellError> {
let target: Tagged<PathBuf> = args.req(0)?;
let rest: Vec<Tagged<PathBuf>> = args.rest(1)?;
for item in vec![target].into_iter().chain(rest.into_iter()) {
match OpenOptions::new().write(true).create(true).open(&item) {
Ok(_) => continue,
Err(err) => {
return Err(ShellError::labeled_error(
"File Error",
err.to_string(),
&item.tag,
))
}
}
}
Ok(ActionStream::empty())
}
#[cfg(test)]
mod tests {
use super::ShellError;
use super::Touch;
#[test]
fn examples_work_as_expected() -> Result<(), ShellError> {
use crate::examples::test as test_examples;
test_examples(Touch {})
}
}