From 51990d0fdfcb54a5b934c2eee36d9d2f5a902b41 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sun, 11 Aug 2024 21:05:59 -0400 Subject: [PATCH 1/7] Initial Grammar --- Cargo.lock | 18 +++++++ crates/deno_task_shell/Cargo.toml | 1 + crates/deno_task_shell/src/grammar.pest | 69 +++++++++++++++++++++++++ scripts/script_3.sh | 1 + 4 files changed, 89 insertions(+) create mode 100644 crates/deno_task_shell/src/grammar.pest create mode 100644 scripts/script_3.sh diff --git a/Cargo.lock b/Cargo.lock index 3ea15eb..08076d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,7 @@ dependencies = [ "os_pipe", "parking_lot", "path-dedot", + "pest", "pretty_assertions", "serde", "serde_json", @@ -335,6 +336,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "pest" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -562,6 +574,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/crates/deno_task_shell/Cargo.toml b/crates/deno_task_shell/Cargo.toml index 2fc7dde..f9bb7a6 100644 --- a/crates/deno_task_shell/Cargo.toml +++ b/crates/deno_task_shell/Cargo.toml @@ -25,6 +25,7 @@ os_pipe = { version = "1.1.4", optional = true } serde = { version = "1", features = ["derive"], optional = true } monch = "0.5.0" thiserror = "1.0.58" +pest = "2.6.0" [dev-dependencies] parking_lot = "0.12.1" diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest new file mode 100644 index 0000000..45b9da5 --- /dev/null +++ b/crates/deno_task_shell/src/grammar.pest @@ -0,0 +1,69 @@ +WHITESPACE = _{ " " | "\t" | ("\\" ~ WHITESPACE+ ~ NEWLINE) } + + +file = _{ SOI ~ sequential_list ~ EOI } + +sequential_list = { + (sequential_list_item ~ (";" | "\n"))* ~ sequential_list_item +} + +sequential_list_item = { async_prefix? ~ sequence } + +async_prefix = { "&" } + +sequence = {env_var | pipeline} + +env_var = { text ~ "=" ~ word } + +text = { char+ } + +char = _{ ASCII_ALPHANUMERIC | "_" | "." } + +env_var_char = _{ ASCII_ALPHANUMERIC | "_" } + +variable = { env_var_char+ } + +word = { quoted_word | unquoted_word } + +unquoted_word = _{ word_part+ } + +// Word Part section +word_part = { !("$" ~ ("$" | "#" | "*")) ~ ("$?" + | ("\\$" ~ word_part) + | ("$" ~ !(variable | "(")) + | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ word_part) + | ("$(" ~ sequential_list ~ ")") + | ("$" ~ variable) + | quoted_word + | char) } + +quoted_word = { (single_quoted_word | double_quoted_word)+ } + +single_quoted_word = _{ "'" ~ word_part* ~ "'" } + +double_quoted_word = _{ "\"" ~ word_part* ~ "\"" } + +pipeline = { negation_prefix? ~ pipesequence } + +negation_prefix = { "!" } + +pipesequence = { command ~ (pipesequence_operator ~ pipesequence)? } + +pipesequence_operator = { "|" | "|&" } + +command = { subshell | simple_command } + +subshell = { "(" ~ sequential_list ~ ")" } + +simple_command = { env_var* ~ command_args* } + +command_args = { + (shell_arg)+ ~ + (list_op | pipesequence_operator | ")")? +} + +list_op = _{ sequential_list_op | async_prefix } + +sequential_list_op = _{ ";" } + +shell_arg = { word } \ No newline at end of file diff --git a/scripts/script_3.sh b/scripts/script_3.sh new file mode 100644 index 0000000..d250ea9 --- /dev/null +++ b/scripts/script_3.sh @@ -0,0 +1 @@ +echo $PATH; \ No newline at end of file From 0d50e122d8d90e5c31e3abee49ec098ba4434c92 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Wed, 14 Aug 2024 15:50:17 -0400 Subject: [PATCH 2/7] Updated grammar --- crates/deno_task_shell/src/grammar.pest | 120 +++++++++++++++--------- scripts/script_4.sh | 1 + 2 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 scripts/script_4.sh diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index 45b9da5..ff148f7 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -1,69 +1,101 @@ -WHITESPACE = _{ " " | "\t" | ("\\" ~ WHITESPACE+ ~ NEWLINE) } - - -file = _{ SOI ~ sequential_list ~ EOI } - -sequential_list = { - (sequential_list_item ~ (";" | "\n"))* ~ sequential_list_item +// Define valid characters for identifiers +VALID_CHAR = _{ ASCII_ALPHANUMERIC | "_" | "." | "/" | "!" | "," } + +// Valid characters for environment variable names +ENV_VAR_CHAR = _{ ASCII_ALPHANUMERIC | "_" } + +// Environment variable names consist of one or more valid characters +VARIABLE = { ENV_VAR_CHAR+ } + +// Define allowed whitespace characters including spaces, tabs, and escaped newlines +whitespace = _{ " " | "\t" | ("\\" ~ whitespace+ ~ NEWLINE) } + +// A word can be either quoted or unquoted +WORD = { QUOTED_WORD | UNQUOTED_WORD } + +// Unquoted words are made up of one or more word parts, separated by optional whitespace +UNQUOTED_WORD = _{ WORD_PART ~ (whitespace* ~ WORD_PART)* } + +// The structure of word parts +WORD_PART = _{ + // TODO: The below are not supported yet + !("$" ~ ("$" | "#" | "*")) + ~ ( + "$?" // Special variable for the last command's exit status + | ("\\$" ~ WORD_PART) // Escaped dollar sign followed by a word part + | ("$" ~ !(VARIABLE | "(")) // A dollar sign not followed by a variable or parenthesis + | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ WORD_PART) // Escaped special characters + | ("$(" ~ SEQUENTIAL_LIST ~ ")") // Command substitution + | ("$" ~ VARIABLE) // Variable substitution + | QUOTED_WORD // Quoted word + | TEXT // Regular text + ) } -sequential_list_item = { async_prefix? ~ sequence } +QUOTED_WORD = { (SINGLE_QUOTED_WORD | DOUBLE_QUOTED_WORD)+ } -async_prefix = { "&" } +SINGLE_QUOTED_WORD = { "'" ~ WORD_PART ~ (whitespace* ~ WORD_PART)* ~ "'" } -sequence = {env_var | pipeline} +DOUBLE_QUOTED_WORD = { "\"" ~ WORD_PART ~ (whitespace* ~ WORD_PART)* ~ "\"" } -env_var = { text ~ "=" ~ word } +// Environment variable assignment in the format: = +ENV_VAR_ASSIGNMENT = { VARIABLE ~ "=" ~ WORD ~ whitespace* } -text = { char+ } +// Identifiers consist of one or more valid characters +TEXT = { VALID_CHAR+ } -char = _{ ASCII_ALPHANUMERIC | "_" | "." } +// A pipeline can optionally start with a negation prefix and consists of a sequence of commands +PIPELINE = { NEGATION_PREFIX? ~ whitespace* ~ PIPE_SEQUENCE } -env_var_char = _{ ASCII_ALPHANUMERIC | "_" } +NEGATION_PREFIX = { "!" } -variable = { env_var_char+ } +// A pipe sequence consists of a command followed by an optional pipeline operator and another command +PIPE_SEQUENCE = { COMMAND ~ whitespace* ~ (PIPE_OPERATOR ~ whitespace* ~ PIPE_SEQUENCE)? } -word = { quoted_word | unquoted_word } +// Pipeline operators for piping output between commands +PIPE_OPERATOR = { STDOUT | STDERR } -unquoted_word = _{ word_part+ } +STDOUT = { "|" } -// Word Part section -word_part = { !("$" ~ ("$" | "#" | "*")) ~ ("$?" - | ("\\$" ~ word_part) - | ("$" ~ !(variable | "(")) - | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ word_part) - | ("$(" ~ sequential_list ~ ")") - | ("$" ~ variable) - | quoted_word - | char) } +STDERR = { "|&" } -quoted_word = { (single_quoted_word | double_quoted_word)+ } +// A command can be a subshell or a simple command +COMMAND = { SUBSHELL | SIMPLE_COMMAND } -single_quoted_word = _{ "'" ~ word_part* ~ "'" } +// Subshell commands enclosed in parentheses +SUBSHELL = { "(" ~ whitespace* ~ SEQUENTIAL_LIST ~ whitespace* ~ ")" } -double_quoted_word = _{ "\"" ~ word_part* ~ "\"" } +// A simple command consists of optional environment variable assignments followed by command arguments +SIMPLE_COMMAND = { ENV_VAR_ASSIGNMENT* ~ COMMAND_ARGUMENTS* } -pipeline = { negation_prefix? ~ pipesequence } +// Command arguments that exclude certain operators and are separated by whitespace +COMMAND_ARGUMENTS = { + !(LIST_OPERATOR | PIPE_OPERATOR | ")") ~ + (SHELL_ARG ~ (whitespace+ ~ SHELL_ARG)* ~ whitespace*) +} -negation_prefix = { "!" } +// Shell arguments are simply words +SHELL_ARG = _{ WORD } -pipesequence = { command ~ (pipesequence_operator ~ pipesequence)? } +// List operators: Sequential list operators or async prefixes +LIST_OPERATOR = _{ SEQUENTIAL_LIST_OPERATOR | ASYNC_PREFIX } -pipesequence_operator = { "|" | "|&" } +// Sequential list operator represented by the semicolon +SEQUENTIAL_LIST_OPERATOR = _{ ";" } -command = { subshell | simple_command } +// An optional async prefix represented by the '&' character +ASYNC_PREFIX = { "&" } -subshell = { "(" ~ sequential_list ~ ")" } +// A sequence is an environment variable assignment or a pipeline, followed by optional whitespace +SEQUENCE = { (ENV_VAR_ASSIGNMENT | PIPELINE) ~ whitespace* } -simple_command = { env_var* ~ command_args* } +// A sequential item can optionally start with an async prefix followed by a sequence +SEQUENTIAL_ITEM = { ASYNC_PREFIX? ~ SEQUENCE } -command_args = { - (shell_arg)+ ~ - (list_op | pipesequence_operator | ")")? +// A list of sequential items separated by either semicolons or newlines +SEQUENTIAL_LIST = { + (SEQUENTIAL_ITEM ~ whitespace* ~ (";" | "\n"))* ~ whitespace* ~ SEQUENTIAL_ITEM } -list_op = _{ sequential_list_op | async_prefix } - -sequential_list_op = _{ ";" } - -shell_arg = { word } \ No newline at end of file +// Entry point: The entire file should match a list of sequential items +FILE = _{ SOI ~ SEQUENTIAL_LIST ~ EOI } diff --git a/scripts/script_4.sh b/scripts/script_4.sh new file mode 100644 index 0000000..c6f954a --- /dev/null +++ b/scripts/script_4.sh @@ -0,0 +1 @@ +echo "Double word" 'Single word' \ No newline at end of file From b8b4bc143cdc6e48ec469de396f2509ae5bcc51d Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Fri, 16 Aug 2024 20:26:49 -0400 Subject: [PATCH 3/7] Updated Grammar --- crates/deno_task_shell/src/grammar.pest | 66 ++++++++++++++++--------- scripts/script_2.sh | 4 +- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index ff148f7..119c9d4 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -1,56 +1,76 @@ +// Define allowed whitespace characters including spaces, tabs, and escaped newlines +WHITESPACE = _{ " " | "\t" | ("\\" ~ WHITESPACE* ~ NEWLINE) } + // Define valid characters for identifiers VALID_CHAR = _{ ASCII_ALPHANUMERIC | "_" | "." | "/" | "!" | "," } +VALID_CHAR_WITH_WHITESPACE = _{ VALID_CHAR | WHITESPACE } + // Valid characters for environment variable names ENV_VAR_CHAR = _{ ASCII_ALPHANUMERIC | "_" } // Environment variable names consist of one or more valid characters -VARIABLE = { ENV_VAR_CHAR+ } - -// Define allowed whitespace characters including spaces, tabs, and escaped newlines -whitespace = _{ " " | "\t" | ("\\" ~ whitespace+ ~ NEWLINE) } +VARIABLE = ${ ENV_VAR_CHAR+ } // A word can be either quoted or unquoted -WORD = { QUOTED_WORD | UNQUOTED_WORD } +WORD = _{ QUOTED_WORD | UNQUOTED_WORD } // Unquoted words are made up of one or more word parts, separated by optional whitespace -UNQUOTED_WORD = _{ WORD_PART ~ (whitespace* ~ WORD_PART)* } +UNQUOTED_WORD = { UNQUOTED_WORD_PART+ } // The structure of word parts -WORD_PART = _{ +UNQUOTED_WORD_PART = ${ // TODO: The below are not supported yet !("$" ~ ("$" | "#" | "*")) ~ ( "$?" // Special variable for the last command's exit status - | ("\\$" ~ WORD_PART) // Escaped dollar sign followed by a word part - | ("$" ~ !(VARIABLE | "(")) // A dollar sign not followed by a variable or parenthesis - | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ WORD_PART) // Escaped special characters + | ("\\$" ~ UNQUOTED_WORD_PART) // Escaped dollar sign followed by a word part | ("$(" ~ SEQUENTIAL_LIST ~ ")") // Command substitution + | ("$" ~ !(VARIABLE | "(")) // A dollar sign not followed by a variable or parenthesis + | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ UNQUOTED_WORD_PART) // Escaped special characters | ("$" ~ VARIABLE) // Variable substitution | QUOTED_WORD // Quoted word | TEXT // Regular text ) } +QUOTED_WORD_PART = ${ + // TODO: The below are not supported yet + !("$" ~ ("$" | "#" | "*")) + ~ ( + "$?" // Special variable for the last command's exit status + | ("\\$" ~ QUOTED_WORD_PART) // Escaped dollar sign followed by a word part + | ("$" ~ !(VARIABLE | "(")) // A dollar sign not followed by a variable or parenthesis + | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ QUOTED_WORD_PART) // Escaped special characters + | ("$(" ~ SEQUENTIAL_LIST ~ ")") // Command substitution + | ("$" ~ VARIABLE) // Variable substitution + | QUOTED_WORD // Quoted word + | TEXT_WITH_WHITESPACE // Regular text or whitespace + ) +} + QUOTED_WORD = { (SINGLE_QUOTED_WORD | DOUBLE_QUOTED_WORD)+ } -SINGLE_QUOTED_WORD = { "'" ~ WORD_PART ~ (whitespace* ~ WORD_PART)* ~ "'" } +SINGLE_QUOTED_WORD = { "'" ~ QUOTED_WORD_PART ~ ("\""* ~ QUOTED_WORD_PART)* ~ "'" } -DOUBLE_QUOTED_WORD = { "\"" ~ WORD_PART ~ (whitespace* ~ WORD_PART)* ~ "\"" } +DOUBLE_QUOTED_WORD = { "\"" ~ QUOTED_WORD_PART ~ ("\""* ~ QUOTED_WORD_PART)* ~ "\"" } // Environment variable assignment in the format: = -ENV_VAR_ASSIGNMENT = { VARIABLE ~ "=" ~ WORD ~ whitespace* } +ENV_VAR_ASSIGNMENT = { VARIABLE ~ "=" ~ WORD } // Identifiers consist of one or more valid characters -TEXT = { VALID_CHAR+ } +TEXT = ${ VALID_CHAR+ } + +// Text can include whitespace used inside quoted words +TEXT_WITH_WHITESPACE = { VALID_CHAR_WITH_WHITESPACE+ } // A pipeline can optionally start with a negation prefix and consists of a sequence of commands -PIPELINE = { NEGATION_PREFIX? ~ whitespace* ~ PIPE_SEQUENCE } +PIPELINE = { NEGATION_PREFIX? ~ PIPE_SEQUENCE } NEGATION_PREFIX = { "!" } // A pipe sequence consists of a command followed by an optional pipeline operator and another command -PIPE_SEQUENCE = { COMMAND ~ whitespace* ~ (PIPE_OPERATOR ~ whitespace* ~ PIPE_SEQUENCE)? } +PIPE_SEQUENCE = { COMMAND ~ (PIPE_OPERATOR ~ PIPE_SEQUENCE)? } // Pipeline operators for piping output between commands PIPE_OPERATOR = { STDOUT | STDERR } @@ -63,7 +83,7 @@ STDERR = { "|&" } COMMAND = { SUBSHELL | SIMPLE_COMMAND } // Subshell commands enclosed in parentheses -SUBSHELL = { "(" ~ whitespace* ~ SEQUENTIAL_LIST ~ whitespace* ~ ")" } +SUBSHELL = { "(" ~ SEQUENTIAL_LIST ~ ")" } // A simple command consists of optional environment variable assignments followed by command arguments SIMPLE_COMMAND = { ENV_VAR_ASSIGNMENT* ~ COMMAND_ARGUMENTS* } @@ -71,31 +91,29 @@ SIMPLE_COMMAND = { ENV_VAR_ASSIGNMENT* ~ COMMAND_ARGUMENTS* } // Command arguments that exclude certain operators and are separated by whitespace COMMAND_ARGUMENTS = { !(LIST_OPERATOR | PIPE_OPERATOR | ")") ~ - (SHELL_ARG ~ (whitespace+ ~ SHELL_ARG)* ~ whitespace*) + SHELL_ARG+ } // Shell arguments are simply words SHELL_ARG = _{ WORD } // List operators: Sequential list operators or async prefixes -LIST_OPERATOR = _{ SEQUENTIAL_LIST_OPERATOR | ASYNC_PREFIX } +LIST_OPERATOR = { SEQUENTIAL_LIST_OPERATOR | ASYNC_PREFIX } // Sequential list operator represented by the semicolon -SEQUENTIAL_LIST_OPERATOR = _{ ";" } +SEQUENTIAL_LIST_OPERATOR = { ";" } // An optional async prefix represented by the '&' character ASYNC_PREFIX = { "&" } // A sequence is an environment variable assignment or a pipeline, followed by optional whitespace -SEQUENCE = { (ENV_VAR_ASSIGNMENT | PIPELINE) ~ whitespace* } +SEQUENCE = { (ENV_VAR_ASSIGNMENT | PIPELINE) } // A sequential item can optionally start with an async prefix followed by a sequence SEQUENTIAL_ITEM = { ASYNC_PREFIX? ~ SEQUENCE } // A list of sequential items separated by either semicolons or newlines -SEQUENTIAL_LIST = { - (SEQUENTIAL_ITEM ~ whitespace* ~ (";" | "\n"))* ~ whitespace* ~ SEQUENTIAL_ITEM -} +SEQUENTIAL_LIST = { (SEQUENTIAL_ITEM ~ (";" | "\n"))* ~ SEQUENTIAL_ITEM } // Entry point: The entire file should match a list of sequential items FILE = _{ SOI ~ SEQUENTIAL_LIST ~ EOI } diff --git a/scripts/script_2.sh b/scripts/script_2.sh index 589b475..04a18ac 100644 --- a/scripts/script_2.sh +++ b/scripts/script_2.sh @@ -1,2 +1,2 @@ -ls; -cat Cargo.toml +MY_VAR=42; +echo $MY_VAR From a3cb8d52fcc5a8155033170ca0754cfde41f427e Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Wed, 21 Aug 2024 00:07:51 -0400 Subject: [PATCH 4/7] Switched to pest for parsing & Changed indentation --- Cargo.lock | 113 +- crates/deno_task_shell/.rustfmt.toml | 2 +- crates/deno_task_shell/Cargo.toml | 2 +- crates/deno_task_shell/src/grammar.pest | 251 +- crates/deno_task_shell/src/parser.rs | 2137 ++++------------- crates/deno_task_shell/src/shell/command.rs | 642 ++--- .../src/shell/commands/args.rs | 140 +- .../deno_task_shell/src/shell/commands/cat.rs | 205 +- .../deno_task_shell/src/shell/commands/cd.rs | 222 +- .../src/shell/commands/cp_mv.rs | 797 +++--- .../src/shell/commands/echo.rs | 14 +- .../src/shell/commands/executable.rs | 104 +- .../src/shell/commands/exit.rs | 168 +- .../src/shell/commands/export.rs | 36 +- .../src/shell/commands/head.rs | 522 ++-- .../src/shell/commands/mkdir.rs | 375 +-- .../deno_task_shell/src/shell/commands/mod.rs | 202 +- .../deno_task_shell/src/shell/commands/pwd.rs | 139 +- .../deno_task_shell/src/shell/commands/rm.rs | 577 ++--- .../src/shell/commands/sleep.rs | 198 +- .../src/shell/commands/unset.rs | 126 +- .../src/shell/commands/xargs.rs | 514 ++-- crates/deno_task_shell/src/shell/execute.rs | 1590 ++++++------ crates/deno_task_shell/src/shell/fs_util.rs | 85 +- crates/deno_task_shell/src/shell/test.rs | 2020 ++++++++-------- .../deno_task_shell/src/shell/test_builder.rs | 421 ++-- crates/deno_task_shell/src/shell/types.rs | 654 ++--- crates/shell/src/main.rs | 7 +- scripts/script_1.sh | 2 +- scripts/script_2.sh | 3 +- scripts/script_4.sh | 3 +- 31 files changed, 5727 insertions(+), 6544 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08076d8..7b00cfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.7.0" @@ -68,6 +77,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deno_task_shell" version = "0.17.0" @@ -75,11 +103,11 @@ dependencies = [ "anyhow", "futures", "glob", - "monch", "os_pipe", "parking_lot", "path-dedot", "pest", + "pest_derive", "pretty_assertions", "serde", "serde_json", @@ -95,6 +123,16 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "errno" version = "0.3.9" @@ -200,6 +238,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gimli" version = "0.29.0" @@ -273,12 +321,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "monch" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" - [[package]] name = "object" version = "0.36.2" @@ -347,6 +389,40 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pest_derive" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -459,6 +535,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shell" version = "0.1.0" @@ -574,6 +661,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "ucd-trie" version = "0.1.6" @@ -586,6 +679,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/crates/deno_task_shell/.rustfmt.toml b/crates/deno_task_shell/.rustfmt.toml index 9bb8d9d..074ce96 100644 --- a/crates/deno_task_shell/.rustfmt.toml +++ b/crates/deno_task_shell/.rustfmt.toml @@ -1,3 +1,3 @@ max_width = 80 -tab_spaces = 2 +tab_spaces = 4 edition = "2021" diff --git a/crates/deno_task_shell/Cargo.toml b/crates/deno_task_shell/Cargo.toml index f9bb7a6..5830b1f 100644 --- a/crates/deno_task_shell/Cargo.toml +++ b/crates/deno_task_shell/Cargo.toml @@ -23,9 +23,9 @@ tokio = { version = "1", features = ["fs", "io-std", "io-util", "macros", "proce tokio-util = { version = "0.7.10", optional = true } os_pipe = { version = "1.1.4", optional = true } serde = { version = "1", features = ["derive"], optional = true } -monch = "0.5.0" thiserror = "1.0.58" pest = "2.6.0" +pest_derive = "2.6.0" [dev-dependencies] parking_lot = "0.12.1" diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index 119c9d4..a169c71 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -1,119 +1,184 @@ -// Define allowed whitespace characters including spaces, tabs, and escaped newlines +// grammar.pest + +// Whitespace and comments WHITESPACE = _{ " " | "\t" | ("\\" ~ WHITESPACE* ~ NEWLINE) } +COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* } + +// Basic tokens +WORD_WITH_EQUAL = ${ "="? ~ (!(WHITESPACE | OPERATOR | NEWLINE) ~ ANY)+ } +WORD = ${ (!(WHITESPACE | OPERATOR | NEWLINE) ~ ANY)+ } +QUOTED_WORD = ${ DOUBLE_QUOTED | SINGLE_QUOTED } +DOUBLE_QUOTED = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" } +SINGLE_QUOTED = @{ "'" ~ (!"'" ~ ANY)* ~ "'" } +NAME = ${ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } +ASSIGNMENT_WORD = ${ NAME ~ "=" ~ WORD } +IO_NUMBER = @{ ASCII_DIGIT+ } + +// Special tokens +AND_IF = { "&&" } +OR_IF = { "||" } +DSEMI = { ";;" } +LESS = { "<" } +GREAT = { ">" } +DLESS = { "<<" } +DGREAT = { ">>" } +LESSAND = { "<&" } +GREATAND = { ">&" } +LESSGREAT = { "<>" } +DLESSDASH = { "<<-" } +CLOBBER = { ">|" } +AMPERSAND = { "&" } + +// Operators +OPERATOR = _{ + AND_IF | OR_IF | DSEMI | DLESS | DGREAT | LESSAND | GREATAND | LESSGREAT | DLESSDASH | CLOBBER | + "(" | ")" | "{" | "}" | ";" | "&" | "|" | "<" | ">" | "!" +} -// Define valid characters for identifiers -VALID_CHAR = _{ ASCII_ALPHANUMERIC | "_" | "." | "/" | "!" | "," } - -VALID_CHAR_WITH_WHITESPACE = _{ VALID_CHAR | WHITESPACE } - -// Valid characters for environment variable names -ENV_VAR_CHAR = _{ ASCII_ALPHANUMERIC | "_" } - -// Environment variable names consist of one or more valid characters -VARIABLE = ${ ENV_VAR_CHAR+ } - -// A word can be either quoted or unquoted -WORD = _{ QUOTED_WORD | UNQUOTED_WORD } - -// Unquoted words are made up of one or more word parts, separated by optional whitespace -UNQUOTED_WORD = { UNQUOTED_WORD_PART+ } - -// The structure of word parts -UNQUOTED_WORD_PART = ${ - // TODO: The below are not supported yet - !("$" ~ ("$" | "#" | "*")) - ~ ( - "$?" // Special variable for the last command's exit status - | ("\\$" ~ UNQUOTED_WORD_PART) // Escaped dollar sign followed by a word part - | ("$(" ~ SEQUENTIAL_LIST ~ ")") // Command substitution - | ("$" ~ !(VARIABLE | "(")) // A dollar sign not followed by a variable or parenthesis - | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ UNQUOTED_WORD_PART) // Escaped special characters - | ("$" ~ VARIABLE) // Variable substitution - | QUOTED_WORD // Quoted word - | TEXT // Regular text - ) +// Reserved words +If = { "if" } +Then = { "then" } +Else = { "else" } +Elif = { "elif" } +Fi = { "fi" } +Do = { "do" } +Done = { "done" } +Case = { "case" } +Esac = { "esac" } +While = { "while" } +Until = { "until" } +For = { "for" } +Lbrace = { "{" } +Rbrace = { "}" } +Bang = { "!" } +In = { "in" } +Stdout = { "|" } +StdoutStderr = { "|&" } + +RESERVED_WORD = _{ + If | Then | Else | Elif | Fi | Do | Done | + Case | Esac | While | Until | For | + Lbrace | Rbrace | Bang | In | + Stdout | StdoutStderr } -QUOTED_WORD_PART = ${ - // TODO: The below are not supported yet - !("$" ~ ("$" | "#" | "*")) - ~ ( - "$?" // Special variable for the last command's exit status - | ("\\$" ~ QUOTED_WORD_PART) // Escaped dollar sign followed by a word part - | ("$" ~ !(VARIABLE | "(")) // A dollar sign not followed by a variable or parenthesis - | ("\\" ~ ("`" | "\"" | ")" | "(" | "'" | " ") ~ QUOTED_WORD_PART) // Escaped special characters - | ("$(" ~ SEQUENTIAL_LIST ~ ")") // Command substitution - | ("$" ~ VARIABLE) // Variable substitution - | QUOTED_WORD // Quoted word - | TEXT_WITH_WHITESPACE // Regular text or whitespace - ) +// Main grammar rules +complete_command = _{ (list ~ separator)* ~ list ~ separator? } +list = { and_or ~ (separator_op ~ and_or)* ~ separator_op? } +and_or = { pipeline ~ ((AND_IF | OR_IF) ~ linebreak ~ pipeline)* } +pipeline = { Bang? ~ pipe_sequence } +pipe_sequence = { command ~ (Stdout | StdoutStderr ~ linebreak ~ pipe_sequence)* } + +command = { + simple_command | + compound_command ~ redirect_list? | + function_definition } -QUOTED_WORD = { (SINGLE_QUOTED_WORD | DOUBLE_QUOTED_WORD)+ } +compound_command = { + brace_group | + subshell | + for_clause | + case_clause | + if_clause | + while_clause | + until_clause +} -SINGLE_QUOTED_WORD = { "'" ~ QUOTED_WORD_PART ~ ("\""* ~ QUOTED_WORD_PART)* ~ "'" } +subshell = { "(" ~ compound_list ~ ")" } +compound_list = { (newline_list? ~ term ~ separator?)+ } +term = { and_or ~ (separator ~ and_or)* } -DOUBLE_QUOTED_WORD = { "\"" ~ QUOTED_WORD_PART ~ ("\""* ~ QUOTED_WORD_PART)* ~ "\"" } +for_clause = { + For ~ name ~ linebreak ~ + (linebreak ~ In ~ wordlist? ~ sequential_sep)? ~ + linebreak ~ do_group +} -// Environment variable assignment in the format: = -ENV_VAR_ASSIGNMENT = { VARIABLE ~ "=" ~ WORD } +case_clause = { + Case ~ WORD ~ linebreak ~ + linebreak ~ In ~ linebreak ~ + (case_list | case_list_ns)? ~ + Esac +} -// Identifiers consist of one or more valid characters -TEXT = ${ VALID_CHAR+ } +case_list = { + case_item+ +} -// Text can include whitespace used inside quoted words -TEXT_WITH_WHITESPACE = { VALID_CHAR_WITH_WHITESPACE+ } +case_list_ns = { + case_item_ns+ +} -// A pipeline can optionally start with a negation prefix and consists of a sequence of commands -PIPELINE = { NEGATION_PREFIX? ~ PIPE_SEQUENCE } +case_item = { + "("? ~ pattern ~ ")" ~ (compound_list | linebreak) ~ DSEMI ~ linebreak +} -NEGATION_PREFIX = { "!" } +case_item_ns = { + "("? ~ pattern ~ ")" ~ compound_list? ~ linebreak +} -// A pipe sequence consists of a command followed by an optional pipeline operator and another command -PIPE_SEQUENCE = { COMMAND ~ (PIPE_OPERATOR ~ PIPE_SEQUENCE)? } +pattern = { + (Esac | WORD) ~ ("|" ~ WORD)* +} -// Pipeline operators for piping output between commands -PIPE_OPERATOR = { STDOUT | STDERR } +if_clause = { + If ~ compound_list ~ + Then ~ compound_list ~ + else_part? ~ + Fi +} -STDOUT = { "|" } +else_part = { + Elif ~ compound_list ~ Then ~ else_part | + Else ~ compound_list +} -STDERR = { "|&" } +while_clause = { While ~ compound_list ~ do_group } +until_clause = { Until ~ compound_list ~ do_group } -// A command can be a subshell or a simple command -COMMAND = { SUBSHELL | SIMPLE_COMMAND } +function_definition = { fname ~ "(" ~ ")" ~ linebreak ~ function_body } +function_body = { compound_command ~ redirect_list? } -// Subshell commands enclosed in parentheses -SUBSHELL = { "(" ~ SEQUENTIAL_LIST ~ ")" } +fname = @{ RESERVED_WORD | NAME | WORD_WITH_EQUAL | ASSIGNMENT_WORD } +name = @{ NAME } -// A simple command consists of optional environment variable assignments followed by command arguments -SIMPLE_COMMAND = { ENV_VAR_ASSIGNMENT* ~ COMMAND_ARGUMENTS* } +brace_group = { Lbrace ~ compound_list ~ Rbrace } +do_group = { Do ~ compound_list ~ Done } -// Command arguments that exclude certain operators and are separated by whitespace -COMMAND_ARGUMENTS = { - !(LIST_OPERATOR | PIPE_OPERATOR | ")") ~ - SHELL_ARG+ +simple_command = { + cmd_prefix ~ cmd_word ~ cmd_suffix? | + cmd_prefix | + cmd_name ~ cmd_suffix? } -// Shell arguments are simply words -SHELL_ARG = _{ WORD } - -// List operators: Sequential list operators or async prefixes -LIST_OPERATOR = { SEQUENTIAL_LIST_OPERATOR | ASYNC_PREFIX } - -// Sequential list operator represented by the semicolon -SEQUENTIAL_LIST_OPERATOR = { ";" } - -// An optional async prefix represented by the '&' character -ASYNC_PREFIX = { "&" } - -// A sequence is an environment variable assignment or a pipeline, followed by optional whitespace -SEQUENCE = { (ENV_VAR_ASSIGNMENT | PIPELINE) } +cmd_prefix = { (io_redirect | ASSIGNMENT_WORD)+ } +cmd_suffix = { (io_redirect | QUOTED_WORD | WORD)+ } +cmd_name = @{ (RESERVED_WORD | WORD) } +cmd_word = @{ WORD_WITH_EQUAL | ASSIGNMENT_WORD } + +redirect_list = { io_redirect+ } +io_redirect = { (IO_NUMBER | AMPERSAND)? ~ (io_file | io_here) } +io_file = { + LESS ~ filename | + GREAT ~ filename | + DGREAT ~ filename | + LESSAND ~ filename | + GREATAND ~ filename | + LESSGREAT ~ filename | + CLOBBER ~ filename +} +filename = @{ WORD } +io_here = { (DLESS | DLESSDASH) ~ here_end } +here_end = @{ ("\"" ~ WORD ~ "\"") | WORD } -// A sequential item can optionally start with an async prefix followed by a sequence -SEQUENTIAL_ITEM = { ASYNC_PREFIX? ~ SEQUENCE } +newline_list = { NEWLINE+ } +linebreak = _{ NEWLINE* } +separator_op = { "&" | ";" } +separator = _{ separator_op ~ linebreak | newline_list } +sequential_sep = { ";" ~ linebreak | newline_list } -// A list of sequential items separated by either semicolons or newlines -SEQUENTIAL_LIST = { (SEQUENTIAL_ITEM ~ (";" | "\n"))* ~ SEQUENTIAL_ITEM } +wordlist = { WORD+ } -// Entry point: The entire file should match a list of sequential items -FILE = _{ SOI ~ SEQUENTIAL_LIST ~ EOI } +// Entry point +FILE = { SOI ~ complete_command ~ EOI } \ No newline at end of file diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index a6ad40f..fa7dabb 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -1,8 +1,9 @@ // Copyright 2018-2024 the Deno authors. MIT license. -use anyhow::bail; -use anyhow::Result; -use monch::*; +use anyhow::{anyhow, Result}; +use pest::iterators::Pair; +use pest::Parser; +use pest_derive::Parser; // Shell grammar rules this is loosely based on: // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_10_02 @@ -11,206 +12,206 @@ use monch::*; #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SequentialList { - pub items: Vec, + pub items: Vec, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SequentialListItem { - pub is_async: bool, - pub sequence: Sequence, + pub is_async: bool, + pub sequence: Sequence, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum Sequence { - /// `MY_VAR=5` - ShellVar(EnvVar), - /// `cmd_name `, `cmd1 | cmd2` - Pipeline(Pipeline), - /// `cmd1 && cmd2 || cmd3` - BooleanList(Box), + /// `MY_VAR=5` + ShellVar(EnvVar), + /// `cmd_name `, `cmd1 | cmd2` + Pipeline(Pipeline), + /// `cmd1 && cmd2 || cmd3` + BooleanList(Box), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Pipeline { - /// `! pipeline` - pub negated: bool, - pub inner: PipelineInner, + /// `! pipeline` + pub negated: bool, + pub inner: PipelineInner, } impl From for Sequence { - fn from(p: Pipeline) -> Self { - Sequence::Pipeline(p) - } + fn from(p: Pipeline) -> Self { + Sequence::Pipeline(p) + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum PipelineInner { - /// Ex. `cmd_name ` - Command(Command), - /// `cmd1 | cmd2` - PipeSequence(Box), + /// Ex. `cmd_name ` + Command(Command), + /// `cmd1 | cmd2` + PipeSequence(Box), } impl From for PipelineInner { - fn from(p: PipeSequence) -> Self { - PipelineInner::PipeSequence(Box::new(p)) - } + fn from(p: PipeSequence) -> Self { + PipelineInner::PipeSequence(Box::new(p)) + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum BooleanListOperator { - // && - And, - // || - Or, + // && + And, + // || + Or, } impl BooleanListOperator { - pub fn as_str(&self) -> &'static str { - match self { - BooleanListOperator::And => "&&", - BooleanListOperator::Or => "||", + pub fn as_str(&self) -> &'static str { + match self { + BooleanListOperator::And => "&&", + BooleanListOperator::Or => "||", + } } - } - pub fn moves_next_for_exit_code(&self, exit_code: i32) -> bool { - *self == BooleanListOperator::Or && exit_code != 0 - || *self == BooleanListOperator::And && exit_code == 0 - } + pub fn moves_next_for_exit_code(&self, exit_code: i32) -> bool { + *self == BooleanListOperator::Or && exit_code != 0 + || *self == BooleanListOperator::And && exit_code == 0 + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct BooleanList { - pub current: Sequence, - pub op: BooleanListOperator, - pub next: Sequence, + pub current: Sequence, + pub op: BooleanListOperator, + pub next: Sequence, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PipeSequenceOperator { - // | - Stdout, - // |& - StdoutStderr, + // | + Stdout, + // |& + StdoutStderr, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct PipeSequence { - pub current: Command, - pub op: PipeSequenceOperator, - pub next: PipelineInner, + pub current: Command, + pub op: PipeSequenceOperator, + pub next: PipelineInner, } impl From for Sequence { - fn from(p: PipeSequence) -> Self { - Sequence::Pipeline(Pipeline { - negated: false, - inner: p.into(), - }) - } + fn from(p: PipeSequence) -> Self { + Sequence::Pipeline(Pipeline { + negated: false, + inner: p.into(), + }) + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Command { - pub inner: CommandInner, - pub redirect: Option, + pub inner: CommandInner, + pub redirect: Option, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum CommandInner { - /// `cmd_name ` - Simple(SimpleCommand), - /// `(list)` - Subshell(Box), + /// `cmd_name ` + Simple(SimpleCommand), + /// `(list)` + Subshell(Box), } impl From for Sequence { - fn from(c: Command) -> Self { - Pipeline { - negated: false, - inner: c.into(), + fn from(c: Command) -> Self { + Pipeline { + negated: false, + inner: c.into(), + } + .into() } - .into() - } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SimpleCommand { - pub env_vars: Vec, - pub args: Vec, + pub env_vars: Vec, + pub args: Vec, } impl From for Command { - fn from(c: SimpleCommand) -> Self { - Command { - redirect: None, - inner: CommandInner::Simple(c), + fn from(c: SimpleCommand) -> Self { + Command { + redirect: None, + inner: CommandInner::Simple(c), + } } - } } impl From for PipelineInner { - fn from(c: SimpleCommand) -> Self { - PipelineInner::Command(c.into()) - } + fn from(c: SimpleCommand) -> Self { + PipelineInner::Command(c.into()) + } } impl From for PipelineInner { - fn from(c: Command) -> Self { - PipelineInner::Command(c) - } + fn from(c: Command) -> Self { + PipelineInner::Command(c) + } } impl From for Sequence { - fn from(c: SimpleCommand) -> Self { - let command: Command = c.into(); - command.into() - } + fn from(c: SimpleCommand) -> Self { + let command: Command = c.into(); + command.into() + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, PartialEq, Eq, Clone)] pub struct EnvVar { - pub name: String, - pub value: Word, + pub name: String, + pub value: Word, } impl EnvVar { - pub fn new(name: String, value: Word) -> Self { - EnvVar { name, value } - } + pub fn new(name: String, value: Word) -> Self { + EnvVar { name, value } + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] @@ -218,1630 +219,494 @@ impl EnvVar { pub struct Word(Vec); impl Word { - pub fn new_string(text: &str) -> Self { - Word(vec![WordPart::Quoted(vec![WordPart::Text( - text.to_string(), - )])]) - } - - pub fn new_word(text: &str) -> Self { - Word(vec![WordPart::Text(text.to_string())]) - } - - pub fn parts(&self) -> &Vec { - &self.0 - } - - pub fn into_parts(self) -> Vec { - self.0 - } + pub fn new(parts: Vec) -> Self { + Word(parts) + } + + pub fn new_empty() -> Self { + Word(vec![]) + } + + pub fn new_string(text: &str) -> Self { + Word(vec![WordPart::Quoted(vec![WordPart::Text( + text.to_string(), + )])]) + } + + pub fn new_word(text: &str) -> Self { + Word(vec![WordPart::Text(text.to_string())]) + } + + pub fn parts(&self) -> &Vec { + &self.0 + } + + pub fn into_parts(self) -> Vec { + self.0 + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "value") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "value") )] #[derive(Debug, PartialEq, Eq, Clone)] pub enum WordPart { - /// Text in the string (ex. `some text`) - Text(String), - /// Variable substitution (ex. `$MY_VAR`) - Variable(String), - /// Command substitution (ex. `$(command)`) - Command(SequentialList), - /// Quoted string (ex. `"hello"` or `'test'`) - Quoted(Vec), + /// Text in the string (ex. `some text`) + Text(String), + /// Variable substitution (ex. `$MY_VAR`) + Variable(String), + /// Command substitution (ex. `$(command)`) + Command(SequentialList), + /// Quoted string (ex. `"hello"` or `'test'`) + Quoted(Vec), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "fd") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "fd") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectFd { - Fd(u32), - StdoutStderr, + Fd(u32), + StdoutStderr, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Redirect { - pub maybe_fd: Option, - pub op: RedirectOp, - pub io_file: IoFile, + pub maybe_fd: Option, + pub op: RedirectOp, + pub io_file: IoFile, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "value") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "value") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum IoFile { - /// Filename to redirect to/from (ex. `file.txt`` in `cmd < file.txt`) - Word(Word), - /// File descriptor to redirect to/from (ex. `2` in `cmd >&2`) - Fd(u32), + /// Filename to redirect to/from (ex. `file.txt`` in `cmd < file.txt`) + Word(Word), + /// File descriptor to redirect to/from (ex. `2` in `cmd >&2`) + Fd(u32), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "value") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "value") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectOp { - Input(RedirectOpInput), - Output(RedirectOpOutput), + Input(RedirectOpInput), + Output(RedirectOpOutput), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectOpInput { - /// < - Redirect, + /// < + Redirect, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectOpOutput { - /// > - Overwrite, - /// >> - Append, + /// > + Overwrite, + /// >> + Append, } +#[derive(Parser)] +#[grammar = "grammar.pest"] +struct ShellParser; + pub fn parse(input: &str) -> Result { - match parse_sequential_list(input) { - Ok((input, expr)) => { - if input.trim().is_empty() { - if expr.items.is_empty() { - bail!("Empty command.") - } else { - Ok(expr) + let pairs = ShellParser::parse(Rule::FILE, input)?; + parse_complete_command(pairs.into_iter().next().unwrap()) +} + +fn parse_complete_command(pair: Pair) -> Result { + let mut items = Vec::new(); + for list in pair.into_inner() { + if list.as_rule() == Rule::list { + let mut is_async = false; + for item in list.into_inner() { + match item.as_rule() { + Rule::and_or => { + let result = parse_and_or(item); + match result { + Ok(sequence) => { + items.push(SequentialListItem { + sequence, + is_async, + }); + } + Err(e) => return Err(e), + } + } + Rule::separator_op => { + is_async = item.as_str() == "&"; + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule: {}", + item.as_str() + )); + } + } + } } - } else { - fail_for_trailing_input(input) - .into_result() - .map_err(|err| err.into()) - } } - Err(ParseError::Backtrace) => fail_for_trailing_input(input) - .into_result() - .map_err(|err| err.into()), - Err(ParseError::Failure(e)) => e.into_result().map_err(|err| err.into()), - } -} - -fn parse_sequential_list(input: &str) -> ParseResult { - let (input, items) = separated_list( - terminated(parse_sequential_list_item, skip_whitespace), - terminated( - skip_whitespace, - or( - map(parse_sequential_list_op, |_| ()), - map(parse_async_list_op, |_| ()), - ), - ), - )(input)?; - Ok((input, SequentialList { items })) -} - -fn parse_sequential_list_item(input: &str) -> ParseResult { - let (input, sequence) = parse_sequence(input)?; - Ok(( - input, - SequentialListItem { - is_async: maybe(parse_async_list_op)(input)?.1.is_some(), - sequence, - }, - )) -} - -fn parse_sequence(input: &str) -> ParseResult { - let (input, current) = terminated( - or( - parse_shell_var_command, - map(parse_pipeline, Sequence::Pipeline), - ), - skip_whitespace, - )(input)?; - - Ok(match parse_boolean_list_op(input) { - Ok((input, op)) => { - let (input, next_sequence) = assert_exists( - &parse_sequence, - "Expected command following boolean operator.", - )(input)?; - ( - input, - Sequence::BooleanList(Box::new(BooleanList { - current, - op, - next: next_sequence, - })), - ) + Ok(SequentialList { items }) +} + +fn parse_and_or(pair: Pair) -> Result { + let mut pipelines = pair.into_inner(); + let first_pipeline = pipelines.next().unwrap(); + let mut current = parse_pipeline(first_pipeline).unwrap(); + + while let Some(op) = pipelines.next() { + if let Some(next_pipeline) = pipelines.next() { + let op = match op.as_str() { + "&&" => BooleanListOperator::And, + "||" => BooleanListOperator::Or, + _ => unreachable!(), + }; + let next = parse_pipeline(next_pipeline)?; + current = Sequence::BooleanList(Box::new(BooleanList { + current, + op, + next, + })); + } } - Err(ParseError::Backtrace) => (input, current), - Err(err) => return Err(err), - }) -} -fn parse_shell_var_command(input: &str) -> ParseResult { - let env_vars_input = input; - let (input, mut env_vars) = if_not_empty(parse_env_vars)(input)?; - let (input, args) = parse_command_args(input)?; - if !args.is_empty() { - return ParseError::backtrace(); - } - if env_vars.len() > 1 { - ParseError::fail(env_vars_input, "Cannot set multiple environment variables when there is no following command.") - } else { - ParseResult::Ok((input, Sequence::ShellVar(env_vars.remove(0)))) - } + Ok(current) } -/// Parses a pipeline, which is a sequence of one or more commands. -/// https://www.gnu.org/software/bash/manual/html_node/Pipelines.html -fn parse_pipeline(input: &str) -> ParseResult { - let (input, maybe_negated) = maybe(parse_negated_op)(input)?; - let (input, inner) = parse_pipeline_inner(input)?; - - let pipeline = Pipeline { - negated: maybe_negated.is_some(), - inner, - }; +fn parse_pipeline(pair: Pair) -> Result { + let mut inner = pair.into_inner(); - Ok((input, pipeline)) -} - -fn parse_pipeline_inner(input: &str) -> ParseResult { - let original_input = input; - let (input, command) = parse_command(input)?; - - let (input, inner) = match parse_pipe_sequence_op(input) { - Ok((input, op)) => { - let (input, next_inner) = assert_exists( - &parse_pipeline_inner, - "Expected command following pipeline operator.", - )(input)?; - - if command.redirect.is_some() { - return ParseError::fail( - original_input, - "Redirects in pipe sequence commands are currently not supported.", - ); - } - - ( - input, - PipelineInner::PipeSequence(Box::new(PipeSequence { - current: command, - op, - next: next_inner, - })), - ) + // Check if the first element is Bang (negation) + let first = inner + .next() + .ok_or_else(|| anyhow::anyhow!("Expected pipeline content"))?; + let (negated, pipe_sequence) = if first.as_rule() == Rule::Bang { + // If it's Bang, the next element should be the pipe_sequence + let pipe_sequence = inner.next().ok_or_else(|| { + anyhow::anyhow!("Expected pipe sequence after negation") + })?; + (true, pipe_sequence) + } else { + // If it's not Bang, this element itself is the pipe_sequence + (false, first) + }; + + let pipeline_inner = parse_pipe_sequence(pipe_sequence)?; + + Ok(Sequence::Pipeline(Pipeline { + negated, + inner: pipeline_inner, + })) +} + +fn parse_pipe_sequence(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + + // Parse the first command + let first_command = inner.next().ok_or_else(|| { + anyhow::anyhow!("Expected at least one command in pipe sequence") + })?; + let current = parse_command(first_command)?; + + // Check if there's a pipe operator + match inner.next() { + Some(pipe_op) => { + let op = match pipe_op.as_rule() { + Rule::Stdout => PipeSequenceOperator::Stdout, + Rule::StdoutStderr => PipeSequenceOperator::StdoutStderr, + _ => { + return Err(anyhow::anyhow!( + "Expected pipe operator, found {:?}", + pipe_op.as_rule() + )) + } + }; + + // Parse the rest of the pipe sequence + let next_sequence = inner.next().ok_or_else(|| { + anyhow::anyhow!("Expected command after pipe operator") + })?; + let next = parse_pipe_sequence(next_sequence)?; + + Ok(PipelineInner::PipeSequence(Box::new(PipeSequence { + current, + op, + next, + }))) + } + None => Ok(PipelineInner::Command(current)), } - Err(ParseError::Backtrace) => (input, PipelineInner::Command(command)), - Err(err) => return Err(err), - }; - - Ok((input, inner)) -} - -fn parse_command(input: &str) -> ParseResult { - let (input, inner) = terminated( - or( - map(parse_subshell, |l| CommandInner::Subshell(Box::new(l))), - map(parse_simple_command, CommandInner::Simple), - ), - skip_whitespace, - )(input)?; - - let before_redirects_input = input; - let (input, mut redirects) = - many0(terminated(parse_redirect, skip_whitespace))(input)?; - - if redirects.len() > 1 { - return ParseError::fail( - before_redirects_input, - "Multiple redirects are currently not supported.", - ); - } - - let command = Command { - redirect: redirects.pop(), - inner, - }; - - Ok((input, command)) -} - -fn parse_simple_command(input: &str) -> ParseResult { - let (input, env_vars) = parse_env_vars(input)?; - let (input, args) = if_not_empty(parse_command_args)(input)?; - ParseResult::Ok((input, SimpleCommand { env_vars, args })) -} - -fn parse_command_args(input: &str) -> ParseResult> { - many_till( - terminated(parse_shell_arg, assert_whitespace_or_end_and_skip), - or4( - parse_list_op, - map(parse_redirect, |_| ()), - map(parse_pipe_sequence_op, |_| ()), - map(ch(')'), |_| ()), - ), - )(input) -} - -fn parse_shell_arg(input: &str) -> ParseResult { - let (input, value) = parse_word(input)?; - if value.parts().is_empty() { - ParseError::backtrace() - } else { - Ok((input, value)) - } -} - -fn parse_list_op(input: &str) -> ParseResult<()> { - or( - map(parse_boolean_list_op, |_| ()), - map(or(parse_sequential_list_op, parse_async_list_op), |_| ()), - )(input) -} - -fn parse_boolean_list_op(input: &str) -> ParseResult { - or( - map(parse_op_str(BooleanListOperator::And.as_str()), |_| { - BooleanListOperator::And - }), - map(parse_op_str(BooleanListOperator::Or.as_str()), |_| { - BooleanListOperator::Or - }), - )(input) -} - -fn parse_sequential_list_op(input: &str) -> ParseResult<&str> { - terminated(tag(";"), skip_whitespace)(input) -} - -fn parse_async_list_op(input: &str) -> ParseResult<&str> { - parse_op_str("&")(input) -} - -fn parse_negated_op(input: &str) -> ParseResult<&str> { - terminated( - tag("!"), - // must have whitespace following - whitespace, - )(input) -} - -fn parse_op_str<'a>( - operator: &str, -) -> impl Fn(&'a str) -> ParseResult<'a, &'a str> { - debug_assert!(operator == "&&" || operator == "||" || operator == "&"); - let operator = operator.to_string(); - terminated( - tag(operator), - terminated(check_not(one_of("|&")), skip_whitespace), - ) -} - -fn parse_pipe_sequence_op(input: &str) -> ParseResult { - terminated( - or( - map(tag("|&"), |_| PipeSequenceOperator::StdoutStderr), - map(ch('|'), |_| PipeSequenceOperator::Stdout), - ), - terminated(check_not(one_of("|&")), skip_whitespace), - )(input) -} - -fn parse_redirect(input: &str) -> ParseResult { - // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_07 - let (input, maybe_fd) = maybe(parse_u32)(input)?; - let (input, maybe_ampersand) = if maybe_fd.is_none() { - maybe(ch('&'))(input)? - } else { - (input, None) - }; - let (input, op) = or3( - map(tag(">>"), |_| RedirectOp::Output(RedirectOpOutput::Append)), - map(or(tag(">"), tag(">|")), |_| { - RedirectOp::Output(RedirectOpOutput::Overwrite) - }), - map(ch('<'), |_| RedirectOp::Input(RedirectOpInput::Redirect)), - )(input)?; - let (input, io_file) = or( - map(preceded(ch('&'), parse_u32), IoFile::Fd), - map(preceded(skip_whitespace, parse_word), IoFile::Word), - )(input)?; - - let maybe_fd = if let Some(fd) = maybe_fd { - Some(RedirectFd::Fd(fd)) - } else if maybe_ampersand.is_some() { - Some(RedirectFd::StdoutStderr) - } else { - None - }; - - Ok(( - input, - Redirect { - maybe_fd, - op, - io_file, - }, - )) -} - -fn parse_env_vars(input: &str) -> ParseResult> { - many0(terminated(parse_env_var, skip_whitespace))(input) } -fn parse_env_var(input: &str) -> ParseResult { - let (input, name) = parse_env_var_name(input)?; - let (input, _) = ch('=')(input)?; - let (input, value) = with_error_context( - terminated(parse_env_var_value, assert_whitespace_or_end), - "Invalid environment variable value.", - )(input)?; - Ok((input, EnvVar::new(name.to_string(), value))) -} - -fn parse_env_var_name(input: &str) -> ParseResult<&str> { - if_not_empty(take_while(is_valid_env_var_char))(input) -} - -fn parse_env_var_value(input: &str) -> ParseResult { - parse_word(input) -} - -fn parse_word(input: &str) -> ParseResult { - let parse_quoted_or_unquoted = or( - map(parse_quoted_string, |parts| vec![WordPart::Quoted(parts)]), - parse_unquoted_word, - ); - let (input, mut parts) = parse_quoted_or_unquoted(input)?; - if parts.is_empty() { - Ok((input, Word(parts))) - } else { - let (input, result) = many0(if_not_empty(parse_quoted_or_unquoted))(input)?; - parts.extend(result.into_iter().flatten()); - Ok((input, Word(parts))) - } +fn parse_command(pair: Pair) -> Result { + let inner = pair.into_inner().next().unwrap(); + match inner.as_rule() { + Rule::simple_command => parse_simple_command(inner), + Rule::compound_command => todo!("inner"), + Rule::function_definition => { + todo!("function definitions are not supported yet") + } + _ => Err(anyhow::anyhow!( + "Unexpected rule in command: {:?}", + inner.as_rule() + )), + } } -fn parse_unquoted_word(input: &str) -> ParseResult> { - assert( - parse_word_parts(ParseWordPartsMode::Unquoted), - |result| { - result - .ok() - .map(|(_, parts)| { - if parts.len() == 1 { - if let WordPart::Text(text) = &parts[0] { - return !is_reserved_word(text); +fn parse_simple_command(pair: Pair) -> Result { + let mut env_vars = Vec::new(); + let mut args = Vec::new(); + let mut redirect = None; + + for item in pair.into_inner() { + match item.as_rule() { + Rule::cmd_prefix => { + for prefix in item.into_inner() { + match prefix.as_rule() { + Rule::ASSIGNMENT_WORD => { + env_vars.push(parse_env_var(prefix)?) + } + Rule::io_redirect => todo!("io_redirect as prefix"), + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in cmd_prefix: {:?}", + prefix.as_rule() + )) + } + } + } + } + Rule::cmd_word | Rule::cmd_name => { + args.push(parse_word(item.into_inner().next().unwrap())?) + } + Rule::cmd_suffix => { + for suffix in item.into_inner() { + match suffix.as_rule() { + Rule::WORD => args.push(parse_word(suffix)?), + Rule::io_redirect => { + redirect = Some(parse_io_redirect(suffix)?); + } + Rule::QUOTED_WORD => args.push(parse_word(suffix)?), + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in cmd_suffix: {:?}", + suffix.as_rule() + )) + } + } + } + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in simple_command: {:?}", + item.as_rule() + )) } - } - true - }) - .unwrap_or(true) - }, - "Unsupported reserved word.", - )(input) -} - -fn parse_quoted_string(input: &str) -> ParseResult> { - // Strings may be up beside each other, and if they are they - // should be categorized as the same argument. - map( - many1(or( - map(parse_single_quoted_string, |text| { - vec![WordPart::Text(text.to_string())] - }), - parse_double_quoted_string, - )), - |vecs| vecs.into_iter().flatten().collect(), - )(input) -} - -fn parse_single_quoted_string(input: &str) -> ParseResult<&str> { - // single quoted strings cannot contain a single quote - // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_02 - delimited( - ch('\''), - take_while(|c| c != '\''), - with_failure_input( - input, - assert_exists(ch('\''), "Expected closing single quote."), - ), - )(input) -} - -fn parse_double_quoted_string(input: &str) -> ParseResult> { - // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03 - // Double quotes may have escaped - delimited( - ch('"'), - parse_word_parts(ParseWordPartsMode::DoubleQuotes), - with_failure_input( - input, - assert_exists(ch('"'), "Expected closing double quote."), - ), - )(input) -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum ParseWordPartsMode { - DoubleQuotes, - Unquoted, -} - -fn parse_word_parts( - mode: ParseWordPartsMode, -) -> impl Fn(&str) -> ParseResult> { - fn parse_escaped_dollar_sign(input: &str) -> ParseResult { - or( - parse_escaped_char('$'), - terminated( - ch('$'), - check_not(or(map(parse_env_var_name, |_| ()), map(ch('('), |_| ()))), - ), - )(input) - } - - fn parse_special_shell_var(input: &str) -> ParseResult { - // for now, these hard error - preceded(ch('$'), |input| { - if let Some(char) = input.chars().next() { - // $$ - process id - // $# - number of arguments in $* - // $* - list of arguments passed to the current process - if "$#*".contains(char) { - return ParseError::fail( - input, - format!("${char} is currently not supported."), - ); } - } - ParseError::backtrace() - })(input) - } - - fn parse_escaped_char<'a>( - c: char, - ) -> impl Fn(&'a str) -> ParseResult<'a, char> { - preceded(ch('\\'), ch(c)) - } - - fn first_escaped_char<'a>( - mode: ParseWordPartsMode, - ) -> impl Fn(&'a str) -> ParseResult<'a, char> { - or7( - parse_special_shell_var, - parse_escaped_dollar_sign, - parse_escaped_char('`'), - parse_escaped_char('"'), - parse_escaped_char('('), - parse_escaped_char(')'), - if_true(parse_escaped_char('\''), move |_| { - mode == ParseWordPartsMode::DoubleQuotes - }), - ) - } - - move |input| { - enum PendingPart<'a> { - Char(char), - Variable(&'a str), - Command(SequentialList), - Parts(Vec), } - let (input, parts) = many0(or7( - or( - map(tag("$?"), |_| PendingPart::Variable("?")), - map(first_escaped_char(mode), PendingPart::Char), - ), - map(parse_command_substitution, PendingPart::Command), - map(preceded(ch('$'), parse_env_var_name), PendingPart::Variable), - |input| { - let (_, _) = ch('`')(input)?; - ParseError::fail( - input, - "Back ticks in strings is currently not supported.", - ) - }, - // words can have escaped spaces - map( - if_true(preceded(ch('\\'), ch(' ')), |_| { - mode == ParseWordPartsMode::Unquoted - }), - PendingPart::Char, - ), - map( - if_true(next_char, |&c| match mode { - ParseWordPartsMode::DoubleQuotes => c != '"', - ParseWordPartsMode::Unquoted => { - !c.is_whitespace() && !"~(){}<>|&;\"'".contains(c) - } - }), - PendingPart::Char, - ), - |input| match mode { - ParseWordPartsMode::DoubleQuotes => ParseError::backtrace(), - ParseWordPartsMode::Unquoted => { - let (input, parts) = - map(parse_quoted_string, |parts| vec![WordPart::Quoted(parts)])( - input, - )?; - Ok((input, PendingPart::Parts(parts))) + Ok(Command { + inner: CommandInner::Simple(SimpleCommand { env_vars, args }), + redirect, + }) +} + +fn parse_word(pair: Pair) -> Result { + let mut parts = Vec::new(); + + match pair.as_rule() { + Rule::WORD => { + let text = pair.as_str(); + if text.starts_with('\'') && text.ends_with('\'') { + // Single quoted text + parts.push(WordPart::Quoted(vec![WordPart::Text( + text[1..text.len() - 1].to_string(), + )])); + } else if text.starts_with('"') && text.ends_with('"') { + // Double quoted text + parts.push(WordPart::Quoted(vec![WordPart::Text( + text[1..text.len() - 1].to_string(), + )])); + } else if let Some(var_name) = text.strip_prefix('$') { + // Variable + parts.push(WordPart::Variable(var_name.to_string())); + } else { + // Regular text + parts.push(WordPart::Text(text.to_string())); + } } - }, - ))(input)?; - - let mut result = Vec::new(); - for part in parts { - match part { - PendingPart::Char(c) => { - if let Some(WordPart::Text(text)) = result.last_mut() { - text.push(c); - } else { - result.push(WordPart::Text(c.to_string())); - } + Rule::WORD_WITH_EQUAL => { + // Handle words that might start with '=' + let text = pair.as_str(); + if let Some(rest) = text.strip_prefix('=') { + parts.push(WordPart::Text("=".to_string())); + parts.push(WordPart::Text(rest.to_string())); + } else if text.starts_with('\'') && text.ends_with('\'') { + // Single quoted text + parts.push(WordPart::Quoted(vec![WordPart::Text( + text[1..text.len() - 1].to_string(), + )])); + } else if text.starts_with('"') && text.ends_with('"') { + // Double quoted text + parts.push(WordPart::Quoted(vec![WordPart::Text( + text[1..text.len() - 1].to_string(), + )])); + } else if let Some(var_name) = text.strip_prefix('$') { + // Variable + parts.push(WordPart::Variable(var_name.to_string())); + } else { + // Regular text + parts.push(WordPart::Text(text.to_string())); + } } - PendingPart::Command(s) => result.push(WordPart::Command(s)), - PendingPart::Variable(v) => { - result.push(WordPart::Variable(v.to_string())) + Rule::QUOTED_WORD => { + let text = pair.as_str(); + let unquoted_text = &text[1..text.len() - 1]; + parts.push(WordPart::Quoted(vec![WordPart::Text( + unquoted_text.to_string(), + )])); } - PendingPart::Parts(parts) => { - result.extend(parts); + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in word: {:?}", + pair.as_rule() + )) } - } } - Ok((input, result)) - } -} - -fn parse_command_substitution(input: &str) -> ParseResult { - delimited(tag("$("), parse_sequential_list, ch(')'))(input) -} - -fn parse_subshell(input: &str) -> ParseResult { - delimited( - terminated(ch('('), skip_whitespace), - parse_sequential_list, - with_failure_input( - input, - assert_exists(ch(')'), "Expected closing parenthesis on subshell."), - ), - )(input) -} - -fn parse_u32(input: &str) -> ParseResult { - let mut value: u32 = 0; - let mut byte_index = 0; - for c in input.chars() { - if c.is_ascii_digit() { - let shifted_val = match value.checked_mul(10) { - Some(val) => val, - None => return ParseError::backtrace(), - }; - value = match shifted_val.checked_add(c.to_digit(10).unwrap()) { - Some(val) => val, - None => return ParseError::backtrace(), - }; - } else if byte_index == 0 { - return ParseError::backtrace(); + if parts.is_empty() { + Ok(Word::new_empty()) } else { - break; + Ok(Word::new(parts)) } - byte_index += c.len_utf8(); - } - Ok((&input[byte_index..], value)) } -fn assert_whitespace_or_end_and_skip(input: &str) -> ParseResult<()> { - terminated(assert_whitespace_or_end, skip_whitespace)(input) -} +fn parse_env_var(pair: Pair) -> Result { + let mut parts = pair.into_inner(); -fn assert_whitespace_or_end(input: &str) -> ParseResult<()> { - if let Some(next_char) = input.chars().next() { - if !next_char.is_whitespace() - && !matches!(next_char, ';' | '&' | '|' | '(' | ')') - { - return Err(ParseError::Failure(fail_for_trailing_input(input))); - } - } - Ok((input, ())) -} + // Get the name of the environment variable + let name = parts + .next() + .ok_or_else(|| anyhow!("Expected variable name"))? + .as_str() + .to_string(); -fn is_valid_env_var_char(c: char) -> bool { - // [a-zA-Z0-9_]+ - c.is_ascii_alphanumeric() || c == '_' -} + // Get the value of the environment variable + let value = parts + .next() + .ok_or_else(|| anyhow!("Expected variable value"))?; -fn is_reserved_word(text: &str) -> bool { - matches!( - text, - "if" - | "then" - | "else" - | "elif" - | "fi" - | "do" - | "done" - | "case" - | "esac" - | "while" - | "until" - | "for" - | "in" - ) + // Parse the value as a Word + let word_value = parse_word(value)?; + + Ok(EnvVar { + name, + value: word_value, + }) } -fn fail_for_trailing_input(input: &str) -> ParseErrorFailure { - ParseErrorFailure::new(input, "Unexpected character.") +fn parse_io_redirect(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + + // Parse the optional IO number or AMPERSAND + let (maybe_fd, op_and_file) = match inner.next() { + Some(p) if p.as_rule() == Rule::IO_NUMBER => ( + Some(RedirectFd::Fd(p.as_str().parse::().unwrap())), + inner.next().ok_or_else(|| { + anyhow!("Expected redirection operator after IO number") + })?, + ), + Some(p) if p.as_rule() == Rule::AMPERSAND => ( + Some(RedirectFd::StdoutStderr), + inner.next().ok_or_else(|| { + anyhow!("Expected redirection operator after &") + })?, + ), + Some(p) => (None, p), + None => return Err(anyhow!("Unexpected end of input in io_redirect")), + }; + + let (op, io_file) = parse_io_file(op_and_file)?; + + Ok(Redirect { + maybe_fd, + op, + io_file, + }) } -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_main() { - assert_eq!(parse("").err().unwrap().to_string(), "Empty command."); - assert_eq!( - parse("&& testing").err().unwrap().to_string(), - concat!("Unexpected character.\n", " && testing\n", " ~",), - ); - assert_eq!( - parse("test { test").err().unwrap().to_string(), - concat!("Unexpected character.\n", " { test\n", " ~",), - ); - assert!(parse("cp test/* other").is_ok()); - assert!(parse("cp test/? other").is_ok()); - assert_eq!( - parse("(test").err().unwrap().to_string(), - concat!( - "Expected closing parenthesis on subshell.\n", - " (test\n", - " ~" - ), - ); - assert_eq!( - parse("cmd \"test").err().unwrap().to_string(), - concat!("Expected closing double quote.\n", " \"test\n", " ~"), - ); - assert_eq!( - parse("cmd 'test").err().unwrap().to_string(), - concat!("Expected closing single quote.\n", " 'test\n", " ~"), - ); - - assert!(parse("( test ||other&&test;test);(t&est );").is_ok()); - assert!(parse("command --arg='value'").is_ok()); - assert!(parse("command --arg=\"value\"").is_ok()); - - assert_eq!( - parse("echo `echo 1`").err().unwrap().to_string(), - concat!( - "Back ticks in strings is currently not supported.\n", - " `echo 1`\n", - " ~", - ), - ); - assert!( - parse("deno run --allow-read=. --allow-write=./testing main.ts").is_ok(), - ); - } - - #[test] - fn test_sequential_list() { - run_test( - parse_sequential_list, - concat!( - "Name=Value OtherVar=Other command arg1 || command2 arg12 arg13 ; ", - "command3 && command4 & command5 ; export ENV6=5 ; ", - "ENV7=other && command8 || command9 ; ", - "cmd10 && (cmd11 || cmd12)" - ), - Ok(SequentialList { - items: vec![ - SequentialListItem { - is_async: false, - sequence: Sequence::BooleanList(Box::new(BooleanList { - current: SimpleCommand { - env_vars: vec![ - EnvVar::new("Name".to_string(), Word::new_word("Value")), - EnvVar::new("OtherVar".to_string(), Word::new_word("Other")), - ], - args: vec![Word::new_word("command"), Word::new_word("arg1")], - } - .into(), - op: BooleanListOperator::Or, - next: SimpleCommand { - env_vars: vec![], - args: vec![ - Word::new_word("command2"), - Word::new_word("arg12"), - Word::new_word("arg13"), - ], - } - .into(), - })), - }, - SequentialListItem { - is_async: true, - sequence: Sequence::BooleanList(Box::new(BooleanList { - current: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command3")], - } - .into(), - op: BooleanListOperator::And, - next: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command4")], - } - .into(), - })), - }, - SequentialListItem { - is_async: false, - sequence: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command5")], - } - .into(), - }, - SequentialListItem { - is_async: false, - sequence: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("export"), Word::new_word("ENV6=5")], - } - .into(), - }, - SequentialListItem { - is_async: false, - sequence: Sequence::BooleanList(Box::new(BooleanList { - current: Sequence::ShellVar(EnvVar::new( - "ENV7".to_string(), - Word::new_word("other"), - )), - op: BooleanListOperator::And, - next: Sequence::BooleanList(Box::new(BooleanList { - current: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command8")], - } - .into(), - op: BooleanListOperator::Or, - next: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command9")], - } - .into(), - })), - })), - }, - SequentialListItem { - is_async: false, - sequence: Sequence::BooleanList(Box::new(BooleanList { - current: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("cmd10")], - } - .into(), - op: BooleanListOperator::And, - next: Command { - inner: CommandInner::Subshell(Box::new(SequentialList { - items: vec![SequentialListItem { - is_async: false, - sequence: Sequence::BooleanList(Box::new(BooleanList { - current: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("cmd11")], - } - .into(), - op: BooleanListOperator::Or, - next: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("cmd12")], - } - .into(), - })), - }], - })), - redirect: None, - } - .into(), - })), - }, - ], - }), - ); - - run_test( - parse_sequential_list, - "command1 ; command2 ; A='b' command3", - Ok(SequentialList { - items: vec![ - SequentialListItem { - is_async: false, - sequence: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command1")], - } - .into(), - }, - SequentialListItem { - is_async: false, - sequence: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command2")], - } - .into(), - }, - SequentialListItem { - is_async: false, - sequence: SimpleCommand { - env_vars: vec![EnvVar::new( - "A".to_string(), - Word::new_string("b"), - )], - args: vec![Word::new_word("command3")], - } - .into(), - }, - ], - }), - ); - - run_test( - parse_sequential_list, - "test &&", - Err("Expected command following boolean operator."), - ); - - run_test( - parse_sequential_list, - "command &", - Ok(SequentialList { - items: vec![SequentialListItem { - is_async: true, - sequence: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command")], - } - .into(), - }], - }), - ); - - run_test( - parse_sequential_list, - "test | other", - Ok(SequentialList { - items: vec![SequentialListItem { - is_async: false, - sequence: PipeSequence { - current: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("test")], - } - .into(), - op: PipeSequenceOperator::Stdout, - next: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("other")], - } - .into(), - } - .into(), - }], - }), - ); - - run_test( - parse_sequential_list, - "test |& other", - Ok(SequentialList { - items: vec![SequentialListItem { - is_async: false, - sequence: PipeSequence { - current: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("test")], +fn parse_io_file(pair: Pair) -> Result<(RedirectOp, IoFile)> { + let mut inner = pair.into_inner(); + let op = inner + .next() + .ok_or_else(|| anyhow!("Expected redirection operator"))?; + let filename = inner.next().ok_or_else(|| { + anyhow!("Expected filename after redirection operator") + })?; + + let redirect_op = match op.as_rule() { + Rule::LESS => RedirectOp::Input(RedirectOpInput::Redirect), + Rule::GREAT => RedirectOp::Output(RedirectOpOutput::Overwrite), + Rule::DGREAT => RedirectOp::Output(RedirectOpOutput::Append), + Rule::LESSAND | Rule::GREATAND => { + // For these operators, the target must be a number (fd) + let target = filename.as_str(); + if let Ok(fd) = target.parse::() { + return Ok(( + if op.as_rule() == Rule::LESSAND { + RedirectOp::Input(RedirectOpInput::Redirect) + } else { + RedirectOp::Output(RedirectOpOutput::Overwrite) + }, + IoFile::Fd(fd), + )); + } else { + return Err(anyhow!( + "Expected a number after {} operator", + if op.as_rule() == Rule::LESSAND { + "<&" + } else { + ">&" + } + )); } - .into(), - op: PipeSequenceOperator::StdoutStderr, - next: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("other")], - } - .into(), - } - .into(), - }], - }), - ); - - run_test( - parse_sequential_list, - "ENV=1 ENV2=3 && test", - Err("Cannot set multiple environment variables when there is no following command."), - ); - - run_test( - parse_sequential_list, - "echo $MY_ENV;", - Ok(SequentialList { - items: vec![SequentialListItem { - is_async: false, - sequence: SimpleCommand { - env_vars: vec![], - args: vec![ - Word::new_word("echo"), - Word(vec![WordPart::Variable("MY_ENV".to_string())]), - ], - } - .into(), - }], - }), - ); - - run_test( - parse_sequential_list, - "! cmd1 | cmd2 && cmd3", - Ok(SequentialList { - items: vec![SequentialListItem { - is_async: false, - sequence: Sequence::BooleanList(Box::new(BooleanList { - current: Pipeline { - negated: true, - inner: PipeSequence { - current: SimpleCommand { - args: vec![Word::new_word("cmd1")], - env_vars: vec![], - } - .into(), - op: PipeSequenceOperator::Stdout, - next: SimpleCommand { - args: vec![Word::new_word("cmd2")], - env_vars: vec![], - } - .into(), - } - .into(), - } - .into(), - op: BooleanListOperator::And, - next: SimpleCommand { - args: vec![Word::new_word("cmd3")], - env_vars: vec![], - } - .into(), - })), - }], - }), - ); - } - - #[test] - fn test_env_var() { - run_test( - parse_env_var, - "Name=Value", - Ok(EnvVar { - name: "Name".to_string(), - value: Word::new_word("Value"), - }), - ); - run_test( - parse_env_var, - "Name='quoted value'", - Ok(EnvVar { - name: "Name".to_string(), - value: Word::new_string("quoted value"), - }), - ); - run_test( - parse_env_var, - "Name=\"double quoted value\"", - Ok(EnvVar { - name: "Name".to_string(), - value: Word::new_string("double quoted value"), - }), - ); - run_test_with_end( - parse_env_var, - "Name= command_name", - Ok(EnvVar { - name: "Name".to_string(), - value: Word(vec![]), - }), - " command_name", - ); - - run_test( - parse_env_var, - "Name=$(test)", - Ok(EnvVar { - name: "Name".to_string(), - value: Word(vec![WordPart::Command(SequentialList { - items: vec![SequentialListItem { - is_async: false, - sequence: SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("test")], - } - .into(), - }], - })]), - }), - ); - - run_test( - parse_env_var, - "Name=$(OTHER=5)", - Ok(EnvVar { - name: "Name".to_string(), - value: Word(vec![WordPart::Command(SequentialList { - items: vec![SequentialListItem { - is_async: false, - sequence: Sequence::ShellVar(EnvVar { - name: "OTHER".to_string(), - value: Word::new_word("5"), - }), - }], - })]), - }), - ); - } - - #[test] - fn test_single_quotes() { - run_test( - parse_quoted_string, - "'test'", - Ok(vec![WordPart::Text("test".to_string())]), - ); - run_test( - parse_quoted_string, - r"'te\\'", - Ok(vec![WordPart::Text(r"te\\".to_string())]), - ); - run_test_with_end( - parse_quoted_string, - r"'te\'st'", - Ok(vec![WordPart::Text(r"te\".to_string())]), - "st'", - ); - run_test( - parse_quoted_string, - "' '", - Ok(vec![WordPart::Text(" ".to_string())]), - ); - run_test( - parse_quoted_string, - "' ", - Err("Expected closing single quote."), - ); - } - - #[test] - fn test_single_quotes_mid_word() { - run_test( - parse_word, - "--inspect='[::0]:3366'", - Ok(Word(vec![ - WordPart::Text("--inspect=".to_string()), - WordPart::Quoted(vec![WordPart::Text("[::0]:3366".to_string())]), - ])), - ); - } - - #[test] - fn test_double_quotes() { - run_test( - parse_quoted_string, - r#"" ""#, - Ok(vec![WordPart::Text(" ".to_string())]), - ); - run_test( - parse_quoted_string, - r#""test""#, - Ok(vec![WordPart::Text("test".to_string())]), - ); - run_test( - parse_quoted_string, - r#""te\"\$\`st""#, - Ok(vec![WordPart::Text(r#"te"$`st"#.to_string())]), - ); - run_test( - parse_quoted_string, - r#"" "#, - Err("Expected closing double quote."), - ); - run_test( - parse_quoted_string, - r#""$Test""#, - Ok(vec![WordPart::Variable("Test".to_string())]), - ); - run_test( - parse_quoted_string, - r#""$Test,$Other_Test""#, - Ok(vec![ - WordPart::Variable("Test".to_string()), - WordPart::Text(",".to_string()), - WordPart::Variable("Other_Test".to_string()), - ]), - ); - run_test( - parse_quoted_string, - r#""asdf`""#, - Err("Back ticks in strings is currently not supported."), - ); - - run_test_with_end( - parse_quoted_string, - r#""test" asdf"#, - Ok(vec![WordPart::Text("test".to_string())]), - " asdf", - ); - } - - #[test] - fn test_parse_word() { - run_test(parse_unquoted_word, "if", Err("Unsupported reserved word.")); - run_test( - parse_unquoted_word, - "$", - Ok(vec![WordPart::Text("$".to_string())]), - ); - // unsupported shell variables - run_test( - parse_unquoted_word, - "$$", - Err("$$ is currently not supported."), - ); - run_test( - parse_unquoted_word, - "$#", - Err("$# is currently not supported."), - ); - run_test( - parse_unquoted_word, - "$*", - Err("$* is currently not supported."), - ); - run_test( - parse_unquoted_word, - "test\\ test", - Ok(vec![WordPart::Text("test test".to_string())]), - ); - } - - #[test] - fn test_parse_u32() { - run_test(parse_u32, "999", Ok(999)); - run_test(parse_u32, "11", Ok(11)); - run_test(parse_u32, "0", Ok(0)); - run_test_with_end(parse_u32, "1>", Ok(1), ">"); - run_test(parse_u32, "-1", Err("backtrace")); - run_test(parse_u32, "a", Err("backtrace")); - run_test( - parse_u32, - "16116951273372934291112534924737", - Err("backtrace"), - ); - run_test(parse_u32, "4294967295", Ok(4294967295)); - run_test(parse_u32, "4294967296", Err("backtrace")); - } - - #[track_caller] - fn run_test<'a, T: PartialEq + std::fmt::Debug>( - combinator: impl Fn(&'a str) -> ParseResult<'a, T>, - input: &'a str, - expected: Result, - ) { - run_test_with_end(combinator, input, expected, ""); - } - - #[track_caller] - fn run_test_with_end<'a, T: PartialEq + std::fmt::Debug>( - combinator: impl Fn(&'a str) -> ParseResult<'a, T>, - input: &'a str, - expected: Result, - expected_end: &str, - ) { - match combinator(input) { - Ok((input, value)) => { - assert_eq!(value, expected.unwrap()); - assert_eq!(input, expected_end); - } - Err(ParseError::Backtrace) => { - assert_eq!("backtrace", expected.err().unwrap()); - } - Err(ParseError::Failure(err)) => { - assert_eq!( - err.message, - match expected.err() { - Some(err) => err, - None => - panic!("Got error: {:#}", err.into_result::().err().unwrap()), - } - ); - } - } - } - - #[test] - fn test_redirects() { - let expected = Ok(Command { - inner: CommandInner::Simple(SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("echo"), Word::new_word("1")], - }), - redirect: Some(Redirect { - maybe_fd: None, - op: RedirectOp::Output(RedirectOpOutput::Overwrite), - io_file: IoFile::Word(Word(vec![WordPart::Text( - "test.txt".to_string(), - )])), - }), - }); - - run_test(parse_command, "echo 1 > test.txt", expected.clone()); - run_test(parse_command, "echo 1 >test.txt", expected.clone()); - - // append - run_test( - parse_command, - r#"command >> "test.txt""#, - Ok(Command { - inner: CommandInner::Simple(SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command")], - }), - redirect: Some(Redirect { - maybe_fd: None, - op: RedirectOp::Output(RedirectOpOutput::Append), - io_file: IoFile::Word(Word(vec![WordPart::Quoted(vec![ - WordPart::Text("test.txt".to_string()), - ])])), - }), - }), - ); - - // fd - run_test( - parse_command, - r#"command 2> test.txt"#, - Ok(Command { - inner: CommandInner::Simple(SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command")], - }), - redirect: Some(Redirect { - maybe_fd: Some(RedirectFd::Fd(2)), - op: RedirectOp::Output(RedirectOpOutput::Overwrite), - io_file: IoFile::Word(Word(vec![WordPart::Text( - "test.txt".to_string(), - )])), - }), - }), - ); - - // both - run_test( - parse_command, - r#"command &> test.txt"#, - Ok(Command { - inner: CommandInner::Simple(SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command")], - }), - redirect: Some(Redirect { - maybe_fd: Some(RedirectFd::StdoutStderr), - op: RedirectOp::Output(RedirectOpOutput::Overwrite), - io_file: IoFile::Word(Word(vec![WordPart::Text( - "test.txt".to_string(), - )])), - }), - }), - ); - - // output redirect to fd - run_test( - parse_command, - r#"command 2>&1"#, - Ok(Command { - inner: CommandInner::Simple(SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command")], - }), - redirect: Some(Redirect { - maybe_fd: Some(RedirectFd::Fd(2)), - op: RedirectOp::Output(RedirectOpOutput::Overwrite), - io_file: IoFile::Fd(1), - }), - }), - ); - - // input redirect to fd - run_test( - parse_command, - r#"command <&0"#, - Ok(Command { - inner: CommandInner::Simple(SimpleCommand { - env_vars: vec![], - args: vec![Word::new_word("command")], - }), - redirect: Some(Redirect { - maybe_fd: None, - op: RedirectOp::Input(RedirectOpInput::Redirect), - io_file: IoFile::Fd(0), - }), - }), - ); - - run_test_with_end( - parse_command, - "echo 1 1> stdout.txt 2> stderr.txt", - Err("Multiple redirects are currently not supported."), - "1> stdout.txt 2> stderr.txt", - ); - - // redirect in pipeline sequence command should error - run_test_with_end( - parse_sequence, - "echo 1 1> stdout.txt | cat", - Err("Redirects in pipe sequence commands are currently not supported."), - "echo 1 1> stdout.txt | cat", - ); - } - - #[cfg(feature = "serialization")] - #[test] - fn serializes_command_to_json() { - assert_json_equals( - serialize_to_json("./example > output.txt"), - serde_json::json!({ - "items": [{ - "isAsync": false, - "sequence": { - "inner": { - "inner": { - "args": [[{ - "kind": "text", - "value": "./example" - }]], - "envVars": [], - "kind": "simple" - }, - "kind": "command", - "redirect": { - "ioFile": { - "kind": "word", - "value": [{ - "kind": "text", - "value": "output.txt" - }], - }, - "maybeFd": null, - "op": { - "kind": "output", - "value": "overwrite", - } - } - }, - "kind": "pipeline", - "negated": false - } - }] - }), - ); - assert_json_equals( - serialize_to_json("./example 2> output.txt"), - serde_json::json!({ - "items": [{ - "isAsync": false, - "sequence": { - "inner": { - "inner": { - "args": [[{ - "kind": "text", - "value": "./example" - }]], - "envVars": [], - "kind": "simple" - }, - "kind": "command", - "redirect": { - "ioFile": { - "kind": "word", - "value": [{ - "kind": "text", - "value": "output.txt" - }], - }, - "maybeFd": { - "kind": "fd", - "fd": 2, - }, - "op": { - "kind": "output", - "value": "overwrite", - } - } - }, - "kind": "pipeline", - "negated": false - } - }] - }), - ); - assert_json_equals( - serialize_to_json("./example &> output.txt"), - serde_json::json!({ - "items": [{ - "isAsync": false, - "sequence": { - "inner": { - "inner": { - "args": [[{ - "kind": "text", - "value": "./example" - }]], - "envVars": [], - "kind": "simple" - }, - "kind": "command", - "redirect": { - "ioFile": { - "kind": "word", - "value": [{ - "kind": "text", - "value": "output.txt" - }], - }, - "maybeFd": { - "kind": "stdoutStderr" - }, - "op": { - "kind": "output", - "value": "overwrite", - } - } - }, - "kind": "pipeline", - "negated": false - } - }] - }), - ); - assert_json_equals( - serialize_to_json("./example < output.txt"), - serde_json::json!({ - "items": [{ - "isAsync": false, - "sequence": { - "inner": { - "inner": { - "args": [[{ - "kind": "text", - "value": "./example" - }]], - "envVars": [], - "kind": "simple" - }, - "kind": "command", - "redirect": { - "ioFile": { - "kind": "word", - "value": [{ - "kind": "text", - "value": "output.txt" - }], - }, - "maybeFd": null, - "op": { - "kind": "input", - "value": "redirect", - } - } - }, - "kind": "pipeline", - "negated": false - } - }] - }), - ); - - assert_json_equals( - serialize_to_json("./example <&0"), - serde_json::json!({ - "items": [{ - "isAsync": false, - "sequence": { - "inner": { - "inner": { - "args": [[{ - "kind": "text", - "value": "./example" - }]], - "envVars": [], - "kind": "simple" - }, - "kind": "command", - "redirect": { - "ioFile": { - "kind": "fd", - "value": 0, - }, - "maybeFd": null, - "op": { - "kind": "input", - "value": "redirect", - } - } - }, - "kind": "pipeline", - "negated": false - } - }] - }), - ); - } - - #[cfg(feature = "serialization")] - #[track_caller] - fn assert_json_equals( - actual: serde_json::Value, - expected: serde_json::Value, - ) { - if actual != expected { - let actual = serde_json::to_string_pretty(&actual).unwrap(); - let expected = serde_json::to_string_pretty(&expected).unwrap(); - assert_eq!(actual, expected); - } - } + } + _ => { + return Err(anyhow!( + "Unexpected redirection operator: {:?}", + op.as_rule() + )) + } + }; + + let io_file = if filename.as_rule() == Rule::WORD { + IoFile::Word(parse_word(filename)?) + } else { + return Err(anyhow!( + "Unexpected filename type: {:?}", + filename.as_rule() + )); + }; - #[cfg(feature = "serialization")] - fn serialize_to_json(text: &str) -> serde_json::Value { - let command = parse(text).unwrap(); - serde_json::to_value(command).unwrap() - } + Ok((redirect_op, io_file)) } diff --git a/crates/deno_task_shell/src/shell/command.rs b/crates/deno_task_shell/src/shell/command.rs index 9ca8a5b..bb2ad6d 100644 --- a/crates/deno_task_shell/src/shell/command.rs +++ b/crates/deno_task_shell/src/shell/command.rs @@ -19,397 +19,403 @@ use thiserror::Error; #[derive(Debug, Clone)] pub struct UnresolvedCommandName { - pub name: String, - pub base_dir: PathBuf, + pub name: String, + pub base_dir: PathBuf, } pub fn execute_unresolved_command_name( - command_name: UnresolvedCommandName, - mut context: ShellCommandContext, + command_name: UnresolvedCommandName, + mut context: ShellCommandContext, ) -> FutureExecuteResult { - async move { - let command = - match resolve_command(&command_name, &context, &context.args).await { - Ok(command_path) => command_path, - Err(ResolveCommandError::CommandPath(err)) => { - let _ = context.stderr.write_line(&format!("{}", err)); - return ExecuteResult::Continue( - err.exit_code(), - Vec::new(), - Vec::new(), - ); - } - Err(ResolveCommandError::FailedShebang(err)) => { - let _ = context - .stderr - .write_line(&format!("{}: {}", command_name.name, err)); - return ExecuteResult::Continue( - err.exit_code(), - Vec::new(), - Vec::new(), - ); + async move { + let command = + match resolve_command(&command_name, &context, &context.args).await + { + Ok(command_path) => command_path, + Err(ResolveCommandError::CommandPath(err)) => { + let _ = context.stderr.write_line(&format!("{}", err)); + return ExecuteResult::Continue( + err.exit_code(), + Vec::new(), + Vec::new(), + ); + } + Err(ResolveCommandError::FailedShebang(err)) => { + let _ = context + .stderr + .write_line(&format!("{}: {}", command_name.name, err)); + return ExecuteResult::Continue( + err.exit_code(), + Vec::new(), + Vec::new(), + ); + } + }; + match command.command_name { + CommandName::Resolved(path) => { + ExecutableCommand::new(command_name.name, path) + .execute(context) + .await + } + CommandName::Unresolved(command_name) => { + context.args = command.args.into_owned(); + execute_unresolved_command_name(command_name, context).await + } } - }; - match command.command_name { - CommandName::Resolved(path) => { - ExecutableCommand::new(command_name.name, path) - .execute(context) - .await - } - CommandName::Unresolved(command_name) => { - context.args = command.args.into_owned(); - execute_unresolved_command_name(command_name, context).await - } } - } - .boxed_local() + .boxed_local() } enum CommandName { - Resolved(PathBuf), - Unresolved(UnresolvedCommandName), + Resolved(PathBuf), + Unresolved(UnresolvedCommandName), } struct ResolvedCommand<'a> { - command_name: CommandName, - args: Cow<'a, Vec>, + command_name: CommandName, + args: Cow<'a, Vec>, } #[derive(Error, Debug)] enum ResolveCommandError { - #[error(transparent)] - CommandPath(#[from] ResolveCommandPathError), - #[error(transparent)] - FailedShebang(#[from] FailedShebangError), + #[error(transparent)] + CommandPath(#[from] ResolveCommandPathError), + #[error(transparent)] + FailedShebang(#[from] FailedShebangError), } #[derive(Error, Debug)] enum FailedShebangError { - #[error(transparent)] - CommandPath(#[from] ResolveCommandPathError), - #[error(transparent)] - Any(#[from] anyhow::Error), + #[error(transparent)] + CommandPath(#[from] ResolveCommandPathError), + #[error(transparent)] + Any(#[from] anyhow::Error), } impl FailedShebangError { - pub fn exit_code(&self) -> i32 { - match self { - FailedShebangError::CommandPath(err) => err.exit_code(), - FailedShebangError::Any(_) => 1, + pub fn exit_code(&self) -> i32 { + match self { + FailedShebangError::CommandPath(err) => err.exit_code(), + FailedShebangError::Any(_) => 1, + } } - } } async fn resolve_command<'a>( - command_name: &UnresolvedCommandName, - context: &ShellCommandContext, - original_args: &'a Vec, + command_name: &UnresolvedCommandName, + context: &ShellCommandContext, + original_args: &'a Vec, ) -> Result, ResolveCommandError> { - let command_path = match resolve_command_path( - &command_name.name, - &command_name.base_dir, - &context.state, - ) { - Ok(command_path) => command_path, - Err(err) => return Err(err.into()), - }; - - // only bother checking for a shebang when the path has a slash - // in it because for global commands someone on Windows likely - // won't have a script with a shebang in it on Windows - if command_name.name.contains('/') { - if let Some(shebang) = resolve_shebang(&command_path).map_err(|err| { - ResolveCommandError::FailedShebang(FailedShebangError::Any(err.into())) - })? { - let (shebang_command_name, mut args) = if shebang.string_split { - let mut args = parse_shebang_args(&shebang.command, context) - .await - .map_err(FailedShebangError::Any)?; - args.push(command_path.to_string_lossy().to_string()); - (args.remove(0), args) - } else { - ( - shebang.command, - vec![command_path.to_string_lossy().to_string()], - ) - }; - args.extend(original_args.iter().cloned()); - return Ok(ResolvedCommand { - command_name: CommandName::Unresolved(UnresolvedCommandName { - name: shebang_command_name, - base_dir: command_path.parent().unwrap().to_path_buf(), - }), - args: Cow::Owned(args), - }); + let command_path = match resolve_command_path( + &command_name.name, + &command_name.base_dir, + &context.state, + ) { + Ok(command_path) => command_path, + Err(err) => return Err(err.into()), + }; + + // only bother checking for a shebang when the path has a slash + // in it because for global commands someone on Windows likely + // won't have a script with a shebang in it on Windows + if command_name.name.contains('/') { + if let Some(shebang) = + resolve_shebang(&command_path).map_err(|err| { + ResolveCommandError::FailedShebang(FailedShebangError::Any( + err.into(), + )) + })? + { + let (shebang_command_name, mut args) = if shebang.string_split { + let mut args = parse_shebang_args(&shebang.command, context) + .await + .map_err(FailedShebangError::Any)?; + args.push(command_path.to_string_lossy().to_string()); + (args.remove(0), args) + } else { + ( + shebang.command, + vec![command_path.to_string_lossy().to_string()], + ) + }; + args.extend(original_args.iter().cloned()); + return Ok(ResolvedCommand { + command_name: CommandName::Unresolved(UnresolvedCommandName { + name: shebang_command_name, + base_dir: command_path.parent().unwrap().to_path_buf(), + }), + args: Cow::Owned(args), + }); + } } - } - return Ok(ResolvedCommand { - command_name: CommandName::Resolved(command_path), - args: Cow::Borrowed(original_args), - }); + return Ok(ResolvedCommand { + command_name: CommandName::Resolved(command_path), + args: Cow::Borrowed(original_args), + }); } async fn parse_shebang_args( - text: &str, - context: &ShellCommandContext, + text: &str, + context: &ShellCommandContext, ) -> Result> { - fn err_unsupported(text: &str) -> Result> { - anyhow::bail!("unsupported shebang. Please report this as a bug (https://github.com/denoland/deno).\n\nShebang: {}", text) - } - - let mut args = crate::parser::parse(text)?; - if args.items.len() != 1 { - return err_unsupported(text); - } - let item = args.items.remove(0); - if item.is_async { - return err_unsupported(text); - } - let pipeline = match item.sequence { - crate::parser::Sequence::Pipeline(pipeline) => pipeline, - _ => return err_unsupported(text), - }; - if pipeline.negated { - return err_unsupported(text); - } - let cmd = match pipeline.inner { - crate::parser::PipelineInner::Command(cmd) => cmd, - crate::parser::PipelineInner::PipeSequence(_) => { - return err_unsupported(text) + fn err_unsupported(text: &str) -> Result> { + anyhow::bail!("unsupported shebang. Please report this as a bug (https://github.com/denoland/deno).\n\nShebang: {}", text) + } + + let mut args = crate::parser::parse(text)?; + if args.items.len() != 1 { + return err_unsupported(text); + } + let item = args.items.remove(0); + if item.is_async { + return err_unsupported(text); + } + let pipeline = match item.sequence { + crate::parser::Sequence::Pipeline(pipeline) => pipeline, + _ => return err_unsupported(text), + }; + if pipeline.negated { + return err_unsupported(text); + } + let cmd = match pipeline.inner { + crate::parser::PipelineInner::Command(cmd) => cmd, + crate::parser::PipelineInner::PipeSequence(_) => { + return err_unsupported(text) + } + }; + if cmd.redirect.is_some() { + return err_unsupported(text); + } + let cmd = match cmd.inner { + crate::parser::CommandInner::Simple(cmd) => cmd, + crate::parser::CommandInner::Subshell(_) => { + return err_unsupported(text) + } + }; + if !cmd.env_vars.is_empty() { + return err_unsupported(text); } - }; - if cmd.redirect.is_some() { - return err_unsupported(text); - } - let cmd = match cmd.inner { - crate::parser::CommandInner::Simple(cmd) => cmd, - crate::parser::CommandInner::Subshell(_) => return err_unsupported(text), - }; - if !cmd.env_vars.is_empty() { - return err_unsupported(text); - } - - Ok( - super::execute::evaluate_args( - cmd.args, - &context.state, - context.stdin.clone(), - context.stderr.clone(), + + Ok(super::execute::evaluate_args( + cmd.args, + &context.state, + context.stdin.clone(), + context.stderr.clone(), ) - .await?, - ) + .await?) } /// Errors for executable commands. #[derive(Error, Debug, PartialEq)] pub enum ResolveCommandPathError { - #[error("{}: command not found", .0)] - CommandNotFound(String), - #[error("command name was empty")] - CommandEmpty, + #[error("{}: command not found", .0)] + CommandNotFound(String), + #[error("command name was empty")] + CommandEmpty, } impl ResolveCommandPathError { - pub fn exit_code(&self) -> i32 { - match self { - // Use the Exit status that is used in bash: https://www.gnu.org/software/bash/manual/bash.html#Exit-Status - ResolveCommandPathError::CommandNotFound(_) => 127, - ResolveCommandPathError::CommandEmpty => 1, + pub fn exit_code(&self) -> i32 { + match self { + // Use the Exit status that is used in bash: https://www.gnu.org/software/bash/manual/bash.html#Exit-Status + ResolveCommandPathError::CommandNotFound(_) => 127, + ResolveCommandPathError::CommandEmpty => 1, + } } - } } pub fn resolve_command_path( - command_name: &str, - base_dir: &Path, - state: &ShellState, + command_name: &str, + base_dir: &Path, + state: &ShellState, ) -> Result { - resolve_command_path_inner(command_name, base_dir, state, || { - Ok(std::env::current_exe()?) - }) + resolve_command_path_inner(command_name, base_dir, state, || { + Ok(std::env::current_exe()?) + }) } fn resolve_command_path_inner( - command_name: &str, - base_dir: &Path, - state: &ShellState, - current_exe: impl FnOnce() -> Result, + command_name: &str, + base_dir: &Path, + state: &ShellState, + current_exe: impl FnOnce() -> Result, ) -> Result { - if command_name.is_empty() { - return Err(ResolveCommandPathError::CommandEmpty); - } - - // Special handling to use the current executable for deno. - // This is to ensure deno tasks that use deno work in environments - // that don't have deno on the path and to ensure it use the current - // version of deno being executed rather than the one on the path, - // which has caused some confusion. - if command_name == "deno" { - if let Ok(exe_path) = current_exe() { - // this condition exists to make the tests pass because it's not - // using the deno as the current executable - let file_stem = exe_path.file_stem().map(|s| s.to_string_lossy()); - if file_stem.map(|s| s.to_string()) == Some("deno".to_string()) { - return Ok(exe_path); - } + if command_name.is_empty() { + return Err(ResolveCommandPathError::CommandEmpty); } - } - - // check for absolute - if PathBuf::from(command_name).is_absolute() { - return Ok(PathBuf::from(command_name)); - } - - // then relative - if command_name.contains('/') - || (cfg!(windows) && command_name.contains('\\')) - { - return Ok(base_dir.join(command_name)); - } - - // now search based on the current environment state - let mut search_dirs = vec![base_dir.to_path_buf()]; - if let Some(path) = state.get_var("PATH") { - for folder in path.split(if cfg!(windows) { ';' } else { ':' }) { - search_dirs.push(PathBuf::from(folder)); + + // Special handling to use the current executable for deno. + // This is to ensure deno tasks that use deno work in environments + // that don't have deno on the path and to ensure it use the current + // version of deno being executed rather than the one on the path, + // which has caused some confusion. + if command_name == "deno" { + if let Ok(exe_path) = current_exe() { + // this condition exists to make the tests pass because it's not + // using the deno as the current executable + let file_stem = exe_path.file_stem().map(|s| s.to_string_lossy()); + if file_stem.map(|s| s.to_string()) == Some("deno".to_string()) { + return Ok(exe_path); + } + } } - } - let path_exts = if cfg!(windows) { - let uc_command_name = command_name.to_uppercase(); - let path_ext = state - .get_var("PATHEXT") - .map(|s| s.as_str()) - .unwrap_or(".EXE;.CMD;.BAT;.COM"); - let command_exts = path_ext - .split(';') - .map(|s| s.trim().to_uppercase()) - .filter(|s| !s.is_empty()) - .collect::>(); - if command_exts.is_empty() - || command_exts - .iter() - .any(|ext| uc_command_name.ends_with(ext)) + + // check for absolute + if PathBuf::from(command_name).is_absolute() { + return Ok(PathBuf::from(command_name)); + } + + // then relative + if command_name.contains('/') + || (cfg!(windows) && command_name.contains('\\')) { - None // use the command name as-is - } else { - Some(command_exts) + return Ok(base_dir.join(command_name)); + } + + // now search based on the current environment state + let mut search_dirs = vec![base_dir.to_path_buf()]; + if let Some(path) = state.get_var("PATH") { + for folder in path.split(if cfg!(windows) { ';' } else { ':' }) { + search_dirs.push(PathBuf::from(folder)); + } } - } else { - None - }; - - for search_dir in search_dirs { - let paths = if let Some(path_exts) = &path_exts { - let mut paths = Vec::new(); - for path_ext in path_exts { - paths.push(search_dir.join(format!("{command_name}{path_ext}"))) - } - paths + let path_exts = if cfg!(windows) { + let uc_command_name = command_name.to_uppercase(); + let path_ext = state + .get_var("PATHEXT") + .map(|s| s.as_str()) + .unwrap_or(".EXE;.CMD;.BAT;.COM"); + let command_exts = path_ext + .split(';') + .map(|s| s.trim().to_uppercase()) + .filter(|s| !s.is_empty()) + .collect::>(); + if command_exts.is_empty() + || command_exts + .iter() + .any(|ext| uc_command_name.ends_with(ext)) + { + None // use the command name as-is + } else { + Some(command_exts) + } } else { - vec![search_dir.join(command_name)] + None }; - for path in paths { - // don't use tokio::fs::metadata here as it was never returning - // in some circumstances for some reason - if let Ok(metadata) = std::fs::metadata(&path) { - if metadata.is_file() { - return Ok(path); + + for search_dir in search_dirs { + let paths = if let Some(path_exts) = &path_exts { + let mut paths = Vec::new(); + for path_ext in path_exts { + paths.push(search_dir.join(format!("{command_name}{path_ext}"))) + } + paths + } else { + vec![search_dir.join(command_name)] + }; + for path in paths { + // don't use tokio::fs::metadata here as it was never returning + // in some circumstances for some reason + if let Ok(metadata) = std::fs::metadata(&path) { + if metadata.is_file() { + return Ok(path); + } + } } - } } - } - Err(ResolveCommandPathError::CommandNotFound( - command_name.to_string(), - )) + Err(ResolveCommandPathError::CommandNotFound( + command_name.to_string(), + )) } struct Shebang { - string_split: bool, - command: String, + string_split: bool, + command: String, } fn resolve_shebang( - file_path: &Path, + file_path: &Path, ) -> Result, std::io::Error> { - let mut file = match std::fs::File::open(file_path) { - Ok(file) => file, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Ok(None); - } - Err(err) => return Err(err), - }; - let text = b"#!/usr/bin/env "; - let mut buffer = vec![0; text.len()]; - match file.read_exact(&mut buffer) { - Ok(_) if buffer == text => (), - _ => return Ok(None), - } - - let mut reader = BufReader::new(file); - let mut line = String::new(); - reader.read_line(&mut line)?; - if line.is_empty() { - return Ok(None); - } - let line = line.trim(); - - Ok(Some(if let Some(command) = line.strip_prefix("-S ") { - Shebang { - string_split: true, - command: command.to_string(), + let mut file = match std::fs::File::open(file_path) { + Ok(file) => file, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Ok(None); + } + Err(err) => return Err(err), + }; + let text = b"#!/usr/bin/env "; + let mut buffer = vec![0; text.len()]; + match file.read_exact(&mut buffer) { + Ok(_) if buffer == text => (), + _ => return Ok(None), } - } else { - Shebang { - string_split: false, - command: line.to_string(), + + let mut reader = BufReader::new(file); + let mut line = String::new(); + reader.read_line(&mut line)?; + if line.is_empty() { + return Ok(None); } - })) + let line = line.trim(); + + Ok(Some(if let Some(command) = line.strip_prefix("-S ") { + Shebang { + string_split: true, + command: command.to_string(), + } + } else { + Shebang { + string_split: false, + command: line.to_string(), + } + })) } #[cfg(test)] mod local_test { - use super::*; - - #[test] - fn should_resolve_current_exe_path_for_deno() { - let cwd = std::env::current_dir().unwrap(); - let state = ShellState::new( - Default::default(), - &std::env::current_dir().unwrap(), - Default::default(), - ); - let path = resolve_command_path_inner("deno", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno")) - }) - .unwrap(); - assert_eq!(path, PathBuf::from("/bin/deno")); + use super::*; + + #[test] + fn should_resolve_current_exe_path_for_deno() { + let cwd = std::env::current_dir().unwrap(); + let state = ShellState::new( + Default::default(), + &std::env::current_dir().unwrap(), + Default::default(), + ); + let path = resolve_command_path_inner("deno", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno")) + }) + .unwrap(); + assert_eq!(path, PathBuf::from("/bin/deno")); + + let path = resolve_command_path_inner("deno", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno.exe")) + }) + .unwrap(); + assert_eq!(path, PathBuf::from("/bin/deno.exe")); + } - let path = resolve_command_path_inner("deno", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno.exe")) - }) - .unwrap(); - assert_eq!(path, PathBuf::from("/bin/deno.exe")); - } - - #[test] - fn should_error_on_unknown_command() { - let cwd = std::env::current_dir().unwrap(); - let state = ShellState::new(Default::default(), &cwd, Default::default()); - // Command not found - let result = resolve_command_path_inner("foobar", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno")) - }); - assert_eq!( - result, - Err(ResolveCommandPathError::CommandNotFound( - "foobar".to_string() - )) - ); - // Command empty - let result = resolve_command_path_inner("", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno")) - }); - assert_eq!(result, Err(ResolveCommandPathError::CommandEmpty)); - } + #[test] + fn should_error_on_unknown_command() { + let cwd = std::env::current_dir().unwrap(); + let state = + ShellState::new(Default::default(), &cwd, Default::default()); + // Command not found + let result = resolve_command_path_inner("foobar", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno")) + }); + assert_eq!( + result, + Err(ResolveCommandPathError::CommandNotFound( + "foobar".to_string() + )) + ); + // Command empty + let result = resolve_command_path_inner("", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno")) + }); + assert_eq!(result, Err(ResolveCommandPathError::CommandEmpty)); + } } diff --git a/crates/deno_task_shell/src/shell/commands/args.rs b/crates/deno_task_shell/src/shell/commands/args.rs index d308500..2679150 100644 --- a/crates/deno_task_shell/src/shell/commands/args.rs +++ b/crates/deno_task_shell/src/shell/commands/args.rs @@ -5,88 +5,88 @@ use anyhow::Result; #[derive(Debug, PartialEq, Eq)] pub enum ArgKind<'a> { - ShortFlag(char), - LongFlag(&'a str), - Arg(&'a str), + ShortFlag(char), + LongFlag(&'a str), + Arg(&'a str), } impl<'a> ArgKind<'a> { - pub fn bail_unsupported(&self) -> Result<()> { - match self { - ArgKind::Arg(arg) => { - bail!("unsupported argument: {}", arg) - } - ArgKind::LongFlag(name) => { - bail!("unsupported flag: --{}", name) - } - ArgKind::ShortFlag(name) => { - bail!("unsupported flag: -{}", name) - } + pub fn bail_unsupported(&self) -> Result<()> { + match self { + ArgKind::Arg(arg) => { + bail!("unsupported argument: {}", arg) + } + ArgKind::LongFlag(name) => { + bail!("unsupported flag: --{}", name) + } + ArgKind::ShortFlag(name) => { + bail!("unsupported flag: -{}", name) + } + } } - } } pub fn parse_arg_kinds(flags: &[String]) -> Vec { - let mut result = Vec::new(); - let mut had_dash_dash = false; - for arg in flags { - if had_dash_dash { - result.push(ArgKind::Arg(arg)); - } else if arg == "-" { - result.push(ArgKind::Arg("-")); - } else if arg == "--" { - had_dash_dash = true; - } else if let Some(flag) = arg.strip_prefix("--") { - result.push(ArgKind::LongFlag(flag)); - } else if let Some(flags) = arg.strip_prefix('-') { - if flags.parse::().is_ok() { - result.push(ArgKind::Arg(arg)); - } else { - for c in flags.chars() { - result.push(ArgKind::ShortFlag(c)); + let mut result = Vec::new(); + let mut had_dash_dash = false; + for arg in flags { + if had_dash_dash { + result.push(ArgKind::Arg(arg)); + } else if arg == "-" { + result.push(ArgKind::Arg("-")); + } else if arg == "--" { + had_dash_dash = true; + } else if let Some(flag) = arg.strip_prefix("--") { + result.push(ArgKind::LongFlag(flag)); + } else if let Some(flags) = arg.strip_prefix('-') { + if flags.parse::().is_ok() { + result.push(ArgKind::Arg(arg)); + } else { + for c in flags.chars() { + result.push(ArgKind::ShortFlag(c)); + } + } + } else { + result.push(ArgKind::Arg(arg)); } - } - } else { - result.push(ArgKind::Arg(arg)); } - } - result + result } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses() { - let data = vec![ - "-f".to_string(), - "-ab".to_string(), - "--force".to_string(), - "testing".to_string(), - "other".to_string(), - "-1".to_string(), - "-6.4".to_string(), - "--".to_string(), - "--test".to_string(), - "-t".to_string(), - ]; - let args = parse_arg_kinds(&data); - assert_eq!( - args, - vec![ - ArgKind::ShortFlag('f'), - ArgKind::ShortFlag('a'), - ArgKind::ShortFlag('b'), - ArgKind::LongFlag("force"), - ArgKind::Arg("testing"), - ArgKind::Arg("other"), - ArgKind::Arg("-1"), - ArgKind::Arg("-6.4"), - ArgKind::Arg("--test"), - ArgKind::Arg("-t"), - ] - ) - } + #[test] + fn parses() { + let data = vec![ + "-f".to_string(), + "-ab".to_string(), + "--force".to_string(), + "testing".to_string(), + "other".to_string(), + "-1".to_string(), + "-6.4".to_string(), + "--".to_string(), + "--test".to_string(), + "-t".to_string(), + ]; + let args = parse_arg_kinds(&data); + assert_eq!( + args, + vec![ + ArgKind::ShortFlag('f'), + ArgKind::ShortFlag('a'), + ArgKind::ShortFlag('b'), + ArgKind::LongFlag("force"), + ArgKind::Arg("testing"), + ArgKind::Arg("other"), + ArgKind::Arg("-1"), + ArgKind::Arg("-6.4"), + ArgKind::Arg("--test"), + ArgKind::Arg("-t"), + ] + ) + } } diff --git a/crates/deno_task_shell/src/shell/commands/cat.rs b/crates/deno_task_shell/src/shell/commands/cat.rs index 674fe33..b233c49 100644 --- a/crates/deno_task_shell/src/shell/commands/cat.rs +++ b/crates/deno_task_shell/src/shell/commands/cat.rs @@ -15,127 +15,130 @@ use super::ShellCommandContext; pub struct CatCommand; impl ShellCommand for CatCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let mut stderr = context.stderr.clone(); - let result = match execute_cat(context) { - Ok(result) => result, - Err(err) => { - let _ = stderr.write_line(&format!("cat: {err}")); - ExecuteResult::from_exit_code(1) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let mut stderr = context.stderr.clone(); + let result = match execute_cat(context) { + Ok(result) => result, + Err(err) => { + let _ = stderr.write_line(&format!("cat: {err}")); + ExecuteResult::from_exit_code(1) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_cat(mut context: ShellCommandContext) -> Result { - let flags = parse_args(context.args)?; - let mut exit_code = 0; - let mut buf = vec![0; 1024]; - for path in flags.paths { - if path == "-" { - context - .stdin - .clone() - .pipe_to_sender(context.stdout.clone())?; - } else { - // buffered to prevent reading an entire file - // in memory - match File::open(context.state.cwd().join(&path)) { - Ok(mut file) => loop { - if context.state.token().is_cancelled() { - return Ok(ExecuteResult::for_cancellation()); - } + let flags = parse_args(context.args)?; + let mut exit_code = 0; + let mut buf = vec![0; 1024]; + for path in flags.paths { + if path == "-" { + context + .stdin + .clone() + .pipe_to_sender(context.stdout.clone())?; + } else { + // buffered to prevent reading an entire file + // in memory + match File::open(context.state.cwd().join(&path)) { + Ok(mut file) => loop { + if context.state.token().is_cancelled() { + return Ok(ExecuteResult::for_cancellation()); + } - let size = file.read(&mut buf)?; - if size == 0 { - break; - } else { - context.stdout.write_all(&buf[..size])?; - } - }, - Err(err) => { - context.stderr.write_line(&format!("cat: {path}: {err}"))?; - exit_code = 1; + let size = file.read(&mut buf)?; + if size == 0 { + break; + } else { + context.stdout.write_all(&buf[..size])?; + } + }, + Err(err) => { + context + .stderr + .write_line(&format!("cat: {path}: {err}"))?; + exit_code = 1; + } + } } - } } - } - Ok(ExecuteResult::from_exit_code(exit_code)) + Ok(ExecuteResult::from_exit_code(exit_code)) } #[derive(Debug, PartialEq)] struct CatFlags { - paths: Vec, + paths: Vec, } fn parse_args(args: Vec) -> Result { - let mut paths = Vec::new(); - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(file_name) => { - paths.push(file_name.to_string()); - } - // for now, we don't support any arguments - _ => arg.bail_unsupported()?, + let mut paths = Vec::new(); + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(file_name) => { + paths.push(file_name.to_string()); + } + // for now, we don't support any arguments + _ => arg.bail_unsupported()?, + } } - } - if paths.is_empty() { - paths.push("-".to_string()); - } + if paths.is_empty() { + paths.push("-".to_string()); + } - Ok(CatFlags { paths }) + Ok(CatFlags { paths }) } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![]).unwrap(), - CatFlags { - paths: vec!["-".to_string()] - } - ); - assert_eq!( - parse_args(vec!["path".to_string()]).unwrap(), - CatFlags { - paths: vec!["path".to_string()] - } - ); - assert_eq!( - parse_args(vec!["path".to_string(), "-".to_string()]).unwrap(), - CatFlags { - paths: vec!["path".to_string(), "-".to_string()] - } - ); - assert_eq!( - parse_args(vec!["path".to_string(), "other-path".to_string()]).unwrap(), - CatFlags { - paths: vec!["path".to_string(), "other-path".to_string()] - } - ); - assert_eq!( - parse_args(vec!["--flag".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --flag" - ); - assert_eq!( - parse_args(vec!["-t".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t" - ); - } + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![]).unwrap(), + CatFlags { + paths: vec!["-".to_string()] + } + ); + assert_eq!( + parse_args(vec!["path".to_string()]).unwrap(), + CatFlags { + paths: vec!["path".to_string()] + } + ); + assert_eq!( + parse_args(vec!["path".to_string(), "-".to_string()]).unwrap(), + CatFlags { + paths: vec!["path".to_string(), "-".to_string()] + } + ); + assert_eq!( + parse_args(vec!["path".to_string(), "other-path".to_string()]) + .unwrap(), + CatFlags { + paths: vec!["path".to_string(), "other-path".to_string()] + } + ); + assert_eq!( + parse_args(vec!["--flag".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --flag" + ); + assert_eq!( + parse_args(vec!["-t".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/cd.rs b/crates/deno_task_shell/src/shell/commands/cd.rs index 54ae007..39a14ac 100644 --- a/crates/deno_task_shell/src/shell/commands/cd.rs +++ b/crates/deno_task_shell/src/shell/commands/cd.rs @@ -20,127 +20,129 @@ use super::ShellCommandContext; pub struct CdCommand; impl ShellCommand for CdCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match execute_cd(context.state.cwd(), context.args) { - Ok(new_dir) => { - ExecuteResult::Continue(0, vec![EnvChange::Cd(new_dir)], Vec::new()) - } - Err(err) => { - let _ = context.stderr.write_line(&format!("cd: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_cd(context.state.cwd(), context.args) { + Ok(new_dir) => ExecuteResult::Continue( + 0, + vec![EnvChange::Cd(new_dir)], + Vec::new(), + ), + Err(err) => { + let _ = context.stderr.write_line(&format!("cd: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_cd(cwd: &Path, args: Vec) -> Result { - let path = parse_args(args)?; - let new_dir = cwd.join(&path); - let new_dir = match new_dir.parse_dot() { - Ok(path) => path.to_path_buf(), - // fallback to canonicalize path just in case - Err(_) => fs_util::canonicalize_path(&new_dir)?, - }; - if !new_dir.is_dir() { - bail!("{}: Not a directory", path) - } - Ok(new_dir) + let path = parse_args(args)?; + let new_dir = cwd.join(&path); + let new_dir = match new_dir.parse_dot() { + Ok(path) => path.to_path_buf(), + // fallback to canonicalize path just in case + Err(_) => fs_util::canonicalize_path(&new_dir)?, + }; + if !new_dir.is_dir() { + bail!("{}: Not a directory", path) + } + Ok(new_dir) } fn parse_args(args: Vec) -> Result { - let args = parse_arg_kinds(&args); - let mut paths = Vec::new(); - for arg in args { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - _ => arg.bail_unsupported()?, + let args = parse_arg_kinds(&args); + let mut paths = Vec::new(); + for arg in args { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, + } } - } - if paths.len() > 1 { - bail!("too many arguments") - } else if paths.is_empty() { - // not the case in actual cd, but it is most likely - // an error if someone does this in deno task - bail!("expected at least 1 argument") - } + if paths.len() > 1 { + bail!("too many arguments") + } else if paths.is_empty() { + // not the case in actual cd, but it is most likely + // an error if someone does this in deno task + bail!("expected at least 1 argument") + } - Ok(paths.remove(0).to_string()) + Ok(paths.remove(0).to_string()) } #[cfg(test)] mod test { - use std::fs; - use tempfile::tempdir; - - use super::*; - - #[test] - fn parses_args() { - assert_eq!(parse_args(vec!["test".to_string()]).unwrap(), "test"); - assert_eq!( - parse_args(vec!["a".to_string(), "b".to_string()]) - .err() - .unwrap() - .to_string(), - "too many arguments" - ); - assert_eq!( - parse_args(vec![]).err().unwrap().to_string(), - "expected at least 1 argument" - ); - assert_eq!( - parse_args(vec!["-a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -a" - ); - assert_eq!( - parse_args(vec!["--a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --a" - ); - } - - #[test] - fn gets_new_cd() { - let dir = tempdir().unwrap(); - let dir_path = fs_util::canonicalize_path(dir.path()).unwrap(); - - // non-existent - assert_eq!( - execute_cd(&dir_path, vec!["non-existent".to_string()]) - .err() - .unwrap() - .to_string(), - "non-existent: Not a directory" - ); - - // existent file - fs::write(dir_path.join("file.txt"), "").unwrap(); - assert_eq!( - execute_cd(&dir_path, vec!["file.txt".to_string()]) - .err() - .unwrap() - .to_string(), - "file.txt: Not a directory" - ); - - // existent dir - let sub_dir_path = dir_path.join("sub_dir"); - fs::create_dir(&sub_dir_path).unwrap(); - assert_eq!( - execute_cd(&dir_path, vec!["sub_dir".to_string()]).unwrap(), - sub_dir_path - ); - } + use std::fs; + use tempfile::tempdir; + + use super::*; + + #[test] + fn parses_args() { + assert_eq!(parse_args(vec!["test".to_string()]).unwrap(), "test"); + assert_eq!( + parse_args(vec!["a".to_string(), "b".to_string()]) + .err() + .unwrap() + .to_string(), + "too many arguments" + ); + assert_eq!( + parse_args(vec![]).err().unwrap().to_string(), + "expected at least 1 argument" + ); + assert_eq!( + parse_args(vec!["-a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -a" + ); + assert_eq!( + parse_args(vec!["--a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --a" + ); + } + + #[test] + fn gets_new_cd() { + let dir = tempdir().unwrap(); + let dir_path = fs_util::canonicalize_path(dir.path()).unwrap(); + + // non-existent + assert_eq!( + execute_cd(&dir_path, vec!["non-existent".to_string()]) + .err() + .unwrap() + .to_string(), + "non-existent: Not a directory" + ); + + // existent file + fs::write(dir_path.join("file.txt"), "").unwrap(); + assert_eq!( + execute_cd(&dir_path, vec!["file.txt".to_string()]) + .err() + .unwrap() + .to_string(), + "file.txt: Not a directory" + ); + + // existent dir + let sub_dir_path = dir_path.join("sub_dir"); + fs::create_dir(&sub_dir_path).unwrap(); + assert_eq!( + execute_cd(&dir_path, vec!["sub_dir".to_string()]).unwrap(), + sub_dir_path + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/cp_mv.rs b/crates/deno_task_shell/src/shell/commands/cp_mv.rs index 5647cfb..34d8456 100644 --- a/crates/deno_task_shell/src/shell/commands/cp_mv.rs +++ b/crates/deno_task_shell/src/shell/commands/cp_mv.rs @@ -22,466 +22,479 @@ use super::ShellCommandContext; pub struct CpCommand; impl ShellCommand for CpCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - cp_command(context.state.cwd(), context.args, context.stderr), - context.state.token() - ) + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + cp_command(context.state.cwd(), context.args, context.stderr), + context.state.token() + ) + } + .boxed_local() } - .boxed_local() - } } async fn cp_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_cp(cwd, args).await { - Ok(()) => ExecuteResult::from_exit_code(0), - Err(err) => { - let _ = stderr.write_line(&format!("cp: {err}")); - ExecuteResult::from_exit_code(1) + match execute_cp(cwd, args).await { + Ok(()) => ExecuteResult::from_exit_code(0), + Err(err) => { + let _ = stderr.write_line(&format!("cp: {err}")); + ExecuteResult::from_exit_code(1) + } } - } } async fn execute_cp(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_cp_args(cwd, args)?; - for (from, to) in &flags.operations { - if let Err(err) = do_copy_operation(&flags, from, to).await { - bail!( - "could not copy {} to {}: {}", - from.specified, - to.specified, - err - ); + let flags = parse_cp_args(cwd, args)?; + for (from, to) in &flags.operations { + if let Err(err) = do_copy_operation(&flags, from, to).await { + bail!( + "could not copy {} to {}: {}", + from.specified, + to.specified, + err + ); + } } - } - Ok(()) + Ok(()) } async fn do_copy_operation( - flags: &CpFlags, - from: &PathWithSpecified, - to: &PathWithSpecified, + flags: &CpFlags, + from: &PathWithSpecified, + to: &PathWithSpecified, ) -> Result<()> { - // These are racy with the file system, but that's ok. - // They only exists to give better error messages. - if from.path.is_dir() { - if flags.recursive { - if to.path.exists() && to.path.is_file() { - bail!("destination was a file"); - } else if to.path.is_symlink() { - bail!("no support for copying to symlinks") - } else if from.path.is_symlink() { - bail!("no support for copying from symlinks") - } else { - copy_dir_recursively(from.path.clone(), to.path.clone()).await?; - } + // These are racy with the file system, but that's ok. + // They only exists to give better error messages. + if from.path.is_dir() { + if flags.recursive { + if to.path.exists() && to.path.is_file() { + bail!("destination was a file"); + } else if to.path.is_symlink() { + bail!("no support for copying to symlinks") + } else if from.path.is_symlink() { + bail!("no support for copying from symlinks") + } else { + copy_dir_recursively(from.path.clone(), to.path.clone()) + .await?; + } + } else { + bail!("source was a directory; maybe specify -r") + } } else { - bail!("source was a directory; maybe specify -r") + tokio::fs::copy(&from.path, &to.path).await?; } - } else { - tokio::fs::copy(&from.path, &to.path).await?; - } - Ok(()) + Ok(()) } fn copy_dir_recursively( - from: PathBuf, - to: PathBuf, + from: PathBuf, + to: PathBuf, ) -> BoxFuture<'static, Result<()>> { - // recursive, so box it - async move { - tokio::fs::create_dir_all(&to) - .await - .with_context(|| format!("Creating {}", to.display()))?; - let mut read_dir = tokio::fs::read_dir(&from) - .await - .with_context(|| format!("Reading {}", from.display()))?; - - while let Some(entry) = read_dir.next_entry().await? { - let file_type = entry.file_type().await?; - let new_from = from.join(entry.file_name()); - let new_to = to.join(entry.file_name()); - - if file_type.is_dir() { - copy_dir_recursively(new_from.clone(), new_to.clone()) - .await - .with_context(|| { - format!("Dir {} to {}", new_from.display(), new_to.display()) - })?; - } else if file_type.is_file() { - tokio::fs::copy(&new_from, &new_to).await.with_context(|| { - format!("Copying {} to {}", new_from.display(), new_to.display()) - })?; - } + // recursive, so box it + async move { + tokio::fs::create_dir_all(&to) + .await + .with_context(|| format!("Creating {}", to.display()))?; + let mut read_dir = tokio::fs::read_dir(&from) + .await + .with_context(|| format!("Reading {}", from.display()))?; + + while let Some(entry) = read_dir.next_entry().await? { + let file_type = entry.file_type().await?; + let new_from = from.join(entry.file_name()); + let new_to = to.join(entry.file_name()); + + if file_type.is_dir() { + copy_dir_recursively(new_from.clone(), new_to.clone()) + .await + .with_context(|| { + format!( + "Dir {} to {}", + new_from.display(), + new_to.display() + ) + })?; + } else if file_type.is_file() { + tokio::fs::copy(&new_from, &new_to).await.with_context( + || { + format!( + "Copying {} to {}", + new_from.display(), + new_to.display() + ) + }, + )?; + } + } + + Ok(()) } - - Ok(()) - } - .boxed() + .boxed() } struct CpFlags { - recursive: bool, - operations: Vec<(PathWithSpecified, PathWithSpecified)>, + recursive: bool, + operations: Vec<(PathWithSpecified, PathWithSpecified)>, } fn parse_cp_args(cwd: &Path, args: Vec) -> Result { - let mut paths = Vec::new(); - let mut recursive = false; - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - ArgKind::LongFlag("recursive") - | ArgKind::ShortFlag('r') - | ArgKind::ShortFlag('R') => { - recursive = true; - } - _ => arg.bail_unsupported()?, + let mut paths = Vec::new(); + let mut recursive = false; + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + ArgKind::LongFlag("recursive") + | ArgKind::ShortFlag('r') + | ArgKind::ShortFlag('R') => { + recursive = true; + } + _ => arg.bail_unsupported()?, + } + } + if paths.is_empty() { + bail!("missing file operand"); + } else if paths.len() == 1 { + bail!("missing destination file operand after '{}'", paths[0]); } - } - if paths.is_empty() { - bail!("missing file operand"); - } else if paths.len() == 1 { - bail!("missing destination file operand after '{}'", paths[0]); - } - - Ok(CpFlags { - recursive, - operations: get_copy_and_move_operations(cwd, paths)?, - }) + + Ok(CpFlags { + recursive, + operations: get_copy_and_move_operations(cwd, paths)?, + }) } pub struct MvCommand; impl ShellCommand for MvCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - mv_command(context.state.cwd(), context.args, context.stderr), - context.state.token() - ) + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + mv_command(context.state.cwd(), context.args, context.stderr), + context.state.token() + ) + } + .boxed_local() } - .boxed_local() - } } async fn mv_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_mv(cwd, args).await { - Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), - Err(err) => { - let _ = stderr.write_line(&format!("mv: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) + match execute_mv(cwd, args).await { + Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), + Err(err) => { + let _ = stderr.write_line(&format!("mv: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } } - } } async fn execute_mv(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_mv_args(cwd, args)?; - for (from, to) in flags.operations { - if let Err(err) = tokio::fs::rename(&from.path, &to.path).await { - bail!( - "could not move {} to {}: {}", - from.specified, - to.specified, - err - ); + let flags = parse_mv_args(cwd, args)?; + for (from, to) in flags.operations { + if let Err(err) = tokio::fs::rename(&from.path, &to.path).await { + bail!( + "could not move {} to {}: {}", + from.specified, + to.specified, + err + ); + } } - } - Ok(()) + Ok(()) } struct MvFlags { - operations: Vec<(PathWithSpecified, PathWithSpecified)>, + operations: Vec<(PathWithSpecified, PathWithSpecified)>, } fn parse_mv_args(cwd: &Path, args: Vec) -> Result { - let mut paths = Vec::new(); - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - _ => arg.bail_unsupported()?, + let mut paths = Vec::new(); + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, + } } - } - if paths.is_empty() { - bail!("missing file operand"); - } else if paths.len() == 1 { - bail!("missing destination file operand after '{}'", paths[0]); - } - - Ok(MvFlags { - operations: get_copy_and_move_operations(cwd, paths)?, - }) + if paths.is_empty() { + bail!("missing file operand"); + } else if paths.len() == 1 { + bail!("missing destination file operand after '{}'", paths[0]); + } + + Ok(MvFlags { + operations: get_copy_and_move_operations(cwd, paths)?, + }) } struct PathWithSpecified { - path: PathBuf, - specified: String, + path: PathBuf, + specified: String, } fn get_copy_and_move_operations( - cwd: &Path, - mut paths: Vec<&str>, + cwd: &Path, + mut paths: Vec<&str>, ) -> Result> { - // copy and move share the same logic - let specified_destination = paths.pop().unwrap(); - let destination = cwd.join(specified_destination); - let from_args = paths; - let mut operations = Vec::new(); - if from_args.len() > 1 { - if !destination.is_dir() { - bail!("target '{}' is not a directory", specified_destination); - } - for from in from_args { - let from_path = cwd.join(from); - let to_path = destination.join(from_path.file_name().unwrap()); - operations.push(( - PathWithSpecified { - specified: from.to_string(), - path: from_path, - }, - PathWithSpecified { - specified: specified_destination.to_string(), - path: to_path, - }, - )); - } - } else { - let from_path = cwd.join(from_args[0]); - let to_path = if destination.is_dir() { - destination.join(from_path.file_name().unwrap()) + // copy and move share the same logic + let specified_destination = paths.pop().unwrap(); + let destination = cwd.join(specified_destination); + let from_args = paths; + let mut operations = Vec::new(); + if from_args.len() > 1 { + if !destination.is_dir() { + bail!("target '{}' is not a directory", specified_destination); + } + for from in from_args { + let from_path = cwd.join(from); + let to_path = destination.join(from_path.file_name().unwrap()); + operations.push(( + PathWithSpecified { + specified: from.to_string(), + path: from_path, + }, + PathWithSpecified { + specified: specified_destination.to_string(), + path: to_path, + }, + )); + } } else { - destination - }; - operations.push(( - PathWithSpecified { - specified: from_args[0].to_string(), - path: from_path, - }, - PathWithSpecified { - specified: specified_destination.to_string(), - path: to_path, - }, - )); - } - Ok(operations) + let from_path = cwd.join(from_args[0]); + let to_path = if destination.is_dir() { + destination.join(from_path.file_name().unwrap()) + } else { + destination + }; + operations.push(( + PathWithSpecified { + specified: from_args[0].to_string(), + path: from_path, + }, + PathWithSpecified { + specified: specified_destination.to_string(), + path: to_path, + }, + )); + } + Ok(operations) } #[cfg(test)] mod test { - use tempfile::tempdir; - - use super::*; - use std::fs; - - #[tokio::test] - async fn should_copy() { - let dir = tempdir().unwrap(); - let file1 = dir.path().join("file1.txt"); - let file2 = dir.path().join("file2.txt"); - fs::write(&file1, "test").unwrap(); - execute_cp( - dir.path(), - vec!["file1.txt".to_string(), "file2.txt".to_string()], - ) - .await - .unwrap(); - assert!(file1.exists()); - assert!(file2.exists()); - - let dest_dir = dir.path().join("dest"); - fs::create_dir(&dest_dir).unwrap(); - execute_cp( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "dest".to_string(), - ], - ) - .await - .unwrap(); - assert!(file1.exists()); - assert!(file2.exists()); - assert!(dest_dir.join("file1.txt").exists()); - assert!(dest_dir.join("file2.txt").exists()); - - let new_file = dir.path().join("new.txt"); - fs::write(&new_file, "test").unwrap(); - execute_cp(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) - .await - .unwrap(); - assert!(dest_dir.is_dir()); - assert!(new_file.exists()); - assert!(dest_dir.join("new.txt").exists()); - - let result = execute_cp( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "non-existent".to_string(), - ], - ) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "target 'non-existent' is not a directory" - ); - - let result = execute_cp(dir.path(), vec![]).await.err().unwrap(); - assert_eq!(result.to_string(), "missing file operand"); - - let result = execute_cp(dir.path(), vec!["file1.txt".to_string()]) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "missing destination file operand after 'file1.txt'" - ); - - // test recursive flag - fs::create_dir_all(dest_dir.join("sub_dir")).unwrap(); - fs::write(dest_dir.join("sub_dir").join("sub.txt"), "test").unwrap(); - let dest_dir2 = dir.path().join("dest2"); - - let result = - execute_cp(dir.path(), vec!["dest".to_string(), "dest2".to_string()]) + use tempfile::tempdir; + + use super::*; + use std::fs; + + #[tokio::test] + async fn should_copy() { + let dir = tempdir().unwrap(); + let file1 = dir.path().join("file1.txt"); + let file2 = dir.path().join("file2.txt"); + fs::write(&file1, "test").unwrap(); + execute_cp( + dir.path(), + vec!["file1.txt".to_string(), "file2.txt".to_string()], + ) + .await + .unwrap(); + assert!(file1.exists()); + assert!(file2.exists()); + + let dest_dir = dir.path().join("dest"); + fs::create_dir(&dest_dir).unwrap(); + execute_cp( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "dest".to_string(), + ], + ) + .await + .unwrap(); + assert!(file1.exists()); + assert!(file2.exists()); + assert!(dest_dir.join("file1.txt").exists()); + assert!(dest_dir.join("file2.txt").exists()); + + let new_file = dir.path().join("new.txt"); + fs::write(&new_file, "test").unwrap(); + execute_cp(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) + .await + .unwrap(); + assert!(dest_dir.is_dir()); + assert!(new_file.exists()); + assert!(dest_dir.join("new.txt").exists()); + + let result = execute_cp( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "non-existent".to_string(), + ], + ) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "target 'non-existent' is not a directory" + ); + + let result = execute_cp(dir.path(), vec![]).await.err().unwrap(); + assert_eq!(result.to_string(), "missing file operand"); + + let result = execute_cp(dir.path(), vec!["file1.txt".to_string()]) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "missing destination file operand after 'file1.txt'" + ); + + // test recursive flag + fs::create_dir_all(dest_dir.join("sub_dir")).unwrap(); + fs::write(dest_dir.join("sub_dir").join("sub.txt"), "test").unwrap(); + let dest_dir2 = dir.path().join("dest2"); + + let result = execute_cp( + dir.path(), + vec!["dest".to_string(), "dest2".to_string()], + ) .await .err() .unwrap(); - assert_eq!( + assert_eq!( result.to_string(), "could not copy dest to dest2: source was a directory; maybe specify -r" ); - assert!(!dest_dir2.exists()); - - execute_cp( - dir.path(), - vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], - ) - .await - .unwrap(); - assert!(dest_dir2.exists()); - assert!(dest_dir2.join("file1.txt").exists()); - assert!(dest_dir2.join("file2.txt").exists()); - assert!(dest_dir2.join("sub_dir").join("sub.txt").exists()); - - // copy again - execute_cp( - dir.path(), - vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], - ) - .await - .unwrap(); - - // try copying to a file - let result = execute_cp( - dir.path(), - vec![ - "-r".to_string(), - "dest".to_string(), - "dest2/file1.txt".to_string(), - ], - ) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "could not copy dest to dest2/file1.txt: destination was a file" - ) - } - - #[tokio::test] - async fn should_move() { - let dir = tempdir().unwrap(); - let file1 = dir.path().join("file1.txt"); - let file2 = dir.path().join("file2.txt"); - fs::write(&file1, "test").unwrap(); - execute_mv( - dir.path(), - vec!["file1.txt".to_string(), "file2.txt".to_string()], - ) - .await - .unwrap(); - assert!(!file1.exists()); - assert!(file2.exists()); - - let dest_dir = dir.path().join("dest"); - fs::write(&file1, "test").unwrap(); // recreate - fs::create_dir(&dest_dir).unwrap(); - execute_mv( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "dest".to_string(), - ], - ) - .await - .unwrap(); - assert!(!file1.exists()); - assert!(!file2.exists()); - assert!(dest_dir.join("file1.txt").exists()); - assert!(dest_dir.join("file2.txt").exists()); - - let new_file = dir.path().join("new.txt"); - fs::write(&new_file, "test").unwrap(); - execute_mv(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) - .await - .unwrap(); - assert!(dest_dir.is_dir()); - assert!(!new_file.exists()); - assert!(dest_dir.join("new.txt").exists()); - - let result = execute_mv( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "non-existent".to_string(), - ], - ) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "target 'non-existent' is not a directory" - ); + assert!(!dest_dir2.exists()); - let result = execute_mv(dir.path(), vec![]).await.err().unwrap(); - assert_eq!(result.to_string(), "missing file operand"); + execute_cp( + dir.path(), + vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], + ) + .await + .unwrap(); + assert!(dest_dir2.exists()); + assert!(dest_dir2.join("file1.txt").exists()); + assert!(dest_dir2.join("file2.txt").exists()); + assert!(dest_dir2.join("sub_dir").join("sub.txt").exists()); + + // copy again + execute_cp( + dir.path(), + vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], + ) + .await + .unwrap(); - let result = execute_mv(dir.path(), vec!["file1.txt".to_string()]) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "missing destination file operand after 'file1.txt'" - ); - } + // try copying to a file + let result = execute_cp( + dir.path(), + vec![ + "-r".to_string(), + "dest".to_string(), + "dest2/file1.txt".to_string(), + ], + ) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "could not copy dest to dest2/file1.txt: destination was a file" + ) + } + + #[tokio::test] + async fn should_move() { + let dir = tempdir().unwrap(); + let file1 = dir.path().join("file1.txt"); + let file2 = dir.path().join("file2.txt"); + fs::write(&file1, "test").unwrap(); + execute_mv( + dir.path(), + vec!["file1.txt".to_string(), "file2.txt".to_string()], + ) + .await + .unwrap(); + assert!(!file1.exists()); + assert!(file2.exists()); + + let dest_dir = dir.path().join("dest"); + fs::write(&file1, "test").unwrap(); // recreate + fs::create_dir(&dest_dir).unwrap(); + execute_mv( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "dest".to_string(), + ], + ) + .await + .unwrap(); + assert!(!file1.exists()); + assert!(!file2.exists()); + assert!(dest_dir.join("file1.txt").exists()); + assert!(dest_dir.join("file2.txt").exists()); + + let new_file = dir.path().join("new.txt"); + fs::write(&new_file, "test").unwrap(); + execute_mv(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) + .await + .unwrap(); + assert!(dest_dir.is_dir()); + assert!(!new_file.exists()); + assert!(dest_dir.join("new.txt").exists()); + + let result = execute_mv( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "non-existent".to_string(), + ], + ) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "target 'non-existent' is not a directory" + ); + + let result = execute_mv(dir.path(), vec![]).await.err().unwrap(); + assert_eq!(result.to_string(), "missing file operand"); + + let result = execute_mv(dir.path(), vec!["file1.txt".to_string()]) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "missing destination file operand after 'file1.txt'" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/echo.rs b/crates/deno_task_shell/src/shell/commands/echo.rs index ec2f6c5..8625516 100644 --- a/crates/deno_task_shell/src/shell/commands/echo.rs +++ b/crates/deno_task_shell/src/shell/commands/echo.rs @@ -10,11 +10,11 @@ use super::ShellCommandContext; pub struct EchoCommand; impl ShellCommand for EchoCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let _ = context.stdout.write_line(&context.args.join(" ")); - Box::pin(futures::future::ready(ExecuteResult::from_exit_code(0))) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let _ = context.stdout.write_line(&context.args.join(" ")); + Box::pin(futures::future::ready(ExecuteResult::from_exit_code(0))) + } } diff --git a/crates/deno_task_shell/src/shell/commands/executable.rs b/crates/deno_task_shell/src/shell/commands/executable.rs index 7cf2b09..30b6c61 100644 --- a/crates/deno_task_shell/src/shell/commands/executable.rs +++ b/crates/deno_task_shell/src/shell/commands/executable.rs @@ -11,68 +11,68 @@ use futures::FutureExt; /// Command that resolves the command name and /// executes it in a separate process. pub struct ExecutableCommand { - display_name: String, - command_path: PathBuf, + display_name: String, + command_path: PathBuf, } impl ExecutableCommand { - pub fn new(display_name: String, command_path: PathBuf) -> Self { - Self { - display_name, - command_path, + pub fn new(display_name: String, command_path: PathBuf) -> Self { + Self { + display_name, + command_path, + } } - } } impl ShellCommand for ExecutableCommand { - fn execute(&self, context: ShellCommandContext) -> FutureExecuteResult { - let display_name = self.display_name.clone(); - let command_name = self.command_path.clone(); - async move { - let mut stderr = context.stderr; - let mut sub_command = tokio::process::Command::new(&command_name); - let child = sub_command - .current_dir(context.state.cwd()) - .args(context.args) - .env_clear() - .envs(context.state.env_vars()) - .stdout(context.stdout.into_stdio()) - .stdin(context.stdin.into_stdio()) - .stderr(stderr.clone().into_stdio()) - .spawn(); + fn execute(&self, context: ShellCommandContext) -> FutureExecuteResult { + let display_name = self.display_name.clone(); + let command_name = self.command_path.clone(); + async move { + let mut stderr = context.stderr; + let mut sub_command = tokio::process::Command::new(&command_name); + let child = sub_command + .current_dir(context.state.cwd()) + .args(context.args) + .env_clear() + .envs(context.state.env_vars()) + .stdout(context.stdout.into_stdio()) + .stdin(context.stdin.into_stdio()) + .stderr(stderr.clone().into_stdio()) + .spawn(); - let mut child = match child { - Ok(child) => child, - Err(err) => { - let _ = stderr.write_line(&format!( - "Error launching '{}': {}", - display_name, err - )); - return ExecuteResult::Continue(1, Vec::new(), Vec::new()); - } - }; + let mut child = match child { + Ok(child) => child, + Err(err) => { + let _ = stderr.write_line(&format!( + "Error launching '{}': {}", + display_name, err + )); + return ExecuteResult::Continue(1, Vec::new(), Vec::new()); + } + }; - // avoid deadlock since this is holding onto the pipes - drop(sub_command); + // avoid deadlock since this is holding onto the pipes + drop(sub_command); - tokio::select! { - result = child.wait() => match result { - Ok(status) => ExecuteResult::Continue( - status.code().unwrap_or(1), - Vec::new(), - Vec::new(), - ), - Err(err) => { - let _ = stderr.write_line(&format!("{}", err)); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } - }, - _ = context.state.token().cancelled() => { - let _ = child.kill().await; - ExecuteResult::for_cancellation() + tokio::select! { + result = child.wait() => match result { + Ok(status) => ExecuteResult::Continue( + status.code().unwrap_or(1), + Vec::new(), + Vec::new(), + ), + Err(err) => { + let _ = stderr.write_line(&format!("{}", err)); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }, + _ = context.state.token().cancelled() => { + let _ = child.kill().await; + ExecuteResult::for_cancellation() + } + } } - } + .boxed_local() } - .boxed_local() - } } diff --git a/crates/deno_task_shell/src/shell/commands/exit.rs b/crates/deno_task_shell/src/shell/commands/exit.rs index 5222c61..b606104 100644 --- a/crates/deno_task_shell/src/shell/commands/exit.rs +++ b/crates/deno_task_shell/src/shell/commands/exit.rs @@ -14,104 +14,104 @@ use super::ShellCommandContext; pub struct ExitCommand; impl ShellCommand for ExitCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match execute_exit(context.args) { - Ok(code) => ExecuteResult::Exit(code, Vec::new()), - Err(err) => { - context.stderr.write_line(&format!("exit: {err}")).unwrap(); - ExecuteResult::Exit(2, Vec::new()) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_exit(context.args) { + Ok(code) => ExecuteResult::Exit(code, Vec::new()), + Err(err) => { + context.stderr.write_line(&format!("exit: {err}")).unwrap(); + ExecuteResult::Exit(2, Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_exit(args: Vec) -> Result { - let exit_code = parse_args(args)?; + let exit_code = parse_args(args)?; - Ok(if exit_code < 0 { - let code = -exit_code % 256; - 256 - code - } else { - exit_code % 256 - }) + Ok(if exit_code < 0 { + let code = -exit_code % 256; + 256 - code + } else { + exit_code % 256 + }) } fn parse_args(args: Vec) -> Result { - let args = parse_arg_kinds(&args); - let mut paths = Vec::new(); - for arg in args { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - _ => arg.bail_unsupported()?, + let args = parse_arg_kinds(&args); + let mut paths = Vec::new(); + for arg in args { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, + } } - } - match paths.len() { - 0 => Ok(1), - 1 => { - let arg = paths.remove(0).to_string(); - match arg.parse::() { - Ok(value) => Ok(value), - Err(_) => bail!("numeric argument required."), - } - } - _ => { - bail!("too many arguments") + match paths.len() { + 0 => Ok(1), + 1 => { + let arg = paths.remove(0).to_string(); + match arg.parse::() { + Ok(value) => Ok(value), + Err(_) => bail!("numeric argument required."), + } + } + _ => { + bail!("too many arguments") + } } - } } #[cfg(test)] mod test { - use super::*; + use super::*; - #[test] - fn parses_args() { - assert_eq!(parse_args(vec![]).unwrap(), 1); - assert_eq!(parse_args(vec!["5".to_string()]).unwrap(), 5); - assert_eq!( - parse_args(vec!["test".to_string()]) - .err() - .unwrap() - .to_string(), - "numeric argument required." - ); - assert_eq!( - parse_args(vec!["1".to_string(), "2".to_string()]) - .err() - .unwrap() - .to_string(), - "too many arguments" - ); - assert_eq!( - parse_args(vec!["-a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -a" - ); - assert_eq!( - parse_args(vec!["--a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --a" - ); - } + #[test] + fn parses_args() { + assert_eq!(parse_args(vec![]).unwrap(), 1); + assert_eq!(parse_args(vec!["5".to_string()]).unwrap(), 5); + assert_eq!( + parse_args(vec!["test".to_string()]) + .err() + .unwrap() + .to_string(), + "numeric argument required." + ); + assert_eq!( + parse_args(vec!["1".to_string(), "2".to_string()]) + .err() + .unwrap() + .to_string(), + "too many arguments" + ); + assert_eq!( + parse_args(vec!["-a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -a" + ); + assert_eq!( + parse_args(vec!["--a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --a" + ); + } - #[test] - fn executes_exit() { - assert_eq!(execute_exit(vec![]).unwrap(), 1); - assert_eq!(execute_exit(vec!["0".to_string()]).unwrap(), 0); - assert_eq!(execute_exit(vec!["255".to_string()]).unwrap(), 255); - assert_eq!(execute_exit(vec!["256".to_string()]).unwrap(), 0); - assert_eq!(execute_exit(vec!["257".to_string()]).unwrap(), 1); - assert_eq!(execute_exit(vec!["-1".to_string()]).unwrap(), 255); - } + #[test] + fn executes_exit() { + assert_eq!(execute_exit(vec![]).unwrap(), 1); + assert_eq!(execute_exit(vec!["0".to_string()]).unwrap(), 0); + assert_eq!(execute_exit(vec!["255".to_string()]).unwrap(), 255); + assert_eq!(execute_exit(vec!["256".to_string()]).unwrap(), 0); + assert_eq!(execute_exit(vec!["257".to_string()]).unwrap(), 1); + assert_eq!(execute_exit(vec!["-1".to_string()]).unwrap(), 255); + } } diff --git a/crates/deno_task_shell/src/shell/commands/export.rs b/crates/deno_task_shell/src/shell/commands/export.rs index bf433c3..83ed6d7 100644 --- a/crates/deno_task_shell/src/shell/commands/export.rs +++ b/crates/deno_task_shell/src/shell/commands/export.rs @@ -11,23 +11,23 @@ use super::ShellCommandContext; pub struct ExportCommand; impl ShellCommand for ExportCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let mut changes = Vec::new(); - for arg in context.args { - // ignore if it doesn't contain an equals - if let Some(equals_index) = arg.find('=') { - let arg_name = &arg[..equals_index]; - let arg_value = &arg[equals_index + 1..]; - changes.push(EnvChange::SetEnvVar( - arg_name.to_string(), - arg_value.to_string(), - )); - } + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let mut changes = Vec::new(); + for arg in context.args { + // ignore if it doesn't contain an equals + if let Some(equals_index) = arg.find('=') { + let arg_name = &arg[..equals_index]; + let arg_value = &arg[equals_index + 1..]; + changes.push(EnvChange::SetEnvVar( + arg_name.to_string(), + arg_value.to_string(), + )); + } + } + let result = ExecuteResult::Continue(0, changes, Vec::new()); + Box::pin(futures::future::ready(result)) } - let result = ExecuteResult::Continue(0, changes, Vec::new()); - Box::pin(futures::future::ready(result)) - } } diff --git a/crates/deno_task_shell/src/shell/commands/head.rs b/crates/deno_task_shell/src/shell/commands/head.rs index bef3a46..d3a0459 100644 --- a/crates/deno_task_shell/src/shell/commands/head.rs +++ b/crates/deno_task_shell/src/shell/commands/head.rs @@ -19,295 +19,305 @@ use super::args::ArgKind; pub struct HeadCommand; impl ShellCommand for HeadCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let mut stderr = context.stderr.clone(); - let result = match execute_head(context) { - Ok(result) => result, - Err(err) => { - let _ = stderr.write_line(&format!("head: {err}")); - ExecuteResult::from_exit_code(1) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let mut stderr = context.stderr.clone(); + let result = match execute_head(context) { + Ok(result) => result, + Err(err) => { + let _ = stderr.write_line(&format!("head: {err}")); + ExecuteResult::from_exit_code(1) + } + }; + Box::pin(futures::future::ready(result)) + } } fn copy_lines Result>( - writer: &mut ShellPipeWriter, - max_lines: u64, - cancellation_token: &CancellationToken, - mut read: F, - buffer_size: usize, + writer: &mut ShellPipeWriter, + max_lines: u64, + cancellation_token: &CancellationToken, + mut read: F, + buffer_size: usize, ) -> Result { - let mut written_lines = 0; - let mut buffer = vec![0; buffer_size]; - while written_lines < max_lines { - if cancellation_token.is_cancelled() { - return Ok(ExecuteResult::for_cancellation()); - } - let read_bytes = read(&mut buffer)?; - if read_bytes == 0 { - break; - } + let mut written_lines = 0; + let mut buffer = vec![0; buffer_size]; + while written_lines < max_lines { + if cancellation_token.is_cancelled() { + return Ok(ExecuteResult::for_cancellation()); + } + let read_bytes = read(&mut buffer)?; + if read_bytes == 0 { + break; + } - if cancellation_token.is_cancelled() { - return Ok(ExecuteResult::for_cancellation()); - } + if cancellation_token.is_cancelled() { + return Ok(ExecuteResult::for_cancellation()); + } - let mut written_bytes: usize = 0; - let split_lines = buffer[..read_bytes].split(|&b| b == b'\n'); - for line in split_lines { - if written_lines >= max_lines - || (written_bytes + line.len()) >= read_bytes - { - break; - } - writer.write_all(line)?; - writer.write_all(b"\n")?; - written_bytes += line.len() + 1; - written_lines += 1; - } + let mut written_bytes: usize = 0; + let split_lines = buffer[..read_bytes].split(|&b| b == b'\n'); + for line in split_lines { + if written_lines >= max_lines + || (written_bytes + line.len()) >= read_bytes + { + break; + } + writer.write_all(line)?; + writer.write_all(b"\n")?; + written_bytes += line.len() + 1; + written_lines += 1; + } - if written_lines < max_lines && written_bytes < read_bytes { - writer.write_all(&buffer[written_bytes..read_bytes])?; + if written_lines < max_lines && written_bytes < read_bytes { + writer.write_all(&buffer[written_bytes..read_bytes])?; + } } - } - Ok(ExecuteResult::from_exit_code(0)) + Ok(ExecuteResult::from_exit_code(0)) } fn execute_head(mut context: ShellCommandContext) -> Result { - let flags = parse_args(context.args)?; - if flags.path == "-" { - copy_lines( - &mut context.stdout, - flags.lines, - context.state.token(), - |buf| context.stdin.read(buf), - 512, - ) - } else { - let path = flags.path; - match File::open(context.state.cwd().join(&path)) { - Ok(mut file) => copy_lines( - &mut context.stdout, - flags.lines, - context.state.token(), - |buf| file.read(buf).map_err(Into::into), - 512, - ), - Err(err) => { - context.stderr.write_line(&format!("head: {path}: {err}"))?; - Ok(ExecuteResult::from_exit_code(1)) - } + let flags = parse_args(context.args)?; + if flags.path == "-" { + copy_lines( + &mut context.stdout, + flags.lines, + context.state.token(), + |buf| context.stdin.read(buf), + 512, + ) + } else { + let path = flags.path; + match File::open(context.state.cwd().join(&path)) { + Ok(mut file) => copy_lines( + &mut context.stdout, + flags.lines, + context.state.token(), + |buf| file.read(buf).map_err(Into::into), + 512, + ), + Err(err) => { + context.stderr.write_line(&format!("head: {path}: {err}"))?; + Ok(ExecuteResult::from_exit_code(1)) + } + } } - } } #[derive(Debug, PartialEq)] struct HeadFlags { - path: String, - lines: u64, + path: String, + lines: u64, } fn parse_args(args: Vec) -> Result { - let mut path: Option = None; - let mut lines: Option = None; - let mut iterator = parse_arg_kinds(&args).into_iter(); - while let Some(arg) = iterator.next() { - match arg { - ArgKind::Arg(file_name) => { - if path.is_none() { - path = Some(file_name.to_string()); - continue; - } + let mut path: Option = None; + let mut lines: Option = None; + let mut iterator = parse_arg_kinds(&args).into_iter(); + while let Some(arg) = iterator.next() { + match arg { + ArgKind::Arg(file_name) => { + if path.is_none() { + path = Some(file_name.to_string()); + continue; + } - // for now, we only support one file - // TODO: support multiple files - bail!("only one file is supported for now"); - } - ArgKind::ShortFlag('n') => match iterator.next() { - Some(ArgKind::Arg(arg)) => { - lines = Some(arg.parse::()?); - } - _ => bail!("expected a value following -n"), - }, - ArgKind::LongFlag(flag) => { - if flag == "lines" || flag == "lines=" { - bail!("expected a value for --lines"); - } else if let Some(arg) = flag.strip_prefix("lines=") { - lines = Some(arg.parse::()?); - } else { - arg.bail_unsupported()? + // for now, we only support one file + // TODO: support multiple files + bail!("only one file is supported for now"); + } + ArgKind::ShortFlag('n') => match iterator.next() { + Some(ArgKind::Arg(arg)) => { + lines = Some(arg.parse::()?); + } + _ => bail!("expected a value following -n"), + }, + ArgKind::LongFlag(flag) => { + if flag == "lines" || flag == "lines=" { + bail!("expected a value for --lines"); + } else if let Some(arg) = flag.strip_prefix("lines=") { + lines = Some(arg.parse::()?); + } else { + arg.bail_unsupported()? + } + } + _ => arg.bail_unsupported()?, } - } - _ => arg.bail_unsupported()?, } - } - Ok(HeadFlags { - path: path.unwrap_or("-".to_string()), - lines: lines.unwrap_or(10), - }) + Ok(HeadFlags { + path: path.unwrap_or("-".to_string()), + lines: lines.unwrap_or(10), + }) } #[cfg(test)] mod test { - use crate::pipe; - use std::cmp::min; + use crate::pipe; + use std::cmp::min; - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - async fn copies_lines( - // #[case] - buffer_size: usize, - ) { - let (reader, mut writer) = pipe(); - let reader_handle = reader.pipe_to_string_handle(); - let data = b"foo\nbar\nbaz\nqux\n"; - let data_length = data.len(); - let mut offset = 0; - let result = copy_lines( - &mut writer, - 2, - &CancellationToken::new(), - |buffer| { - if offset >= data.len() { - return Ok(0); - } - let buffer_length = buffer.len(); - let read_length = min(buffer_length, data_length); - buffer[..read_length] - .copy_from_slice(&data[offset..(offset + read_length)]); - offset += read_length; - Ok(read_length) - }, - buffer_size, - ); - drop(writer); // Drop the writer ahead of the reader to prevent a deadlock. - assert_eq!(reader_handle.await.unwrap(), "foo\nbar\n"); - assert_eq!(result.unwrap().into_exit_code_and_handles().0, 0); - } + async fn copies_lines( + // #[case] + buffer_size: usize, + ) { + let (reader, mut writer) = pipe(); + let reader_handle = reader.pipe_to_string_handle(); + let data = b"foo\nbar\nbaz\nqux\n"; + let data_length = data.len(); + let mut offset = 0; + let result = copy_lines( + &mut writer, + 2, + &CancellationToken::new(), + |buffer| { + if offset >= data.len() { + return Ok(0); + } + let buffer_length = buffer.len(); + let read_length = min(buffer_length, data_length); + buffer[..read_length] + .copy_from_slice(&data[offset..(offset + read_length)]); + offset += read_length; + Ok(read_length) + }, + buffer_size, + ); + drop(writer); // Drop the writer ahead of the reader to prevent a deadlock. + assert_eq!(reader_handle.await.unwrap(), "foo\nbar\n"); + assert_eq!(result.unwrap().into_exit_code_and_handles().0, 0); + } - #[tokio::test] - async fn copies_lines_with_shorter_buffer_size() { - copies_lines(2).await; - } + #[tokio::test] + async fn copies_lines_with_shorter_buffer_size() { + copies_lines(2).await; + } - #[tokio::test] - async fn copies_lines_with_buffer_size_to_match_each_line_length() { - copies_lines(4).await; - } + #[tokio::test] + async fn copies_lines_with_buffer_size_to_match_each_line_length() { + copies_lines(4).await; + } - #[tokio::test] - async fn copies_lines_with_buffer_of_one_and_half_times_of_each_line_length() - { - copies_lines(6).await; - } + #[tokio::test] + async fn copies_lines_with_buffer_of_one_and_half_times_of_each_line_length( + ) { + copies_lines(6).await; + } - #[tokio::test] - async fn copies_lines_with_long_buffer_size() { - copies_lines(512).await; - } + #[tokio::test] + async fn copies_lines_with_long_buffer_size() { + copies_lines(512).await; + } - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![]).unwrap(), - HeadFlags { - path: "-".to_string(), - lines: 10 - } - ); - assert_eq!( - parse_args(vec!["-n".to_string(), "5".to_string()]).unwrap(), - HeadFlags { - path: "-".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["--lines=5".to_string()]).unwrap(), - HeadFlags { - path: "-".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["path".to_string()]).unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 10 - } - ); - assert_eq!( - parse_args(vec!["-n".to_string(), "5".to_string(), "path".to_string()]) - .unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["--lines=5".to_string(), "path".to_string()]).unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["path".to_string(), "-n".to_string(), "5".to_string()]) - .unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["path".to_string(), "--lines=5".to_string()]).unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["-n".to_string()]) - .err() - .unwrap() - .to_string(), - "expected a value following -n" - ); - assert_eq!( - parse_args(vec!["--lines".to_string()]) - .err() - .unwrap() - .to_string(), - "expected a value for --lines" - ); - assert_eq!( - parse_args(vec!["--lines=".to_string()]) - .err() - .unwrap() - .to_string(), - "expected a value for --lines" - ); - assert_eq!( - parse_args(vec!["--flag".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --flag" - ); - assert_eq!( - parse_args(vec!["-t".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t" - ); - } + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![]).unwrap(), + HeadFlags { + path: "-".to_string(), + lines: 10 + } + ); + assert_eq!( + parse_args(vec!["-n".to_string(), "5".to_string()]).unwrap(), + HeadFlags { + path: "-".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["--lines=5".to_string()]).unwrap(), + HeadFlags { + path: "-".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["path".to_string()]).unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 10 + } + ); + assert_eq!( + parse_args(vec![ + "-n".to_string(), + "5".to_string(), + "path".to_string() + ]) + .unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["--lines=5".to_string(), "path".to_string()]) + .unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec![ + "path".to_string(), + "-n".to_string(), + "5".to_string() + ]) + .unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["path".to_string(), "--lines=5".to_string()]) + .unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["-n".to_string()]) + .err() + .unwrap() + .to_string(), + "expected a value following -n" + ); + assert_eq!( + parse_args(vec!["--lines".to_string()]) + .err() + .unwrap() + .to_string(), + "expected a value for --lines" + ); + assert_eq!( + parse_args(vec!["--lines=".to_string()]) + .err() + .unwrap() + .to_string(), + "expected a value for --lines" + ); + assert_eq!( + parse_args(vec!["--flag".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --flag" + ); + assert_eq!( + parse_args(vec!["-t".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/mkdir.rs b/crates/deno_task_shell/src/shell/commands/mkdir.rs index 85d9741..9245b3a 100644 --- a/crates/deno_task_shell/src/shell/commands/mkdir.rs +++ b/crates/deno_task_shell/src/shell/commands/mkdir.rs @@ -18,209 +18,226 @@ use super::ShellCommandContext; pub struct MkdirCommand; impl ShellCommand for MkdirCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - mkdir_command(context.state.cwd(), context.args, context.stderr), - context.state.token() - ) + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + mkdir_command( + context.state.cwd(), + context.args, + context.stderr + ), + context.state.token() + ) + } + .boxed_local() } - .boxed_local() - } } async fn mkdir_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_mkdir(cwd, args).await { - Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), - Err(err) => { - let _ = stderr.write_line(&format!("mkdir: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) + match execute_mkdir(cwd, args).await { + Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), + Err(err) => { + let _ = stderr.write_line(&format!("mkdir: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } } - } } async fn execute_mkdir(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_args(args)?; - for specified_path in &flags.paths { - let path = cwd.join(specified_path); - if path.is_file() || !flags.parents && path.is_dir() { - bail!("cannot create directory '{}': File exists", specified_path); + let flags = parse_args(args)?; + for specified_path in &flags.paths { + let path = cwd.join(specified_path); + if path.is_file() || !flags.parents && path.is_dir() { + bail!("cannot create directory '{}': File exists", specified_path); + } + if flags.parents { + if let Err(err) = tokio::fs::create_dir_all(&path).await { + bail!("cannot create directory '{}': {}", specified_path, err); + } + } else if let Err(err) = tokio::fs::create_dir(&path).await { + bail!("cannot create directory '{}': {}", specified_path, err); + } } - if flags.parents { - if let Err(err) = tokio::fs::create_dir_all(&path).await { - bail!("cannot create directory '{}': {}", specified_path, err); - } - } else if let Err(err) = tokio::fs::create_dir(&path).await { - bail!("cannot create directory '{}': {}", specified_path, err); - } - } - Ok(()) + Ok(()) } #[derive(Default, Debug, PartialEq)] struct MkdirFlags { - parents: bool, - paths: Vec, + parents: bool, + paths: Vec, } fn parse_args(args: Vec) -> Result { - let mut result = MkdirFlags::default(); - - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::LongFlag("parents") | ArgKind::ShortFlag('p') => { - result.parents = true; - } - ArgKind::Arg(path) => { - result.paths.push(path.to_string()); - } - ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => arg.bail_unsupported()?, + let mut result = MkdirFlags::default(); + + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::LongFlag("parents") | ArgKind::ShortFlag('p') => { + result.parents = true; + } + ArgKind::Arg(path) => { + result.paths.push(path.to_string()); + } + ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => { + arg.bail_unsupported()? + } + } } - } - if result.paths.is_empty() { - bail!("missing operand"); - } + if result.paths.is_empty() { + bail!("missing operand"); + } - Ok(result) + Ok(result) } #[cfg(test)] mod test { - use tempfile::tempdir; - - use super::*; - use std::fs; - - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![ - "--parents".to_string(), - "a".to_string(), - "b".to_string(), - ]) - .unwrap(), - MkdirFlags { - parents: true, - paths: vec!["a".to_string(), "b".to_string()], - } - ); - assert_eq!( - parse_args(vec!["-p".to_string(), "a".to_string(), "b".to_string(),]) - .unwrap(), - MkdirFlags { - parents: true, - paths: vec!["a".to_string(), "b".to_string()], - } - ); - assert_eq!( - parse_args(vec!["--parents".to_string()]) - .err() - .unwrap() - .to_string(), - "missing operand", - ); - assert_eq!( - parse_args(vec![ - "--parents".to_string(), - "-p".to_string(), - "-u".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -u", - ); - assert_eq!( - parse_args(vec![ - "--parents".to_string(), - "--random-flag".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --random-flag", - ); - } - - #[tokio::test] - async fn test_creates() { - let dir = tempdir().unwrap(); - let file_path = dir.path().join("file.txt"); - let sub_dir_path = dir.path().join("folder"); - fs::write(&file_path, "").unwrap(); - fs::create_dir(sub_dir_path).unwrap(); - - assert_eq!( - execute_mkdir(dir.path(), vec!["file.txt".to_string()],) - .await - .err() - .unwrap() - .to_string(), - "cannot create directory 'file.txt': File exists" - ); - - assert_eq!(execute_mkdir( - dir.path(), - vec!["-p".to_string(), "file.txt".to_string()], - ) - .await - .err() - .unwrap().to_string(), "cannot create directory 'file.txt': File exists"); - - assert_eq!( - execute_mkdir(dir.path(), vec!["folder".to_string()],) - .await - .err() - .unwrap() - .to_string(), - "cannot create directory 'folder': File exists" - ); - - // should work because of -p - execute_mkdir(dir.path(), vec!["-p".to_string(), "folder".to_string()]) - .await - .unwrap(); - - execute_mkdir(dir.path(), vec!["other".to_string()]) - .await - .unwrap(); - assert!(dir.path().join("other").exists()); - - // sub folder - assert_eq!( - execute_mkdir(dir.path(), vec!["sub/folder".to_string()],) + use tempfile::tempdir; + + use super::*; + use std::fs; + + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![ + "--parents".to_string(), + "a".to_string(), + "b".to_string(), + ]) + .unwrap(), + MkdirFlags { + parents: true, + paths: vec!["a".to_string(), "b".to_string()], + } + ); + assert_eq!( + parse_args(vec![ + "-p".to_string(), + "a".to_string(), + "b".to_string(), + ]) + .unwrap(), + MkdirFlags { + parents: true, + paths: vec!["a".to_string(), "b".to_string()], + } + ); + assert_eq!( + parse_args(vec!["--parents".to_string()]) + .err() + .unwrap() + .to_string(), + "missing operand", + ); + assert_eq!( + parse_args(vec![ + "--parents".to_string(), + "-p".to_string(), + "-u".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -u", + ); + assert_eq!( + parse_args(vec![ + "--parents".to_string(), + "--random-flag".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --random-flag", + ); + } + + #[tokio::test] + async fn test_creates() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("file.txt"); + let sub_dir_path = dir.path().join("folder"); + fs::write(&file_path, "").unwrap(); + fs::create_dir(sub_dir_path).unwrap(); + + assert_eq!( + execute_mkdir(dir.path(), vec!["file.txt".to_string()],) + .await + .err() + .unwrap() + .to_string(), + "cannot create directory 'file.txt': File exists" + ); + + assert_eq!( + execute_mkdir( + dir.path(), + vec!["-p".to_string(), "file.txt".to_string()], + ) + .await + .err() + .unwrap() + .to_string(), + "cannot create directory 'file.txt': File exists" + ); + + assert_eq!( + execute_mkdir(dir.path(), vec!["folder".to_string()],) + .await + .err() + .unwrap() + .to_string(), + "cannot create directory 'folder': File exists" + ); + + // should work because of -p + execute_mkdir(dir.path(), vec!["-p".to_string(), "folder".to_string()]) + .await + .unwrap(); + + execute_mkdir(dir.path(), vec!["other".to_string()]) + .await + .unwrap(); + assert!(dir.path().join("other").exists()); + + // sub folder + assert_eq!( + execute_mkdir(dir.path(), vec!["sub/folder".to_string()],) + .await + .err() + .unwrap() + .to_string(), + format!( + "cannot create directory 'sub/folder': {}", + no_such_file_error_text() + ) + ); + + execute_mkdir( + dir.path(), + vec!["-p".to_string(), "sub/folder".to_string()], + ) .await - .err() - .unwrap() - .to_string(), - format!( - "cannot create directory 'sub/folder': {}", - no_such_file_error_text() - ) - ); - - execute_mkdir(dir.path(), vec!["-p".to_string(), "sub/folder".to_string()]) - .await - .unwrap(); - assert!(dir.path().join("sub").join("folder").exists()); - } - - fn no_such_file_error_text() -> &'static str { - if cfg!(windows) { - "The system cannot find the path specified. (os error 3)" - } else { - "No such file or directory (os error 2)" + .unwrap(); + assert!(dir.path().join("sub").join("folder").exists()); + } + + fn no_such_file_error_text() -> &'static str { + if cfg!(windows) { + "The system cannot find the path specified. (os error 3)" + } else { + "No such file or directory (os error 2)" + } } - } } diff --git a/crates/deno_task_shell/src/shell/commands/mod.rs b/crates/deno_task_shell/src/shell/commands/mod.rs index 028392c..ef77a00 100644 --- a/crates/deno_task_shell/src/shell/commands/mod.rs +++ b/crates/deno_task_shell/src/shell/commands/mod.rs @@ -30,110 +30,110 @@ use super::types::ShellPipeWriter; use super::types::ShellState; pub fn builtin_commands() -> HashMap> { - HashMap::from([ - ( - "cat".to_string(), - Rc::new(cat::CatCommand) as Rc, - ), - ( - "cd".to_string(), - Rc::new(cd::CdCommand) as Rc, - ), - ( - "cp".to_string(), - Rc::new(cp_mv::CpCommand) as Rc, - ), - ( - "echo".to_string(), - Rc::new(echo::EchoCommand) as Rc, - ), - ( - "exit".to_string(), - Rc::new(exit::ExitCommand) as Rc, - ), - ( - "export".to_string(), - Rc::new(export::ExportCommand) as Rc, - ), - ( - "head".to_string(), - Rc::new(head::HeadCommand) as Rc, - ), - ( - "mkdir".to_string(), - Rc::new(mkdir::MkdirCommand) as Rc, - ), - ( - "mv".to_string(), - Rc::new(cp_mv::MvCommand) as Rc, - ), - ( - "pwd".to_string(), - Rc::new(pwd::PwdCommand) as Rc, - ), - ( - "rm".to_string(), - Rc::new(rm::RmCommand) as Rc, - ), - ( - "sleep".to_string(), - Rc::new(sleep::SleepCommand) as Rc, - ), - ( - "true".to_string(), - Rc::new(ExitCodeCommand(0)) as Rc, - ), - ( - "false".to_string(), - Rc::new(ExitCodeCommand(1)) as Rc, - ), - ( - "unset".to_string(), - Rc::new(unset::UnsetCommand) as Rc, - ), - ( - "xargs".to_string(), - Rc::new(xargs::XargsCommand) as Rc, - ), - ]) + HashMap::from([ + ( + "cat".to_string(), + Rc::new(cat::CatCommand) as Rc, + ), + ( + "cd".to_string(), + Rc::new(cd::CdCommand) as Rc, + ), + ( + "cp".to_string(), + Rc::new(cp_mv::CpCommand) as Rc, + ), + ( + "echo".to_string(), + Rc::new(echo::EchoCommand) as Rc, + ), + ( + "exit".to_string(), + Rc::new(exit::ExitCommand) as Rc, + ), + ( + "export".to_string(), + Rc::new(export::ExportCommand) as Rc, + ), + ( + "head".to_string(), + Rc::new(head::HeadCommand) as Rc, + ), + ( + "mkdir".to_string(), + Rc::new(mkdir::MkdirCommand) as Rc, + ), + ( + "mv".to_string(), + Rc::new(cp_mv::MvCommand) as Rc, + ), + ( + "pwd".to_string(), + Rc::new(pwd::PwdCommand) as Rc, + ), + ( + "rm".to_string(), + Rc::new(rm::RmCommand) as Rc, + ), + ( + "sleep".to_string(), + Rc::new(sleep::SleepCommand) as Rc, + ), + ( + "true".to_string(), + Rc::new(ExitCodeCommand(0)) as Rc, + ), + ( + "false".to_string(), + Rc::new(ExitCodeCommand(1)) as Rc, + ), + ( + "unset".to_string(), + Rc::new(unset::UnsetCommand) as Rc, + ), + ( + "xargs".to_string(), + Rc::new(xargs::XargsCommand) as Rc, + ), + ]) } pub struct ExecuteCommandArgsContext { - pub args: Vec, - pub state: ShellState, - pub stdin: ShellPipeReader, - pub stdout: ShellPipeWriter, - pub stderr: ShellPipeWriter, + pub args: Vec, + pub state: ShellState, + pub stdin: ShellPipeReader, + pub stdout: ShellPipeWriter, + pub stderr: ShellPipeWriter, } pub struct ShellCommandContext { - pub args: Vec, - pub state: ShellState, - pub stdin: ShellPipeReader, - pub stdout: ShellPipeWriter, - pub stderr: ShellPipeWriter, - pub execute_command_args: - Box FutureExecuteResult>, + pub args: Vec, + pub state: ShellState, + pub stdin: ShellPipeReader, + pub stdout: ShellPipeWriter, + pub stderr: ShellPipeWriter, + pub execute_command_args: + Box FutureExecuteResult>, } pub trait ShellCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult>; + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult>; } macro_rules! execute_with_cancellation { - ($result_expr:expr, $token:expr) => { - tokio::select! { - result = $result_expr => { - result - }, - _ = $token.cancelled() => { - ExecuteResult::for_cancellation() - } - } - }; + ($result_expr:expr, $token:expr) => { + tokio::select! { + result = $result_expr => { + result + }, + _ = $token.cancelled() => { + ExecuteResult::for_cancellation() + } + } + }; } pub(super) use execute_with_cancellation; @@ -141,13 +141,13 @@ pub(super) use execute_with_cancellation; struct ExitCodeCommand(i32); impl ShellCommand for ExitCodeCommand { - fn execute( - &self, - _context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - // ignores additional arguments - Box::pin(futures::future::ready(ExecuteResult::from_exit_code( - self.0, - ))) - } + fn execute( + &self, + _context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + // ignores additional arguments + Box::pin(futures::future::ready(ExecuteResult::from_exit_code( + self.0, + ))) + } } diff --git a/crates/deno_task_shell/src/shell/commands/pwd.rs b/crates/deno_task_shell/src/shell/commands/pwd.rs index 9da211c..0b7bebf 100644 --- a/crates/deno_task_shell/src/shell/commands/pwd.rs +++ b/crates/deno_task_shell/src/shell/commands/pwd.rs @@ -16,90 +16,91 @@ use super::ShellCommandContext; pub struct PwdCommand; impl ShellCommand for PwdCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match execute_pwd(context.state.cwd(), context.args) { - Ok(output) => { - let _ = context.stdout.write_line(&output); - ExecuteResult::from_exit_code(0) - } - Err(err) => { - let _ = context.stderr.write_line(&format!("pwd: {err}")); - ExecuteResult::from_exit_code(1) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_pwd(context.state.cwd(), context.args) { + Ok(output) => { + let _ = context.stdout.write_line(&output); + ExecuteResult::from_exit_code(0) + } + Err(err) => { + let _ = context.stderr.write_line(&format!("pwd: {err}")); + ExecuteResult::from_exit_code(1) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_pwd(cwd: &Path, args: Vec) -> Result { - let flags = parse_args(args)?; - let cwd = if flags.logical { - fs_util::canonicalize_path(cwd) - .with_context(|| format!("error canonicalizing: {}", cwd.display()))? - } else { - cwd.to_path_buf() - }; - Ok(cwd.display().to_string()) + let flags = parse_args(args)?; + let cwd = if flags.logical { + fs_util::canonicalize_path(cwd).with_context(|| { + format!("error canonicalizing: {}", cwd.display()) + })? + } else { + cwd.to_path_buf() + }; + Ok(cwd.display().to_string()) } #[derive(Debug, PartialEq)] struct PwdFlags { - logical: bool, + logical: bool, } fn parse_args(args: Vec) -> Result { - let mut logical = false; - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::ShortFlag('L') => { - logical = true; - } - ArgKind::ShortFlag('P') => { - // ignore, this is the default - } - ArgKind::Arg(_) => { - // args are ignored by pwd - } - _ => arg.bail_unsupported()?, + let mut logical = false; + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::ShortFlag('L') => { + logical = true; + } + ArgKind::ShortFlag('P') => { + // ignore, this is the default + } + ArgKind::Arg(_) => { + // args are ignored by pwd + } + _ => arg.bail_unsupported()?, + } } - } - Ok(PwdFlags { logical }) + Ok(PwdFlags { logical }) } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses_args() { - assert_eq!(parse_args(vec![]).unwrap(), PwdFlags { logical: false }); - assert_eq!( - parse_args(vec!["-P".to_string()]).unwrap(), - PwdFlags { logical: false } - ); - assert_eq!( - parse_args(vec!["-L".to_string()]).unwrap(), - PwdFlags { logical: true } - ); - assert!(parse_args(vec!["test".to_string()]).is_ok()); - assert_eq!( - parse_args(vec!["--flag".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --flag" - ); - assert_eq!( - parse_args(vec!["-t".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t" - ); - } + #[test] + fn parses_args() { + assert_eq!(parse_args(vec![]).unwrap(), PwdFlags { logical: false }); + assert_eq!( + parse_args(vec!["-P".to_string()]).unwrap(), + PwdFlags { logical: false } + ); + assert_eq!( + parse_args(vec!["-L".to_string()]).unwrap(), + PwdFlags { logical: true } + ); + assert!(parse_args(vec!["test".to_string()]).is_ok()); + assert_eq!( + parse_args(vec!["--flag".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --flag" + ); + assert_eq!( + parse_args(vec!["-t".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/rm.rs b/crates/deno_task_shell/src/shell/commands/rm.rs index 72c1410..db60411 100644 --- a/crates/deno_task_shell/src/shell/commands/rm.rs +++ b/crates/deno_task_shell/src/shell/commands/rm.rs @@ -19,318 +19,333 @@ use super::ShellCommandContext; pub struct RmCommand; impl ShellCommand for RmCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - rm_command(context.state.cwd(), context.args, context.stderr), - context.state.token() - ) + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + rm_command(context.state.cwd(), context.args, context.stderr), + context.state.token() + ) + } + .boxed_local() } - .boxed_local() - } } async fn rm_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_remove(cwd, args).await { - Ok(()) => ExecuteResult::from_exit_code(0), - Err(err) => { - let _ = stderr.write_line(&format!("rm: {err}")); - ExecuteResult::from_exit_code(1) + match execute_remove(cwd, args).await { + Ok(()) => ExecuteResult::from_exit_code(0), + Err(err) => { + let _ = stderr.write_line(&format!("rm: {err}")); + ExecuteResult::from_exit_code(1) + } } - } } async fn execute_remove(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_args(args)?; - for specified_path in &flags.paths { - let path = cwd.join(specified_path); - let result = if flags.recursive { - if path.is_dir() { - tokio::fs::remove_dir_all(&path).await - } else { - remove_file_or_dir(&path, &flags).await - } - } else { - remove_file_or_dir(&path, &flags).await - }; - if let Err(err) = result { - if err.kind() != ErrorKind::NotFound || !flags.force { - bail!("cannot remove '{}': {}", specified_path, err); - } + let flags = parse_args(args)?; + for specified_path in &flags.paths { + let path = cwd.join(specified_path); + let result = if flags.recursive { + if path.is_dir() { + tokio::fs::remove_dir_all(&path).await + } else { + remove_file_or_dir(&path, &flags).await + } + } else { + remove_file_or_dir(&path, &flags).await + }; + if let Err(err) = result { + if err.kind() != ErrorKind::NotFound || !flags.force { + bail!("cannot remove '{}': {}", specified_path, err); + } + } } - } - Ok(()) + Ok(()) } async fn remove_file_or_dir( - path: &Path, - flags: &RmFlags, + path: &Path, + flags: &RmFlags, ) -> std::io::Result<()> { - if flags.dir && path.is_dir() { - tokio::fs::remove_dir(path).await - } else { - tokio::fs::remove_file(path).await - } + if flags.dir && path.is_dir() { + tokio::fs::remove_dir(path).await + } else { + tokio::fs::remove_file(path).await + } } #[derive(Default, Debug, PartialEq)] struct RmFlags { - force: bool, - recursive: bool, - dir: bool, - paths: Vec, + force: bool, + recursive: bool, + dir: bool, + paths: Vec, } fn parse_args(args: Vec) -> Result { - let mut result = RmFlags::default(); - - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::LongFlag("recursive") - | ArgKind::ShortFlag('r') - | ArgKind::ShortFlag('R') => { - result.recursive = true; - } - ArgKind::LongFlag("dir") | ArgKind::ShortFlag('d') => { - result.dir = true; - } - ArgKind::LongFlag("force") | ArgKind::ShortFlag('f') => { - result.force = true; - } - ArgKind::Arg(path) => { - result.paths.push(path.to_string()); - } - ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => arg.bail_unsupported()?, + let mut result = RmFlags::default(); + + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::LongFlag("recursive") + | ArgKind::ShortFlag('r') + | ArgKind::ShortFlag('R') => { + result.recursive = true; + } + ArgKind::LongFlag("dir") | ArgKind::ShortFlag('d') => { + result.dir = true; + } + ArgKind::LongFlag("force") | ArgKind::ShortFlag('f') => { + result.force = true; + } + ArgKind::Arg(path) => { + result.paths.push(path.to_string()); + } + ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => { + arg.bail_unsupported()? + } + } } - } - if result.paths.is_empty() { - bail!("missing operand"); - } + if result.paths.is_empty() { + bail!("missing operand"); + } - Ok(result) + Ok(result) } #[cfg(test)] mod test { - use tempfile::tempdir; - - use super::*; - use std::fs; - - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![ - "--recursive".to_string(), - "--dir".to_string(), - "a".to_string(), - "b".to_string(), - ]) - .unwrap(), - RmFlags { - recursive: true, - dir: true, - paths: vec!["a".to_string(), "b".to_string()], - ..Default::default() - } - ); - assert_eq!( - parse_args(vec!["-rf".to_string(), "a".to_string(), "b".to_string(),]) - .unwrap(), - RmFlags { - recursive: true, - force: true, - dir: false, - paths: vec!["a".to_string(), "b".to_string()], - } - ); - assert_eq!( - parse_args(vec!["-d".to_string(), "a".to_string()]).unwrap(), - RmFlags { - recursive: false, - force: false, - dir: true, - paths: vec!["a".to_string()], - } - ); - assert_eq!( - parse_args(vec!["--recursive".to_string(), "-f".to_string(),]) - .err() - .unwrap() - .to_string(), - "missing operand", - ); - assert_eq!( - parse_args(vec![ - "--recursive".to_string(), - "-u".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -u", - ); - assert_eq!( - parse_args(vec![ - "--recursive".to_string(), - "--random-flag".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --random-flag", - ); - } - - #[tokio::test] - async fn test_force() { - let dir = tempdir().unwrap(); - let existent_file = dir.path().join("existent.txt"); - fs::write(&existent_file, "").unwrap(); - - execute_remove( - dir.path(), - vec!["-f".to_string(), "non_existent.txt".to_string()], - ) - .await - .unwrap(); - - let result = - execute_remove(dir.path(), vec!["non_existent.txt".to_string()]).await; - assert_eq!( - result.err().unwrap().to_string(), - format!( - "cannot remove 'non_existent.txt': {}", - no_such_file_error_text() - ) - ); - - assert!(existent_file.exists()); - execute_remove(dir.path(), vec!["existent.txt".to_string()]) - .await - .unwrap(); - assert!(!existent_file.exists()); - } - - #[tokio::test] - async fn test_recursive() { - let dir = tempdir().unwrap(); - let existent_file = dir.path().join("existent.txt"); - fs::write(&existent_file, "").unwrap(); - - let result = execute_remove( - dir.path(), - vec!["-r".to_string(), "non_existent.txt".to_string()], - ) - .await; - assert_eq!( - result.err().unwrap().to_string(), - format!( - "cannot remove 'non_existent.txt': {}", - no_such_file_error_text() - ) - ); - - // test on a file - assert!(existent_file.exists()); - execute_remove( - dir.path(), - vec!["-r".to_string(), "existent.txt".to_string()], - ) - .await - .unwrap(); - assert!(!existent_file.exists()); - - // test on a directory - let sub_dir = dir.path().join("folder").join("sub"); - fs::create_dir_all(&sub_dir).unwrap(); - let sub_file = sub_dir.join("file.txt"); - fs::write(&sub_file, "test").unwrap(); - assert!(sub_file.exists()); - execute_remove(dir.path(), vec!["-r".to_string(), "folder".to_string()]) - .await - .unwrap(); - assert!(!sub_file.exists()); - - let result = - execute_remove(dir.path(), vec!["-r".to_string(), "folder".to_string()]) + use tempfile::tempdir; + + use super::*; + use std::fs; + + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![ + "--recursive".to_string(), + "--dir".to_string(), + "a".to_string(), + "b".to_string(), + ]) + .unwrap(), + RmFlags { + recursive: true, + dir: true, + paths: vec!["a".to_string(), "b".to_string()], + ..Default::default() + } + ); + assert_eq!( + parse_args(vec![ + "-rf".to_string(), + "a".to_string(), + "b".to_string(), + ]) + .unwrap(), + RmFlags { + recursive: true, + force: true, + dir: false, + paths: vec!["a".to_string(), "b".to_string()], + } + ); + assert_eq!( + parse_args(vec!["-d".to_string(), "a".to_string()]).unwrap(), + RmFlags { + recursive: false, + force: false, + dir: true, + paths: vec!["a".to_string()], + } + ); + assert_eq!( + parse_args(vec!["--recursive".to_string(), "-f".to_string(),]) + .err() + .unwrap() + .to_string(), + "missing operand", + ); + assert_eq!( + parse_args(vec![ + "--recursive".to_string(), + "-u".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -u", + ); + assert_eq!( + parse_args(vec![ + "--recursive".to_string(), + "--random-flag".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --random-flag", + ); + } + + #[tokio::test] + async fn test_force() { + let dir = tempdir().unwrap(); + let existent_file = dir.path().join("existent.txt"); + fs::write(&existent_file, "").unwrap(); + + execute_remove( + dir.path(), + vec!["-f".to_string(), "non_existent.txt".to_string()], + ) + .await + .unwrap(); + + let result = + execute_remove(dir.path(), vec!["non_existent.txt".to_string()]) + .await; + assert_eq!( + result.err().unwrap().to_string(), + format!( + "cannot remove 'non_existent.txt': {}", + no_such_file_error_text() + ) + ); + + assert!(existent_file.exists()); + execute_remove(dir.path(), vec!["existent.txt".to_string()]) + .await + .unwrap(); + assert!(!existent_file.exists()); + } + + #[tokio::test] + async fn test_recursive() { + let dir = tempdir().unwrap(); + let existent_file = dir.path().join("existent.txt"); + fs::write(&existent_file, "").unwrap(); + + let result = execute_remove( + dir.path(), + vec!["-r".to_string(), "non_existent.txt".to_string()], + ) .await; - assert_eq!( - result.err().unwrap().to_string(), - format!("cannot remove 'folder': {}", no_such_file_error_text()) - ); - execute_remove(dir.path(), vec!["-rf".to_string(), "folder".to_string()]) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_dir() { - let dir = tempdir().unwrap(); - let existent_file = dir.path().join("existent.txt"); - let existent_dir = dir.path().join("sub_dir"); - let existent_dir_files = dir.path().join("sub_dir_files"); - fs::write(&existent_file, "").unwrap(); - fs::create_dir(&existent_dir).unwrap(); - fs::create_dir(&existent_dir_files).unwrap(); - fs::write(existent_dir_files.join("file.txt"), "").unwrap(); - - assert!(execute_remove( - dir.path(), - vec!["-d".to_string(), "existent.txt".to_string()], - ) - .await - .is_ok()); - - assert!(execute_remove( - dir.path(), - vec!["-d".to_string(), "sub_dir".to_string()], - ) - .await - .is_ok()); - assert!(!existent_dir.exists()); - - let result = execute_remove( - dir.path(), - vec!["-d".to_string(), "sub_dir_files".to_string()], - ) - .await; - assert_eq!( - result.err().unwrap().to_string(), - format!( - "cannot remove 'sub_dir_files': {}", - directory_not_empty_text() - ), - ); - assert!(existent_dir_files.exists()); - } - - fn no_such_file_error_text() -> &'static str { - if cfg!(windows) { - "The system cannot find the file specified. (os error 2)" - } else { - "No such file or directory (os error 2)" + assert_eq!( + result.err().unwrap().to_string(), + format!( + "cannot remove 'non_existent.txt': {}", + no_such_file_error_text() + ) + ); + + // test on a file + assert!(existent_file.exists()); + execute_remove( + dir.path(), + vec!["-r".to_string(), "existent.txt".to_string()], + ) + .await + .unwrap(); + assert!(!existent_file.exists()); + + // test on a directory + let sub_dir = dir.path().join("folder").join("sub"); + fs::create_dir_all(&sub_dir).unwrap(); + let sub_file = sub_dir.join("file.txt"); + fs::write(&sub_file, "test").unwrap(); + assert!(sub_file.exists()); + execute_remove( + dir.path(), + vec!["-r".to_string(), "folder".to_string()], + ) + .await + .unwrap(); + assert!(!sub_file.exists()); + + let result = execute_remove( + dir.path(), + vec!["-r".to_string(), "folder".to_string()], + ) + .await; + assert_eq!( + result.err().unwrap().to_string(), + format!("cannot remove 'folder': {}", no_such_file_error_text()) + ); + execute_remove( + dir.path(), + vec!["-rf".to_string(), "folder".to_string()], + ) + .await + .unwrap(); } - } - fn directory_not_empty_text() -> &'static str { - if cfg!(windows) { - "The directory is not empty. (os error 145)" - } else if cfg!(target_os = "macos") { - "Directory not empty (os error 66)" - } else { - "Directory not empty (os error 39)" + #[tokio::test] + async fn test_dir() { + let dir = tempdir().unwrap(); + let existent_file = dir.path().join("existent.txt"); + let existent_dir = dir.path().join("sub_dir"); + let existent_dir_files = dir.path().join("sub_dir_files"); + fs::write(&existent_file, "").unwrap(); + fs::create_dir(&existent_dir).unwrap(); + fs::create_dir(&existent_dir_files).unwrap(); + fs::write(existent_dir_files.join("file.txt"), "").unwrap(); + + assert!(execute_remove( + dir.path(), + vec!["-d".to_string(), "existent.txt".to_string()], + ) + .await + .is_ok()); + + assert!(execute_remove( + dir.path(), + vec!["-d".to_string(), "sub_dir".to_string()], + ) + .await + .is_ok()); + assert!(!existent_dir.exists()); + + let result = execute_remove( + dir.path(), + vec!["-d".to_string(), "sub_dir_files".to_string()], + ) + .await; + assert_eq!( + result.err().unwrap().to_string(), + format!( + "cannot remove 'sub_dir_files': {}", + directory_not_empty_text() + ), + ); + assert!(existent_dir_files.exists()); + } + + fn no_such_file_error_text() -> &'static str { + if cfg!(windows) { + "The system cannot find the file specified. (os error 2)" + } else { + "No such file or directory (os error 2)" + } + } + + fn directory_not_empty_text() -> &'static str { + if cfg!(windows) { + "The directory is not empty. (os error 145)" + } else if cfg!(target_os = "macos") { + "Directory not empty (os error 66)" + } else { + "Directory not empty (os error 39)" + } } - } } diff --git a/crates/deno_task_shell/src/shell/commands/sleep.rs b/crates/deno_task_shell/src/shell/commands/sleep.rs index f1e68e6..c1852f6 100644 --- a/crates/deno_task_shell/src/shell/commands/sleep.rs +++ b/crates/deno_task_shell/src/shell/commands/sleep.rs @@ -19,121 +19,127 @@ use super::ShellCommandContext; pub struct SleepCommand; impl ShellCommand for SleepCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - sleep_command(context.args, context.stderr), - context.state.token() - ) + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + sleep_command(context.args, context.stderr), + context.state.token() + ) + } + .boxed_local() } - .boxed_local() - } } async fn sleep_command( - args: Vec, - mut stderr: ShellPipeWriter, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_sleep(args).await { - Ok(()) => ExecuteResult::from_exit_code(0), - Err(err) => { - let _ = stderr.write_line(&format!("sleep: {err}")); - ExecuteResult::from_exit_code(1) + match execute_sleep(args).await { + Ok(()) => ExecuteResult::from_exit_code(0), + Err(err) => { + let _ = stderr.write_line(&format!("sleep: {err}")); + ExecuteResult::from_exit_code(1) + } } - } } async fn execute_sleep(args: Vec) -> Result<()> { - let ms = parse_args(args)?; - tokio::time::sleep(Duration::from_millis(ms)).await; - Ok(()) + let ms = parse_args(args)?; + tokio::time::sleep(Duration::from_millis(ms)).await; + Ok(()) } fn parse_arg(arg: &str) -> Result { - if let Some(t) = arg.strip_suffix('s') { - return Ok(t.parse()?); - } - if let Some(t) = arg.strip_suffix('m') { - return Ok(t.parse::()? * 60.); - } - if let Some(t) = arg.strip_suffix('h') { - return Ok(t.parse::()? * 60. * 60.); - } - if let Some(t) = arg.strip_suffix('d') { - return Ok(t.parse::()? * 60. * 60. * 24.); - } - - Ok(arg.parse()?) + if let Some(t) = arg.strip_suffix('s') { + return Ok(t.parse()?); + } + if let Some(t) = arg.strip_suffix('m') { + return Ok(t.parse::()? * 60.); + } + if let Some(t) = arg.strip_suffix('h') { + return Ok(t.parse::()? * 60. * 60.); + } + if let Some(t) = arg.strip_suffix('d') { + return Ok(t.parse::()? * 60. * 60. * 24.); + } + + Ok(arg.parse()?) } fn parse_args(args: Vec) -> Result { - // the time to sleep is the sum of all the arguments - let mut total_time_ms = 0; - let mut had_value = false; - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(arg) => match parse_arg(arg) { - Ok(value_s) => { - let ms = (value_s * 1000f64) as u64; - total_time_ms += ms; - had_value = true; + // the time to sleep is the sum of all the arguments + let mut total_time_ms = 0; + let mut had_value = false; + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(arg) => match parse_arg(arg) { + Ok(value_s) => { + let ms = (value_s * 1000f64) as u64; + total_time_ms += ms; + had_value = true; + } + Err(err) => { + bail!( + "error parsing argument '{}' to number: {}", + arg, + err + ); + } + }, + ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => { + arg.bail_unsupported()? + } } - Err(err) => { - bail!("error parsing argument '{}' to number: {}", arg, err); - } - }, - ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => arg.bail_unsupported()?, } - } - if !had_value { - bail!("missing operand"); - } - Ok(total_time_ms) + if !had_value { + bail!("missing operand"); + } + Ok(total_time_ms) } #[cfg(test)] mod test { - use std::time::Instant; - - use super::*; - - #[test] - fn should_parse_arg() { - assert_eq!(parse_arg("1").unwrap(), 1.); - assert_eq!(parse_arg("1s").unwrap(), 1.); - assert_eq!(parse_arg("1m").unwrap(), 1. * 60.); - assert_eq!(parse_arg("1h").unwrap(), 1. * 60. * 60.); - assert_eq!(parse_arg("1d").unwrap(), 1. * 60. * 60. * 24.); - assert!(parse_arg("d").err().is_some()); - } - - #[test] - fn should_parse_args() { - let value = parse_args(vec![ - "0.5".to_string(), - "1m".to_string(), - "1.25".to_string(), - ]) - .unwrap(); - assert_eq!(value, 500 + 1000 * 60 + 1250); - - let result = parse_args(vec![]).err().unwrap(); - assert_eq!(result.to_string(), "missing operand"); - - let result = parse_args(vec!["test".to_string()]).err().unwrap(); - assert_eq!( - result.to_string(), - "error parsing argument 'test' to number: invalid float literal" - ); - } - - #[tokio::test] - async fn should_execute() { - let time = Instant::now(); - execute_sleep(vec!["0.1".to_string()]).await.unwrap(); - assert!(time.elapsed().as_millis() >= 100); - } + use std::time::Instant; + + use super::*; + + #[test] + fn should_parse_arg() { + assert_eq!(parse_arg("1").unwrap(), 1.); + assert_eq!(parse_arg("1s").unwrap(), 1.); + assert_eq!(parse_arg("1m").unwrap(), 1. * 60.); + assert_eq!(parse_arg("1h").unwrap(), 1. * 60. * 60.); + assert_eq!(parse_arg("1d").unwrap(), 1. * 60. * 60. * 24.); + assert!(parse_arg("d").err().is_some()); + } + + #[test] + fn should_parse_args() { + let value = parse_args(vec![ + "0.5".to_string(), + "1m".to_string(), + "1.25".to_string(), + ]) + .unwrap(); + assert_eq!(value, 500 + 1000 * 60 + 1250); + + let result = parse_args(vec![]).err().unwrap(); + assert_eq!(result.to_string(), "missing operand"); + + let result = parse_args(vec!["test".to_string()]).err().unwrap(); + assert_eq!( + result.to_string(), + "error parsing argument 'test' to number: invalid float literal" + ); + } + + #[tokio::test] + async fn should_execute() { + let time = Instant::now(); + execute_sleep(vec!["0.1".to_string()]).await.unwrap(); + assert!(time.elapsed().as_millis() >= 100); + } } diff --git a/crates/deno_task_shell/src/shell/commands/unset.rs b/crates/deno_task_shell/src/shell/commands/unset.rs index 9a60c3b..ab9c9e5 100644 --- a/crates/deno_task_shell/src/shell/commands/unset.rs +++ b/crates/deno_task_shell/src/shell/commands/unset.rs @@ -13,76 +13,76 @@ use super::ShellCommandContext; pub struct UnsetCommand; impl ShellCommand for UnsetCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match parse_names(context.args) { - Ok(names) => ExecuteResult::Continue( - 0, - names.into_iter().map(EnvChange::UnsetVar).collect(), - Vec::new(), - ), - Err(err) => { - let _ = context.stderr.write_line(&format!("unset: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match parse_names(context.args) { + Ok(names) => ExecuteResult::Continue( + 0, + names.into_iter().map(EnvChange::UnsetVar).collect(), + Vec::new(), + ), + Err(err) => { + let _ = context.stderr.write_line(&format!("unset: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } } fn parse_names(mut args: Vec) -> Result> { - match args.first() { - None => { - // Running the actual `unset` with no argument completes with success. - Ok(args) - } - Some(flag) if flag == "-f" => bail!("unsupported flag: -f"), - Some(flag) if flag == "-v" => { - // It's fine to use `swap_remove` (instead of `remove`) because the order - // of args doesn't matter for `unset` command. - args.swap_remove(0); - Ok(args) + match args.first() { + None => { + // Running the actual `unset` with no argument completes with success. + Ok(args) + } + Some(flag) if flag == "-f" => bail!("unsupported flag: -f"), + Some(flag) if flag == "-v" => { + // It's fine to use `swap_remove` (instead of `remove`) because the order + // of args doesn't matter for `unset` command. + args.swap_remove(0); + Ok(args) + } + Some(_) => Ok(args), } - Some(_) => Ok(args), - } } #[cfg(test)] mod test { - use super::*; + use super::*; - #[test] - fn parse_args() { - assert_eq!( - parse_names(vec!["VAR1".to_string()]).unwrap(), - vec!["VAR1".to_string()] - ); - assert_eq!( - parse_names(vec!["VAR1".to_string(), "VAR2".to_string()]).unwrap(), - vec!["VAR1".to_string(), "VAR2".to_string()] - ); - assert!(parse_names(vec![]).unwrap().is_empty()); - assert_eq!( - parse_names(vec![ - "-f".to_string(), - "VAR1".to_string(), - "VAR2".to_string() - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -f".to_string() - ); - assert_eq!( - parse_names(vec![ - "-v".to_string(), - "VAR1".to_string(), - "VAR2".to_string() - ]) - .unwrap(), - vec!["VAR2".to_string(), "VAR1".to_string()] - ); - } + #[test] + fn parse_args() { + assert_eq!( + parse_names(vec!["VAR1".to_string()]).unwrap(), + vec!["VAR1".to_string()] + ); + assert_eq!( + parse_names(vec!["VAR1".to_string(), "VAR2".to_string()]).unwrap(), + vec!["VAR1".to_string(), "VAR2".to_string()] + ); + assert!(parse_names(vec![]).unwrap().is_empty()); + assert_eq!( + parse_names(vec![ + "-f".to_string(), + "VAR1".to_string(), + "VAR2".to_string() + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -f".to_string() + ); + assert_eq!( + parse_names(vec![ + "-v".to_string(), + "VAR1".to_string(), + "VAR2".to_string() + ]) + .unwrap(), + vec!["VAR2".to_string(), "VAR1".to_string()] + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/xargs.rs b/crates/deno_task_shell/src/shell/commands/xargs.rs index 7ff01e1..44c2dcf 100644 --- a/crates/deno_task_shell/src/shell/commands/xargs.rs +++ b/crates/deno_task_shell/src/shell/commands/xargs.rs @@ -17,298 +17,310 @@ use super::ShellCommandContext; pub struct XargsCommand; impl ShellCommand for XargsCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - match xargs_collect_args(context.args, context.stdin.clone()) { - Ok(args) => { - // don't select on cancellation here as that will occur at a lower level - (context.execute_command_args)(ExecuteCommandArgsContext { - args, - state: context.state, - stdin: context.stdin, - stdout: context.stdout, - stderr: context.stderr, - }) - .await - } - Err(err) => { - let _ = context.stderr.write_line(&format!("xargs: {err}")); - ExecuteResult::from_exit_code(1) + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + match xargs_collect_args(context.args, context.stdin.clone()) { + Ok(args) => { + // don't select on cancellation here as that will occur at a lower level + (context.execute_command_args)(ExecuteCommandArgsContext { + args, + state: context.state, + stdin: context.stdin, + stdout: context.stdout, + stderr: context.stderr, + }) + .await + } + Err(err) => { + let _ = context.stderr.write_line(&format!("xargs: {err}")); + ExecuteResult::from_exit_code(1) + } + } } - } + .boxed_local() } - .boxed_local() - } } fn xargs_collect_args( - cli_args: Vec, - stdin: ShellPipeReader, + cli_args: Vec, + stdin: ShellPipeReader, ) -> Result> { - let flags = parse_args(cli_args)?; - let mut buf = Vec::new(); - stdin.pipe_to(&mut buf)?; - let text = String::from_utf8(buf)?; - let mut args = flags.initial_args; + let flags = parse_args(cli_args)?; + let mut buf = Vec::new(); + stdin.pipe_to(&mut buf)?; + let text = String::from_utf8(buf)?; + let mut args = flags.initial_args; - if args.is_empty() { - // defaults to echo - args.push("echo".to_string()); - } + if args.is_empty() { + // defaults to echo + args.push("echo".to_string()); + } - if let Some(delim) = &flags.delimiter { - // strip a single trailing newline (xargs seems to do this) - let text = if *delim == '\n' { - if let Some(text) = text.strip_suffix(&delim.to_string()) { - text - } else { - &text - } - } else { - &text - }; + if let Some(delim) = &flags.delimiter { + // strip a single trailing newline (xargs seems to do this) + let text = if *delim == '\n' { + if let Some(text) = text.strip_suffix(&delim.to_string()) { + text + } else { + &text + } + } else { + &text + }; - args.extend(text.split(*delim).map(|t| t.to_string())); - } else if flags.is_null_delimited { - args.extend(text.split('\0').map(|t| t.to_string())); - } else { - args.extend(delimit_blanks(&text)?); - } + args.extend(text.split(*delim).map(|t| t.to_string())); + } else if flags.is_null_delimited { + args.extend(text.split('\0').map(|t| t.to_string())); + } else { + args.extend(delimit_blanks(&text)?); + } - Ok(args) + Ok(args) } fn delimit_blanks(text: &str) -> Result> { - let mut chars = text.chars().peekable(); - let mut result = Vec::new(); - while chars.peek().is_some() { - let mut current = String::new(); - while let Some(c) = chars.next() { - match c { - '\n' | '\t' | ' ' => break, - '"' | '\'' => { - const UNMATCHED_MESSAGE: &str = "unmatched quote; by default quotes are special to xargs unless you use the -0 option"; - let original_quote_char = c; - while let Some(c) = chars.next() { - if c == original_quote_char { - break; - } + let mut chars = text.chars().peekable(); + let mut result = Vec::new(); + while chars.peek().is_some() { + let mut current = String::new(); + while let Some(c) = chars.next() { match c { - '\n' => bail!("{}", UNMATCHED_MESSAGE), - _ => current.push(c), - } - if chars.peek().is_none() { - bail!("{}", UNMATCHED_MESSAGE) + '\n' | '\t' | ' ' => break, + '"' | '\'' => { + const UNMATCHED_MESSAGE: &str = "unmatched quote; by default quotes are special to xargs unless you use the -0 option"; + let original_quote_char = c; + while let Some(c) = chars.next() { + if c == original_quote_char { + break; + } + match c { + '\n' => bail!("{}", UNMATCHED_MESSAGE), + _ => current.push(c), + } + if chars.peek().is_none() { + bail!("{}", UNMATCHED_MESSAGE) + } + } + } + '\\' => { + if matches!( + chars.peek(), + Some('\n' | '\t' | ' ' | '"' | '\'') + ) { + current.push(chars.next().unwrap()); + } else { + current.push(c); + } + } + _ => current.push(c), } - } } - '\\' => { - if matches!(chars.peek(), Some('\n' | '\t' | ' ' | '"' | '\'')) { - current.push(chars.next().unwrap()); - } else { - current.push(c); - } - } - _ => current.push(c), - } - } - if !current.is_empty() { - result.push(current); + if !current.is_empty() { + result.push(current); + } } - } - Ok(result) + Ok(result) } #[derive(Debug, PartialEq)] struct XargsFlags { - initial_args: Vec, - delimiter: Option, - is_null_delimited: bool, + initial_args: Vec, + delimiter: Option, + is_null_delimited: bool, } fn parse_args(args: Vec) -> Result { - fn parse_delimiter(arg: &str) -> Result { - let mut chars = arg.chars(); - if let Some(first_char) = chars.next() { - let mut delimiter = first_char; - if first_char == '\\' { - delimiter = match chars.next() { - // todo(dsherret): support more - Some('n') => '\n', - Some('r') => '\r', - Some('t') => '\t', - Some('\\') => '\\', - Some('0') => '\0', - None => bail!("expected character following escape"), - _ => bail!("unsupported/not implemented escape character"), - }; - } + fn parse_delimiter(arg: &str) -> Result { + let mut chars = arg.chars(); + if let Some(first_char) = chars.next() { + let mut delimiter = first_char; + if first_char == '\\' { + delimiter = match chars.next() { + // todo(dsherret): support more + Some('n') => '\n', + Some('r') => '\r', + Some('t') => '\t', + Some('\\') => '\\', + Some('0') => '\0', + None => bail!("expected character following escape"), + _ => bail!("unsupported/not implemented escape character"), + }; + } - if chars.next().is_some() { - bail!("expected a single byte char delimiter. Found: {}", arg); - } + if chars.next().is_some() { + bail!("expected a single byte char delimiter. Found: {}", arg); + } - Ok(delimiter) - } else { - bail!("expected non-empty delimiter"); + Ok(delimiter) + } else { + bail!("expected non-empty delimiter"); + } } - } - let mut initial_args = Vec::new(); - let mut delimiter = None; - let mut iterator = parse_arg_kinds(&args).into_iter(); - let mut is_null_delimited = false; - while let Some(arg) = iterator.next() { - match arg { - ArgKind::Arg(arg) => { - if arg == "-0" { - is_null_delimited = true; - } else { - initial_args.push(arg.to_string()); - // parse the remainder as arguments - for arg in iterator.by_ref() { - match arg { - ArgKind::Arg(arg) => { - initial_args.push(arg.to_string()); - } - ArgKind::ShortFlag(f) => initial_args.push(format!("-{f}")), - ArgKind::LongFlag(f) => initial_args.push(format!("--{f}")), + let mut initial_args = Vec::new(); + let mut delimiter = None; + let mut iterator = parse_arg_kinds(&args).into_iter(); + let mut is_null_delimited = false; + while let Some(arg) = iterator.next() { + match arg { + ArgKind::Arg(arg) => { + if arg == "-0" { + is_null_delimited = true; + } else { + initial_args.push(arg.to_string()); + // parse the remainder as arguments + for arg in iterator.by_ref() { + match arg { + ArgKind::Arg(arg) => { + initial_args.push(arg.to_string()); + } + ArgKind::ShortFlag(f) => { + initial_args.push(format!("-{f}")) + } + ArgKind::LongFlag(f) => { + initial_args.push(format!("--{f}")) + } + } + } + } } - } - } - } - ArgKind::LongFlag("null") => { - is_null_delimited = true; - } - ArgKind::ShortFlag('d') => match iterator.next() { - Some(ArgKind::Arg(arg)) => { - delimiter = Some(parse_delimiter(arg)?); - } - _ => bail!("expected delimiter argument following -d"), - }, - ArgKind::LongFlag(flag) => { - if let Some(arg) = flag.strip_prefix("delimiter=") { - delimiter = Some(parse_delimiter(arg)?); - } else { - arg.bail_unsupported()? + ArgKind::LongFlag("null") => { + is_null_delimited = true; + } + ArgKind::ShortFlag('d') => match iterator.next() { + Some(ArgKind::Arg(arg)) => { + delimiter = Some(parse_delimiter(arg)?); + } + _ => bail!("expected delimiter argument following -d"), + }, + ArgKind::LongFlag(flag) => { + if let Some(arg) = flag.strip_prefix("delimiter=") { + delimiter = Some(parse_delimiter(arg)?); + } else { + arg.bail_unsupported()? + } + } + _ => arg.bail_unsupported()?, } - } - _ => arg.bail_unsupported()?, } - } - if is_null_delimited && delimiter.is_some() { - bail!("cannot specify both null and delimiter flag") - } + if is_null_delimited && delimiter.is_some() { + bail!("cannot specify both null and delimiter flag") + } - Ok(XargsFlags { - initial_args, - delimiter, - is_null_delimited, - }) + Ok(XargsFlags { + initial_args, + delimiter, + is_null_delimited, + }) } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![]).unwrap(), - XargsFlags { - initial_args: Vec::new(), - delimiter: None, - is_null_delimited: false, - } - ); - assert_eq!( - parse_args(vec![ - "-0".to_string(), - "echo".to_string(), - "2".to_string(), - "-d".to_string(), - "--test=3".to_string() - ]) - .unwrap(), - XargsFlags { - initial_args: vec![ - "echo".to_string(), - "2".to_string(), - "-d".to_string(), - "--test=3".to_string() - ], - delimiter: None, - is_null_delimited: true, - } - ); - assert_eq!( - parse_args(vec![ - "-d".to_string(), - "\\n".to_string(), - "echo".to_string() - ]) - .unwrap(), - XargsFlags { - initial_args: vec!["echo".to_string()], - delimiter: Some('\n'), - is_null_delimited: false, - } - ); - assert_eq!( - parse_args(vec![ - "--delimiter=5".to_string(), - "echo".to_string(), - "-d".to_string() - ]) - .unwrap(), - XargsFlags { - initial_args: vec!["echo".to_string(), "-d".to_string()], - delimiter: Some('5'), - is_null_delimited: false, - } - ); - assert_eq!( - parse_args(vec!["-d".to_string(), "5".to_string(), "-t".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t", - ); - assert_eq!( - parse_args(vec!["-d".to_string(), "-t".to_string()]) - .err() - .unwrap() - .to_string(), - "expected delimiter argument following -d", - ); - assert_eq!( - parse_args(vec!["--delimiter=5".to_string(), "--null".to_string()]) - .err() - .unwrap() - .to_string(), - "cannot specify both null and delimiter flag", - ); - } + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![]).unwrap(), + XargsFlags { + initial_args: Vec::new(), + delimiter: None, + is_null_delimited: false, + } + ); + assert_eq!( + parse_args(vec![ + "-0".to_string(), + "echo".to_string(), + "2".to_string(), + "-d".to_string(), + "--test=3".to_string() + ]) + .unwrap(), + XargsFlags { + initial_args: vec![ + "echo".to_string(), + "2".to_string(), + "-d".to_string(), + "--test=3".to_string() + ], + delimiter: None, + is_null_delimited: true, + } + ); + assert_eq!( + parse_args(vec![ + "-d".to_string(), + "\\n".to_string(), + "echo".to_string() + ]) + .unwrap(), + XargsFlags { + initial_args: vec!["echo".to_string()], + delimiter: Some('\n'), + is_null_delimited: false, + } + ); + assert_eq!( + parse_args(vec![ + "--delimiter=5".to_string(), + "echo".to_string(), + "-d".to_string() + ]) + .unwrap(), + XargsFlags { + initial_args: vec!["echo".to_string(), "-d".to_string()], + delimiter: Some('5'), + is_null_delimited: false, + } + ); + assert_eq!( + parse_args(vec![ + "-d".to_string(), + "5".to_string(), + "-t".to_string() + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t", + ); + assert_eq!( + parse_args(vec!["-d".to_string(), "-t".to_string()]) + .err() + .unwrap() + .to_string(), + "expected delimiter argument following -d", + ); + assert_eq!( + parse_args(vec!["--delimiter=5".to_string(), "--null".to_string()]) + .err() + .unwrap() + .to_string(), + "cannot specify both null and delimiter flag", + ); + } - #[test] - fn should_delimit_blanks() { - assert_eq!( - delimit_blanks("testing this\tout\nhere\n \n\t\t test").unwrap(), - vec!["testing", "this", "out", "here", "test",] - ); - assert_eq!( - delimit_blanks("testing 'this\tout here ' \"now double\"").unwrap(), - vec!["testing", "this\tout here ", "now double"] - ); - assert_eq!( + #[test] + fn should_delimit_blanks() { + assert_eq!( + delimit_blanks("testing this\tout\nhere\n \n\t\t test").unwrap(), + vec!["testing", "this", "out", "here", "test",] + ); + assert_eq!( + delimit_blanks("testing 'this\tout here ' \"now double\"") + .unwrap(), + vec!["testing", "this\tout here ", "now double"] + ); + assert_eq!( delimit_blanks("testing 'this\nout here '").err().unwrap().to_string(), "unmatched quote; by default quotes are special to xargs unless you use the -0 option", ); - } + } } diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index a294095..9a60ce6 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -58,20 +58,20 @@ use super::types::CANCELLATION_EXIT_CODE; /// # Returns /// The exit code of the command execution. pub async fn execute( - list: SequentialList, - env_vars: HashMap, - cwd: &Path, - custom_commands: HashMap>, + list: SequentialList, + env_vars: HashMap, + cwd: &Path, + custom_commands: HashMap>, ) -> i32 { - let state = ShellState::new(env_vars, cwd, custom_commands); - execute_with_pipes( - list, - state, - ShellPipeReader::stdin(), - ShellPipeWriter::stdout(), - ShellPipeWriter::stderr(), - ) - .await + let state = ShellState::new(env_vars, cwd, custom_commands); + execute_with_pipes( + list, + state, + ShellPipeReader::stdin(), + ShellPipeWriter::stdout(), + ShellPipeWriter::stderr(), + ) + .await } /// Executes a `SequentialList` of commands with specified input and output pipes. @@ -92,561 +92,607 @@ pub async fn execute( /// /// The exit code of the command execution. pub async fn execute_with_pipes( - list: SequentialList, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + list: SequentialList, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> i32 { - // spawn a sequential list and pipe its output to the environment - let result = execute_sequential_list( - list, - state, - stdin, - stdout, - stderr, - AsyncCommandBehavior::Wait, - ) - .await; - - match result { - ExecuteResult::Exit(code, _) => code, - ExecuteResult::Continue(exit_code, _, _) => exit_code, - } + // spawn a sequential list and pipe its output to the environment + let result = execute_sequential_list( + list, + state, + stdin, + stdout, + stderr, + AsyncCommandBehavior::Wait, + ) + .await; + + match result { + ExecuteResult::Exit(code, _) => code, + ExecuteResult::Continue(exit_code, _, _) => exit_code, + } } #[derive(Debug, PartialEq)] enum AsyncCommandBehavior { - Wait, - Yield, + Wait, + Yield, } fn execute_sequential_list( - list: SequentialList, - mut state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, - async_command_behavior: AsyncCommandBehavior, + list: SequentialList, + mut state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, + async_command_behavior: AsyncCommandBehavior, ) -> FutureExecuteResult { - async move { - let mut final_exit_code = 0; - let mut final_changes = Vec::new(); - let mut async_handles = Vec::new(); - let mut was_exit = false; - for item in list.items { - if item.is_async { - let state = state.clone(); - let stdin = stdin.clone(); - let stdout = stdout.clone(); - let stderr = stderr.clone(); - async_handles.push(tokio::task::spawn_local(async move { - let main_token = state.token().clone(); - let result = - execute_sequence(item.sequence, state, stdin, stdout, stderr).await; - let (exit_code, handles) = result.into_exit_code_and_handles(); - wait_handles(exit_code, handles, main_token).await - })); - } else { - let result = execute_sequence( - item.sequence, - state.clone(), - stdin.clone(), - stdout.clone(), - stderr.clone(), - ) - .await; - match result { - ExecuteResult::Exit(exit_code, handles) => { - async_handles.extend(handles); - final_exit_code = exit_code; - was_exit = true; - break; - } - ExecuteResult::Continue(exit_code, changes, handles) => { - state.apply_changes(&changes); - state.apply_env_var("?", &exit_code.to_string()); - final_changes.extend(changes); - async_handles.extend(handles); - // use the final sequential item's exit code - final_exit_code = exit_code; - } + async move { + let mut final_exit_code = 0; + let mut final_changes = Vec::new(); + let mut async_handles = Vec::new(); + let mut was_exit = false; + for item in list.items { + if item.is_async { + let state = state.clone(); + let stdin = stdin.clone(); + let stdout = stdout.clone(); + let stderr = stderr.clone(); + async_handles.push(tokio::task::spawn_local(async move { + let main_token = state.token().clone(); + let result = execute_sequence( + item.sequence, + state, + stdin, + stdout, + stderr, + ) + .await; + let (exit_code, handles) = + result.into_exit_code_and_handles(); + wait_handles(exit_code, handles, main_token).await + })); + } else { + let result = execute_sequence( + item.sequence, + state.clone(), + stdin.clone(), + stdout.clone(), + stderr.clone(), + ) + .await; + match result { + ExecuteResult::Exit(exit_code, handles) => { + async_handles.extend(handles); + final_exit_code = exit_code; + was_exit = true; + break; + } + ExecuteResult::Continue(exit_code, changes, handles) => { + state.apply_changes(&changes); + state.apply_env_var("?", &exit_code.to_string()); + final_changes.extend(changes); + async_handles.extend(handles); + // use the final sequential item's exit code + final_exit_code = exit_code; + } + } + } } - } - } - // wait for async commands to complete - if async_command_behavior == AsyncCommandBehavior::Wait { - final_exit_code = wait_handles( - final_exit_code, - std::mem::take(&mut async_handles), - state.token().clone(), - ) - .await; - } + // wait for async commands to complete + if async_command_behavior == AsyncCommandBehavior::Wait { + final_exit_code = wait_handles( + final_exit_code, + std::mem::take(&mut async_handles), + state.token().clone(), + ) + .await; + } - if was_exit { - ExecuteResult::Exit(final_exit_code, async_handles) - } else { - ExecuteResult::Continue(final_exit_code, final_changes, async_handles) + if was_exit { + ExecuteResult::Exit(final_exit_code, async_handles) + } else { + ExecuteResult::Continue( + final_exit_code, + final_changes, + async_handles, + ) + } } - } - .boxed_local() + .boxed_local() } async fn wait_handles( - mut exit_code: i32, - mut handles: Vec>, - token: CancellationToken, + mut exit_code: i32, + mut handles: Vec>, + token: CancellationToken, ) -> i32 { - if exit_code != 0 { - token.cancel(); - } - while !handles.is_empty() { - let result = futures::future::select_all(handles).await; - - // prefer the first non-zero then non-cancellation exit code - let new_exit_code = result.0.unwrap(); - if matches!(exit_code, 0 | CANCELLATION_EXIT_CODE) && new_exit_code != 0 { - exit_code = new_exit_code; + if exit_code != 0 { + token.cancel(); } + while !handles.is_empty() { + let result = futures::future::select_all(handles).await; + + // prefer the first non-zero then non-cancellation exit code + let new_exit_code = result.0.unwrap(); + if matches!(exit_code, 0 | CANCELLATION_EXIT_CODE) && new_exit_code != 0 + { + exit_code = new_exit_code; + } - handles = result.2; - } - exit_code + handles = result.2; + } + exit_code } fn execute_sequence( - sequence: Sequence, - mut state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + sequence: Sequence, + mut state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> FutureExecuteResult { - // requires boxed async because of recursive async - async move { - match sequence { - Sequence::ShellVar(var) => ExecuteResult::Continue( - 0, - vec![EnvChange::SetShellVar( - var.name, - match evaluate_word(var.value, &state, stdin, stderr.clone()).await { - Ok(value) => value, - Err(err) => { - return err.into_exit_code(&mut stderr); - } - }, - )], - Vec::new(), - ), - Sequence::BooleanList(list) => { - let mut changes = vec![]; - let first_result = execute_sequence( - list.current, - state.clone(), - stdin.clone(), - stdout.clone(), - stderr.clone(), - ) - .await; - let (exit_code, mut async_handles) = match first_result { - ExecuteResult::Exit(_, _) => return first_result, - ExecuteResult::Continue(exit_code, sub_changes, async_handles) => { - state.apply_env_var("?", &exit_code.to_string()); - state.apply_changes(&sub_changes); - changes.extend(sub_changes); - (exit_code, async_handles) - } - }; + // requires boxed async because of recursive async + async move { + match sequence { + Sequence::ShellVar(var) => ExecuteResult::Continue( + 0, + vec![EnvChange::SetShellVar( + var.name, + match evaluate_word( + var.value, + &state, + stdin, + stderr.clone(), + ) + .await + { + Ok(value) => value, + Err(err) => { + return err.into_exit_code(&mut stderr); + } + }, + )], + Vec::new(), + ), + Sequence::BooleanList(list) => { + let mut changes = vec![]; + let first_result = execute_sequence( + list.current, + state.clone(), + stdin.clone(), + stdout.clone(), + stderr.clone(), + ) + .await; + let (exit_code, mut async_handles) = match first_result { + ExecuteResult::Exit(_, _) => return first_result, + ExecuteResult::Continue( + exit_code, + sub_changes, + async_handles, + ) => { + state.apply_env_var("?", &exit_code.to_string()); + state.apply_changes(&sub_changes); + changes.extend(sub_changes); + (exit_code, async_handles) + } + }; - let next = if list.op.moves_next_for_exit_code(exit_code) { - Some(list.next) - } else { - let mut next = list.next; - loop { - // boolean lists always move right on the tree - match next { - Sequence::BooleanList(list) => { - if list.op.moves_next_for_exit_code(exit_code) { - break Some(list.next); + let next = if list.op.moves_next_for_exit_code(exit_code) { + Some(list.next) + } else { + let mut next = list.next; + loop { + // boolean lists always move right on the tree + match next { + Sequence::BooleanList(list) => { + if list.op.moves_next_for_exit_code(exit_code) { + break Some(list.next); + } + next = list.next; + } + _ => break None, + } + } + }; + if let Some(next) = next { + let next_result = + execute_sequence(next, state, stdin, stdout, stderr) + .await; + match next_result { + ExecuteResult::Exit(code, sub_handles) => { + async_handles.extend(sub_handles); + ExecuteResult::Exit(code, async_handles) + } + ExecuteResult::Continue( + exit_code, + sub_changes, + sub_handles, + ) => { + changes.extend(sub_changes); + async_handles.extend(sub_handles); + ExecuteResult::Continue( + exit_code, + changes, + async_handles, + ) + } + } + } else { + ExecuteResult::Continue(exit_code, changes, async_handles) } - next = list.next; - } - _ => break None, } - } - }; - if let Some(next) = next { - let next_result = - execute_sequence(next, state, stdin, stdout, stderr).await; - match next_result { - ExecuteResult::Exit(code, sub_handles) => { - async_handles.extend(sub_handles); - ExecuteResult::Exit(code, async_handles) - } - ExecuteResult::Continue(exit_code, sub_changes, sub_handles) => { - changes.extend(sub_changes); - async_handles.extend(sub_handles); - ExecuteResult::Continue(exit_code, changes, async_handles) + Sequence::Pipeline(pipeline) => { + execute_pipeline(pipeline, state, stdin, stdout, stderr).await } - } - } else { - ExecuteResult::Continue(exit_code, changes, async_handles) } - } - Sequence::Pipeline(pipeline) => { - execute_pipeline(pipeline, state, stdin, stdout, stderr).await - } } - } - .boxed_local() + .boxed_local() } async fn execute_pipeline( - pipeline: Pipeline, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + pipeline: Pipeline, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - let result = - execute_pipeline_inner(pipeline.inner, state, stdin, stdout, stderr).await; - if pipeline.negated { - match result { - ExecuteResult::Exit(code, handles) => ExecuteResult::Exit(code, handles), - ExecuteResult::Continue(code, changes, handles) => { - let new_code = if code == 0 { 1 } else { 0 }; - ExecuteResult::Continue(new_code, changes, handles) - } + let result = + execute_pipeline_inner(pipeline.inner, state, stdin, stdout, stderr) + .await; + if pipeline.negated { + match result { + ExecuteResult::Exit(code, handles) => { + ExecuteResult::Exit(code, handles) + } + ExecuteResult::Continue(code, changes, handles) => { + let new_code = if code == 0 { 1 } else { 0 }; + ExecuteResult::Continue(new_code, changes, handles) + } + } + } else { + result } - } else { - result - } } async fn execute_pipeline_inner( - pipeline: PipelineInner, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + pipeline: PipelineInner, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - match pipeline { - PipelineInner::Command(command) => { - execute_command(command, state, stdin, stdout, stderr).await - } - PipelineInner::PipeSequence(pipe_sequence) => { - execute_pipe_sequence(*pipe_sequence, state, stdin, stdout, stderr).await + match pipeline { + PipelineInner::Command(command) => { + execute_command(command, state, stdin, stdout, stderr).await + } + PipelineInner::PipeSequence(pipe_sequence) => { + execute_pipe_sequence(*pipe_sequence, state, stdin, stdout, stderr) + .await + } } - } } #[derive(Debug)] enum RedirectPipe { - Input(ShellPipeReader), - Output(ShellPipeWriter), + Input(ShellPipeReader), + Output(ShellPipeWriter), } async fn resolve_redirect_pipe( - redirect: &Redirect, - state: &ShellState, - stdin: &ShellPipeReader, - stdout: &ShellPipeWriter, - stderr: &mut ShellPipeWriter, + redirect: &Redirect, + state: &ShellState, + stdin: &ShellPipeReader, + stdout: &ShellPipeWriter, + stderr: &mut ShellPipeWriter, ) -> Result { - match redirect.io_file.clone() { - IoFile::Word(word) => { - resolve_redirect_word_pipe(word, &redirect.op, state, stdin, stderr).await - } - IoFile::Fd(fd) => match &redirect.op { - RedirectOp::Input(RedirectOpInput::Redirect) => { - let _ = stderr.write_line( + match redirect.io_file.clone() { + IoFile::Word(word) => { + resolve_redirect_word_pipe(word, &redirect.op, state, stdin, stderr) + .await + } + IoFile::Fd(fd) => match &redirect.op { + RedirectOp::Input(RedirectOpInput::Redirect) => { + let _ = stderr.write_line( "deno_task_shell: input redirecting file descriptors is not implemented", ); - Err(ExecuteResult::from_exit_code(1)) - } - RedirectOp::Output(_op) => match fd { - 1 => Ok(RedirectPipe::Output(stdout.clone())), - 2 => Ok(RedirectPipe::Output(stderr.clone())), - _ => { - let _ = stderr.write_line( + Err(ExecuteResult::from_exit_code(1)) + } + RedirectOp::Output(_op) => match fd { + 1 => Ok(RedirectPipe::Output(stdout.clone())), + 2 => Ok(RedirectPipe::Output(stderr.clone())), + _ => { + let _ = stderr.write_line( "deno_task_shell: output redirecting file descriptors beyond stdout and stderr is not implemented", ); - Err(ExecuteResult::from_exit_code(1)) - } - }, - }, - } + Err(ExecuteResult::from_exit_code(1)) + } + }, + }, + } } async fn resolve_redirect_word_pipe( - word: Word, - redirect_op: &RedirectOp, - state: &ShellState, - stdin: &ShellPipeReader, - stderr: &mut ShellPipeWriter, -) -> Result { - fn handle_std_result( - output_path: &Path, - std_file_result: std::io::Result, + word: Word, + redirect_op: &RedirectOp, + state: &ShellState, + stdin: &ShellPipeReader, stderr: &mut ShellPipeWriter, - ) -> Result { - match std_file_result { - Ok(std_file) => Ok(std_file), - Err(err) => { +) -> Result { + fn handle_std_result( + output_path: &Path, + std_file_result: std::io::Result, + stderr: &mut ShellPipeWriter, + ) -> Result { + match std_file_result { + Ok(std_file) => Ok(std_file), + Err(err) => { + let _ = stderr.write_line(&format!( + "error opening file for redirect ({}). {:#}", + output_path.display(), + err + )); + Err(ExecuteResult::from_exit_code(1)) + } + } + } + + let words = evaluate_word_parts( + word.into_parts(), + state, + stdin.clone(), + stderr.clone(), + ) + .await; + let words = match words { + Ok(word) => word, + Err(err) => { + return Err(err.into_exit_code(stderr)); + } + }; + // edge case that's not supported + if words.is_empty() { + let _ = + stderr.write_line("redirect path must be 1 argument, but found 0"); + return Err(ExecuteResult::from_exit_code(1)); + } else if words.len() > 1 { let _ = stderr.write_line(&format!( - "error opening file for redirect ({}). {:#}", - output_path.display(), - err + concat!( + "redirect path must be 1 argument, but found {0} ({1}). ", + "Did you mean to quote it (ex. \"{1}\")?" + ), + words.len(), + words.join(" ") )); - Err(ExecuteResult::from_exit_code(1)) - } - } - } - - let words = evaluate_word_parts( - word.into_parts(), - state, - stdin.clone(), - stderr.clone(), - ) - .await; - let words = match words { - Ok(word) => word, - Err(err) => { - return Err(err.into_exit_code(stderr)); - } - }; - // edge case that's not supported - if words.is_empty() { - let _ = stderr.write_line("redirect path must be 1 argument, but found 0"); - return Err(ExecuteResult::from_exit_code(1)); - } else if words.len() > 1 { - let _ = stderr.write_line(&format!( - concat!( - "redirect path must be 1 argument, but found {0} ({1}). ", - "Did you mean to quote it (ex. \"{1}\")?" - ), - words.len(), - words.join(" ") - )); - return Err(ExecuteResult::from_exit_code(1)); - } - let output_path = &words[0]; - - match &redirect_op { - RedirectOp::Input(RedirectOpInput::Redirect) => { - let output_path = state.cwd().join(output_path); - let std_file_result = - std::fs::OpenOptions::new().read(true).open(&output_path); - handle_std_result(&output_path, std_file_result, stderr).map(|std_file| { - RedirectPipe::Input(ShellPipeReader::from_std(std_file)) - }) + return Err(ExecuteResult::from_exit_code(1)); } - RedirectOp::Output(op) => { - // cross platform suppress output - if output_path == "/dev/null" { - return Ok(RedirectPipe::Output(ShellPipeWriter::null())); - } - let output_path = state.cwd().join(output_path); - let is_append = *op == RedirectOpOutput::Append; - let std_file_result = std::fs::OpenOptions::new() - .write(true) - .create(true) - .append(is_append) - .truncate(!is_append) - .open(&output_path); - handle_std_result(&output_path, std_file_result, stderr).map(|std_file| { - RedirectPipe::Output(ShellPipeWriter::from_std(std_file)) - }) + let output_path = &words[0]; + + match &redirect_op { + RedirectOp::Input(RedirectOpInput::Redirect) => { + let output_path = state.cwd().join(output_path); + let std_file_result = + std::fs::OpenOptions::new().read(true).open(&output_path); + handle_std_result(&output_path, std_file_result, stderr).map( + |std_file| { + RedirectPipe::Input(ShellPipeReader::from_std(std_file)) + }, + ) + } + RedirectOp::Output(op) => { + // cross platform suppress output + if output_path == "/dev/null" { + return Ok(RedirectPipe::Output(ShellPipeWriter::null())); + } + let output_path = state.cwd().join(output_path); + let is_append = *op == RedirectOpOutput::Append; + let std_file_result = std::fs::OpenOptions::new() + .write(true) + .create(true) + .append(is_append) + .truncate(!is_append) + .open(&output_path); + handle_std_result(&output_path, std_file_result, stderr).map( + |std_file| { + RedirectPipe::Output(ShellPipeWriter::from_std(std_file)) + }, + ) + } } - } } async fn execute_command( - command: Command, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + command: Command, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - let (stdin, stdout, stderr) = if let Some(redirect) = &command.redirect { - let pipe = match resolve_redirect_pipe( - redirect, - &state, - &stdin, - &stdout, - &mut stderr, - ) - .await - { - Ok(value) => value, - Err(value) => return value, - }; - match pipe { - RedirectPipe::Input(pipe) => match redirect.maybe_fd { - Some(_) => { - let _ = stderr.write_line( + let (stdin, stdout, stderr) = if let Some(redirect) = &command.redirect { + let pipe = match resolve_redirect_pipe( + redirect, + &state, + &stdin, + &stdout, + &mut stderr, + ) + .await + { + Ok(value) => value, + Err(value) => return value, + }; + match pipe { + RedirectPipe::Input(pipe) => match redirect.maybe_fd { + Some(_) => { + let _ = stderr.write_line( "input redirects with file descriptors are not supported", ); - return ExecuteResult::from_exit_code(1); - } - None => (pipe, stdout, stderr), - }, - RedirectPipe::Output(pipe) => match redirect.maybe_fd { - Some(RedirectFd::Fd(2)) => (stdin, stdout, pipe), - Some(RedirectFd::Fd(1)) | None => (stdin, pipe, stderr), - Some(RedirectFd::Fd(_)) => { - let _ = stderr.write_line( + return ExecuteResult::from_exit_code(1); + } + None => (pipe, stdout, stderr), + }, + RedirectPipe::Output(pipe) => match redirect.maybe_fd { + Some(RedirectFd::Fd(2)) => (stdin, stdout, pipe), + Some(RedirectFd::Fd(1)) | None => (stdin, pipe, stderr), + Some(RedirectFd::Fd(_)) => { + let _ = stderr.write_line( "only redirecting to stdout (1) and stderr (2) is supported", ); - return ExecuteResult::from_exit_code(1); + return ExecuteResult::from_exit_code(1); + } + Some(RedirectFd::StdoutStderr) => (stdin, pipe.clone(), pipe), + }, + } + } else { + (stdin, stdout, stderr) + }; + match command.inner { + CommandInner::Simple(command) => { + execute_simple_command(command, state, stdin, stdout, stderr).await + } + CommandInner::Subshell(list) => { + execute_subshell(list, state, stdin, stdout, stderr).await } - Some(RedirectFd::StdoutStderr) => (stdin, pipe.clone(), pipe), - }, - } - } else { - (stdin, stdout, stderr) - }; - match command.inner { - CommandInner::Simple(command) => { - execute_simple_command(command, state, stdin, stdout, stderr).await - } - CommandInner::Subshell(list) => { - execute_subshell(list, state, stdin, stdout, stderr).await } - } } async fn execute_pipe_sequence( - pipe_sequence: PipeSequence, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + pipe_sequence: PipeSequence, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - let mut wait_tasks = vec![]; - let mut last_output = Some(stdin); - let mut next_inner: Option = Some(pipe_sequence.into()); - while let Some(sequence) = next_inner.take() { - let (output_reader, output_writer) = pipe(); - let (stderr, command) = match sequence { - PipelineInner::PipeSequence(pipe_sequence) => { - next_inner = Some(pipe_sequence.next); - ( - match pipe_sequence.op { - PipeSequenceOperator::Stdout => stderr.clone(), - PipeSequenceOperator::StdoutStderr => output_writer.clone(), - }, - pipe_sequence.current, - ) - } - PipelineInner::Command(command) => (stderr.clone(), command), - }; - wait_tasks.push(execute_command( - command, - state.clone(), - last_output.take().unwrap(), - output_writer.clone(), - stderr.clone(), - )); - last_output = Some(output_reader); - } - let output_handle = tokio::task::spawn_blocking(|| { - last_output.unwrap().pipe_to_sender(stdout).unwrap(); - }); - let mut results = futures::future::join_all(wait_tasks).await; - output_handle.await.unwrap(); - let last_result = results.pop().unwrap(); - let all_handles = results.into_iter().flat_map(|r| r.into_handles()); - match last_result { - ExecuteResult::Exit(code, mut handles) => { - handles.extend(all_handles); - ExecuteResult::Continue(code, Vec::new(), handles) + let mut wait_tasks = vec![]; + let mut last_output = Some(stdin); + let mut next_inner: Option = Some(pipe_sequence.into()); + while let Some(sequence) = next_inner.take() { + let (output_reader, output_writer) = pipe(); + let (stderr, command) = match sequence { + PipelineInner::PipeSequence(pipe_sequence) => { + next_inner = Some(pipe_sequence.next); + ( + match pipe_sequence.op { + PipeSequenceOperator::Stdout => stderr.clone(), + PipeSequenceOperator::StdoutStderr => { + output_writer.clone() + } + }, + pipe_sequence.current, + ) + } + PipelineInner::Command(command) => (stderr.clone(), command), + }; + wait_tasks.push(execute_command( + command, + state.clone(), + last_output.take().unwrap(), + output_writer.clone(), + stderr.clone(), + )); + last_output = Some(output_reader); } - ExecuteResult::Continue(code, _, mut handles) => { - handles.extend(all_handles); - ExecuteResult::Continue(code, Vec::new(), handles) + let output_handle = tokio::task::spawn_blocking(|| { + last_output.unwrap().pipe_to_sender(stdout).unwrap(); + }); + let mut results = futures::future::join_all(wait_tasks).await; + output_handle.await.unwrap(); + let last_result = results.pop().unwrap(); + let all_handles = results.into_iter().flat_map(|r| r.into_handles()); + match last_result { + ExecuteResult::Exit(code, mut handles) => { + handles.extend(all_handles); + ExecuteResult::Continue(code, Vec::new(), handles) + } + ExecuteResult::Continue(code, _, mut handles) => { + handles.extend(all_handles); + ExecuteResult::Continue(code, Vec::new(), handles) + } } - } } async fn execute_subshell( - list: Box, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + list: Box, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - let result = execute_sequential_list( - *list, - state, - stdin, - stdout, - stderr, - // yield async commands to the parent - AsyncCommandBehavior::Yield, - ) - .await; - - match result { - ExecuteResult::Exit(code, handles) => { - // sub shells do not cause an exit - ExecuteResult::Continue(code, Vec::new(), handles) - } - ExecuteResult::Continue(code, _env_changes, handles) => { - // env changes are not propagated - ExecuteResult::Continue(code, Vec::new(), handles) + let result = execute_sequential_list( + *list, + state, + stdin, + stdout, + stderr, + // yield async commands to the parent + AsyncCommandBehavior::Yield, + ) + .await; + + match result { + ExecuteResult::Exit(code, handles) => { + // sub shells do not cause an exit + ExecuteResult::Continue(code, Vec::new(), handles) + } + ExecuteResult::Continue(code, _env_changes, handles) => { + // env changes are not propagated + ExecuteResult::Continue(code, Vec::new(), handles) + } } - } } async fn execute_simple_command( - command: SimpleCommand, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + command: SimpleCommand, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - let args = - evaluate_args(command.args, &state, stdin.clone(), stderr.clone()).await; - let args = match args { - Ok(args) => args, - Err(err) => { - return err.into_exit_code(&mut stderr); - } - }; - let mut state = state.clone(); - for env_var in command.env_vars { - let value = - evaluate_word(env_var.value, &state, stdin.clone(), stderr.clone()).await; - let value = match value { - Ok(value) => value, - Err(err) => { - return err.into_exit_code(&mut stderr); - } + let args = + evaluate_args(command.args, &state, stdin.clone(), stderr.clone()) + .await; + let args = match args { + Ok(args) => args, + Err(err) => { + return err.into_exit_code(&mut stderr); + } }; - state.apply_env_var(&env_var.name, &value); - } - execute_command_args(args, state, stdin, stdout, stderr).await + let mut state = state.clone(); + for env_var in command.env_vars { + let value = + evaluate_word(env_var.value, &state, stdin.clone(), stderr.clone()) + .await; + let value = match value { + Ok(value) => value, + Err(err) => { + return err.into_exit_code(&mut stderr); + } + }; + state.apply_env_var(&env_var.name, &value); + } + execute_command_args(args, state, stdin, stdout, stderr).await } fn execute_command_args( - mut args: Vec, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + mut args: Vec, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> FutureExecuteResult { - let command_name = if args.is_empty() { - String::new() - } else { - args.remove(0) - }; - if state.token().is_cancelled() { - Box::pin(future::ready(ExecuteResult::for_cancellation())) - } else if let Some(stripped_name) = command_name.strip_prefix('!') { - let _ = stderr.write_line( + let command_name = if args.is_empty() { + String::new() + } else { + args.remove(0) + }; + if state.token().is_cancelled() { + Box::pin(future::ready(ExecuteResult::for_cancellation())) + } else if let Some(stripped_name) = command_name.strip_prefix('!') { + let _ = stderr.write_line( &format!(concat!( "History expansion is not supported:\n", " {}\n", @@ -655,341 +701,347 @@ fn execute_command_args( " ! {}", ), command_name, stripped_name) ); - Box::pin(future::ready(ExecuteResult::from_exit_code(1))) - } else { - let command_context = ShellCommandContext { - args, - state, - stdin, - stdout, - stderr, - execute_command_args: Box::new(move |context| { - execute_command_args( - context.args, - context.state, - context.stdin, - context.stdout, - context.stderr, - ) - }), - }; - match command_context.state.resolve_custom_command(&command_name) { - Some(command) => command.execute(command_context), - None => execute_unresolved_command_name( - UnresolvedCommandName { - name: command_name, - base_dir: command_context.state.cwd().to_path_buf(), - }, - command_context, - ), + Box::pin(future::ready(ExecuteResult::from_exit_code(1))) + } else { + let command_context = ShellCommandContext { + args, + state, + stdin, + stdout, + stderr, + execute_command_args: Box::new(move |context| { + execute_command_args( + context.args, + context.state, + context.stdin, + context.stdout, + context.stderr, + ) + }), + }; + match command_context.state.resolve_custom_command(&command_name) { + Some(command) => command.execute(command_context), + None => execute_unresolved_command_name( + UnresolvedCommandName { + name: command_name, + base_dir: command_context.state.cwd().to_path_buf(), + }, + command_context, + ), + } } - } } pub async fn evaluate_args( - args: Vec, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + args: Vec, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> Result, EvaluateWordTextError> { - let mut result = Vec::new(); - for arg in args { - let parts = evaluate_word_parts( - arg.into_parts(), - state, - stdin.clone(), - stderr.clone(), - ) - .await?; - result.extend(parts); - } - Ok(result) + let mut result = Vec::new(); + for arg in args { + let parts = evaluate_word_parts( + arg.into_parts(), + state, + stdin.clone(), + stderr.clone(), + ) + .await?; + result.extend(parts); + } + Ok(result) } async fn evaluate_word( - word: Word, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + word: Word, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> Result { - Ok( - evaluate_word_parts(word.into_parts(), state, stdin, stderr) - .await? - .join(" "), - ) + Ok(evaluate_word_parts(word.into_parts(), state, stdin, stderr) + .await? + .join(" ")) } #[derive(Debug, Error)] pub enum EvaluateWordTextError { - #[error("glob: no matches found '{}'. {}", pattern, err)] - InvalidPattern { - pattern: String, - err: glob::PatternError, - }, - #[error("glob: no matches found '{}'", pattern)] - NoFilesMatched { pattern: String }, + #[error("glob: no matches found '{}'. {}", pattern, err)] + InvalidPattern { + pattern: String, + err: glob::PatternError, + }, + #[error("glob: no matches found '{}'", pattern)] + NoFilesMatched { pattern: String }, } impl EvaluateWordTextError { - pub fn into_exit_code(self, stderr: &mut ShellPipeWriter) -> ExecuteResult { - let _ = stderr.write_line(&self.to_string()); - ExecuteResult::from_exit_code(1) - } + pub fn into_exit_code(self, stderr: &mut ShellPipeWriter) -> ExecuteResult { + let _ = stderr.write_line(&self.to_string()); + ExecuteResult::from_exit_code(1) + } } fn evaluate_word_parts( - parts: Vec, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + parts: Vec, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> LocalBoxFuture, EvaluateWordTextError>> { - #[derive(Debug)] - enum TextPart { - Quoted(String), - Text(String), - } - - impl TextPart { - pub fn as_str(&self) -> &str { - match self { - TextPart::Quoted(text) => text, - TextPart::Text(text) => text, - } + #[derive(Debug)] + enum TextPart { + Quoted(String), + Text(String), } - } - fn text_parts_to_string(parts: Vec) -> String { - let mut result = - String::with_capacity(parts.iter().map(|p| p.as_str().len()).sum()); - for part in parts { - result.push_str(part.as_str()); + impl TextPart { + pub fn as_str(&self) -> &str { + match self { + TextPart::Quoted(text) => text, + TextPart::Text(text) => text, + } + } } - result - } - fn evaluate_word_text( - state: &ShellState, - text_parts: Vec, - is_quoted: bool, - ) -> Result, EvaluateWordTextError> { - if !is_quoted - && text_parts - .iter() - .filter_map(|p| match p { - TextPart::Quoted(_) => None, - TextPart::Text(text) => Some(text.as_str()), - }) - .any(|text| text.chars().any(|c| matches!(c, '?' | '*' | '['))) - { - let mut current_text = String::new(); - for text_part in text_parts { - match text_part { - TextPart::Quoted(text) => { - for c in text.chars() { - match c { - '?' | '*' | '[' | ']' => { - // escape because it was quoted - current_text.push('['); - current_text.push(c); - current_text.push(']'); + fn text_parts_to_string(parts: Vec) -> String { + let mut result = + String::with_capacity(parts.iter().map(|p| p.as_str().len()).sum()); + for part in parts { + result.push_str(part.as_str()); + } + result + } + + fn evaluate_word_text( + state: &ShellState, + text_parts: Vec, + is_quoted: bool, + ) -> Result, EvaluateWordTextError> { + if !is_quoted + && text_parts + .iter() + .filter_map(|p| match p { + TextPart::Quoted(_) => None, + TextPart::Text(text) => Some(text.as_str()), + }) + .any(|text| text.chars().any(|c| matches!(c, '?' | '*' | '['))) + { + let mut current_text = String::new(); + for text_part in text_parts { + match text_part { + TextPart::Quoted(text) => { + for c in text.chars() { + match c { + '?' | '*' | '[' | ']' => { + // escape because it was quoted + current_text.push('['); + current_text.push(c); + current_text.push(']'); + } + _ => current_text.push(c), + } + } + } + TextPart::Text(text) => { + current_text.push_str(&text); + } } - _ => current_text.push(c), - } } - } - TextPart::Text(text) => { - current_text.push_str(&text); - } - } - } - let is_absolute = std::path::PathBuf::from(¤t_text).is_absolute(); - let cwd = state.cwd(); - let pattern = if is_absolute { - current_text - } else { - format!("{}/{}", cwd.display(), current_text) - }; - let result = glob::glob_with( - &pattern, - glob::MatchOptions { - // false because it should work the same way on case insensitive file systems - case_sensitive: false, - // true because it copies what sh does - require_literal_separator: true, - // true because it copies with sh does—these files are considered "hidden" - require_literal_leading_dot: true, - }, - ); - match result { - Ok(paths) => { - let paths = - paths.into_iter().filter_map(|p| p.ok()).collect::>(); - if paths.is_empty() { - Err(EvaluateWordTextError::NoFilesMatched { pattern }) - } else { - let paths = if is_absolute { - paths - .into_iter() - .map(|p| p.display().to_string()) - .collect::>() + let is_absolute = + std::path::PathBuf::from(¤t_text).is_absolute(); + let cwd = state.cwd(); + let pattern = if is_absolute { + current_text } else { - paths - .into_iter() - .map(|p| { - let path = p.strip_prefix(cwd).unwrap(); - path.display().to_string() - }) - .collect::>() + format!("{}/{}", cwd.display(), current_text) }; - Ok(paths) - } + let result = glob::glob_with( + &pattern, + glob::MatchOptions { + // false because it should work the same way on case insensitive file systems + case_sensitive: false, + // true because it copies what sh does + require_literal_separator: true, + // true because it copies with sh does—these files are considered "hidden" + require_literal_leading_dot: true, + }, + ); + match result { + Ok(paths) => { + let paths = paths + .into_iter() + .filter_map(|p| p.ok()) + .collect::>(); + if paths.is_empty() { + Err(EvaluateWordTextError::NoFilesMatched { pattern }) + } else { + let paths = if is_absolute { + paths + .into_iter() + .map(|p| p.display().to_string()) + .collect::>() + } else { + paths + .into_iter() + .map(|p| { + let path = p.strip_prefix(cwd).unwrap(); + path.display().to_string() + }) + .collect::>() + }; + Ok(paths) + } + } + Err(err) => { + Err(EvaluateWordTextError::InvalidPattern { pattern, err }) + } + } + } else { + Ok(vec![text_parts_to_string(text_parts)]) } - Err(err) => Err(EvaluateWordTextError::InvalidPattern { pattern, err }), - } - } else { - Ok(vec![text_parts_to_string(text_parts)]) } - } - fn evaluate_word_parts_inner( - parts: Vec, - is_quoted: bool, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, - ) -> LocalBoxFuture, EvaluateWordTextError>> { - // recursive async, so requires boxing - async move { - let mut result = Vec::new(); - let mut current_text = Vec::new(); - for part in parts { - let evaluation_result_text = match part { - WordPart::Text(text) => { - current_text.push(TextPart::Text(text)); - None - } - WordPart::Variable(name) => { - state.get_var(&name).map(|v| v.to_string()) - } - WordPart::Command(list) => Some( - evaluate_command_substitution( - list, - // contain cancellation to the command substitution - &state.with_child_token(), - stdin.clone(), - stderr.clone(), - ) - .await, - ), - WordPart::Quoted(parts) => { - let text = evaluate_word_parts_inner( - parts, - true, - state, - stdin.clone(), - stderr.clone(), - ) - .await? - .join(" "); + fn evaluate_word_parts_inner( + parts: Vec, + is_quoted: bool, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, + ) -> LocalBoxFuture, EvaluateWordTextError>> { + // recursive async, so requires boxing + async move { + let mut result = Vec::new(); + let mut current_text = Vec::new(); + for part in parts { + let evaluation_result_text = match part { + WordPart::Text(text) => { + current_text.push(TextPart::Text(text)); + None + } + WordPart::Variable(name) => { + state.get_var(&name).map(|v| v.to_string()) + } + WordPart::Command(list) => Some( + evaluate_command_substitution( + list, + // contain cancellation to the command substitution + &state.with_child_token(), + stdin.clone(), + stderr.clone(), + ) + .await, + ), + WordPart::Quoted(parts) => { + let text = evaluate_word_parts_inner( + parts, + true, + state, + stdin.clone(), + stderr.clone(), + ) + .await? + .join(" "); - current_text.push(TextPart::Quoted(text)); - continue; - } - }; + current_text.push(TextPart::Quoted(text)); + continue; + } + }; + + // This text needs to be turned into a vector of strings. + // For now we do a very basic string split on whitespace, but in the future + // we should continue to improve this functionality. + if let Some(text) = evaluation_result_text { + let mut parts = text + .split(' ') + .map(|p| p.trim()) + .filter(|p| !p.is_empty()) + .map(|p| TextPart::Text(p.to_string())) + .collect::>(); + + if !parts.is_empty() { + // append the first part to the current text + let first_part = parts.remove(0); + current_text.push(first_part); + + if !parts.is_empty() { + // evaluate and store the current text + result.extend(evaluate_word_text( + state, + current_text, + is_quoted, + )?); - // This text needs to be turned into a vector of strings. - // For now we do a very basic string split on whitespace, but in the future - // we should continue to improve this functionality. - if let Some(text) = evaluation_result_text { - let mut parts = text - .split(' ') - .map(|p| p.trim()) - .filter(|p| !p.is_empty()) - .map(|p| TextPart::Text(p.to_string())) - .collect::>(); - - if !parts.is_empty() { - // append the first part to the current text - let first_part = parts.remove(0); - current_text.push(first_part); - - if !parts.is_empty() { - // evaluate and store the current text - result.extend(evaluate_word_text( - state, - current_text, - is_quoted, - )?); - - // store all the parts except the last one - for part in parts.drain(..parts.len() - 1) { + // store all the parts except the last one + for part in parts.drain(..parts.len() - 1) { + result.extend(evaluate_word_text( + state, + vec![part], + is_quoted, + )?); + } + + // use the last part as the current text so it maybe + // gets appended to in the future + current_text = parts; + } + } + } + } + if !current_text.is_empty() { result.extend(evaluate_word_text( - state, - vec![part], - is_quoted, + state, + current_text, + is_quoted, )?); - } - - // use the last part as the current text so it maybe - // gets appended to in the future - current_text = parts; } - } + Ok(result) } - } - if !current_text.is_empty() { - result.extend(evaluate_word_text(state, current_text, is_quoted)?); - } - Ok(result) + .boxed_local() } - .boxed_local() - } - evaluate_word_parts_inner(parts, false, state, stdin, stderr) + evaluate_word_parts_inner(parts, false, state, stdin, stderr) } async fn evaluate_command_substitution( - list: SequentialList, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + list: SequentialList, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> String { - let text = execute_with_stdout_as_text(|shell_stdout_writer| { - execute_sequential_list( - list, - state.clone(), - stdin, - shell_stdout_writer, - stderr, - AsyncCommandBehavior::Wait, - ) - }) - .await; - - // Remove the trailing newline and then replace inner newlines with a space - // This seems to be what sh does, but I'm not entirely sure: - // - // > echo $(echo 1 && echo -e "\n2\n") - // 1 2 - text - .strip_suffix("\r\n") - .or_else(|| text.strip_suffix('\n')) - .unwrap_or(&text) - .replace("\r\n", " ") - .replace('\n', " ") + let text = execute_with_stdout_as_text(|shell_stdout_writer| { + execute_sequential_list( + list, + state.clone(), + stdin, + shell_stdout_writer, + stderr, + AsyncCommandBehavior::Wait, + ) + }) + .await; + + // Remove the trailing newline and then replace inner newlines with a space + // This seems to be what sh does, but I'm not entirely sure: + // + // > echo $(echo 1 && echo -e "\n2\n") + // 1 2 + text.strip_suffix("\r\n") + .or_else(|| text.strip_suffix('\n')) + .unwrap_or(&text) + .replace("\r\n", " ") + .replace('\n', " ") } async fn execute_with_stdout_as_text( - execute: impl FnOnce(ShellPipeWriter) -> FutureExecuteResult, + execute: impl FnOnce(ShellPipeWriter) -> FutureExecuteResult, ) -> String { - let (shell_stdout_reader, shell_stdout_writer) = pipe(); - let spawned_output = execute(shell_stdout_writer); - let output_handle = tokio::task::spawn_blocking(move || { - let mut final_data = Vec::new(); - shell_stdout_reader.pipe_to(&mut final_data).unwrap(); - final_data - }); - let _ = spawned_output.await; - let data = output_handle.await.unwrap(); - String::from_utf8_lossy(&data).to_string() + let (shell_stdout_reader, shell_stdout_writer) = pipe(); + let spawned_output = execute(shell_stdout_writer); + let output_handle = tokio::task::spawn_blocking(move || { + let mut final_data = Vec::new(); + shell_stdout_reader.pipe_to(&mut final_data).unwrap(); + final_data + }); + let _ = spawned_output.await; + let data = output_handle.await.unwrap(); + String::from_utf8_lossy(&data).to_string() } diff --git a/crates/deno_task_shell/src/shell/fs_util.rs b/crates/deno_task_shell/src/shell/fs_util.rs index 043b81f..03c79a1 100644 --- a/crates/deno_task_shell/src/shell/fs_util.rs +++ b/crates/deno_task_shell/src/shell/fs_util.rs @@ -7,11 +7,11 @@ use anyhow::Result; /// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. pub fn canonicalize_path(path: &Path) -> Result { - let path = path.canonicalize()?; - #[cfg(windows)] - return Ok(strip_unc_prefix(path)); - #[cfg(not(windows))] - return Ok(path); + let path = path.canonicalize()?; + #[cfg(windows)] + return Ok(strip_unc_prefix(path)); + #[cfg(not(windows))] + return Ok(path); } // todo(dsherret): This function was copy and pasted from deno @@ -20,41 +20,50 @@ pub fn canonicalize_path(path: &Path) -> Result { #[cfg(windows)] fn strip_unc_prefix(path: PathBuf) -> PathBuf { - use std::path::Component; - use std::path::Prefix; + use std::path::Component; + use std::path::Prefix; - let mut components = path.components(); - match components.next() { - Some(Component::Prefix(prefix)) => { - match prefix.kind() { - // \\?\device - Prefix::Verbatim(device) => { - let mut path = PathBuf::new(); - path.push(format!(r"\\{}\", device.to_string_lossy())); - path.extend(components.filter(|c| !matches!(c, Component::RootDir))); - path - } - // \\?\c:\path - Prefix::VerbatimDisk(_) => { - let mut path = PathBuf::new(); - path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); - path.extend(components); - path - } - // \\?\UNC\hostname\share_name\path - Prefix::VerbatimUNC(hostname, share_name) => { - let mut path = PathBuf::new(); - path.push(format!( - r"\\{}\{}\", - hostname.to_string_lossy(), - share_name.to_string_lossy() - )); - path.extend(components.filter(|c| !matches!(c, Component::RootDir))); - path + let mut components = path.components(); + match components.next() { + Some(Component::Prefix(prefix)) => { + match prefix.kind() { + // \\?\device + Prefix::Verbatim(device) => { + let mut path = PathBuf::new(); + path.push(format!(r"\\{}\", device.to_string_lossy())); + path.extend( + components.filter(|c| !matches!(c, Component::RootDir)), + ); + path + } + // \\?\c:\path + Prefix::VerbatimDisk(_) => { + let mut path = PathBuf::new(); + path.push( + prefix + .as_os_str() + .to_string_lossy() + .replace(r"\\?\", ""), + ); + path.extend(components); + path + } + // \\?\UNC\hostname\share_name\path + Prefix::VerbatimUNC(hostname, share_name) => { + let mut path = PathBuf::new(); + path.push(format!( + r"\\{}\{}\", + hostname.to_string_lossy(), + share_name.to_string_lossy() + )); + path.extend( + components.filter(|c| !matches!(c, Component::RootDir)), + ); + path + } + _ => path, + } } _ => path, - } } - _ => path, - } } diff --git a/crates/deno_task_shell/src/shell/test.rs b/crates/deno_task_shell/src/shell/test.rs index 64d05fa..d46c36e 100644 --- a/crates/deno_task_shell/src/shell/test.rs +++ b/crates/deno_task_shell/src/shell/test.rs @@ -9,49 +9,49 @@ const FOLDER_SEPERATOR: char = if cfg!(windows) { '\\' } else { '/' }; #[tokio::test] async fn commands() { - TestBuilder::new() - .command("echo 1") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("echo 1 2 3") - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo "1 2 3""#) - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r"echo 1 2\ \ \ 3") - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo "1 2\ \ \ 3""#) - .assert_stdout("1 2\\ \\ \\ 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo test$(echo "1 2")"#) - .assert_stdout("test1 2\n") - .run() - .await; - - TestBuilder::new() - .command(r#"TEST="1 2" ; echo $TEST"#) - .assert_stdout("1 2\n") - .run() - .await; - - TestBuilder::new() + TestBuilder::new() + .command("echo 1") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("echo 1 2 3") + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "1 2 3""#) + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r"echo 1 2\ \ \ 3") + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "1 2\ \ \ 3""#) + .assert_stdout("1 2\\ \\ \\ 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo test$(echo "1 2")"#) + .assert_stdout("test1 2\n") + .run() + .await; + + TestBuilder::new() + .command(r#"TEST="1 2" ; echo $TEST"#) + .assert_stdout("1 2\n") + .run() + .await; + + TestBuilder::new() .command( r#"VAR=1 deno eval 'console.log(Deno.env.get("VAR"))' && echo $VAR"#, ) @@ -59,13 +59,13 @@ async fn commands() { .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"VAR=1 VAR2=2 deno eval 'console.log(Deno.env.get("VAR") + Deno.env.get("VAR2"))'"#) .assert_stdout("12\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command( r#"EMPTY= deno eval 'console.log(`EMPTY: ${Deno.env.get("EMPTY")}`)'"#, ) @@ -73,154 +73,154 @@ async fn commands() { .run() .await; - TestBuilder::new() - .command(r#""echo" "1""#) - .assert_stdout("1\n") - .run() - .await; + TestBuilder::new() + .command(r#""echo" "1""#) + .assert_stdout("1\n") + .run() + .await; - TestBuilder::new() - .command(r#""echo" "*""#) - .assert_stdout("*\n") - .run() - .await; + TestBuilder::new() + .command(r#""echo" "*""#) + .assert_stdout("*\n") + .run() + .await; - TestBuilder::new() - .command("echo test-dashes") - .assert_stdout("test-dashes\n") - .run() - .await; + TestBuilder::new() + .command("echo test-dashes") + .assert_stdout("test-dashes\n") + .run() + .await; - TestBuilder::new() - .command("echo 'a/b'/c") - .assert_stdout("a/b/c\n") - .run() - .await; + TestBuilder::new() + .command("echo 'a/b'/c") + .assert_stdout("a/b/c\n") + .run() + .await; - TestBuilder::new() - .command("echo 'a/b'ctest\"te st\"'asdf'") - .assert_stdout("a/bctestte stasdf\n") - .run() - .await; + TestBuilder::new() + .command("echo 'a/b'ctest\"te st\"'asdf'") + .assert_stdout("a/bctestte stasdf\n") + .run() + .await; - TestBuilder::new() + TestBuilder::new() .command("echo --test=\"2\" --test='2' test\"TEST\" TEST'test'TEST 'test''test' test'test'\"test\" \"test\"\"test\"'test'") .assert_stdout("--test=2 --test=2 testTEST TESTtestTEST testtest testtesttest testtesttest\n") .run() .await; - TestBuilder::new() - .command("deno eval 'console.log(1)'") - .env_var("PATH", "") - .assert_stderr("deno: command not found\n") - .assert_exit_code(127) - .run() - .await; + TestBuilder::new() + .command("deno eval 'console.log(1)'") + .env_var("PATH", "") + .assert_stderr("deno: command not found\n") + .assert_exit_code(127) + .run() + .await; - TestBuilder::new().command("unset").run().await; + TestBuilder::new().command("unset").run().await; } #[tokio::test] async fn boolean_logic() { - TestBuilder::new() - .command("echo 1 && echo 2 || echo 3") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() - .command("echo 1 || echo 2 && echo 3") - .assert_stdout("1\n3\n") - .run() - .await; - - TestBuilder::new() - .command("echo 1 || (echo 2 && echo 3)") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("false || false || (echo 2 && false) || echo 3") - .assert_stdout("2\n3\n") - .run() - .await; + TestBuilder::new() + .command("echo 1 && echo 2 || echo 3") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() + .command("echo 1 || echo 2 && echo 3") + .assert_stdout("1\n3\n") + .run() + .await; + + TestBuilder::new() + .command("echo 1 || (echo 2 && echo 3)") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("false || false || (echo 2 && false) || echo 3") + .assert_stdout("2\n3\n") + .run() + .await; } #[tokio::test] async fn exit() { - TestBuilder::new() - .command("exit 1") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("exit 5") - .assert_exit_code(5) - .run() - .await; - - TestBuilder::new() - .command("exit 258 && echo 1") - .assert_exit_code(2) - .run() - .await; - - TestBuilder::new() - .command("(exit 0) && echo 1") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("(exit 1) && echo 1") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("echo 1 && (exit 1)") - .assert_stdout("1\n") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("exit ; echo 2") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("exit bad args") - .assert_stderr("exit: too many arguments\n") - .assert_exit_code(2) - .run() - .await; + TestBuilder::new() + .command("exit 1") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("exit 5") + .assert_exit_code(5) + .run() + .await; + + TestBuilder::new() + .command("exit 258 && echo 1") + .assert_exit_code(2) + .run() + .await; + + TestBuilder::new() + .command("(exit 0) && echo 1") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("(exit 1) && echo 1") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("echo 1 && (exit 1)") + .assert_stdout("1\n") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("exit ; echo 2") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("exit bad args") + .assert_stderr("exit: too many arguments\n") + .assert_exit_code(2) + .run() + .await; } #[tokio::test] async fn async_commands() { - TestBuilder::new() - .command("sleep 0.1 && echo 2 & echo 1") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() - .command("(sleep 0.1 && echo 2 &) ; echo 1") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() - .command("(sleep 0.1 && echo 2) & echo 1") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() + TestBuilder::new() + .command("sleep 0.1 && echo 2 & echo 1") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() + .command("(sleep 0.1 && echo 2 &) ; echo 1") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() + .command("(sleep 0.1 && echo 2) & echo 1") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() .command( "$(sleep 0.1 && echo 1 & $(sleep 0.2 && echo 2 & echo echo) & echo echo)", ) @@ -228,94 +228,94 @@ async fn async_commands() { .run() .await; - TestBuilder::new() - .command("exit 1 & exit 0") - .assert_exit_code(1) - .run() - .await; - - // should not output because the `exit 1` will cancel the sleep - TestBuilder::new() - .command("sleep 5 && echo 1 & exit 1") - .assert_exit_code(1) - .run() - .await; - - // should fail when async command exits - TestBuilder::new() - .command("exit 1 & exit 0") - .assert_exit_code(1) - .run() - .await; - - // should fail when async command fails and cancel any running command - TestBuilder::new() - .command("deno eval 'Deno.exit(1)' & sleep 5 && echo 2 & echo 1") - .assert_stdout("1\n") - .assert_exit_code(1) - .run() - .await; - - // should cancel running command - TestBuilder::new() + TestBuilder::new() + .command("exit 1 & exit 0") + .assert_exit_code(1) + .run() + .await; + + // should not output because the `exit 1` will cancel the sleep + TestBuilder::new() + .command("sleep 5 && echo 1 & exit 1") + .assert_exit_code(1) + .run() + .await; + + // should fail when async command exits + TestBuilder::new() + .command("exit 1 & exit 0") + .assert_exit_code(1) + .run() + .await; + + // should fail when async command fails and cancel any running command + TestBuilder::new() + .command("deno eval 'Deno.exit(1)' & sleep 5 && echo 2 & echo 1") + .assert_stdout("1\n") + .assert_exit_code(1) + .run() + .await; + + // should cancel running command + TestBuilder::new() .command("sleep 10 & sleep 0.5 && deno eval 'Deno.exit(2)' & deno eval 'console.log(1); setTimeout(() => { console.log(3) }, 10_000);'") .assert_stdout("1\n") .assert_exit_code(2) .run() .await; - // should be able to opt out by doing an `|| exit 0` - TestBuilder::new() - .command("deno eval 'Deno.exit(1)' || exit 0 & echo 1") - .assert_stdout("1\n") - .assert_exit_code(0) - .run() - .await; + // should be able to opt out by doing an `|| exit 0` + TestBuilder::new() + .command("deno eval 'Deno.exit(1)' || exit 0 & echo 1") + .assert_stdout("1\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] async fn command_substition() { - TestBuilder::new() - .command("echo $(echo 1)") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("echo $(echo 1 && echo 2)") - .assert_stdout("1 2\n") - .run() - .await; - - // async inside subshell should wait - TestBuilder::new() - .command("$(sleep 0.1 && echo 1 & echo echo) 2") - .assert_stdout("1 2\n") - .run() - .await; - TestBuilder::new() - .command("$(sleep 0.1 && echo 1 && exit 5 &) ; echo 2") - .assert_stdout("2\n") - .assert_stderr("1: command not found\n") - .run() - .await; + TestBuilder::new() + .command("echo $(echo 1)") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("echo $(echo 1 && echo 2)") + .assert_stdout("1 2\n") + .run() + .await; + + // async inside subshell should wait + TestBuilder::new() + .command("$(sleep 0.1 && echo 1 & echo echo) 2") + .assert_stdout("1 2\n") + .run() + .await; + TestBuilder::new() + .command("$(sleep 0.1 && echo 1 && exit 5 &) ; echo 2") + .assert_stdout("2\n") + .assert_stderr("1: command not found\n") + .run() + .await; } #[tokio::test] async fn shell_variables() { - TestBuilder::new() + TestBuilder::new() .command(r#"echo $VAR && VAR=1 && echo $VAR && deno eval 'console.log(Deno.env.get("VAR"))'"#) .assert_stdout("\n1\nundefined\n") .run() .await; - TestBuilder::new() - .command(r#"VAR=1 && echo $VAR$VAR"#) - .assert_stdout("11\n") - .run() - .await; + TestBuilder::new() + .command(r#"VAR=1 && echo $VAR$VAR"#) + .assert_stdout("11\n") + .run() + .await; - TestBuilder::new() + TestBuilder::new() .command(r#"VAR=1 && echo Test$VAR && echo $(echo "Test: $VAR") ; echo CommandSub$($VAR); echo $ ; echo \$VAR"#) .assert_stdout("Test1\nTest: 1\nCommandSub\n$\n$VAR\n") .assert_stderr("1: command not found\n") @@ -325,13 +325,13 @@ async fn shell_variables() { #[tokio::test] async fn env_variables() { - TestBuilder::new() + TestBuilder::new() .command(r#"echo $VAR && export VAR=1 && echo $VAR && deno eval 'console.log(Deno.env.get("VAR"))'"#) .assert_stdout("\n1\n1\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"export VAR=1 VAR2=testing VAR3="test this out" && echo $VAR $VAR2 $VAR3"#) .assert_stdout("1 testing test this out\n") .run() @@ -340,79 +340,79 @@ async fn env_variables() { #[tokio::test] async fn exit_code_var() { - TestBuilder::new() - .command(r#"echo $? ; echo $? ; false ; echo $?"#) - .assert_stdout("\n0\n1\n") - .run() - .await; - TestBuilder::new() - .command(r#"(false || echo $?) && echo $?"#) - .assert_stdout("1\n0\n") - .run() - .await; - TestBuilder::new() - .command(r#"! false && echo $?"#) - .assert_stdout("0\n") - .run() - .await; - TestBuilder::new() - .command(r#"(deno eval 'Deno.exit(25)') || echo $?"#) - .assert_stdout("25\n") - .run() - .await; + TestBuilder::new() + .command(r#"echo $? ; echo $? ; false ; echo $?"#) + .assert_stdout("\n0\n1\n") + .run() + .await; + TestBuilder::new() + .command(r#"(false || echo $?) && echo $?"#) + .assert_stdout("1\n0\n") + .run() + .await; + TestBuilder::new() + .command(r#"! false && echo $?"#) + .assert_stdout("0\n") + .run() + .await; + TestBuilder::new() + .command(r#"(deno eval 'Deno.exit(25)') || echo $?"#) + .assert_stdout("25\n") + .run() + .await; } #[tokio::test] async fn sequential_lists() { - TestBuilder::new() - .command(r#"echo 1 ; sleep 0.1 && echo 4 & echo 2 ; echo 3;"#) - .assert_stdout("1\n2\n3\n4\n") - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 ; sleep 0.1 && echo 4 & echo 2 ; echo 3;"#) + .assert_stdout("1\n2\n3\n4\n") + .run() + .await; } #[tokio::test] async fn pipeline() { - TestBuilder::new() + TestBuilder::new() .command(r#"echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .run() .await; - TestBuilder::new() - .command(r#"echo 1 | echo 2 && echo 3"#) - .assert_stdout("2\n3\n") - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 | echo 2 && echo 3"#) + .assert_stdout("2\n3\n") + .run() + .await; - TestBuilder::new() + TestBuilder::new() .command(r#"echo $(sleep 0.1 && echo 2 & echo 1) | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1 2\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"echo 2 | echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"deno eval 'console.log(1); console.error(2);' | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .assert_stderr("2\n") .run() .await; - // stdout and stderr pipeline + // stdout and stderr pipeline - TestBuilder::new() + TestBuilder::new() .command(r#"deno eval 'console.log(1); console.error(2);' |& deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n2\n") .run() .await; - TestBuilder::new() + TestBuilder::new() // add bit of a delay while outputting stdout so that it doesn't race with stderr .command(r#"deno eval 'console.log(1); console.error(2);' | deno eval 'setTimeout(async () => { await Deno.stdin.readable.pipeTo(Deno.stderr.writable) }, 10)' |& deno eval 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable)'"#) // still outputs 2 because the first command didn't pipe stderr @@ -420,22 +420,22 @@ async fn pipeline() { .run() .await; - // |& pipeline should still pipe stdout - TestBuilder::new() + // |& pipeline should still pipe stdout + TestBuilder::new() .command(r#"echo 1 |& deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .run() .await; - // pipeline with redirect - TestBuilder::new() + // pipeline with redirect + TestBuilder::new() .command(r#"echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)' > output.txt"#) .assert_file_equals("output.txt", "1\n") .run() .await; - // pipeline with stderr redirect - TestBuilder::new() + // pipeline with stderr redirect + TestBuilder::new() .command(r#"echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable)' 2> output.txt"#) .assert_file_equals("output.txt", "1\n") .run() @@ -444,48 +444,48 @@ async fn pipeline() { #[tokio::test] async fn negated() { - TestBuilder::new() - .command(r#"! echo 1 && echo 2"#) - .assert_stdout("1\n") - .assert_exit_code(1) - .run() - .await; - TestBuilder::new() - .command(r#"! echo 1 || echo 2"#) - .assert_stdout("1\n2\n") - .run() - .await; - TestBuilder::new() - .command(r#"! (echo 1 | echo 2 && echo 3) || echo 4"#) - .assert_stdout("2\n3\n4\n") - .run() - .await; - TestBuilder::new() - .command(r#"! echo 1 | echo 2 && echo 3"#) - .assert_stdout("2\n") - .assert_exit_code(1) - .run() - .await; - TestBuilder::new() - .command(r#"! (exit 5) && echo 1"#) - .assert_stdout("1\n") - .run() - .await; - TestBuilder::new() - .command(r#"! exit 5 && echo 1"#) - .assert_exit_code(5) - .run() - .await; - TestBuilder::new() - .command(r#"! echo 1 && echo 2 &"#) - .assert_stdout("1\n") - // differing behaviour to shells, where this async command will actually fail - .assert_exit_code(1) - .run() - .await; - - // test no spaces - TestBuilder::new() + TestBuilder::new() + .command(r#"! echo 1 && echo 2"#) + .assert_stdout("1\n") + .assert_exit_code(1) + .run() + .await; + TestBuilder::new() + .command(r#"! echo 1 || echo 2"#) + .assert_stdout("1\n2\n") + .run() + .await; + TestBuilder::new() + .command(r#"! (echo 1 | echo 2 && echo 3) || echo 4"#) + .assert_stdout("2\n3\n4\n") + .run() + .await; + TestBuilder::new() + .command(r#"! echo 1 | echo 2 && echo 3"#) + .assert_stdout("2\n") + .assert_exit_code(1) + .run() + .await; + TestBuilder::new() + .command(r#"! (exit 5) && echo 1"#) + .assert_stdout("1\n") + .run() + .await; + TestBuilder::new() + .command(r#"! exit 5 && echo 1"#) + .assert_exit_code(5) + .run() + .await; + TestBuilder::new() + .command(r#"! echo 1 && echo 2 &"#) + .assert_stdout("1\n") + // differing behaviour to shells, where this async command will actually fail + .assert_exit_code(1) + .run() + .await; + + // test no spaces + TestBuilder::new() .command(r#"!echo 1 && echo 2"#) .assert_stderr("History expansion is not supported:\n !echo\n ~\n\nPerhaps you meant to add a space after the exclamation point to negate the command?\n ! echo\n") .assert_exit_code(1) @@ -495,76 +495,76 @@ async fn negated() { #[tokio::test] async fn redirects_output() { - TestBuilder::new() - .command(r#"echo 5 6 7 > test.txt"#) - .assert_file_equals("test.txt", "5 6 7\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo 1 2 3 && echo 1 > test.txt"#) - .assert_stdout("1 2 3\n") - .assert_file_equals("test.txt", "1\n") - .run() - .await; - - // subdir - TestBuilder::new() - .command(r#"mkdir subdir && cd subdir && echo 1 2 3 > test.txt"#) - .assert_file_equals("subdir/test.txt", "1 2 3\n") - .run() - .await; - - // absolute path - TestBuilder::new() - .command(r#"echo 1 2 3 > "$PWD/test.txt""#) - .assert_file_equals("test.txt", "1 2 3\n") - .run() - .await; - - // stdout - TestBuilder::new() - .command(r#"deno eval 'console.log(1); console.error(5)' 1> test.txt"#) - .assert_stderr("5\n") - .assert_file_equals("test.txt", "1\n") - .run() - .await; - - // stderr - TestBuilder::new() - .command(r#"deno eval 'console.log(1); console.error(5)' 2> test.txt"#) - .assert_stdout("1\n") - .assert_file_equals("test.txt", "5\n") - .run() - .await; - - // invalid fd - TestBuilder::new() - .command(r#"echo 2 3> test.txt"#) - .ensure_temp_dir() - .assert_stderr( - "only redirecting to stdout (1) and stderr (2) is supported\n", - ) - .assert_exit_code(1) - .run() - .await; - - // /dev/null - TestBuilder::new() - .command(r#"deno eval 'console.log(1); console.error(5)' 2> /dev/null"#) - .assert_stdout("1\n") - .run() - .await; - - // appending - TestBuilder::new() - .command(r#"echo 1 > test.txt && echo 2 >> test.txt"#) - .assert_file_equals("test.txt", "1\n2\n") - .run() - .await; - - // &> and &>> redirect - TestBuilder::new() + TestBuilder::new() + .command(r#"echo 5 6 7 > test.txt"#) + .assert_file_equals("test.txt", "5 6 7\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo 1 2 3 && echo 1 > test.txt"#) + .assert_stdout("1 2 3\n") + .assert_file_equals("test.txt", "1\n") + .run() + .await; + + // subdir + TestBuilder::new() + .command(r#"mkdir subdir && cd subdir && echo 1 2 3 > test.txt"#) + .assert_file_equals("subdir/test.txt", "1 2 3\n") + .run() + .await; + + // absolute path + TestBuilder::new() + .command(r#"echo 1 2 3 > "$PWD/test.txt""#) + .assert_file_equals("test.txt", "1 2 3\n") + .run() + .await; + + // stdout + TestBuilder::new() + .command(r#"deno eval 'console.log(1); console.error(5)' 1> test.txt"#) + .assert_stderr("5\n") + .assert_file_equals("test.txt", "1\n") + .run() + .await; + + // stderr + TestBuilder::new() + .command(r#"deno eval 'console.log(1); console.error(5)' 2> test.txt"#) + .assert_stdout("1\n") + .assert_file_equals("test.txt", "5\n") + .run() + .await; + + // invalid fd + TestBuilder::new() + .command(r#"echo 2 3> test.txt"#) + .ensure_temp_dir() + .assert_stderr( + "only redirecting to stdout (1) and stderr (2) is supported\n", + ) + .assert_exit_code(1) + .run() + .await; + + // /dev/null + TestBuilder::new() + .command(r#"deno eval 'console.log(1); console.error(5)' 2> /dev/null"#) + .assert_stdout("1\n") + .run() + .await; + + // appending + TestBuilder::new() + .command(r#"echo 1 > test.txt && echo 2 >> test.txt"#) + .assert_file_equals("test.txt", "1\n2\n") + .run() + .await; + + // &> and &>> redirect + TestBuilder::new() .command( concat!( "deno eval 'console.log(1); setTimeout(() => console.error(23), 10)' &> file.txt &&", @@ -575,26 +575,26 @@ async fn redirects_output() { .run() .await; - // multiple arguments after re-direct - TestBuilder::new() - .command(r"export TwoArgs=testing\ this && echo 1 > $TwoArgs") - .assert_stderr(concat!( + // multiple arguments after re-direct + TestBuilder::new() + .command(r"export TwoArgs=testing\ this && echo 1 > $TwoArgs") + .assert_stderr(concat!( "redirect path must be 1 argument, but found 2 ", "(testing this). Did you mean to quote it (ex. \"testing this\")?\n" )) - .assert_exit_code(1) - .run() - .await; - - // zero arguments after re-direct - TestBuilder::new() - .command(r#"echo 1 > $EMPTY"#) - .assert_stderr("redirect path must be 1 argument, but found 0\n") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() + .assert_exit_code(1) + .run() + .await; + + // zero arguments after re-direct + TestBuilder::new() + .command(r#"echo 1 > $EMPTY"#) + .assert_stderr("redirect path must be 1 argument, but found 0\n") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() .command(r#"echo 1 >&3"#) .assert_stderr( "deno_task_shell: output redirecting file descriptors beyond stdout and stderr is not implemented\n", @@ -603,45 +603,45 @@ async fn redirects_output() { .run() .await; - TestBuilder::new() - .command(r#"echo 1 >&1"#) - .assert_stdout("1\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 >&1"#) + .assert_stdout("1\n") + .assert_exit_code(0) + .run() + .await; - TestBuilder::new() - .command(r#"echo 1 >&2"#) - .assert_stderr("1\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 >&2"#) + .assert_stderr("1\n") + .assert_exit_code(0) + .run() + .await; - TestBuilder::new() - .command(r#"deno eval 'console.error(2)' 2>&1"#) - .assert_stdout("2\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command(r#"deno eval 'console.error(2)' 2>&1"#) + .assert_stdout("2\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] async fn redirects_input() { - TestBuilder::new() - .file("test.txt", "Hi!") - .command(r#"cat - < test.txt"#) - .assert_stdout("Hi!") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "Hi!\n") - .command(r#"cat - < test.txt && echo There"#) - .assert_stdout("Hi!\nThere\n") - .run() - .await; - - TestBuilder::new() + TestBuilder::new() + .file("test.txt", "Hi!") + .command(r#"cat - < test.txt"#) + .assert_stdout("Hi!") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "Hi!\n") + .command(r#"cat - < test.txt && echo There"#) + .assert_stdout("Hi!\nThere\n") + .run() + .await; + + TestBuilder::new() .command(r#"cat - <&0"#) .assert_stderr( "deno_task_shell: input redirecting file descriptors is not implemented\n", @@ -653,43 +653,43 @@ async fn redirects_input() { #[tokio::test] async fn pwd() { - TestBuilder::new() - .directory("sub_dir") - .file("file.txt", "test") - .command("pwd && cd sub_dir && pwd && cd ../ && pwd") - // the actual temp directory will get replaced here - .assert_stdout(&format!( - "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" - )) - .run() - .await; - - TestBuilder::new() - .command("pwd -M") - .assert_stderr("pwd: unsupported flag: -M\n") - .assert_exit_code(1) - .run() - .await; + TestBuilder::new() + .directory("sub_dir") + .file("file.txt", "test") + .command("pwd && cd sub_dir && pwd && cd ../ && pwd") + // the actual temp directory will get replaced here + .assert_stdout(&format!( + "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" + )) + .run() + .await; + + TestBuilder::new() + .command("pwd -M") + .assert_stderr("pwd: unsupported flag: -M\n") + .assert_exit_code(1) + .run() + .await; } #[tokio::test] async fn subshells() { - TestBuilder::new() - .command("(export TEST=1) && echo $TEST") - .assert_stdout("\n") - .assert_exit_code(0) - .run() - .await; - TestBuilder::new() - .directory("sub_dir") - .command("echo $PWD && (cd sub_dir && echo $PWD) && echo $PWD") - .assert_stdout(&format!( - "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" - )) - .assert_exit_code(0) - .run() - .await; - TestBuilder::new() + TestBuilder::new() + .command("(export TEST=1) && echo $TEST") + .assert_stdout("\n") + .assert_exit_code(0) + .run() + .await; + TestBuilder::new() + .directory("sub_dir") + .command("echo $PWD && (cd sub_dir && echo $PWD) && echo $PWD") + .assert_stdout(&format!( + "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" + )) + .assert_exit_code(0) + .run() + .await; + TestBuilder::new() .command( "export TEST=1 && (echo $TEST && unset TEST && echo $TEST) && echo $TEST", ) @@ -697,98 +697,100 @@ async fn subshells() { .assert_exit_code(0) .run() .await; - TestBuilder::new() - .command("(exit 1) && echo 1") - .assert_exit_code(1) - .run() - .await; - TestBuilder::new() - .command("(exit 1) || echo 1") - .assert_stdout("1\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command("(exit 1) && echo 1") + .assert_exit_code(1) + .run() + .await; + TestBuilder::new() + .command("(exit 1) || echo 1") + .assert_stdout("1\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] #[cfg(unix)] async fn pwd_logical() { - TestBuilder::new() - .directory("main") - .command("ln -s main symlinked_main && cd symlinked_main && pwd && pwd -L") - .assert_stdout("$TEMP_DIR/symlinked_main\n$TEMP_DIR/main\n") - .run() - .await; + TestBuilder::new() + .directory("main") + .command( + "ln -s main symlinked_main && cd symlinked_main && pwd && pwd -L", + ) + .assert_stdout("$TEMP_DIR/symlinked_main\n$TEMP_DIR/main\n") + .run() + .await; } #[tokio::test] async fn cat() { - // no args - TestBuilder::new() - .command("cat") - .stdin("hello") - .assert_stdout("hello") - .run() - .await; - - // dash - TestBuilder::new() - .command("cat -") - .stdin("hello") - .assert_stdout("hello") - .run() - .await; - - // file - TestBuilder::new() - .command("cat file") - .file("file", "test") - .assert_stdout("test") - .run() - .await; - - // multiple files - TestBuilder::new() - .command("cat file1 file2") - .file("file1", "test") - .file("file2", "other") - .assert_stdout("testother") - .run() - .await; - - // multiple files and stdin - TestBuilder::new() - .command("cat file1 file2 -") - .file("file1", "test\n") - .file("file2", "other\n") - .stdin("hello") - .assert_stdout("test\nother\nhello") - .run() - .await; - - // multiple files and stdin different order - TestBuilder::new() - .command("cat file1 - file2") - .file("file1", "test\n") - .file("file2", "other\n") - .stdin("hello\n") - .assert_stdout("test\nhello\nother\n") - .run() - .await; - - // file containing a command to evaluate - TestBuilder::new() - .command("$(cat file)") - .file("file", "echo hello") - .assert_stdout("hello\n") - .run() - .await; + // no args + TestBuilder::new() + .command("cat") + .stdin("hello") + .assert_stdout("hello") + .run() + .await; + + // dash + TestBuilder::new() + .command("cat -") + .stdin("hello") + .assert_stdout("hello") + .run() + .await; + + // file + TestBuilder::new() + .command("cat file") + .file("file", "test") + .assert_stdout("test") + .run() + .await; + + // multiple files + TestBuilder::new() + .command("cat file1 file2") + .file("file1", "test") + .file("file2", "other") + .assert_stdout("testother") + .run() + .await; + + // multiple files and stdin + TestBuilder::new() + .command("cat file1 file2 -") + .file("file1", "test\n") + .file("file2", "other\n") + .stdin("hello") + .assert_stdout("test\nother\nhello") + .run() + .await; + + // multiple files and stdin different order + TestBuilder::new() + .command("cat file1 - file2") + .file("file1", "test\n") + .file("file2", "other\n") + .stdin("hello\n") + .assert_stdout("test\nhello\nother\n") + .run() + .await; + + // file containing a command to evaluate + TestBuilder::new() + .command("$(cat file)") + .file("file", "echo hello") + .assert_stdout("hello\n") + .run() + .await; } #[tokio::test] async fn head() { - // no args - TestBuilder::new() + // no args + TestBuilder::new() .command("head") .stdin( "foo\nbar\nbaz\nqux\nquuux\ncorge\ngrault\ngarply\nwaldo\nfred\nplugh\n", @@ -799,8 +801,8 @@ async fn head() { .run() .await; - // dash - TestBuilder::new() + // dash + TestBuilder::new() .command("head -") .stdin( "foo\nbar\nbaz\nqux\nquuux\ncorge\ngrault\ngarply\nwaldo\nfred\nplugh\n", @@ -811,8 +813,8 @@ async fn head() { .run() .await; - // file - TestBuilder::new() + // file + TestBuilder::new() .command("head file") .file( "file", @@ -824,8 +826,8 @@ async fn head() { .run() .await; - // dash + longer than internal buffer (512) - TestBuilder::new() + // dash + longer than internal buffer (512) + TestBuilder::new() .command("head -") .stdin( "foo\nbar\nbaz\nqux\nquuux\ncorge\ngrault\ngarply\nwaldo\nfred\nplugh\n" @@ -838,8 +840,8 @@ async fn head() { .run() .await; - // file + longer than internal buffer (512) - TestBuilder::new() + // file + longer than internal buffer (512) + TestBuilder::new() .command("head file") .file( "file", @@ -853,150 +855,152 @@ async fn head() { .run() .await; - // shorter than 10 lines - TestBuilder::new() - .command("head") - .stdin("foo\nbar") - .assert_stdout("foo\nbar") - .run() - .await; - - // -n - TestBuilder::new() - .command("head -n 2") - .stdin("foo\nbar\nbaz\nqux\nquuux") - .assert_stdout("foo\nbar\n") - .run() - .await; - - // --lines - TestBuilder::new() - .command("head --lines=3") - .stdin("foo\nbar\nbaz\nqux\nquuux") - .assert_stdout("foo\nbar\nbaz\n") - .run() - .await; + // shorter than 10 lines + TestBuilder::new() + .command("head") + .stdin("foo\nbar") + .assert_stdout("foo\nbar") + .run() + .await; + + // -n + TestBuilder::new() + .command("head -n 2") + .stdin("foo\nbar\nbaz\nqux\nquuux") + .assert_stdout("foo\nbar\n") + .run() + .await; + + // --lines + TestBuilder::new() + .command("head --lines=3") + .stdin("foo\nbar\nbaz\nqux\nquuux") + .assert_stdout("foo\nbar\nbaz\n") + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn mv() { - // single file - TestBuilder::new() - .command("mv file1.txt file2.txt") - .file("file1.txt", "test") - .assert_not_exists("file1.txt") - .assert_exists("file2.txt") - .run() - .await; - - // multiple files to folder - TestBuilder::new() - .command("mkdir sub_dir && mv file1.txt file2.txt sub_dir") - .file("file1.txt", "test1") - .file("file2.txt", "test2") - .assert_not_exists("file1.txt") - .assert_not_exists("file2.txt") - .assert_exists("sub_dir/file1.txt") - .assert_exists("sub_dir/file2.txt") - .run() - .await; - - // error message - TestBuilder::new() - .command("mv file1.txt file2.txt") - .assert_exit_code(1) - .assert_stderr(&format!( - "mv: could not move file1.txt to file2.txt: {}\n", - no_such_file_error_text() - )) - .run() - .await; + // single file + TestBuilder::new() + .command("mv file1.txt file2.txt") + .file("file1.txt", "test") + .assert_not_exists("file1.txt") + .assert_exists("file2.txt") + .run() + .await; + + // multiple files to folder + TestBuilder::new() + .command("mkdir sub_dir && mv file1.txt file2.txt sub_dir") + .file("file1.txt", "test1") + .file("file2.txt", "test2") + .assert_not_exists("file1.txt") + .assert_not_exists("file2.txt") + .assert_exists("sub_dir/file1.txt") + .assert_exists("sub_dir/file2.txt") + .run() + .await; + + // error message + TestBuilder::new() + .command("mv file1.txt file2.txt") + .assert_exit_code(1) + .assert_stderr(&format!( + "mv: could not move file1.txt to file2.txt: {}\n", + no_such_file_error_text() + )) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn cp() { - // single file - TestBuilder::new() - .command("cp file1.txt file2.txt") - .file("file1.txt", "test") - .assert_exists("file1.txt") - .assert_exists("file2.txt") - .run() - .await; - - // multiple files to folder - TestBuilder::new() - .command("mkdir sub_dir && cp file1.txt file2.txt sub_dir") - .file("file1.txt", "test1") - .file("file2.txt", "test2") - .assert_exists("file1.txt") - .assert_exists("file2.txt") - .assert_exists("sub_dir/file1.txt") - .assert_exists("sub_dir/file2.txt") - .run() - .await; - - // error message - TestBuilder::new() - .command("cp file1.txt file2.txt") - .assert_exit_code(1) - .assert_stderr(&format!( - "cp: could not copy file1.txt to file2.txt: {}\n", - no_such_file_error_text() - )) - .run() - .await; + // single file + TestBuilder::new() + .command("cp file1.txt file2.txt") + .file("file1.txt", "test") + .assert_exists("file1.txt") + .assert_exists("file2.txt") + .run() + .await; + + // multiple files to folder + TestBuilder::new() + .command("mkdir sub_dir && cp file1.txt file2.txt sub_dir") + .file("file1.txt", "test1") + .file("file2.txt", "test2") + .assert_exists("file1.txt") + .assert_exists("file2.txt") + .assert_exists("sub_dir/file1.txt") + .assert_exists("sub_dir/file2.txt") + .run() + .await; + + // error message + TestBuilder::new() + .command("cp file1.txt file2.txt") + .assert_exit_code(1) + .assert_stderr(&format!( + "cp: could not copy file1.txt to file2.txt: {}\n", + no_such_file_error_text() + )) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn mkdir() { - TestBuilder::new() - .command("mkdir sub_dir") - .assert_exists("sub_dir") - .run() - .await; - - // error message - TestBuilder::new() - .command("mkdir file.txt") - .file("file.txt", "test") - .assert_stderr("mkdir: cannot create directory 'file.txt': File exists\n") - .assert_exit_code(1) - .run() - .await; + TestBuilder::new() + .command("mkdir sub_dir") + .assert_exists("sub_dir") + .run() + .await; + + // error message + TestBuilder::new() + .command("mkdir file.txt") + .file("file.txt", "test") + .assert_stderr( + "mkdir: cannot create directory 'file.txt': File exists\n", + ) + .assert_exit_code(1) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn rm() { - TestBuilder::new() - .command("mkdir sub_dir && rm -d sub_dir && rm file.txt") - .file("file.txt", "") - .assert_not_exists("sub_dir") - .assert_not_exists("file.txt") - .run() - .await; - - // error message - TestBuilder::new() - .command("rm file.txt") - .assert_stderr(&format!( - "rm: cannot remove 'file.txt': {}\n", - no_such_file_error_text() - )) - .assert_exit_code(1) - .run() - .await; + TestBuilder::new() + .command("mkdir sub_dir && rm -d sub_dir && rm file.txt") + .file("file.txt", "") + .assert_not_exists("sub_dir") + .assert_not_exists("file.txt") + .run() + .await; + + // error message + TestBuilder::new() + .command("rm file.txt") + .assert_stderr(&format!( + "rm: cannot remove 'file.txt': {}\n", + no_such_file_error_text() + )) + .assert_exit_code(1) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn unset() { - // Unset 1 shell variable - TestBuilder::new() + // Unset 1 shell variable + TestBuilder::new() .command( r#"VAR1=1 && VAR2=2 && VAR3=3 && unset VAR1 && echo $VAR1 $VAR2 $VAR3"#, ) @@ -1004,15 +1008,15 @@ async fn unset() { .run() .await; - // Unset 1 env variable - TestBuilder::new() + // Unset 1 env variable + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset VAR1 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\n2\n3\n") .run() .await; - // Unset 2 shell variables - TestBuilder::new() + // Unset 2 shell variables + TestBuilder::new() .command( r#"VAR1=1 && VAR2=2 && VAR3=3 && unset VAR1 VAR2 && echo $VAR1 $VAR2 $VAR3"#, ) @@ -1020,15 +1024,15 @@ async fn unset() { .run() .await; - // Unset 2 env variables - TestBuilder::new() + // Unset 2 env variables + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset VAR1 VAR2 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\nundefined\n3\n") .run() .await; - // Unset 2 shell variables with -v enabled - TestBuilder::new() + // Unset 2 shell variables with -v enabled + TestBuilder::new() .command( r#"VAR1=1 && VAR2=2 && VAR3=3 && unset -v VAR1 VAR2 && echo $VAR1 $VAR2 $VAR3"#, ) @@ -1036,22 +1040,22 @@ async fn unset() { .run() .await; - // Unset 1 env variable with -v enabled - TestBuilder::new() + // Unset 1 env variable with -v enabled + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset -v VAR1 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\n2\n3\n") .run() .await; - // Unset 2 env variables with -v enabled - TestBuilder::new() + // Unset 2 env variables with -v enabled + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset -v VAR1 VAR2 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\nundefined\n3\n") .run() .await; - // Unset 1 shell variable and 1 env variable at the same time - TestBuilder::new() + // Unset 1 shell variable and 1 env variable at the same time + TestBuilder::new() .command( r#"VAR=1 && export ENV_VAR=2 && unset VAR ENV_VAR && echo $VAR $ENV_VAR"#, ) @@ -1059,91 +1063,91 @@ async fn unset() { .run() .await; - // -f is not supported - TestBuilder::new() - .command(r#"export VAR=42 && unset -f VAR"#) - .assert_stderr("unset: unsupported flag: -f\n") - .assert_exit_code(1) - .run() - .await; + // -f is not supported + TestBuilder::new() + .command(r#"export VAR=42 && unset -f VAR"#) + .assert_stderr("unset: unsupported flag: -f\n") + .assert_exit_code(1) + .run() + .await; } #[tokio::test] async fn xargs() { - TestBuilder::new() - .command("echo '1 2 3 ' | xargs") - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command("echo '1 2 \t\t\t3 ' | xargs echo test") - .assert_stdout("test 1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"deno eval "console.log('testing\nthis')" | xargs"#) - .assert_stdout("testing this\n") - .run() - .await; - - // \n delimiter - TestBuilder::new() + TestBuilder::new() + .command("echo '1 2 3 ' | xargs") + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command("echo '1 2 \t\t\t3 ' | xargs echo test") + .assert_stdout("test 1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"deno eval "console.log('testing\nthis')" | xargs"#) + .assert_stdout("testing this\n") + .run() + .await; + + // \n delimiter + TestBuilder::new() .command(r#"deno eval "console.log('testing this out\n\ntest\n')" | xargs -d \n deno eval "console.log(Deno.args)""#) .assert_stdout("[ \"testing this out\", \"\", \"test\", \"\" ]\n") .run() .await; - // \0 delimiter - TestBuilder::new() + // \0 delimiter + TestBuilder::new() .command(r#"deno eval "console.log('testing this out\ntest\0other')" | xargs -0 deno eval "console.log(Deno.args)""#) .assert_stdout("[ \"testing this out\\ntest\", \"other\\n\" ]\n") .run() .await; - // unmatched single quote - TestBuilder::new() + // unmatched single quote + TestBuilder::new() .command(r#"deno eval "console.log(\"'test\")" | xargs"#) .assert_stderr("xargs: unmatched quote; by default quotes are special to xargs unless you use the -0 option\n") .assert_exit_code(1) .run() .await; - // unmatched double quote - TestBuilder::new() + // unmatched double quote + TestBuilder::new() .command(r#"deno eval "console.log('\"test')" | xargs"#) .assert_stderr("xargs: unmatched quote; by default quotes are special to xargs unless you use the -0 option\n") .assert_exit_code(1) .run() .await; - // test reading env file - TestBuilder::new() - .file( - ".env", - r#"VAR1="testing" + // test reading env file + TestBuilder::new() + .file( + ".env", + r#"VAR1="testing" VAR2="other" "#, - ) - // most likely people would want to do `export $(grep -v '^#' .env | xargs)` though - // in order to remove comments... - .command("export $(cat .env | xargs) && echo $VAR1 $VAR2") - .assert_stdout("testing other\n") - .run() - .await; + ) + // most likely people would want to do `export $(grep -v '^#' .env | xargs)` though + // in order to remove comments... + .command("export $(cat .env | xargs) && echo $VAR1 $VAR2") + .assert_stdout("testing other\n") + .run() + .await; } #[tokio::test] async fn stdin() { - TestBuilder::new() + TestBuilder::new() .command(r#"deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)" && deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)""#) .stdin("12345") .assert_stdout("Uint8Array(1) [ 49 ]\nUint8Array(1) [ 50 ]\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"echo "12345" | (deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)" && deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)")"#) .stdin("55555") // should not use this because stdin is piped from the echo .assert_stdout("Uint8Array(1) [ 49 ]\nUint8Array(1) [ 50 ]\n") @@ -1154,169 +1158,169 @@ async fn stdin() { #[cfg(windows)] #[tokio::test] async fn windows_resolve_command() { - // not cross platform, but still allow this - TestBuilder::new() - .command("deno.exe eval 'console.log(1)'") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("deno eval 'console.log(1)'") - // handle trailing semi-colon - .env_var("PATHEXT", ".EXE;") - .assert_stdout("1\n") - .run() - .await; + // not cross platform, but still allow this + TestBuilder::new() + .command("deno.exe eval 'console.log(1)'") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("deno eval 'console.log(1)'") + // handle trailing semi-colon + .env_var("PATHEXT", ".EXE;") + .assert_stdout("1\n") + .run() + .await; } #[tokio::test] async fn custom_command() { - // not cross platform, but still allow this - TestBuilder::new() - .command("add 1 2") - .custom_command( - "add", - Box::new(|mut context| { - async move { - let mut sum = 0; - for val in context.args { - sum += val.parse::().unwrap(); - } - let _ = context.stderr.write_line(&sum.to_string()); - ExecuteResult::from_exit_code(0) - } - .boxed_local() - }), - ) - .assert_stderr("3\n") - .run() - .await; + // not cross platform, but still allow this + TestBuilder::new() + .command("add 1 2") + .custom_command( + "add", + Box::new(|mut context| { + async move { + let mut sum = 0; + for val in context.args { + sum += val.parse::().unwrap(); + } + let _ = context.stderr.write_line(&sum.to_string()); + ExecuteResult::from_exit_code(0) + } + .boxed_local() + }), + ) + .assert_stderr("3\n") + .run() + .await; } #[tokio::test] async fn custom_command_resolve_command_path() { - TestBuilder::new() - .command("$(custom_which deno) eval 'console.log(1)'") - .custom_command( - "custom_which", - Box::new(|mut context| { - async move { - let path = context - .state - .resolve_command_path(&context.args[0]) - .unwrap(); - let _ = context.stdout.write_line(&path.to_string_lossy()); - ExecuteResult::from_exit_code(0) - } - .boxed_local() - }), - ) - .assert_stdout("1\n") - .run() - .await; + TestBuilder::new() + .command("$(custom_which deno) eval 'console.log(1)'") + .custom_command( + "custom_which", + Box::new(|mut context| { + async move { + let path = context + .state + .resolve_command_path(&context.args[0]) + .unwrap(); + let _ = context.stdout.write_line(&path.to_string_lossy()); + ExecuteResult::from_exit_code(0) + } + .boxed_local() + }), + ) + .assert_stdout("1\n") + .run() + .await; } #[tokio::test] async fn glob_basic() { - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.txt") - .assert_stdout("test\ntest2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat test?.txt") - .assert_stdout("test2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat test[0-9].txt") - .assert_stdout("test2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat test[!a-z].txt") - .assert_stdout("test2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat test[a-z].txt") - .assert_stdout("testa\n") - .run() - .await; - - TestBuilder::new() - .directory("sub_dir/sub") - .file("sub_dir/sub/1.txt", "1\n") - .file("sub_dir/2.txt", "2\n") - .file("sub_dir/other.ts", "other\n") - .file("3.txt", "3\n") - .command("cat */*.txt") - .assert_stdout("2\n") - .run() - .await; - - TestBuilder::new() - .directory("sub_dir/sub") - .file("sub_dir/sub/1.txt", "1\n") - .file("sub_dir/2.txt", "2\n") - .file("sub_dir/other.ts", "other\n") - .file("3.txt", "3\n") - .command("cat **/*.txt") - .assert_stdout("3\n2\n1\n") - .run() - .await; - - TestBuilder::new() - .directory("sub_dir/sub") - .file("sub_dir/sub/1.txt", "1\n") - .file("sub_dir/2.txt", "2\n") - .file("sub_dir/other.ts", "other\n") - .file("3.txt", "3\n") - .command("cat $PWD/**/*.txt") - .assert_stdout("3\n2\n1\n") - .run() - .await; - - TestBuilder::new() - .directory("dir") - .file("dir/1.txt", "1\n") - .file("dir_1.txt", "2\n") - .command("cat dir*1.txt") - .assert_stdout("2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.ts") - .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") - .assert_exit_code(1) - .run() - .await; - - let mut builder = TestBuilder::new(); - let temp_dir_path = builder.temp_dir_path(); - let error_pos = temp_dir_path.to_string_lossy().len() + 1; - builder.file("test.txt", "test\n") + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.txt") + .assert_stdout("test\ntest2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat test?.txt") + .assert_stdout("test2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat test[0-9].txt") + .assert_stdout("test2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat test[!a-z].txt") + .assert_stdout("test2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat test[a-z].txt") + .assert_stdout("testa\n") + .run() + .await; + + TestBuilder::new() + .directory("sub_dir/sub") + .file("sub_dir/sub/1.txt", "1\n") + .file("sub_dir/2.txt", "2\n") + .file("sub_dir/other.ts", "other\n") + .file("3.txt", "3\n") + .command("cat */*.txt") + .assert_stdout("2\n") + .run() + .await; + + TestBuilder::new() + .directory("sub_dir/sub") + .file("sub_dir/sub/1.txt", "1\n") + .file("sub_dir/2.txt", "2\n") + .file("sub_dir/other.ts", "other\n") + .file("3.txt", "3\n") + .command("cat **/*.txt") + .assert_stdout("3\n2\n1\n") + .run() + .await; + + TestBuilder::new() + .directory("sub_dir/sub") + .file("sub_dir/sub/1.txt", "1\n") + .file("sub_dir/2.txt", "2\n") + .file("sub_dir/other.ts", "other\n") + .file("3.txt", "3\n") + .command("cat $PWD/**/*.txt") + .assert_stdout("3\n2\n1\n") + .run() + .await; + + TestBuilder::new() + .directory("dir") + .file("dir/1.txt", "1\n") + .file("dir_1.txt", "2\n") + .command("cat dir*1.txt") + .assert_stdout("2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.ts") + .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") + .assert_exit_code(1) + .run() + .await; + + let mut builder = TestBuilder::new(); + let temp_dir_path = builder.temp_dir_path(); + let error_pos = temp_dir_path.to_string_lossy().len() + 1; + builder.file("test.txt", "test\n") .file("test2.txt", "test2\n") .command("cat [].ts") .assert_stderr(&format!("glob: no matches found '$TEMP_DIR/[].ts'. Pattern syntax error near position {}: invalid range pattern\n", error_pos)) @@ -1324,125 +1328,125 @@ async fn glob_basic() { .run() .await; - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.ts || echo 2") - .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") - .assert_stdout("2\n") - .assert_exit_code(0) - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.ts 2> /dev/null || echo 2") - .assert_stderr("") - .assert_stdout("2\n") - .assert_exit_code(0) - .run() - .await; - - TestBuilder::new() - .command("echo --inspect='[::0]:3366'") - .assert_stderr("") - .assert_stdout("--inspect=[::0]:3366\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.ts || echo 2") + .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") + .assert_stdout("2\n") + .assert_exit_code(0) + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.ts 2> /dev/null || echo 2") + .assert_stderr("") + .assert_stdout("2\n") + .assert_exit_code(0) + .run() + .await; + + TestBuilder::new() + .command("echo --inspect='[::0]:3366'") + .assert_stderr("") + .assert_stdout("--inspect=[::0]:3366\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] async fn glob_case_insensitive() { - TestBuilder::new() - .file("TEST.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat tes*.txt") - .assert_stdout("test\ntest2\ntesta\n") - .run() - .await; + TestBuilder::new() + .file("TEST.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat tes*.txt") + .assert_stdout("test\ntest2\ntesta\n") + .run() + .await; } #[tokio::test] async fn glob_escapes() { - // no escape - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat [test].txt") - .assert_stdout("t\n") - .run() - .await; - - // escape - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat [[]test[]].txt") - .assert_stdout("test\n") - .run() - .await; - - // single quotes - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat '[test].txt'") - .assert_stdout("test\n") - .run() - .await; - - // double quotes - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat \"[test].txt\"") - .assert_stdout("test\n") - .run() - .await; - - // mix - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat \"[\"test\"]\".txt") - .assert_stdout("test\n") - .run() - .await; + // no escape + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat [test].txt") + .assert_stdout("t\n") + .run() + .await; + + // escape + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat [[]test[]].txt") + .assert_stdout("test\n") + .run() + .await; + + // single quotes + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat '[test].txt'") + .assert_stdout("test\n") + .run() + .await; + + // double quotes + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat \"[test].txt\"") + .assert_stdout("test\n") + .run() + .await; + + // mix + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat \"[\"test\"]\".txt") + .assert_stdout("test\n") + .run() + .await; } #[tokio::test] async fn paren_escapes() { - TestBuilder::new() - .command(r"echo \( foo bar \)") - .assert_stdout("( foo bar )\n") - .run() - .await; + TestBuilder::new() + .command(r"echo \( foo bar \)") + .assert_stdout("( foo bar )\n") + .run() + .await; } #[tokio::test] async fn cross_platform_shebang() { - // with -S - TestBuilder::new() - .file("file.ts", "#!/usr/bin/env -S deno run\nconsole.log(5)") - .command("./file.ts") - .assert_stdout("5\n") - .run() - .await; - - // without -S and invalid - TestBuilder::new() - .file("file.ts", "#!/usr/bin/env deno run\nconsole.log(5)") - .command("./file.ts") - .assert_stderr("deno run: command not found\n") - .assert_exit_code(127) - .run() - .await; - - // without -S, but valid - TestBuilder::new() + // with -S + TestBuilder::new() + .file("file.ts", "#!/usr/bin/env -S deno run\nconsole.log(5)") + .command("./file.ts") + .assert_stdout("5\n") + .run() + .await; + + // without -S and invalid + TestBuilder::new() + .file("file.ts", "#!/usr/bin/env deno run\nconsole.log(5)") + .command("./file.ts") + .assert_stderr("deno run: command not found\n") + .assert_exit_code(127) + .run() + .await; + + // without -S, but valid + TestBuilder::new() .file("file.ts", "#!/usr/bin/env ./echo_stdin.ts\nconsole.log('Hello')") .file("echo_stdin.ts", "#!/usr/bin/env -S deno run --allow-run\nawait new Deno.Command('deno', { args: ['run', ...Deno.args] }).spawn();") .command("./file.ts") @@ -1450,8 +1454,8 @@ async fn cross_platform_shebang() { .run() .await; - // sub dir - TestBuilder::new() + // sub dir + TestBuilder::new() .directory("sub") .file("sub/file.ts", "#!/usr/bin/env ../echo_stdin.ts\nconsole.log('Hello')") .file("echo_stdin.ts", "#!/usr/bin/env -S deno run --allow-run\nawait new Deno.Command('deno', { args: ['run', ...Deno.args] }).spawn();") @@ -1460,8 +1464,8 @@ async fn cross_platform_shebang() { .run() .await; - // arguments - TestBuilder::new() + // arguments + TestBuilder::new() .file( "file.ts", "#!/usr/bin/env -S deno run --allow-read\nconsole.log(Deno.args)\nconst text = Deno.readTextFileSync(import.meta.filename);\nconsole.log(text.length)\n", @@ -1475,9 +1479,9 @@ async fn cross_platform_shebang() { } fn no_such_file_error_text() -> &'static str { - if cfg!(windows) { - "The system cannot find the file specified. (os error 2)" - } else { - "No such file or directory (os error 2)" - } + if cfg!(windows) { + "The system cannot find the file specified. (os error 2)" + } else { + "No such file or directory (os error 2)" + } } diff --git a/crates/deno_task_shell/src/shell/test_builder.rs b/crates/deno_task_shell/src/shell/test_builder.rs index 0aacc65..37f3f1e 100644 --- a/crates/deno_task_shell/src/shell/test_builder.rs +++ b/crates/deno_task_shell/src/shell/test_builder.rs @@ -21,265 +21,262 @@ use crate::ShellCommandContext; use super::types::ExecuteResult; type FnShellCommandExecute = - Box LocalBoxFuture<'static, ExecuteResult>>; + Box LocalBoxFuture<'static, ExecuteResult>>; struct FnShellCommand(FnShellCommandExecute); impl ShellCommand for FnShellCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - (self.0)(context) - } + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + (self.0)(context) + } } // Clippy is complaining about them all having `File` prefixes, // but there might be non-file variants in the future. #[allow(clippy::enum_variant_names)] enum TestAssertion { - FileExists(String), - FileNotExists(String), - FileTextEquals(String, String), + FileExists(String), + FileNotExists(String), + FileTextEquals(String, String), } struct TempDir { - // hold to keep it alive until drop - _inner: tempfile::TempDir, - cwd: PathBuf, + // hold to keep it alive until drop + _inner: tempfile::TempDir, + cwd: PathBuf, } impl TempDir { - pub fn new() -> Self { - let temp_dir = tempfile::tempdir().unwrap(); - let cwd = fs_util::canonicalize_path(temp_dir.path()).unwrap(); - Self { - _inner: temp_dir, - cwd, + pub fn new() -> Self { + let temp_dir = tempfile::tempdir().unwrap(); + let cwd = fs_util::canonicalize_path(temp_dir.path()).unwrap(); + Self { + _inner: temp_dir, + cwd, + } } - } } pub struct TestBuilder { - // it is much much faster to lazily create this - temp_dir: Option, - env_vars: HashMap, - custom_commands: HashMap>, - command: String, - stdin: Vec, - expected_exit_code: i32, - expected_stderr: String, - expected_stdout: String, - assertions: Vec, + // it is much much faster to lazily create this + temp_dir: Option, + env_vars: HashMap, + custom_commands: HashMap>, + command: String, + stdin: Vec, + expected_exit_code: i32, + expected_stderr: String, + expected_stdout: String, + assertions: Vec, } impl TestBuilder { - pub fn new() -> Self { - let env_vars = std::env::vars() - .map(|(key, value)| { - // For some very strange reason, key will sometimes be cased as "Path" - // or other times "PATH" on Windows. Since keys are case-insensitive on - // Windows, normalize the keys to be upper case. - if cfg!(windows) { - // need to normalize on windows - (key.to_uppercase(), value) - } else { - (key, value) - } - }) - .collect(); + pub fn new() -> Self { + let env_vars = std::env::vars() + .map(|(key, value)| { + // For some very strange reason, key will sometimes be cased as "Path" + // or other times "PATH" on Windows. Since keys are case-insensitive on + // Windows, normalize the keys to be upper case. + if cfg!(windows) { + // need to normalize on windows + (key.to_uppercase(), value) + } else { + (key, value) + } + }) + .collect(); - Self { - temp_dir: None, - env_vars, - custom_commands: Default::default(), - command: Default::default(), - stdin: Default::default(), - expected_exit_code: 0, - expected_stderr: Default::default(), - expected_stdout: Default::default(), - assertions: Default::default(), + Self { + temp_dir: None, + env_vars, + custom_commands: Default::default(), + command: Default::default(), + stdin: Default::default(), + expected_exit_code: 0, + expected_stderr: Default::default(), + expected_stdout: Default::default(), + assertions: Default::default(), + } } - } - pub fn ensure_temp_dir(&mut self) -> &mut Self { - self.get_temp_dir(); - self - } + pub fn ensure_temp_dir(&mut self) -> &mut Self { + self.get_temp_dir(); + self + } - fn get_temp_dir(&mut self) -> &mut TempDir { - if self.temp_dir.is_none() { - self.temp_dir = Some(TempDir::new()); + fn get_temp_dir(&mut self) -> &mut TempDir { + if self.temp_dir.is_none() { + self.temp_dir = Some(TempDir::new()); + } + self.temp_dir.as_mut().unwrap() } - self.temp_dir.as_mut().unwrap() - } - pub fn temp_dir_path(&mut self) -> PathBuf { - self.get_temp_dir().cwd.clone() - } + pub fn temp_dir_path(&mut self) -> PathBuf { + self.get_temp_dir().cwd.clone() + } - pub fn command(&mut self, command: &str) -> &mut Self { - self.command = command.to_string(); - self - } + pub fn command(&mut self, command: &str) -> &mut Self { + self.command = command.to_string(); + self + } - pub fn stdin(&mut self, stdin: &str) -> &mut Self { - self.stdin = stdin.as_bytes().to_vec(); - self - } + pub fn stdin(&mut self, stdin: &str) -> &mut Self { + self.stdin = stdin.as_bytes().to_vec(); + self + } - pub fn directory(&mut self, path: &str) -> &mut Self { - let temp_dir = self.get_temp_dir(); - fs::create_dir_all(temp_dir.cwd.join(path)).unwrap(); - self - } + pub fn directory(&mut self, path: &str) -> &mut Self { + let temp_dir = self.get_temp_dir(); + fs::create_dir_all(temp_dir.cwd.join(path)).unwrap(); + self + } - pub fn env_var(&mut self, name: &str, value: &str) -> &mut Self { - self.env_vars.insert(name.to_string(), value.to_string()); - self - } + pub fn env_var(&mut self, name: &str, value: &str) -> &mut Self { + self.env_vars.insert(name.to_string(), value.to_string()); + self + } - pub fn custom_command( - &mut self, - name: &str, - execute: FnShellCommandExecute, - ) -> &mut Self { - self - .custom_commands - .insert(name.to_string(), Rc::new(FnShellCommand(execute))); - self - } + pub fn custom_command( + &mut self, + name: &str, + execute: FnShellCommandExecute, + ) -> &mut Self { + self.custom_commands + .insert(name.to_string(), Rc::new(FnShellCommand(execute))); + self + } - pub fn file(&mut self, path: &str, text: &str) -> &mut Self { - let temp_dir = self.get_temp_dir(); - fs::write(temp_dir.cwd.join(path), text).unwrap(); - self - } + pub fn file(&mut self, path: &str, text: &str) -> &mut Self { + let temp_dir = self.get_temp_dir(); + fs::write(temp_dir.cwd.join(path), text).unwrap(); + self + } - pub fn assert_exit_code(&mut self, code: i32) -> &mut Self { - self.expected_exit_code = code; - self - } + pub fn assert_exit_code(&mut self, code: i32) -> &mut Self { + self.expected_exit_code = code; + self + } - pub fn assert_stderr(&mut self, output: &str) -> &mut Self { - self.expected_stderr.push_str(output); - self - } + pub fn assert_stderr(&mut self, output: &str) -> &mut Self { + self.expected_stderr.push_str(output); + self + } - pub fn assert_stdout(&mut self, output: &str) -> &mut Self { - self.expected_stdout.push_str(output); - self - } + pub fn assert_stdout(&mut self, output: &str) -> &mut Self { + self.expected_stdout.push_str(output); + self + } - pub fn assert_exists(&mut self, path: &str) -> &mut Self { - self.ensure_temp_dir(); - self - .assertions - .push(TestAssertion::FileExists(path.to_string())); - self - } + pub fn assert_exists(&mut self, path: &str) -> &mut Self { + self.ensure_temp_dir(); + self.assertions + .push(TestAssertion::FileExists(path.to_string())); + self + } - pub fn assert_not_exists(&mut self, path: &str) -> &mut Self { - self.ensure_temp_dir(); - self - .assertions - .push(TestAssertion::FileNotExists(path.to_string())); - self - } + pub fn assert_not_exists(&mut self, path: &str) -> &mut Self { + self.ensure_temp_dir(); + self.assertions + .push(TestAssertion::FileNotExists(path.to_string())); + self + } - pub fn assert_file_equals( - &mut self, - path: &str, - file_text: &str, - ) -> &mut Self { - self.ensure_temp_dir(); - self.assertions.push(TestAssertion::FileTextEquals( - path.to_string(), - file_text.to_string(), - )); - self - } + pub fn assert_file_equals( + &mut self, + path: &str, + file_text: &str, + ) -> &mut Self { + self.ensure_temp_dir(); + self.assertions.push(TestAssertion::FileTextEquals( + path.to_string(), + file_text.to_string(), + )); + self + } - pub async fn run(&mut self) { - let list = parse(&self.command).unwrap(); - let cwd = if let Some(temp_dir) = &self.temp_dir { - temp_dir.cwd.clone() - } else { - std::env::temp_dir() - }; - let (stdin, mut stdin_writer) = pipe(); - stdin_writer.write_all(&self.stdin).unwrap(); - drop(stdin_writer); // prevent a deadlock by dropping the writer - let (stdout, stdout_handle) = get_output_writer_and_handle(); - let (stderr, stderr_handle) = get_output_writer_and_handle(); + pub async fn run(&mut self) { + let list = parse(&self.command).unwrap(); + let cwd = if let Some(temp_dir) = &self.temp_dir { + temp_dir.cwd.clone() + } else { + std::env::temp_dir() + }; + let (stdin, mut stdin_writer) = pipe(); + stdin_writer.write_all(&self.stdin).unwrap(); + drop(stdin_writer); // prevent a deadlock by dropping the writer + let (stdout, stdout_handle) = get_output_writer_and_handle(); + let (stderr, stderr_handle) = get_output_writer_and_handle(); - let local_set = tokio::task::LocalSet::new(); - let state = ShellState::new( - self.env_vars.clone(), - &cwd, - self.custom_commands.drain().collect(), - ); - let exit_code = local_set - .run_until(execute_with_pipes(list, state, stdin, stdout, stderr)) - .await; - let temp_dir = if let Some(temp_dir) = &self.temp_dir { - temp_dir.cwd.display().to_string() - } else { - "NO_TEMP_DIR".to_string() - }; - assert_eq!( - stderr_handle.await.unwrap(), - self.expected_stderr.replace("$TEMP_DIR", &temp_dir), - "\n\nFailed for: {}", - self.command - ); - assert_eq!( - stdout_handle.await.unwrap(), - self.expected_stdout.replace("$TEMP_DIR", &temp_dir), - "\n\nFailed for: {}", - self.command - ); - assert_eq!( - exit_code, self.expected_exit_code, - "\n\nFailed for: {}", - self.command - ); + let local_set = tokio::task::LocalSet::new(); + let state = ShellState::new( + self.env_vars.clone(), + &cwd, + self.custom_commands.drain().collect(), + ); + let exit_code = local_set + .run_until(execute_with_pipes(list, state, stdin, stdout, stderr)) + .await; + let temp_dir = if let Some(temp_dir) = &self.temp_dir { + temp_dir.cwd.display().to_string() + } else { + "NO_TEMP_DIR".to_string() + }; + assert_eq!( + stderr_handle.await.unwrap(), + self.expected_stderr.replace("$TEMP_DIR", &temp_dir), + "\n\nFailed for: {}", + self.command + ); + assert_eq!( + stdout_handle.await.unwrap(), + self.expected_stdout.replace("$TEMP_DIR", &temp_dir), + "\n\nFailed for: {}", + self.command + ); + assert_eq!( + exit_code, self.expected_exit_code, + "\n\nFailed for: {}", + self.command + ); - for assertion in &self.assertions { - match assertion { - TestAssertion::FileExists(path) => { - assert!( - cwd.join(path).exists(), - "\n\nFailed for: {}\nExpected '{}' to exist.", - self.command, - path, - ) - } - TestAssertion::FileNotExists(path) => { - assert!( - !cwd.join(path).exists(), - "\n\nFailed for: {}\nExpected '{}' to not exist.", - self.command, - path, - ) - } - TestAssertion::FileTextEquals(path, text) => { - let actual_text = std::fs::read_to_string(cwd.join(path)) - .with_context(|| format!("Error reading {path}")) - .unwrap(); - assert_eq!( - &actual_text, text, - "\n\nFailed for: {}\nPath: {}", - self.command, path, - ) + for assertion in &self.assertions { + match assertion { + TestAssertion::FileExists(path) => { + assert!( + cwd.join(path).exists(), + "\n\nFailed for: {}\nExpected '{}' to exist.", + self.command, + path, + ) + } + TestAssertion::FileNotExists(path) => { + assert!( + !cwd.join(path).exists(), + "\n\nFailed for: {}\nExpected '{}' to not exist.", + self.command, + path, + ) + } + TestAssertion::FileTextEquals(path, text) => { + let actual_text = std::fs::read_to_string(cwd.join(path)) + .with_context(|| format!("Error reading {path}")) + .unwrap(); + assert_eq!( + &actual_text, text, + "\n\nFailed for: {}\nPath: {}", + self.command, path, + ) + } + } } - } } - } } fn get_output_writer_and_handle() -> (ShellPipeWriter, JoinHandle) { - let (reader, writer) = pipe(); - let handle = reader.pipe_to_string_handle(); - (writer, handle) + let (reader, writer) = pipe(); + let handle = reader.pipe_to_string_handle(); + (writer, handle) } diff --git a/crates/deno_task_shell/src/shell/types.rs b/crates/deno_task_shell/src/shell/types.rs index f78ba66..df4984a 100644 --- a/crates/deno_task_shell/src/shell/types.rs +++ b/crates/deno_task_shell/src/shell/types.rs @@ -20,156 +20,156 @@ use super::commands::ShellCommand; #[derive(Clone)] pub struct ShellState { - /// Environment variables that should be passed down to sub commands - /// and used when evaluating environment variables. - env_vars: HashMap, - /// Variables that should be evaluated within the shell and - /// not passed down to any sub commands. - shell_vars: HashMap, - cwd: PathBuf, - commands: Rc>>, - /// Token to cancel execution. - token: CancellationToken, + /// Environment variables that should be passed down to sub commands + /// and used when evaluating environment variables. + env_vars: HashMap, + /// Variables that should be evaluated within the shell and + /// not passed down to any sub commands. + shell_vars: HashMap, + cwd: PathBuf, + commands: Rc>>, + /// Token to cancel execution. + token: CancellationToken, } impl ShellState { - pub fn new( - env_vars: HashMap, - cwd: &Path, - custom_commands: HashMap>, - ) -> Self { - assert!(cwd.is_absolute()); - let mut commands = builtin_commands(); - commands.extend(custom_commands); - let mut result = Self { - env_vars: Default::default(), - shell_vars: Default::default(), - cwd: PathBuf::new(), - commands: Rc::new(commands), - token: CancellationToken::default(), - }; - // ensure the data is normalized - for (name, value) in env_vars { - result.apply_env_var(&name, &value); - } - result.set_cwd(cwd); - result - } - - pub fn cwd(&self) -> &PathBuf { - &self.cwd - } - - pub fn env_vars(&self) -> &HashMap { - &self.env_vars - } - - pub fn get_var(&self, name: &str) -> Option<&String> { - let name = if cfg!(windows) { - Cow::Owned(name.to_uppercase()) - } else { - Cow::Borrowed(name) - }; - self - .env_vars - .get(name.as_ref()) - .or_else(|| self.shell_vars.get(name.as_ref())) - } - - pub fn set_cwd(&mut self, cwd: &Path) { - self.cwd = cwd.to_path_buf(); - // $PWD holds the current working directory, so we keep cwd and $PWD in sync - self - .env_vars - .insert("PWD".to_string(), self.cwd.display().to_string()); - } - - pub fn apply_changes(&mut self, changes: &[EnvChange]) { - for change in changes { - self.apply_change(change); - } - } - - pub fn apply_change(&mut self, change: &EnvChange) { - match change { - EnvChange::SetEnvVar(name, value) => self.apply_env_var(name, value), - EnvChange::SetShellVar(name, value) => { - if self.env_vars.contains_key(name) { - self.apply_env_var(name, value); + pub fn new( + env_vars: HashMap, + cwd: &Path, + custom_commands: HashMap>, + ) -> Self { + assert!(cwd.is_absolute()); + let mut commands = builtin_commands(); + commands.extend(custom_commands); + let mut result = Self { + env_vars: Default::default(), + shell_vars: Default::default(), + cwd: PathBuf::new(), + commands: Rc::new(commands), + token: CancellationToken::default(), + }; + // ensure the data is normalized + for (name, value) in env_vars { + result.apply_env_var(&name, &value); + } + result.set_cwd(cwd); + result + } + + pub fn cwd(&self) -> &PathBuf { + &self.cwd + } + + pub fn env_vars(&self) -> &HashMap { + &self.env_vars + } + + pub fn get_var(&self, name: &str) -> Option<&String> { + let name = if cfg!(windows) { + Cow::Owned(name.to_uppercase()) } else { - self.shell_vars.insert(name.to_string(), value.to_string()); + Cow::Borrowed(name) + }; + self.env_vars + .get(name.as_ref()) + .or_else(|| self.shell_vars.get(name.as_ref())) + } + + pub fn set_cwd(&mut self, cwd: &Path) { + self.cwd = cwd.to_path_buf(); + // $PWD holds the current working directory, so we keep cwd and $PWD in sync + self.env_vars + .insert("PWD".to_string(), self.cwd.display().to_string()); + } + + pub fn apply_changes(&mut self, changes: &[EnvChange]) { + for change in changes { + self.apply_change(change); } - } - EnvChange::UnsetVar(name) => { - self.shell_vars.remove(name); - self.env_vars.remove(name); - } - EnvChange::Cd(new_dir) => { - self.set_cwd(new_dir); - } - } - } - - pub fn apply_env_var(&mut self, name: &str, value: &str) { - let name = if cfg!(windows) { - // environment variables are case insensitive on windows - name.to_uppercase() - } else { - name.to_string() - }; - if name == "PWD" { - let cwd = PathBuf::from(value); - if cwd.is_absolute() { - if let Ok(cwd) = fs_util::canonicalize_path(&cwd) { - // this will update the environment variable too - self.set_cwd(&cwd); + } + + pub fn apply_change(&mut self, change: &EnvChange) { + match change { + EnvChange::SetEnvVar(name, value) => { + self.apply_env_var(name, value) + } + EnvChange::SetShellVar(name, value) => { + if self.env_vars.contains_key(name) { + self.apply_env_var(name, value); + } else { + self.shell_vars.insert(name.to_string(), value.to_string()); + } + } + EnvChange::UnsetVar(name) => { + self.shell_vars.remove(name); + self.env_vars.remove(name); + } + EnvChange::Cd(new_dir) => { + self.set_cwd(new_dir); + } } - } - } else { - self.shell_vars.remove(&name); - self.env_vars.insert(name, value.to_string()); - } - } - - pub fn token(&self) -> &CancellationToken { - &self.token - } - - /// Resolves a custom command that was injected. - pub fn resolve_custom_command( - &self, - name: &str, - ) -> Option> { - // uses an Rc to allow resolving a command without borrowing from self - self.commands.get(name).cloned() - } - - /// Resolves the path to a command from the current working directory. - /// - /// Does not take injected custom commands into account. - pub fn resolve_command_path( - &self, - command_name: &str, - ) -> Result { - super::command::resolve_command_path(command_name, self.cwd(), self) - } - - pub fn with_child_token(&self) -> ShellState { - let mut state = self.clone(); - state.token = self.token.child_token(); - state - } + } + + pub fn apply_env_var(&mut self, name: &str, value: &str) { + let name = if cfg!(windows) { + // environment variables are case insensitive on windows + name.to_uppercase() + } else { + name.to_string() + }; + if name == "PWD" { + let cwd = PathBuf::from(value); + if cwd.is_absolute() { + if let Ok(cwd) = fs_util::canonicalize_path(&cwd) { + // this will update the environment variable too + self.set_cwd(&cwd); + } + } + } else { + self.shell_vars.remove(&name); + self.env_vars.insert(name, value.to_string()); + } + } + + pub fn token(&self) -> &CancellationToken { + &self.token + } + + /// Resolves a custom command that was injected. + pub fn resolve_custom_command( + &self, + name: &str, + ) -> Option> { + // uses an Rc to allow resolving a command without borrowing from self + self.commands.get(name).cloned() + } + + /// Resolves the path to a command from the current working directory. + /// + /// Does not take injected custom commands into account. + pub fn resolve_command_path( + &self, + command_name: &str, + ) -> Result { + super::command::resolve_command_path(command_name, self.cwd(), self) + } + + pub fn with_child_token(&self) -> ShellState { + let mut state = self.clone(); + state.token = self.token.child_token(); + state + } } #[derive(Debug, PartialEq, Eq)] pub enum EnvChange { - // `export ENV_VAR=VALUE` - SetEnvVar(String, String), - // `ENV_VAR=VALUE` - SetShellVar(String, String), - // `unset ENV_VAR` - UnsetVar(String), - Cd(PathBuf), + // `export ENV_VAR=VALUE` + SetEnvVar(String, String), + // `ENV_VAR=VALUE` + SetShellVar(String, String), + // `unset ENV_VAR` + UnsetVar(String), + Cd(PathBuf), } pub type FutureExecuteResult = LocalBoxFuture<'static, ExecuteResult>; @@ -180,135 +180,139 @@ pub const CANCELLATION_EXIT_CODE: i32 = 130; #[derive(Debug)] pub enum ExecuteResult { - Exit(i32, Vec>), - Continue(i32, Vec, Vec>), + Exit(i32, Vec>), + Continue(i32, Vec, Vec>), } impl ExecuteResult { - pub fn for_cancellation() -> ExecuteResult { - ExecuteResult::Exit(CANCELLATION_EXIT_CODE, Vec::new()) - } + pub fn for_cancellation() -> ExecuteResult { + ExecuteResult::Exit(CANCELLATION_EXIT_CODE, Vec::new()) + } - pub fn from_exit_code(exit_code: i32) -> ExecuteResult { - ExecuteResult::Continue(exit_code, Vec::new(), Vec::new()) - } + pub fn from_exit_code(exit_code: i32) -> ExecuteResult { + ExecuteResult::Continue(exit_code, Vec::new(), Vec::new()) + } - pub fn into_exit_code_and_handles(self) -> (i32, Vec>) { - match self { - ExecuteResult::Exit(code, handles) => (code, handles), - ExecuteResult::Continue(code, _, handles) => (code, handles), + pub fn into_exit_code_and_handles(self) -> (i32, Vec>) { + match self { + ExecuteResult::Exit(code, handles) => (code, handles), + ExecuteResult::Continue(code, _, handles) => (code, handles), + } } - } - pub fn into_handles(self) -> Vec> { - self.into_exit_code_and_handles().1 - } + pub fn into_handles(self) -> Vec> { + self.into_exit_code_and_handles().1 + } } /// Reader side of a pipe. #[derive(Debug)] pub enum ShellPipeReader { - OsPipe(os_pipe::PipeReader), - StdFile(std::fs::File), + OsPipe(os_pipe::PipeReader), + StdFile(std::fs::File), } impl Clone for ShellPipeReader { - fn clone(&self) -> Self { - match self { - Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), - Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), + fn clone(&self) -> Self { + match self { + Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), + Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), + } } - } } impl ShellPipeReader { - pub fn stdin() -> ShellPipeReader { - ShellPipeReader::from_raw(os_pipe::dup_stdin().unwrap()) - } - - pub fn from_raw(reader: os_pipe::PipeReader) -> Self { - Self::OsPipe(reader) - } - - pub fn from_std(std_file: std::fs::File) -> Self { - Self::StdFile(std_file) - } - - pub fn into_stdio(self) -> std::process::Stdio { - match self { - Self::OsPipe(pipe) => pipe.into(), - Self::StdFile(file) => file.into(), - } - } - - /// Pipe everything to the specified writer - pub fn pipe_to(self, writer: &mut dyn Write) -> Result<()> { - // don't bother flushing here because this won't ever be called - // with a Rust wrapped stdout/stderr - self.pipe_to_inner(writer, false) - } - - fn pipe_to_with_flushing(self, writer: &mut dyn Write) -> Result<()> { - self.pipe_to_inner(writer, true) - } - - fn pipe_to_inner( - mut self, - writer: &mut dyn Write, - flush: bool, - ) -> Result<()> { - loop { - let mut buffer = [0; 512]; // todo: what is an appropriate buffer size? - let size = match &mut self { - ShellPipeReader::OsPipe(pipe) => pipe.read(&mut buffer)?, - ShellPipeReader::StdFile(file) => file.read(&mut buffer)?, - }; - if size == 0 { - break; - } - writer.write_all(&buffer[0..size])?; - if flush { - writer.flush()?; - } - } - Ok(()) - } - - /// Pipes this pipe to the specified sender. - pub fn pipe_to_sender(self, mut sender: ShellPipeWriter) -> Result<()> { - match &mut sender { - ShellPipeWriter::OsPipe(pipe) => self.pipe_to(pipe), - ShellPipeWriter::StdFile(file) => self.pipe_to(file), - // Don't lock stdout/stderr here because we want to release the lock - // when reading from the sending pipe. Additionally, we want - // to flush after every write because Rust's wrapper has an - // internal buffer and Deno doesn't buffer stdout/stderr. - ShellPipeWriter::Stdout => { - self.pipe_to_with_flushing(&mut std::io::stdout()) - } - ShellPipeWriter::Stderr => { - self.pipe_to_with_flushing(&mut std::io::stderr()) - } - ShellPipeWriter::Null => Ok(()), - } - } - - /// Pipes the reader to a string handle that is resolved when the pipe's - /// writer is closed. - pub fn pipe_to_string_handle(self) -> JoinHandle { - tokio::task::spawn_blocking(|| { - let mut buf = Vec::new(); - self.pipe_to(&mut buf).unwrap(); - String::from_utf8_lossy(&buf).to_string() - }) - } - - pub fn read(&mut self, buf: &mut [u8]) -> Result { - match self { - ShellPipeReader::OsPipe(pipe) => pipe.read(buf).map_err(|e| e.into()), - ShellPipeReader::StdFile(file) => file.read(buf).map_err(|e| e.into()), - } - } + pub fn stdin() -> ShellPipeReader { + ShellPipeReader::from_raw(os_pipe::dup_stdin().unwrap()) + } + + pub fn from_raw(reader: os_pipe::PipeReader) -> Self { + Self::OsPipe(reader) + } + + pub fn from_std(std_file: std::fs::File) -> Self { + Self::StdFile(std_file) + } + + pub fn into_stdio(self) -> std::process::Stdio { + match self { + Self::OsPipe(pipe) => pipe.into(), + Self::StdFile(file) => file.into(), + } + } + + /// Pipe everything to the specified writer + pub fn pipe_to(self, writer: &mut dyn Write) -> Result<()> { + // don't bother flushing here because this won't ever be called + // with a Rust wrapped stdout/stderr + self.pipe_to_inner(writer, false) + } + + fn pipe_to_with_flushing(self, writer: &mut dyn Write) -> Result<()> { + self.pipe_to_inner(writer, true) + } + + fn pipe_to_inner( + mut self, + writer: &mut dyn Write, + flush: bool, + ) -> Result<()> { + loop { + let mut buffer = [0; 512]; // todo: what is an appropriate buffer size? + let size = match &mut self { + ShellPipeReader::OsPipe(pipe) => pipe.read(&mut buffer)?, + ShellPipeReader::StdFile(file) => file.read(&mut buffer)?, + }; + if size == 0 { + break; + } + writer.write_all(&buffer[0..size])?; + if flush { + writer.flush()?; + } + } + Ok(()) + } + + /// Pipes this pipe to the specified sender. + pub fn pipe_to_sender(self, mut sender: ShellPipeWriter) -> Result<()> { + match &mut sender { + ShellPipeWriter::OsPipe(pipe) => self.pipe_to(pipe), + ShellPipeWriter::StdFile(file) => self.pipe_to(file), + // Don't lock stdout/stderr here because we want to release the lock + // when reading from the sending pipe. Additionally, we want + // to flush after every write because Rust's wrapper has an + // internal buffer and Deno doesn't buffer stdout/stderr. + ShellPipeWriter::Stdout => { + self.pipe_to_with_flushing(&mut std::io::stdout()) + } + ShellPipeWriter::Stderr => { + self.pipe_to_with_flushing(&mut std::io::stderr()) + } + ShellPipeWriter::Null => Ok(()), + } + } + + /// Pipes the reader to a string handle that is resolved when the pipe's + /// writer is closed. + pub fn pipe_to_string_handle(self) -> JoinHandle { + tokio::task::spawn_blocking(|| { + let mut buf = Vec::new(); + self.pipe_to(&mut buf).unwrap(); + String::from_utf8_lossy(&buf).to_string() + }) + } + + pub fn read(&mut self, buf: &mut [u8]) -> Result { + match self { + ShellPipeReader::OsPipe(pipe) => { + pipe.read(buf).map_err(|e| e.into()) + } + ShellPipeReader::StdFile(file) => { + file.read(buf).map_err(|e| e.into()) + } + } + } } /// Writer side of a pipe. @@ -317,89 +321,89 @@ impl ShellPipeReader { /// prevent deadlocks where the reader hangs waiting for a read. #[derive(Debug)] pub enum ShellPipeWriter { - OsPipe(os_pipe::PipeWriter), - StdFile(std::fs::File), - // For stdout and stderr, instead of directly duplicating the raw pipes - // and putting them in a ShellPipeWriter::OsPipe(...), we use Rust std's - // stdout() and stderr() wrappers because it contains some code to solve - // some encoding issues on Windows (ex. emojis). For more details, see - // library/std/src/sys/windows/stdio.rs in Rust's source code. - Stdout, - Stderr, - Null, + OsPipe(os_pipe::PipeWriter), + StdFile(std::fs::File), + // For stdout and stderr, instead of directly duplicating the raw pipes + // and putting them in a ShellPipeWriter::OsPipe(...), we use Rust std's + // stdout() and stderr() wrappers because it contains some code to solve + // some encoding issues on Windows (ex. emojis). For more details, see + // library/std/src/sys/windows/stdio.rs in Rust's source code. + Stdout, + Stderr, + Null, } impl Clone for ShellPipeWriter { - fn clone(&self) -> Self { - match self { - Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), - Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), - Self::Stdout => Self::Stdout, - Self::Stderr => Self::Stderr, - Self::Null => Self::Null, - } - } + fn clone(&self) -> Self { + match self { + Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), + Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), + Self::Stdout => Self::Stdout, + Self::Stderr => Self::Stderr, + Self::Null => Self::Null, + } + } } impl ShellPipeWriter { - pub fn stdout() -> Self { - Self::Stdout - } - - pub fn stderr() -> Self { - Self::Stderr - } - - pub fn null() -> Self { - Self::Null - } - - pub fn from_std(std_file: std::fs::File) -> Self { - Self::StdFile(std_file) - } - - pub fn into_stdio(self) -> std::process::Stdio { - match self { - Self::OsPipe(pipe) => pipe.into(), - Self::StdFile(file) => file.into(), - Self::Stdout => std::process::Stdio::inherit(), - Self::Stderr => std::process::Stdio::inherit(), - Self::Null => std::process::Stdio::null(), - } - } - - pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> { - match self { - Self::OsPipe(pipe) => pipe.write_all(bytes)?, - Self::StdFile(file) => file.write_all(bytes)?, - // For both stdout & stderr, we want to flush after each - // write in order to bypass Rust's internal buffer. - Self::Stdout => { - let mut stdout = std::io::stdout().lock(); - stdout.write_all(bytes)?; - stdout.flush()?; - } - Self::Stderr => { - let mut stderr = std::io::stderr().lock(); - stderr.write_all(bytes)?; - stderr.flush()?; - } - Self::Null => {} - } - Ok(()) - } - - pub fn write_line(&mut self, line: &str) -> Result<()> { - let bytes = format!("{line}\n"); - self.write_all(bytes.as_bytes()) - } + pub fn stdout() -> Self { + Self::Stdout + } + + pub fn stderr() -> Self { + Self::Stderr + } + + pub fn null() -> Self { + Self::Null + } + + pub fn from_std(std_file: std::fs::File) -> Self { + Self::StdFile(std_file) + } + + pub fn into_stdio(self) -> std::process::Stdio { + match self { + Self::OsPipe(pipe) => pipe.into(), + Self::StdFile(file) => file.into(), + Self::Stdout => std::process::Stdio::inherit(), + Self::Stderr => std::process::Stdio::inherit(), + Self::Null => std::process::Stdio::null(), + } + } + + pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> { + match self { + Self::OsPipe(pipe) => pipe.write_all(bytes)?, + Self::StdFile(file) => file.write_all(bytes)?, + // For both stdout & stderr, we want to flush after each + // write in order to bypass Rust's internal buffer. + Self::Stdout => { + let mut stdout = std::io::stdout().lock(); + stdout.write_all(bytes)?; + stdout.flush()?; + } + Self::Stderr => { + let mut stderr = std::io::stderr().lock(); + stderr.write_all(bytes)?; + stderr.flush()?; + } + Self::Null => {} + } + Ok(()) + } + + pub fn write_line(&mut self, line: &str) -> Result<()> { + let bytes = format!("{line}\n"); + self.write_all(bytes.as_bytes()) + } } /// Used to communicate between commands. pub fn pipe() -> (ShellPipeReader, ShellPipeWriter) { - let (reader, writer) = os_pipe::pipe().unwrap(); - ( - ShellPipeReader::OsPipe(reader), - ShellPipeWriter::OsPipe(writer), - ) + let (reader, writer) = os_pipe::pipe().unwrap(); + ( + ShellPipeReader::OsPipe(reader), + ShellPipeWriter::OsPipe(writer), + ) } diff --git a/crates/shell/src/main.rs b/crates/shell/src/main.rs index 0ed2550..9d8586c 100644 --- a/crates/shell/src/main.rs +++ b/crates/shell/src/main.rs @@ -11,11 +11,14 @@ async fn main() { // read text from stdin and print it let script_text = std::fs::read_to_string(&args[1]).unwrap(); - println!("Executing:\n\n{}\n\n-----------------------------------\n\n", script_text); + println!( + "Executing:\n\n{}\n\n-----------------------------------\n\n", + script_text + ); let list = deno_task_shell::parser::parse(&script_text).unwrap(); - // execute + // // execute let env_vars = std::env::vars().collect(); let cwd = std::env::current_dir().expect("Failed to get current directory"); diff --git a/scripts/script_1.sh b/scripts/script_1.sh index 5543a6b..2fcee71 100644 --- a/scripts/script_1.sh +++ b/scripts/script_1.sh @@ -1 +1 @@ -echo "Hello World!" \ No newline at end of file +echo "Hello World" \ No newline at end of file diff --git a/scripts/script_2.sh b/scripts/script_2.sh index 04a18ac..c6f954a 100644 --- a/scripts/script_2.sh +++ b/scripts/script_2.sh @@ -1,2 +1 @@ -MY_VAR=42; -echo $MY_VAR +echo "Double word" 'Single word' \ No newline at end of file diff --git a/scripts/script_4.sh b/scripts/script_4.sh index c6f954a..8c1a164 100644 --- a/scripts/script_4.sh +++ b/scripts/script_4.sh @@ -1 +1,2 @@ -echo "Double word" 'Single word' \ No newline at end of file +ls; +cat Cargo.toml \ No newline at end of file From 5fd0e9c0daaefd59ba5922e9095b1edbc2cd4f8f Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sat, 31 Aug 2024 03:32:09 -0400 Subject: [PATCH 5/7] Fixes and added tests --- crates/deno_task_shell/src/grammar.pest | 79 +- crates/deno_task_shell/src/parser.rs | 964 +++++++++++++++++++++--- scripts/script_2.sh | 2 +- scripts/script_3.sh | 3 +- scripts/script_5.sh | 2 + 5 files changed, 919 insertions(+), 131 deletions(-) create mode 100644 scripts/script_5.sh diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index a169c71..b87ab16 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -5,13 +5,43 @@ WHITESPACE = _{ " " | "\t" | ("\\" ~ WHITESPACE* ~ NEWLINE) } COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* } // Basic tokens -WORD_WITH_EQUAL = ${ "="? ~ (!(WHITESPACE | OPERATOR | NEWLINE) ~ ANY)+ } -WORD = ${ (!(WHITESPACE | OPERATOR | NEWLINE) ~ ANY)+ } -QUOTED_WORD = ${ DOUBLE_QUOTED | SINGLE_QUOTED } -DOUBLE_QUOTED = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" } +QUOTED_WORD = { DOUBLE_QUOTED | SINGLE_QUOTED } + +UNQUOTED_PENDING_WORD = ${ + (!(WHITESPACE | OPERATOR | NEWLINE) ~ ( + EXIT_STATUS | + UNQUOTED_ESCAPE_CHAR | + SUB_COMMAND | + ("$" ~ VARIABLE) | + UNQUOTED_CHAR | + QUOTED_WORD + ) +)+ } + +FILE_NAME_PENDING_WORD = ${ (!(WHITESPACE | OPERATOR | NEWLINE) ~ (UNQUOTED_ESCAPE_CHAR | VARIABLE | UNQUOTED_CHAR | QUOTED_WORD))+ } + +QUOTED_PENDING_WORD = ${ ( + EXIT_STATUS | + QUOTED_ESCAPE_CHAR | + SUB_COMMAND | + ("$" ~ VARIABLE) | + QUOTED_CHAR +)* } + +UNQUOTED_ESCAPE_CHAR = ${ ("\\" ~ "$" | "$" ~ !"(" ~ !VARIABLE) | "\\" ~ ("`" | "\"" | "(" | ")")* } +QUOTED_ESCAPE_CHAR = ${ "\\" ~ "$" | "$" ~ !"(" ~ !VARIABLE | "\\" ~ ("`" | "\"" | "(" | ")" | "'")* } + +UNQUOTED_CHAR = ${ ("\\" ~ " ") | !(WHITESPACE | "~" | "(" | ")" | "{" | "}" | "<" | ">" | "|" | "&" | ";" | "\"" | "'") ~ ANY } +QUOTED_CHAR = ${ !"\"" ~ ANY } + +VARIABLE = ${ (ASCII_ALPHANUMERIC | "_")+ } +SUB_COMMAND = { "$(" ~ complete_command ~ ")" } + +DOUBLE_QUOTED = @{ "\"" ~ QUOTED_PENDING_WORD ~ "\"" } SINGLE_QUOTED = @{ "'" ~ (!"'" ~ ANY)* ~ "'" } + NAME = ${ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } -ASSIGNMENT_WORD = ${ NAME ~ "=" ~ WORD } +ASSIGNMENT_WORD = { NAME ~ "=" ~ UNQUOTED_PENDING_WORD? } IO_NUMBER = @{ ASCII_DIGIT+ } // Special tokens @@ -28,6 +58,8 @@ LESSGREAT = { "<>" } DLESSDASH = { "<<-" } CLOBBER = { ">|" } AMPERSAND = { "&" } +EXIT_STATUS = ${ "$?" } + // Operators OPERATOR = _{ @@ -52,22 +84,22 @@ Lbrace = { "{" } Rbrace = { "}" } Bang = { "!" } In = { "in" } -Stdout = { "|" } +Stdout = ${ "|" ~ !"|" ~ !"&"} StdoutStderr = { "|&" } RESERVED_WORD = _{ If | Then | Else | Elif | Fi | Do | Done | Case | Esac | While | Until | For | Lbrace | Rbrace | Bang | In | - Stdout | StdoutStderr + StdoutStderr | Stdout } // Main grammar rules -complete_command = _{ (list ~ separator)* ~ list ~ separator? } +complete_command = { list? ~ (separator+ ~ list)* ~ separator? } list = { and_or ~ (separator_op ~ and_or)* ~ separator_op? } -and_or = { pipeline ~ ((AND_IF | OR_IF) ~ linebreak ~ pipeline)* } +and_or = { (pipeline | ASSIGNMENT_WORD+) ~ ((AND_IF | OR_IF) ~ linebreak ~ and_or)? } pipeline = { Bang? ~ pipe_sequence } -pipe_sequence = { command ~ (Stdout | StdoutStderr ~ linebreak ~ pipe_sequence)* } +pipe_sequence = { command ~ ((StdoutStderr | Stdout) ~ linebreak ~ pipe_sequence)? } command = { simple_command | @@ -96,7 +128,7 @@ for_clause = { } case_clause = { - Case ~ WORD ~ linebreak ~ + Case ~ UNQUOTED_PENDING_WORD ~ linebreak ~ linebreak ~ In ~ linebreak ~ (case_list | case_list_ns)? ~ Esac @@ -119,7 +151,7 @@ case_item_ns = { } pattern = { - (Esac | WORD) ~ ("|" ~ WORD)* + (Esac | UNQUOTED_PENDING_WORD) ~ ("|" ~ UNQUOTED_PENDING_WORD)* } if_clause = { @@ -140,22 +172,21 @@ until_clause = { Until ~ compound_list ~ do_group } function_definition = { fname ~ "(" ~ ")" ~ linebreak ~ function_body } function_body = { compound_command ~ redirect_list? } -fname = @{ RESERVED_WORD | NAME | WORD_WITH_EQUAL | ASSIGNMENT_WORD } -name = @{ NAME } +fname = @{ RESERVED_WORD | NAME | ASSIGNMENT_WORD | UNQUOTED_PENDING_WORD } +name = @{ NAME } brace_group = { Lbrace ~ compound_list ~ Rbrace } do_group = { Do ~ compound_list ~ Done } simple_command = { - cmd_prefix ~ cmd_word ~ cmd_suffix? | - cmd_prefix | - cmd_name ~ cmd_suffix? + cmd_prefix ~ WHITESPACE* ~ cmd_word ~ WHITESPACE* ~ cmd_suffix? | + (!cmd_prefix ~ cmd_name ~ WHITESPACE* ~ cmd_suffix?) } cmd_prefix = { (io_redirect | ASSIGNMENT_WORD)+ } -cmd_suffix = { (io_redirect | QUOTED_WORD | WORD)+ } -cmd_name = @{ (RESERVED_WORD | WORD) } -cmd_word = @{ WORD_WITH_EQUAL | ASSIGNMENT_WORD } +cmd_suffix = { (io_redirect | QUOTED_WORD | UNQUOTED_PENDING_WORD)+ } +cmd_name = @{ (RESERVED_WORD | UNQUOTED_PENDING_WORD) } +cmd_word = @{ (ASSIGNMENT_WORD | UNQUOTED_PENDING_WORD) } redirect_list = { io_redirect+ } io_redirect = { (IO_NUMBER | AMPERSAND)? ~ (io_file | io_here) } @@ -168,17 +199,17 @@ io_file = { LESSGREAT ~ filename | CLOBBER ~ filename } -filename = @{ WORD } +filename = _{ FILE_NAME_PENDING_WORD } io_here = { (DLESS | DLESSDASH) ~ here_end } -here_end = @{ ("\"" ~ WORD ~ "\"") | WORD } +here_end = @{ ("\"" ~ UNQUOTED_PENDING_WORD ~ "\"") | UNQUOTED_PENDING_WORD } -newline_list = { NEWLINE+ } +newline_list = _{ NEWLINE+ } linebreak = _{ NEWLINE* } separator_op = { "&" | ";" } separator = _{ separator_op ~ linebreak | newline_list } sequential_sep = { ";" ~ linebreak | newline_list } -wordlist = { WORD+ } +wordlist = { UNQUOTED_PENDING_WORD+ } // Entry point FILE = { SOI ~ complete_command ~ EOI } \ No newline at end of file diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index fa7dabb..5689ffa 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -5,6 +5,7 @@ use pest::iterators::Pair; use pest::Parser; use pest_derive::Parser; + // Shell grammar rules this is loosely based on: // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_10_02 @@ -330,69 +331,159 @@ pub enum RedirectOpOutput { struct ShellParser; pub fn parse(input: &str) -> Result { - let pairs = ShellParser::parse(Rule::FILE, input)?; - parse_complete_command(pairs.into_iter().next().unwrap()) + let mut pairs = ShellParser::parse(Rule::FILE, input)?; + + parse_file(pairs.next().unwrap()) +} + +fn parse_file(pairs: Pair) -> Result { + parse_complete_command(pairs.into_inner().next().unwrap()) } fn parse_complete_command(pair: Pair) -> Result { + assert!(pair.as_rule() == Rule::complete_command); let mut items = Vec::new(); - for list in pair.into_inner() { - if list.as_rule() == Rule::list { - let mut is_async = false; - for item in list.into_inner() { - match item.as_rule() { - Rule::and_or => { - let result = parse_and_or(item); - match result { - Ok(sequence) => { - items.push(SequentialListItem { - sequence, - is_async, - }); - } - Err(e) => return Err(e), - } - } - Rule::separator_op => { - is_async = item.as_str() == "&"; - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected rule: {}", - item.as_str() - )); - } - } + for command in pair.into_inner() { + match command.as_rule() { + Rule::list => { + parse_list(command, &mut items)?; + } + Rule::EOI => { + break; + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in complete_command: {:?}", + command.as_rule() + )); } } } Ok(SequentialList { items }) } +fn parse_list(pair: Pair, items: &mut Vec) -> Result<()> { + for item in pair.into_inner() { + match item.as_rule() { + Rule::and_or => { + let sequence = parse_and_or(item)?; + items.push(SequentialListItem { + is_async: false, + sequence, + }); + } + Rule::separator_op => { + if let Some(last) = items.last_mut() { + last.is_async = item.as_str() == "&"; + } + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in list: {:?}", + item.as_rule() + )); + } + } + } + Ok(()) +} + +fn parse_compound_list(pair: Pair, items: &mut Vec) -> Result<()> { + for item in pair.into_inner() { + match item.as_rule() { + Rule::term => { + parse_term(item, items)?; + } + Rule::newline_list => { + // Ignore newlines + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in compound_list: {:?}", + item.as_rule() + )); + } + } + } + Ok(()) +} + +fn parse_term(pair: Pair, items: &mut Vec) -> Result<()> { + for item in pair.into_inner() { + match item.as_rule() { + Rule::and_or => { + let sequence = parse_and_or(item)?; + items.push(SequentialListItem { + sequence, + is_async: false, + }); + } + Rule::separator_op => { + if let Some(last) = items.last_mut() { + last.is_async = item.as_str() == "&"; + } + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in term: {:?}", + item.as_rule() + )); + } + } + } + Ok(()) +} + fn parse_and_or(pair: Pair) -> Result { - let mut pipelines = pair.into_inner(); - let first_pipeline = pipelines.next().unwrap(); - let mut current = parse_pipeline(first_pipeline).unwrap(); - - while let Some(op) = pipelines.next() { - if let Some(next_pipeline) = pipelines.next() { - let op = match op.as_str() { - "&&" => BooleanListOperator::And, - "||" => BooleanListOperator::Or, - _ => unreachable!(), - }; - let next = parse_pipeline(next_pipeline)?; - current = Sequence::BooleanList(Box::new(BooleanList { - current, - op, - next, - })); + assert!(pair.as_rule() == Rule::and_or); + let mut items = pair.into_inner(); + let first_item = items.next().unwrap(); + let mut current = match first_item.as_rule() { + Rule::ASSIGNMENT_WORD => parse_shell_var(first_item)?, + Rule::pipeline => parse_pipeline(first_item)?, + _ => unreachable!(), + }; + + match items.next() { + Some(next_item) => { + if next_item.as_rule() == Rule::ASSIGNMENT_WORD { + anyhow::bail!("Multiple assignment words before && or || is not supported yet"); + } else { + let op = match next_item.as_str() { + "&&" => BooleanListOperator::And, + "||" => BooleanListOperator::Or, + _ => unreachable!(), + }; + + let next_item = items.next().unwrap(); + let next = parse_and_or(next_item)?; + current = Sequence::BooleanList(Box::new(BooleanList { + current, + op, + next, + })); + } + } + None => { + return Ok(current); } } Ok(current) } +fn parse_shell_var(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + let name = inner.next() + .ok_or_else(|| anyhow::anyhow!("Expected variable name"))? + .as_str() + .to_string(); + let value = inner.next() + .ok_or_else(|| anyhow::anyhow!("Expected variable value"))?; + let value = parse_word(value)?; + Ok(Sequence::ShellVar(EnvVar { name, value })) +} + fn parse_pipeline(pair: Pair) -> Result { let mut inner = pair.into_inner(); @@ -462,7 +553,7 @@ fn parse_command(pair: Pair) -> Result { let inner = pair.into_inner().next().unwrap(); match inner.as_rule() { Rule::simple_command => parse_simple_command(inner), - Rule::compound_command => todo!("inner"), + Rule::compound_command => parse_compound_command(inner), Rule::function_definition => { todo!("function definitions are not supported yet") } @@ -502,11 +593,11 @@ fn parse_simple_command(pair: Pair) -> Result { Rule::cmd_suffix => { for suffix in item.into_inner() { match suffix.as_rule() { - Rule::WORD => args.push(parse_word(suffix)?), + Rule::UNQUOTED_PENDING_WORD => args.push(parse_word(suffix)?), Rule::io_redirect => { redirect = Some(parse_io_redirect(suffix)?); } - Rule::QUOTED_WORD => args.push(parse_word(suffix)?), + Rule::QUOTED_WORD => args.push(Word::new(vec![parse_quoted_word(suffix)?])), _ => { return Err(anyhow::anyhow!( "Unexpected rule in cmd_suffix: {:?}", @@ -531,61 +622,74 @@ fn parse_simple_command(pair: Pair) -> Result { }) } +fn parse_compound_command(pair: Pair) -> Result { + let inner = pair.into_inner().next().unwrap(); + match inner.as_rule() { + Rule::brace_group => todo!("brace_group"), + Rule::subshell => parse_subshell(inner), + Rule::for_clause => todo!("for_clause"), + Rule::case_clause => todo!("case_clause"), + Rule::if_clause => todo!("if_clause"), + Rule::while_clause => todo!("while_clause"), + Rule::until_clause => todo!("until_clause"), + _ => Err(anyhow::anyhow!( + "Unexpected rule in compound_command: {:?}", + inner.as_rule() + )) + } +} + +fn parse_subshell(pair: Pair) -> Result { + let mut items = Vec::new(); + if let Some(inner) = pair.into_inner().next() { + parse_compound_list(inner, &mut items)?; + Ok(Command { + inner: CommandInner::Subshell(Box::new(SequentialList { items })), + redirect: None, + }) + } else { + Err(anyhow::anyhow!( + "Unexpected end of input in subshell" + )) + } +} + fn parse_word(pair: Pair) -> Result { let mut parts = Vec::new(); match pair.as_rule() { - Rule::WORD => { - let text = pair.as_str(); - if text.starts_with('\'') && text.ends_with('\'') { - // Single quoted text - parts.push(WordPart::Quoted(vec![WordPart::Text( - text[1..text.len() - 1].to_string(), - )])); - } else if text.starts_with('"') && text.ends_with('"') { - // Double quoted text - parts.push(WordPart::Quoted(vec![WordPart::Text( - text[1..text.len() - 1].to_string(), - )])); - } else if let Some(var_name) = text.strip_prefix('$') { - // Variable - parts.push(WordPart::Variable(var_name.to_string())); - } else { - // Regular text - parts.push(WordPart::Text(text.to_string())); - } - } - Rule::WORD_WITH_EQUAL => { - // Handle words that might start with '=' - let text = pair.as_str(); - if let Some(rest) = text.strip_prefix('=') { - parts.push(WordPart::Text("=".to_string())); - parts.push(WordPart::Text(rest.to_string())); - } else if text.starts_with('\'') && text.ends_with('\'') { - // Single quoted text - parts.push(WordPart::Quoted(vec![WordPart::Text( - text[1..text.len() - 1].to_string(), - )])); - } else if text.starts_with('"') && text.ends_with('"') { - // Double quoted text - parts.push(WordPart::Quoted(vec![WordPart::Text( - text[1..text.len() - 1].to_string(), - )])); - } else if let Some(var_name) = text.strip_prefix('$') { - // Variable - parts.push(WordPart::Variable(var_name.to_string())); - } else { - // Regular text - parts.push(WordPart::Text(text.to_string())); + Rule::UNQUOTED_PENDING_WORD => { + for part in pair.into_inner() { + match part.as_rule() { + Rule::EXIT_STATUS => parts.push(WordPart::Variable("?".to_string())), + Rule::UNQUOTED_ESCAPE_CHAR | Rule::UNQUOTED_CHAR => { + if let Some(WordPart::Text(ref mut text)) = parts.last_mut() { + text.push(part.as_str().chars().next().unwrap()); + } else { + parts.push(WordPart::Text(part.as_str().to_string())); + } + }, + Rule::SUB_COMMAND => { + let command = parse_complete_command(part.into_inner().next().unwrap())?; + parts.push(WordPart::Command(command)); + }, + Rule::VARIABLE => parts.push(WordPart::Variable(part.as_str().to_string())), + Rule::QUOTED_WORD => { + let quoted = parse_quoted_word(part)?; + parts.push(quoted); + }, + _ => return Err(anyhow::anyhow!("Unexpected rule in UNQUOTED_PENDING_WORD: {:?}", part.as_rule())), + } } - } + }, Rule::QUOTED_WORD => { - let text = pair.as_str(); - let unquoted_text = &text[1..text.len() - 1]; - parts.push(WordPart::Quoted(vec![WordPart::Text( - unquoted_text.to_string(), - )])); - } + let quoted = parse_quoted_word(pair)?; + parts.push(quoted); + }, + Rule::ASSIGNMENT_WORD => { + let assignment_str = pair.as_str().to_string(); + parts.push(WordPart::Text(assignment_str)); + }, _ => { return Err(anyhow::anyhow!( "Unexpected rule in word: {:?}", @@ -601,6 +705,49 @@ fn parse_word(pair: Pair) -> Result { } } +fn parse_quoted_word(pair: Pair) -> Result { + let mut parts = Vec::new(); + let inner = pair.into_inner().next().unwrap(); + + match inner.as_rule() { + Rule::DOUBLE_QUOTED => { + let inner = inner.into_inner().next().unwrap(); + for part in inner.into_inner() { + match part.as_rule() { + Rule::EXIT_STATUS => parts.push(WordPart::Text("$?".to_string())), + Rule::QUOTED_ESCAPE_CHAR => { + if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { + s.push_str(part.as_str()); + } else { + parts.push(WordPart::Text(part.as_str().to_string())); + } + }, + Rule::SUB_COMMAND => { + let command = parse_complete_command(part.into_inner().next().unwrap())?; + parts.push(WordPart::Command(command)); + }, + Rule::VARIABLE => parts.push(WordPart::Variable(part.as_str()[1..].to_string())), + Rule::QUOTED_CHAR => { + if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { + s.push_str(part.as_str()); + } else { + parts.push(WordPart::Text(part.as_str().to_string())); + } + }, + _ => return Err(anyhow::anyhow!("Unexpected rule in DOUBLE_QUOTED: {:?}", part.as_rule())), + } + }; + Ok(WordPart::Quoted(parts)) + }, + Rule::SINGLE_QUOTED => { + let inner_str = inner.as_str(); + let trimmed_str = &inner_str[1..inner_str.len() - 1]; + Ok(WordPart::Quoted(vec![WordPart::Text(trimmed_str.to_string())])) + }, + _ => Err(anyhow::anyhow!("Unexpected rule in QUOTED_WORD: {:?}", inner.as_rule())), + } +} + fn parse_env_var(pair: Pair) -> Result { let mut parts = pair.into_inner(); @@ -612,12 +759,11 @@ fn parse_env_var(pair: Pair) -> Result { .to_string(); // Get the value of the environment variable - let value = parts - .next() - .ok_or_else(|| anyhow!("Expected variable value"))?; - - // Parse the value as a Word - let word_value = parse_word(value)?; + let word_value = if let Some(value) = parts.next() { + parse_word(value)? + } else { + Word::new_empty() + }; Ok(EnvVar { name, @@ -699,7 +845,7 @@ fn parse_io_file(pair: Pair) -> Result<(RedirectOp, IoFile)> { } }; - let io_file = if filename.as_rule() == Rule::WORD { + let io_file = if filename.as_rule() == Rule::FILE_NAME_PENDING_WORD { IoFile::Word(parse_word(filename)?) } else { return Err(anyhow!( @@ -710,3 +856,611 @@ fn parse_io_file(pair: Pair) -> Result<(RedirectOp, IoFile)> { Ok((redirect_op, io_file)) } + + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_main() { + assert!(parse("&& testing").is_err()); + assert!(parse("test { test").is_err()); + assert!(parse("cp test/* other").is_ok()); + assert!(parse("cp test/? other").is_ok()); + assert!(parse("(test").is_err()); + assert!(parse("cmd \"test").is_err()); + assert!(parse("cmd 'test").is_err()); + + assert!(parse("( test ||other&&test;test);(t&est );").is_ok()); + assert!(parse("command --arg='value'").is_ok()); + assert!(parse("command --arg=\"value\"").is_ok()); + + assert!( + parse("deno run --allow-read=. --allow-write=./testing main.ts").is_ok(), + ); + } + + #[test] + fn test_sequential_list() { + let parse_and_create = |input: &str| -> Result { + let pairs = ShellParser::parse(Rule::complete_command, input) + .map_err(|e| anyhow::Error::msg(e.to_string()))? + .next() + .unwrap(); + // println!("pairs: {:?}", pairs); + parse_complete_command(pairs) + }; + + // Test case 1 + let input = concat!( + "Name=Value OtherVar=Other command arg1 || command2 arg12 arg13 ; ", + "command3 && command4 & command5 ; export ENV6=5 ; ", + "ENV7=other && command8 || command9 ; ", + "cmd10 && (cmd11 || cmd12)" + ); + let result = parse_and_create(input).unwrap(); + let expected = SequentialList { + items: vec![ + SequentialListItem { + is_async: false, + sequence: Sequence::BooleanList(Box::new(BooleanList { + current: SimpleCommand { + env_vars: vec![ + EnvVar::new("Name".to_string(), Word::new_word("Value")), + EnvVar::new("OtherVar".to_string(), Word::new_word("Other")), + ], + args: vec![Word::new_word("command"), Word::new_word("arg1")], + } + .into(), + op: BooleanListOperator::Or, + next: SimpleCommand { + env_vars: vec![], + args: vec![ + Word::new_word("command2"), + Word::new_word("arg12"), + Word::new_word("arg13"), + ], + } + .into(), + })), + }, + SequentialListItem { + is_async: true, + sequence: Sequence::BooleanList(Box::new(BooleanList { + current: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command3")], + } + .into(), + op: BooleanListOperator::And, + next: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command4")], + } + .into(), + })), + }, + SequentialListItem { + is_async: false, + sequence: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command5")], + } + .into(), + }, + SequentialListItem { + is_async: false, + sequence: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("export"), Word::new_word("ENV6=5")], + } + .into(), + }, + SequentialListItem { + is_async: false, + sequence: Sequence::BooleanList(Box::new(BooleanList { + current: Sequence::ShellVar(EnvVar::new( + "ENV7".to_string(), + Word::new_word("other"), + )), + op: BooleanListOperator::And, + next: Sequence::BooleanList(Box::new(BooleanList { + current: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command8")], + } + .into(), + op: BooleanListOperator::Or, + next: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command9")], + } + .into(), + })), + })), + }, + SequentialListItem { + is_async: false, + sequence: Sequence::BooleanList(Box::new(BooleanList { + current: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("cmd10")], + } + .into(), + op: BooleanListOperator::And, + next: Command { + inner: CommandInner::Subshell(Box::new(SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: Sequence::BooleanList(Box::new(BooleanList { + current: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("cmd11")], + } + .into(), + op: BooleanListOperator::Or, + next: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("cmd12")], + } + .into(), + })), + }], + })), + redirect: None, + } + .into(), + })), + }, + ], + }; + assert_eq!(result, expected); + + // Test case 2 + let input = "command1 ; command2 ; A='b' command3"; + let result = parse_and_create(input).unwrap(); + let expected = SequentialList { + items: vec![ + SequentialListItem { + is_async: false, + sequence: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command1")], + } + .into(), + }, + SequentialListItem { + is_async: false, + sequence: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command2")], + } + .into(), + }, + SequentialListItem { + is_async: false, + sequence: SimpleCommand { + env_vars: vec![EnvVar::new( + "A".to_string(), + Word::new_string("b"), + )], + args: vec![Word::new_word("command3")], + } + .into(), + }, + ], + }; + assert_eq!(result, expected); + + // Test case 3 + let input = "test &&"; + assert!(parse_and_create(input).is_err()); + + // Test case 4 + let input = "command &"; + let result = parse_and_create(input).unwrap(); + let expected = SequentialList { + items: vec![SequentialListItem { + is_async: true, + sequence: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("command")], + } + .into(), + }], + }; + assert_eq!(result, expected); + + // Test case 5 + let input = "test | other"; + let result = parse_and_create(input).unwrap(); + let expected = SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: PipeSequence { + current: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("test")], + } + .into(), + op: PipeSequenceOperator::Stdout, + next: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("other")], + } + .into(), + } + .into(), + }], + }; + assert_eq!(result, expected); + + // Test case 6 + let input = "test |& other"; + let result = parse_and_create(input).unwrap(); + let expected = SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: PipeSequence { + current: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("test")], + } + .into(), + op: PipeSequenceOperator::StdoutStderr, + next: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("other")], + } + .into(), + } + .into(), + }], + }; + assert_eq!(result, expected); + + // Test case 8 + let input = "echo $MY_ENV;"; + let result = parse_and_create(input).unwrap(); + let expected = SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: SimpleCommand { + env_vars: vec![], + args: vec![ + Word::new_word("echo"), + Word(vec![WordPart::Variable("MY_ENV".to_string())]), + ], + } + .into(), + }], + }; + assert_eq!(result, expected); + + // Test case 9 + let input = "! cmd1 | cmd2 && cmd3"; + let result = parse_and_create(input).unwrap(); + let expected = SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: Sequence::BooleanList(Box::new(BooleanList { + current: Pipeline { + negated: true, + inner: PipeSequence { + current: SimpleCommand { + args: vec![Word::new_word("cmd1")], + env_vars: vec![], + } + .into(), + op: PipeSequenceOperator::Stdout, + next: SimpleCommand { + args: vec![Word::new_word("cmd2")], + env_vars: vec![], + } + .into(), + } + .into(), + } + .into(), + op: BooleanListOperator::And, + next: SimpleCommand { + args: vec![Word::new_word("cmd3")], + env_vars: vec![], + } + .into(), + })), + }], + }; + assert_eq!(result, expected); + } + + #[test] + fn test_env_var() { + let parse_and_create = |input: &str| -> Result { + let pairs = ShellParser::parse(Rule::ASSIGNMENT_WORD, input) + .map_err(|e| anyhow::anyhow!(e.to_string()))? + .next() + .unwrap(); + parse_env_var(pairs) + }; + + assert_eq!( + parse_and_create("Name=Value").unwrap(), + EnvVar { + name: "Name".to_string(), + value: Word::new_word("Value"), + } + ); + + assert_eq!( + parse_and_create("Name='quoted value'").unwrap(), + EnvVar { + name: "Name".to_string(), + value: Word::new_string("quoted value"), + } + ); + + assert_eq!( + parse_and_create("Name=\"double quoted value\"").unwrap(), + EnvVar { + name: "Name".to_string(), + value: Word::new_string("double quoted value"), + } + ); + + assert_eq!( + parse_and_create("Name=").unwrap(), + EnvVar { + name: "Name".to_string(), + value: Word(vec![]), + } + ); + + assert_eq!( + parse_and_create("Name=$(test)").unwrap(), + EnvVar { + name: "Name".to_string(), + value: Word(vec![WordPart::Command(SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: SimpleCommand { + env_vars: vec![], + args: vec![Word::new_word("test")], + } + .into(), + }], + })]), + } + ); + + assert_eq!( + parse_and_create("Name=$(OTHER=5)").unwrap(), + EnvVar { + name: "Name".to_string(), + value: Word(vec![WordPart::Command(SequentialList { + items: vec![SequentialListItem { + is_async: false, + sequence: Sequence::ShellVar(EnvVar { + name: "OTHER".to_string(), + value: Word::new_word("5"), + }), + }], + })]), + } + ); + } + + #[cfg(feature = "serialization")] + #[test] + fn serializes_command_to_json() { + assert_json_equals( + serialize_to_json("./example > output.txt"), + serde_json::json!({ + "items": [{ + "isAsync": false, + "sequence": { + "inner": { + "inner": { + "args": [[{ + "kind": "text", + "value": "./example" + }]], + "envVars": [], + "kind": "simple" + }, + "kind": "command", + "redirect": { + "ioFile": { + "kind": "word", + "value": [{ + "kind": "text", + "value": "output.txt" + }], + }, + "maybeFd": null, + "op": { + "kind": "output", + "value": "overwrite", + } + } + }, + "kind": "pipeline", + "negated": false + } + }] + }), + ); + assert_json_equals( + serialize_to_json("./example 2> output.txt"), + serde_json::json!({ + "items": [{ + "isAsync": false, + "sequence": { + "inner": { + "inner": { + "args": [[{ + "kind": "text", + "value": "./example" + }]], + "envVars": [], + "kind": "simple" + }, + "kind": "command", + "redirect": { + "ioFile": { + "kind": "word", + "value": [{ + "kind": "text", + "value": "output.txt" + }], + }, + "maybeFd": { + "kind": "fd", + "fd": 2, + }, + "op": { + "kind": "output", + "value": "overwrite", + } + } + }, + "kind": "pipeline", + "negated": false + } + }] + }), + ); + assert_json_equals( + serialize_to_json("./example &> output.txt"), + serde_json::json!({ + "items": [{ + "isAsync": false, + "sequence": { + "inner": { + "inner": { + "args": [[{ + "kind": "text", + "value": "./example" + }]], + "envVars": [], + "kind": "simple" + }, + "kind": "command", + "redirect": { + "ioFile": { + "kind": "word", + "value": [{ + "kind": "text", + "value": "output.txt" + }], + }, + "maybeFd": { + "kind": "stdoutStderr" + }, + "op": { + "kind": "output", + "value": "overwrite", + } + } + }, + "kind": "pipeline", + "negated": false + } + }] + }), + ); + assert_json_equals( + serialize_to_json("./example < output.txt"), + serde_json::json!({ + "items": [{ + "isAsync": false, + "sequence": { + "inner": { + "inner": { + "args": [[{ + "kind": "text", + "value": "./example" + }]], + "envVars": [], + "kind": "simple" + }, + "kind": "command", + "redirect": { + "ioFile": { + "kind": "word", + "value": [{ + "kind": "text", + "value": "output.txt" + }], + }, + "maybeFd": null, + "op": { + "kind": "input", + "value": "redirect", + } + } + }, + "kind": "pipeline", + "negated": false + } + }] + }), + ); + + assert_json_equals( + serialize_to_json("./example <&0"), + serde_json::json!({ + "items": [{ + "isAsync": false, + "sequence": { + "inner": { + "inner": { + "args": [[{ + "kind": "text", + "value": "./example" + }]], + "envVars": [], + "kind": "simple" + }, + "kind": "command", + "redirect": { + "ioFile": { + "kind": "fd", + "value": 0, + }, + "maybeFd": null, + "op": { + "kind": "input", + "value": "redirect", + } + } + }, + "kind": "pipeline", + "negated": false + } + }] + }), + ); + } + + #[cfg(feature = "serialization")] + #[track_caller] + fn assert_json_equals( + actual: serde_json::Value, + expected: serde_json::Value, + ) { + if actual != expected { + let actual = serde_json::to_string_pretty(&actual).unwrap(); + let expected = serde_json::to_string_pretty(&expected).unwrap(); + assert_eq!(actual, expected); + } + } + + #[cfg(feature = "serialization")] + fn serialize_to_json(text: &str) -> serde_json::Value { + let command = parse(text).unwrap(); + serde_json::to_value(command).unwrap() + } +} diff --git a/scripts/script_2.sh b/scripts/script_2.sh index c6f954a..7dae140 100644 --- a/scripts/script_2.sh +++ b/scripts/script_2.sh @@ -1 +1 @@ -echo "Double word" 'Single word' \ No newline at end of file +echo "Hello, world" | grep "Hello" > output.txt && cat output.txt \ No newline at end of file diff --git a/scripts/script_3.sh b/scripts/script_3.sh index d250ea9..77556c4 100644 --- a/scripts/script_3.sh +++ b/scripts/script_3.sh @@ -1 +1,2 @@ -echo $PATH; \ No newline at end of file +echo $PATH +echo "Hello, world" \ No newline at end of file diff --git a/scripts/script_5.sh b/scripts/script_5.sh new file mode 100644 index 0000000..237614e --- /dev/null +++ b/scripts/script_5.sh @@ -0,0 +1,2 @@ +TEST=1 +echo $TEST From 1abd9d5df10a87b9c68cf275b0017ba6e16632b4 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sat, 31 Aug 2024 12:24:12 -0400 Subject: [PATCH 6/7] Revert formatting settings --- crates/deno_task_shell/.rustfmt.toml | 2 +- crates/deno_task_shell/src/parser.rs | 1214 +++++----- crates/deno_task_shell/src/shell/command.rs | 642 +++--- .../src/shell/commands/args.rs | 140 +- .../deno_task_shell/src/shell/commands/cat.rs | 205 +- .../deno_task_shell/src/shell/commands/cd.rs | 222 +- .../src/shell/commands/cp_mv.rs | 797 ++++--- .../src/shell/commands/echo.rs | 14 +- .../src/shell/commands/executable.rs | 104 +- .../src/shell/commands/exit.rs | 168 +- .../src/shell/commands/export.rs | 36 +- .../src/shell/commands/head.rs | 522 +++-- .../src/shell/commands/mkdir.rs | 355 ++- .../deno_task_shell/src/shell/commands/mod.rs | 202 +- .../deno_task_shell/src/shell/commands/pwd.rs | 139 +- .../deno_task_shell/src/shell/commands/rm.rs | 577 +++-- .../src/shell/commands/sleep.rs | 198 +- .../src/shell/commands/unset.rs | 126 +- .../src/shell/commands/xargs.rs | 514 ++--- crates/deno_task_shell/src/shell/execute.rs | 1590 +++++++------ crates/deno_task_shell/src/shell/fs_util.rs | 85 +- crates/deno_task_shell/src/shell/test.rs | 2020 ++++++++--------- .../deno_task_shell/src/shell/test_builder.rs | 421 ++-- crates/deno_task_shell/src/shell/types.rs | 654 +++--- 24 files changed, 5412 insertions(+), 5535 deletions(-) diff --git a/crates/deno_task_shell/.rustfmt.toml b/crates/deno_task_shell/.rustfmt.toml index 074ce96..9bb8d9d 100644 --- a/crates/deno_task_shell/.rustfmt.toml +++ b/crates/deno_task_shell/.rustfmt.toml @@ -1,3 +1,3 @@ max_width = 80 -tab_spaces = 4 +tab_spaces = 2 edition = "2021" diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index 5689ffa..d0c830c 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -5,7 +5,6 @@ use pest::iterators::Pair; use pest::Parser; use pest_derive::Parser; - // Shell grammar rules this is loosely based on: // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_10_02 @@ -13,206 +12,206 @@ use pest_derive::Parser; #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SequentialList { - pub items: Vec, + pub items: Vec, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SequentialListItem { - pub is_async: bool, - pub sequence: Sequence, + pub is_async: bool, + pub sequence: Sequence, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum Sequence { - /// `MY_VAR=5` - ShellVar(EnvVar), - /// `cmd_name `, `cmd1 | cmd2` - Pipeline(Pipeline), - /// `cmd1 && cmd2 || cmd3` - BooleanList(Box), + /// `MY_VAR=5` + ShellVar(EnvVar), + /// `cmd_name `, `cmd1 | cmd2` + Pipeline(Pipeline), + /// `cmd1 && cmd2 || cmd3` + BooleanList(Box), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Pipeline { - /// `! pipeline` - pub negated: bool, - pub inner: PipelineInner, + /// `! pipeline` + pub negated: bool, + pub inner: PipelineInner, } impl From for Sequence { - fn from(p: Pipeline) -> Self { - Sequence::Pipeline(p) - } + fn from(p: Pipeline) -> Self { + Sequence::Pipeline(p) + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum PipelineInner { - /// Ex. `cmd_name ` - Command(Command), - /// `cmd1 | cmd2` - PipeSequence(Box), + /// Ex. `cmd_name ` + Command(Command), + /// `cmd1 | cmd2` + PipeSequence(Box), } impl From for PipelineInner { - fn from(p: PipeSequence) -> Self { - PipelineInner::PipeSequence(Box::new(p)) - } + fn from(p: PipeSequence) -> Self { + PipelineInner::PipeSequence(Box::new(p)) + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum BooleanListOperator { - // && - And, - // || - Or, + // && + And, + // || + Or, } impl BooleanListOperator { - pub fn as_str(&self) -> &'static str { - match self { - BooleanListOperator::And => "&&", - BooleanListOperator::Or => "||", - } + pub fn as_str(&self) -> &'static str { + match self { + BooleanListOperator::And => "&&", + BooleanListOperator::Or => "||", } + } - pub fn moves_next_for_exit_code(&self, exit_code: i32) -> bool { - *self == BooleanListOperator::Or && exit_code != 0 - || *self == BooleanListOperator::And && exit_code == 0 - } + pub fn moves_next_for_exit_code(&self, exit_code: i32) -> bool { + *self == BooleanListOperator::Or && exit_code != 0 + || *self == BooleanListOperator::And && exit_code == 0 + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct BooleanList { - pub current: Sequence, - pub op: BooleanListOperator, - pub next: Sequence, + pub current: Sequence, + pub op: BooleanListOperator, + pub next: Sequence, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PipeSequenceOperator { - // | - Stdout, - // |& - StdoutStderr, + // | + Stdout, + // |& + StdoutStderr, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct PipeSequence { - pub current: Command, - pub op: PipeSequenceOperator, - pub next: PipelineInner, + pub current: Command, + pub op: PipeSequenceOperator, + pub next: PipelineInner, } impl From for Sequence { - fn from(p: PipeSequence) -> Self { - Sequence::Pipeline(Pipeline { - negated: false, - inner: p.into(), - }) - } + fn from(p: PipeSequence) -> Self { + Sequence::Pipeline(Pipeline { + negated: false, + inner: p.into(), + }) + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Command { - pub inner: CommandInner, - pub redirect: Option, + pub inner: CommandInner, + pub redirect: Option, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum CommandInner { - /// `cmd_name ` - Simple(SimpleCommand), - /// `(list)` - Subshell(Box), + /// `cmd_name ` + Simple(SimpleCommand), + /// `(list)` + Subshell(Box), } impl From for Sequence { - fn from(c: Command) -> Self { - Pipeline { - negated: false, - inner: c.into(), - } - .into() + fn from(c: Command) -> Self { + Pipeline { + negated: false, + inner: c.into(), } + .into() + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SimpleCommand { - pub env_vars: Vec, - pub args: Vec, + pub env_vars: Vec, + pub args: Vec, } impl From for Command { - fn from(c: SimpleCommand) -> Self { - Command { - redirect: None, - inner: CommandInner::Simple(c), - } + fn from(c: SimpleCommand) -> Self { + Command { + redirect: None, + inner: CommandInner::Simple(c), } + } } impl From for PipelineInner { - fn from(c: SimpleCommand) -> Self { - PipelineInner::Command(c.into()) - } + fn from(c: SimpleCommand) -> Self { + PipelineInner::Command(c.into()) + } } impl From for PipelineInner { - fn from(c: Command) -> Self { - PipelineInner::Command(c) - } + fn from(c: Command) -> Self { + PipelineInner::Command(c) + } } impl From for Sequence { - fn from(c: SimpleCommand) -> Self { - let command: Command = c.into(); - command.into() - } + fn from(c: SimpleCommand) -> Self { + let command: Command = c.into(); + command.into() + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, PartialEq, Eq, Clone)] pub struct EnvVar { - pub name: String, - pub value: Word, + pub name: String, + pub value: Word, } impl EnvVar { - pub fn new(name: String, value: Word) -> Self { - EnvVar { name, value } - } + pub fn new(name: String, value: Word) -> Self { + EnvVar { name, value } + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] @@ -220,110 +219,110 @@ impl EnvVar { pub struct Word(Vec); impl Word { - pub fn new(parts: Vec) -> Self { - Word(parts) - } + pub fn new(parts: Vec) -> Self { + Word(parts) + } - pub fn new_empty() -> Self { - Word(vec![]) - } + pub fn new_empty() -> Self { + Word(vec![]) + } - pub fn new_string(text: &str) -> Self { - Word(vec![WordPart::Quoted(vec![WordPart::Text( - text.to_string(), - )])]) - } + pub fn new_string(text: &str) -> Self { + Word(vec![WordPart::Quoted(vec![WordPart::Text( + text.to_string(), + )])]) + } - pub fn new_word(text: &str) -> Self { - Word(vec![WordPart::Text(text.to_string())]) - } + pub fn new_word(text: &str) -> Self { + Word(vec![WordPart::Text(text.to_string())]) + } - pub fn parts(&self) -> &Vec { - &self.0 - } + pub fn parts(&self) -> &Vec { + &self.0 + } - pub fn into_parts(self) -> Vec { - self.0 - } + pub fn into_parts(self) -> Vec { + self.0 + } } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "value") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "value") )] #[derive(Debug, PartialEq, Eq, Clone)] pub enum WordPart { - /// Text in the string (ex. `some text`) - Text(String), - /// Variable substitution (ex. `$MY_VAR`) - Variable(String), - /// Command substitution (ex. `$(command)`) - Command(SequentialList), - /// Quoted string (ex. `"hello"` or `'test'`) - Quoted(Vec), + /// Text in the string (ex. `some text`) + Text(String), + /// Variable substitution (ex. `$MY_VAR`) + Variable(String), + /// Command substitution (ex. `$(command)`) + Command(SequentialList), + /// Quoted string (ex. `"hello"` or `'test'`) + Quoted(Vec), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "fd") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "fd") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectFd { - Fd(u32), - StdoutStderr, + Fd(u32), + StdoutStderr, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Redirect { - pub maybe_fd: Option, - pub op: RedirectOp, - pub io_file: IoFile, + pub maybe_fd: Option, + pub op: RedirectOp, + pub io_file: IoFile, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "value") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "value") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum IoFile { - /// Filename to redirect to/from (ex. `file.txt`` in `cmd < file.txt`) - Word(Word), - /// File descriptor to redirect to/from (ex. `2` in `cmd >&2`) - Fd(u32), + /// Filename to redirect to/from (ex. `file.txt`` in `cmd < file.txt`) + Word(Word), + /// File descriptor to redirect to/from (ex. `2` in `cmd >&2`) + Fd(u32), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( - feature = "serialization", - serde(rename_all = "camelCase", tag = "kind", content = "value") + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "value") )] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectOp { - Input(RedirectOpInput), - Output(RedirectOpOutput), + Input(RedirectOpInput), + Output(RedirectOpOutput), } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectOpInput { - /// < - Redirect, + /// < + Redirect, } #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RedirectOpOutput { - /// > - Overwrite, - /// >> - Append, + /// > + Overwrite, + /// >> + Append, } #[derive(Parser)] @@ -331,533 +330,561 @@ pub enum RedirectOpOutput { struct ShellParser; pub fn parse(input: &str) -> Result { - let mut pairs = ShellParser::parse(Rule::FILE, input)?; + let mut pairs = ShellParser::parse(Rule::FILE, input)?; - parse_file(pairs.next().unwrap()) + parse_file(pairs.next().unwrap()) } fn parse_file(pairs: Pair) -> Result { - parse_complete_command(pairs.into_inner().next().unwrap()) + parse_complete_command(pairs.into_inner().next().unwrap()) } fn parse_complete_command(pair: Pair) -> Result { - assert!(pair.as_rule() == Rule::complete_command); - let mut items = Vec::new(); - for command in pair.into_inner() { - match command.as_rule() { - Rule::list => { - parse_list(command, &mut items)?; - } - Rule::EOI => { - break; - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected rule in complete_command: {:?}", - command.as_rule() - )); - } - } + assert!(pair.as_rule() == Rule::complete_command); + let mut items = Vec::new(); + for command in pair.into_inner() { + match command.as_rule() { + Rule::list => { + parse_list(command, &mut items)?; + } + Rule::EOI => { + break; + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in complete_command: {:?}", + command.as_rule() + )); + } } - Ok(SequentialList { items }) + } + Ok(SequentialList { items }) } -fn parse_list(pair: Pair, items: &mut Vec) -> Result<()> { - for item in pair.into_inner() { - match item.as_rule() { - Rule::and_or => { - let sequence = parse_and_or(item)?; - items.push(SequentialListItem { - is_async: false, - sequence, - }); - } - Rule::separator_op => { - if let Some(last) = items.last_mut() { - last.is_async = item.as_str() == "&"; - } - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected rule in list: {:?}", - item.as_rule() - )); - } +fn parse_list( + pair: Pair, + items: &mut Vec, +) -> Result<()> { + for item in pair.into_inner() { + match item.as_rule() { + Rule::and_or => { + let sequence = parse_and_or(item)?; + items.push(SequentialListItem { + is_async: false, + sequence, + }); + } + Rule::separator_op => { + if let Some(last) = items.last_mut() { + last.is_async = item.as_str() == "&"; } + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in list: {:?}", + item.as_rule() + )); + } } - Ok(()) + } + Ok(()) } -fn parse_compound_list(pair: Pair, items: &mut Vec) -> Result<()> { - for item in pair.into_inner() { - match item.as_rule() { - Rule::term => { - parse_term(item, items)?; - } - Rule::newline_list => { - // Ignore newlines - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected rule in compound_list: {:?}", - item.as_rule() - )); - } - } +fn parse_compound_list( + pair: Pair, + items: &mut Vec, +) -> Result<()> { + for item in pair.into_inner() { + match item.as_rule() { + Rule::term => { + parse_term(item, items)?; + } + Rule::newline_list => { + // Ignore newlines + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in compound_list: {:?}", + item.as_rule() + )); + } } - Ok(()) + } + Ok(()) } -fn parse_term(pair: Pair, items: &mut Vec) -> Result<()> { - for item in pair.into_inner() { - match item.as_rule() { - Rule::and_or => { - let sequence = parse_and_or(item)?; - items.push(SequentialListItem { - sequence, - is_async: false, - }); - } - Rule::separator_op => { - if let Some(last) = items.last_mut() { - last.is_async = item.as_str() == "&"; - } - } - _ => { - return Err(anyhow::anyhow!( - "Unexpected rule in term: {:?}", - item.as_rule() - )); - } +fn parse_term( + pair: Pair, + items: &mut Vec, +) -> Result<()> { + for item in pair.into_inner() { + match item.as_rule() { + Rule::and_or => { + let sequence = parse_and_or(item)?; + items.push(SequentialListItem { + sequence, + is_async: false, + }); + } + Rule::separator_op => { + if let Some(last) = items.last_mut() { + last.is_async = item.as_str() == "&"; } + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in term: {:?}", + item.as_rule() + )); + } } - Ok(()) + } + Ok(()) } fn parse_and_or(pair: Pair) -> Result { - assert!(pair.as_rule() == Rule::and_or); - let mut items = pair.into_inner(); - let first_item = items.next().unwrap(); - let mut current = match first_item.as_rule() { - Rule::ASSIGNMENT_WORD => parse_shell_var(first_item)?, - Rule::pipeline => parse_pipeline(first_item)?, - _ => unreachable!(), - }; - - match items.next() { - Some(next_item) => { - if next_item.as_rule() == Rule::ASSIGNMENT_WORD { - anyhow::bail!("Multiple assignment words before && or || is not supported yet"); - } else { - let op = match next_item.as_str() { - "&&" => BooleanListOperator::And, - "||" => BooleanListOperator::Or, - _ => unreachable!(), - }; - - let next_item = items.next().unwrap(); - let next = parse_and_or(next_item)?; - current = Sequence::BooleanList(Box::new(BooleanList { - current, - op, - next, - })); - } - } - None => { - return Ok(current); - } + assert!(pair.as_rule() == Rule::and_or); + let mut items = pair.into_inner(); + let first_item = items.next().unwrap(); + let mut current = match first_item.as_rule() { + Rule::ASSIGNMENT_WORD => parse_shell_var(first_item)?, + Rule::pipeline => parse_pipeline(first_item)?, + _ => unreachable!(), + }; + + match items.next() { + Some(next_item) => { + if next_item.as_rule() == Rule::ASSIGNMENT_WORD { + anyhow::bail!( + "Multiple assignment words before && or || is not supported yet" + ); + } else { + let op = match next_item.as_str() { + "&&" => BooleanListOperator::And, + "||" => BooleanListOperator::Or, + _ => unreachable!(), + }; + + let next_item = items.next().unwrap(); + let next = parse_and_or(next_item)?; + current = + Sequence::BooleanList(Box::new(BooleanList { current, op, next })); + } } + None => { + return Ok(current); + } + } - Ok(current) + Ok(current) } fn parse_shell_var(pair: Pair) -> Result { - let mut inner = pair.into_inner(); - let name = inner.next() - .ok_or_else(|| anyhow::anyhow!("Expected variable name"))? - .as_str() - .to_string(); - let value = inner.next() - .ok_or_else(|| anyhow::anyhow!("Expected variable value"))?; - let value = parse_word(value)?; - Ok(Sequence::ShellVar(EnvVar { name, value })) + let mut inner = pair.into_inner(); + let name = inner + .next() + .ok_or_else(|| anyhow::anyhow!("Expected variable name"))? + .as_str() + .to_string(); + let value = inner + .next() + .ok_or_else(|| anyhow::anyhow!("Expected variable value"))?; + let value = parse_word(value)?; + Ok(Sequence::ShellVar(EnvVar { name, value })) } fn parse_pipeline(pair: Pair) -> Result { - let mut inner = pair.into_inner(); - - // Check if the first element is Bang (negation) - let first = inner - .next() - .ok_or_else(|| anyhow::anyhow!("Expected pipeline content"))?; - let (negated, pipe_sequence) = if first.as_rule() == Rule::Bang { - // If it's Bang, the next element should be the pipe_sequence - let pipe_sequence = inner.next().ok_or_else(|| { - anyhow::anyhow!("Expected pipe sequence after negation") - })?; - (true, pipe_sequence) - } else { - // If it's not Bang, this element itself is the pipe_sequence - (false, first) - }; - - let pipeline_inner = parse_pipe_sequence(pipe_sequence)?; - - Ok(Sequence::Pipeline(Pipeline { - negated, - inner: pipeline_inner, - })) + let mut inner = pair.into_inner(); + + // Check if the first element is Bang (negation) + let first = inner + .next() + .ok_or_else(|| anyhow::anyhow!("Expected pipeline content"))?; + let (negated, pipe_sequence) = if first.as_rule() == Rule::Bang { + // If it's Bang, the next element should be the pipe_sequence + let pipe_sequence = inner.next().ok_or_else(|| { + anyhow::anyhow!("Expected pipe sequence after negation") + })?; + (true, pipe_sequence) + } else { + // If it's not Bang, this element itself is the pipe_sequence + (false, first) + }; + + let pipeline_inner = parse_pipe_sequence(pipe_sequence)?; + + Ok(Sequence::Pipeline(Pipeline { + negated, + inner: pipeline_inner, + })) } fn parse_pipe_sequence(pair: Pair) -> Result { - let mut inner = pair.into_inner(); - - // Parse the first command - let first_command = inner.next().ok_or_else(|| { - anyhow::anyhow!("Expected at least one command in pipe sequence") - })?; - let current = parse_command(first_command)?; - - // Check if there's a pipe operator - match inner.next() { - Some(pipe_op) => { - let op = match pipe_op.as_rule() { - Rule::Stdout => PipeSequenceOperator::Stdout, - Rule::StdoutStderr => PipeSequenceOperator::StdoutStderr, - _ => { - return Err(anyhow::anyhow!( - "Expected pipe operator, found {:?}", - pipe_op.as_rule() - )) - } - }; - - // Parse the rest of the pipe sequence - let next_sequence = inner.next().ok_or_else(|| { - anyhow::anyhow!("Expected command after pipe operator") - })?; - let next = parse_pipe_sequence(next_sequence)?; - - Ok(PipelineInner::PipeSequence(Box::new(PipeSequence { - current, - op, - next, - }))) + let mut inner = pair.into_inner(); + + // Parse the first command + let first_command = inner.next().ok_or_else(|| { + anyhow::anyhow!("Expected at least one command in pipe sequence") + })?; + let current = parse_command(first_command)?; + + // Check if there's a pipe operator + match inner.next() { + Some(pipe_op) => { + let op = match pipe_op.as_rule() { + Rule::Stdout => PipeSequenceOperator::Stdout, + Rule::StdoutStderr => PipeSequenceOperator::StdoutStderr, + _ => { + return Err(anyhow::anyhow!( + "Expected pipe operator, found {:?}", + pipe_op.as_rule() + )) } - None => Ok(PipelineInner::Command(current)), + }; + + // Parse the rest of the pipe sequence + let next_sequence = inner.next().ok_or_else(|| { + anyhow::anyhow!("Expected command after pipe operator") + })?; + let next = parse_pipe_sequence(next_sequence)?; + + Ok(PipelineInner::PipeSequence(Box::new(PipeSequence { + current, + op, + next, + }))) } + None => Ok(PipelineInner::Command(current)), + } } fn parse_command(pair: Pair) -> Result { - let inner = pair.into_inner().next().unwrap(); - match inner.as_rule() { - Rule::simple_command => parse_simple_command(inner), - Rule::compound_command => parse_compound_command(inner), - Rule::function_definition => { - todo!("function definitions are not supported yet") - } - _ => Err(anyhow::anyhow!( - "Unexpected rule in command: {:?}", - inner.as_rule() - )), + let inner = pair.into_inner().next().unwrap(); + match inner.as_rule() { + Rule::simple_command => parse_simple_command(inner), + Rule::compound_command => parse_compound_command(inner), + Rule::function_definition => { + todo!("function definitions are not supported yet") } + _ => Err(anyhow::anyhow!( + "Unexpected rule in command: {:?}", + inner.as_rule() + )), + } } fn parse_simple_command(pair: Pair) -> Result { - let mut env_vars = Vec::new(); - let mut args = Vec::new(); - let mut redirect = None; - - for item in pair.into_inner() { - match item.as_rule() { - Rule::cmd_prefix => { - for prefix in item.into_inner() { - match prefix.as_rule() { - Rule::ASSIGNMENT_WORD => { - env_vars.push(parse_env_var(prefix)?) - } - Rule::io_redirect => todo!("io_redirect as prefix"), - _ => { - return Err(anyhow::anyhow!( - "Unexpected rule in cmd_prefix: {:?}", - prefix.as_rule() - )) - } - } - } + let mut env_vars = Vec::new(); + let mut args = Vec::new(); + let mut redirect = None; + + for item in pair.into_inner() { + match item.as_rule() { + Rule::cmd_prefix => { + for prefix in item.into_inner() { + match prefix.as_rule() { + Rule::ASSIGNMENT_WORD => env_vars.push(parse_env_var(prefix)?), + Rule::io_redirect => todo!("io_redirect as prefix"), + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in cmd_prefix: {:?}", + prefix.as_rule() + )) } - Rule::cmd_word | Rule::cmd_name => { - args.push(parse_word(item.into_inner().next().unwrap())?) + } + } + } + Rule::cmd_word | Rule::cmd_name => { + args.push(parse_word(item.into_inner().next().unwrap())?) + } + Rule::cmd_suffix => { + for suffix in item.into_inner() { + match suffix.as_rule() { + Rule::UNQUOTED_PENDING_WORD => args.push(parse_word(suffix)?), + Rule::io_redirect => { + redirect = Some(parse_io_redirect(suffix)?); } - Rule::cmd_suffix => { - for suffix in item.into_inner() { - match suffix.as_rule() { - Rule::UNQUOTED_PENDING_WORD => args.push(parse_word(suffix)?), - Rule::io_redirect => { - redirect = Some(parse_io_redirect(suffix)?); - } - Rule::QUOTED_WORD => args.push(Word::new(vec![parse_quoted_word(suffix)?])), - _ => { - return Err(anyhow::anyhow!( - "Unexpected rule in cmd_suffix: {:?}", - suffix.as_rule() - )) - } - } - } + Rule::QUOTED_WORD => { + args.push(Word::new(vec![parse_quoted_word(suffix)?])) } _ => { - return Err(anyhow::anyhow!( - "Unexpected rule in simple_command: {:?}", - item.as_rule() - )) + return Err(anyhow::anyhow!( + "Unexpected rule in cmd_suffix: {:?}", + suffix.as_rule() + )) } + } } + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in simple_command: {:?}", + item.as_rule() + )) + } } + } - Ok(Command { - inner: CommandInner::Simple(SimpleCommand { env_vars, args }), - redirect, - }) + Ok(Command { + inner: CommandInner::Simple(SimpleCommand { env_vars, args }), + redirect, + }) } fn parse_compound_command(pair: Pair) -> Result { - let inner = pair.into_inner().next().unwrap(); - match inner.as_rule() { - Rule::brace_group => todo!("brace_group"), - Rule::subshell => parse_subshell(inner), - Rule::for_clause => todo!("for_clause"), - Rule::case_clause => todo!("case_clause"), - Rule::if_clause => todo!("if_clause"), - Rule::while_clause => todo!("while_clause"), - Rule::until_clause => todo!("until_clause"), - _ => Err(anyhow::anyhow!( - "Unexpected rule in compound_command: {:?}", - inner.as_rule() - )) - } + let inner = pair.into_inner().next().unwrap(); + match inner.as_rule() { + Rule::brace_group => todo!("brace_group"), + Rule::subshell => parse_subshell(inner), + Rule::for_clause => todo!("for_clause"), + Rule::case_clause => todo!("case_clause"), + Rule::if_clause => todo!("if_clause"), + Rule::while_clause => todo!("while_clause"), + Rule::until_clause => todo!("until_clause"), + _ => Err(anyhow::anyhow!( + "Unexpected rule in compound_command: {:?}", + inner.as_rule() + )), + } } fn parse_subshell(pair: Pair) -> Result { - let mut items = Vec::new(); - if let Some(inner) = pair.into_inner().next() { - parse_compound_list(inner, &mut items)?; - Ok(Command { - inner: CommandInner::Subshell(Box::new(SequentialList { items })), - redirect: None, - }) - } else { - Err(anyhow::anyhow!( - "Unexpected end of input in subshell" - )) - } + let mut items = Vec::new(); + if let Some(inner) = pair.into_inner().next() { + parse_compound_list(inner, &mut items)?; + Ok(Command { + inner: CommandInner::Subshell(Box::new(SequentialList { items })), + redirect: None, + }) + } else { + Err(anyhow::anyhow!("Unexpected end of input in subshell")) + } } fn parse_word(pair: Pair) -> Result { - let mut parts = Vec::new(); - - match pair.as_rule() { - Rule::UNQUOTED_PENDING_WORD => { - for part in pair.into_inner() { - match part.as_rule() { - Rule::EXIT_STATUS => parts.push(WordPart::Variable("?".to_string())), - Rule::UNQUOTED_ESCAPE_CHAR | Rule::UNQUOTED_CHAR => { - if let Some(WordPart::Text(ref mut text)) = parts.last_mut() { - text.push(part.as_str().chars().next().unwrap()); - } else { - parts.push(WordPart::Text(part.as_str().to_string())); - } - }, - Rule::SUB_COMMAND => { - let command = parse_complete_command(part.into_inner().next().unwrap())?; - parts.push(WordPart::Command(command)); - }, - Rule::VARIABLE => parts.push(WordPart::Variable(part.as_str().to_string())), - Rule::QUOTED_WORD => { - let quoted = parse_quoted_word(part)?; - parts.push(quoted); - }, - _ => return Err(anyhow::anyhow!("Unexpected rule in UNQUOTED_PENDING_WORD: {:?}", part.as_rule())), - } + let mut parts = Vec::new(); + + match pair.as_rule() { + Rule::UNQUOTED_PENDING_WORD => { + for part in pair.into_inner() { + match part.as_rule() { + Rule::EXIT_STATUS => parts.push(WordPart::Variable("?".to_string())), + Rule::UNQUOTED_ESCAPE_CHAR | Rule::UNQUOTED_CHAR => { + if let Some(WordPart::Text(ref mut text)) = parts.last_mut() { + text.push(part.as_str().chars().next().unwrap()); + } else { + parts.push(WordPart::Text(part.as_str().to_string())); } - }, - Rule::QUOTED_WORD => { - let quoted = parse_quoted_word(pair)?; + } + Rule::SUB_COMMAND => { + let command = + parse_complete_command(part.into_inner().next().unwrap())?; + parts.push(WordPart::Command(command)); + } + Rule::VARIABLE => { + parts.push(WordPart::Variable(part.as_str().to_string())) + } + Rule::QUOTED_WORD => { + let quoted = parse_quoted_word(part)?; parts.push(quoted); - }, - Rule::ASSIGNMENT_WORD => { - let assignment_str = pair.as_str().to_string(); - parts.push(WordPart::Text(assignment_str)); - }, - _ => { + } + _ => { return Err(anyhow::anyhow!( - "Unexpected rule in word: {:?}", - pair.as_rule() + "Unexpected rule in UNQUOTED_PENDING_WORD: {:?}", + part.as_rule() )) + } } + } } - - if parts.is_empty() { - Ok(Word::new_empty()) - } else { - Ok(Word::new(parts)) + Rule::QUOTED_WORD => { + let quoted = parse_quoted_word(pair)?; + parts.push(quoted); } + Rule::ASSIGNMENT_WORD => { + let assignment_str = pair.as_str().to_string(); + parts.push(WordPart::Text(assignment_str)); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in word: {:?}", + pair.as_rule() + )) + } + } + + if parts.is_empty() { + Ok(Word::new_empty()) + } else { + Ok(Word::new(parts)) + } } fn parse_quoted_word(pair: Pair) -> Result { - let mut parts = Vec::new(); - let inner = pair.into_inner().next().unwrap(); - - match inner.as_rule() { - Rule::DOUBLE_QUOTED => { - let inner = inner.into_inner().next().unwrap(); - for part in inner.into_inner() { - match part.as_rule() { - Rule::EXIT_STATUS => parts.push(WordPart::Text("$?".to_string())), - Rule::QUOTED_ESCAPE_CHAR => { - if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { - s.push_str(part.as_str()); - } else { - parts.push(WordPart::Text(part.as_str().to_string())); - } - }, - Rule::SUB_COMMAND => { - let command = parse_complete_command(part.into_inner().next().unwrap())?; - parts.push(WordPart::Command(command)); - }, - Rule::VARIABLE => parts.push(WordPart::Variable(part.as_str()[1..].to_string())), - Rule::QUOTED_CHAR => { - if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { - s.push_str(part.as_str()); - } else { - parts.push(WordPart::Text(part.as_str().to_string())); - } - }, - _ => return Err(anyhow::anyhow!("Unexpected rule in DOUBLE_QUOTED: {:?}", part.as_rule())), - } - }; - Ok(WordPart::Quoted(parts)) - }, - Rule::SINGLE_QUOTED => { - let inner_str = inner.as_str(); - let trimmed_str = &inner_str[1..inner_str.len() - 1]; - Ok(WordPart::Quoted(vec![WordPart::Text(trimmed_str.to_string())])) - }, - _ => Err(anyhow::anyhow!("Unexpected rule in QUOTED_WORD: {:?}", inner.as_rule())), + let mut parts = Vec::new(); + let inner = pair.into_inner().next().unwrap(); + + match inner.as_rule() { + Rule::DOUBLE_QUOTED => { + let inner = inner.into_inner().next().unwrap(); + for part in inner.into_inner() { + match part.as_rule() { + Rule::EXIT_STATUS => parts.push(WordPart::Text("$?".to_string())), + Rule::QUOTED_ESCAPE_CHAR => { + if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { + s.push_str(part.as_str()); + } else { + parts.push(WordPart::Text(part.as_str().to_string())); + } + } + Rule::SUB_COMMAND => { + let command = + parse_complete_command(part.into_inner().next().unwrap())?; + parts.push(WordPart::Command(command)); + } + Rule::VARIABLE => { + parts.push(WordPart::Variable(part.as_str()[1..].to_string())) + } + Rule::QUOTED_CHAR => { + if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { + s.push_str(part.as_str()); + } else { + parts.push(WordPart::Text(part.as_str().to_string())); + } + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected rule in DOUBLE_QUOTED: {:?}", + part.as_rule() + )) + } + } + } + Ok(WordPart::Quoted(parts)) } + Rule::SINGLE_QUOTED => { + let inner_str = inner.as_str(); + let trimmed_str = &inner_str[1..inner_str.len() - 1]; + Ok(WordPart::Quoted(vec![WordPart::Text( + trimmed_str.to_string(), + )])) + } + _ => Err(anyhow::anyhow!( + "Unexpected rule in QUOTED_WORD: {:?}", + inner.as_rule() + )), + } } fn parse_env_var(pair: Pair) -> Result { - let mut parts = pair.into_inner(); - - // Get the name of the environment variable - let name = parts - .next() - .ok_or_else(|| anyhow!("Expected variable name"))? - .as_str() - .to_string(); - - // Get the value of the environment variable - let word_value = if let Some(value) = parts.next() { - parse_word(value)? - } else { - Word::new_empty() - }; - - Ok(EnvVar { - name, - value: word_value, - }) + let mut parts = pair.into_inner(); + + // Get the name of the environment variable + let name = parts + .next() + .ok_or_else(|| anyhow!("Expected variable name"))? + .as_str() + .to_string(); + + // Get the value of the environment variable + let word_value = if let Some(value) = parts.next() { + parse_word(value)? + } else { + Word::new_empty() + }; + + Ok(EnvVar { + name, + value: word_value, + }) } fn parse_io_redirect(pair: Pair) -> Result { - let mut inner = pair.into_inner(); - - // Parse the optional IO number or AMPERSAND - let (maybe_fd, op_and_file) = match inner.next() { - Some(p) if p.as_rule() == Rule::IO_NUMBER => ( - Some(RedirectFd::Fd(p.as_str().parse::().unwrap())), - inner.next().ok_or_else(|| { - anyhow!("Expected redirection operator after IO number") - })?, - ), - Some(p) if p.as_rule() == Rule::AMPERSAND => ( - Some(RedirectFd::StdoutStderr), - inner.next().ok_or_else(|| { - anyhow!("Expected redirection operator after &") - })?, - ), - Some(p) => (None, p), - None => return Err(anyhow!("Unexpected end of input in io_redirect")), - }; - - let (op, io_file) = parse_io_file(op_and_file)?; - - Ok(Redirect { - maybe_fd, - op, - io_file, - }) + let mut inner = pair.into_inner(); + + // Parse the optional IO number or AMPERSAND + let (maybe_fd, op_and_file) = match inner.next() { + Some(p) if p.as_rule() == Rule::IO_NUMBER => ( + Some(RedirectFd::Fd(p.as_str().parse::().unwrap())), + inner.next().ok_or_else(|| { + anyhow!("Expected redirection operator after IO number") + })?, + ), + Some(p) if p.as_rule() == Rule::AMPERSAND => ( + Some(RedirectFd::StdoutStderr), + inner + .next() + .ok_or_else(|| anyhow!("Expected redirection operator after &"))?, + ), + Some(p) => (None, p), + None => return Err(anyhow!("Unexpected end of input in io_redirect")), + }; + + let (op, io_file) = parse_io_file(op_and_file)?; + + Ok(Redirect { + maybe_fd, + op, + io_file, + }) } fn parse_io_file(pair: Pair) -> Result<(RedirectOp, IoFile)> { - let mut inner = pair.into_inner(); - let op = inner - .next() - .ok_or_else(|| anyhow!("Expected redirection operator"))?; - let filename = inner.next().ok_or_else(|| { - anyhow!("Expected filename after redirection operator") - })?; - - let redirect_op = match op.as_rule() { - Rule::LESS => RedirectOp::Input(RedirectOpInput::Redirect), - Rule::GREAT => RedirectOp::Output(RedirectOpOutput::Overwrite), - Rule::DGREAT => RedirectOp::Output(RedirectOpOutput::Append), - Rule::LESSAND | Rule::GREATAND => { - // For these operators, the target must be a number (fd) - let target = filename.as_str(); - if let Ok(fd) = target.parse::() { - return Ok(( - if op.as_rule() == Rule::LESSAND { - RedirectOp::Input(RedirectOpInput::Redirect) - } else { - RedirectOp::Output(RedirectOpOutput::Overwrite) - }, - IoFile::Fd(fd), - )); - } else { - return Err(anyhow!( - "Expected a number after {} operator", - if op.as_rule() == Rule::LESSAND { - "<&" - } else { - ">&" - } - )); - } - } - _ => { - return Err(anyhow!( - "Unexpected redirection operator: {:?}", - op.as_rule() - )) - } - }; - - let io_file = if filename.as_rule() == Rule::FILE_NAME_PENDING_WORD { - IoFile::Word(parse_word(filename)?) - } else { + let mut inner = pair.into_inner(); + let op = inner + .next() + .ok_or_else(|| anyhow!("Expected redirection operator"))?; + let filename = inner + .next() + .ok_or_else(|| anyhow!("Expected filename after redirection operator"))?; + + let redirect_op = match op.as_rule() { + Rule::LESS => RedirectOp::Input(RedirectOpInput::Redirect), + Rule::GREAT => RedirectOp::Output(RedirectOpOutput::Overwrite), + Rule::DGREAT => RedirectOp::Output(RedirectOpOutput::Append), + Rule::LESSAND | Rule::GREATAND => { + // For these operators, the target must be a number (fd) + let target = filename.as_str(); + if let Ok(fd) = target.parse::() { + return Ok(( + if op.as_rule() == Rule::LESSAND { + RedirectOp::Input(RedirectOpInput::Redirect) + } else { + RedirectOp::Output(RedirectOpOutput::Overwrite) + }, + IoFile::Fd(fd), + )); + } else { return Err(anyhow!( - "Unexpected filename type: {:?}", - filename.as_rule() + "Expected a number after {} operator", + if op.as_rule() == Rule::LESSAND { + "<&" + } else { + ">&" + } )); - }; - - Ok((redirect_op, io_file)) + } + } + _ => { + return Err(anyhow!( + "Unexpected redirection operator: {:?}", + op.as_rule() + )) + } + }; + + let io_file = if filename.as_rule() == Rule::FILE_NAME_PENDING_WORD { + IoFile::Word(parse_word(filename)?) + } else { + return Err(anyhow!( + "Unexpected filename type: {:?}", + filename.as_rule() + )); + }; + + Ok((redirect_op, io_file)) } - #[cfg(test)] mod test { use super::*; @@ -889,8 +916,8 @@ mod test { .map_err(|e| anyhow::Error::msg(e.to_string()))? .next() .unwrap(); - // println!("pairs: {:?}", pairs); - parse_complete_command(pairs) + // println!("pairs: {:?}", pairs); + parse_complete_command(pairs) }; // Test case 1 @@ -1042,10 +1069,7 @@ mod test { SequentialListItem { is_async: false, sequence: SimpleCommand { - env_vars: vec![EnvVar::new( - "A".to_string(), - Word::new_string("b"), - )], + env_vars: vec![EnvVar::new("A".to_string(), Word::new_string("b"))], args: vec![Word::new_word("command3")], } .into(), diff --git a/crates/deno_task_shell/src/shell/command.rs b/crates/deno_task_shell/src/shell/command.rs index bb2ad6d..9ca8a5b 100644 --- a/crates/deno_task_shell/src/shell/command.rs +++ b/crates/deno_task_shell/src/shell/command.rs @@ -19,403 +19,397 @@ use thiserror::Error; #[derive(Debug, Clone)] pub struct UnresolvedCommandName { - pub name: String, - pub base_dir: PathBuf, + pub name: String, + pub base_dir: PathBuf, } pub fn execute_unresolved_command_name( - command_name: UnresolvedCommandName, - mut context: ShellCommandContext, + command_name: UnresolvedCommandName, + mut context: ShellCommandContext, ) -> FutureExecuteResult { - async move { - let command = - match resolve_command(&command_name, &context, &context.args).await - { - Ok(command_path) => command_path, - Err(ResolveCommandError::CommandPath(err)) => { - let _ = context.stderr.write_line(&format!("{}", err)); - return ExecuteResult::Continue( - err.exit_code(), - Vec::new(), - Vec::new(), - ); - } - Err(ResolveCommandError::FailedShebang(err)) => { - let _ = context - .stderr - .write_line(&format!("{}: {}", command_name.name, err)); - return ExecuteResult::Continue( - err.exit_code(), - Vec::new(), - Vec::new(), - ); - } - }; - match command.command_name { - CommandName::Resolved(path) => { - ExecutableCommand::new(command_name.name, path) - .execute(context) - .await - } - CommandName::Unresolved(command_name) => { - context.args = command.args.into_owned(); - execute_unresolved_command_name(command_name, context).await - } + async move { + let command = + match resolve_command(&command_name, &context, &context.args).await { + Ok(command_path) => command_path, + Err(ResolveCommandError::CommandPath(err)) => { + let _ = context.stderr.write_line(&format!("{}", err)); + return ExecuteResult::Continue( + err.exit_code(), + Vec::new(), + Vec::new(), + ); + } + Err(ResolveCommandError::FailedShebang(err)) => { + let _ = context + .stderr + .write_line(&format!("{}: {}", command_name.name, err)); + return ExecuteResult::Continue( + err.exit_code(), + Vec::new(), + Vec::new(), + ); } + }; + match command.command_name { + CommandName::Resolved(path) => { + ExecutableCommand::new(command_name.name, path) + .execute(context) + .await + } + CommandName::Unresolved(command_name) => { + context.args = command.args.into_owned(); + execute_unresolved_command_name(command_name, context).await + } } - .boxed_local() + } + .boxed_local() } enum CommandName { - Resolved(PathBuf), - Unresolved(UnresolvedCommandName), + Resolved(PathBuf), + Unresolved(UnresolvedCommandName), } struct ResolvedCommand<'a> { - command_name: CommandName, - args: Cow<'a, Vec>, + command_name: CommandName, + args: Cow<'a, Vec>, } #[derive(Error, Debug)] enum ResolveCommandError { - #[error(transparent)] - CommandPath(#[from] ResolveCommandPathError), - #[error(transparent)] - FailedShebang(#[from] FailedShebangError), + #[error(transparent)] + CommandPath(#[from] ResolveCommandPathError), + #[error(transparent)] + FailedShebang(#[from] FailedShebangError), } #[derive(Error, Debug)] enum FailedShebangError { - #[error(transparent)] - CommandPath(#[from] ResolveCommandPathError), - #[error(transparent)] - Any(#[from] anyhow::Error), + #[error(transparent)] + CommandPath(#[from] ResolveCommandPathError), + #[error(transparent)] + Any(#[from] anyhow::Error), } impl FailedShebangError { - pub fn exit_code(&self) -> i32 { - match self { - FailedShebangError::CommandPath(err) => err.exit_code(), - FailedShebangError::Any(_) => 1, - } + pub fn exit_code(&self) -> i32 { + match self { + FailedShebangError::CommandPath(err) => err.exit_code(), + FailedShebangError::Any(_) => 1, } + } } async fn resolve_command<'a>( - command_name: &UnresolvedCommandName, - context: &ShellCommandContext, - original_args: &'a Vec, + command_name: &UnresolvedCommandName, + context: &ShellCommandContext, + original_args: &'a Vec, ) -> Result, ResolveCommandError> { - let command_path = match resolve_command_path( - &command_name.name, - &command_name.base_dir, - &context.state, - ) { - Ok(command_path) => command_path, - Err(err) => return Err(err.into()), - }; - - // only bother checking for a shebang when the path has a slash - // in it because for global commands someone on Windows likely - // won't have a script with a shebang in it on Windows - if command_name.name.contains('/') { - if let Some(shebang) = - resolve_shebang(&command_path).map_err(|err| { - ResolveCommandError::FailedShebang(FailedShebangError::Any( - err.into(), - )) - })? - { - let (shebang_command_name, mut args) = if shebang.string_split { - let mut args = parse_shebang_args(&shebang.command, context) - .await - .map_err(FailedShebangError::Any)?; - args.push(command_path.to_string_lossy().to_string()); - (args.remove(0), args) - } else { - ( - shebang.command, - vec![command_path.to_string_lossy().to_string()], - ) - }; - args.extend(original_args.iter().cloned()); - return Ok(ResolvedCommand { - command_name: CommandName::Unresolved(UnresolvedCommandName { - name: shebang_command_name, - base_dir: command_path.parent().unwrap().to_path_buf(), - }), - args: Cow::Owned(args), - }); - } + let command_path = match resolve_command_path( + &command_name.name, + &command_name.base_dir, + &context.state, + ) { + Ok(command_path) => command_path, + Err(err) => return Err(err.into()), + }; + + // only bother checking for a shebang when the path has a slash + // in it because for global commands someone on Windows likely + // won't have a script with a shebang in it on Windows + if command_name.name.contains('/') { + if let Some(shebang) = resolve_shebang(&command_path).map_err(|err| { + ResolveCommandError::FailedShebang(FailedShebangError::Any(err.into())) + })? { + let (shebang_command_name, mut args) = if shebang.string_split { + let mut args = parse_shebang_args(&shebang.command, context) + .await + .map_err(FailedShebangError::Any)?; + args.push(command_path.to_string_lossy().to_string()); + (args.remove(0), args) + } else { + ( + shebang.command, + vec![command_path.to_string_lossy().to_string()], + ) + }; + args.extend(original_args.iter().cloned()); + return Ok(ResolvedCommand { + command_name: CommandName::Unresolved(UnresolvedCommandName { + name: shebang_command_name, + base_dir: command_path.parent().unwrap().to_path_buf(), + }), + args: Cow::Owned(args), + }); } + } - return Ok(ResolvedCommand { - command_name: CommandName::Resolved(command_path), - args: Cow::Borrowed(original_args), - }); + return Ok(ResolvedCommand { + command_name: CommandName::Resolved(command_path), + args: Cow::Borrowed(original_args), + }); } async fn parse_shebang_args( - text: &str, - context: &ShellCommandContext, + text: &str, + context: &ShellCommandContext, ) -> Result> { - fn err_unsupported(text: &str) -> Result> { - anyhow::bail!("unsupported shebang. Please report this as a bug (https://github.com/denoland/deno).\n\nShebang: {}", text) - } - - let mut args = crate::parser::parse(text)?; - if args.items.len() != 1 { - return err_unsupported(text); - } - let item = args.items.remove(0); - if item.is_async { - return err_unsupported(text); - } - let pipeline = match item.sequence { - crate::parser::Sequence::Pipeline(pipeline) => pipeline, - _ => return err_unsupported(text), - }; - if pipeline.negated { - return err_unsupported(text); - } - let cmd = match pipeline.inner { - crate::parser::PipelineInner::Command(cmd) => cmd, - crate::parser::PipelineInner::PipeSequence(_) => { - return err_unsupported(text) - } - }; - if cmd.redirect.is_some() { - return err_unsupported(text); - } - let cmd = match cmd.inner { - crate::parser::CommandInner::Simple(cmd) => cmd, - crate::parser::CommandInner::Subshell(_) => { - return err_unsupported(text) - } - }; - if !cmd.env_vars.is_empty() { - return err_unsupported(text); + fn err_unsupported(text: &str) -> Result> { + anyhow::bail!("unsupported shebang. Please report this as a bug (https://github.com/denoland/deno).\n\nShebang: {}", text) + } + + let mut args = crate::parser::parse(text)?; + if args.items.len() != 1 { + return err_unsupported(text); + } + let item = args.items.remove(0); + if item.is_async { + return err_unsupported(text); + } + let pipeline = match item.sequence { + crate::parser::Sequence::Pipeline(pipeline) => pipeline, + _ => return err_unsupported(text), + }; + if pipeline.negated { + return err_unsupported(text); + } + let cmd = match pipeline.inner { + crate::parser::PipelineInner::Command(cmd) => cmd, + crate::parser::PipelineInner::PipeSequence(_) => { + return err_unsupported(text) } - - Ok(super::execute::evaluate_args( - cmd.args, - &context.state, - context.stdin.clone(), - context.stderr.clone(), + }; + if cmd.redirect.is_some() { + return err_unsupported(text); + } + let cmd = match cmd.inner { + crate::parser::CommandInner::Simple(cmd) => cmd, + crate::parser::CommandInner::Subshell(_) => return err_unsupported(text), + }; + if !cmd.env_vars.is_empty() { + return err_unsupported(text); + } + + Ok( + super::execute::evaluate_args( + cmd.args, + &context.state, + context.stdin.clone(), + context.stderr.clone(), ) - .await?) + .await?, + ) } /// Errors for executable commands. #[derive(Error, Debug, PartialEq)] pub enum ResolveCommandPathError { - #[error("{}: command not found", .0)] - CommandNotFound(String), - #[error("command name was empty")] - CommandEmpty, + #[error("{}: command not found", .0)] + CommandNotFound(String), + #[error("command name was empty")] + CommandEmpty, } impl ResolveCommandPathError { - pub fn exit_code(&self) -> i32 { - match self { - // Use the Exit status that is used in bash: https://www.gnu.org/software/bash/manual/bash.html#Exit-Status - ResolveCommandPathError::CommandNotFound(_) => 127, - ResolveCommandPathError::CommandEmpty => 1, - } + pub fn exit_code(&self) -> i32 { + match self { + // Use the Exit status that is used in bash: https://www.gnu.org/software/bash/manual/bash.html#Exit-Status + ResolveCommandPathError::CommandNotFound(_) => 127, + ResolveCommandPathError::CommandEmpty => 1, } + } } pub fn resolve_command_path( - command_name: &str, - base_dir: &Path, - state: &ShellState, + command_name: &str, + base_dir: &Path, + state: &ShellState, ) -> Result { - resolve_command_path_inner(command_name, base_dir, state, || { - Ok(std::env::current_exe()?) - }) + resolve_command_path_inner(command_name, base_dir, state, || { + Ok(std::env::current_exe()?) + }) } fn resolve_command_path_inner( - command_name: &str, - base_dir: &Path, - state: &ShellState, - current_exe: impl FnOnce() -> Result, + command_name: &str, + base_dir: &Path, + state: &ShellState, + current_exe: impl FnOnce() -> Result, ) -> Result { - if command_name.is_empty() { - return Err(ResolveCommandPathError::CommandEmpty); + if command_name.is_empty() { + return Err(ResolveCommandPathError::CommandEmpty); + } + + // Special handling to use the current executable for deno. + // This is to ensure deno tasks that use deno work in environments + // that don't have deno on the path and to ensure it use the current + // version of deno being executed rather than the one on the path, + // which has caused some confusion. + if command_name == "deno" { + if let Ok(exe_path) = current_exe() { + // this condition exists to make the tests pass because it's not + // using the deno as the current executable + let file_stem = exe_path.file_stem().map(|s| s.to_string_lossy()); + if file_stem.map(|s| s.to_string()) == Some("deno".to_string()) { + return Ok(exe_path); + } } - - // Special handling to use the current executable for deno. - // This is to ensure deno tasks that use deno work in environments - // that don't have deno on the path and to ensure it use the current - // version of deno being executed rather than the one on the path, - // which has caused some confusion. - if command_name == "deno" { - if let Ok(exe_path) = current_exe() { - // this condition exists to make the tests pass because it's not - // using the deno as the current executable - let file_stem = exe_path.file_stem().map(|s| s.to_string_lossy()); - if file_stem.map(|s| s.to_string()) == Some("deno".to_string()) { - return Ok(exe_path); - } - } + } + + // check for absolute + if PathBuf::from(command_name).is_absolute() { + return Ok(PathBuf::from(command_name)); + } + + // then relative + if command_name.contains('/') + || (cfg!(windows) && command_name.contains('\\')) + { + return Ok(base_dir.join(command_name)); + } + + // now search based on the current environment state + let mut search_dirs = vec![base_dir.to_path_buf()]; + if let Some(path) = state.get_var("PATH") { + for folder in path.split(if cfg!(windows) { ';' } else { ':' }) { + search_dirs.push(PathBuf::from(folder)); } - - // check for absolute - if PathBuf::from(command_name).is_absolute() { - return Ok(PathBuf::from(command_name)); - } - - // then relative - if command_name.contains('/') - || (cfg!(windows) && command_name.contains('\\')) + } + let path_exts = if cfg!(windows) { + let uc_command_name = command_name.to_uppercase(); + let path_ext = state + .get_var("PATHEXT") + .map(|s| s.as_str()) + .unwrap_or(".EXE;.CMD;.BAT;.COM"); + let command_exts = path_ext + .split(';') + .map(|s| s.trim().to_uppercase()) + .filter(|s| !s.is_empty()) + .collect::>(); + if command_exts.is_empty() + || command_exts + .iter() + .any(|ext| uc_command_name.ends_with(ext)) { - return Ok(base_dir.join(command_name)); - } - - // now search based on the current environment state - let mut search_dirs = vec![base_dir.to_path_buf()]; - if let Some(path) = state.get_var("PATH") { - for folder in path.split(if cfg!(windows) { ';' } else { ':' }) { - search_dirs.push(PathBuf::from(folder)); - } + None // use the command name as-is + } else { + Some(command_exts) } - let path_exts = if cfg!(windows) { - let uc_command_name = command_name.to_uppercase(); - let path_ext = state - .get_var("PATHEXT") - .map(|s| s.as_str()) - .unwrap_or(".EXE;.CMD;.BAT;.COM"); - let command_exts = path_ext - .split(';') - .map(|s| s.trim().to_uppercase()) - .filter(|s| !s.is_empty()) - .collect::>(); - if command_exts.is_empty() - || command_exts - .iter() - .any(|ext| uc_command_name.ends_with(ext)) - { - None // use the command name as-is - } else { - Some(command_exts) - } + } else { + None + }; + + for search_dir in search_dirs { + let paths = if let Some(path_exts) = &path_exts { + let mut paths = Vec::new(); + for path_ext in path_exts { + paths.push(search_dir.join(format!("{command_name}{path_ext}"))) + } + paths } else { - None + vec![search_dir.join(command_name)] }; - - for search_dir in search_dirs { - let paths = if let Some(path_exts) = &path_exts { - let mut paths = Vec::new(); - for path_ext in path_exts { - paths.push(search_dir.join(format!("{command_name}{path_ext}"))) - } - paths - } else { - vec![search_dir.join(command_name)] - }; - for path in paths { - // don't use tokio::fs::metadata here as it was never returning - // in some circumstances for some reason - if let Ok(metadata) = std::fs::metadata(&path) { - if metadata.is_file() { - return Ok(path); - } - } + for path in paths { + // don't use tokio::fs::metadata here as it was never returning + // in some circumstances for some reason + if let Ok(metadata) = std::fs::metadata(&path) { + if metadata.is_file() { + return Ok(path); } + } } - Err(ResolveCommandPathError::CommandNotFound( - command_name.to_string(), - )) + } + Err(ResolveCommandPathError::CommandNotFound( + command_name.to_string(), + )) } struct Shebang { - string_split: bool, - command: String, + string_split: bool, + command: String, } fn resolve_shebang( - file_path: &Path, + file_path: &Path, ) -> Result, std::io::Error> { - let mut file = match std::fs::File::open(file_path) { - Ok(file) => file, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Ok(None); - } - Err(err) => return Err(err), - }; - let text = b"#!/usr/bin/env "; - let mut buffer = vec![0; text.len()]; - match file.read_exact(&mut buffer) { - Ok(_) if buffer == text => (), - _ => return Ok(None), + let mut file = match std::fs::File::open(file_path) { + Ok(file) => file, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Ok(None); } - - let mut reader = BufReader::new(file); - let mut line = String::new(); - reader.read_line(&mut line)?; - if line.is_empty() { - return Ok(None); + Err(err) => return Err(err), + }; + let text = b"#!/usr/bin/env "; + let mut buffer = vec![0; text.len()]; + match file.read_exact(&mut buffer) { + Ok(_) if buffer == text => (), + _ => return Ok(None), + } + + let mut reader = BufReader::new(file); + let mut line = String::new(); + reader.read_line(&mut line)?; + if line.is_empty() { + return Ok(None); + } + let line = line.trim(); + + Ok(Some(if let Some(command) = line.strip_prefix("-S ") { + Shebang { + string_split: true, + command: command.to_string(), } - let line = line.trim(); - - Ok(Some(if let Some(command) = line.strip_prefix("-S ") { - Shebang { - string_split: true, - command: command.to_string(), - } - } else { - Shebang { - string_split: false, - command: line.to_string(), - } - })) + } else { + Shebang { + string_split: false, + command: line.to_string(), + } + })) } #[cfg(test)] mod local_test { - use super::*; - - #[test] - fn should_resolve_current_exe_path_for_deno() { - let cwd = std::env::current_dir().unwrap(); - let state = ShellState::new( - Default::default(), - &std::env::current_dir().unwrap(), - Default::default(), - ); - let path = resolve_command_path_inner("deno", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno")) - }) - .unwrap(); - assert_eq!(path, PathBuf::from("/bin/deno")); - - let path = resolve_command_path_inner("deno", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno.exe")) - }) - .unwrap(); - assert_eq!(path, PathBuf::from("/bin/deno.exe")); - } + use super::*; + + #[test] + fn should_resolve_current_exe_path_for_deno() { + let cwd = std::env::current_dir().unwrap(); + let state = ShellState::new( + Default::default(), + &std::env::current_dir().unwrap(), + Default::default(), + ); + let path = resolve_command_path_inner("deno", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno")) + }) + .unwrap(); + assert_eq!(path, PathBuf::from("/bin/deno")); - #[test] - fn should_error_on_unknown_command() { - let cwd = std::env::current_dir().unwrap(); - let state = - ShellState::new(Default::default(), &cwd, Default::default()); - // Command not found - let result = resolve_command_path_inner("foobar", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno")) - }); - assert_eq!( - result, - Err(ResolveCommandPathError::CommandNotFound( - "foobar".to_string() - )) - ); - // Command empty - let result = resolve_command_path_inner("", &cwd, &state, || { - Ok(PathBuf::from("/bin/deno")) - }); - assert_eq!(result, Err(ResolveCommandPathError::CommandEmpty)); - } + let path = resolve_command_path_inner("deno", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno.exe")) + }) + .unwrap(); + assert_eq!(path, PathBuf::from("/bin/deno.exe")); + } + + #[test] + fn should_error_on_unknown_command() { + let cwd = std::env::current_dir().unwrap(); + let state = ShellState::new(Default::default(), &cwd, Default::default()); + // Command not found + let result = resolve_command_path_inner("foobar", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno")) + }); + assert_eq!( + result, + Err(ResolveCommandPathError::CommandNotFound( + "foobar".to_string() + )) + ); + // Command empty + let result = resolve_command_path_inner("", &cwd, &state, || { + Ok(PathBuf::from("/bin/deno")) + }); + assert_eq!(result, Err(ResolveCommandPathError::CommandEmpty)); + } } diff --git a/crates/deno_task_shell/src/shell/commands/args.rs b/crates/deno_task_shell/src/shell/commands/args.rs index 2679150..d308500 100644 --- a/crates/deno_task_shell/src/shell/commands/args.rs +++ b/crates/deno_task_shell/src/shell/commands/args.rs @@ -5,88 +5,88 @@ use anyhow::Result; #[derive(Debug, PartialEq, Eq)] pub enum ArgKind<'a> { - ShortFlag(char), - LongFlag(&'a str), - Arg(&'a str), + ShortFlag(char), + LongFlag(&'a str), + Arg(&'a str), } impl<'a> ArgKind<'a> { - pub fn bail_unsupported(&self) -> Result<()> { - match self { - ArgKind::Arg(arg) => { - bail!("unsupported argument: {}", arg) - } - ArgKind::LongFlag(name) => { - bail!("unsupported flag: --{}", name) - } - ArgKind::ShortFlag(name) => { - bail!("unsupported flag: -{}", name) - } - } + pub fn bail_unsupported(&self) -> Result<()> { + match self { + ArgKind::Arg(arg) => { + bail!("unsupported argument: {}", arg) + } + ArgKind::LongFlag(name) => { + bail!("unsupported flag: --{}", name) + } + ArgKind::ShortFlag(name) => { + bail!("unsupported flag: -{}", name) + } } + } } pub fn parse_arg_kinds(flags: &[String]) -> Vec { - let mut result = Vec::new(); - let mut had_dash_dash = false; - for arg in flags { - if had_dash_dash { - result.push(ArgKind::Arg(arg)); - } else if arg == "-" { - result.push(ArgKind::Arg("-")); - } else if arg == "--" { - had_dash_dash = true; - } else if let Some(flag) = arg.strip_prefix("--") { - result.push(ArgKind::LongFlag(flag)); - } else if let Some(flags) = arg.strip_prefix('-') { - if flags.parse::().is_ok() { - result.push(ArgKind::Arg(arg)); - } else { - for c in flags.chars() { - result.push(ArgKind::ShortFlag(c)); - } - } - } else { - result.push(ArgKind::Arg(arg)); + let mut result = Vec::new(); + let mut had_dash_dash = false; + for arg in flags { + if had_dash_dash { + result.push(ArgKind::Arg(arg)); + } else if arg == "-" { + result.push(ArgKind::Arg("-")); + } else if arg == "--" { + had_dash_dash = true; + } else if let Some(flag) = arg.strip_prefix("--") { + result.push(ArgKind::LongFlag(flag)); + } else if let Some(flags) = arg.strip_prefix('-') { + if flags.parse::().is_ok() { + result.push(ArgKind::Arg(arg)); + } else { + for c in flags.chars() { + result.push(ArgKind::ShortFlag(c)); } + } + } else { + result.push(ArgKind::Arg(arg)); } - result + } + result } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses() { - let data = vec![ - "-f".to_string(), - "-ab".to_string(), - "--force".to_string(), - "testing".to_string(), - "other".to_string(), - "-1".to_string(), - "-6.4".to_string(), - "--".to_string(), - "--test".to_string(), - "-t".to_string(), - ]; - let args = parse_arg_kinds(&data); - assert_eq!( - args, - vec![ - ArgKind::ShortFlag('f'), - ArgKind::ShortFlag('a'), - ArgKind::ShortFlag('b'), - ArgKind::LongFlag("force"), - ArgKind::Arg("testing"), - ArgKind::Arg("other"), - ArgKind::Arg("-1"), - ArgKind::Arg("-6.4"), - ArgKind::Arg("--test"), - ArgKind::Arg("-t"), - ] - ) - } + #[test] + fn parses() { + let data = vec![ + "-f".to_string(), + "-ab".to_string(), + "--force".to_string(), + "testing".to_string(), + "other".to_string(), + "-1".to_string(), + "-6.4".to_string(), + "--".to_string(), + "--test".to_string(), + "-t".to_string(), + ]; + let args = parse_arg_kinds(&data); + assert_eq!( + args, + vec![ + ArgKind::ShortFlag('f'), + ArgKind::ShortFlag('a'), + ArgKind::ShortFlag('b'), + ArgKind::LongFlag("force"), + ArgKind::Arg("testing"), + ArgKind::Arg("other"), + ArgKind::Arg("-1"), + ArgKind::Arg("-6.4"), + ArgKind::Arg("--test"), + ArgKind::Arg("-t"), + ] + ) + } } diff --git a/crates/deno_task_shell/src/shell/commands/cat.rs b/crates/deno_task_shell/src/shell/commands/cat.rs index b233c49..674fe33 100644 --- a/crates/deno_task_shell/src/shell/commands/cat.rs +++ b/crates/deno_task_shell/src/shell/commands/cat.rs @@ -15,130 +15,127 @@ use super::ShellCommandContext; pub struct CatCommand; impl ShellCommand for CatCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let mut stderr = context.stderr.clone(); - let result = match execute_cat(context) { - Ok(result) => result, - Err(err) => { - let _ = stderr.write_line(&format!("cat: {err}")); - ExecuteResult::from_exit_code(1) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let mut stderr = context.stderr.clone(); + let result = match execute_cat(context) { + Ok(result) => result, + Err(err) => { + let _ = stderr.write_line(&format!("cat: {err}")); + ExecuteResult::from_exit_code(1) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_cat(mut context: ShellCommandContext) -> Result { - let flags = parse_args(context.args)?; - let mut exit_code = 0; - let mut buf = vec![0; 1024]; - for path in flags.paths { - if path == "-" { - context - .stdin - .clone() - .pipe_to_sender(context.stdout.clone())?; - } else { - // buffered to prevent reading an entire file - // in memory - match File::open(context.state.cwd().join(&path)) { - Ok(mut file) => loop { - if context.state.token().is_cancelled() { - return Ok(ExecuteResult::for_cancellation()); - } + let flags = parse_args(context.args)?; + let mut exit_code = 0; + let mut buf = vec![0; 1024]; + for path in flags.paths { + if path == "-" { + context + .stdin + .clone() + .pipe_to_sender(context.stdout.clone())?; + } else { + // buffered to prevent reading an entire file + // in memory + match File::open(context.state.cwd().join(&path)) { + Ok(mut file) => loop { + if context.state.token().is_cancelled() { + return Ok(ExecuteResult::for_cancellation()); + } - let size = file.read(&mut buf)?; - if size == 0 { - break; - } else { - context.stdout.write_all(&buf[..size])?; - } - }, - Err(err) => { - context - .stderr - .write_line(&format!("cat: {path}: {err}"))?; - exit_code = 1; - } - } + let size = file.read(&mut buf)?; + if size == 0 { + break; + } else { + context.stdout.write_all(&buf[..size])?; + } + }, + Err(err) => { + context.stderr.write_line(&format!("cat: {path}: {err}"))?; + exit_code = 1; } + } } + } - Ok(ExecuteResult::from_exit_code(exit_code)) + Ok(ExecuteResult::from_exit_code(exit_code)) } #[derive(Debug, PartialEq)] struct CatFlags { - paths: Vec, + paths: Vec, } fn parse_args(args: Vec) -> Result { - let mut paths = Vec::new(); - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(file_name) => { - paths.push(file_name.to_string()); - } - // for now, we don't support any arguments - _ => arg.bail_unsupported()?, - } + let mut paths = Vec::new(); + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(file_name) => { + paths.push(file_name.to_string()); + } + // for now, we don't support any arguments + _ => arg.bail_unsupported()?, } + } - if paths.is_empty() { - paths.push("-".to_string()); - } + if paths.is_empty() { + paths.push("-".to_string()); + } - Ok(CatFlags { paths }) + Ok(CatFlags { paths }) } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![]).unwrap(), - CatFlags { - paths: vec!["-".to_string()] - } - ); - assert_eq!( - parse_args(vec!["path".to_string()]).unwrap(), - CatFlags { - paths: vec!["path".to_string()] - } - ); - assert_eq!( - parse_args(vec!["path".to_string(), "-".to_string()]).unwrap(), - CatFlags { - paths: vec!["path".to_string(), "-".to_string()] - } - ); - assert_eq!( - parse_args(vec!["path".to_string(), "other-path".to_string()]) - .unwrap(), - CatFlags { - paths: vec!["path".to_string(), "other-path".to_string()] - } - ); - assert_eq!( - parse_args(vec!["--flag".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --flag" - ); - assert_eq!( - parse_args(vec!["-t".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t" - ); - } + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![]).unwrap(), + CatFlags { + paths: vec!["-".to_string()] + } + ); + assert_eq!( + parse_args(vec!["path".to_string()]).unwrap(), + CatFlags { + paths: vec!["path".to_string()] + } + ); + assert_eq!( + parse_args(vec!["path".to_string(), "-".to_string()]).unwrap(), + CatFlags { + paths: vec!["path".to_string(), "-".to_string()] + } + ); + assert_eq!( + parse_args(vec!["path".to_string(), "other-path".to_string()]).unwrap(), + CatFlags { + paths: vec!["path".to_string(), "other-path".to_string()] + } + ); + assert_eq!( + parse_args(vec!["--flag".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --flag" + ); + assert_eq!( + parse_args(vec!["-t".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/cd.rs b/crates/deno_task_shell/src/shell/commands/cd.rs index 39a14ac..54ae007 100644 --- a/crates/deno_task_shell/src/shell/commands/cd.rs +++ b/crates/deno_task_shell/src/shell/commands/cd.rs @@ -20,129 +20,127 @@ use super::ShellCommandContext; pub struct CdCommand; impl ShellCommand for CdCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match execute_cd(context.state.cwd(), context.args) { - Ok(new_dir) => ExecuteResult::Continue( - 0, - vec![EnvChange::Cd(new_dir)], - Vec::new(), - ), - Err(err) => { - let _ = context.stderr.write_line(&format!("cd: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_cd(context.state.cwd(), context.args) { + Ok(new_dir) => { + ExecuteResult::Continue(0, vec![EnvChange::Cd(new_dir)], Vec::new()) + } + Err(err) => { + let _ = context.stderr.write_line(&format!("cd: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_cd(cwd: &Path, args: Vec) -> Result { - let path = parse_args(args)?; - let new_dir = cwd.join(&path); - let new_dir = match new_dir.parse_dot() { - Ok(path) => path.to_path_buf(), - // fallback to canonicalize path just in case - Err(_) => fs_util::canonicalize_path(&new_dir)?, - }; - if !new_dir.is_dir() { - bail!("{}: Not a directory", path) - } - Ok(new_dir) + let path = parse_args(args)?; + let new_dir = cwd.join(&path); + let new_dir = match new_dir.parse_dot() { + Ok(path) => path.to_path_buf(), + // fallback to canonicalize path just in case + Err(_) => fs_util::canonicalize_path(&new_dir)?, + }; + if !new_dir.is_dir() { + bail!("{}: Not a directory", path) + } + Ok(new_dir) } fn parse_args(args: Vec) -> Result { - let args = parse_arg_kinds(&args); - let mut paths = Vec::new(); - for arg in args { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - _ => arg.bail_unsupported()?, - } + let args = parse_arg_kinds(&args); + let mut paths = Vec::new(); + for arg in args { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, } + } - if paths.len() > 1 { - bail!("too many arguments") - } else if paths.is_empty() { - // not the case in actual cd, but it is most likely - // an error if someone does this in deno task - bail!("expected at least 1 argument") - } + if paths.len() > 1 { + bail!("too many arguments") + } else if paths.is_empty() { + // not the case in actual cd, but it is most likely + // an error if someone does this in deno task + bail!("expected at least 1 argument") + } - Ok(paths.remove(0).to_string()) + Ok(paths.remove(0).to_string()) } #[cfg(test)] mod test { - use std::fs; - use tempfile::tempdir; - - use super::*; - - #[test] - fn parses_args() { - assert_eq!(parse_args(vec!["test".to_string()]).unwrap(), "test"); - assert_eq!( - parse_args(vec!["a".to_string(), "b".to_string()]) - .err() - .unwrap() - .to_string(), - "too many arguments" - ); - assert_eq!( - parse_args(vec![]).err().unwrap().to_string(), - "expected at least 1 argument" - ); - assert_eq!( - parse_args(vec!["-a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -a" - ); - assert_eq!( - parse_args(vec!["--a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --a" - ); - } - - #[test] - fn gets_new_cd() { - let dir = tempdir().unwrap(); - let dir_path = fs_util::canonicalize_path(dir.path()).unwrap(); - - // non-existent - assert_eq!( - execute_cd(&dir_path, vec!["non-existent".to_string()]) - .err() - .unwrap() - .to_string(), - "non-existent: Not a directory" - ); - - // existent file - fs::write(dir_path.join("file.txt"), "").unwrap(); - assert_eq!( - execute_cd(&dir_path, vec!["file.txt".to_string()]) - .err() - .unwrap() - .to_string(), - "file.txt: Not a directory" - ); - - // existent dir - let sub_dir_path = dir_path.join("sub_dir"); - fs::create_dir(&sub_dir_path).unwrap(); - assert_eq!( - execute_cd(&dir_path, vec!["sub_dir".to_string()]).unwrap(), - sub_dir_path - ); - } + use std::fs; + use tempfile::tempdir; + + use super::*; + + #[test] + fn parses_args() { + assert_eq!(parse_args(vec!["test".to_string()]).unwrap(), "test"); + assert_eq!( + parse_args(vec!["a".to_string(), "b".to_string()]) + .err() + .unwrap() + .to_string(), + "too many arguments" + ); + assert_eq!( + parse_args(vec![]).err().unwrap().to_string(), + "expected at least 1 argument" + ); + assert_eq!( + parse_args(vec!["-a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -a" + ); + assert_eq!( + parse_args(vec!["--a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --a" + ); + } + + #[test] + fn gets_new_cd() { + let dir = tempdir().unwrap(); + let dir_path = fs_util::canonicalize_path(dir.path()).unwrap(); + + // non-existent + assert_eq!( + execute_cd(&dir_path, vec!["non-existent".to_string()]) + .err() + .unwrap() + .to_string(), + "non-existent: Not a directory" + ); + + // existent file + fs::write(dir_path.join("file.txt"), "").unwrap(); + assert_eq!( + execute_cd(&dir_path, vec!["file.txt".to_string()]) + .err() + .unwrap() + .to_string(), + "file.txt: Not a directory" + ); + + // existent dir + let sub_dir_path = dir_path.join("sub_dir"); + fs::create_dir(&sub_dir_path).unwrap(); + assert_eq!( + execute_cd(&dir_path, vec!["sub_dir".to_string()]).unwrap(), + sub_dir_path + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/cp_mv.rs b/crates/deno_task_shell/src/shell/commands/cp_mv.rs index 34d8456..5647cfb 100644 --- a/crates/deno_task_shell/src/shell/commands/cp_mv.rs +++ b/crates/deno_task_shell/src/shell/commands/cp_mv.rs @@ -22,479 +22,466 @@ use super::ShellCommandContext; pub struct CpCommand; impl ShellCommand for CpCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - cp_command(context.state.cwd(), context.args, context.stderr), - context.state.token() - ) - } - .boxed_local() + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + cp_command(context.state.cwd(), context.args, context.stderr), + context.state.token() + ) } + .boxed_local() + } } async fn cp_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_cp(cwd, args).await { - Ok(()) => ExecuteResult::from_exit_code(0), - Err(err) => { - let _ = stderr.write_line(&format!("cp: {err}")); - ExecuteResult::from_exit_code(1) - } + match execute_cp(cwd, args).await { + Ok(()) => ExecuteResult::from_exit_code(0), + Err(err) => { + let _ = stderr.write_line(&format!("cp: {err}")); + ExecuteResult::from_exit_code(1) } + } } async fn execute_cp(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_cp_args(cwd, args)?; - for (from, to) in &flags.operations { - if let Err(err) = do_copy_operation(&flags, from, to).await { - bail!( - "could not copy {} to {}: {}", - from.specified, - to.specified, - err - ); - } + let flags = parse_cp_args(cwd, args)?; + for (from, to) in &flags.operations { + if let Err(err) = do_copy_operation(&flags, from, to).await { + bail!( + "could not copy {} to {}: {}", + from.specified, + to.specified, + err + ); } - Ok(()) + } + Ok(()) } async fn do_copy_operation( - flags: &CpFlags, - from: &PathWithSpecified, - to: &PathWithSpecified, + flags: &CpFlags, + from: &PathWithSpecified, + to: &PathWithSpecified, ) -> Result<()> { - // These are racy with the file system, but that's ok. - // They only exists to give better error messages. - if from.path.is_dir() { - if flags.recursive { - if to.path.exists() && to.path.is_file() { - bail!("destination was a file"); - } else if to.path.is_symlink() { - bail!("no support for copying to symlinks") - } else if from.path.is_symlink() { - bail!("no support for copying from symlinks") - } else { - copy_dir_recursively(from.path.clone(), to.path.clone()) - .await?; - } - } else { - bail!("source was a directory; maybe specify -r") - } + // These are racy with the file system, but that's ok. + // They only exists to give better error messages. + if from.path.is_dir() { + if flags.recursive { + if to.path.exists() && to.path.is_file() { + bail!("destination was a file"); + } else if to.path.is_symlink() { + bail!("no support for copying to symlinks") + } else if from.path.is_symlink() { + bail!("no support for copying from symlinks") + } else { + copy_dir_recursively(from.path.clone(), to.path.clone()).await?; + } } else { - tokio::fs::copy(&from.path, &to.path).await?; + bail!("source was a directory; maybe specify -r") } - Ok(()) + } else { + tokio::fs::copy(&from.path, &to.path).await?; + } + Ok(()) } fn copy_dir_recursively( - from: PathBuf, - to: PathBuf, + from: PathBuf, + to: PathBuf, ) -> BoxFuture<'static, Result<()>> { - // recursive, so box it - async move { - tokio::fs::create_dir_all(&to) - .await - .with_context(|| format!("Creating {}", to.display()))?; - let mut read_dir = tokio::fs::read_dir(&from) - .await - .with_context(|| format!("Reading {}", from.display()))?; - - while let Some(entry) = read_dir.next_entry().await? { - let file_type = entry.file_type().await?; - let new_from = from.join(entry.file_name()); - let new_to = to.join(entry.file_name()); - - if file_type.is_dir() { - copy_dir_recursively(new_from.clone(), new_to.clone()) - .await - .with_context(|| { - format!( - "Dir {} to {}", - new_from.display(), - new_to.display() - ) - })?; - } else if file_type.is_file() { - tokio::fs::copy(&new_from, &new_to).await.with_context( - || { - format!( - "Copying {} to {}", - new_from.display(), - new_to.display() - ) - }, - )?; - } - } - - Ok(()) + // recursive, so box it + async move { + tokio::fs::create_dir_all(&to) + .await + .with_context(|| format!("Creating {}", to.display()))?; + let mut read_dir = tokio::fs::read_dir(&from) + .await + .with_context(|| format!("Reading {}", from.display()))?; + + while let Some(entry) = read_dir.next_entry().await? { + let file_type = entry.file_type().await?; + let new_from = from.join(entry.file_name()); + let new_to = to.join(entry.file_name()); + + if file_type.is_dir() { + copy_dir_recursively(new_from.clone(), new_to.clone()) + .await + .with_context(|| { + format!("Dir {} to {}", new_from.display(), new_to.display()) + })?; + } else if file_type.is_file() { + tokio::fs::copy(&new_from, &new_to).await.with_context(|| { + format!("Copying {} to {}", new_from.display(), new_to.display()) + })?; + } } - .boxed() + + Ok(()) + } + .boxed() } struct CpFlags { - recursive: bool, - operations: Vec<(PathWithSpecified, PathWithSpecified)>, + recursive: bool, + operations: Vec<(PathWithSpecified, PathWithSpecified)>, } fn parse_cp_args(cwd: &Path, args: Vec) -> Result { - let mut paths = Vec::new(); - let mut recursive = false; - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - ArgKind::LongFlag("recursive") - | ArgKind::ShortFlag('r') - | ArgKind::ShortFlag('R') => { - recursive = true; - } - _ => arg.bail_unsupported()?, - } - } - if paths.is_empty() { - bail!("missing file operand"); - } else if paths.len() == 1 { - bail!("missing destination file operand after '{}'", paths[0]); + let mut paths = Vec::new(); + let mut recursive = false; + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + ArgKind::LongFlag("recursive") + | ArgKind::ShortFlag('r') + | ArgKind::ShortFlag('R') => { + recursive = true; + } + _ => arg.bail_unsupported()?, } - - Ok(CpFlags { - recursive, - operations: get_copy_and_move_operations(cwd, paths)?, - }) + } + if paths.is_empty() { + bail!("missing file operand"); + } else if paths.len() == 1 { + bail!("missing destination file operand after '{}'", paths[0]); + } + + Ok(CpFlags { + recursive, + operations: get_copy_and_move_operations(cwd, paths)?, + }) } pub struct MvCommand; impl ShellCommand for MvCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - mv_command(context.state.cwd(), context.args, context.stderr), - context.state.token() - ) - } - .boxed_local() + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + mv_command(context.state.cwd(), context.args, context.stderr), + context.state.token() + ) } + .boxed_local() + } } async fn mv_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_mv(cwd, args).await { - Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), - Err(err) => { - let _ = stderr.write_line(&format!("mv: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } + match execute_mv(cwd, args).await { + Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), + Err(err) => { + let _ = stderr.write_line(&format!("mv: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) } + } } async fn execute_mv(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_mv_args(cwd, args)?; - for (from, to) in flags.operations { - if let Err(err) = tokio::fs::rename(&from.path, &to.path).await { - bail!( - "could not move {} to {}: {}", - from.specified, - to.specified, - err - ); - } + let flags = parse_mv_args(cwd, args)?; + for (from, to) in flags.operations { + if let Err(err) = tokio::fs::rename(&from.path, &to.path).await { + bail!( + "could not move {} to {}: {}", + from.specified, + to.specified, + err + ); } - Ok(()) + } + Ok(()) } struct MvFlags { - operations: Vec<(PathWithSpecified, PathWithSpecified)>, + operations: Vec<(PathWithSpecified, PathWithSpecified)>, } fn parse_mv_args(cwd: &Path, args: Vec) -> Result { - let mut paths = Vec::new(); - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - _ => arg.bail_unsupported()?, - } + let mut paths = Vec::new(); + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, } - if paths.is_empty() { - bail!("missing file operand"); - } else if paths.len() == 1 { - bail!("missing destination file operand after '{}'", paths[0]); - } - - Ok(MvFlags { - operations: get_copy_and_move_operations(cwd, paths)?, - }) + } + if paths.is_empty() { + bail!("missing file operand"); + } else if paths.len() == 1 { + bail!("missing destination file operand after '{}'", paths[0]); + } + + Ok(MvFlags { + operations: get_copy_and_move_operations(cwd, paths)?, + }) } struct PathWithSpecified { - path: PathBuf, - specified: String, + path: PathBuf, + specified: String, } fn get_copy_and_move_operations( - cwd: &Path, - mut paths: Vec<&str>, + cwd: &Path, + mut paths: Vec<&str>, ) -> Result> { - // copy and move share the same logic - let specified_destination = paths.pop().unwrap(); - let destination = cwd.join(specified_destination); - let from_args = paths; - let mut operations = Vec::new(); - if from_args.len() > 1 { - if !destination.is_dir() { - bail!("target '{}' is not a directory", specified_destination); - } - for from in from_args { - let from_path = cwd.join(from); - let to_path = destination.join(from_path.file_name().unwrap()); - operations.push(( - PathWithSpecified { - specified: from.to_string(), - path: from_path, - }, - PathWithSpecified { - specified: specified_destination.to_string(), - path: to_path, - }, - )); - } - } else { - let from_path = cwd.join(from_args[0]); - let to_path = if destination.is_dir() { - destination.join(from_path.file_name().unwrap()) - } else { - destination - }; - operations.push(( - PathWithSpecified { - specified: from_args[0].to_string(), - path: from_path, - }, - PathWithSpecified { - specified: specified_destination.to_string(), - path: to_path, - }, - )); + // copy and move share the same logic + let specified_destination = paths.pop().unwrap(); + let destination = cwd.join(specified_destination); + let from_args = paths; + let mut operations = Vec::new(); + if from_args.len() > 1 { + if !destination.is_dir() { + bail!("target '{}' is not a directory", specified_destination); } - Ok(operations) + for from in from_args { + let from_path = cwd.join(from); + let to_path = destination.join(from_path.file_name().unwrap()); + operations.push(( + PathWithSpecified { + specified: from.to_string(), + path: from_path, + }, + PathWithSpecified { + specified: specified_destination.to_string(), + path: to_path, + }, + )); + } + } else { + let from_path = cwd.join(from_args[0]); + let to_path = if destination.is_dir() { + destination.join(from_path.file_name().unwrap()) + } else { + destination + }; + operations.push(( + PathWithSpecified { + specified: from_args[0].to_string(), + path: from_path, + }, + PathWithSpecified { + specified: specified_destination.to_string(), + path: to_path, + }, + )); + } + Ok(operations) } #[cfg(test)] mod test { - use tempfile::tempdir; - - use super::*; - use std::fs; - - #[tokio::test] - async fn should_copy() { - let dir = tempdir().unwrap(); - let file1 = dir.path().join("file1.txt"); - let file2 = dir.path().join("file2.txt"); - fs::write(&file1, "test").unwrap(); - execute_cp( - dir.path(), - vec!["file1.txt".to_string(), "file2.txt".to_string()], - ) - .await - .unwrap(); - assert!(file1.exists()); - assert!(file2.exists()); - - let dest_dir = dir.path().join("dest"); - fs::create_dir(&dest_dir).unwrap(); - execute_cp( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "dest".to_string(), - ], - ) - .await - .unwrap(); - assert!(file1.exists()); - assert!(file2.exists()); - assert!(dest_dir.join("file1.txt").exists()); - assert!(dest_dir.join("file2.txt").exists()); - - let new_file = dir.path().join("new.txt"); - fs::write(&new_file, "test").unwrap(); - execute_cp(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) - .await - .unwrap(); - assert!(dest_dir.is_dir()); - assert!(new_file.exists()); - assert!(dest_dir.join("new.txt").exists()); - - let result = execute_cp( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "non-existent".to_string(), - ], - ) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "target 'non-existent' is not a directory" - ); - - let result = execute_cp(dir.path(), vec![]).await.err().unwrap(); - assert_eq!(result.to_string(), "missing file operand"); - - let result = execute_cp(dir.path(), vec!["file1.txt".to_string()]) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "missing destination file operand after 'file1.txt'" - ); - - // test recursive flag - fs::create_dir_all(dest_dir.join("sub_dir")).unwrap(); - fs::write(dest_dir.join("sub_dir").join("sub.txt"), "test").unwrap(); - let dest_dir2 = dir.path().join("dest2"); - - let result = execute_cp( - dir.path(), - vec!["dest".to_string(), "dest2".to_string()], - ) - .await - .err() - .unwrap(); - assert_eq!( + use tempfile::tempdir; + + use super::*; + use std::fs; + + #[tokio::test] + async fn should_copy() { + let dir = tempdir().unwrap(); + let file1 = dir.path().join("file1.txt"); + let file2 = dir.path().join("file2.txt"); + fs::write(&file1, "test").unwrap(); + execute_cp( + dir.path(), + vec!["file1.txt".to_string(), "file2.txt".to_string()], + ) + .await + .unwrap(); + assert!(file1.exists()); + assert!(file2.exists()); + + let dest_dir = dir.path().join("dest"); + fs::create_dir(&dest_dir).unwrap(); + execute_cp( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "dest".to_string(), + ], + ) + .await + .unwrap(); + assert!(file1.exists()); + assert!(file2.exists()); + assert!(dest_dir.join("file1.txt").exists()); + assert!(dest_dir.join("file2.txt").exists()); + + let new_file = dir.path().join("new.txt"); + fs::write(&new_file, "test").unwrap(); + execute_cp(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) + .await + .unwrap(); + assert!(dest_dir.is_dir()); + assert!(new_file.exists()); + assert!(dest_dir.join("new.txt").exists()); + + let result = execute_cp( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "non-existent".to_string(), + ], + ) + .await + .err() + .unwrap(); + assert_eq!( result.to_string(), - "could not copy dest to dest2: source was a directory; maybe specify -r" + "target 'non-existent' is not a directory" ); - assert!(!dest_dir2.exists()); - execute_cp( - dir.path(), - vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], - ) - .await - .unwrap(); - assert!(dest_dir2.exists()); - assert!(dest_dir2.join("file1.txt").exists()); - assert!(dest_dir2.join("file2.txt").exists()); - assert!(dest_dir2.join("sub_dir").join("sub.txt").exists()); - - // copy again - execute_cp( - dir.path(), - vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], - ) - .await - .unwrap(); + let result = execute_cp(dir.path(), vec![]).await.err().unwrap(); + assert_eq!(result.to_string(), "missing file operand"); - // try copying to a file - let result = execute_cp( - dir.path(), - vec![ - "-r".to_string(), - "dest".to_string(), - "dest2/file1.txt".to_string(), - ], - ) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "could not copy dest to dest2/file1.txt: destination was a file" - ) - } + let result = execute_cp(dir.path(), vec!["file1.txt".to_string()]) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "missing destination file operand after 'file1.txt'" + ); - #[tokio::test] - async fn should_move() { - let dir = tempdir().unwrap(); - let file1 = dir.path().join("file1.txt"); - let file2 = dir.path().join("file2.txt"); - fs::write(&file1, "test").unwrap(); - execute_mv( - dir.path(), - vec!["file1.txt".to_string(), "file2.txt".to_string()], - ) - .await - .unwrap(); - assert!(!file1.exists()); - assert!(file2.exists()); - - let dest_dir = dir.path().join("dest"); - fs::write(&file1, "test").unwrap(); // recreate - fs::create_dir(&dest_dir).unwrap(); - execute_mv( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "dest".to_string(), - ], - ) - .await - .unwrap(); - assert!(!file1.exists()); - assert!(!file2.exists()); - assert!(dest_dir.join("file1.txt").exists()); - assert!(dest_dir.join("file2.txt").exists()); - - let new_file = dir.path().join("new.txt"); - fs::write(&new_file, "test").unwrap(); - execute_mv(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) - .await - .unwrap(); - assert!(dest_dir.is_dir()); - assert!(!new_file.exists()); - assert!(dest_dir.join("new.txt").exists()); - - let result = execute_mv( - dir.path(), - vec![ - "file1.txt".to_string(), - "file2.txt".to_string(), - "non-existent".to_string(), - ], - ) + // test recursive flag + fs::create_dir_all(dest_dir.join("sub_dir")).unwrap(); + fs::write(dest_dir.join("sub_dir").join("sub.txt"), "test").unwrap(); + let dest_dir2 = dir.path().join("dest2"); + + let result = + execute_cp(dir.path(), vec!["dest".to_string(), "dest2".to_string()]) .await .err() .unwrap(); - assert_eq!( - result.to_string(), - "target 'non-existent' is not a directory" - ); - - let result = execute_mv(dir.path(), vec![]).await.err().unwrap(); - assert_eq!(result.to_string(), "missing file operand"); - - let result = execute_mv(dir.path(), vec!["file1.txt".to_string()]) - .await - .err() - .unwrap(); - assert_eq!( - result.to_string(), - "missing destination file operand after 'file1.txt'" - ); - } + assert_eq!( + result.to_string(), + "could not copy dest to dest2: source was a directory; maybe specify -r" + ); + assert!(!dest_dir2.exists()); + + execute_cp( + dir.path(), + vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], + ) + .await + .unwrap(); + assert!(dest_dir2.exists()); + assert!(dest_dir2.join("file1.txt").exists()); + assert!(dest_dir2.join("file2.txt").exists()); + assert!(dest_dir2.join("sub_dir").join("sub.txt").exists()); + + // copy again + execute_cp( + dir.path(), + vec!["-r".to_string(), "dest".to_string(), "dest2".to_string()], + ) + .await + .unwrap(); + + // try copying to a file + let result = execute_cp( + dir.path(), + vec![ + "-r".to_string(), + "dest".to_string(), + "dest2/file1.txt".to_string(), + ], + ) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "could not copy dest to dest2/file1.txt: destination was a file" + ) + } + + #[tokio::test] + async fn should_move() { + let dir = tempdir().unwrap(); + let file1 = dir.path().join("file1.txt"); + let file2 = dir.path().join("file2.txt"); + fs::write(&file1, "test").unwrap(); + execute_mv( + dir.path(), + vec!["file1.txt".to_string(), "file2.txt".to_string()], + ) + .await + .unwrap(); + assert!(!file1.exists()); + assert!(file2.exists()); + + let dest_dir = dir.path().join("dest"); + fs::write(&file1, "test").unwrap(); // recreate + fs::create_dir(&dest_dir).unwrap(); + execute_mv( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "dest".to_string(), + ], + ) + .await + .unwrap(); + assert!(!file1.exists()); + assert!(!file2.exists()); + assert!(dest_dir.join("file1.txt").exists()); + assert!(dest_dir.join("file2.txt").exists()); + + let new_file = dir.path().join("new.txt"); + fs::write(&new_file, "test").unwrap(); + execute_mv(dir.path(), vec!["new.txt".to_string(), "dest".to_string()]) + .await + .unwrap(); + assert!(dest_dir.is_dir()); + assert!(!new_file.exists()); + assert!(dest_dir.join("new.txt").exists()); + + let result = execute_mv( + dir.path(), + vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "non-existent".to_string(), + ], + ) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "target 'non-existent' is not a directory" + ); + + let result = execute_mv(dir.path(), vec![]).await.err().unwrap(); + assert_eq!(result.to_string(), "missing file operand"); + + let result = execute_mv(dir.path(), vec!["file1.txt".to_string()]) + .await + .err() + .unwrap(); + assert_eq!( + result.to_string(), + "missing destination file operand after 'file1.txt'" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/echo.rs b/crates/deno_task_shell/src/shell/commands/echo.rs index 8625516..ec2f6c5 100644 --- a/crates/deno_task_shell/src/shell/commands/echo.rs +++ b/crates/deno_task_shell/src/shell/commands/echo.rs @@ -10,11 +10,11 @@ use super::ShellCommandContext; pub struct EchoCommand; impl ShellCommand for EchoCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let _ = context.stdout.write_line(&context.args.join(" ")); - Box::pin(futures::future::ready(ExecuteResult::from_exit_code(0))) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let _ = context.stdout.write_line(&context.args.join(" ")); + Box::pin(futures::future::ready(ExecuteResult::from_exit_code(0))) + } } diff --git a/crates/deno_task_shell/src/shell/commands/executable.rs b/crates/deno_task_shell/src/shell/commands/executable.rs index 30b6c61..7cf2b09 100644 --- a/crates/deno_task_shell/src/shell/commands/executable.rs +++ b/crates/deno_task_shell/src/shell/commands/executable.rs @@ -11,68 +11,68 @@ use futures::FutureExt; /// Command that resolves the command name and /// executes it in a separate process. pub struct ExecutableCommand { - display_name: String, - command_path: PathBuf, + display_name: String, + command_path: PathBuf, } impl ExecutableCommand { - pub fn new(display_name: String, command_path: PathBuf) -> Self { - Self { - display_name, - command_path, - } + pub fn new(display_name: String, command_path: PathBuf) -> Self { + Self { + display_name, + command_path, } + } } impl ShellCommand for ExecutableCommand { - fn execute(&self, context: ShellCommandContext) -> FutureExecuteResult { - let display_name = self.display_name.clone(); - let command_name = self.command_path.clone(); - async move { - let mut stderr = context.stderr; - let mut sub_command = tokio::process::Command::new(&command_name); - let child = sub_command - .current_dir(context.state.cwd()) - .args(context.args) - .env_clear() - .envs(context.state.env_vars()) - .stdout(context.stdout.into_stdio()) - .stdin(context.stdin.into_stdio()) - .stderr(stderr.clone().into_stdio()) - .spawn(); + fn execute(&self, context: ShellCommandContext) -> FutureExecuteResult { + let display_name = self.display_name.clone(); + let command_name = self.command_path.clone(); + async move { + let mut stderr = context.stderr; + let mut sub_command = tokio::process::Command::new(&command_name); + let child = sub_command + .current_dir(context.state.cwd()) + .args(context.args) + .env_clear() + .envs(context.state.env_vars()) + .stdout(context.stdout.into_stdio()) + .stdin(context.stdin.into_stdio()) + .stderr(stderr.clone().into_stdio()) + .spawn(); - let mut child = match child { - Ok(child) => child, - Err(err) => { - let _ = stderr.write_line(&format!( - "Error launching '{}': {}", - display_name, err - )); - return ExecuteResult::Continue(1, Vec::new(), Vec::new()); - } - }; + let mut child = match child { + Ok(child) => child, + Err(err) => { + let _ = stderr.write_line(&format!( + "Error launching '{}': {}", + display_name, err + )); + return ExecuteResult::Continue(1, Vec::new(), Vec::new()); + } + }; - // avoid deadlock since this is holding onto the pipes - drop(sub_command); + // avoid deadlock since this is holding onto the pipes + drop(sub_command); - tokio::select! { - result = child.wait() => match result { - Ok(status) => ExecuteResult::Continue( - status.code().unwrap_or(1), - Vec::new(), - Vec::new(), - ), - Err(err) => { - let _ = stderr.write_line(&format!("{}", err)); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } - }, - _ = context.state.token().cancelled() => { - let _ = child.kill().await; - ExecuteResult::for_cancellation() - } - } + tokio::select! { + result = child.wait() => match result { + Ok(status) => ExecuteResult::Continue( + status.code().unwrap_or(1), + Vec::new(), + Vec::new(), + ), + Err(err) => { + let _ = stderr.write_line(&format!("{}", err)); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }, + _ = context.state.token().cancelled() => { + let _ = child.kill().await; + ExecuteResult::for_cancellation() } - .boxed_local() + } } + .boxed_local() + } } diff --git a/crates/deno_task_shell/src/shell/commands/exit.rs b/crates/deno_task_shell/src/shell/commands/exit.rs index b606104..5222c61 100644 --- a/crates/deno_task_shell/src/shell/commands/exit.rs +++ b/crates/deno_task_shell/src/shell/commands/exit.rs @@ -14,104 +14,104 @@ use super::ShellCommandContext; pub struct ExitCommand; impl ShellCommand for ExitCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match execute_exit(context.args) { - Ok(code) => ExecuteResult::Exit(code, Vec::new()), - Err(err) => { - context.stderr.write_line(&format!("exit: {err}")).unwrap(); - ExecuteResult::Exit(2, Vec::new()) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_exit(context.args) { + Ok(code) => ExecuteResult::Exit(code, Vec::new()), + Err(err) => { + context.stderr.write_line(&format!("exit: {err}")).unwrap(); + ExecuteResult::Exit(2, Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_exit(args: Vec) -> Result { - let exit_code = parse_args(args)?; + let exit_code = parse_args(args)?; - Ok(if exit_code < 0 { - let code = -exit_code % 256; - 256 - code - } else { - exit_code % 256 - }) + Ok(if exit_code < 0 { + let code = -exit_code % 256; + 256 - code + } else { + exit_code % 256 + }) } fn parse_args(args: Vec) -> Result { - let args = parse_arg_kinds(&args); - let mut paths = Vec::new(); - for arg in args { - match arg { - ArgKind::Arg(arg) => { - paths.push(arg); - } - _ => arg.bail_unsupported()?, - } + let args = parse_arg_kinds(&args); + let mut paths = Vec::new(); + for arg in args { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, } + } - match paths.len() { - 0 => Ok(1), - 1 => { - let arg = paths.remove(0).to_string(); - match arg.parse::() { - Ok(value) => Ok(value), - Err(_) => bail!("numeric argument required."), - } - } - _ => { - bail!("too many arguments") - } + match paths.len() { + 0 => Ok(1), + 1 => { + let arg = paths.remove(0).to_string(); + match arg.parse::() { + Ok(value) => Ok(value), + Err(_) => bail!("numeric argument required."), + } + } + _ => { + bail!("too many arguments") } + } } #[cfg(test)] mod test { - use super::*; + use super::*; - #[test] - fn parses_args() { - assert_eq!(parse_args(vec![]).unwrap(), 1); - assert_eq!(parse_args(vec!["5".to_string()]).unwrap(), 5); - assert_eq!( - parse_args(vec!["test".to_string()]) - .err() - .unwrap() - .to_string(), - "numeric argument required." - ); - assert_eq!( - parse_args(vec!["1".to_string(), "2".to_string()]) - .err() - .unwrap() - .to_string(), - "too many arguments" - ); - assert_eq!( - parse_args(vec!["-a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -a" - ); - assert_eq!( - parse_args(vec!["--a".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --a" - ); - } + #[test] + fn parses_args() { + assert_eq!(parse_args(vec![]).unwrap(), 1); + assert_eq!(parse_args(vec!["5".to_string()]).unwrap(), 5); + assert_eq!( + parse_args(vec!["test".to_string()]) + .err() + .unwrap() + .to_string(), + "numeric argument required." + ); + assert_eq!( + parse_args(vec!["1".to_string(), "2".to_string()]) + .err() + .unwrap() + .to_string(), + "too many arguments" + ); + assert_eq!( + parse_args(vec!["-a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -a" + ); + assert_eq!( + parse_args(vec!["--a".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --a" + ); + } - #[test] - fn executes_exit() { - assert_eq!(execute_exit(vec![]).unwrap(), 1); - assert_eq!(execute_exit(vec!["0".to_string()]).unwrap(), 0); - assert_eq!(execute_exit(vec!["255".to_string()]).unwrap(), 255); - assert_eq!(execute_exit(vec!["256".to_string()]).unwrap(), 0); - assert_eq!(execute_exit(vec!["257".to_string()]).unwrap(), 1); - assert_eq!(execute_exit(vec!["-1".to_string()]).unwrap(), 255); - } + #[test] + fn executes_exit() { + assert_eq!(execute_exit(vec![]).unwrap(), 1); + assert_eq!(execute_exit(vec!["0".to_string()]).unwrap(), 0); + assert_eq!(execute_exit(vec!["255".to_string()]).unwrap(), 255); + assert_eq!(execute_exit(vec!["256".to_string()]).unwrap(), 0); + assert_eq!(execute_exit(vec!["257".to_string()]).unwrap(), 1); + assert_eq!(execute_exit(vec!["-1".to_string()]).unwrap(), 255); + } } diff --git a/crates/deno_task_shell/src/shell/commands/export.rs b/crates/deno_task_shell/src/shell/commands/export.rs index 83ed6d7..bf433c3 100644 --- a/crates/deno_task_shell/src/shell/commands/export.rs +++ b/crates/deno_task_shell/src/shell/commands/export.rs @@ -11,23 +11,23 @@ use super::ShellCommandContext; pub struct ExportCommand; impl ShellCommand for ExportCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let mut changes = Vec::new(); - for arg in context.args { - // ignore if it doesn't contain an equals - if let Some(equals_index) = arg.find('=') { - let arg_name = &arg[..equals_index]; - let arg_value = &arg[equals_index + 1..]; - changes.push(EnvChange::SetEnvVar( - arg_name.to_string(), - arg_value.to_string(), - )); - } - } - let result = ExecuteResult::Continue(0, changes, Vec::new()); - Box::pin(futures::future::ready(result)) + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let mut changes = Vec::new(); + for arg in context.args { + // ignore if it doesn't contain an equals + if let Some(equals_index) = arg.find('=') { + let arg_name = &arg[..equals_index]; + let arg_value = &arg[equals_index + 1..]; + changes.push(EnvChange::SetEnvVar( + arg_name.to_string(), + arg_value.to_string(), + )); + } } + let result = ExecuteResult::Continue(0, changes, Vec::new()); + Box::pin(futures::future::ready(result)) + } } diff --git a/crates/deno_task_shell/src/shell/commands/head.rs b/crates/deno_task_shell/src/shell/commands/head.rs index d3a0459..bef3a46 100644 --- a/crates/deno_task_shell/src/shell/commands/head.rs +++ b/crates/deno_task_shell/src/shell/commands/head.rs @@ -19,305 +19,295 @@ use super::args::ArgKind; pub struct HeadCommand; impl ShellCommand for HeadCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let mut stderr = context.stderr.clone(); - let result = match execute_head(context) { - Ok(result) => result, - Err(err) => { - let _ = stderr.write_line(&format!("head: {err}")); - ExecuteResult::from_exit_code(1) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let mut stderr = context.stderr.clone(); + let result = match execute_head(context) { + Ok(result) => result, + Err(err) => { + let _ = stderr.write_line(&format!("head: {err}")); + ExecuteResult::from_exit_code(1) + } + }; + Box::pin(futures::future::ready(result)) + } } fn copy_lines Result>( - writer: &mut ShellPipeWriter, - max_lines: u64, - cancellation_token: &CancellationToken, - mut read: F, - buffer_size: usize, + writer: &mut ShellPipeWriter, + max_lines: u64, + cancellation_token: &CancellationToken, + mut read: F, + buffer_size: usize, ) -> Result { - let mut written_lines = 0; - let mut buffer = vec![0; buffer_size]; - while written_lines < max_lines { - if cancellation_token.is_cancelled() { - return Ok(ExecuteResult::for_cancellation()); - } - let read_bytes = read(&mut buffer)?; - if read_bytes == 0 { - break; - } + let mut written_lines = 0; + let mut buffer = vec![0; buffer_size]; + while written_lines < max_lines { + if cancellation_token.is_cancelled() { + return Ok(ExecuteResult::for_cancellation()); + } + let read_bytes = read(&mut buffer)?; + if read_bytes == 0 { + break; + } - if cancellation_token.is_cancelled() { - return Ok(ExecuteResult::for_cancellation()); - } + if cancellation_token.is_cancelled() { + return Ok(ExecuteResult::for_cancellation()); + } - let mut written_bytes: usize = 0; - let split_lines = buffer[..read_bytes].split(|&b| b == b'\n'); - for line in split_lines { - if written_lines >= max_lines - || (written_bytes + line.len()) >= read_bytes - { - break; - } - writer.write_all(line)?; - writer.write_all(b"\n")?; - written_bytes += line.len() + 1; - written_lines += 1; - } + let mut written_bytes: usize = 0; + let split_lines = buffer[..read_bytes].split(|&b| b == b'\n'); + for line in split_lines { + if written_lines >= max_lines + || (written_bytes + line.len()) >= read_bytes + { + break; + } + writer.write_all(line)?; + writer.write_all(b"\n")?; + written_bytes += line.len() + 1; + written_lines += 1; + } - if written_lines < max_lines && written_bytes < read_bytes { - writer.write_all(&buffer[written_bytes..read_bytes])?; - } + if written_lines < max_lines && written_bytes < read_bytes { + writer.write_all(&buffer[written_bytes..read_bytes])?; } + } - Ok(ExecuteResult::from_exit_code(0)) + Ok(ExecuteResult::from_exit_code(0)) } fn execute_head(mut context: ShellCommandContext) -> Result { - let flags = parse_args(context.args)?; - if flags.path == "-" { - copy_lines( - &mut context.stdout, - flags.lines, - context.state.token(), - |buf| context.stdin.read(buf), - 512, - ) - } else { - let path = flags.path; - match File::open(context.state.cwd().join(&path)) { - Ok(mut file) => copy_lines( - &mut context.stdout, - flags.lines, - context.state.token(), - |buf| file.read(buf).map_err(Into::into), - 512, - ), - Err(err) => { - context.stderr.write_line(&format!("head: {path}: {err}"))?; - Ok(ExecuteResult::from_exit_code(1)) - } - } + let flags = parse_args(context.args)?; + if flags.path == "-" { + copy_lines( + &mut context.stdout, + flags.lines, + context.state.token(), + |buf| context.stdin.read(buf), + 512, + ) + } else { + let path = flags.path; + match File::open(context.state.cwd().join(&path)) { + Ok(mut file) => copy_lines( + &mut context.stdout, + flags.lines, + context.state.token(), + |buf| file.read(buf).map_err(Into::into), + 512, + ), + Err(err) => { + context.stderr.write_line(&format!("head: {path}: {err}"))?; + Ok(ExecuteResult::from_exit_code(1)) + } } + } } #[derive(Debug, PartialEq)] struct HeadFlags { - path: String, - lines: u64, + path: String, + lines: u64, } fn parse_args(args: Vec) -> Result { - let mut path: Option = None; - let mut lines: Option = None; - let mut iterator = parse_arg_kinds(&args).into_iter(); - while let Some(arg) = iterator.next() { - match arg { - ArgKind::Arg(file_name) => { - if path.is_none() { - path = Some(file_name.to_string()); - continue; - } + let mut path: Option = None; + let mut lines: Option = None; + let mut iterator = parse_arg_kinds(&args).into_iter(); + while let Some(arg) = iterator.next() { + match arg { + ArgKind::Arg(file_name) => { + if path.is_none() { + path = Some(file_name.to_string()); + continue; + } - // for now, we only support one file - // TODO: support multiple files - bail!("only one file is supported for now"); - } - ArgKind::ShortFlag('n') => match iterator.next() { - Some(ArgKind::Arg(arg)) => { - lines = Some(arg.parse::()?); - } - _ => bail!("expected a value following -n"), - }, - ArgKind::LongFlag(flag) => { - if flag == "lines" || flag == "lines=" { - bail!("expected a value for --lines"); - } else if let Some(arg) = flag.strip_prefix("lines=") { - lines = Some(arg.parse::()?); - } else { - arg.bail_unsupported()? - } - } - _ => arg.bail_unsupported()?, + // for now, we only support one file + // TODO: support multiple files + bail!("only one file is supported for now"); + } + ArgKind::ShortFlag('n') => match iterator.next() { + Some(ArgKind::Arg(arg)) => { + lines = Some(arg.parse::()?); + } + _ => bail!("expected a value following -n"), + }, + ArgKind::LongFlag(flag) => { + if flag == "lines" || flag == "lines=" { + bail!("expected a value for --lines"); + } else if let Some(arg) = flag.strip_prefix("lines=") { + lines = Some(arg.parse::()?); + } else { + arg.bail_unsupported()? } + } + _ => arg.bail_unsupported()?, } + } - Ok(HeadFlags { - path: path.unwrap_or("-".to_string()), - lines: lines.unwrap_or(10), - }) + Ok(HeadFlags { + path: path.unwrap_or("-".to_string()), + lines: lines.unwrap_or(10), + }) } #[cfg(test)] mod test { - use crate::pipe; - use std::cmp::min; + use crate::pipe; + use std::cmp::min; - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - async fn copies_lines( - // #[case] - buffer_size: usize, - ) { - let (reader, mut writer) = pipe(); - let reader_handle = reader.pipe_to_string_handle(); - let data = b"foo\nbar\nbaz\nqux\n"; - let data_length = data.len(); - let mut offset = 0; - let result = copy_lines( - &mut writer, - 2, - &CancellationToken::new(), - |buffer| { - if offset >= data.len() { - return Ok(0); - } - let buffer_length = buffer.len(); - let read_length = min(buffer_length, data_length); - buffer[..read_length] - .copy_from_slice(&data[offset..(offset + read_length)]); - offset += read_length; - Ok(read_length) - }, - buffer_size, - ); - drop(writer); // Drop the writer ahead of the reader to prevent a deadlock. - assert_eq!(reader_handle.await.unwrap(), "foo\nbar\n"); - assert_eq!(result.unwrap().into_exit_code_and_handles().0, 0); - } + async fn copies_lines( + // #[case] + buffer_size: usize, + ) { + let (reader, mut writer) = pipe(); + let reader_handle = reader.pipe_to_string_handle(); + let data = b"foo\nbar\nbaz\nqux\n"; + let data_length = data.len(); + let mut offset = 0; + let result = copy_lines( + &mut writer, + 2, + &CancellationToken::new(), + |buffer| { + if offset >= data.len() { + return Ok(0); + } + let buffer_length = buffer.len(); + let read_length = min(buffer_length, data_length); + buffer[..read_length] + .copy_from_slice(&data[offset..(offset + read_length)]); + offset += read_length; + Ok(read_length) + }, + buffer_size, + ); + drop(writer); // Drop the writer ahead of the reader to prevent a deadlock. + assert_eq!(reader_handle.await.unwrap(), "foo\nbar\n"); + assert_eq!(result.unwrap().into_exit_code_and_handles().0, 0); + } - #[tokio::test] - async fn copies_lines_with_shorter_buffer_size() { - copies_lines(2).await; - } + #[tokio::test] + async fn copies_lines_with_shorter_buffer_size() { + copies_lines(2).await; + } - #[tokio::test] - async fn copies_lines_with_buffer_size_to_match_each_line_length() { - copies_lines(4).await; - } + #[tokio::test] + async fn copies_lines_with_buffer_size_to_match_each_line_length() { + copies_lines(4).await; + } - #[tokio::test] - async fn copies_lines_with_buffer_of_one_and_half_times_of_each_line_length( - ) { - copies_lines(6).await; - } + #[tokio::test] + async fn copies_lines_with_buffer_of_one_and_half_times_of_each_line_length() + { + copies_lines(6).await; + } - #[tokio::test] - async fn copies_lines_with_long_buffer_size() { - copies_lines(512).await; - } + #[tokio::test] + async fn copies_lines_with_long_buffer_size() { + copies_lines(512).await; + } - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![]).unwrap(), - HeadFlags { - path: "-".to_string(), - lines: 10 - } - ); - assert_eq!( - parse_args(vec!["-n".to_string(), "5".to_string()]).unwrap(), - HeadFlags { - path: "-".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["--lines=5".to_string()]).unwrap(), - HeadFlags { - path: "-".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["path".to_string()]).unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 10 - } - ); - assert_eq!( - parse_args(vec![ - "-n".to_string(), - "5".to_string(), - "path".to_string() - ]) - .unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["--lines=5".to_string(), "path".to_string()]) - .unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec![ - "path".to_string(), - "-n".to_string(), - "5".to_string() - ]) - .unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["path".to_string(), "--lines=5".to_string()]) - .unwrap(), - HeadFlags { - path: "path".to_string(), - lines: 5 - } - ); - assert_eq!( - parse_args(vec!["-n".to_string()]) - .err() - .unwrap() - .to_string(), - "expected a value following -n" - ); - assert_eq!( - parse_args(vec!["--lines".to_string()]) - .err() - .unwrap() - .to_string(), - "expected a value for --lines" - ); - assert_eq!( - parse_args(vec!["--lines=".to_string()]) - .err() - .unwrap() - .to_string(), - "expected a value for --lines" - ); - assert_eq!( - parse_args(vec!["--flag".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --flag" - ); - assert_eq!( - parse_args(vec!["-t".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t" - ); - } + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![]).unwrap(), + HeadFlags { + path: "-".to_string(), + lines: 10 + } + ); + assert_eq!( + parse_args(vec!["-n".to_string(), "5".to_string()]).unwrap(), + HeadFlags { + path: "-".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["--lines=5".to_string()]).unwrap(), + HeadFlags { + path: "-".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["path".to_string()]).unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 10 + } + ); + assert_eq!( + parse_args(vec!["-n".to_string(), "5".to_string(), "path".to_string()]) + .unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["--lines=5".to_string(), "path".to_string()]).unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["path".to_string(), "-n".to_string(), "5".to_string()]) + .unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["path".to_string(), "--lines=5".to_string()]).unwrap(), + HeadFlags { + path: "path".to_string(), + lines: 5 + } + ); + assert_eq!( + parse_args(vec!["-n".to_string()]) + .err() + .unwrap() + .to_string(), + "expected a value following -n" + ); + assert_eq!( + parse_args(vec!["--lines".to_string()]) + .err() + .unwrap() + .to_string(), + "expected a value for --lines" + ); + assert_eq!( + parse_args(vec!["--lines=".to_string()]) + .err() + .unwrap() + .to_string(), + "expected a value for --lines" + ); + assert_eq!( + parse_args(vec!["--flag".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --flag" + ); + assert_eq!( + parse_args(vec!["-t".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/mkdir.rs b/crates/deno_task_shell/src/shell/commands/mkdir.rs index 9245b3a..25636ca 100644 --- a/crates/deno_task_shell/src/shell/commands/mkdir.rs +++ b/crates/deno_task_shell/src/shell/commands/mkdir.rs @@ -18,169 +18,159 @@ use super::ShellCommandContext; pub struct MkdirCommand; impl ShellCommand for MkdirCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - mkdir_command( - context.state.cwd(), - context.args, - context.stderr - ), - context.state.token() - ) - } - .boxed_local() + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + mkdir_command(context.state.cwd(), context.args, context.stderr), + context.state.token() + ) } + .boxed_local() + } } async fn mkdir_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_mkdir(cwd, args).await { - Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), - Err(err) => { - let _ = stderr.write_line(&format!("mkdir: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } + match execute_mkdir(cwd, args).await { + Ok(()) => ExecuteResult::Continue(0, Vec::new(), Vec::new()), + Err(err) => { + let _ = stderr.write_line(&format!("mkdir: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) } + } } async fn execute_mkdir(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_args(args)?; - for specified_path in &flags.paths { - let path = cwd.join(specified_path); - if path.is_file() || !flags.parents && path.is_dir() { - bail!("cannot create directory '{}': File exists", specified_path); - } - if flags.parents { - if let Err(err) = tokio::fs::create_dir_all(&path).await { - bail!("cannot create directory '{}': {}", specified_path, err); - } - } else if let Err(err) = tokio::fs::create_dir(&path).await { - bail!("cannot create directory '{}': {}", specified_path, err); - } + let flags = parse_args(args)?; + for specified_path in &flags.paths { + let path = cwd.join(specified_path); + if path.is_file() || !flags.parents && path.is_dir() { + bail!("cannot create directory '{}': File exists", specified_path); + } + if flags.parents { + if let Err(err) = tokio::fs::create_dir_all(&path).await { + bail!("cannot create directory '{}': {}", specified_path, err); + } + } else if let Err(err) = tokio::fs::create_dir(&path).await { + bail!("cannot create directory '{}': {}", specified_path, err); } - Ok(()) + } + Ok(()) } #[derive(Default, Debug, PartialEq)] struct MkdirFlags { - parents: bool, - paths: Vec, + parents: bool, + paths: Vec, } fn parse_args(args: Vec) -> Result { - let mut result = MkdirFlags::default(); - - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::LongFlag("parents") | ArgKind::ShortFlag('p') => { - result.parents = true; - } - ArgKind::Arg(path) => { - result.paths.push(path.to_string()); - } - ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => { - arg.bail_unsupported()? - } - } + let mut result = MkdirFlags::default(); + + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::LongFlag("parents") | ArgKind::ShortFlag('p') => { + result.parents = true; + } + ArgKind::Arg(path) => { + result.paths.push(path.to_string()); + } + ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => arg.bail_unsupported()?, } + } - if result.paths.is_empty() { - bail!("missing operand"); - } + if result.paths.is_empty() { + bail!("missing operand"); + } - Ok(result) + Ok(result) } #[cfg(test)] mod test { - use tempfile::tempdir; - - use super::*; - use std::fs; - - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![ - "--parents".to_string(), - "a".to_string(), - "b".to_string(), - ]) - .unwrap(), - MkdirFlags { - parents: true, - paths: vec!["a".to_string(), "b".to_string()], - } - ); - assert_eq!( - parse_args(vec![ - "-p".to_string(), - "a".to_string(), - "b".to_string(), - ]) - .unwrap(), - MkdirFlags { - parents: true, - paths: vec!["a".to_string(), "b".to_string()], - } - ); - assert_eq!( - parse_args(vec!["--parents".to_string()]) - .err() - .unwrap() - .to_string(), - "missing operand", - ); - assert_eq!( - parse_args(vec![ - "--parents".to_string(), - "-p".to_string(), - "-u".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -u", - ); - assert_eq!( - parse_args(vec![ - "--parents".to_string(), - "--random-flag".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --random-flag", - ); - } - - #[tokio::test] - async fn test_creates() { - let dir = tempdir().unwrap(); - let file_path = dir.path().join("file.txt"); - let sub_dir_path = dir.path().join("folder"); - fs::write(&file_path, "").unwrap(); - fs::create_dir(sub_dir_path).unwrap(); - - assert_eq!( - execute_mkdir(dir.path(), vec!["file.txt".to_string()],) - .await - .err() - .unwrap() - .to_string(), - "cannot create directory 'file.txt': File exists" - ); + use tempfile::tempdir; + + use super::*; + use std::fs; + + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![ + "--parents".to_string(), + "a".to_string(), + "b".to_string(), + ]) + .unwrap(), + MkdirFlags { + parents: true, + paths: vec!["a".to_string(), "b".to_string()], + } + ); + assert_eq!( + parse_args(vec!["-p".to_string(), "a".to_string(), "b".to_string(),]) + .unwrap(), + MkdirFlags { + parents: true, + paths: vec!["a".to_string(), "b".to_string()], + } + ); + assert_eq!( + parse_args(vec!["--parents".to_string()]) + .err() + .unwrap() + .to_string(), + "missing operand", + ); + assert_eq!( + parse_args(vec![ + "--parents".to_string(), + "-p".to_string(), + "-u".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -u", + ); + assert_eq!( + parse_args(vec![ + "--parents".to_string(), + "--random-flag".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --random-flag", + ); + } + + #[tokio::test] + async fn test_creates() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("file.txt"); + let sub_dir_path = dir.path().join("folder"); + fs::write(&file_path, "").unwrap(); + fs::create_dir(sub_dir_path).unwrap(); + + assert_eq!( + execute_mkdir(dir.path(), vec!["file.txt".to_string()],) + .await + .err() + .unwrap() + .to_string(), + "cannot create directory 'file.txt': File exists" + ); - assert_eq!( + assert_eq!( execute_mkdir( dir.path(), vec!["-p".to_string(), "file.txt".to_string()], @@ -192,52 +182,49 @@ mod test { "cannot create directory 'file.txt': File exists" ); - assert_eq!( - execute_mkdir(dir.path(), vec!["folder".to_string()],) - .await - .err() - .unwrap() - .to_string(), - "cannot create directory 'folder': File exists" - ); - - // should work because of -p - execute_mkdir(dir.path(), vec!["-p".to_string(), "folder".to_string()]) - .await - .unwrap(); - - execute_mkdir(dir.path(), vec!["other".to_string()]) - .await - .unwrap(); - assert!(dir.path().join("other").exists()); - - // sub folder - assert_eq!( - execute_mkdir(dir.path(), vec!["sub/folder".to_string()],) - .await - .err() - .unwrap() - .to_string(), - format!( - "cannot create directory 'sub/folder': {}", - no_such_file_error_text() - ) - ); - - execute_mkdir( - dir.path(), - vec!["-p".to_string(), "sub/folder".to_string()], - ) + assert_eq!( + execute_mkdir(dir.path(), vec!["folder".to_string()],) .await - .unwrap(); - assert!(dir.path().join("sub").join("folder").exists()); - } - - fn no_such_file_error_text() -> &'static str { - if cfg!(windows) { - "The system cannot find the path specified. (os error 3)" - } else { - "No such file or directory (os error 2)" - } + .err() + .unwrap() + .to_string(), + "cannot create directory 'folder': File exists" + ); + + // should work because of -p + execute_mkdir(dir.path(), vec!["-p".to_string(), "folder".to_string()]) + .await + .unwrap(); + + execute_mkdir(dir.path(), vec!["other".to_string()]) + .await + .unwrap(); + assert!(dir.path().join("other").exists()); + + // sub folder + assert_eq!( + execute_mkdir(dir.path(), vec!["sub/folder".to_string()],) + .await + .err() + .unwrap() + .to_string(), + format!( + "cannot create directory 'sub/folder': {}", + no_such_file_error_text() + ) + ); + + execute_mkdir(dir.path(), vec!["-p".to_string(), "sub/folder".to_string()]) + .await + .unwrap(); + assert!(dir.path().join("sub").join("folder").exists()); + } + + fn no_such_file_error_text() -> &'static str { + if cfg!(windows) { + "The system cannot find the path specified. (os error 3)" + } else { + "No such file or directory (os error 2)" } + } } diff --git a/crates/deno_task_shell/src/shell/commands/mod.rs b/crates/deno_task_shell/src/shell/commands/mod.rs index ef77a00..028392c 100644 --- a/crates/deno_task_shell/src/shell/commands/mod.rs +++ b/crates/deno_task_shell/src/shell/commands/mod.rs @@ -30,110 +30,110 @@ use super::types::ShellPipeWriter; use super::types::ShellState; pub fn builtin_commands() -> HashMap> { - HashMap::from([ - ( - "cat".to_string(), - Rc::new(cat::CatCommand) as Rc, - ), - ( - "cd".to_string(), - Rc::new(cd::CdCommand) as Rc, - ), - ( - "cp".to_string(), - Rc::new(cp_mv::CpCommand) as Rc, - ), - ( - "echo".to_string(), - Rc::new(echo::EchoCommand) as Rc, - ), - ( - "exit".to_string(), - Rc::new(exit::ExitCommand) as Rc, - ), - ( - "export".to_string(), - Rc::new(export::ExportCommand) as Rc, - ), - ( - "head".to_string(), - Rc::new(head::HeadCommand) as Rc, - ), - ( - "mkdir".to_string(), - Rc::new(mkdir::MkdirCommand) as Rc, - ), - ( - "mv".to_string(), - Rc::new(cp_mv::MvCommand) as Rc, - ), - ( - "pwd".to_string(), - Rc::new(pwd::PwdCommand) as Rc, - ), - ( - "rm".to_string(), - Rc::new(rm::RmCommand) as Rc, - ), - ( - "sleep".to_string(), - Rc::new(sleep::SleepCommand) as Rc, - ), - ( - "true".to_string(), - Rc::new(ExitCodeCommand(0)) as Rc, - ), - ( - "false".to_string(), - Rc::new(ExitCodeCommand(1)) as Rc, - ), - ( - "unset".to_string(), - Rc::new(unset::UnsetCommand) as Rc, - ), - ( - "xargs".to_string(), - Rc::new(xargs::XargsCommand) as Rc, - ), - ]) + HashMap::from([ + ( + "cat".to_string(), + Rc::new(cat::CatCommand) as Rc, + ), + ( + "cd".to_string(), + Rc::new(cd::CdCommand) as Rc, + ), + ( + "cp".to_string(), + Rc::new(cp_mv::CpCommand) as Rc, + ), + ( + "echo".to_string(), + Rc::new(echo::EchoCommand) as Rc, + ), + ( + "exit".to_string(), + Rc::new(exit::ExitCommand) as Rc, + ), + ( + "export".to_string(), + Rc::new(export::ExportCommand) as Rc, + ), + ( + "head".to_string(), + Rc::new(head::HeadCommand) as Rc, + ), + ( + "mkdir".to_string(), + Rc::new(mkdir::MkdirCommand) as Rc, + ), + ( + "mv".to_string(), + Rc::new(cp_mv::MvCommand) as Rc, + ), + ( + "pwd".to_string(), + Rc::new(pwd::PwdCommand) as Rc, + ), + ( + "rm".to_string(), + Rc::new(rm::RmCommand) as Rc, + ), + ( + "sleep".to_string(), + Rc::new(sleep::SleepCommand) as Rc, + ), + ( + "true".to_string(), + Rc::new(ExitCodeCommand(0)) as Rc, + ), + ( + "false".to_string(), + Rc::new(ExitCodeCommand(1)) as Rc, + ), + ( + "unset".to_string(), + Rc::new(unset::UnsetCommand) as Rc, + ), + ( + "xargs".to_string(), + Rc::new(xargs::XargsCommand) as Rc, + ), + ]) } pub struct ExecuteCommandArgsContext { - pub args: Vec, - pub state: ShellState, - pub stdin: ShellPipeReader, - pub stdout: ShellPipeWriter, - pub stderr: ShellPipeWriter, + pub args: Vec, + pub state: ShellState, + pub stdin: ShellPipeReader, + pub stdout: ShellPipeWriter, + pub stderr: ShellPipeWriter, } pub struct ShellCommandContext { - pub args: Vec, - pub state: ShellState, - pub stdin: ShellPipeReader, - pub stdout: ShellPipeWriter, - pub stderr: ShellPipeWriter, - pub execute_command_args: - Box FutureExecuteResult>, + pub args: Vec, + pub state: ShellState, + pub stdin: ShellPipeReader, + pub stdout: ShellPipeWriter, + pub stderr: ShellPipeWriter, + pub execute_command_args: + Box FutureExecuteResult>, } pub trait ShellCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult>; + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult>; } macro_rules! execute_with_cancellation { - ($result_expr:expr, $token:expr) => { - tokio::select! { - result = $result_expr => { - result - }, - _ = $token.cancelled() => { - ExecuteResult::for_cancellation() - } - } - }; + ($result_expr:expr, $token:expr) => { + tokio::select! { + result = $result_expr => { + result + }, + _ = $token.cancelled() => { + ExecuteResult::for_cancellation() + } + } + }; } pub(super) use execute_with_cancellation; @@ -141,13 +141,13 @@ pub(super) use execute_with_cancellation; struct ExitCodeCommand(i32); impl ShellCommand for ExitCodeCommand { - fn execute( - &self, - _context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - // ignores additional arguments - Box::pin(futures::future::ready(ExecuteResult::from_exit_code( - self.0, - ))) - } + fn execute( + &self, + _context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + // ignores additional arguments + Box::pin(futures::future::ready(ExecuteResult::from_exit_code( + self.0, + ))) + } } diff --git a/crates/deno_task_shell/src/shell/commands/pwd.rs b/crates/deno_task_shell/src/shell/commands/pwd.rs index 0b7bebf..9da211c 100644 --- a/crates/deno_task_shell/src/shell/commands/pwd.rs +++ b/crates/deno_task_shell/src/shell/commands/pwd.rs @@ -16,91 +16,90 @@ use super::ShellCommandContext; pub struct PwdCommand; impl ShellCommand for PwdCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match execute_pwd(context.state.cwd(), context.args) { - Ok(output) => { - let _ = context.stdout.write_line(&output); - ExecuteResult::from_exit_code(0) - } - Err(err) => { - let _ = context.stderr.write_line(&format!("pwd: {err}")); - ExecuteResult::from_exit_code(1) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_pwd(context.state.cwd(), context.args) { + Ok(output) => { + let _ = context.stdout.write_line(&output); + ExecuteResult::from_exit_code(0) + } + Err(err) => { + let _ = context.stderr.write_line(&format!("pwd: {err}")); + ExecuteResult::from_exit_code(1) + } + }; + Box::pin(futures::future::ready(result)) + } } fn execute_pwd(cwd: &Path, args: Vec) -> Result { - let flags = parse_args(args)?; - let cwd = if flags.logical { - fs_util::canonicalize_path(cwd).with_context(|| { - format!("error canonicalizing: {}", cwd.display()) - })? - } else { - cwd.to_path_buf() - }; - Ok(cwd.display().to_string()) + let flags = parse_args(args)?; + let cwd = if flags.logical { + fs_util::canonicalize_path(cwd) + .with_context(|| format!("error canonicalizing: {}", cwd.display()))? + } else { + cwd.to_path_buf() + }; + Ok(cwd.display().to_string()) } #[derive(Debug, PartialEq)] struct PwdFlags { - logical: bool, + logical: bool, } fn parse_args(args: Vec) -> Result { - let mut logical = false; - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::ShortFlag('L') => { - logical = true; - } - ArgKind::ShortFlag('P') => { - // ignore, this is the default - } - ArgKind::Arg(_) => { - // args are ignored by pwd - } - _ => arg.bail_unsupported()?, - } + let mut logical = false; + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::ShortFlag('L') => { + logical = true; + } + ArgKind::ShortFlag('P') => { + // ignore, this is the default + } + ArgKind::Arg(_) => { + // args are ignored by pwd + } + _ => arg.bail_unsupported()?, } + } - Ok(PwdFlags { logical }) + Ok(PwdFlags { logical }) } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses_args() { - assert_eq!(parse_args(vec![]).unwrap(), PwdFlags { logical: false }); - assert_eq!( - parse_args(vec!["-P".to_string()]).unwrap(), - PwdFlags { logical: false } - ); - assert_eq!( - parse_args(vec!["-L".to_string()]).unwrap(), - PwdFlags { logical: true } - ); - assert!(parse_args(vec!["test".to_string()]).is_ok()); - assert_eq!( - parse_args(vec!["--flag".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --flag" - ); - assert_eq!( - parse_args(vec!["-t".to_string()]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t" - ); - } + #[test] + fn parses_args() { + assert_eq!(parse_args(vec![]).unwrap(), PwdFlags { logical: false }); + assert_eq!( + parse_args(vec!["-P".to_string()]).unwrap(), + PwdFlags { logical: false } + ); + assert_eq!( + parse_args(vec!["-L".to_string()]).unwrap(), + PwdFlags { logical: true } + ); + assert!(parse_args(vec!["test".to_string()]).is_ok()); + assert_eq!( + parse_args(vec!["--flag".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --flag" + ); + assert_eq!( + parse_args(vec!["-t".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t" + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/rm.rs b/crates/deno_task_shell/src/shell/commands/rm.rs index db60411..72c1410 100644 --- a/crates/deno_task_shell/src/shell/commands/rm.rs +++ b/crates/deno_task_shell/src/shell/commands/rm.rs @@ -19,333 +19,318 @@ use super::ShellCommandContext; pub struct RmCommand; impl ShellCommand for RmCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - rm_command(context.state.cwd(), context.args, context.stderr), - context.state.token() - ) - } - .boxed_local() + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + rm_command(context.state.cwd(), context.args, context.stderr), + context.state.token() + ) } + .boxed_local() + } } async fn rm_command( - cwd: &Path, - args: Vec, - mut stderr: ShellPipeWriter, + cwd: &Path, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_remove(cwd, args).await { - Ok(()) => ExecuteResult::from_exit_code(0), - Err(err) => { - let _ = stderr.write_line(&format!("rm: {err}")); - ExecuteResult::from_exit_code(1) - } + match execute_remove(cwd, args).await { + Ok(()) => ExecuteResult::from_exit_code(0), + Err(err) => { + let _ = stderr.write_line(&format!("rm: {err}")); + ExecuteResult::from_exit_code(1) } + } } async fn execute_remove(cwd: &Path, args: Vec) -> Result<()> { - let flags = parse_args(args)?; - for specified_path in &flags.paths { - let path = cwd.join(specified_path); - let result = if flags.recursive { - if path.is_dir() { - tokio::fs::remove_dir_all(&path).await - } else { - remove_file_or_dir(&path, &flags).await - } - } else { - remove_file_or_dir(&path, &flags).await - }; - if let Err(err) = result { - if err.kind() != ErrorKind::NotFound || !flags.force { - bail!("cannot remove '{}': {}", specified_path, err); - } - } + let flags = parse_args(args)?; + for specified_path in &flags.paths { + let path = cwd.join(specified_path); + let result = if flags.recursive { + if path.is_dir() { + tokio::fs::remove_dir_all(&path).await + } else { + remove_file_or_dir(&path, &flags).await + } + } else { + remove_file_or_dir(&path, &flags).await + }; + if let Err(err) = result { + if err.kind() != ErrorKind::NotFound || !flags.force { + bail!("cannot remove '{}': {}", specified_path, err); + } } + } - Ok(()) + Ok(()) } async fn remove_file_or_dir( - path: &Path, - flags: &RmFlags, + path: &Path, + flags: &RmFlags, ) -> std::io::Result<()> { - if flags.dir && path.is_dir() { - tokio::fs::remove_dir(path).await - } else { - tokio::fs::remove_file(path).await - } + if flags.dir && path.is_dir() { + tokio::fs::remove_dir(path).await + } else { + tokio::fs::remove_file(path).await + } } #[derive(Default, Debug, PartialEq)] struct RmFlags { - force: bool, - recursive: bool, - dir: bool, - paths: Vec, + force: bool, + recursive: bool, + dir: bool, + paths: Vec, } fn parse_args(args: Vec) -> Result { - let mut result = RmFlags::default(); - - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::LongFlag("recursive") - | ArgKind::ShortFlag('r') - | ArgKind::ShortFlag('R') => { - result.recursive = true; - } - ArgKind::LongFlag("dir") | ArgKind::ShortFlag('d') => { - result.dir = true; - } - ArgKind::LongFlag("force") | ArgKind::ShortFlag('f') => { - result.force = true; - } - ArgKind::Arg(path) => { - result.paths.push(path.to_string()); - } - ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => { - arg.bail_unsupported()? - } - } + let mut result = RmFlags::default(); + + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::LongFlag("recursive") + | ArgKind::ShortFlag('r') + | ArgKind::ShortFlag('R') => { + result.recursive = true; + } + ArgKind::LongFlag("dir") | ArgKind::ShortFlag('d') => { + result.dir = true; + } + ArgKind::LongFlag("force") | ArgKind::ShortFlag('f') => { + result.force = true; + } + ArgKind::Arg(path) => { + result.paths.push(path.to_string()); + } + ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => arg.bail_unsupported()?, } + } - if result.paths.is_empty() { - bail!("missing operand"); - } + if result.paths.is_empty() { + bail!("missing operand"); + } - Ok(result) + Ok(result) } #[cfg(test)] mod test { - use tempfile::tempdir; - - use super::*; - use std::fs; - - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![ - "--recursive".to_string(), - "--dir".to_string(), - "a".to_string(), - "b".to_string(), - ]) - .unwrap(), - RmFlags { - recursive: true, - dir: true, - paths: vec!["a".to_string(), "b".to_string()], - ..Default::default() - } - ); - assert_eq!( - parse_args(vec![ - "-rf".to_string(), - "a".to_string(), - "b".to_string(), - ]) - .unwrap(), - RmFlags { - recursive: true, - force: true, - dir: false, - paths: vec!["a".to_string(), "b".to_string()], - } - ); - assert_eq!( - parse_args(vec!["-d".to_string(), "a".to_string()]).unwrap(), - RmFlags { - recursive: false, - force: false, - dir: true, - paths: vec!["a".to_string()], - } - ); - assert_eq!( - parse_args(vec!["--recursive".to_string(), "-f".to_string(),]) - .err() - .unwrap() - .to_string(), - "missing operand", - ); - assert_eq!( - parse_args(vec![ - "--recursive".to_string(), - "-u".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -u", - ); - assert_eq!( - parse_args(vec![ - "--recursive".to_string(), - "--random-flag".to_string(), - "a".to_string(), - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: --random-flag", - ); - } - - #[tokio::test] - async fn test_force() { - let dir = tempdir().unwrap(); - let existent_file = dir.path().join("existent.txt"); - fs::write(&existent_file, "").unwrap(); - - execute_remove( - dir.path(), - vec!["-f".to_string(), "non_existent.txt".to_string()], - ) - .await - .unwrap(); - - let result = - execute_remove(dir.path(), vec!["non_existent.txt".to_string()]) - .await; - assert_eq!( - result.err().unwrap().to_string(), - format!( - "cannot remove 'non_existent.txt': {}", - no_such_file_error_text() - ) - ); - - assert!(existent_file.exists()); - execute_remove(dir.path(), vec!["existent.txt".to_string()]) - .await - .unwrap(); - assert!(!existent_file.exists()); - } - - #[tokio::test] - async fn test_recursive() { - let dir = tempdir().unwrap(); - let existent_file = dir.path().join("existent.txt"); - fs::write(&existent_file, "").unwrap(); - - let result = execute_remove( - dir.path(), - vec!["-r".to_string(), "non_existent.txt".to_string()], - ) + use tempfile::tempdir; + + use super::*; + use std::fs; + + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![ + "--recursive".to_string(), + "--dir".to_string(), + "a".to_string(), + "b".to_string(), + ]) + .unwrap(), + RmFlags { + recursive: true, + dir: true, + paths: vec!["a".to_string(), "b".to_string()], + ..Default::default() + } + ); + assert_eq!( + parse_args(vec!["-rf".to_string(), "a".to_string(), "b".to_string(),]) + .unwrap(), + RmFlags { + recursive: true, + force: true, + dir: false, + paths: vec!["a".to_string(), "b".to_string()], + } + ); + assert_eq!( + parse_args(vec!["-d".to_string(), "a".to_string()]).unwrap(), + RmFlags { + recursive: false, + force: false, + dir: true, + paths: vec!["a".to_string()], + } + ); + assert_eq!( + parse_args(vec!["--recursive".to_string(), "-f".to_string(),]) + .err() + .unwrap() + .to_string(), + "missing operand", + ); + assert_eq!( + parse_args(vec![ + "--recursive".to_string(), + "-u".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -u", + ); + assert_eq!( + parse_args(vec![ + "--recursive".to_string(), + "--random-flag".to_string(), + "a".to_string(), + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: --random-flag", + ); + } + + #[tokio::test] + async fn test_force() { + let dir = tempdir().unwrap(); + let existent_file = dir.path().join("existent.txt"); + fs::write(&existent_file, "").unwrap(); + + execute_remove( + dir.path(), + vec!["-f".to_string(), "non_existent.txt".to_string()], + ) + .await + .unwrap(); + + let result = + execute_remove(dir.path(), vec!["non_existent.txt".to_string()]).await; + assert_eq!( + result.err().unwrap().to_string(), + format!( + "cannot remove 'non_existent.txt': {}", + no_such_file_error_text() + ) + ); + + assert!(existent_file.exists()); + execute_remove(dir.path(), vec!["existent.txt".to_string()]) + .await + .unwrap(); + assert!(!existent_file.exists()); + } + + #[tokio::test] + async fn test_recursive() { + let dir = tempdir().unwrap(); + let existent_file = dir.path().join("existent.txt"); + fs::write(&existent_file, "").unwrap(); + + let result = execute_remove( + dir.path(), + vec!["-r".to_string(), "non_existent.txt".to_string()], + ) + .await; + assert_eq!( + result.err().unwrap().to_string(), + format!( + "cannot remove 'non_existent.txt': {}", + no_such_file_error_text() + ) + ); + + // test on a file + assert!(existent_file.exists()); + execute_remove( + dir.path(), + vec!["-r".to_string(), "existent.txt".to_string()], + ) + .await + .unwrap(); + assert!(!existent_file.exists()); + + // test on a directory + let sub_dir = dir.path().join("folder").join("sub"); + fs::create_dir_all(&sub_dir).unwrap(); + let sub_file = sub_dir.join("file.txt"); + fs::write(&sub_file, "test").unwrap(); + assert!(sub_file.exists()); + execute_remove(dir.path(), vec!["-r".to_string(), "folder".to_string()]) + .await + .unwrap(); + assert!(!sub_file.exists()); + + let result = + execute_remove(dir.path(), vec!["-r".to_string(), "folder".to_string()]) .await; - assert_eq!( - result.err().unwrap().to_string(), - format!( - "cannot remove 'non_existent.txt': {}", - no_such_file_error_text() - ) - ); - - // test on a file - assert!(existent_file.exists()); - execute_remove( - dir.path(), - vec!["-r".to_string(), "existent.txt".to_string()], - ) - .await - .unwrap(); - assert!(!existent_file.exists()); - - // test on a directory - let sub_dir = dir.path().join("folder").join("sub"); - fs::create_dir_all(&sub_dir).unwrap(); - let sub_file = sub_dir.join("file.txt"); - fs::write(&sub_file, "test").unwrap(); - assert!(sub_file.exists()); - execute_remove( - dir.path(), - vec!["-r".to_string(), "folder".to_string()], - ) - .await - .unwrap(); - assert!(!sub_file.exists()); - - let result = execute_remove( - dir.path(), - vec!["-r".to_string(), "folder".to_string()], - ) - .await; - assert_eq!( - result.err().unwrap().to_string(), - format!("cannot remove 'folder': {}", no_such_file_error_text()) - ); - execute_remove( - dir.path(), - vec!["-rf".to_string(), "folder".to_string()], - ) - .await - .unwrap(); - } - - #[tokio::test] - async fn test_dir() { - let dir = tempdir().unwrap(); - let existent_file = dir.path().join("existent.txt"); - let existent_dir = dir.path().join("sub_dir"); - let existent_dir_files = dir.path().join("sub_dir_files"); - fs::write(&existent_file, "").unwrap(); - fs::create_dir(&existent_dir).unwrap(); - fs::create_dir(&existent_dir_files).unwrap(); - fs::write(existent_dir_files.join("file.txt"), "").unwrap(); - - assert!(execute_remove( - dir.path(), - vec!["-d".to_string(), "existent.txt".to_string()], - ) - .await - .is_ok()); - - assert!(execute_remove( - dir.path(), - vec!["-d".to_string(), "sub_dir".to_string()], - ) - .await - .is_ok()); - assert!(!existent_dir.exists()); - - let result = execute_remove( - dir.path(), - vec!["-d".to_string(), "sub_dir_files".to_string()], - ) - .await; - assert_eq!( - result.err().unwrap().to_string(), - format!( - "cannot remove 'sub_dir_files': {}", - directory_not_empty_text() - ), - ); - assert!(existent_dir_files.exists()); - } - - fn no_such_file_error_text() -> &'static str { - if cfg!(windows) { - "The system cannot find the file specified. (os error 2)" - } else { - "No such file or directory (os error 2)" - } + assert_eq!( + result.err().unwrap().to_string(), + format!("cannot remove 'folder': {}", no_such_file_error_text()) + ); + execute_remove(dir.path(), vec!["-rf".to_string(), "folder".to_string()]) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_dir() { + let dir = tempdir().unwrap(); + let existent_file = dir.path().join("existent.txt"); + let existent_dir = dir.path().join("sub_dir"); + let existent_dir_files = dir.path().join("sub_dir_files"); + fs::write(&existent_file, "").unwrap(); + fs::create_dir(&existent_dir).unwrap(); + fs::create_dir(&existent_dir_files).unwrap(); + fs::write(existent_dir_files.join("file.txt"), "").unwrap(); + + assert!(execute_remove( + dir.path(), + vec!["-d".to_string(), "existent.txt".to_string()], + ) + .await + .is_ok()); + + assert!(execute_remove( + dir.path(), + vec!["-d".to_string(), "sub_dir".to_string()], + ) + .await + .is_ok()); + assert!(!existent_dir.exists()); + + let result = execute_remove( + dir.path(), + vec!["-d".to_string(), "sub_dir_files".to_string()], + ) + .await; + assert_eq!( + result.err().unwrap().to_string(), + format!( + "cannot remove 'sub_dir_files': {}", + directory_not_empty_text() + ), + ); + assert!(existent_dir_files.exists()); + } + + fn no_such_file_error_text() -> &'static str { + if cfg!(windows) { + "The system cannot find the file specified. (os error 2)" + } else { + "No such file or directory (os error 2)" } + } - fn directory_not_empty_text() -> &'static str { - if cfg!(windows) { - "The directory is not empty. (os error 145)" - } else if cfg!(target_os = "macos") { - "Directory not empty (os error 66)" - } else { - "Directory not empty (os error 39)" - } + fn directory_not_empty_text() -> &'static str { + if cfg!(windows) { + "The directory is not empty. (os error 145)" + } else if cfg!(target_os = "macos") { + "Directory not empty (os error 66)" + } else { + "Directory not empty (os error 39)" } + } } diff --git a/crates/deno_task_shell/src/shell/commands/sleep.rs b/crates/deno_task_shell/src/shell/commands/sleep.rs index c1852f6..f1e68e6 100644 --- a/crates/deno_task_shell/src/shell/commands/sleep.rs +++ b/crates/deno_task_shell/src/shell/commands/sleep.rs @@ -19,127 +19,121 @@ use super::ShellCommandContext; pub struct SleepCommand; impl ShellCommand for SleepCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - execute_with_cancellation!( - sleep_command(context.args, context.stderr), - context.state.token() - ) - } - .boxed_local() + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + execute_with_cancellation!( + sleep_command(context.args, context.stderr), + context.state.token() + ) } + .boxed_local() + } } async fn sleep_command( - args: Vec, - mut stderr: ShellPipeWriter, + args: Vec, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - match execute_sleep(args).await { - Ok(()) => ExecuteResult::from_exit_code(0), - Err(err) => { - let _ = stderr.write_line(&format!("sleep: {err}")); - ExecuteResult::from_exit_code(1) - } + match execute_sleep(args).await { + Ok(()) => ExecuteResult::from_exit_code(0), + Err(err) => { + let _ = stderr.write_line(&format!("sleep: {err}")); + ExecuteResult::from_exit_code(1) } + } } async fn execute_sleep(args: Vec) -> Result<()> { - let ms = parse_args(args)?; - tokio::time::sleep(Duration::from_millis(ms)).await; - Ok(()) + let ms = parse_args(args)?; + tokio::time::sleep(Duration::from_millis(ms)).await; + Ok(()) } fn parse_arg(arg: &str) -> Result { - if let Some(t) = arg.strip_suffix('s') { - return Ok(t.parse()?); - } - if let Some(t) = arg.strip_suffix('m') { - return Ok(t.parse::()? * 60.); - } - if let Some(t) = arg.strip_suffix('h') { - return Ok(t.parse::()? * 60. * 60.); - } - if let Some(t) = arg.strip_suffix('d') { - return Ok(t.parse::()? * 60. * 60. * 24.); - } - - Ok(arg.parse()?) + if let Some(t) = arg.strip_suffix('s') { + return Ok(t.parse()?); + } + if let Some(t) = arg.strip_suffix('m') { + return Ok(t.parse::()? * 60.); + } + if let Some(t) = arg.strip_suffix('h') { + return Ok(t.parse::()? * 60. * 60.); + } + if let Some(t) = arg.strip_suffix('d') { + return Ok(t.parse::()? * 60. * 60. * 24.); + } + + Ok(arg.parse()?) } fn parse_args(args: Vec) -> Result { - // the time to sleep is the sum of all the arguments - let mut total_time_ms = 0; - let mut had_value = false; - for arg in parse_arg_kinds(&args) { - match arg { - ArgKind::Arg(arg) => match parse_arg(arg) { - Ok(value_s) => { - let ms = (value_s * 1000f64) as u64; - total_time_ms += ms; - had_value = true; - } - Err(err) => { - bail!( - "error parsing argument '{}' to number: {}", - arg, - err - ); - } - }, - ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => { - arg.bail_unsupported()? - } + // the time to sleep is the sum of all the arguments + let mut total_time_ms = 0; + let mut had_value = false; + for arg in parse_arg_kinds(&args) { + match arg { + ArgKind::Arg(arg) => match parse_arg(arg) { + Ok(value_s) => { + let ms = (value_s * 1000f64) as u64; + total_time_ms += ms; + had_value = true; } + Err(err) => { + bail!("error parsing argument '{}' to number: {}", arg, err); + } + }, + ArgKind::LongFlag(_) | ArgKind::ShortFlag(_) => arg.bail_unsupported()?, } - if !had_value { - bail!("missing operand"); - } - Ok(total_time_ms) + } + if !had_value { + bail!("missing operand"); + } + Ok(total_time_ms) } #[cfg(test)] mod test { - use std::time::Instant; - - use super::*; - - #[test] - fn should_parse_arg() { - assert_eq!(parse_arg("1").unwrap(), 1.); - assert_eq!(parse_arg("1s").unwrap(), 1.); - assert_eq!(parse_arg("1m").unwrap(), 1. * 60.); - assert_eq!(parse_arg("1h").unwrap(), 1. * 60. * 60.); - assert_eq!(parse_arg("1d").unwrap(), 1. * 60. * 60. * 24.); - assert!(parse_arg("d").err().is_some()); - } - - #[test] - fn should_parse_args() { - let value = parse_args(vec![ - "0.5".to_string(), - "1m".to_string(), - "1.25".to_string(), - ]) - .unwrap(); - assert_eq!(value, 500 + 1000 * 60 + 1250); - - let result = parse_args(vec![]).err().unwrap(); - assert_eq!(result.to_string(), "missing operand"); - - let result = parse_args(vec!["test".to_string()]).err().unwrap(); - assert_eq!( - result.to_string(), - "error parsing argument 'test' to number: invalid float literal" - ); - } - - #[tokio::test] - async fn should_execute() { - let time = Instant::now(); - execute_sleep(vec!["0.1".to_string()]).await.unwrap(); - assert!(time.elapsed().as_millis() >= 100); - } + use std::time::Instant; + + use super::*; + + #[test] + fn should_parse_arg() { + assert_eq!(parse_arg("1").unwrap(), 1.); + assert_eq!(parse_arg("1s").unwrap(), 1.); + assert_eq!(parse_arg("1m").unwrap(), 1. * 60.); + assert_eq!(parse_arg("1h").unwrap(), 1. * 60. * 60.); + assert_eq!(parse_arg("1d").unwrap(), 1. * 60. * 60. * 24.); + assert!(parse_arg("d").err().is_some()); + } + + #[test] + fn should_parse_args() { + let value = parse_args(vec![ + "0.5".to_string(), + "1m".to_string(), + "1.25".to_string(), + ]) + .unwrap(); + assert_eq!(value, 500 + 1000 * 60 + 1250); + + let result = parse_args(vec![]).err().unwrap(); + assert_eq!(result.to_string(), "missing operand"); + + let result = parse_args(vec!["test".to_string()]).err().unwrap(); + assert_eq!( + result.to_string(), + "error parsing argument 'test' to number: invalid float literal" + ); + } + + #[tokio::test] + async fn should_execute() { + let time = Instant::now(); + execute_sleep(vec!["0.1".to_string()]).await.unwrap(); + assert!(time.elapsed().as_millis() >= 100); + } } diff --git a/crates/deno_task_shell/src/shell/commands/unset.rs b/crates/deno_task_shell/src/shell/commands/unset.rs index ab9c9e5..9a60c3b 100644 --- a/crates/deno_task_shell/src/shell/commands/unset.rs +++ b/crates/deno_task_shell/src/shell/commands/unset.rs @@ -13,76 +13,76 @@ use super::ShellCommandContext; pub struct UnsetCommand; impl ShellCommand for UnsetCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - let result = match parse_names(context.args) { - Ok(names) => ExecuteResult::Continue( - 0, - names.into_iter().map(EnvChange::UnsetVar).collect(), - Vec::new(), - ), - Err(err) => { - let _ = context.stderr.write_line(&format!("unset: {err}")); - ExecuteResult::Continue(1, Vec::new(), Vec::new()) - } - }; - Box::pin(futures::future::ready(result)) - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match parse_names(context.args) { + Ok(names) => ExecuteResult::Continue( + 0, + names.into_iter().map(EnvChange::UnsetVar).collect(), + Vec::new(), + ), + Err(err) => { + let _ = context.stderr.write_line(&format!("unset: {err}")); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } } fn parse_names(mut args: Vec) -> Result> { - match args.first() { - None => { - // Running the actual `unset` with no argument completes with success. - Ok(args) - } - Some(flag) if flag == "-f" => bail!("unsupported flag: -f"), - Some(flag) if flag == "-v" => { - // It's fine to use `swap_remove` (instead of `remove`) because the order - // of args doesn't matter for `unset` command. - args.swap_remove(0); - Ok(args) - } - Some(_) => Ok(args), + match args.first() { + None => { + // Running the actual `unset` with no argument completes with success. + Ok(args) + } + Some(flag) if flag == "-f" => bail!("unsupported flag: -f"), + Some(flag) if flag == "-v" => { + // It's fine to use `swap_remove` (instead of `remove`) because the order + // of args doesn't matter for `unset` command. + args.swap_remove(0); + Ok(args) } + Some(_) => Ok(args), + } } #[cfg(test)] mod test { - use super::*; + use super::*; - #[test] - fn parse_args() { - assert_eq!( - parse_names(vec!["VAR1".to_string()]).unwrap(), - vec!["VAR1".to_string()] - ); - assert_eq!( - parse_names(vec!["VAR1".to_string(), "VAR2".to_string()]).unwrap(), - vec!["VAR1".to_string(), "VAR2".to_string()] - ); - assert!(parse_names(vec![]).unwrap().is_empty()); - assert_eq!( - parse_names(vec![ - "-f".to_string(), - "VAR1".to_string(), - "VAR2".to_string() - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -f".to_string() - ); - assert_eq!( - parse_names(vec![ - "-v".to_string(), - "VAR1".to_string(), - "VAR2".to_string() - ]) - .unwrap(), - vec!["VAR2".to_string(), "VAR1".to_string()] - ); - } + #[test] + fn parse_args() { + assert_eq!( + parse_names(vec!["VAR1".to_string()]).unwrap(), + vec!["VAR1".to_string()] + ); + assert_eq!( + parse_names(vec!["VAR1".to_string(), "VAR2".to_string()]).unwrap(), + vec!["VAR1".to_string(), "VAR2".to_string()] + ); + assert!(parse_names(vec![]).unwrap().is_empty()); + assert_eq!( + parse_names(vec![ + "-f".to_string(), + "VAR1".to_string(), + "VAR2".to_string() + ]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -f".to_string() + ); + assert_eq!( + parse_names(vec![ + "-v".to_string(), + "VAR1".to_string(), + "VAR2".to_string() + ]) + .unwrap(), + vec!["VAR2".to_string(), "VAR1".to_string()] + ); + } } diff --git a/crates/deno_task_shell/src/shell/commands/xargs.rs b/crates/deno_task_shell/src/shell/commands/xargs.rs index 44c2dcf..7ff01e1 100644 --- a/crates/deno_task_shell/src/shell/commands/xargs.rs +++ b/crates/deno_task_shell/src/shell/commands/xargs.rs @@ -17,310 +17,298 @@ use super::ShellCommandContext; pub struct XargsCommand; impl ShellCommand for XargsCommand { - fn execute( - &self, - mut context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - async move { - match xargs_collect_args(context.args, context.stdin.clone()) { - Ok(args) => { - // don't select on cancellation here as that will occur at a lower level - (context.execute_command_args)(ExecuteCommandArgsContext { - args, - state: context.state, - stdin: context.stdin, - stdout: context.stdout, - stderr: context.stderr, - }) - .await - } - Err(err) => { - let _ = context.stderr.write_line(&format!("xargs: {err}")); - ExecuteResult::from_exit_code(1) - } - } + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + async move { + match xargs_collect_args(context.args, context.stdin.clone()) { + Ok(args) => { + // don't select on cancellation here as that will occur at a lower level + (context.execute_command_args)(ExecuteCommandArgsContext { + args, + state: context.state, + stdin: context.stdin, + stdout: context.stdout, + stderr: context.stderr, + }) + .await + } + Err(err) => { + let _ = context.stderr.write_line(&format!("xargs: {err}")); + ExecuteResult::from_exit_code(1) } - .boxed_local() + } } + .boxed_local() + } } fn xargs_collect_args( - cli_args: Vec, - stdin: ShellPipeReader, + cli_args: Vec, + stdin: ShellPipeReader, ) -> Result> { - let flags = parse_args(cli_args)?; - let mut buf = Vec::new(); - stdin.pipe_to(&mut buf)?; - let text = String::from_utf8(buf)?; - let mut args = flags.initial_args; + let flags = parse_args(cli_args)?; + let mut buf = Vec::new(); + stdin.pipe_to(&mut buf)?; + let text = String::from_utf8(buf)?; + let mut args = flags.initial_args; - if args.is_empty() { - // defaults to echo - args.push("echo".to_string()); - } + if args.is_empty() { + // defaults to echo + args.push("echo".to_string()); + } - if let Some(delim) = &flags.delimiter { - // strip a single trailing newline (xargs seems to do this) - let text = if *delim == '\n' { - if let Some(text) = text.strip_suffix(&delim.to_string()) { - text - } else { - &text - } - } else { - &text - }; - - args.extend(text.split(*delim).map(|t| t.to_string())); - } else if flags.is_null_delimited { - args.extend(text.split('\0').map(|t| t.to_string())); + if let Some(delim) = &flags.delimiter { + // strip a single trailing newline (xargs seems to do this) + let text = if *delim == '\n' { + if let Some(text) = text.strip_suffix(&delim.to_string()) { + text + } else { + &text + } } else { - args.extend(delimit_blanks(&text)?); - } + &text + }; + + args.extend(text.split(*delim).map(|t| t.to_string())); + } else if flags.is_null_delimited { + args.extend(text.split('\0').map(|t| t.to_string())); + } else { + args.extend(delimit_blanks(&text)?); + } - Ok(args) + Ok(args) } fn delimit_blanks(text: &str) -> Result> { - let mut chars = text.chars().peekable(); - let mut result = Vec::new(); - while chars.peek().is_some() { - let mut current = String::new(); - while let Some(c) = chars.next() { + let mut chars = text.chars().peekable(); + let mut result = Vec::new(); + while chars.peek().is_some() { + let mut current = String::new(); + while let Some(c) = chars.next() { + match c { + '\n' | '\t' | ' ' => break, + '"' | '\'' => { + const UNMATCHED_MESSAGE: &str = "unmatched quote; by default quotes are special to xargs unless you use the -0 option"; + let original_quote_char = c; + while let Some(c) = chars.next() { + if c == original_quote_char { + break; + } match c { - '\n' | '\t' | ' ' => break, - '"' | '\'' => { - const UNMATCHED_MESSAGE: &str = "unmatched quote; by default quotes are special to xargs unless you use the -0 option"; - let original_quote_char = c; - while let Some(c) = chars.next() { - if c == original_quote_char { - break; - } - match c { - '\n' => bail!("{}", UNMATCHED_MESSAGE), - _ => current.push(c), - } - if chars.peek().is_none() { - bail!("{}", UNMATCHED_MESSAGE) - } - } - } - '\\' => { - if matches!( - chars.peek(), - Some('\n' | '\t' | ' ' | '"' | '\'') - ) { - current.push(chars.next().unwrap()); - } else { - current.push(c); - } - } - _ => current.push(c), + '\n' => bail!("{}", UNMATCHED_MESSAGE), + _ => current.push(c), + } + if chars.peek().is_none() { + bail!("{}", UNMATCHED_MESSAGE) } + } } - - if !current.is_empty() { - result.push(current); + '\\' => { + if matches!(chars.peek(), Some('\n' | '\t' | ' ' | '"' | '\'')) { + current.push(chars.next().unwrap()); + } else { + current.push(c); + } } + _ => current.push(c), + } + } + + if !current.is_empty() { + result.push(current); } - Ok(result) + } + Ok(result) } #[derive(Debug, PartialEq)] struct XargsFlags { - initial_args: Vec, - delimiter: Option, - is_null_delimited: bool, + initial_args: Vec, + delimiter: Option, + is_null_delimited: bool, } fn parse_args(args: Vec) -> Result { - fn parse_delimiter(arg: &str) -> Result { - let mut chars = arg.chars(); - if let Some(first_char) = chars.next() { - let mut delimiter = first_char; - if first_char == '\\' { - delimiter = match chars.next() { - // todo(dsherret): support more - Some('n') => '\n', - Some('r') => '\r', - Some('t') => '\t', - Some('\\') => '\\', - Some('0') => '\0', - None => bail!("expected character following escape"), - _ => bail!("unsupported/not implemented escape character"), - }; - } + fn parse_delimiter(arg: &str) -> Result { + let mut chars = arg.chars(); + if let Some(first_char) = chars.next() { + let mut delimiter = first_char; + if first_char == '\\' { + delimiter = match chars.next() { + // todo(dsherret): support more + Some('n') => '\n', + Some('r') => '\r', + Some('t') => '\t', + Some('\\') => '\\', + Some('0') => '\0', + None => bail!("expected character following escape"), + _ => bail!("unsupported/not implemented escape character"), + }; + } - if chars.next().is_some() { - bail!("expected a single byte char delimiter. Found: {}", arg); - } + if chars.next().is_some() { + bail!("expected a single byte char delimiter. Found: {}", arg); + } - Ok(delimiter) - } else { - bail!("expected non-empty delimiter"); - } + Ok(delimiter) + } else { + bail!("expected non-empty delimiter"); } + } - let mut initial_args = Vec::new(); - let mut delimiter = None; - let mut iterator = parse_arg_kinds(&args).into_iter(); - let mut is_null_delimited = false; - while let Some(arg) = iterator.next() { - match arg { - ArgKind::Arg(arg) => { - if arg == "-0" { - is_null_delimited = true; - } else { - initial_args.push(arg.to_string()); - // parse the remainder as arguments - for arg in iterator.by_ref() { - match arg { - ArgKind::Arg(arg) => { - initial_args.push(arg.to_string()); - } - ArgKind::ShortFlag(f) => { - initial_args.push(format!("-{f}")) - } - ArgKind::LongFlag(f) => { - initial_args.push(format!("--{f}")) - } - } - } - } - } - ArgKind::LongFlag("null") => { - is_null_delimited = true; - } - ArgKind::ShortFlag('d') => match iterator.next() { - Some(ArgKind::Arg(arg)) => { - delimiter = Some(parse_delimiter(arg)?); - } - _ => bail!("expected delimiter argument following -d"), - }, - ArgKind::LongFlag(flag) => { - if let Some(arg) = flag.strip_prefix("delimiter=") { - delimiter = Some(parse_delimiter(arg)?); - } else { - arg.bail_unsupported()? - } + let mut initial_args = Vec::new(); + let mut delimiter = None; + let mut iterator = parse_arg_kinds(&args).into_iter(); + let mut is_null_delimited = false; + while let Some(arg) = iterator.next() { + match arg { + ArgKind::Arg(arg) => { + if arg == "-0" { + is_null_delimited = true; + } else { + initial_args.push(arg.to_string()); + // parse the remainder as arguments + for arg in iterator.by_ref() { + match arg { + ArgKind::Arg(arg) => { + initial_args.push(arg.to_string()); + } + ArgKind::ShortFlag(f) => initial_args.push(format!("-{f}")), + ArgKind::LongFlag(f) => initial_args.push(format!("--{f}")), } - _ => arg.bail_unsupported()?, + } + } + } + ArgKind::LongFlag("null") => { + is_null_delimited = true; + } + ArgKind::ShortFlag('d') => match iterator.next() { + Some(ArgKind::Arg(arg)) => { + delimiter = Some(parse_delimiter(arg)?); } + _ => bail!("expected delimiter argument following -d"), + }, + ArgKind::LongFlag(flag) => { + if let Some(arg) = flag.strip_prefix("delimiter=") { + delimiter = Some(parse_delimiter(arg)?); + } else { + arg.bail_unsupported()? + } + } + _ => arg.bail_unsupported()?, } + } - if is_null_delimited && delimiter.is_some() { - bail!("cannot specify both null and delimiter flag") - } + if is_null_delimited && delimiter.is_some() { + bail!("cannot specify both null and delimiter flag") + } - Ok(XargsFlags { - initial_args, - delimiter, - is_null_delimited, - }) + Ok(XargsFlags { + initial_args, + delimiter, + is_null_delimited, + }) } #[cfg(test)] mod test { - use super::*; - use pretty_assertions::assert_eq; + use super::*; + use pretty_assertions::assert_eq; - #[test] - fn parses_args() { - assert_eq!( - parse_args(vec![]).unwrap(), - XargsFlags { - initial_args: Vec::new(), - delimiter: None, - is_null_delimited: false, - } - ); - assert_eq!( - parse_args(vec![ - "-0".to_string(), - "echo".to_string(), - "2".to_string(), - "-d".to_string(), - "--test=3".to_string() - ]) - .unwrap(), - XargsFlags { - initial_args: vec![ - "echo".to_string(), - "2".to_string(), - "-d".to_string(), - "--test=3".to_string() - ], - delimiter: None, - is_null_delimited: true, - } - ); - assert_eq!( - parse_args(vec![ - "-d".to_string(), - "\\n".to_string(), - "echo".to_string() - ]) - .unwrap(), - XargsFlags { - initial_args: vec!["echo".to_string()], - delimiter: Some('\n'), - is_null_delimited: false, - } - ); - assert_eq!( - parse_args(vec![ - "--delimiter=5".to_string(), - "echo".to_string(), - "-d".to_string() - ]) - .unwrap(), - XargsFlags { - initial_args: vec!["echo".to_string(), "-d".to_string()], - delimiter: Some('5'), - is_null_delimited: false, - } - ); - assert_eq!( - parse_args(vec![ - "-d".to_string(), - "5".to_string(), - "-t".to_string() - ]) - .err() - .unwrap() - .to_string(), - "unsupported flag: -t", - ); - assert_eq!( - parse_args(vec!["-d".to_string(), "-t".to_string()]) - .err() - .unwrap() - .to_string(), - "expected delimiter argument following -d", - ); - assert_eq!( - parse_args(vec!["--delimiter=5".to_string(), "--null".to_string()]) - .err() - .unwrap() - .to_string(), - "cannot specify both null and delimiter flag", - ); - } + #[test] + fn parses_args() { + assert_eq!( + parse_args(vec![]).unwrap(), + XargsFlags { + initial_args: Vec::new(), + delimiter: None, + is_null_delimited: false, + } + ); + assert_eq!( + parse_args(vec![ + "-0".to_string(), + "echo".to_string(), + "2".to_string(), + "-d".to_string(), + "--test=3".to_string() + ]) + .unwrap(), + XargsFlags { + initial_args: vec![ + "echo".to_string(), + "2".to_string(), + "-d".to_string(), + "--test=3".to_string() + ], + delimiter: None, + is_null_delimited: true, + } + ); + assert_eq!( + parse_args(vec![ + "-d".to_string(), + "\\n".to_string(), + "echo".to_string() + ]) + .unwrap(), + XargsFlags { + initial_args: vec!["echo".to_string()], + delimiter: Some('\n'), + is_null_delimited: false, + } + ); + assert_eq!( + parse_args(vec![ + "--delimiter=5".to_string(), + "echo".to_string(), + "-d".to_string() + ]) + .unwrap(), + XargsFlags { + initial_args: vec!["echo".to_string(), "-d".to_string()], + delimiter: Some('5'), + is_null_delimited: false, + } + ); + assert_eq!( + parse_args(vec!["-d".to_string(), "5".to_string(), "-t".to_string()]) + .err() + .unwrap() + .to_string(), + "unsupported flag: -t", + ); + assert_eq!( + parse_args(vec!["-d".to_string(), "-t".to_string()]) + .err() + .unwrap() + .to_string(), + "expected delimiter argument following -d", + ); + assert_eq!( + parse_args(vec!["--delimiter=5".to_string(), "--null".to_string()]) + .err() + .unwrap() + .to_string(), + "cannot specify both null and delimiter flag", + ); + } - #[test] - fn should_delimit_blanks() { - assert_eq!( - delimit_blanks("testing this\tout\nhere\n \n\t\t test").unwrap(), - vec!["testing", "this", "out", "here", "test",] - ); - assert_eq!( - delimit_blanks("testing 'this\tout here ' \"now double\"") - .unwrap(), - vec!["testing", "this\tout here ", "now double"] - ); - assert_eq!( + #[test] + fn should_delimit_blanks() { + assert_eq!( + delimit_blanks("testing this\tout\nhere\n \n\t\t test").unwrap(), + vec!["testing", "this", "out", "here", "test",] + ); + assert_eq!( + delimit_blanks("testing 'this\tout here ' \"now double\"").unwrap(), + vec!["testing", "this\tout here ", "now double"] + ); + assert_eq!( delimit_blanks("testing 'this\nout here '").err().unwrap().to_string(), "unmatched quote; by default quotes are special to xargs unless you use the -0 option", ); - } + } } diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index 9a60ce6..a294095 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -58,20 +58,20 @@ use super::types::CANCELLATION_EXIT_CODE; /// # Returns /// The exit code of the command execution. pub async fn execute( - list: SequentialList, - env_vars: HashMap, - cwd: &Path, - custom_commands: HashMap>, + list: SequentialList, + env_vars: HashMap, + cwd: &Path, + custom_commands: HashMap>, ) -> i32 { - let state = ShellState::new(env_vars, cwd, custom_commands); - execute_with_pipes( - list, - state, - ShellPipeReader::stdin(), - ShellPipeWriter::stdout(), - ShellPipeWriter::stderr(), - ) - .await + let state = ShellState::new(env_vars, cwd, custom_commands); + execute_with_pipes( + list, + state, + ShellPipeReader::stdin(), + ShellPipeWriter::stdout(), + ShellPipeWriter::stderr(), + ) + .await } /// Executes a `SequentialList` of commands with specified input and output pipes. @@ -92,607 +92,561 @@ pub async fn execute( /// /// The exit code of the command execution. pub async fn execute_with_pipes( - list: SequentialList, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + list: SequentialList, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> i32 { - // spawn a sequential list and pipe its output to the environment - let result = execute_sequential_list( - list, - state, - stdin, - stdout, - stderr, - AsyncCommandBehavior::Wait, - ) - .await; - - match result { - ExecuteResult::Exit(code, _) => code, - ExecuteResult::Continue(exit_code, _, _) => exit_code, - } + // spawn a sequential list and pipe its output to the environment + let result = execute_sequential_list( + list, + state, + stdin, + stdout, + stderr, + AsyncCommandBehavior::Wait, + ) + .await; + + match result { + ExecuteResult::Exit(code, _) => code, + ExecuteResult::Continue(exit_code, _, _) => exit_code, + } } #[derive(Debug, PartialEq)] enum AsyncCommandBehavior { - Wait, - Yield, + Wait, + Yield, } fn execute_sequential_list( - list: SequentialList, - mut state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, - async_command_behavior: AsyncCommandBehavior, + list: SequentialList, + mut state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, + async_command_behavior: AsyncCommandBehavior, ) -> FutureExecuteResult { - async move { - let mut final_exit_code = 0; - let mut final_changes = Vec::new(); - let mut async_handles = Vec::new(); - let mut was_exit = false; - for item in list.items { - if item.is_async { - let state = state.clone(); - let stdin = stdin.clone(); - let stdout = stdout.clone(); - let stderr = stderr.clone(); - async_handles.push(tokio::task::spawn_local(async move { - let main_token = state.token().clone(); - let result = execute_sequence( - item.sequence, - state, - stdin, - stdout, - stderr, - ) - .await; - let (exit_code, handles) = - result.into_exit_code_and_handles(); - wait_handles(exit_code, handles, main_token).await - })); - } else { - let result = execute_sequence( - item.sequence, - state.clone(), - stdin.clone(), - stdout.clone(), - stderr.clone(), - ) - .await; - match result { - ExecuteResult::Exit(exit_code, handles) => { - async_handles.extend(handles); - final_exit_code = exit_code; - was_exit = true; - break; - } - ExecuteResult::Continue(exit_code, changes, handles) => { - state.apply_changes(&changes); - state.apply_env_var("?", &exit_code.to_string()); - final_changes.extend(changes); - async_handles.extend(handles); - // use the final sequential item's exit code - final_exit_code = exit_code; - } - } - } + async move { + let mut final_exit_code = 0; + let mut final_changes = Vec::new(); + let mut async_handles = Vec::new(); + let mut was_exit = false; + for item in list.items { + if item.is_async { + let state = state.clone(); + let stdin = stdin.clone(); + let stdout = stdout.clone(); + let stderr = stderr.clone(); + async_handles.push(tokio::task::spawn_local(async move { + let main_token = state.token().clone(); + let result = + execute_sequence(item.sequence, state, stdin, stdout, stderr).await; + let (exit_code, handles) = result.into_exit_code_and_handles(); + wait_handles(exit_code, handles, main_token).await + })); + } else { + let result = execute_sequence( + item.sequence, + state.clone(), + stdin.clone(), + stdout.clone(), + stderr.clone(), + ) + .await; + match result { + ExecuteResult::Exit(exit_code, handles) => { + async_handles.extend(handles); + final_exit_code = exit_code; + was_exit = true; + break; + } + ExecuteResult::Continue(exit_code, changes, handles) => { + state.apply_changes(&changes); + state.apply_env_var("?", &exit_code.to_string()); + final_changes.extend(changes); + async_handles.extend(handles); + // use the final sequential item's exit code + final_exit_code = exit_code; + } } + } + } - // wait for async commands to complete - if async_command_behavior == AsyncCommandBehavior::Wait { - final_exit_code = wait_handles( - final_exit_code, - std::mem::take(&mut async_handles), - state.token().clone(), - ) - .await; - } + // wait for async commands to complete + if async_command_behavior == AsyncCommandBehavior::Wait { + final_exit_code = wait_handles( + final_exit_code, + std::mem::take(&mut async_handles), + state.token().clone(), + ) + .await; + } - if was_exit { - ExecuteResult::Exit(final_exit_code, async_handles) - } else { - ExecuteResult::Continue( - final_exit_code, - final_changes, - async_handles, - ) - } + if was_exit { + ExecuteResult::Exit(final_exit_code, async_handles) + } else { + ExecuteResult::Continue(final_exit_code, final_changes, async_handles) } - .boxed_local() + } + .boxed_local() } async fn wait_handles( - mut exit_code: i32, - mut handles: Vec>, - token: CancellationToken, + mut exit_code: i32, + mut handles: Vec>, + token: CancellationToken, ) -> i32 { - if exit_code != 0 { - token.cancel(); + if exit_code != 0 { + token.cancel(); + } + while !handles.is_empty() { + let result = futures::future::select_all(handles).await; + + // prefer the first non-zero then non-cancellation exit code + let new_exit_code = result.0.unwrap(); + if matches!(exit_code, 0 | CANCELLATION_EXIT_CODE) && new_exit_code != 0 { + exit_code = new_exit_code; } - while !handles.is_empty() { - let result = futures::future::select_all(handles).await; - - // prefer the first non-zero then non-cancellation exit code - let new_exit_code = result.0.unwrap(); - if matches!(exit_code, 0 | CANCELLATION_EXIT_CODE) && new_exit_code != 0 - { - exit_code = new_exit_code; - } - handles = result.2; - } - exit_code + handles = result.2; + } + exit_code } fn execute_sequence( - sequence: Sequence, - mut state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + sequence: Sequence, + mut state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> FutureExecuteResult { - // requires boxed async because of recursive async - async move { - match sequence { - Sequence::ShellVar(var) => ExecuteResult::Continue( - 0, - vec![EnvChange::SetShellVar( - var.name, - match evaluate_word( - var.value, - &state, - stdin, - stderr.clone(), - ) - .await - { - Ok(value) => value, - Err(err) => { - return err.into_exit_code(&mut stderr); - } - }, - )], - Vec::new(), - ), - Sequence::BooleanList(list) => { - let mut changes = vec![]; - let first_result = execute_sequence( - list.current, - state.clone(), - stdin.clone(), - stdout.clone(), - stderr.clone(), - ) - .await; - let (exit_code, mut async_handles) = match first_result { - ExecuteResult::Exit(_, _) => return first_result, - ExecuteResult::Continue( - exit_code, - sub_changes, - async_handles, - ) => { - state.apply_env_var("?", &exit_code.to_string()); - state.apply_changes(&sub_changes); - changes.extend(sub_changes); - (exit_code, async_handles) - } - }; + // requires boxed async because of recursive async + async move { + match sequence { + Sequence::ShellVar(var) => ExecuteResult::Continue( + 0, + vec![EnvChange::SetShellVar( + var.name, + match evaluate_word(var.value, &state, stdin, stderr.clone()).await { + Ok(value) => value, + Err(err) => { + return err.into_exit_code(&mut stderr); + } + }, + )], + Vec::new(), + ), + Sequence::BooleanList(list) => { + let mut changes = vec![]; + let first_result = execute_sequence( + list.current, + state.clone(), + stdin.clone(), + stdout.clone(), + stderr.clone(), + ) + .await; + let (exit_code, mut async_handles) = match first_result { + ExecuteResult::Exit(_, _) => return first_result, + ExecuteResult::Continue(exit_code, sub_changes, async_handles) => { + state.apply_env_var("?", &exit_code.to_string()); + state.apply_changes(&sub_changes); + changes.extend(sub_changes); + (exit_code, async_handles) + } + }; - let next = if list.op.moves_next_for_exit_code(exit_code) { - Some(list.next) - } else { - let mut next = list.next; - loop { - // boolean lists always move right on the tree - match next { - Sequence::BooleanList(list) => { - if list.op.moves_next_for_exit_code(exit_code) { - break Some(list.next); - } - next = list.next; - } - _ => break None, - } - } - }; - if let Some(next) = next { - let next_result = - execute_sequence(next, state, stdin, stdout, stderr) - .await; - match next_result { - ExecuteResult::Exit(code, sub_handles) => { - async_handles.extend(sub_handles); - ExecuteResult::Exit(code, async_handles) - } - ExecuteResult::Continue( - exit_code, - sub_changes, - sub_handles, - ) => { - changes.extend(sub_changes); - async_handles.extend(sub_handles); - ExecuteResult::Continue( - exit_code, - changes, - async_handles, - ) - } - } - } else { - ExecuteResult::Continue(exit_code, changes, async_handles) + let next = if list.op.moves_next_for_exit_code(exit_code) { + Some(list.next) + } else { + let mut next = list.next; + loop { + // boolean lists always move right on the tree + match next { + Sequence::BooleanList(list) => { + if list.op.moves_next_for_exit_code(exit_code) { + break Some(list.next); } + next = list.next; + } + _ => break None, } - Sequence::Pipeline(pipeline) => { - execute_pipeline(pipeline, state, stdin, stdout, stderr).await + } + }; + if let Some(next) = next { + let next_result = + execute_sequence(next, state, stdin, stdout, stderr).await; + match next_result { + ExecuteResult::Exit(code, sub_handles) => { + async_handles.extend(sub_handles); + ExecuteResult::Exit(code, async_handles) + } + ExecuteResult::Continue(exit_code, sub_changes, sub_handles) => { + changes.extend(sub_changes); + async_handles.extend(sub_handles); + ExecuteResult::Continue(exit_code, changes, async_handles) } + } + } else { + ExecuteResult::Continue(exit_code, changes, async_handles) } + } + Sequence::Pipeline(pipeline) => { + execute_pipeline(pipeline, state, stdin, stdout, stderr).await + } } - .boxed_local() + } + .boxed_local() } async fn execute_pipeline( - pipeline: Pipeline, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + pipeline: Pipeline, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - let result = - execute_pipeline_inner(pipeline.inner, state, stdin, stdout, stderr) - .await; - if pipeline.negated { - match result { - ExecuteResult::Exit(code, handles) => { - ExecuteResult::Exit(code, handles) - } - ExecuteResult::Continue(code, changes, handles) => { - let new_code = if code == 0 { 1 } else { 0 }; - ExecuteResult::Continue(new_code, changes, handles) - } - } - } else { - result + let result = + execute_pipeline_inner(pipeline.inner, state, stdin, stdout, stderr).await; + if pipeline.negated { + match result { + ExecuteResult::Exit(code, handles) => ExecuteResult::Exit(code, handles), + ExecuteResult::Continue(code, changes, handles) => { + let new_code = if code == 0 { 1 } else { 0 }; + ExecuteResult::Continue(new_code, changes, handles) + } } + } else { + result + } } async fn execute_pipeline_inner( - pipeline: PipelineInner, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + pipeline: PipelineInner, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - match pipeline { - PipelineInner::Command(command) => { - execute_command(command, state, stdin, stdout, stderr).await - } - PipelineInner::PipeSequence(pipe_sequence) => { - execute_pipe_sequence(*pipe_sequence, state, stdin, stdout, stderr) - .await - } + match pipeline { + PipelineInner::Command(command) => { + execute_command(command, state, stdin, stdout, stderr).await + } + PipelineInner::PipeSequence(pipe_sequence) => { + execute_pipe_sequence(*pipe_sequence, state, stdin, stdout, stderr).await } + } } #[derive(Debug)] enum RedirectPipe { - Input(ShellPipeReader), - Output(ShellPipeWriter), + Input(ShellPipeReader), + Output(ShellPipeWriter), } async fn resolve_redirect_pipe( - redirect: &Redirect, - state: &ShellState, - stdin: &ShellPipeReader, - stdout: &ShellPipeWriter, - stderr: &mut ShellPipeWriter, + redirect: &Redirect, + state: &ShellState, + stdin: &ShellPipeReader, + stdout: &ShellPipeWriter, + stderr: &mut ShellPipeWriter, ) -> Result { - match redirect.io_file.clone() { - IoFile::Word(word) => { - resolve_redirect_word_pipe(word, &redirect.op, state, stdin, stderr) - .await - } - IoFile::Fd(fd) => match &redirect.op { - RedirectOp::Input(RedirectOpInput::Redirect) => { - let _ = stderr.write_line( + match redirect.io_file.clone() { + IoFile::Word(word) => { + resolve_redirect_word_pipe(word, &redirect.op, state, stdin, stderr).await + } + IoFile::Fd(fd) => match &redirect.op { + RedirectOp::Input(RedirectOpInput::Redirect) => { + let _ = stderr.write_line( "deno_task_shell: input redirecting file descriptors is not implemented", ); - Err(ExecuteResult::from_exit_code(1)) - } - RedirectOp::Output(_op) => match fd { - 1 => Ok(RedirectPipe::Output(stdout.clone())), - 2 => Ok(RedirectPipe::Output(stderr.clone())), - _ => { - let _ = stderr.write_line( + Err(ExecuteResult::from_exit_code(1)) + } + RedirectOp::Output(_op) => match fd { + 1 => Ok(RedirectPipe::Output(stdout.clone())), + 2 => Ok(RedirectPipe::Output(stderr.clone())), + _ => { + let _ = stderr.write_line( "deno_task_shell: output redirecting file descriptors beyond stdout and stderr is not implemented", ); - Err(ExecuteResult::from_exit_code(1)) - } - }, - }, - } + Err(ExecuteResult::from_exit_code(1)) + } + }, + }, + } } async fn resolve_redirect_word_pipe( - word: Word, - redirect_op: &RedirectOp, - state: &ShellState, - stdin: &ShellPipeReader, - stderr: &mut ShellPipeWriter, + word: Word, + redirect_op: &RedirectOp, + state: &ShellState, + stdin: &ShellPipeReader, + stderr: &mut ShellPipeWriter, ) -> Result { - fn handle_std_result( - output_path: &Path, - std_file_result: std::io::Result, - stderr: &mut ShellPipeWriter, - ) -> Result { - match std_file_result { - Ok(std_file) => Ok(std_file), - Err(err) => { - let _ = stderr.write_line(&format!( - "error opening file for redirect ({}). {:#}", - output_path.display(), - err - )); - Err(ExecuteResult::from_exit_code(1)) - } - } - } - - let words = evaluate_word_parts( - word.into_parts(), - state, - stdin.clone(), - stderr.clone(), - ) - .await; - let words = match words { - Ok(word) => word, - Err(err) => { - return Err(err.into_exit_code(stderr)); - } - }; - // edge case that's not supported - if words.is_empty() { - let _ = - stderr.write_line("redirect path must be 1 argument, but found 0"); - return Err(ExecuteResult::from_exit_code(1)); - } else if words.len() > 1 { + fn handle_std_result( + output_path: &Path, + std_file_result: std::io::Result, + stderr: &mut ShellPipeWriter, + ) -> Result { + match std_file_result { + Ok(std_file) => Ok(std_file), + Err(err) => { let _ = stderr.write_line(&format!( - concat!( - "redirect path must be 1 argument, but found {0} ({1}). ", - "Did you mean to quote it (ex. \"{1}\")?" - ), - words.len(), - words.join(" ") + "error opening file for redirect ({}). {:#}", + output_path.display(), + err )); - return Err(ExecuteResult::from_exit_code(1)); + Err(ExecuteResult::from_exit_code(1)) + } } - let output_path = &words[0]; - - match &redirect_op { - RedirectOp::Input(RedirectOpInput::Redirect) => { - let output_path = state.cwd().join(output_path); - let std_file_result = - std::fs::OpenOptions::new().read(true).open(&output_path); - handle_std_result(&output_path, std_file_result, stderr).map( - |std_file| { - RedirectPipe::Input(ShellPipeReader::from_std(std_file)) - }, - ) - } - RedirectOp::Output(op) => { - // cross platform suppress output - if output_path == "/dev/null" { - return Ok(RedirectPipe::Output(ShellPipeWriter::null())); - } - let output_path = state.cwd().join(output_path); - let is_append = *op == RedirectOpOutput::Append; - let std_file_result = std::fs::OpenOptions::new() - .write(true) - .create(true) - .append(is_append) - .truncate(!is_append) - .open(&output_path); - handle_std_result(&output_path, std_file_result, stderr).map( - |std_file| { - RedirectPipe::Output(ShellPipeWriter::from_std(std_file)) - }, - ) - } + } + + let words = evaluate_word_parts( + word.into_parts(), + state, + stdin.clone(), + stderr.clone(), + ) + .await; + let words = match words { + Ok(word) => word, + Err(err) => { + return Err(err.into_exit_code(stderr)); + } + }; + // edge case that's not supported + if words.is_empty() { + let _ = stderr.write_line("redirect path must be 1 argument, but found 0"); + return Err(ExecuteResult::from_exit_code(1)); + } else if words.len() > 1 { + let _ = stderr.write_line(&format!( + concat!( + "redirect path must be 1 argument, but found {0} ({1}). ", + "Did you mean to quote it (ex. \"{1}\")?" + ), + words.len(), + words.join(" ") + )); + return Err(ExecuteResult::from_exit_code(1)); + } + let output_path = &words[0]; + + match &redirect_op { + RedirectOp::Input(RedirectOpInput::Redirect) => { + let output_path = state.cwd().join(output_path); + let std_file_result = + std::fs::OpenOptions::new().read(true).open(&output_path); + handle_std_result(&output_path, std_file_result, stderr).map(|std_file| { + RedirectPipe::Input(ShellPipeReader::from_std(std_file)) + }) + } + RedirectOp::Output(op) => { + // cross platform suppress output + if output_path == "/dev/null" { + return Ok(RedirectPipe::Output(ShellPipeWriter::null())); + } + let output_path = state.cwd().join(output_path); + let is_append = *op == RedirectOpOutput::Append; + let std_file_result = std::fs::OpenOptions::new() + .write(true) + .create(true) + .append(is_append) + .truncate(!is_append) + .open(&output_path); + handle_std_result(&output_path, std_file_result, stderr).map(|std_file| { + RedirectPipe::Output(ShellPipeWriter::from_std(std_file)) + }) } + } } async fn execute_command( - command: Command, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + command: Command, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - let (stdin, stdout, stderr) = if let Some(redirect) = &command.redirect { - let pipe = match resolve_redirect_pipe( - redirect, - &state, - &stdin, - &stdout, - &mut stderr, - ) - .await - { - Ok(value) => value, - Err(value) => return value, - }; - match pipe { - RedirectPipe::Input(pipe) => match redirect.maybe_fd { - Some(_) => { - let _ = stderr.write_line( + let (stdin, stdout, stderr) = if let Some(redirect) = &command.redirect { + let pipe = match resolve_redirect_pipe( + redirect, + &state, + &stdin, + &stdout, + &mut stderr, + ) + .await + { + Ok(value) => value, + Err(value) => return value, + }; + match pipe { + RedirectPipe::Input(pipe) => match redirect.maybe_fd { + Some(_) => { + let _ = stderr.write_line( "input redirects with file descriptors are not supported", ); - return ExecuteResult::from_exit_code(1); - } - None => (pipe, stdout, stderr), - }, - RedirectPipe::Output(pipe) => match redirect.maybe_fd { - Some(RedirectFd::Fd(2)) => (stdin, stdout, pipe), - Some(RedirectFd::Fd(1)) | None => (stdin, pipe, stderr), - Some(RedirectFd::Fd(_)) => { - let _ = stderr.write_line( + return ExecuteResult::from_exit_code(1); + } + None => (pipe, stdout, stderr), + }, + RedirectPipe::Output(pipe) => match redirect.maybe_fd { + Some(RedirectFd::Fd(2)) => (stdin, stdout, pipe), + Some(RedirectFd::Fd(1)) | None => (stdin, pipe, stderr), + Some(RedirectFd::Fd(_)) => { + let _ = stderr.write_line( "only redirecting to stdout (1) and stderr (2) is supported", ); - return ExecuteResult::from_exit_code(1); - } - Some(RedirectFd::StdoutStderr) => (stdin, pipe.clone(), pipe), - }, - } - } else { - (stdin, stdout, stderr) - }; - match command.inner { - CommandInner::Simple(command) => { - execute_simple_command(command, state, stdin, stdout, stderr).await - } - CommandInner::Subshell(list) => { - execute_subshell(list, state, stdin, stdout, stderr).await + return ExecuteResult::from_exit_code(1); } + Some(RedirectFd::StdoutStderr) => (stdin, pipe.clone(), pipe), + }, } + } else { + (stdin, stdout, stderr) + }; + match command.inner { + CommandInner::Simple(command) => { + execute_simple_command(command, state, stdin, stdout, stderr).await + } + CommandInner::Subshell(list) => { + execute_subshell(list, state, stdin, stdout, stderr).await + } + } } async fn execute_pipe_sequence( - pipe_sequence: PipeSequence, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + pipe_sequence: PipeSequence, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - let mut wait_tasks = vec![]; - let mut last_output = Some(stdin); - let mut next_inner: Option = Some(pipe_sequence.into()); - while let Some(sequence) = next_inner.take() { - let (output_reader, output_writer) = pipe(); - let (stderr, command) = match sequence { - PipelineInner::PipeSequence(pipe_sequence) => { - next_inner = Some(pipe_sequence.next); - ( - match pipe_sequence.op { - PipeSequenceOperator::Stdout => stderr.clone(), - PipeSequenceOperator::StdoutStderr => { - output_writer.clone() - } - }, - pipe_sequence.current, - ) - } - PipelineInner::Command(command) => (stderr.clone(), command), - }; - wait_tasks.push(execute_command( - command, - state.clone(), - last_output.take().unwrap(), - output_writer.clone(), - stderr.clone(), - )); - last_output = Some(output_reader); + let mut wait_tasks = vec![]; + let mut last_output = Some(stdin); + let mut next_inner: Option = Some(pipe_sequence.into()); + while let Some(sequence) = next_inner.take() { + let (output_reader, output_writer) = pipe(); + let (stderr, command) = match sequence { + PipelineInner::PipeSequence(pipe_sequence) => { + next_inner = Some(pipe_sequence.next); + ( + match pipe_sequence.op { + PipeSequenceOperator::Stdout => stderr.clone(), + PipeSequenceOperator::StdoutStderr => output_writer.clone(), + }, + pipe_sequence.current, + ) + } + PipelineInner::Command(command) => (stderr.clone(), command), + }; + wait_tasks.push(execute_command( + command, + state.clone(), + last_output.take().unwrap(), + output_writer.clone(), + stderr.clone(), + )); + last_output = Some(output_reader); + } + let output_handle = tokio::task::spawn_blocking(|| { + last_output.unwrap().pipe_to_sender(stdout).unwrap(); + }); + let mut results = futures::future::join_all(wait_tasks).await; + output_handle.await.unwrap(); + let last_result = results.pop().unwrap(); + let all_handles = results.into_iter().flat_map(|r| r.into_handles()); + match last_result { + ExecuteResult::Exit(code, mut handles) => { + handles.extend(all_handles); + ExecuteResult::Continue(code, Vec::new(), handles) } - let output_handle = tokio::task::spawn_blocking(|| { - last_output.unwrap().pipe_to_sender(stdout).unwrap(); - }); - let mut results = futures::future::join_all(wait_tasks).await; - output_handle.await.unwrap(); - let last_result = results.pop().unwrap(); - let all_handles = results.into_iter().flat_map(|r| r.into_handles()); - match last_result { - ExecuteResult::Exit(code, mut handles) => { - handles.extend(all_handles); - ExecuteResult::Continue(code, Vec::new(), handles) - } - ExecuteResult::Continue(code, _, mut handles) => { - handles.extend(all_handles); - ExecuteResult::Continue(code, Vec::new(), handles) - } + ExecuteResult::Continue(code, _, mut handles) => { + handles.extend(all_handles); + ExecuteResult::Continue(code, Vec::new(), handles) } + } } async fn execute_subshell( - list: Box, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - stderr: ShellPipeWriter, + list: Box, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + stderr: ShellPipeWriter, ) -> ExecuteResult { - let result = execute_sequential_list( - *list, - state, - stdin, - stdout, - stderr, - // yield async commands to the parent - AsyncCommandBehavior::Yield, - ) - .await; - - match result { - ExecuteResult::Exit(code, handles) => { - // sub shells do not cause an exit - ExecuteResult::Continue(code, Vec::new(), handles) - } - ExecuteResult::Continue(code, _env_changes, handles) => { - // env changes are not propagated - ExecuteResult::Continue(code, Vec::new(), handles) - } + let result = execute_sequential_list( + *list, + state, + stdin, + stdout, + stderr, + // yield async commands to the parent + AsyncCommandBehavior::Yield, + ) + .await; + + match result { + ExecuteResult::Exit(code, handles) => { + // sub shells do not cause an exit + ExecuteResult::Continue(code, Vec::new(), handles) } + ExecuteResult::Continue(code, _env_changes, handles) => { + // env changes are not propagated + ExecuteResult::Continue(code, Vec::new(), handles) + } + } } async fn execute_simple_command( - command: SimpleCommand, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + command: SimpleCommand, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> ExecuteResult { - let args = - evaluate_args(command.args, &state, stdin.clone(), stderr.clone()) - .await; - let args = match args { - Ok(args) => args, - Err(err) => { - return err.into_exit_code(&mut stderr); - } - }; - let mut state = state.clone(); - for env_var in command.env_vars { - let value = - evaluate_word(env_var.value, &state, stdin.clone(), stderr.clone()) - .await; - let value = match value { - Ok(value) => value, - Err(err) => { - return err.into_exit_code(&mut stderr); - } - }; - state.apply_env_var(&env_var.name, &value); + let args = + evaluate_args(command.args, &state, stdin.clone(), stderr.clone()).await; + let args = match args { + Ok(args) => args, + Err(err) => { + return err.into_exit_code(&mut stderr); } - execute_command_args(args, state, stdin, stdout, stderr).await + }; + let mut state = state.clone(); + for env_var in command.env_vars { + let value = + evaluate_word(env_var.value, &state, stdin.clone(), stderr.clone()).await; + let value = match value { + Ok(value) => value, + Err(err) => { + return err.into_exit_code(&mut stderr); + } + }; + state.apply_env_var(&env_var.name, &value); + } + execute_command_args(args, state, stdin, stdout, stderr).await } fn execute_command_args( - mut args: Vec, - state: ShellState, - stdin: ShellPipeReader, - stdout: ShellPipeWriter, - mut stderr: ShellPipeWriter, + mut args: Vec, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, ) -> FutureExecuteResult { - let command_name = if args.is_empty() { - String::new() - } else { - args.remove(0) - }; - if state.token().is_cancelled() { - Box::pin(future::ready(ExecuteResult::for_cancellation())) - } else if let Some(stripped_name) = command_name.strip_prefix('!') { - let _ = stderr.write_line( + let command_name = if args.is_empty() { + String::new() + } else { + args.remove(0) + }; + if state.token().is_cancelled() { + Box::pin(future::ready(ExecuteResult::for_cancellation())) + } else if let Some(stripped_name) = command_name.strip_prefix('!') { + let _ = stderr.write_line( &format!(concat!( "History expansion is not supported:\n", " {}\n", @@ -701,347 +655,341 @@ fn execute_command_args( " ! {}", ), command_name, stripped_name) ); - Box::pin(future::ready(ExecuteResult::from_exit_code(1))) - } else { - let command_context = ShellCommandContext { - args, - state, - stdin, - stdout, - stderr, - execute_command_args: Box::new(move |context| { - execute_command_args( - context.args, - context.state, - context.stdin, - context.stdout, - context.stderr, - ) - }), - }; - match command_context.state.resolve_custom_command(&command_name) { - Some(command) => command.execute(command_context), - None => execute_unresolved_command_name( - UnresolvedCommandName { - name: command_name, - base_dir: command_context.state.cwd().to_path_buf(), - }, - command_context, - ), - } + Box::pin(future::ready(ExecuteResult::from_exit_code(1))) + } else { + let command_context = ShellCommandContext { + args, + state, + stdin, + stdout, + stderr, + execute_command_args: Box::new(move |context| { + execute_command_args( + context.args, + context.state, + context.stdin, + context.stdout, + context.stderr, + ) + }), + }; + match command_context.state.resolve_custom_command(&command_name) { + Some(command) => command.execute(command_context), + None => execute_unresolved_command_name( + UnresolvedCommandName { + name: command_name, + base_dir: command_context.state.cwd().to_path_buf(), + }, + command_context, + ), } + } } pub async fn evaluate_args( - args: Vec, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + args: Vec, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> Result, EvaluateWordTextError> { - let mut result = Vec::new(); - for arg in args { - let parts = evaluate_word_parts( - arg.into_parts(), - state, - stdin.clone(), - stderr.clone(), - ) - .await?; - result.extend(parts); - } - Ok(result) + let mut result = Vec::new(); + for arg in args { + let parts = evaluate_word_parts( + arg.into_parts(), + state, + stdin.clone(), + stderr.clone(), + ) + .await?; + result.extend(parts); + } + Ok(result) } async fn evaluate_word( - word: Word, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + word: Word, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> Result { - Ok(evaluate_word_parts(word.into_parts(), state, stdin, stderr) - .await? - .join(" ")) + Ok( + evaluate_word_parts(word.into_parts(), state, stdin, stderr) + .await? + .join(" "), + ) } #[derive(Debug, Error)] pub enum EvaluateWordTextError { - #[error("glob: no matches found '{}'. {}", pattern, err)] - InvalidPattern { - pattern: String, - err: glob::PatternError, - }, - #[error("glob: no matches found '{}'", pattern)] - NoFilesMatched { pattern: String }, + #[error("glob: no matches found '{}'. {}", pattern, err)] + InvalidPattern { + pattern: String, + err: glob::PatternError, + }, + #[error("glob: no matches found '{}'", pattern)] + NoFilesMatched { pattern: String }, } impl EvaluateWordTextError { - pub fn into_exit_code(self, stderr: &mut ShellPipeWriter) -> ExecuteResult { - let _ = stderr.write_line(&self.to_string()); - ExecuteResult::from_exit_code(1) - } + pub fn into_exit_code(self, stderr: &mut ShellPipeWriter) -> ExecuteResult { + let _ = stderr.write_line(&self.to_string()); + ExecuteResult::from_exit_code(1) + } } fn evaluate_word_parts( - parts: Vec, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + parts: Vec, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> LocalBoxFuture, EvaluateWordTextError>> { - #[derive(Debug)] - enum TextPart { - Quoted(String), - Text(String), + #[derive(Debug)] + enum TextPart { + Quoted(String), + Text(String), + } + + impl TextPart { + pub fn as_str(&self) -> &str { + match self { + TextPart::Quoted(text) => text, + TextPart::Text(text) => text, + } } + } - impl TextPart { - pub fn as_str(&self) -> &str { - match self { - TextPart::Quoted(text) => text, - TextPart::Text(text) => text, - } - } + fn text_parts_to_string(parts: Vec) -> String { + let mut result = + String::with_capacity(parts.iter().map(|p| p.as_str().len()).sum()); + for part in parts { + result.push_str(part.as_str()); } + result + } - fn text_parts_to_string(parts: Vec) -> String { - let mut result = - String::with_capacity(parts.iter().map(|p| p.as_str().len()).sum()); - for part in parts { - result.push_str(part.as_str()); - } - result - } - - fn evaluate_word_text( - state: &ShellState, - text_parts: Vec, - is_quoted: bool, - ) -> Result, EvaluateWordTextError> { - if !is_quoted - && text_parts - .iter() - .filter_map(|p| match p { - TextPart::Quoted(_) => None, - TextPart::Text(text) => Some(text.as_str()), - }) - .any(|text| text.chars().any(|c| matches!(c, '?' | '*' | '['))) - { - let mut current_text = String::new(); - for text_part in text_parts { - match text_part { - TextPart::Quoted(text) => { - for c in text.chars() { - match c { - '?' | '*' | '[' | ']' => { - // escape because it was quoted - current_text.push('['); - current_text.push(c); - current_text.push(']'); - } - _ => current_text.push(c), - } - } - } - TextPart::Text(text) => { - current_text.push_str(&text); - } + fn evaluate_word_text( + state: &ShellState, + text_parts: Vec, + is_quoted: bool, + ) -> Result, EvaluateWordTextError> { + if !is_quoted + && text_parts + .iter() + .filter_map(|p| match p { + TextPart::Quoted(_) => None, + TextPart::Text(text) => Some(text.as_str()), + }) + .any(|text| text.chars().any(|c| matches!(c, '?' | '*' | '['))) + { + let mut current_text = String::new(); + for text_part in text_parts { + match text_part { + TextPart::Quoted(text) => { + for c in text.chars() { + match c { + '?' | '*' | '[' | ']' => { + // escape because it was quoted + current_text.push('['); + current_text.push(c); + current_text.push(']'); } + _ => current_text.push(c), + } } - let is_absolute = - std::path::PathBuf::from(¤t_text).is_absolute(); - let cwd = state.cwd(); - let pattern = if is_absolute { - current_text + } + TextPart::Text(text) => { + current_text.push_str(&text); + } + } + } + let is_absolute = std::path::PathBuf::from(¤t_text).is_absolute(); + let cwd = state.cwd(); + let pattern = if is_absolute { + current_text + } else { + format!("{}/{}", cwd.display(), current_text) + }; + let result = glob::glob_with( + &pattern, + glob::MatchOptions { + // false because it should work the same way on case insensitive file systems + case_sensitive: false, + // true because it copies what sh does + require_literal_separator: true, + // true because it copies with sh does—these files are considered "hidden" + require_literal_leading_dot: true, + }, + ); + match result { + Ok(paths) => { + let paths = + paths.into_iter().filter_map(|p| p.ok()).collect::>(); + if paths.is_empty() { + Err(EvaluateWordTextError::NoFilesMatched { pattern }) + } else { + let paths = if is_absolute { + paths + .into_iter() + .map(|p| p.display().to_string()) + .collect::>() } else { - format!("{}/{}", cwd.display(), current_text) + paths + .into_iter() + .map(|p| { + let path = p.strip_prefix(cwd).unwrap(); + path.display().to_string() + }) + .collect::>() }; - let result = glob::glob_with( - &pattern, - glob::MatchOptions { - // false because it should work the same way on case insensitive file systems - case_sensitive: false, - // true because it copies what sh does - require_literal_separator: true, - // true because it copies with sh does—these files are considered "hidden" - require_literal_leading_dot: true, - }, - ); - match result { - Ok(paths) => { - let paths = paths - .into_iter() - .filter_map(|p| p.ok()) - .collect::>(); - if paths.is_empty() { - Err(EvaluateWordTextError::NoFilesMatched { pattern }) - } else { - let paths = if is_absolute { - paths - .into_iter() - .map(|p| p.display().to_string()) - .collect::>() - } else { - paths - .into_iter() - .map(|p| { - let path = p.strip_prefix(cwd).unwrap(); - path.display().to_string() - }) - .collect::>() - }; - Ok(paths) - } - } - Err(err) => { - Err(EvaluateWordTextError::InvalidPattern { pattern, err }) - } - } - } else { - Ok(vec![text_parts_to_string(text_parts)]) + Ok(paths) + } } + Err(err) => Err(EvaluateWordTextError::InvalidPattern { pattern, err }), + } + } else { + Ok(vec![text_parts_to_string(text_parts)]) } + } - fn evaluate_word_parts_inner( - parts: Vec, - is_quoted: bool, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, - ) -> LocalBoxFuture, EvaluateWordTextError>> { - // recursive async, so requires boxing - async move { - let mut result = Vec::new(); - let mut current_text = Vec::new(); - for part in parts { - let evaluation_result_text = match part { - WordPart::Text(text) => { - current_text.push(TextPart::Text(text)); - None - } - WordPart::Variable(name) => { - state.get_var(&name).map(|v| v.to_string()) - } - WordPart::Command(list) => Some( - evaluate_command_substitution( - list, - // contain cancellation to the command substitution - &state.with_child_token(), - stdin.clone(), - stderr.clone(), - ) - .await, - ), - WordPart::Quoted(parts) => { - let text = evaluate_word_parts_inner( - parts, - true, - state, - stdin.clone(), - stderr.clone(), - ) - .await? - .join(" "); - - current_text.push(TextPart::Quoted(text)); - continue; - } - }; - - // This text needs to be turned into a vector of strings. - // For now we do a very basic string split on whitespace, but in the future - // we should continue to improve this functionality. - if let Some(text) = evaluation_result_text { - let mut parts = text - .split(' ') - .map(|p| p.trim()) - .filter(|p| !p.is_empty()) - .map(|p| TextPart::Text(p.to_string())) - .collect::>(); - - if !parts.is_empty() { - // append the first part to the current text - let first_part = parts.remove(0); - current_text.push(first_part); - - if !parts.is_empty() { - // evaluate and store the current text - result.extend(evaluate_word_text( - state, - current_text, - is_quoted, - )?); + fn evaluate_word_parts_inner( + parts: Vec, + is_quoted: bool, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, + ) -> LocalBoxFuture, EvaluateWordTextError>> { + // recursive async, so requires boxing + async move { + let mut result = Vec::new(); + let mut current_text = Vec::new(); + for part in parts { + let evaluation_result_text = match part { + WordPart::Text(text) => { + current_text.push(TextPart::Text(text)); + None + } + WordPart::Variable(name) => { + state.get_var(&name).map(|v| v.to_string()) + } + WordPart::Command(list) => Some( + evaluate_command_substitution( + list, + // contain cancellation to the command substitution + &state.with_child_token(), + stdin.clone(), + stderr.clone(), + ) + .await, + ), + WordPart::Quoted(parts) => { + let text = evaluate_word_parts_inner( + parts, + true, + state, + stdin.clone(), + stderr.clone(), + ) + .await? + .join(" "); - // store all the parts except the last one - for part in parts.drain(..parts.len() - 1) { - result.extend(evaluate_word_text( - state, - vec![part], - is_quoted, - )?); - } + current_text.push(TextPart::Quoted(text)); + continue; + } + }; - // use the last part as the current text so it maybe - // gets appended to in the future - current_text = parts; - } - } - } - } - if !current_text.is_empty() { + // This text needs to be turned into a vector of strings. + // For now we do a very basic string split on whitespace, but in the future + // we should continue to improve this functionality. + if let Some(text) = evaluation_result_text { + let mut parts = text + .split(' ') + .map(|p| p.trim()) + .filter(|p| !p.is_empty()) + .map(|p| TextPart::Text(p.to_string())) + .collect::>(); + + if !parts.is_empty() { + // append the first part to the current text + let first_part = parts.remove(0); + current_text.push(first_part); + + if !parts.is_empty() { + // evaluate and store the current text + result.extend(evaluate_word_text( + state, + current_text, + is_quoted, + )?); + + // store all the parts except the last one + for part in parts.drain(..parts.len() - 1) { result.extend(evaluate_word_text( - state, - current_text, - is_quoted, + state, + vec![part], + is_quoted, )?); + } + + // use the last part as the current text so it maybe + // gets appended to in the future + current_text = parts; } - Ok(result) + } } - .boxed_local() + } + if !current_text.is_empty() { + result.extend(evaluate_word_text(state, current_text, is_quoted)?); + } + Ok(result) } + .boxed_local() + } - evaluate_word_parts_inner(parts, false, state, stdin, stderr) + evaluate_word_parts_inner(parts, false, state, stdin, stderr) } async fn evaluate_command_substitution( - list: SequentialList, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, + list: SequentialList, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, ) -> String { - let text = execute_with_stdout_as_text(|shell_stdout_writer| { - execute_sequential_list( - list, - state.clone(), - stdin, - shell_stdout_writer, - stderr, - AsyncCommandBehavior::Wait, - ) - }) - .await; - - // Remove the trailing newline and then replace inner newlines with a space - // This seems to be what sh does, but I'm not entirely sure: - // - // > echo $(echo 1 && echo -e "\n2\n") - // 1 2 - text.strip_suffix("\r\n") - .or_else(|| text.strip_suffix('\n')) - .unwrap_or(&text) - .replace("\r\n", " ") - .replace('\n', " ") + let text = execute_with_stdout_as_text(|shell_stdout_writer| { + execute_sequential_list( + list, + state.clone(), + stdin, + shell_stdout_writer, + stderr, + AsyncCommandBehavior::Wait, + ) + }) + .await; + + // Remove the trailing newline and then replace inner newlines with a space + // This seems to be what sh does, but I'm not entirely sure: + // + // > echo $(echo 1 && echo -e "\n2\n") + // 1 2 + text + .strip_suffix("\r\n") + .or_else(|| text.strip_suffix('\n')) + .unwrap_or(&text) + .replace("\r\n", " ") + .replace('\n', " ") } async fn execute_with_stdout_as_text( - execute: impl FnOnce(ShellPipeWriter) -> FutureExecuteResult, + execute: impl FnOnce(ShellPipeWriter) -> FutureExecuteResult, ) -> String { - let (shell_stdout_reader, shell_stdout_writer) = pipe(); - let spawned_output = execute(shell_stdout_writer); - let output_handle = tokio::task::spawn_blocking(move || { - let mut final_data = Vec::new(); - shell_stdout_reader.pipe_to(&mut final_data).unwrap(); - final_data - }); - let _ = spawned_output.await; - let data = output_handle.await.unwrap(); - String::from_utf8_lossy(&data).to_string() + let (shell_stdout_reader, shell_stdout_writer) = pipe(); + let spawned_output = execute(shell_stdout_writer); + let output_handle = tokio::task::spawn_blocking(move || { + let mut final_data = Vec::new(); + shell_stdout_reader.pipe_to(&mut final_data).unwrap(); + final_data + }); + let _ = spawned_output.await; + let data = output_handle.await.unwrap(); + String::from_utf8_lossy(&data).to_string() } diff --git a/crates/deno_task_shell/src/shell/fs_util.rs b/crates/deno_task_shell/src/shell/fs_util.rs index 03c79a1..043b81f 100644 --- a/crates/deno_task_shell/src/shell/fs_util.rs +++ b/crates/deno_task_shell/src/shell/fs_util.rs @@ -7,11 +7,11 @@ use anyhow::Result; /// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. pub fn canonicalize_path(path: &Path) -> Result { - let path = path.canonicalize()?; - #[cfg(windows)] - return Ok(strip_unc_prefix(path)); - #[cfg(not(windows))] - return Ok(path); + let path = path.canonicalize()?; + #[cfg(windows)] + return Ok(strip_unc_prefix(path)); + #[cfg(not(windows))] + return Ok(path); } // todo(dsherret): This function was copy and pasted from deno @@ -20,50 +20,41 @@ pub fn canonicalize_path(path: &Path) -> Result { #[cfg(windows)] fn strip_unc_prefix(path: PathBuf) -> PathBuf { - use std::path::Component; - use std::path::Prefix; + use std::path::Component; + use std::path::Prefix; - let mut components = path.components(); - match components.next() { - Some(Component::Prefix(prefix)) => { - match prefix.kind() { - // \\?\device - Prefix::Verbatim(device) => { - let mut path = PathBuf::new(); - path.push(format!(r"\\{}\", device.to_string_lossy())); - path.extend( - components.filter(|c| !matches!(c, Component::RootDir)), - ); - path - } - // \\?\c:\path - Prefix::VerbatimDisk(_) => { - let mut path = PathBuf::new(); - path.push( - prefix - .as_os_str() - .to_string_lossy() - .replace(r"\\?\", ""), - ); - path.extend(components); - path - } - // \\?\UNC\hostname\share_name\path - Prefix::VerbatimUNC(hostname, share_name) => { - let mut path = PathBuf::new(); - path.push(format!( - r"\\{}\{}\", - hostname.to_string_lossy(), - share_name.to_string_lossy() - )); - path.extend( - components.filter(|c| !matches!(c, Component::RootDir)), - ); - path - } - _ => path, - } + let mut components = path.components(); + match components.next() { + Some(Component::Prefix(prefix)) => { + match prefix.kind() { + // \\?\device + Prefix::Verbatim(device) => { + let mut path = PathBuf::new(); + path.push(format!(r"\\{}\", device.to_string_lossy())); + path.extend(components.filter(|c| !matches!(c, Component::RootDir))); + path + } + // \\?\c:\path + Prefix::VerbatimDisk(_) => { + let mut path = PathBuf::new(); + path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); + path.extend(components); + path + } + // \\?\UNC\hostname\share_name\path + Prefix::VerbatimUNC(hostname, share_name) => { + let mut path = PathBuf::new(); + path.push(format!( + r"\\{}\{}\", + hostname.to_string_lossy(), + share_name.to_string_lossy() + )); + path.extend(components.filter(|c| !matches!(c, Component::RootDir))); + path } _ => path, + } } + _ => path, + } } diff --git a/crates/deno_task_shell/src/shell/test.rs b/crates/deno_task_shell/src/shell/test.rs index d46c36e..64d05fa 100644 --- a/crates/deno_task_shell/src/shell/test.rs +++ b/crates/deno_task_shell/src/shell/test.rs @@ -9,49 +9,49 @@ const FOLDER_SEPERATOR: char = if cfg!(windows) { '\\' } else { '/' }; #[tokio::test] async fn commands() { - TestBuilder::new() - .command("echo 1") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("echo 1 2 3") - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo "1 2 3""#) - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r"echo 1 2\ \ \ 3") - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo "1 2\ \ \ 3""#) - .assert_stdout("1 2\\ \\ \\ 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo test$(echo "1 2")"#) - .assert_stdout("test1 2\n") - .run() - .await; - - TestBuilder::new() - .command(r#"TEST="1 2" ; echo $TEST"#) - .assert_stdout("1 2\n") - .run() - .await; - - TestBuilder::new() + TestBuilder::new() + .command("echo 1") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("echo 1 2 3") + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "1 2 3""#) + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r"echo 1 2\ \ \ 3") + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "1 2\ \ \ 3""#) + .assert_stdout("1 2\\ \\ \\ 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo test$(echo "1 2")"#) + .assert_stdout("test1 2\n") + .run() + .await; + + TestBuilder::new() + .command(r#"TEST="1 2" ; echo $TEST"#) + .assert_stdout("1 2\n") + .run() + .await; + + TestBuilder::new() .command( r#"VAR=1 deno eval 'console.log(Deno.env.get("VAR"))' && echo $VAR"#, ) @@ -59,13 +59,13 @@ async fn commands() { .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"VAR=1 VAR2=2 deno eval 'console.log(Deno.env.get("VAR") + Deno.env.get("VAR2"))'"#) .assert_stdout("12\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command( r#"EMPTY= deno eval 'console.log(`EMPTY: ${Deno.env.get("EMPTY")}`)'"#, ) @@ -73,154 +73,154 @@ async fn commands() { .run() .await; - TestBuilder::new() - .command(r#""echo" "1""#) - .assert_stdout("1\n") - .run() - .await; + TestBuilder::new() + .command(r#""echo" "1""#) + .assert_stdout("1\n") + .run() + .await; - TestBuilder::new() - .command(r#""echo" "*""#) - .assert_stdout("*\n") - .run() - .await; + TestBuilder::new() + .command(r#""echo" "*""#) + .assert_stdout("*\n") + .run() + .await; - TestBuilder::new() - .command("echo test-dashes") - .assert_stdout("test-dashes\n") - .run() - .await; + TestBuilder::new() + .command("echo test-dashes") + .assert_stdout("test-dashes\n") + .run() + .await; - TestBuilder::new() - .command("echo 'a/b'/c") - .assert_stdout("a/b/c\n") - .run() - .await; + TestBuilder::new() + .command("echo 'a/b'/c") + .assert_stdout("a/b/c\n") + .run() + .await; - TestBuilder::new() - .command("echo 'a/b'ctest\"te st\"'asdf'") - .assert_stdout("a/bctestte stasdf\n") - .run() - .await; + TestBuilder::new() + .command("echo 'a/b'ctest\"te st\"'asdf'") + .assert_stdout("a/bctestte stasdf\n") + .run() + .await; - TestBuilder::new() + TestBuilder::new() .command("echo --test=\"2\" --test='2' test\"TEST\" TEST'test'TEST 'test''test' test'test'\"test\" \"test\"\"test\"'test'") .assert_stdout("--test=2 --test=2 testTEST TESTtestTEST testtest testtesttest testtesttest\n") .run() .await; - TestBuilder::new() - .command("deno eval 'console.log(1)'") - .env_var("PATH", "") - .assert_stderr("deno: command not found\n") - .assert_exit_code(127) - .run() - .await; + TestBuilder::new() + .command("deno eval 'console.log(1)'") + .env_var("PATH", "") + .assert_stderr("deno: command not found\n") + .assert_exit_code(127) + .run() + .await; - TestBuilder::new().command("unset").run().await; + TestBuilder::new().command("unset").run().await; } #[tokio::test] async fn boolean_logic() { - TestBuilder::new() - .command("echo 1 && echo 2 || echo 3") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() - .command("echo 1 || echo 2 && echo 3") - .assert_stdout("1\n3\n") - .run() - .await; - - TestBuilder::new() - .command("echo 1 || (echo 2 && echo 3)") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("false || false || (echo 2 && false) || echo 3") - .assert_stdout("2\n3\n") - .run() - .await; + TestBuilder::new() + .command("echo 1 && echo 2 || echo 3") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() + .command("echo 1 || echo 2 && echo 3") + .assert_stdout("1\n3\n") + .run() + .await; + + TestBuilder::new() + .command("echo 1 || (echo 2 && echo 3)") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("false || false || (echo 2 && false) || echo 3") + .assert_stdout("2\n3\n") + .run() + .await; } #[tokio::test] async fn exit() { - TestBuilder::new() - .command("exit 1") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("exit 5") - .assert_exit_code(5) - .run() - .await; - - TestBuilder::new() - .command("exit 258 && echo 1") - .assert_exit_code(2) - .run() - .await; - - TestBuilder::new() - .command("(exit 0) && echo 1") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("(exit 1) && echo 1") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("echo 1 && (exit 1)") - .assert_stdout("1\n") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("exit ; echo 2") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() - .command("exit bad args") - .assert_stderr("exit: too many arguments\n") - .assert_exit_code(2) - .run() - .await; + TestBuilder::new() + .command("exit 1") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("exit 5") + .assert_exit_code(5) + .run() + .await; + + TestBuilder::new() + .command("exit 258 && echo 1") + .assert_exit_code(2) + .run() + .await; + + TestBuilder::new() + .command("(exit 0) && echo 1") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("(exit 1) && echo 1") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("echo 1 && (exit 1)") + .assert_stdout("1\n") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("exit ; echo 2") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() + .command("exit bad args") + .assert_stderr("exit: too many arguments\n") + .assert_exit_code(2) + .run() + .await; } #[tokio::test] async fn async_commands() { - TestBuilder::new() - .command("sleep 0.1 && echo 2 & echo 1") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() - .command("(sleep 0.1 && echo 2 &) ; echo 1") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() - .command("(sleep 0.1 && echo 2) & echo 1") - .assert_stdout("1\n2\n") - .run() - .await; - - TestBuilder::new() + TestBuilder::new() + .command("sleep 0.1 && echo 2 & echo 1") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() + .command("(sleep 0.1 && echo 2 &) ; echo 1") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() + .command("(sleep 0.1 && echo 2) & echo 1") + .assert_stdout("1\n2\n") + .run() + .await; + + TestBuilder::new() .command( "$(sleep 0.1 && echo 1 & $(sleep 0.2 && echo 2 & echo echo) & echo echo)", ) @@ -228,94 +228,94 @@ async fn async_commands() { .run() .await; - TestBuilder::new() - .command("exit 1 & exit 0") - .assert_exit_code(1) - .run() - .await; - - // should not output because the `exit 1` will cancel the sleep - TestBuilder::new() - .command("sleep 5 && echo 1 & exit 1") - .assert_exit_code(1) - .run() - .await; - - // should fail when async command exits - TestBuilder::new() - .command("exit 1 & exit 0") - .assert_exit_code(1) - .run() - .await; - - // should fail when async command fails and cancel any running command - TestBuilder::new() - .command("deno eval 'Deno.exit(1)' & sleep 5 && echo 2 & echo 1") - .assert_stdout("1\n") - .assert_exit_code(1) - .run() - .await; - - // should cancel running command - TestBuilder::new() + TestBuilder::new() + .command("exit 1 & exit 0") + .assert_exit_code(1) + .run() + .await; + + // should not output because the `exit 1` will cancel the sleep + TestBuilder::new() + .command("sleep 5 && echo 1 & exit 1") + .assert_exit_code(1) + .run() + .await; + + // should fail when async command exits + TestBuilder::new() + .command("exit 1 & exit 0") + .assert_exit_code(1) + .run() + .await; + + // should fail when async command fails and cancel any running command + TestBuilder::new() + .command("deno eval 'Deno.exit(1)' & sleep 5 && echo 2 & echo 1") + .assert_stdout("1\n") + .assert_exit_code(1) + .run() + .await; + + // should cancel running command + TestBuilder::new() .command("sleep 10 & sleep 0.5 && deno eval 'Deno.exit(2)' & deno eval 'console.log(1); setTimeout(() => { console.log(3) }, 10_000);'") .assert_stdout("1\n") .assert_exit_code(2) .run() .await; - // should be able to opt out by doing an `|| exit 0` - TestBuilder::new() - .command("deno eval 'Deno.exit(1)' || exit 0 & echo 1") - .assert_stdout("1\n") - .assert_exit_code(0) - .run() - .await; + // should be able to opt out by doing an `|| exit 0` + TestBuilder::new() + .command("deno eval 'Deno.exit(1)' || exit 0 & echo 1") + .assert_stdout("1\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] async fn command_substition() { - TestBuilder::new() - .command("echo $(echo 1)") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("echo $(echo 1 && echo 2)") - .assert_stdout("1 2\n") - .run() - .await; - - // async inside subshell should wait - TestBuilder::new() - .command("$(sleep 0.1 && echo 1 & echo echo) 2") - .assert_stdout("1 2\n") - .run() - .await; - TestBuilder::new() - .command("$(sleep 0.1 && echo 1 && exit 5 &) ; echo 2") - .assert_stdout("2\n") - .assert_stderr("1: command not found\n") - .run() - .await; + TestBuilder::new() + .command("echo $(echo 1)") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("echo $(echo 1 && echo 2)") + .assert_stdout("1 2\n") + .run() + .await; + + // async inside subshell should wait + TestBuilder::new() + .command("$(sleep 0.1 && echo 1 & echo echo) 2") + .assert_stdout("1 2\n") + .run() + .await; + TestBuilder::new() + .command("$(sleep 0.1 && echo 1 && exit 5 &) ; echo 2") + .assert_stdout("2\n") + .assert_stderr("1: command not found\n") + .run() + .await; } #[tokio::test] async fn shell_variables() { - TestBuilder::new() + TestBuilder::new() .command(r#"echo $VAR && VAR=1 && echo $VAR && deno eval 'console.log(Deno.env.get("VAR"))'"#) .assert_stdout("\n1\nundefined\n") .run() .await; - TestBuilder::new() - .command(r#"VAR=1 && echo $VAR$VAR"#) - .assert_stdout("11\n") - .run() - .await; + TestBuilder::new() + .command(r#"VAR=1 && echo $VAR$VAR"#) + .assert_stdout("11\n") + .run() + .await; - TestBuilder::new() + TestBuilder::new() .command(r#"VAR=1 && echo Test$VAR && echo $(echo "Test: $VAR") ; echo CommandSub$($VAR); echo $ ; echo \$VAR"#) .assert_stdout("Test1\nTest: 1\nCommandSub\n$\n$VAR\n") .assert_stderr("1: command not found\n") @@ -325,13 +325,13 @@ async fn shell_variables() { #[tokio::test] async fn env_variables() { - TestBuilder::new() + TestBuilder::new() .command(r#"echo $VAR && export VAR=1 && echo $VAR && deno eval 'console.log(Deno.env.get("VAR"))'"#) .assert_stdout("\n1\n1\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"export VAR=1 VAR2=testing VAR3="test this out" && echo $VAR $VAR2 $VAR3"#) .assert_stdout("1 testing test this out\n") .run() @@ -340,79 +340,79 @@ async fn env_variables() { #[tokio::test] async fn exit_code_var() { - TestBuilder::new() - .command(r#"echo $? ; echo $? ; false ; echo $?"#) - .assert_stdout("\n0\n1\n") - .run() - .await; - TestBuilder::new() - .command(r#"(false || echo $?) && echo $?"#) - .assert_stdout("1\n0\n") - .run() - .await; - TestBuilder::new() - .command(r#"! false && echo $?"#) - .assert_stdout("0\n") - .run() - .await; - TestBuilder::new() - .command(r#"(deno eval 'Deno.exit(25)') || echo $?"#) - .assert_stdout("25\n") - .run() - .await; + TestBuilder::new() + .command(r#"echo $? ; echo $? ; false ; echo $?"#) + .assert_stdout("\n0\n1\n") + .run() + .await; + TestBuilder::new() + .command(r#"(false || echo $?) && echo $?"#) + .assert_stdout("1\n0\n") + .run() + .await; + TestBuilder::new() + .command(r#"! false && echo $?"#) + .assert_stdout("0\n") + .run() + .await; + TestBuilder::new() + .command(r#"(deno eval 'Deno.exit(25)') || echo $?"#) + .assert_stdout("25\n") + .run() + .await; } #[tokio::test] async fn sequential_lists() { - TestBuilder::new() - .command(r#"echo 1 ; sleep 0.1 && echo 4 & echo 2 ; echo 3;"#) - .assert_stdout("1\n2\n3\n4\n") - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 ; sleep 0.1 && echo 4 & echo 2 ; echo 3;"#) + .assert_stdout("1\n2\n3\n4\n") + .run() + .await; } #[tokio::test] async fn pipeline() { - TestBuilder::new() + TestBuilder::new() .command(r#"echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .run() .await; - TestBuilder::new() - .command(r#"echo 1 | echo 2 && echo 3"#) - .assert_stdout("2\n3\n") - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 | echo 2 && echo 3"#) + .assert_stdout("2\n3\n") + .run() + .await; - TestBuilder::new() + TestBuilder::new() .command(r#"echo $(sleep 0.1 && echo 2 & echo 1) | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1 2\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"echo 2 | echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"deno eval 'console.log(1); console.error(2);' | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .assert_stderr("2\n") .run() .await; - // stdout and stderr pipeline + // stdout and stderr pipeline - TestBuilder::new() + TestBuilder::new() .command(r#"deno eval 'console.log(1); console.error(2);' |& deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n2\n") .run() .await; - TestBuilder::new() + TestBuilder::new() // add bit of a delay while outputting stdout so that it doesn't race with stderr .command(r#"deno eval 'console.log(1); console.error(2);' | deno eval 'setTimeout(async () => { await Deno.stdin.readable.pipeTo(Deno.stderr.writable) }, 10)' |& deno eval 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable)'"#) // still outputs 2 because the first command didn't pipe stderr @@ -420,22 +420,22 @@ async fn pipeline() { .run() .await; - // |& pipeline should still pipe stdout - TestBuilder::new() + // |& pipeline should still pipe stdout + TestBuilder::new() .command(r#"echo 1 |& deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)'"#) .assert_stdout("1\n") .run() .await; - // pipeline with redirect - TestBuilder::new() + // pipeline with redirect + TestBuilder::new() .command(r#"echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stdout.writable)' > output.txt"#) .assert_file_equals("output.txt", "1\n") .run() .await; - // pipeline with stderr redirect - TestBuilder::new() + // pipeline with stderr redirect + TestBuilder::new() .command(r#"echo 1 | deno eval 'await Deno.stdin.readable.pipeTo(Deno.stderr.writable)' 2> output.txt"#) .assert_file_equals("output.txt", "1\n") .run() @@ -444,48 +444,48 @@ async fn pipeline() { #[tokio::test] async fn negated() { - TestBuilder::new() - .command(r#"! echo 1 && echo 2"#) - .assert_stdout("1\n") - .assert_exit_code(1) - .run() - .await; - TestBuilder::new() - .command(r#"! echo 1 || echo 2"#) - .assert_stdout("1\n2\n") - .run() - .await; - TestBuilder::new() - .command(r#"! (echo 1 | echo 2 && echo 3) || echo 4"#) - .assert_stdout("2\n3\n4\n") - .run() - .await; - TestBuilder::new() - .command(r#"! echo 1 | echo 2 && echo 3"#) - .assert_stdout("2\n") - .assert_exit_code(1) - .run() - .await; - TestBuilder::new() - .command(r#"! (exit 5) && echo 1"#) - .assert_stdout("1\n") - .run() - .await; - TestBuilder::new() - .command(r#"! exit 5 && echo 1"#) - .assert_exit_code(5) - .run() - .await; - TestBuilder::new() - .command(r#"! echo 1 && echo 2 &"#) - .assert_stdout("1\n") - // differing behaviour to shells, where this async command will actually fail - .assert_exit_code(1) - .run() - .await; - - // test no spaces - TestBuilder::new() + TestBuilder::new() + .command(r#"! echo 1 && echo 2"#) + .assert_stdout("1\n") + .assert_exit_code(1) + .run() + .await; + TestBuilder::new() + .command(r#"! echo 1 || echo 2"#) + .assert_stdout("1\n2\n") + .run() + .await; + TestBuilder::new() + .command(r#"! (echo 1 | echo 2 && echo 3) || echo 4"#) + .assert_stdout("2\n3\n4\n") + .run() + .await; + TestBuilder::new() + .command(r#"! echo 1 | echo 2 && echo 3"#) + .assert_stdout("2\n") + .assert_exit_code(1) + .run() + .await; + TestBuilder::new() + .command(r#"! (exit 5) && echo 1"#) + .assert_stdout("1\n") + .run() + .await; + TestBuilder::new() + .command(r#"! exit 5 && echo 1"#) + .assert_exit_code(5) + .run() + .await; + TestBuilder::new() + .command(r#"! echo 1 && echo 2 &"#) + .assert_stdout("1\n") + // differing behaviour to shells, where this async command will actually fail + .assert_exit_code(1) + .run() + .await; + + // test no spaces + TestBuilder::new() .command(r#"!echo 1 && echo 2"#) .assert_stderr("History expansion is not supported:\n !echo\n ~\n\nPerhaps you meant to add a space after the exclamation point to negate the command?\n ! echo\n") .assert_exit_code(1) @@ -495,76 +495,76 @@ async fn negated() { #[tokio::test] async fn redirects_output() { - TestBuilder::new() - .command(r#"echo 5 6 7 > test.txt"#) - .assert_file_equals("test.txt", "5 6 7\n") - .run() - .await; - - TestBuilder::new() - .command(r#"echo 1 2 3 && echo 1 > test.txt"#) - .assert_stdout("1 2 3\n") - .assert_file_equals("test.txt", "1\n") - .run() - .await; - - // subdir - TestBuilder::new() - .command(r#"mkdir subdir && cd subdir && echo 1 2 3 > test.txt"#) - .assert_file_equals("subdir/test.txt", "1 2 3\n") - .run() - .await; - - // absolute path - TestBuilder::new() - .command(r#"echo 1 2 3 > "$PWD/test.txt""#) - .assert_file_equals("test.txt", "1 2 3\n") - .run() - .await; - - // stdout - TestBuilder::new() - .command(r#"deno eval 'console.log(1); console.error(5)' 1> test.txt"#) - .assert_stderr("5\n") - .assert_file_equals("test.txt", "1\n") - .run() - .await; - - // stderr - TestBuilder::new() - .command(r#"deno eval 'console.log(1); console.error(5)' 2> test.txt"#) - .assert_stdout("1\n") - .assert_file_equals("test.txt", "5\n") - .run() - .await; - - // invalid fd - TestBuilder::new() - .command(r#"echo 2 3> test.txt"#) - .ensure_temp_dir() - .assert_stderr( - "only redirecting to stdout (1) and stderr (2) is supported\n", - ) - .assert_exit_code(1) - .run() - .await; - - // /dev/null - TestBuilder::new() - .command(r#"deno eval 'console.log(1); console.error(5)' 2> /dev/null"#) - .assert_stdout("1\n") - .run() - .await; - - // appending - TestBuilder::new() - .command(r#"echo 1 > test.txt && echo 2 >> test.txt"#) - .assert_file_equals("test.txt", "1\n2\n") - .run() - .await; - - // &> and &>> redirect - TestBuilder::new() + TestBuilder::new() + .command(r#"echo 5 6 7 > test.txt"#) + .assert_file_equals("test.txt", "5 6 7\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo 1 2 3 && echo 1 > test.txt"#) + .assert_stdout("1 2 3\n") + .assert_file_equals("test.txt", "1\n") + .run() + .await; + + // subdir + TestBuilder::new() + .command(r#"mkdir subdir && cd subdir && echo 1 2 3 > test.txt"#) + .assert_file_equals("subdir/test.txt", "1 2 3\n") + .run() + .await; + + // absolute path + TestBuilder::new() + .command(r#"echo 1 2 3 > "$PWD/test.txt""#) + .assert_file_equals("test.txt", "1 2 3\n") + .run() + .await; + + // stdout + TestBuilder::new() + .command(r#"deno eval 'console.log(1); console.error(5)' 1> test.txt"#) + .assert_stderr("5\n") + .assert_file_equals("test.txt", "1\n") + .run() + .await; + + // stderr + TestBuilder::new() + .command(r#"deno eval 'console.log(1); console.error(5)' 2> test.txt"#) + .assert_stdout("1\n") + .assert_file_equals("test.txt", "5\n") + .run() + .await; + + // invalid fd + TestBuilder::new() + .command(r#"echo 2 3> test.txt"#) + .ensure_temp_dir() + .assert_stderr( + "only redirecting to stdout (1) and stderr (2) is supported\n", + ) + .assert_exit_code(1) + .run() + .await; + + // /dev/null + TestBuilder::new() + .command(r#"deno eval 'console.log(1); console.error(5)' 2> /dev/null"#) + .assert_stdout("1\n") + .run() + .await; + + // appending + TestBuilder::new() + .command(r#"echo 1 > test.txt && echo 2 >> test.txt"#) + .assert_file_equals("test.txt", "1\n2\n") + .run() + .await; + + // &> and &>> redirect + TestBuilder::new() .command( concat!( "deno eval 'console.log(1); setTimeout(() => console.error(23), 10)' &> file.txt &&", @@ -575,26 +575,26 @@ async fn redirects_output() { .run() .await; - // multiple arguments after re-direct - TestBuilder::new() - .command(r"export TwoArgs=testing\ this && echo 1 > $TwoArgs") - .assert_stderr(concat!( + // multiple arguments after re-direct + TestBuilder::new() + .command(r"export TwoArgs=testing\ this && echo 1 > $TwoArgs") + .assert_stderr(concat!( "redirect path must be 1 argument, but found 2 ", "(testing this). Did you mean to quote it (ex. \"testing this\")?\n" )) - .assert_exit_code(1) - .run() - .await; - - // zero arguments after re-direct - TestBuilder::new() - .command(r#"echo 1 > $EMPTY"#) - .assert_stderr("redirect path must be 1 argument, but found 0\n") - .assert_exit_code(1) - .run() - .await; - - TestBuilder::new() + .assert_exit_code(1) + .run() + .await; + + // zero arguments after re-direct + TestBuilder::new() + .command(r#"echo 1 > $EMPTY"#) + .assert_stderr("redirect path must be 1 argument, but found 0\n") + .assert_exit_code(1) + .run() + .await; + + TestBuilder::new() .command(r#"echo 1 >&3"#) .assert_stderr( "deno_task_shell: output redirecting file descriptors beyond stdout and stderr is not implemented\n", @@ -603,45 +603,45 @@ async fn redirects_output() { .run() .await; - TestBuilder::new() - .command(r#"echo 1 >&1"#) - .assert_stdout("1\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 >&1"#) + .assert_stdout("1\n") + .assert_exit_code(0) + .run() + .await; - TestBuilder::new() - .command(r#"echo 1 >&2"#) - .assert_stderr("1\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command(r#"echo 1 >&2"#) + .assert_stderr("1\n") + .assert_exit_code(0) + .run() + .await; - TestBuilder::new() - .command(r#"deno eval 'console.error(2)' 2>&1"#) - .assert_stdout("2\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command(r#"deno eval 'console.error(2)' 2>&1"#) + .assert_stdout("2\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] async fn redirects_input() { - TestBuilder::new() - .file("test.txt", "Hi!") - .command(r#"cat - < test.txt"#) - .assert_stdout("Hi!") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "Hi!\n") - .command(r#"cat - < test.txt && echo There"#) - .assert_stdout("Hi!\nThere\n") - .run() - .await; - - TestBuilder::new() + TestBuilder::new() + .file("test.txt", "Hi!") + .command(r#"cat - < test.txt"#) + .assert_stdout("Hi!") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "Hi!\n") + .command(r#"cat - < test.txt && echo There"#) + .assert_stdout("Hi!\nThere\n") + .run() + .await; + + TestBuilder::new() .command(r#"cat - <&0"#) .assert_stderr( "deno_task_shell: input redirecting file descriptors is not implemented\n", @@ -653,43 +653,43 @@ async fn redirects_input() { #[tokio::test] async fn pwd() { - TestBuilder::new() - .directory("sub_dir") - .file("file.txt", "test") - .command("pwd && cd sub_dir && pwd && cd ../ && pwd") - // the actual temp directory will get replaced here - .assert_stdout(&format!( - "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" - )) - .run() - .await; - - TestBuilder::new() - .command("pwd -M") - .assert_stderr("pwd: unsupported flag: -M\n") - .assert_exit_code(1) - .run() - .await; + TestBuilder::new() + .directory("sub_dir") + .file("file.txt", "test") + .command("pwd && cd sub_dir && pwd && cd ../ && pwd") + // the actual temp directory will get replaced here + .assert_stdout(&format!( + "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" + )) + .run() + .await; + + TestBuilder::new() + .command("pwd -M") + .assert_stderr("pwd: unsupported flag: -M\n") + .assert_exit_code(1) + .run() + .await; } #[tokio::test] async fn subshells() { - TestBuilder::new() - .command("(export TEST=1) && echo $TEST") - .assert_stdout("\n") - .assert_exit_code(0) - .run() - .await; - TestBuilder::new() - .directory("sub_dir") - .command("echo $PWD && (cd sub_dir && echo $PWD) && echo $PWD") - .assert_stdout(&format!( - "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" - )) - .assert_exit_code(0) - .run() - .await; - TestBuilder::new() + TestBuilder::new() + .command("(export TEST=1) && echo $TEST") + .assert_stdout("\n") + .assert_exit_code(0) + .run() + .await; + TestBuilder::new() + .directory("sub_dir") + .command("echo $PWD && (cd sub_dir && echo $PWD) && echo $PWD") + .assert_stdout(&format!( + "$TEMP_DIR\n$TEMP_DIR{FOLDER_SEPERATOR}sub_dir\n$TEMP_DIR\n" + )) + .assert_exit_code(0) + .run() + .await; + TestBuilder::new() .command( "export TEST=1 && (echo $TEST && unset TEST && echo $TEST) && echo $TEST", ) @@ -697,100 +697,98 @@ async fn subshells() { .assert_exit_code(0) .run() .await; - TestBuilder::new() - .command("(exit 1) && echo 1") - .assert_exit_code(1) - .run() - .await; - TestBuilder::new() - .command("(exit 1) || echo 1") - .assert_stdout("1\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .command("(exit 1) && echo 1") + .assert_exit_code(1) + .run() + .await; + TestBuilder::new() + .command("(exit 1) || echo 1") + .assert_stdout("1\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] #[cfg(unix)] async fn pwd_logical() { - TestBuilder::new() - .directory("main") - .command( - "ln -s main symlinked_main && cd symlinked_main && pwd && pwd -L", - ) - .assert_stdout("$TEMP_DIR/symlinked_main\n$TEMP_DIR/main\n") - .run() - .await; + TestBuilder::new() + .directory("main") + .command("ln -s main symlinked_main && cd symlinked_main && pwd && pwd -L") + .assert_stdout("$TEMP_DIR/symlinked_main\n$TEMP_DIR/main\n") + .run() + .await; } #[tokio::test] async fn cat() { - // no args - TestBuilder::new() - .command("cat") - .stdin("hello") - .assert_stdout("hello") - .run() - .await; - - // dash - TestBuilder::new() - .command("cat -") - .stdin("hello") - .assert_stdout("hello") - .run() - .await; - - // file - TestBuilder::new() - .command("cat file") - .file("file", "test") - .assert_stdout("test") - .run() - .await; - - // multiple files - TestBuilder::new() - .command("cat file1 file2") - .file("file1", "test") - .file("file2", "other") - .assert_stdout("testother") - .run() - .await; - - // multiple files and stdin - TestBuilder::new() - .command("cat file1 file2 -") - .file("file1", "test\n") - .file("file2", "other\n") - .stdin("hello") - .assert_stdout("test\nother\nhello") - .run() - .await; - - // multiple files and stdin different order - TestBuilder::new() - .command("cat file1 - file2") - .file("file1", "test\n") - .file("file2", "other\n") - .stdin("hello\n") - .assert_stdout("test\nhello\nother\n") - .run() - .await; - - // file containing a command to evaluate - TestBuilder::new() - .command("$(cat file)") - .file("file", "echo hello") - .assert_stdout("hello\n") - .run() - .await; + // no args + TestBuilder::new() + .command("cat") + .stdin("hello") + .assert_stdout("hello") + .run() + .await; + + // dash + TestBuilder::new() + .command("cat -") + .stdin("hello") + .assert_stdout("hello") + .run() + .await; + + // file + TestBuilder::new() + .command("cat file") + .file("file", "test") + .assert_stdout("test") + .run() + .await; + + // multiple files + TestBuilder::new() + .command("cat file1 file2") + .file("file1", "test") + .file("file2", "other") + .assert_stdout("testother") + .run() + .await; + + // multiple files and stdin + TestBuilder::new() + .command("cat file1 file2 -") + .file("file1", "test\n") + .file("file2", "other\n") + .stdin("hello") + .assert_stdout("test\nother\nhello") + .run() + .await; + + // multiple files and stdin different order + TestBuilder::new() + .command("cat file1 - file2") + .file("file1", "test\n") + .file("file2", "other\n") + .stdin("hello\n") + .assert_stdout("test\nhello\nother\n") + .run() + .await; + + // file containing a command to evaluate + TestBuilder::new() + .command("$(cat file)") + .file("file", "echo hello") + .assert_stdout("hello\n") + .run() + .await; } #[tokio::test] async fn head() { - // no args - TestBuilder::new() + // no args + TestBuilder::new() .command("head") .stdin( "foo\nbar\nbaz\nqux\nquuux\ncorge\ngrault\ngarply\nwaldo\nfred\nplugh\n", @@ -801,8 +799,8 @@ async fn head() { .run() .await; - // dash - TestBuilder::new() + // dash + TestBuilder::new() .command("head -") .stdin( "foo\nbar\nbaz\nqux\nquuux\ncorge\ngrault\ngarply\nwaldo\nfred\nplugh\n", @@ -813,8 +811,8 @@ async fn head() { .run() .await; - // file - TestBuilder::new() + // file + TestBuilder::new() .command("head file") .file( "file", @@ -826,8 +824,8 @@ async fn head() { .run() .await; - // dash + longer than internal buffer (512) - TestBuilder::new() + // dash + longer than internal buffer (512) + TestBuilder::new() .command("head -") .stdin( "foo\nbar\nbaz\nqux\nquuux\ncorge\ngrault\ngarply\nwaldo\nfred\nplugh\n" @@ -840,8 +838,8 @@ async fn head() { .run() .await; - // file + longer than internal buffer (512) - TestBuilder::new() + // file + longer than internal buffer (512) + TestBuilder::new() .command("head file") .file( "file", @@ -855,152 +853,150 @@ async fn head() { .run() .await; - // shorter than 10 lines - TestBuilder::new() - .command("head") - .stdin("foo\nbar") - .assert_stdout("foo\nbar") - .run() - .await; - - // -n - TestBuilder::new() - .command("head -n 2") - .stdin("foo\nbar\nbaz\nqux\nquuux") - .assert_stdout("foo\nbar\n") - .run() - .await; - - // --lines - TestBuilder::new() - .command("head --lines=3") - .stdin("foo\nbar\nbaz\nqux\nquuux") - .assert_stdout("foo\nbar\nbaz\n") - .run() - .await; + // shorter than 10 lines + TestBuilder::new() + .command("head") + .stdin("foo\nbar") + .assert_stdout("foo\nbar") + .run() + .await; + + // -n + TestBuilder::new() + .command("head -n 2") + .stdin("foo\nbar\nbaz\nqux\nquuux") + .assert_stdout("foo\nbar\n") + .run() + .await; + + // --lines + TestBuilder::new() + .command("head --lines=3") + .stdin("foo\nbar\nbaz\nqux\nquuux") + .assert_stdout("foo\nbar\nbaz\n") + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn mv() { - // single file - TestBuilder::new() - .command("mv file1.txt file2.txt") - .file("file1.txt", "test") - .assert_not_exists("file1.txt") - .assert_exists("file2.txt") - .run() - .await; - - // multiple files to folder - TestBuilder::new() - .command("mkdir sub_dir && mv file1.txt file2.txt sub_dir") - .file("file1.txt", "test1") - .file("file2.txt", "test2") - .assert_not_exists("file1.txt") - .assert_not_exists("file2.txt") - .assert_exists("sub_dir/file1.txt") - .assert_exists("sub_dir/file2.txt") - .run() - .await; - - // error message - TestBuilder::new() - .command("mv file1.txt file2.txt") - .assert_exit_code(1) - .assert_stderr(&format!( - "mv: could not move file1.txt to file2.txt: {}\n", - no_such_file_error_text() - )) - .run() - .await; + // single file + TestBuilder::new() + .command("mv file1.txt file2.txt") + .file("file1.txt", "test") + .assert_not_exists("file1.txt") + .assert_exists("file2.txt") + .run() + .await; + + // multiple files to folder + TestBuilder::new() + .command("mkdir sub_dir && mv file1.txt file2.txt sub_dir") + .file("file1.txt", "test1") + .file("file2.txt", "test2") + .assert_not_exists("file1.txt") + .assert_not_exists("file2.txt") + .assert_exists("sub_dir/file1.txt") + .assert_exists("sub_dir/file2.txt") + .run() + .await; + + // error message + TestBuilder::new() + .command("mv file1.txt file2.txt") + .assert_exit_code(1) + .assert_stderr(&format!( + "mv: could not move file1.txt to file2.txt: {}\n", + no_such_file_error_text() + )) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn cp() { - // single file - TestBuilder::new() - .command("cp file1.txt file2.txt") - .file("file1.txt", "test") - .assert_exists("file1.txt") - .assert_exists("file2.txt") - .run() - .await; - - // multiple files to folder - TestBuilder::new() - .command("mkdir sub_dir && cp file1.txt file2.txt sub_dir") - .file("file1.txt", "test1") - .file("file2.txt", "test2") - .assert_exists("file1.txt") - .assert_exists("file2.txt") - .assert_exists("sub_dir/file1.txt") - .assert_exists("sub_dir/file2.txt") - .run() - .await; - - // error message - TestBuilder::new() - .command("cp file1.txt file2.txt") - .assert_exit_code(1) - .assert_stderr(&format!( - "cp: could not copy file1.txt to file2.txt: {}\n", - no_such_file_error_text() - )) - .run() - .await; + // single file + TestBuilder::new() + .command("cp file1.txt file2.txt") + .file("file1.txt", "test") + .assert_exists("file1.txt") + .assert_exists("file2.txt") + .run() + .await; + + // multiple files to folder + TestBuilder::new() + .command("mkdir sub_dir && cp file1.txt file2.txt sub_dir") + .file("file1.txt", "test1") + .file("file2.txt", "test2") + .assert_exists("file1.txt") + .assert_exists("file2.txt") + .assert_exists("sub_dir/file1.txt") + .assert_exists("sub_dir/file2.txt") + .run() + .await; + + // error message + TestBuilder::new() + .command("cp file1.txt file2.txt") + .assert_exit_code(1) + .assert_stderr(&format!( + "cp: could not copy file1.txt to file2.txt: {}\n", + no_such_file_error_text() + )) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn mkdir() { - TestBuilder::new() - .command("mkdir sub_dir") - .assert_exists("sub_dir") - .run() - .await; - - // error message - TestBuilder::new() - .command("mkdir file.txt") - .file("file.txt", "test") - .assert_stderr( - "mkdir: cannot create directory 'file.txt': File exists\n", - ) - .assert_exit_code(1) - .run() - .await; + TestBuilder::new() + .command("mkdir sub_dir") + .assert_exists("sub_dir") + .run() + .await; + + // error message + TestBuilder::new() + .command("mkdir file.txt") + .file("file.txt", "test") + .assert_stderr("mkdir: cannot create directory 'file.txt': File exists\n") + .assert_exit_code(1) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn rm() { - TestBuilder::new() - .command("mkdir sub_dir && rm -d sub_dir && rm file.txt") - .file("file.txt", "") - .assert_not_exists("sub_dir") - .assert_not_exists("file.txt") - .run() - .await; - - // error message - TestBuilder::new() - .command("rm file.txt") - .assert_stderr(&format!( - "rm: cannot remove 'file.txt': {}\n", - no_such_file_error_text() - )) - .assert_exit_code(1) - .run() - .await; + TestBuilder::new() + .command("mkdir sub_dir && rm -d sub_dir && rm file.txt") + .file("file.txt", "") + .assert_not_exists("sub_dir") + .assert_not_exists("file.txt") + .run() + .await; + + // error message + TestBuilder::new() + .command("rm file.txt") + .assert_stderr(&format!( + "rm: cannot remove 'file.txt': {}\n", + no_such_file_error_text() + )) + .assert_exit_code(1) + .run() + .await; } // Basic integration tests as there are unit tests in the commands #[tokio::test] async fn unset() { - // Unset 1 shell variable - TestBuilder::new() + // Unset 1 shell variable + TestBuilder::new() .command( r#"VAR1=1 && VAR2=2 && VAR3=3 && unset VAR1 && echo $VAR1 $VAR2 $VAR3"#, ) @@ -1008,15 +1004,15 @@ async fn unset() { .run() .await; - // Unset 1 env variable - TestBuilder::new() + // Unset 1 env variable + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset VAR1 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\n2\n3\n") .run() .await; - // Unset 2 shell variables - TestBuilder::new() + // Unset 2 shell variables + TestBuilder::new() .command( r#"VAR1=1 && VAR2=2 && VAR3=3 && unset VAR1 VAR2 && echo $VAR1 $VAR2 $VAR3"#, ) @@ -1024,15 +1020,15 @@ async fn unset() { .run() .await; - // Unset 2 env variables - TestBuilder::new() + // Unset 2 env variables + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset VAR1 VAR2 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\nundefined\n3\n") .run() .await; - // Unset 2 shell variables with -v enabled - TestBuilder::new() + // Unset 2 shell variables with -v enabled + TestBuilder::new() .command( r#"VAR1=1 && VAR2=2 && VAR3=3 && unset -v VAR1 VAR2 && echo $VAR1 $VAR2 $VAR3"#, ) @@ -1040,22 +1036,22 @@ async fn unset() { .run() .await; - // Unset 1 env variable with -v enabled - TestBuilder::new() + // Unset 1 env variable with -v enabled + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset -v VAR1 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\n2\n3\n") .run() .await; - // Unset 2 env variables with -v enabled - TestBuilder::new() + // Unset 2 env variables with -v enabled + TestBuilder::new() .command(r#"export VAR1=1 VAR2=2 VAR3=3 && unset -v VAR1 VAR2 && deno eval 'for (let i = 1; i <= 3; i++) { const val = Deno.env.get(`VAR${i}`); console.log(val); }'"#) .assert_stdout("undefined\nundefined\n3\n") .run() .await; - // Unset 1 shell variable and 1 env variable at the same time - TestBuilder::new() + // Unset 1 shell variable and 1 env variable at the same time + TestBuilder::new() .command( r#"VAR=1 && export ENV_VAR=2 && unset VAR ENV_VAR && echo $VAR $ENV_VAR"#, ) @@ -1063,91 +1059,91 @@ async fn unset() { .run() .await; - // -f is not supported - TestBuilder::new() - .command(r#"export VAR=42 && unset -f VAR"#) - .assert_stderr("unset: unsupported flag: -f\n") - .assert_exit_code(1) - .run() - .await; + // -f is not supported + TestBuilder::new() + .command(r#"export VAR=42 && unset -f VAR"#) + .assert_stderr("unset: unsupported flag: -f\n") + .assert_exit_code(1) + .run() + .await; } #[tokio::test] async fn xargs() { - TestBuilder::new() - .command("echo '1 2 3 ' | xargs") - .assert_stdout("1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command("echo '1 2 \t\t\t3 ' | xargs echo test") - .assert_stdout("test 1 2 3\n") - .run() - .await; - - TestBuilder::new() - .command(r#"deno eval "console.log('testing\nthis')" | xargs"#) - .assert_stdout("testing this\n") - .run() - .await; - - // \n delimiter - TestBuilder::new() + TestBuilder::new() + .command("echo '1 2 3 ' | xargs") + .assert_stdout("1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command("echo '1 2 \t\t\t3 ' | xargs echo test") + .assert_stdout("test 1 2 3\n") + .run() + .await; + + TestBuilder::new() + .command(r#"deno eval "console.log('testing\nthis')" | xargs"#) + .assert_stdout("testing this\n") + .run() + .await; + + // \n delimiter + TestBuilder::new() .command(r#"deno eval "console.log('testing this out\n\ntest\n')" | xargs -d \n deno eval "console.log(Deno.args)""#) .assert_stdout("[ \"testing this out\", \"\", \"test\", \"\" ]\n") .run() .await; - // \0 delimiter - TestBuilder::new() + // \0 delimiter + TestBuilder::new() .command(r#"deno eval "console.log('testing this out\ntest\0other')" | xargs -0 deno eval "console.log(Deno.args)""#) .assert_stdout("[ \"testing this out\\ntest\", \"other\\n\" ]\n") .run() .await; - // unmatched single quote - TestBuilder::new() + // unmatched single quote + TestBuilder::new() .command(r#"deno eval "console.log(\"'test\")" | xargs"#) .assert_stderr("xargs: unmatched quote; by default quotes are special to xargs unless you use the -0 option\n") .assert_exit_code(1) .run() .await; - // unmatched double quote - TestBuilder::new() + // unmatched double quote + TestBuilder::new() .command(r#"deno eval "console.log('\"test')" | xargs"#) .assert_stderr("xargs: unmatched quote; by default quotes are special to xargs unless you use the -0 option\n") .assert_exit_code(1) .run() .await; - // test reading env file - TestBuilder::new() - .file( - ".env", - r#"VAR1="testing" + // test reading env file + TestBuilder::new() + .file( + ".env", + r#"VAR1="testing" VAR2="other" "#, - ) - // most likely people would want to do `export $(grep -v '^#' .env | xargs)` though - // in order to remove comments... - .command("export $(cat .env | xargs) && echo $VAR1 $VAR2") - .assert_stdout("testing other\n") - .run() - .await; + ) + // most likely people would want to do `export $(grep -v '^#' .env | xargs)` though + // in order to remove comments... + .command("export $(cat .env | xargs) && echo $VAR1 $VAR2") + .assert_stdout("testing other\n") + .run() + .await; } #[tokio::test] async fn stdin() { - TestBuilder::new() + TestBuilder::new() .command(r#"deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)" && deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)""#) .stdin("12345") .assert_stdout("Uint8Array(1) [ 49 ]\nUint8Array(1) [ 50 ]\n") .run() .await; - TestBuilder::new() + TestBuilder::new() .command(r#"echo "12345" | (deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)" && deno eval "const b = new Uint8Array(1);Deno.stdin.readSync(b);console.log(b)")"#) .stdin("55555") // should not use this because stdin is piped from the echo .assert_stdout("Uint8Array(1) [ 49 ]\nUint8Array(1) [ 50 ]\n") @@ -1158,169 +1154,169 @@ async fn stdin() { #[cfg(windows)] #[tokio::test] async fn windows_resolve_command() { - // not cross platform, but still allow this - TestBuilder::new() - .command("deno.exe eval 'console.log(1)'") - .assert_stdout("1\n") - .run() - .await; - - TestBuilder::new() - .command("deno eval 'console.log(1)'") - // handle trailing semi-colon - .env_var("PATHEXT", ".EXE;") - .assert_stdout("1\n") - .run() - .await; + // not cross platform, but still allow this + TestBuilder::new() + .command("deno.exe eval 'console.log(1)'") + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command("deno eval 'console.log(1)'") + // handle trailing semi-colon + .env_var("PATHEXT", ".EXE;") + .assert_stdout("1\n") + .run() + .await; } #[tokio::test] async fn custom_command() { - // not cross platform, but still allow this - TestBuilder::new() - .command("add 1 2") - .custom_command( - "add", - Box::new(|mut context| { - async move { - let mut sum = 0; - for val in context.args { - sum += val.parse::().unwrap(); - } - let _ = context.stderr.write_line(&sum.to_string()); - ExecuteResult::from_exit_code(0) - } - .boxed_local() - }), - ) - .assert_stderr("3\n") - .run() - .await; + // not cross platform, but still allow this + TestBuilder::new() + .command("add 1 2") + .custom_command( + "add", + Box::new(|mut context| { + async move { + let mut sum = 0; + for val in context.args { + sum += val.parse::().unwrap(); + } + let _ = context.stderr.write_line(&sum.to_string()); + ExecuteResult::from_exit_code(0) + } + .boxed_local() + }), + ) + .assert_stderr("3\n") + .run() + .await; } #[tokio::test] async fn custom_command_resolve_command_path() { - TestBuilder::new() - .command("$(custom_which deno) eval 'console.log(1)'") - .custom_command( - "custom_which", - Box::new(|mut context| { - async move { - let path = context - .state - .resolve_command_path(&context.args[0]) - .unwrap(); - let _ = context.stdout.write_line(&path.to_string_lossy()); - ExecuteResult::from_exit_code(0) - } - .boxed_local() - }), - ) - .assert_stdout("1\n") - .run() - .await; + TestBuilder::new() + .command("$(custom_which deno) eval 'console.log(1)'") + .custom_command( + "custom_which", + Box::new(|mut context| { + async move { + let path = context + .state + .resolve_command_path(&context.args[0]) + .unwrap(); + let _ = context.stdout.write_line(&path.to_string_lossy()); + ExecuteResult::from_exit_code(0) + } + .boxed_local() + }), + ) + .assert_stdout("1\n") + .run() + .await; } #[tokio::test] async fn glob_basic() { - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.txt") - .assert_stdout("test\ntest2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat test?.txt") - .assert_stdout("test2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat test[0-9].txt") - .assert_stdout("test2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat test[!a-z].txt") - .assert_stdout("test2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat test[a-z].txt") - .assert_stdout("testa\n") - .run() - .await; - - TestBuilder::new() - .directory("sub_dir/sub") - .file("sub_dir/sub/1.txt", "1\n") - .file("sub_dir/2.txt", "2\n") - .file("sub_dir/other.ts", "other\n") - .file("3.txt", "3\n") - .command("cat */*.txt") - .assert_stdout("2\n") - .run() - .await; - - TestBuilder::new() - .directory("sub_dir/sub") - .file("sub_dir/sub/1.txt", "1\n") - .file("sub_dir/2.txt", "2\n") - .file("sub_dir/other.ts", "other\n") - .file("3.txt", "3\n") - .command("cat **/*.txt") - .assert_stdout("3\n2\n1\n") - .run() - .await; - - TestBuilder::new() - .directory("sub_dir/sub") - .file("sub_dir/sub/1.txt", "1\n") - .file("sub_dir/2.txt", "2\n") - .file("sub_dir/other.ts", "other\n") - .file("3.txt", "3\n") - .command("cat $PWD/**/*.txt") - .assert_stdout("3\n2\n1\n") - .run() - .await; - - TestBuilder::new() - .directory("dir") - .file("dir/1.txt", "1\n") - .file("dir_1.txt", "2\n") - .command("cat dir*1.txt") - .assert_stdout("2\n") - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.ts") - .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") - .assert_exit_code(1) - .run() - .await; - - let mut builder = TestBuilder::new(); - let temp_dir_path = builder.temp_dir_path(); - let error_pos = temp_dir_path.to_string_lossy().len() + 1; - builder.file("test.txt", "test\n") + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.txt") + .assert_stdout("test\ntest2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat test?.txt") + .assert_stdout("test2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat test[0-9].txt") + .assert_stdout("test2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat test[!a-z].txt") + .assert_stdout("test2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat test[a-z].txt") + .assert_stdout("testa\n") + .run() + .await; + + TestBuilder::new() + .directory("sub_dir/sub") + .file("sub_dir/sub/1.txt", "1\n") + .file("sub_dir/2.txt", "2\n") + .file("sub_dir/other.ts", "other\n") + .file("3.txt", "3\n") + .command("cat */*.txt") + .assert_stdout("2\n") + .run() + .await; + + TestBuilder::new() + .directory("sub_dir/sub") + .file("sub_dir/sub/1.txt", "1\n") + .file("sub_dir/2.txt", "2\n") + .file("sub_dir/other.ts", "other\n") + .file("3.txt", "3\n") + .command("cat **/*.txt") + .assert_stdout("3\n2\n1\n") + .run() + .await; + + TestBuilder::new() + .directory("sub_dir/sub") + .file("sub_dir/sub/1.txt", "1\n") + .file("sub_dir/2.txt", "2\n") + .file("sub_dir/other.ts", "other\n") + .file("3.txt", "3\n") + .command("cat $PWD/**/*.txt") + .assert_stdout("3\n2\n1\n") + .run() + .await; + + TestBuilder::new() + .directory("dir") + .file("dir/1.txt", "1\n") + .file("dir_1.txt", "2\n") + .command("cat dir*1.txt") + .assert_stdout("2\n") + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.ts") + .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") + .assert_exit_code(1) + .run() + .await; + + let mut builder = TestBuilder::new(); + let temp_dir_path = builder.temp_dir_path(); + let error_pos = temp_dir_path.to_string_lossy().len() + 1; + builder.file("test.txt", "test\n") .file("test2.txt", "test2\n") .command("cat [].ts") .assert_stderr(&format!("glob: no matches found '$TEMP_DIR/[].ts'. Pattern syntax error near position {}: invalid range pattern\n", error_pos)) @@ -1328,125 +1324,125 @@ async fn glob_basic() { .run() .await; - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.ts || echo 2") - .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") - .assert_stdout("2\n") - .assert_exit_code(0) - .run() - .await; - - TestBuilder::new() - .file("test.txt", "test\n") - .file("test2.txt", "test2\n") - .command("cat *.ts 2> /dev/null || echo 2") - .assert_stderr("") - .assert_stdout("2\n") - .assert_exit_code(0) - .run() - .await; - - TestBuilder::new() - .command("echo --inspect='[::0]:3366'") - .assert_stderr("") - .assert_stdout("--inspect=[::0]:3366\n") - .assert_exit_code(0) - .run() - .await; + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.ts || echo 2") + .assert_stderr("glob: no matches found '$TEMP_DIR/*.ts'\n") + .assert_stdout("2\n") + .assert_exit_code(0) + .run() + .await; + + TestBuilder::new() + .file("test.txt", "test\n") + .file("test2.txt", "test2\n") + .command("cat *.ts 2> /dev/null || echo 2") + .assert_stderr("") + .assert_stdout("2\n") + .assert_exit_code(0) + .run() + .await; + + TestBuilder::new() + .command("echo --inspect='[::0]:3366'") + .assert_stderr("") + .assert_stdout("--inspect=[::0]:3366\n") + .assert_exit_code(0) + .run() + .await; } #[tokio::test] async fn glob_case_insensitive() { - TestBuilder::new() - .file("TEST.txt", "test\n") - .file("testa.txt", "testa\n") - .file("test2.txt", "test2\n") - .command("cat tes*.txt") - .assert_stdout("test\ntest2\ntesta\n") - .run() - .await; + TestBuilder::new() + .file("TEST.txt", "test\n") + .file("testa.txt", "testa\n") + .file("test2.txt", "test2\n") + .command("cat tes*.txt") + .assert_stdout("test\ntest2\ntesta\n") + .run() + .await; } #[tokio::test] async fn glob_escapes() { - // no escape - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat [test].txt") - .assert_stdout("t\n") - .run() - .await; - - // escape - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat [[]test[]].txt") - .assert_stdout("test\n") - .run() - .await; - - // single quotes - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat '[test].txt'") - .assert_stdout("test\n") - .run() - .await; - - // double quotes - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat \"[test].txt\"") - .assert_stdout("test\n") - .run() - .await; - - // mix - TestBuilder::new() - .file("[test].txt", "test\n") - .file("t.txt", "t\n") - .command("cat \"[\"test\"]\".txt") - .assert_stdout("test\n") - .run() - .await; + // no escape + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat [test].txt") + .assert_stdout("t\n") + .run() + .await; + + // escape + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat [[]test[]].txt") + .assert_stdout("test\n") + .run() + .await; + + // single quotes + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat '[test].txt'") + .assert_stdout("test\n") + .run() + .await; + + // double quotes + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat \"[test].txt\"") + .assert_stdout("test\n") + .run() + .await; + + // mix + TestBuilder::new() + .file("[test].txt", "test\n") + .file("t.txt", "t\n") + .command("cat \"[\"test\"]\".txt") + .assert_stdout("test\n") + .run() + .await; } #[tokio::test] async fn paren_escapes() { - TestBuilder::new() - .command(r"echo \( foo bar \)") - .assert_stdout("( foo bar )\n") - .run() - .await; + TestBuilder::new() + .command(r"echo \( foo bar \)") + .assert_stdout("( foo bar )\n") + .run() + .await; } #[tokio::test] async fn cross_platform_shebang() { - // with -S - TestBuilder::new() - .file("file.ts", "#!/usr/bin/env -S deno run\nconsole.log(5)") - .command("./file.ts") - .assert_stdout("5\n") - .run() - .await; - - // without -S and invalid - TestBuilder::new() - .file("file.ts", "#!/usr/bin/env deno run\nconsole.log(5)") - .command("./file.ts") - .assert_stderr("deno run: command not found\n") - .assert_exit_code(127) - .run() - .await; - - // without -S, but valid - TestBuilder::new() + // with -S + TestBuilder::new() + .file("file.ts", "#!/usr/bin/env -S deno run\nconsole.log(5)") + .command("./file.ts") + .assert_stdout("5\n") + .run() + .await; + + // without -S and invalid + TestBuilder::new() + .file("file.ts", "#!/usr/bin/env deno run\nconsole.log(5)") + .command("./file.ts") + .assert_stderr("deno run: command not found\n") + .assert_exit_code(127) + .run() + .await; + + // without -S, but valid + TestBuilder::new() .file("file.ts", "#!/usr/bin/env ./echo_stdin.ts\nconsole.log('Hello')") .file("echo_stdin.ts", "#!/usr/bin/env -S deno run --allow-run\nawait new Deno.Command('deno', { args: ['run', ...Deno.args] }).spawn();") .command("./file.ts") @@ -1454,8 +1450,8 @@ async fn cross_platform_shebang() { .run() .await; - // sub dir - TestBuilder::new() + // sub dir + TestBuilder::new() .directory("sub") .file("sub/file.ts", "#!/usr/bin/env ../echo_stdin.ts\nconsole.log('Hello')") .file("echo_stdin.ts", "#!/usr/bin/env -S deno run --allow-run\nawait new Deno.Command('deno', { args: ['run', ...Deno.args] }).spawn();") @@ -1464,8 +1460,8 @@ async fn cross_platform_shebang() { .run() .await; - // arguments - TestBuilder::new() + // arguments + TestBuilder::new() .file( "file.ts", "#!/usr/bin/env -S deno run --allow-read\nconsole.log(Deno.args)\nconst text = Deno.readTextFileSync(import.meta.filename);\nconsole.log(text.length)\n", @@ -1479,9 +1475,9 @@ async fn cross_platform_shebang() { } fn no_such_file_error_text() -> &'static str { - if cfg!(windows) { - "The system cannot find the file specified. (os error 2)" - } else { - "No such file or directory (os error 2)" - } + if cfg!(windows) { + "The system cannot find the file specified. (os error 2)" + } else { + "No such file or directory (os error 2)" + } } diff --git a/crates/deno_task_shell/src/shell/test_builder.rs b/crates/deno_task_shell/src/shell/test_builder.rs index 37f3f1e..0aacc65 100644 --- a/crates/deno_task_shell/src/shell/test_builder.rs +++ b/crates/deno_task_shell/src/shell/test_builder.rs @@ -21,262 +21,265 @@ use crate::ShellCommandContext; use super::types::ExecuteResult; type FnShellCommandExecute = - Box LocalBoxFuture<'static, ExecuteResult>>; + Box LocalBoxFuture<'static, ExecuteResult>>; struct FnShellCommand(FnShellCommandExecute); impl ShellCommand for FnShellCommand { - fn execute( - &self, - context: ShellCommandContext, - ) -> LocalBoxFuture<'static, ExecuteResult> { - (self.0)(context) - } + fn execute( + &self, + context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + (self.0)(context) + } } // Clippy is complaining about them all having `File` prefixes, // but there might be non-file variants in the future. #[allow(clippy::enum_variant_names)] enum TestAssertion { - FileExists(String), - FileNotExists(String), - FileTextEquals(String, String), + FileExists(String), + FileNotExists(String), + FileTextEquals(String, String), } struct TempDir { - // hold to keep it alive until drop - _inner: tempfile::TempDir, - cwd: PathBuf, + // hold to keep it alive until drop + _inner: tempfile::TempDir, + cwd: PathBuf, } impl TempDir { - pub fn new() -> Self { - let temp_dir = tempfile::tempdir().unwrap(); - let cwd = fs_util::canonicalize_path(temp_dir.path()).unwrap(); - Self { - _inner: temp_dir, - cwd, - } + pub fn new() -> Self { + let temp_dir = tempfile::tempdir().unwrap(); + let cwd = fs_util::canonicalize_path(temp_dir.path()).unwrap(); + Self { + _inner: temp_dir, + cwd, } + } } pub struct TestBuilder { - // it is much much faster to lazily create this - temp_dir: Option, - env_vars: HashMap, - custom_commands: HashMap>, - command: String, - stdin: Vec, - expected_exit_code: i32, - expected_stderr: String, - expected_stdout: String, - assertions: Vec, + // it is much much faster to lazily create this + temp_dir: Option, + env_vars: HashMap, + custom_commands: HashMap>, + command: String, + stdin: Vec, + expected_exit_code: i32, + expected_stderr: String, + expected_stdout: String, + assertions: Vec, } impl TestBuilder { - pub fn new() -> Self { - let env_vars = std::env::vars() - .map(|(key, value)| { - // For some very strange reason, key will sometimes be cased as "Path" - // or other times "PATH" on Windows. Since keys are case-insensitive on - // Windows, normalize the keys to be upper case. - if cfg!(windows) { - // need to normalize on windows - (key.to_uppercase(), value) - } else { - (key, value) - } - }) - .collect(); - - Self { - temp_dir: None, - env_vars, - custom_commands: Default::default(), - command: Default::default(), - stdin: Default::default(), - expected_exit_code: 0, - expected_stderr: Default::default(), - expected_stdout: Default::default(), - assertions: Default::default(), + pub fn new() -> Self { + let env_vars = std::env::vars() + .map(|(key, value)| { + // For some very strange reason, key will sometimes be cased as "Path" + // or other times "PATH" on Windows. Since keys are case-insensitive on + // Windows, normalize the keys to be upper case. + if cfg!(windows) { + // need to normalize on windows + (key.to_uppercase(), value) + } else { + (key, value) } - } + }) + .collect(); - pub fn ensure_temp_dir(&mut self) -> &mut Self { - self.get_temp_dir(); - self + Self { + temp_dir: None, + env_vars, + custom_commands: Default::default(), + command: Default::default(), + stdin: Default::default(), + expected_exit_code: 0, + expected_stderr: Default::default(), + expected_stdout: Default::default(), + assertions: Default::default(), } + } - fn get_temp_dir(&mut self) -> &mut TempDir { - if self.temp_dir.is_none() { - self.temp_dir = Some(TempDir::new()); - } - self.temp_dir.as_mut().unwrap() - } + pub fn ensure_temp_dir(&mut self) -> &mut Self { + self.get_temp_dir(); + self + } - pub fn temp_dir_path(&mut self) -> PathBuf { - self.get_temp_dir().cwd.clone() + fn get_temp_dir(&mut self) -> &mut TempDir { + if self.temp_dir.is_none() { + self.temp_dir = Some(TempDir::new()); } + self.temp_dir.as_mut().unwrap() + } - pub fn command(&mut self, command: &str) -> &mut Self { - self.command = command.to_string(); - self - } + pub fn temp_dir_path(&mut self) -> PathBuf { + self.get_temp_dir().cwd.clone() + } - pub fn stdin(&mut self, stdin: &str) -> &mut Self { - self.stdin = stdin.as_bytes().to_vec(); - self - } + pub fn command(&mut self, command: &str) -> &mut Self { + self.command = command.to_string(); + self + } - pub fn directory(&mut self, path: &str) -> &mut Self { - let temp_dir = self.get_temp_dir(); - fs::create_dir_all(temp_dir.cwd.join(path)).unwrap(); - self - } + pub fn stdin(&mut self, stdin: &str) -> &mut Self { + self.stdin = stdin.as_bytes().to_vec(); + self + } - pub fn env_var(&mut self, name: &str, value: &str) -> &mut Self { - self.env_vars.insert(name.to_string(), value.to_string()); - self - } + pub fn directory(&mut self, path: &str) -> &mut Self { + let temp_dir = self.get_temp_dir(); + fs::create_dir_all(temp_dir.cwd.join(path)).unwrap(); + self + } - pub fn custom_command( - &mut self, - name: &str, - execute: FnShellCommandExecute, - ) -> &mut Self { - self.custom_commands - .insert(name.to_string(), Rc::new(FnShellCommand(execute))); - self - } + pub fn env_var(&mut self, name: &str, value: &str) -> &mut Self { + self.env_vars.insert(name.to_string(), value.to_string()); + self + } - pub fn file(&mut self, path: &str, text: &str) -> &mut Self { - let temp_dir = self.get_temp_dir(); - fs::write(temp_dir.cwd.join(path), text).unwrap(); - self - } + pub fn custom_command( + &mut self, + name: &str, + execute: FnShellCommandExecute, + ) -> &mut Self { + self + .custom_commands + .insert(name.to_string(), Rc::new(FnShellCommand(execute))); + self + } - pub fn assert_exit_code(&mut self, code: i32) -> &mut Self { - self.expected_exit_code = code; - self - } + pub fn file(&mut self, path: &str, text: &str) -> &mut Self { + let temp_dir = self.get_temp_dir(); + fs::write(temp_dir.cwd.join(path), text).unwrap(); + self + } - pub fn assert_stderr(&mut self, output: &str) -> &mut Self { - self.expected_stderr.push_str(output); - self - } + pub fn assert_exit_code(&mut self, code: i32) -> &mut Self { + self.expected_exit_code = code; + self + } - pub fn assert_stdout(&mut self, output: &str) -> &mut Self { - self.expected_stdout.push_str(output); - self - } + pub fn assert_stderr(&mut self, output: &str) -> &mut Self { + self.expected_stderr.push_str(output); + self + } - pub fn assert_exists(&mut self, path: &str) -> &mut Self { - self.ensure_temp_dir(); - self.assertions - .push(TestAssertion::FileExists(path.to_string())); - self - } + pub fn assert_stdout(&mut self, output: &str) -> &mut Self { + self.expected_stdout.push_str(output); + self + } - pub fn assert_not_exists(&mut self, path: &str) -> &mut Self { - self.ensure_temp_dir(); - self.assertions - .push(TestAssertion::FileNotExists(path.to_string())); - self - } + pub fn assert_exists(&mut self, path: &str) -> &mut Self { + self.ensure_temp_dir(); + self + .assertions + .push(TestAssertion::FileExists(path.to_string())); + self + } - pub fn assert_file_equals( - &mut self, - path: &str, - file_text: &str, - ) -> &mut Self { - self.ensure_temp_dir(); - self.assertions.push(TestAssertion::FileTextEquals( - path.to_string(), - file_text.to_string(), - )); - self - } + pub fn assert_not_exists(&mut self, path: &str) -> &mut Self { + self.ensure_temp_dir(); + self + .assertions + .push(TestAssertion::FileNotExists(path.to_string())); + self + } - pub async fn run(&mut self) { - let list = parse(&self.command).unwrap(); - let cwd = if let Some(temp_dir) = &self.temp_dir { - temp_dir.cwd.clone() - } else { - std::env::temp_dir() - }; - let (stdin, mut stdin_writer) = pipe(); - stdin_writer.write_all(&self.stdin).unwrap(); - drop(stdin_writer); // prevent a deadlock by dropping the writer - let (stdout, stdout_handle) = get_output_writer_and_handle(); - let (stderr, stderr_handle) = get_output_writer_and_handle(); + pub fn assert_file_equals( + &mut self, + path: &str, + file_text: &str, + ) -> &mut Self { + self.ensure_temp_dir(); + self.assertions.push(TestAssertion::FileTextEquals( + path.to_string(), + file_text.to_string(), + )); + self + } - let local_set = tokio::task::LocalSet::new(); - let state = ShellState::new( - self.env_vars.clone(), - &cwd, - self.custom_commands.drain().collect(), - ); - let exit_code = local_set - .run_until(execute_with_pipes(list, state, stdin, stdout, stderr)) - .await; - let temp_dir = if let Some(temp_dir) = &self.temp_dir { - temp_dir.cwd.display().to_string() - } else { - "NO_TEMP_DIR".to_string() - }; - assert_eq!( - stderr_handle.await.unwrap(), - self.expected_stderr.replace("$TEMP_DIR", &temp_dir), - "\n\nFailed for: {}", - self.command - ); - assert_eq!( - stdout_handle.await.unwrap(), - self.expected_stdout.replace("$TEMP_DIR", &temp_dir), - "\n\nFailed for: {}", - self.command - ); - assert_eq!( - exit_code, self.expected_exit_code, - "\n\nFailed for: {}", - self.command - ); + pub async fn run(&mut self) { + let list = parse(&self.command).unwrap(); + let cwd = if let Some(temp_dir) = &self.temp_dir { + temp_dir.cwd.clone() + } else { + std::env::temp_dir() + }; + let (stdin, mut stdin_writer) = pipe(); + stdin_writer.write_all(&self.stdin).unwrap(); + drop(stdin_writer); // prevent a deadlock by dropping the writer + let (stdout, stdout_handle) = get_output_writer_and_handle(); + let (stderr, stderr_handle) = get_output_writer_and_handle(); - for assertion in &self.assertions { - match assertion { - TestAssertion::FileExists(path) => { - assert!( - cwd.join(path).exists(), - "\n\nFailed for: {}\nExpected '{}' to exist.", - self.command, - path, - ) - } - TestAssertion::FileNotExists(path) => { - assert!( - !cwd.join(path).exists(), - "\n\nFailed for: {}\nExpected '{}' to not exist.", - self.command, - path, - ) - } - TestAssertion::FileTextEquals(path, text) => { - let actual_text = std::fs::read_to_string(cwd.join(path)) - .with_context(|| format!("Error reading {path}")) - .unwrap(); - assert_eq!( - &actual_text, text, - "\n\nFailed for: {}\nPath: {}", - self.command, path, - ) - } - } + let local_set = tokio::task::LocalSet::new(); + let state = ShellState::new( + self.env_vars.clone(), + &cwd, + self.custom_commands.drain().collect(), + ); + let exit_code = local_set + .run_until(execute_with_pipes(list, state, stdin, stdout, stderr)) + .await; + let temp_dir = if let Some(temp_dir) = &self.temp_dir { + temp_dir.cwd.display().to_string() + } else { + "NO_TEMP_DIR".to_string() + }; + assert_eq!( + stderr_handle.await.unwrap(), + self.expected_stderr.replace("$TEMP_DIR", &temp_dir), + "\n\nFailed for: {}", + self.command + ); + assert_eq!( + stdout_handle.await.unwrap(), + self.expected_stdout.replace("$TEMP_DIR", &temp_dir), + "\n\nFailed for: {}", + self.command + ); + assert_eq!( + exit_code, self.expected_exit_code, + "\n\nFailed for: {}", + self.command + ); + + for assertion in &self.assertions { + match assertion { + TestAssertion::FileExists(path) => { + assert!( + cwd.join(path).exists(), + "\n\nFailed for: {}\nExpected '{}' to exist.", + self.command, + path, + ) + } + TestAssertion::FileNotExists(path) => { + assert!( + !cwd.join(path).exists(), + "\n\nFailed for: {}\nExpected '{}' to not exist.", + self.command, + path, + ) + } + TestAssertion::FileTextEquals(path, text) => { + let actual_text = std::fs::read_to_string(cwd.join(path)) + .with_context(|| format!("Error reading {path}")) + .unwrap(); + assert_eq!( + &actual_text, text, + "\n\nFailed for: {}\nPath: {}", + self.command, path, + ) } + } } + } } fn get_output_writer_and_handle() -> (ShellPipeWriter, JoinHandle) { - let (reader, writer) = pipe(); - let handle = reader.pipe_to_string_handle(); - (writer, handle) + let (reader, writer) = pipe(); + let handle = reader.pipe_to_string_handle(); + (writer, handle) } diff --git a/crates/deno_task_shell/src/shell/types.rs b/crates/deno_task_shell/src/shell/types.rs index df4984a..f78ba66 100644 --- a/crates/deno_task_shell/src/shell/types.rs +++ b/crates/deno_task_shell/src/shell/types.rs @@ -20,156 +20,156 @@ use super::commands::ShellCommand; #[derive(Clone)] pub struct ShellState { - /// Environment variables that should be passed down to sub commands - /// and used when evaluating environment variables. - env_vars: HashMap, - /// Variables that should be evaluated within the shell and - /// not passed down to any sub commands. - shell_vars: HashMap, - cwd: PathBuf, - commands: Rc>>, - /// Token to cancel execution. - token: CancellationToken, + /// Environment variables that should be passed down to sub commands + /// and used when evaluating environment variables. + env_vars: HashMap, + /// Variables that should be evaluated within the shell and + /// not passed down to any sub commands. + shell_vars: HashMap, + cwd: PathBuf, + commands: Rc>>, + /// Token to cancel execution. + token: CancellationToken, } impl ShellState { - pub fn new( - env_vars: HashMap, - cwd: &Path, - custom_commands: HashMap>, - ) -> Self { - assert!(cwd.is_absolute()); - let mut commands = builtin_commands(); - commands.extend(custom_commands); - let mut result = Self { - env_vars: Default::default(), - shell_vars: Default::default(), - cwd: PathBuf::new(), - commands: Rc::new(commands), - token: CancellationToken::default(), - }; - // ensure the data is normalized - for (name, value) in env_vars { - result.apply_env_var(&name, &value); - } - result.set_cwd(cwd); - result - } - - pub fn cwd(&self) -> &PathBuf { - &self.cwd - } - - pub fn env_vars(&self) -> &HashMap { - &self.env_vars - } - - pub fn get_var(&self, name: &str) -> Option<&String> { - let name = if cfg!(windows) { - Cow::Owned(name.to_uppercase()) + pub fn new( + env_vars: HashMap, + cwd: &Path, + custom_commands: HashMap>, + ) -> Self { + assert!(cwd.is_absolute()); + let mut commands = builtin_commands(); + commands.extend(custom_commands); + let mut result = Self { + env_vars: Default::default(), + shell_vars: Default::default(), + cwd: PathBuf::new(), + commands: Rc::new(commands), + token: CancellationToken::default(), + }; + // ensure the data is normalized + for (name, value) in env_vars { + result.apply_env_var(&name, &value); + } + result.set_cwd(cwd); + result + } + + pub fn cwd(&self) -> &PathBuf { + &self.cwd + } + + pub fn env_vars(&self) -> &HashMap { + &self.env_vars + } + + pub fn get_var(&self, name: &str) -> Option<&String> { + let name = if cfg!(windows) { + Cow::Owned(name.to_uppercase()) + } else { + Cow::Borrowed(name) + }; + self + .env_vars + .get(name.as_ref()) + .or_else(|| self.shell_vars.get(name.as_ref())) + } + + pub fn set_cwd(&mut self, cwd: &Path) { + self.cwd = cwd.to_path_buf(); + // $PWD holds the current working directory, so we keep cwd and $PWD in sync + self + .env_vars + .insert("PWD".to_string(), self.cwd.display().to_string()); + } + + pub fn apply_changes(&mut self, changes: &[EnvChange]) { + for change in changes { + self.apply_change(change); + } + } + + pub fn apply_change(&mut self, change: &EnvChange) { + match change { + EnvChange::SetEnvVar(name, value) => self.apply_env_var(name, value), + EnvChange::SetShellVar(name, value) => { + if self.env_vars.contains_key(name) { + self.apply_env_var(name, value); } else { - Cow::Borrowed(name) - }; - self.env_vars - .get(name.as_ref()) - .or_else(|| self.shell_vars.get(name.as_ref())) - } - - pub fn set_cwd(&mut self, cwd: &Path) { - self.cwd = cwd.to_path_buf(); - // $PWD holds the current working directory, so we keep cwd and $PWD in sync - self.env_vars - .insert("PWD".to_string(), self.cwd.display().to_string()); - } - - pub fn apply_changes(&mut self, changes: &[EnvChange]) { - for change in changes { - self.apply_change(change); + self.shell_vars.insert(name.to_string(), value.to_string()); } - } - - pub fn apply_change(&mut self, change: &EnvChange) { - match change { - EnvChange::SetEnvVar(name, value) => { - self.apply_env_var(name, value) - } - EnvChange::SetShellVar(name, value) => { - if self.env_vars.contains_key(name) { - self.apply_env_var(name, value); - } else { - self.shell_vars.insert(name.to_string(), value.to_string()); - } - } - EnvChange::UnsetVar(name) => { - self.shell_vars.remove(name); - self.env_vars.remove(name); - } - EnvChange::Cd(new_dir) => { - self.set_cwd(new_dir); - } + } + EnvChange::UnsetVar(name) => { + self.shell_vars.remove(name); + self.env_vars.remove(name); + } + EnvChange::Cd(new_dir) => { + self.set_cwd(new_dir); + } + } + } + + pub fn apply_env_var(&mut self, name: &str, value: &str) { + let name = if cfg!(windows) { + // environment variables are case insensitive on windows + name.to_uppercase() + } else { + name.to_string() + }; + if name == "PWD" { + let cwd = PathBuf::from(value); + if cwd.is_absolute() { + if let Ok(cwd) = fs_util::canonicalize_path(&cwd) { + // this will update the environment variable too + self.set_cwd(&cwd); } - } - - pub fn apply_env_var(&mut self, name: &str, value: &str) { - let name = if cfg!(windows) { - // environment variables are case insensitive on windows - name.to_uppercase() - } else { - name.to_string() - }; - if name == "PWD" { - let cwd = PathBuf::from(value); - if cwd.is_absolute() { - if let Ok(cwd) = fs_util::canonicalize_path(&cwd) { - // this will update the environment variable too - self.set_cwd(&cwd); - } - } - } else { - self.shell_vars.remove(&name); - self.env_vars.insert(name, value.to_string()); - } - } - - pub fn token(&self) -> &CancellationToken { - &self.token - } - - /// Resolves a custom command that was injected. - pub fn resolve_custom_command( - &self, - name: &str, - ) -> Option> { - // uses an Rc to allow resolving a command without borrowing from self - self.commands.get(name).cloned() - } - - /// Resolves the path to a command from the current working directory. - /// - /// Does not take injected custom commands into account. - pub fn resolve_command_path( - &self, - command_name: &str, - ) -> Result { - super::command::resolve_command_path(command_name, self.cwd(), self) - } - - pub fn with_child_token(&self) -> ShellState { - let mut state = self.clone(); - state.token = self.token.child_token(); - state - } + } + } else { + self.shell_vars.remove(&name); + self.env_vars.insert(name, value.to_string()); + } + } + + pub fn token(&self) -> &CancellationToken { + &self.token + } + + /// Resolves a custom command that was injected. + pub fn resolve_custom_command( + &self, + name: &str, + ) -> Option> { + // uses an Rc to allow resolving a command without borrowing from self + self.commands.get(name).cloned() + } + + /// Resolves the path to a command from the current working directory. + /// + /// Does not take injected custom commands into account. + pub fn resolve_command_path( + &self, + command_name: &str, + ) -> Result { + super::command::resolve_command_path(command_name, self.cwd(), self) + } + + pub fn with_child_token(&self) -> ShellState { + let mut state = self.clone(); + state.token = self.token.child_token(); + state + } } #[derive(Debug, PartialEq, Eq)] pub enum EnvChange { - // `export ENV_VAR=VALUE` - SetEnvVar(String, String), - // `ENV_VAR=VALUE` - SetShellVar(String, String), - // `unset ENV_VAR` - UnsetVar(String), - Cd(PathBuf), + // `export ENV_VAR=VALUE` + SetEnvVar(String, String), + // `ENV_VAR=VALUE` + SetShellVar(String, String), + // `unset ENV_VAR` + UnsetVar(String), + Cd(PathBuf), } pub type FutureExecuteResult = LocalBoxFuture<'static, ExecuteResult>; @@ -180,139 +180,135 @@ pub const CANCELLATION_EXIT_CODE: i32 = 130; #[derive(Debug)] pub enum ExecuteResult { - Exit(i32, Vec>), - Continue(i32, Vec, Vec>), + Exit(i32, Vec>), + Continue(i32, Vec, Vec>), } impl ExecuteResult { - pub fn for_cancellation() -> ExecuteResult { - ExecuteResult::Exit(CANCELLATION_EXIT_CODE, Vec::new()) - } + pub fn for_cancellation() -> ExecuteResult { + ExecuteResult::Exit(CANCELLATION_EXIT_CODE, Vec::new()) + } - pub fn from_exit_code(exit_code: i32) -> ExecuteResult { - ExecuteResult::Continue(exit_code, Vec::new(), Vec::new()) - } + pub fn from_exit_code(exit_code: i32) -> ExecuteResult { + ExecuteResult::Continue(exit_code, Vec::new(), Vec::new()) + } - pub fn into_exit_code_and_handles(self) -> (i32, Vec>) { - match self { - ExecuteResult::Exit(code, handles) => (code, handles), - ExecuteResult::Continue(code, _, handles) => (code, handles), - } + pub fn into_exit_code_and_handles(self) -> (i32, Vec>) { + match self { + ExecuteResult::Exit(code, handles) => (code, handles), + ExecuteResult::Continue(code, _, handles) => (code, handles), } + } - pub fn into_handles(self) -> Vec> { - self.into_exit_code_and_handles().1 - } + pub fn into_handles(self) -> Vec> { + self.into_exit_code_and_handles().1 + } } /// Reader side of a pipe. #[derive(Debug)] pub enum ShellPipeReader { - OsPipe(os_pipe::PipeReader), - StdFile(std::fs::File), + OsPipe(os_pipe::PipeReader), + StdFile(std::fs::File), } impl Clone for ShellPipeReader { - fn clone(&self) -> Self { - match self { - Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), - Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), - } + fn clone(&self) -> Self { + match self { + Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), + Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), } + } } impl ShellPipeReader { - pub fn stdin() -> ShellPipeReader { - ShellPipeReader::from_raw(os_pipe::dup_stdin().unwrap()) - } - - pub fn from_raw(reader: os_pipe::PipeReader) -> Self { - Self::OsPipe(reader) - } - - pub fn from_std(std_file: std::fs::File) -> Self { - Self::StdFile(std_file) - } - - pub fn into_stdio(self) -> std::process::Stdio { - match self { - Self::OsPipe(pipe) => pipe.into(), - Self::StdFile(file) => file.into(), - } - } - - /// Pipe everything to the specified writer - pub fn pipe_to(self, writer: &mut dyn Write) -> Result<()> { - // don't bother flushing here because this won't ever be called - // with a Rust wrapped stdout/stderr - self.pipe_to_inner(writer, false) - } - - fn pipe_to_with_flushing(self, writer: &mut dyn Write) -> Result<()> { - self.pipe_to_inner(writer, true) - } - - fn pipe_to_inner( - mut self, - writer: &mut dyn Write, - flush: bool, - ) -> Result<()> { - loop { - let mut buffer = [0; 512]; // todo: what is an appropriate buffer size? - let size = match &mut self { - ShellPipeReader::OsPipe(pipe) => pipe.read(&mut buffer)?, - ShellPipeReader::StdFile(file) => file.read(&mut buffer)?, - }; - if size == 0 { - break; - } - writer.write_all(&buffer[0..size])?; - if flush { - writer.flush()?; - } - } - Ok(()) - } - - /// Pipes this pipe to the specified sender. - pub fn pipe_to_sender(self, mut sender: ShellPipeWriter) -> Result<()> { - match &mut sender { - ShellPipeWriter::OsPipe(pipe) => self.pipe_to(pipe), - ShellPipeWriter::StdFile(file) => self.pipe_to(file), - // Don't lock stdout/stderr here because we want to release the lock - // when reading from the sending pipe. Additionally, we want - // to flush after every write because Rust's wrapper has an - // internal buffer and Deno doesn't buffer stdout/stderr. - ShellPipeWriter::Stdout => { - self.pipe_to_with_flushing(&mut std::io::stdout()) - } - ShellPipeWriter::Stderr => { - self.pipe_to_with_flushing(&mut std::io::stderr()) - } - ShellPipeWriter::Null => Ok(()), - } - } - - /// Pipes the reader to a string handle that is resolved when the pipe's - /// writer is closed. - pub fn pipe_to_string_handle(self) -> JoinHandle { - tokio::task::spawn_blocking(|| { - let mut buf = Vec::new(); - self.pipe_to(&mut buf).unwrap(); - String::from_utf8_lossy(&buf).to_string() - }) - } - - pub fn read(&mut self, buf: &mut [u8]) -> Result { - match self { - ShellPipeReader::OsPipe(pipe) => { - pipe.read(buf).map_err(|e| e.into()) - } - ShellPipeReader::StdFile(file) => { - file.read(buf).map_err(|e| e.into()) - } - } - } + pub fn stdin() -> ShellPipeReader { + ShellPipeReader::from_raw(os_pipe::dup_stdin().unwrap()) + } + + pub fn from_raw(reader: os_pipe::PipeReader) -> Self { + Self::OsPipe(reader) + } + + pub fn from_std(std_file: std::fs::File) -> Self { + Self::StdFile(std_file) + } + + pub fn into_stdio(self) -> std::process::Stdio { + match self { + Self::OsPipe(pipe) => pipe.into(), + Self::StdFile(file) => file.into(), + } + } + + /// Pipe everything to the specified writer + pub fn pipe_to(self, writer: &mut dyn Write) -> Result<()> { + // don't bother flushing here because this won't ever be called + // with a Rust wrapped stdout/stderr + self.pipe_to_inner(writer, false) + } + + fn pipe_to_with_flushing(self, writer: &mut dyn Write) -> Result<()> { + self.pipe_to_inner(writer, true) + } + + fn pipe_to_inner( + mut self, + writer: &mut dyn Write, + flush: bool, + ) -> Result<()> { + loop { + let mut buffer = [0; 512]; // todo: what is an appropriate buffer size? + let size = match &mut self { + ShellPipeReader::OsPipe(pipe) => pipe.read(&mut buffer)?, + ShellPipeReader::StdFile(file) => file.read(&mut buffer)?, + }; + if size == 0 { + break; + } + writer.write_all(&buffer[0..size])?; + if flush { + writer.flush()?; + } + } + Ok(()) + } + + /// Pipes this pipe to the specified sender. + pub fn pipe_to_sender(self, mut sender: ShellPipeWriter) -> Result<()> { + match &mut sender { + ShellPipeWriter::OsPipe(pipe) => self.pipe_to(pipe), + ShellPipeWriter::StdFile(file) => self.pipe_to(file), + // Don't lock stdout/stderr here because we want to release the lock + // when reading from the sending pipe. Additionally, we want + // to flush after every write because Rust's wrapper has an + // internal buffer and Deno doesn't buffer stdout/stderr. + ShellPipeWriter::Stdout => { + self.pipe_to_with_flushing(&mut std::io::stdout()) + } + ShellPipeWriter::Stderr => { + self.pipe_to_with_flushing(&mut std::io::stderr()) + } + ShellPipeWriter::Null => Ok(()), + } + } + + /// Pipes the reader to a string handle that is resolved when the pipe's + /// writer is closed. + pub fn pipe_to_string_handle(self) -> JoinHandle { + tokio::task::spawn_blocking(|| { + let mut buf = Vec::new(); + self.pipe_to(&mut buf).unwrap(); + String::from_utf8_lossy(&buf).to_string() + }) + } + + pub fn read(&mut self, buf: &mut [u8]) -> Result { + match self { + ShellPipeReader::OsPipe(pipe) => pipe.read(buf).map_err(|e| e.into()), + ShellPipeReader::StdFile(file) => file.read(buf).map_err(|e| e.into()), + } + } } /// Writer side of a pipe. @@ -321,89 +317,89 @@ impl ShellPipeReader { /// prevent deadlocks where the reader hangs waiting for a read. #[derive(Debug)] pub enum ShellPipeWriter { - OsPipe(os_pipe::PipeWriter), - StdFile(std::fs::File), - // For stdout and stderr, instead of directly duplicating the raw pipes - // and putting them in a ShellPipeWriter::OsPipe(...), we use Rust std's - // stdout() and stderr() wrappers because it contains some code to solve - // some encoding issues on Windows (ex. emojis). For more details, see - // library/std/src/sys/windows/stdio.rs in Rust's source code. - Stdout, - Stderr, - Null, + OsPipe(os_pipe::PipeWriter), + StdFile(std::fs::File), + // For stdout and stderr, instead of directly duplicating the raw pipes + // and putting them in a ShellPipeWriter::OsPipe(...), we use Rust std's + // stdout() and stderr() wrappers because it contains some code to solve + // some encoding issues on Windows (ex. emojis). For more details, see + // library/std/src/sys/windows/stdio.rs in Rust's source code. + Stdout, + Stderr, + Null, } impl Clone for ShellPipeWriter { - fn clone(&self) -> Self { - match self { - Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), - Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), - Self::Stdout => Self::Stdout, - Self::Stderr => Self::Stderr, - Self::Null => Self::Null, - } - } + fn clone(&self) -> Self { + match self { + Self::OsPipe(pipe) => Self::OsPipe(pipe.try_clone().unwrap()), + Self::StdFile(file) => Self::StdFile(file.try_clone().unwrap()), + Self::Stdout => Self::Stdout, + Self::Stderr => Self::Stderr, + Self::Null => Self::Null, + } + } } impl ShellPipeWriter { - pub fn stdout() -> Self { - Self::Stdout - } - - pub fn stderr() -> Self { - Self::Stderr - } - - pub fn null() -> Self { - Self::Null - } - - pub fn from_std(std_file: std::fs::File) -> Self { - Self::StdFile(std_file) - } - - pub fn into_stdio(self) -> std::process::Stdio { - match self { - Self::OsPipe(pipe) => pipe.into(), - Self::StdFile(file) => file.into(), - Self::Stdout => std::process::Stdio::inherit(), - Self::Stderr => std::process::Stdio::inherit(), - Self::Null => std::process::Stdio::null(), - } - } - - pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> { - match self { - Self::OsPipe(pipe) => pipe.write_all(bytes)?, - Self::StdFile(file) => file.write_all(bytes)?, - // For both stdout & stderr, we want to flush after each - // write in order to bypass Rust's internal buffer. - Self::Stdout => { - let mut stdout = std::io::stdout().lock(); - stdout.write_all(bytes)?; - stdout.flush()?; - } - Self::Stderr => { - let mut stderr = std::io::stderr().lock(); - stderr.write_all(bytes)?; - stderr.flush()?; - } - Self::Null => {} - } - Ok(()) - } - - pub fn write_line(&mut self, line: &str) -> Result<()> { - let bytes = format!("{line}\n"); - self.write_all(bytes.as_bytes()) - } + pub fn stdout() -> Self { + Self::Stdout + } + + pub fn stderr() -> Self { + Self::Stderr + } + + pub fn null() -> Self { + Self::Null + } + + pub fn from_std(std_file: std::fs::File) -> Self { + Self::StdFile(std_file) + } + + pub fn into_stdio(self) -> std::process::Stdio { + match self { + Self::OsPipe(pipe) => pipe.into(), + Self::StdFile(file) => file.into(), + Self::Stdout => std::process::Stdio::inherit(), + Self::Stderr => std::process::Stdio::inherit(), + Self::Null => std::process::Stdio::null(), + } + } + + pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> { + match self { + Self::OsPipe(pipe) => pipe.write_all(bytes)?, + Self::StdFile(file) => file.write_all(bytes)?, + // For both stdout & stderr, we want to flush after each + // write in order to bypass Rust's internal buffer. + Self::Stdout => { + let mut stdout = std::io::stdout().lock(); + stdout.write_all(bytes)?; + stdout.flush()?; + } + Self::Stderr => { + let mut stderr = std::io::stderr().lock(); + stderr.write_all(bytes)?; + stderr.flush()?; + } + Self::Null => {} + } + Ok(()) + } + + pub fn write_line(&mut self, line: &str) -> Result<()> { + let bytes = format!("{line}\n"); + self.write_all(bytes.as_bytes()) + } } /// Used to communicate between commands. pub fn pipe() -> (ShellPipeReader, ShellPipeWriter) { - let (reader, writer) = os_pipe::pipe().unwrap(); - ( - ShellPipeReader::OsPipe(reader), - ShellPipeWriter::OsPipe(writer), - ) + let (reader, writer) = os_pipe::pipe().unwrap(); + ( + ShellPipeReader::OsPipe(reader), + ShellPipeWriter::OsPipe(writer), + ) } From d6caed973a3e4e9078686e6fa5a9afaeada16244 Mon Sep 17 00:00:00 2001 From: prsabahrami Date: Sat, 31 Aug 2024 12:26:01 -0400 Subject: [PATCH 7/7] Fixing comment --- crates/shell/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/shell/src/main.rs b/crates/shell/src/main.rs index 9d8586c..c2a63ed 100644 --- a/crates/shell/src/main.rs +++ b/crates/shell/src/main.rs @@ -18,7 +18,7 @@ async fn main() { let list = deno_task_shell::parser::parse(&script_text).unwrap(); - // // execute + // execute let env_vars = std::env::vars().collect(); let cwd = std::env::current_dir().expect("Failed to get current directory");