mirror of
https://github.com/nushell/nushell.git
synced 2025-05-20 01:40:47 +02:00
* Test commands for proper names and search terms Assert that the `Command.name()` is equal to `Signature.name` Check that search terms are not just substrings of the command name as they would not help finding the command. * Clean up search terms Remove redundant terms that just replicate the command name. Try to eliminate substring between search terms, clean up where necessary.
367 lines
14 KiB
Rust
367 lines
14 KiB
Rust
use std::fs::OpenOptions;
|
|
use std::path::Path;
|
|
|
|
use chrono::{DateTime, Datelike, Local};
|
|
use filetime::FileTime;
|
|
|
|
use nu_engine::CallExt;
|
|
use nu_protocol::ast::Call;
|
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
|
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape};
|
|
|
|
use crate::parse_date_from_string;
|
|
|
|
enum AddYear {
|
|
Full,
|
|
FirstDigits,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Touch;
|
|
|
|
impl Command for Touch {
|
|
fn name(&self) -> &str {
|
|
"touch"
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["create", "file"]
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("touch")
|
|
.required(
|
|
"filename",
|
|
SyntaxShape::Filepath,
|
|
"the path of the file you want to create",
|
|
)
|
|
.named(
|
|
"timestamp",
|
|
SyntaxShape::String,
|
|
"change the file or directory time to a timestamp. Format: [[CC]YY]MMDDhhmm[.ss]\n\n If neither YY or CC is given, the current year will be assumed. If YY is specified, but CC is not, CC will be derived as follows:\n \tIf YY is between [69, 99], CC is 19\n \tIf YY is between [00, 68], CC is 20\n Note: It is expected that in a future version of this standard the default century inferred from a 2-digit year will change",
|
|
Some('t'),
|
|
)
|
|
.named(
|
|
"date",
|
|
SyntaxShape::String,
|
|
"change the file or directory time to a date",
|
|
Some('d'),
|
|
)
|
|
.named(
|
|
"reference",
|
|
SyntaxShape::String,
|
|
"change the file or directory time to the time of the reference file/directory",
|
|
Some('r'),
|
|
)
|
|
.switch(
|
|
"modified",
|
|
"change the modification time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
|
|
Some('m'),
|
|
)
|
|
.switch(
|
|
"access",
|
|
"change the access time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
|
|
Some('a'),
|
|
)
|
|
.switch(
|
|
"no-create",
|
|
"do not create the file if it does not exist",
|
|
Some('c'),
|
|
)
|
|
.rest("rest", SyntaxShape::Filepath, "additional files to create")
|
|
.category(Category::FileSystem)
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"Creates one or more files."
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
_input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let mut change_mtime: bool = call.has_flag("modified");
|
|
let mut change_atime: bool = call.has_flag("access");
|
|
let use_stamp: bool = call.has_flag("timestamp");
|
|
let use_date: bool = call.has_flag("date");
|
|
let use_reference: bool = call.has_flag("reference");
|
|
let no_create: bool = call.has_flag("no-create");
|
|
let target: String = call.req(engine_state, stack, 0)?;
|
|
let rest: Vec<String> = call.rest(engine_state, stack, 1)?;
|
|
|
|
let mut date: Option<DateTime<Local>> = None;
|
|
let mut ref_date_atime: Option<DateTime<Local>> = None;
|
|
|
|
// Change both times if none is specified
|
|
if !change_mtime && !change_atime {
|
|
change_mtime = true;
|
|
change_atime = true;
|
|
}
|
|
|
|
if change_mtime || change_atime {
|
|
date = Some(Local::now());
|
|
}
|
|
|
|
if use_stamp || use_date {
|
|
let (val, span) = if use_stamp {
|
|
let stamp: Option<Spanned<String>> =
|
|
call.get_flag(engine_state, stack, "timestamp")?;
|
|
let (stamp, span) = match stamp {
|
|
Some(stamp) => (stamp.item, stamp.span),
|
|
None => {
|
|
return Err(ShellError::MissingParameter(
|
|
"timestamp".to_string(),
|
|
call.head,
|
|
));
|
|
}
|
|
};
|
|
|
|
// Checks for the seconds stamp and removes the '.' delimiter if any
|
|
let (val, has_sec): (String, bool) = match stamp.split_once('.') {
|
|
Some((dtime, sec)) => match sec.parse::<u8>() {
|
|
Ok(sec) if sec < 60 => (format!("{}{}", dtime, sec), true),
|
|
_ => {
|
|
return Err(ShellError::UnsupportedInput(
|
|
"input has an invalid timestamp".to_string(),
|
|
span,
|
|
))
|
|
}
|
|
},
|
|
None => (stamp.to_string(), false),
|
|
};
|
|
|
|
let size = val.len();
|
|
|
|
// Each stamp is a 2 digit number and the whole stamp must not be less than 4 or greater than 7 pairs
|
|
if (size % 2 != 0 || !(8..=14).contains(&size)) || val.parse::<u64>().is_err() {
|
|
return Err(ShellError::UnsupportedInput(
|
|
"input has an invalid timestamp".to_string(),
|
|
span,
|
|
));
|
|
}
|
|
|
|
let add_year: Option<AddYear> = if has_sec {
|
|
match size {
|
|
10 => Some(AddYear::Full),
|
|
12 => Some(AddYear::FirstDigits),
|
|
14 => None,
|
|
_ => {
|
|
return Err(ShellError::UnsupportedInput(
|
|
"input has an invalid timestamp".to_string(),
|
|
span,
|
|
))
|
|
}
|
|
}
|
|
} else {
|
|
match size {
|
|
8 => Some(AddYear::Full),
|
|
10 => Some(AddYear::FirstDigits),
|
|
12 => None,
|
|
_ => {
|
|
return Err(ShellError::UnsupportedInput(
|
|
"input has an invalid timestamp".to_string(),
|
|
span,
|
|
))
|
|
}
|
|
}
|
|
};
|
|
|
|
if let Some(add_year) = add_year {
|
|
let year = Local::now().year();
|
|
match add_year {
|
|
AddYear::Full => (format!("{}{}", year, val), span),
|
|
AddYear::FirstDigits => {
|
|
// Compliance with the Unix version of touch
|
|
let yy = val[0..2]
|
|
.parse::<u8>()
|
|
.expect("should be a valid 2 digit number");
|
|
let mut year = 20;
|
|
if (69..=99).contains(&yy) {
|
|
year = 19;
|
|
}
|
|
(format!("{}{}", year, val), span)
|
|
}
|
|
}
|
|
} else {
|
|
(val, span)
|
|
}
|
|
} else {
|
|
let date_string: Option<Spanned<String>> =
|
|
call.get_flag(engine_state, stack, "date")?;
|
|
match date_string {
|
|
Some(date_string) => (date_string.item, date_string.span),
|
|
None => {
|
|
return Err(ShellError::MissingParameter("date".to_string(), call.head));
|
|
}
|
|
}
|
|
};
|
|
|
|
date = if let Ok(parsed_date) = parse_date_from_string(&val, span) {
|
|
Some(parsed_date.into())
|
|
} else {
|
|
let flag = if use_stamp { "timestamp" } else { "date" };
|
|
return Err(ShellError::UnsupportedInput(
|
|
format!("input has an invalid {}", flag),
|
|
span,
|
|
));
|
|
};
|
|
}
|
|
|
|
if use_reference {
|
|
let reference: Option<Spanned<String>> =
|
|
call.get_flag(engine_state, stack, "reference")?;
|
|
match reference {
|
|
Some(reference) => {
|
|
let reference_path = Path::new(&reference.item);
|
|
if !reference_path.exists() {
|
|
return Err(ShellError::UnsupportedInput(
|
|
"path provided is invalid".to_string(),
|
|
reference.span,
|
|
));
|
|
}
|
|
|
|
date = Some(
|
|
reference_path
|
|
.metadata()
|
|
.expect("should be a valid path") // Should never fail as the path exists
|
|
.modified()
|
|
.expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS)
|
|
.into(),
|
|
);
|
|
|
|
ref_date_atime = Some(
|
|
reference_path
|
|
.metadata()
|
|
.expect("should be a valid path") // Should never fail as the path exists
|
|
.accessed()
|
|
.expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS)
|
|
.into(),
|
|
);
|
|
}
|
|
None => {
|
|
return Err(ShellError::MissingParameter(
|
|
"reference".to_string(),
|
|
call.head,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (index, item) in vec![target].into_iter().chain(rest).enumerate() {
|
|
if no_create {
|
|
let path = Path::new(&item);
|
|
if !path.exists() {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if let Err(err) = OpenOptions::new().write(true).create(true).open(&item) {
|
|
return Err(ShellError::CreateNotPossible(
|
|
format!("Failed to create file: {}", err),
|
|
call.positional_nth(index)
|
|
.expect("already checked positional")
|
|
.span,
|
|
));
|
|
};
|
|
|
|
if change_mtime {
|
|
// Should not panic as we return an error above if we can't parse the date
|
|
if let Err(err) = filetime::set_file_mtime(
|
|
&item,
|
|
FileTime::from_system_time(date.expect("should be a valid date").into()),
|
|
) {
|
|
return Err(ShellError::ChangeModifiedTimeNotPossible(
|
|
format!("Failed to change the modified time: {}", err),
|
|
call.positional_nth(index)
|
|
.expect("already checked positional")
|
|
.span,
|
|
));
|
|
};
|
|
}
|
|
|
|
if change_atime {
|
|
// Reference file/directory may have different access and modified times
|
|
if use_reference {
|
|
// Should not panic as we return an error above if we can't parse the date
|
|
if let Err(err) = filetime::set_file_atime(
|
|
&item,
|
|
FileTime::from_system_time(
|
|
ref_date_atime.expect("should be a valid date").into(),
|
|
),
|
|
) {
|
|
return Err(ShellError::ChangeAccessTimeNotPossible(
|
|
format!("Failed to change the access time: {}", err),
|
|
call.positional_nth(index)
|
|
.expect("already checked positional")
|
|
.span,
|
|
));
|
|
};
|
|
} else {
|
|
// Should not panic as we return an error above if we can't parse the date
|
|
if let Err(err) = filetime::set_file_atime(
|
|
&item,
|
|
FileTime::from_system_time(date.expect("should be a valid date").into()),
|
|
) {
|
|
return Err(ShellError::ChangeAccessTimeNotPossible(
|
|
format!("Failed to change the access time: {}", err),
|
|
call.positional_nth(index)
|
|
.expect("already checked positional")
|
|
.span,
|
|
));
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(PipelineData::new(call.head))
|
|
}
|
|
|
|
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,
|
|
},
|
|
Example {
|
|
description: r#"Changes the last modified time of "fixture.json" to today's date"#,
|
|
example: "touch -m fixture.json",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Creates files d and e and set its last modified time to a timestamp",
|
|
example: "touch -m -t 201908241230.30 d e",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Changes the last modified time of files a, b and c to a date",
|
|
example: r#"touch -m -d "yesterday" a b c"#,
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: r#"Changes the last modified time of file d and e to "fixture.json"'s last modified time"#,
|
|
example: r#"touch -m -r fixture.json d e"#,
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: r#"Changes the last accessed time of "fixture.json" to a date"#,
|
|
example: r#"touch -a -d "August 24, 2019; 12:30:30" fixture.json"#,
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Changes both last modified and accessed time of a, b and c to a timestamp only if they exist",
|
|
example: r#"touch -c -t 201908241230.30 a b c"#,
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
}
|