diff --git a/Cargo.toml b/Cargo.toml index 4c501e14..11d914e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,9 @@ readme = "README.md" repository = "https://github.com/reubeno/brush" version = "0.1.0" +[workspace.lints.rust] +missing_docs = "warn" + [workspace.lints.clippy] all = { level = "deny", priority = -1 } pedantic = { level = "deny", priority = -1 } diff --git a/cli/src/main.rs b/cli/src/main.rs index 7e7eab28..d32e91ab 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,5 @@ +//! The main entry point for the brush shell. + use std::{collections::HashSet, io::IsTerminal, path::Path}; use clap::{builder::styling, Parser}; @@ -72,20 +74,28 @@ struct CommandLineArgs { script_args: Vec, } +/// Type of event to trace. #[derive(Clone, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] enum TraceEvent { + /// Traces parsing and evaluation of arithmetic expressions. #[clap(name = "arithmetic")] Arithmetic, + /// Traces command execution. #[clap(name = "commands")] Commands, + /// Traces command completion generation. #[clap(name = "complete")] Complete, + /// Traces word expansion. #[clap(name = "expand")] Expand, + /// Traces the process of parsing tokens into an abstract syntax tree. #[clap(name = "parse")] Parse, + /// Traces pattern matching. #[clap(name = "pattern")] Pattern, + /// Traces the process of tokenizing input text. #[clap(name = "tokenize")] Tokenize, } diff --git a/cli/tests/cases/builtins/shopt.yaml b/cli/tests/cases/builtins/shopt.yaml index fedacd98..dd18a7da 100644 --- a/cli/tests/cases/builtins/shopt.yaml +++ b/cli/tests/cases/builtins/shopt.yaml @@ -34,11 +34,12 @@ cases: stdin: | shopt extglob - # - name: "shopt -o interactive monitor default" - # args: ["-i"] - # ignore_stderr: true - # stdin: | - # shopt -o monitor + - name: "shopt -o interactive monitor default" + skip: true + args: ["-i"] + ignore_stderr: true + stdin: | + shopt -o monitor - name: "shopt toggle" stdin: | diff --git a/cli/tests/cases/patterns.yaml b/cli/tests/cases/patterns.yaml index b9b474e7..97148aa9 100644 --- a/cli/tests/cases/patterns.yaml +++ b/cli/tests/cases/patterns.yaml @@ -149,7 +149,6 @@ cases: test_pattern "ad" "+(ab|ac)" - name: "Pathname expansion: extglob disabled" - known_failure: true ignore_stderr: true test_files: - path: "ab.txt" @@ -159,23 +158,26 @@ cases: - path: "abadac.txt" - path: "fabadac.txt" - path: "f.txt" - stdin: | - shopt -u extglob + - path: "script.sh" + contents: | + echo !(a*) + echo "result: $?" - echo !(a*) - echo "result: $?" - - echo @(abc|abd).txt - echo "result: $?" + echo @(abc|abd).txt + echo "result: $?" - echo ab?(c).txt - echo "result: $?" + echo ab?(c).txt + echo "result: $?" - echo *(ab|ad|ac).txt - echo "result: $?" + echo *(ab|ad|ac).txt + echo "result: $?" - echo f+(ab|ad|ac).txt - echo "result: $?" + echo f+(ab|ad|ac).txt + echo "result: $?" + stdin: | + shopt -u extglob + chmod +x ./script.sh + ./script.sh - name: "Pathname expansion: Inverted patterns" ignore_stderr: true diff --git a/cli/tests/cases/word_expansion.yaml b/cli/tests/cases/word_expansion.yaml index e6b7a463..bd53ffda 100644 --- a/cli/tests/cases/word_expansion.yaml +++ b/cli/tests/cases/word_expansion.yaml @@ -673,7 +673,6 @@ cases: echo "${var[@]@L}" - name: "Parameter quote transformations - Q" - known_failure: false stdin: | var='""' echo "\${var@Q}: ${var@Q}" @@ -705,11 +704,20 @@ cases: var="Hello, world!" echo "\${var@K}: ${var@K}" + var="a 'b c' d" + echo "\${var@K}: ${var@K}" + declare -a arr1=(a b c) echo "\${arr1@K}: ${arr1@K}" + echo "\${arr1[1]@K}: ${arr1[1]@K}" + echo "\${arr1[@]@K}: ${arr1[@]@K}" + echo "\${arr1[*]@K}: ${arr1[*]@K}" declare -A arr2=(["a"]=1 ["b"]=2) echo "\${arr2@K}: ${arr2@K}" + echo "\${arr2[b]@K}: ${arr2[b]@K}" + echo "\${arr2[@]@K}: ${arr2[@]@K}" + echo "\${arr2[*]@K}: ${arr2[*]@K}" - name: "Parameter quote transformations - k" known_failure: true diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 63459470..d1669df5 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -1,3 +1,5 @@ +//! The test harness for brush shell integration tests. + use anyhow::{Context, Result}; use assert_fs::fixture::{FileWriteStr, PathChild}; use clap::Parser; diff --git a/interactive-shell/src/interactive_shell.rs b/interactive-shell/src/interactive_shell.rs index 4bc5a56a..0c82c785 100644 --- a/interactive-shell/src/interactive_shell.rs +++ b/interactive-shell/src/interactive_shell.rs @@ -7,25 +7,36 @@ use std::{ type Editor = rustyline::Editor; +/// Options for creating an interactive shell. pub struct Options { + /// Lower-level options for creating the shell. pub shell: shell::CreateOptions, + /// Whether to disable bracketed paste mode. pub disable_bracketed_paste: bool, } +/// Represents an interactive shell capable of taking commands from standard input +/// and reporting results to standard output and standard error streams. pub struct InteractiveShell { + /// The `rustyline` editor. editor: Editor, + /// Optional path to the history file used for the shell. history_file_path: Option, } +/// Represents an error encountered while running or otherwise managing an interactive shell. #[allow(clippy::module_name_repetitions)] #[derive(thiserror::Error, Debug)] pub enum InteractiveShellError { + /// An error occurred with the embedded shell. #[error("{0}")] ShellError(#[from] shell::Error), + /// A generic I/O error occurred. #[error("I/O error: {0}")] IoError(#[from] std::io::Error), + /// An error occurred while reading input. #[error("input error: {0}")] ReadlineError(#[from] rustyline::error::ReadlineError), } @@ -37,6 +48,11 @@ enum InteractiveExecutionResult { } impl InteractiveShell { + /// Returns a new interactive shell instance, created with the provided options. + /// + /// # Arguments + /// + /// * `options` - Options for creating the interactive shell. pub async fn new(options: &Options) -> Result { // Set up shell first. Its initialization may influence how the // editor needs to operate. @@ -58,10 +74,12 @@ impl InteractiveShell { }) } + /// Returns an immutable reference to the inner shell object. pub fn shell(&self) -> &shell::Shell { &self.editor.helper().unwrap().shell } + /// Returns a mutable reference to the inner shell object. pub fn shell_mut(&mut self) -> &mut shell::Shell { &mut self.editor.helper_mut().unwrap().shell } @@ -82,6 +100,9 @@ impl InteractiveShell { Ok(editor) } + /// Runs the interactive shell loop, reading commands from standard input and writing + /// results to standard output and standard error. Continues until the shell + /// normally exits or until a fatal error occurs. pub async fn run_interactively(&mut self) -> Result<(), InteractiveShellError> { loop { // Check for any completed jobs. diff --git a/interactive-shell/src/lib.rs b/interactive-shell/src/lib.rs index 35e20921..45818a71 100644 --- a/interactive-shell/src/lib.rs +++ b/interactive-shell/src/lib.rs @@ -1,3 +1,5 @@ +//! Library implementing interactive command input and completion for the brush shell. + mod interactive_shell; pub use interactive_shell::{InteractiveShell, InteractiveShellError, Options}; diff --git a/parser/src/ast.rs b/parser/src/ast.rs index d234e2f2..4104db71 100644 --- a/parser/src/ast.rs +++ b/parser/src/ast.rs @@ -4,8 +4,10 @@ use crate::tokenizer; const DISPLAY_INDENT: &str = " "; +/// Represents a complete shell program. #[derive(Clone, Debug)] pub struct Program { + /// A sequence of complete shell commands. pub complete_commands: Vec, } @@ -18,12 +20,18 @@ impl Display for Program { } } +/// Represents a complete shell command. pub type CompleteCommand = CompoundList; + +/// Represents a complete shell command item. pub type CompleteCommandItem = CompoundListItem; +/// Indicates whether the preceding command is executed synchronously or asynchronously. #[derive(Clone, Debug)] pub enum SeparatorOperator { + /// The preceding command is executed asynchronously. Async, + /// The preceding command is executed synchronously. Sequence, } @@ -36,9 +44,12 @@ impl Display for SeparatorOperator { } } +/// Represents a sequence of command pipelines connected by boolean operators. #[derive(Clone, Debug)] pub struct AndOrList { + /// The first command pipeline. pub first: Pipeline, + /// Any additional command pipelines, in sequence order. pub additional: Vec, } @@ -53,9 +64,15 @@ impl Display for AndOrList { } } +/// Represents a boolean operator used to connect command pipelines, along with the +/// succeeding pipeline. #[derive(Clone, Debug)] pub enum AndOr { + /// Boolean AND operator; the embedded pipeline is only to be executed if the + /// preceding command has succeeded. And(Pipeline), + /// Boolean OR operator; the embedded pipeline is only to be executed if the + /// preceding command has not succeeded. Or(Pipeline), } @@ -68,9 +85,14 @@ impl Display for AndOr { } } +/// A pipeline of commands, where each command's output is passed as standard input +/// to the command that follows it. #[derive(Clone, Debug)] pub struct Pipeline { + /// Indicates whether the result of the overall pipeline should be the logical + /// negation of the result of the pipeline. pub bang: bool, + /// The sequence of commands in the pipeline. pub seq: Vec, } @@ -90,11 +112,17 @@ impl Display for Pipeline { } } +/// Represents a shell command. #[derive(Clone, Debug)] pub enum Command { + /// A simple command, directly invoking an external command, a built-in command, + /// a shell function, or similar. Simple(SimpleCommand), + /// A compound command, composed of multiple commands. Compound(CompoundCommand, Option), + /// A command whose side effect is to define a shell function. Function(FunctionDefinition), + /// A command that evaluates an extended test expression. ExtendedTest(ExtendedTestExpr), } @@ -117,16 +145,27 @@ impl Display for Command { } } +/// Represents a compound command, potentially made up of multiple nested commands. #[derive(Clone, Debug)] pub enum CompoundCommand { + /// An arithmetic command, evaluating an arithmetic expression. Arithmetic(ArithmeticCommand), + /// An arithmetic for clause, which loops until an arithmetic condition is reached. ArithmeticForClause(ArithmeticForClauseCommand), + /// A brace group, which groups commands together. BraceGroup(BraceGroupCommand), + /// A subshell, which executes commands in a subshell. Subshell(SubshellCommand), + /// A for clause, which loops over a set of values. ForClause(ForClauseCommand), + /// A case clause, which selects a command based on a value and a set of + /// pattern-based filters. CaseClause(CaseClauseCommand), + /// An if clause, which conditionally executes a command. IfClause(IfClauseCommand), + /// A while clause, which loops while a condition is met. WhileClause(WhileOrUntilClauseCommand), + /// An until clause, which loops until a condition is met. UntilClause(WhileOrUntilClauseCommand), } @@ -156,8 +195,10 @@ impl Display for CompoundCommand { } } +/// An arithmetic command, evaluating an arithmetic expression. #[derive(Clone, Debug)] pub struct ArithmeticCommand { + /// The raw, unparsed and unexpanded arithmetic expression. pub expr: UnexpandedArithmeticExpr, } @@ -167,6 +208,7 @@ impl Display for ArithmeticCommand { } } +/// A subshell, which executes commands in a subshell. #[derive(Clone, Debug)] pub struct SubshellCommand(pub CompoundList); @@ -178,10 +220,14 @@ impl Display for SubshellCommand { } } +/// A for clause, which loops over a set of values. #[derive(Clone, Debug)] pub struct ForClauseCommand { + /// The name of the iterator variable. pub variable_name: String, + /// The values being iterated over. pub values: Option>, + /// The command to run for each iteration of the loop. pub body: DoGroupCommand, } @@ -205,11 +251,16 @@ impl Display for ForClauseCommand { } } +/// An arithmetic for clause, which loops until an arithmetic condition is reached. #[derive(Clone, Debug)] pub struct ArithmeticForClauseCommand { + /// Optionally, the initializer expression evaluated before the first iteration of the loop. pub initializer: Option, + /// Optionally, the expression evaluated as the exit condition of the loop. pub condition: Option, + /// Optionally, the expression evaluated after each iteration of the loop. pub updater: Option, + /// The command to run for each iteration of the loop. pub body: DoGroupCommand, } @@ -239,9 +290,13 @@ impl Display for ArithmeticForClauseCommand { } } +/// A case clause, which selects a command based on a value and a set of +/// pattern-based filters. #[derive(Clone, Debug)] pub struct CaseClauseCommand { + /// The value being matched on. pub value: Word, + /// The individual case branches. pub cases: Vec, } @@ -256,6 +311,7 @@ impl Display for CaseClauseCommand { } } +/// A sequence of commands. #[derive(Clone, Debug)] pub struct CompoundList(pub Vec); @@ -281,6 +337,7 @@ impl Display for CompoundList { } } +/// An element of a compound command list. #[derive(Clone, Debug)] pub struct CompoundListItem(pub AndOrList, pub SeparatorOperator); @@ -292,10 +349,14 @@ impl Display for CompoundListItem { } } +/// An if clause, which conditionally executes a command. #[derive(Clone, Debug)] pub struct IfClauseCommand { + /// The command whose execution result is inspected. pub condition: CompoundList, + /// The command to execute if the condition is true. pub then: CompoundList, + /// Optionally, `else` clauses that will be evaluated if the condition is false. pub elses: Option>, } @@ -320,9 +381,12 @@ impl Display for IfClauseCommand { } } +/// Represents the `else` clause of a conditional command. #[derive(Clone, Debug)] pub struct ElseClause { + /// If present, the condition that must be met for this `else` clause to be executed. pub condition: Option, + /// The commands to execute if this `else` clause is selected. pub body: CompoundList, } @@ -343,9 +407,12 @@ impl Display for ElseClause { } } +/// An individual matching case item in a case clause. #[derive(Clone, Debug)] pub struct CaseItem { + /// The patterns that select this case branch. pub patterns: Vec, + /// The commands to execute if this case branch is selected. pub cmd: Option, } @@ -368,6 +435,7 @@ impl Display for CaseItem { } } +/// A while or until clause, whose looping is controlled by a condition. #[derive(Clone, Debug)] pub struct WhileOrUntilClauseCommand(pub CompoundList, pub DoGroupCommand); @@ -377,10 +445,14 @@ impl Display for WhileOrUntilClauseCommand { } } +/// Encapsulates the definition of a shell function. #[derive(Clone, Debug)] pub struct FunctionDefinition { + /// The name of the function. pub fname: String, + /// The body of the function. pub body: FunctionBody, + /// The source of the function definition. pub source: String, } @@ -392,6 +464,7 @@ impl Display for FunctionDefinition { } } +/// Encapsulates the body of a function definition. #[derive(Clone, Debug)] pub struct FunctionBody(pub CompoundCommand, pub Option); @@ -406,6 +479,7 @@ impl Display for FunctionBody { } } +/// A brace group, which groups commands together. #[derive(Clone, Debug)] pub struct BraceGroupCommand(pub CompoundList); @@ -420,6 +494,7 @@ impl Display for BraceGroupCommand { } } +/// A do group, which groups commands together. #[derive(Clone, Debug)] pub struct DoGroupCommand(pub CompoundList); @@ -432,10 +507,14 @@ impl Display for DoGroupCommand { } } +/// Represents the invocation of a simple command. #[derive(Clone, Debug)] pub struct SimpleCommand { + /// Optionally, a prefix to the command. pub prefix: Option, + /// The name of the command to execute. pub word_or_name: Option, + /// Optionally, a suffix to the command. pub suffix: Option, } @@ -473,6 +552,7 @@ impl Display for SimpleCommand { } } +/// Represents a prefix to a simple command. #[derive(Clone, Debug, Default)] pub struct CommandPrefix(pub Vec); @@ -489,6 +569,7 @@ impl Display for CommandPrefix { } } +/// Represents a suffix to a simple command; a word argument, declaration, or I/O redirection. #[derive(Clone, Default, Debug)] pub struct CommandSuffix(pub Vec); @@ -505,10 +586,14 @@ impl Display for CommandSuffix { } } +/// A prefix or suffix for a simple command. #[derive(Clone, Debug)] pub enum CommandPrefixOrSuffixItem { + /// An I/O redirection. IoRedirect(IoRedirect), + /// A word. Word(Word), + /// An assignment/declaration word. AssignmentWord(Assignment, Word), } @@ -522,10 +607,14 @@ impl Display for CommandPrefixOrSuffixItem { } } +/// Encapsulates an assignment declaration. #[derive(Clone, Debug)] pub struct Assignment { + /// Name being assigned to. pub name: AssignmentName, + /// Value being assigned. pub value: AssignmentValue, + /// Whether or not to append to the preexisting value associated with the named variable. pub append: bool, } @@ -539,9 +628,12 @@ impl Display for Assignment { } } +/// The target of an assignment. #[derive(Clone, Debug)] pub enum AssignmentName { + /// A named variable. VariableName(String), + /// An element in a named array. ArrayElementName(String, String), } @@ -556,9 +648,12 @@ impl Display for AssignmentName { } } +/// A value being assigned to a variable. #[derive(Clone, Debug)] pub enum AssignmentValue { + /// A scalar (word) value. Scalar(Word), + /// An array of elements. Array(Vec<(Option, Word)>), } @@ -583,6 +678,7 @@ impl Display for AssignmentValue { } } +/// A list of I/O redirections to be applied to a command. #[derive(Clone, Debug)] pub struct RedirectList(pub Vec); @@ -595,11 +691,16 @@ impl Display for RedirectList { } } +/// An I/O redirection. #[derive(Clone, Debug)] pub enum IoRedirect { + /// Redirection to a file. File(Option, IoFileRedirectKind, IoFileRedirectTarget), + /// Redirection from a here-document. HereDocument(Option, IoHereDocument), + /// Redirection from a here-string. HereString(Option, Word), + /// Redirection of both standard output and standard error. OutputAndError(Word), } @@ -651,14 +752,22 @@ impl Display for IoRedirect { } } +/// Kind of file I/O redirection. #[derive(Clone, Debug)] pub enum IoFileRedirectKind { + /// Read (`<`). Read, + /// Write (`>`). Write, + /// Append (`>>`). Append, + /// Read and write (`<>`). ReadAndWrite, + /// Clobber (`>|`). Clobber, + /// Duplicate input (`<&`). DuplicateInput, + /// Duplicate output (`>&`). DuplicateOutput, } @@ -676,10 +785,15 @@ impl Display for IoFileRedirectKind { } } +/// Target for an I/O file redirection. #[derive(Clone, Debug)] pub enum IoFileRedirectTarget { + /// Path to a file. Filename(Word), + /// File descriptor number. Fd(u32), + /// Process substitution: substitution with the results of executing the given + /// command in a subshell. ProcessSubstitution(SubshellCommand), } @@ -695,22 +809,35 @@ impl Display for IoFileRedirectTarget { } } +/// Represents an I/O here document. #[derive(Clone, Debug)] pub struct IoHereDocument { + /// Whether to remove leading tabs from the here document. pub remove_tabs: bool, + /// The delimiter marking the end of the here document. pub here_end: Word, + /// The contents of the here document. pub doc: Word, } +/// A (non-extended) test expression. #[derive(Clone, Debug)] pub enum TestExpr { + /// Always evaluates to false. False, + /// A literal string. Literal(String), + /// Logical AND operation on two nested expressions. And(Box, Box), + /// Logical OR operation on two nested expressions. Or(Box, Box), + /// Logical NOT operation on a nested expression. Not(Box), + /// A parenthesized expression. Parenthesized(Box), + /// A unary test operation. UnaryTest(UnaryPredicate, String), + /// A binary test operation. BinaryTest(BinaryPredicate, String, String), } @@ -729,13 +856,20 @@ impl Display for TestExpr { } } +/// An extended test expression. #[derive(Clone, Debug)] pub enum ExtendedTestExpr { + /// Logical AND operation on two nested expressions. And(Box, Box), + /// Logical OR operation on two nested expressions. Or(Box, Box), + /// Logical NOT operation on a nested expression. Not(Box), + /// A parenthesized expression. Parenthesized(Box), + /// A unary test operation. UnaryTest(UnaryPredicate, Word), + /// A binary test operation. BinaryTest(BinaryPredicate, Word, Word), } @@ -764,31 +898,56 @@ impl Display for ExtendedTestExpr { } } +/// A unary predicate usable in an extended test expression. #[derive(Clone, Debug)] pub enum UnaryPredicate { + /// Computes if the operand is a path to an existing file. FileExists, + /// Computes if the operand is a path to an existing block device file. FileExistsAndIsBlockSpecialFile, + /// Computes if the operand is a path to an existing character device file. FileExistsAndIsCharSpecialFile, + /// Computes if the operand is a path to an existing directory. FileExistsAndIsDir, + /// Computes if the operand is a path to an existing regular file. FileExistsAndIsRegularFile, + /// Computes if the operand is a path to an existing file with the setgid bit set. FileExistsAndIsSetgid, + /// Computes if the operand is a path to an existing symbolic link. FileExistsAndIsSymlink, + /// Computes if the operand is a path to an existing file with the sticky bit set. FileExistsAndHasStickyBit, + /// Computes if the operand is a path to an existing FIFO file. FileExistsAndIsFifo, + /// Computes if the operand is a path to an existing file that is readable. FileExistsAndIsReadable, + /// Computes if the operand is a path to an existing file with a non-zero length. FileExistsAndIsNotZeroLength, + /// Computes if the operand is a file descriptor that is an open terminal. FdIsOpenTerminal, + /// Computes if the operand is a path to an existing file with the setuid bit set. FileExistsAndIsSetuid, + /// Computes if the operand is a path to an existing file that is writable. FileExistsAndIsWritable, + /// Computes if the operand is a path to an existing file that is executable. FileExistsAndIsExecutable, + /// Computes if the operand is a path to an existing file owned by the current context's effective group ID. FileExistsAndOwnedByEffectiveGroupId, + /// Computes if the operand is a path to an existing file that has been modified since last being read. FileExistsAndModifiedSinceLastRead, + /// Computes if the operand is a path to an existing file owned by the current context's effective user ID. FileExistsAndOwnedByEffectiveUserId, + /// Computes if the operand is a path to an existing socket file. FileExistsAndIsSocket, + /// Computes if the operand is a 'set -o' option that is enabled. ShellOptionEnabled, + /// Computes if the operand names a shell variable that is set and assigned a value. ShellVariableIsSetAndAssigned, + /// Computes if the operand names a shell variable that is set and of nameref type. ShellVariableIsSetAndNameRef, + /// Computes if the operand is a string with zero length. StringHasZeroLength, + /// Computes if the operand is a string with non-zero length. StringHasNonZeroLength, } @@ -823,22 +982,38 @@ impl Display for UnaryPredicate { } } +/// A binary predicate usable in an extended test expression. #[derive(Clone, Debug)] pub enum BinaryPredicate { + /// Computes if two files refer to the same device and inode numbers. FilesReferToSameDeviceAndInodeNumbers, + /// Computes if the left file is newer than the right, or exists when the right does not. LeftFileIsNewerOrExistsWhenRightDoesNot, + /// Computes if the left file is older than the right, or does not exist when the right does. LeftFileIsOlderOrDoesNotExistWhenRightDoes, + /// Computes if a string exactly matches a pattern. StringExactlyMatchesPattern, + /// Computes if a string does not exactly match a pattern. StringDoesNotExactlyMatchPattern, + /// Computes if a string matches a regular expression. StringMatchesRegex, + /// Computes if a string contains a substring. StringContainsSubstring, + /// Computes if the left value sorts before the right. LeftSortsBeforeRight, + /// Computes if the left value sorts after the right. LeftSortsAfterRight, + /// Computes if two values are equal via arithmetic comparison. ArithmeticEqualTo, + /// Computes if two values are not equal via arithmetic comparison. ArithmeticNotEqualTo, + /// Computes if the left value is less than the right via arithmetic comparison. ArithmeticLessThan, + /// Computes if the left value is less than or equal to the right via arithmetic comparison. ArithmeticLessThanOrEqualTo, + /// Computes if the left value is greater than the right via arithmetic comparison. ArithmeticGreaterThan, + /// Computes if the left value is greater than or equal to the right via arithmetic comparison. ArithmeticGreaterThanOrEqualTo, } @@ -864,8 +1039,10 @@ impl Display for BinaryPredicate { } } +/// Represents a shell word. #[derive(Clone, Debug)] pub struct Word { + /// Raw text of the word. pub value: String, } @@ -889,19 +1066,23 @@ impl From<&tokenizer::Token> for Word { } impl Word { + /// Constructs a new `Word` from a given string. pub fn new(s: &str) -> Self { Self { value: s.to_owned(), } } + /// Returns the raw text of the word, consuming the `Word`. pub fn flatten(&self) -> String { self.value.clone() } } +/// Encapsulates an unparsed arithmetic expression. #[derive(Clone, Debug)] pub struct UnexpandedArithmeticExpr { + /// The raw text of the expression. pub value: String, } @@ -911,19 +1092,28 @@ impl Display for UnexpandedArithmeticExpr { } } +/// An arithmetic expression. #[derive(Clone, Debug)] pub enum ArithmeticExpr { + /// A literal integer value. Literal(i64), + /// A dereference of a variable or array element. Reference(ArithmeticTarget), + /// A unary operation on an the result of a given nested expression. UnaryOp(UnaryOperator, Box), + /// A binary operation on two nested expressions. BinaryOp(BinaryOperator, Box, Box), + /// A ternary conditional expression. Conditional( Box, Box, Box, ), + /// An assignment operation. Assignment(ArithmeticTarget, Box), + /// A binary assignment operation. BinaryAssignment(BinaryOperator, ArithmeticTarget, Box), + /// A unary assignment operation. UnaryAssignment(UnaryAssignmentOperator, ArithmeticTarget), } @@ -957,27 +1147,48 @@ impl Display for ArithmeticExpr { } } +/// A binary arithmetic operator. #[derive(Clone, Copy, Debug)] pub enum BinaryOperator { + /// Exponentiation (e.g., `x ** y`). Power, + /// Multiplication (e.g., `x * y`). Multiply, + /// Division (e.g., `x / y`). Divide, + /// Modulo (e.g., `x % y`). Modulo, + /// Comma (e.g., `x, y`). Comma, + /// Addition (e.g., `x + y`). Add, + /// Subtraction (e.g., `x - y`). Subtract, + /// Bitwise left shift (e.g., `x << y`). ShiftLeft, + /// Bitwise right shift (e.g., `x >> y`). ShiftRight, + /// Less than (e.g., `x < y`). LessThan, + /// Less than or equal to (e.g., `x <= y`). LessThanOrEqualTo, + /// Greater than (e.g., `x > y`). GreaterThan, + /// Greater than or equal to (e.g., `x >= y`). GreaterThanOrEqualTo, + /// Equals (e.g., `x == y`). Equals, + /// Not equals (e.g., `x != y`). NotEquals, + /// Bitwise AND (e.g., `x & y`). BitwiseAnd, + /// Bitwise exclusive OR (xor) (e.g., `x ^ y`). BitwiseXor, + /// Bitwise OR (e.g., `x | y`). BitwiseOr, + /// Logical AND (e.g., `x && y`). LogicalAnd, + /// Logical OR (e.g., `x || y`). LogicalOr, } @@ -1008,11 +1219,16 @@ impl Display for BinaryOperator { } } +/// A unary arithmetic operator. #[derive(Clone, Copy, Debug)] pub enum UnaryOperator { + /// Unary plus (e.g., `+x`). UnaryPlus, + /// Unary minus (e.g., `-x`). UnaryMinus, + /// Bitwise not (e.g., `~x`). BitwiseNot, + /// Logical not (e.g., `!x`). LogicalNot, } @@ -1027,11 +1243,16 @@ impl Display for UnaryOperator { } } +/// A unary arithmetic assignment operator. #[derive(Clone, Copy, Debug)] pub enum UnaryAssignmentOperator { + /// Prefix increment (e.g., `++x`). PrefixIncrement, + /// Prefix increment (e.g., `--x`). PrefixDecrement, + /// Postfix increment (e.g., `x++`). PostfixIncrement, + /// Postfix decrement (e.g., `x--`). PostfixDecrement, } @@ -1046,9 +1267,12 @@ impl Display for UnaryAssignmentOperator { } } +/// Identifies the target of an arithmetic assignment expression. #[derive(Clone, Debug)] pub enum ArithmeticTarget { + /// A named variable. Variable(String), + /// An element in an array. ArrayElement(String, Box), } diff --git a/parser/src/error.rs b/parser/src/error.rs index 2c8a9ed7..1a8659b4 100644 --- a/parser/src/error.rs +++ b/parser/src/error.rs @@ -1,36 +1,50 @@ use crate::tokenizer; use crate::Token; +/// Represents an error that occurred while parsing tokens. #[derive(Debug)] pub enum ParseError { + /// A parsing error occurred near the given token. ParsingNearToken(Token), + /// A parsing error occurred at the end of the input. ParsingAtEndOfInput, + /// An error occurred while tokenizing the input stream. Tokenizing { + /// The inner error. inner: tokenizer::TokenizerError, + /// Optionally provides the position of the error. position: Option, }, } +/// Represents an error that occurred while parsing a word. #[derive(Debug, thiserror::Error)] pub enum WordParseError { + /// An error occurred while parsing an arithmetic expression. #[error("failed to parse arithmetic expression")] ArithmeticExpression(peg::error::ParseError), + /// An error occurred while parsing a shell pattern. #[error("failed to parse pattern")] Pattern(peg::error::ParseError), + /// An error occurred while parsing a prompt string. #[error("failed to parse prompt string")] Prompt(peg::error::ParseError), + /// An error occurred while parsing a parameter. #[error("failed to parse parameter '{0}'")] Parameter(String, peg::error::ParseError), + /// An error occurred while parsing a word. #[error("failed to parse word '{0}'")] Word(String, peg::error::ParseError), } +/// Represents an error that occurred while parsing a (non-extended) test command. #[derive(Debug, thiserror::Error)] pub enum TestCommandParseError { + /// An error occurred while parsing a test command. #[error("failed to parse test command")] TestCommand(peg::error::ParseError), } diff --git a/parser/src/parser.rs b/parser/src/parser.rs index c8380d69..7a16630e 100644 --- a/parser/src/parser.rs +++ b/parser/src/parser.rs @@ -2,11 +2,16 @@ use crate::ast::{self, SeparatorOperator}; use crate::error; use crate::tokenizer::{Token, TokenEndReason, Tokenizer, TokenizerOptions, Tokens}; +/// Options used to control the behavior of the parser. #[derive(Clone, Eq, Hash, PartialEq)] pub struct ParserOptions { + /// Whether or not to enable extended globbing (a.k.a. `extglob`). pub enable_extended_globbing: bool, + /// Whether or not to enable POSIX complaince mode. pub posix_mode: bool, + /// Whether or not to enable maximal compatibility with the `sh` shell. pub sh_mode: bool, + /// Whether or not to perform tilde expansion. pub tilde_expansion: bool, } @@ -21,6 +26,7 @@ impl Default for ParserOptions { } } +/// Implements parsing for shell programs. pub struct Parser { reader: R, options: ParserOptions, @@ -93,6 +99,13 @@ impl Parser { } } +/// Parses a sequence of tokens into the abstract syntax tree (AST) of a shell program. +/// +/// # Arguments +/// +/// * `tokens` - The tokens to parse. +/// * `options` - The options to use when parsing. +/// * `source_info` - Information about the source of the tokens. pub fn parse_tokens( tokens: &Vec, options: &ParserOptions, @@ -180,8 +193,10 @@ impl<'a> peg::ParseSlice<'a> for Tokens<'a> { } } +/// Information about the source of tokens. #[derive(Clone, Default)] pub struct SourceInfo { + /// The source of the tokens. pub source: String, } diff --git a/parser/src/pattern.rs b/parser/src/pattern.rs index c0ac3919..26e2eaa1 100644 --- a/parser/src/pattern.rs +++ b/parser/src/pattern.rs @@ -1,13 +1,25 @@ use crate::error; +/// Represents the kind of an extended glob. pub enum ExtendedGlobKind { + /// The `+` extended glob; matches one or more occurrences of the inner pattern. Plus, + /// The `@` extended glob; allows matching an alternation of inner patterns. At, + /// The `!` extended glob; matches the negation of the inner pattern. Exclamation, + /// The `?` extended glob; matches zero or one occurrence of the inner pattern. Question, + /// The `*` extended glob; matches zero or more occurrences of the inner pattern. Star, } +/// Converts a shell pattern to a regular expression string. +/// +/// # Arguments +/// +/// * `pattern` - The shell pattern to convert. +/// * `enable_extended_globbing` - Whether to enable extended globbing (extglob). pub fn pattern_to_regex_str( pattern: &str, enable_extended_globbing: bool, diff --git a/parser/src/prompt.rs b/parser/src/prompt.rs index c37d25d5..1f7204d3 100644 --- a/parser/src/prompt.rs +++ b/parser/src/prompt.rs @@ -1,46 +1,78 @@ use crate::error; +/// A piece of a prompt string. #[derive(Clone)] pub enum PromptPiece { + /// An ASCII character. AsciiCharacter(u32), + /// A backslash character. Backslash, + /// The bell character. BellCharacter, + /// A carriage return character. CarriageReturn, + /// The current command number. CurrentCommandNumber, + /// The current history number. CurrentHistoryNumber, + /// The name of the current user. CurrentUser, + /// Path to the current working directory. CurrentWorkingDirectory { + /// Whether or not to apply tilde-replacement before expanding. tilde_replaced: bool, basename: bool, }, + /// The current date, using the given format. Date(PromptDateFormat), + /// The dollar or pound character. DollarOrPound, + /// Special marker indicating the end of a non-printing sequence of characters. EndNonPrintingSequence, + /// The escape character. EscapeCharacter, + /// The hostname of the system. Hostname { + /// Whether or not to include only up to the first dot of the name. only_up_to_first_dot: bool, }, + /// A literal string. Literal(String), + /// A newline character. Newline, + /// The number of actively managed jobs. NumberOfManagedJobs, + /// The base name of the shell. ShellBaseName, + /// The release of the shell. ShellRelease, + /// The version of the shell. ShellVersion, + /// Special marker indicating the start of a non-printing sequence of characters. StartNonPrintingSequence, + /// The base name of the terminal device. TerminalDeviceBaseName, + /// The current time, using the given format. Time(PromptTimeFormat), } +/// Format for a date in a prompt. #[derive(Clone)] pub enum PromptDateFormat { + /// A format including weekday, month, and date. WeekdayMonthDate, + /// A customer string format. Custom(String), } +/// Format for a time in a prompt. #[derive(Clone)] pub enum PromptTimeFormat { + /// A twelve-hour time format with AM/PM. TwelveHourAM, + /// A twelve-hour time format (HHMMSS). TwelveHourHHMMSS, + /// A twenty-four-hour time format (HHMMSS). TwentyFourHourHHMMSS, } diff --git a/parser/src/tokenizer.rs b/parser/src/tokenizer.rs index 2a09ead1..b861bad2 100644 --- a/parser/src/tokenizer.rs +++ b/parser/src/tokenizer.rs @@ -16,10 +16,14 @@ pub(crate) enum TokenEndReason { Other, } +/// Represents a position in a source shell script. #[derive(Clone, Default, Debug)] pub struct SourcePosition { + /// The 0-based index of the character in the input stream. pub index: i32, + /// The 1-based line number. pub line: i32, + /// The 1-based column number. pub column: i32, } @@ -29,15 +33,21 @@ impl Display for SourcePosition { } } +/// Represents the location of a token in its source shell script. #[derive(Clone, Default, Debug)] pub struct TokenLocation { + /// The start position of the token. pub start: SourcePosition, + /// The end position of the token (exclusive). pub end: SourcePosition, } +/// Represents a token extracted from a shell script. #[derive(Clone, Debug)] pub enum Token { + /// An operator token. Operator(String, TokenLocation), + /// A word token. Word(String, TokenLocation), } @@ -57,44 +67,59 @@ impl Token { } } +/// Encapsulates the result of tokenizing a shell script. #[derive(Clone, Debug)] pub(crate) struct TokenizeResult { + /// Reason for tokenization ending. pub reason: TokenEndReason, + /// The token that was extracted, if any. pub token: Option, } +/// Represents an error that occurred during tokenization. #[derive(thiserror::Error, Debug)] pub enum TokenizerError { + /// An unterminated escape sequence was encountered at the end of the input stream. #[error("unterminated escape sequence")] UnterminatedEscapeSequence, + /// An unterminated single-quoted substring was encountered at the end of the input stream. #[error("unterminated single quote at {0}")] UnterminatedSingleQuote(SourcePosition), + /// An unterminated double-quoted substring was encountered at the end of the input stream. #[error("unterminated double quote at {0}")] UnterminatedDoubleQuote(SourcePosition), + /// An unterminated back-quoted substring was encountered at the end of the input stream. #[error("unterminated backquote near {0}")] UnterminatedBackquote(SourcePosition), + /// An unterminated extended glob (extglob) pattern was encountered at the end of the input stream. #[error("unterminated extglob near {0}")] UnterminatedExtendedGlob(SourcePosition), + /// An error occurred decoding UTF-8 characters in the input stream. #[error("failed to decode UTF-8 characters")] FailedDecoding, + /// An I/O here tag was missing. #[error("missing here tag for here document body")] MissingHereTagForDocumentBody, + /// The indicated I/O here tag was missing. #[error("missing here tag '{0}'")] MissingHereTag(String), + /// An unterminated here document sequence was encountered at the end of the input stream. #[error("unterminated here document sequence; tag(s) found at: [{0}]")] UnterminatedHereDocuments(String), + /// An I/O error occurred while reading from the input stream. #[error("failed to read input")] ReadError(#[from] std::io::Error), + /// An unimplemented tokenization feature was encountered. #[error("unimplemented tokenization: {0}")] Unimplemented(&'static str), } @@ -113,8 +138,10 @@ impl TokenizerError { } } +/// Encapsulates a sequence of tokens. #[derive(Debug)] pub(crate) struct Tokens<'a> { + /// Sequence of tokens. pub tokens: &'a [Token], } @@ -163,9 +190,12 @@ struct CrossTokenParseState { arithmetic_expansion: bool, } +/// Options controlling how the tokenizer operates. #[derive(Clone, Debug)] pub struct TokenizerOptions { + /// Whether or not to enable extended globbing patterns (extglob). pub enable_extended_globbing: bool, + /// Whether or not to operate in POSIX compliance mode. pub posix_mode: bool, } @@ -178,12 +208,14 @@ impl Default for TokenizerOptions { } } +/// A tokenizer for shell scripts. pub(crate) struct Tokenizer<'a, R: ?Sized + std::io::BufRead> { char_reader: std::iter::Peekable>, cross_state: CrossTokenParseState, options: TokenizerOptions, } +/// Encapsulates the current token parsing state. #[derive(Clone, Debug)] struct TokenParseState { pub start_position: SourcePosition, @@ -348,6 +380,11 @@ impl TokenParseState { } } +/// Break the given input shell script string into tokens, returning the tokens. +/// +/// # Arguments +/// +/// * `input` - The shell script to tokenize. pub fn tokenize_str(input: &str) -> Result, TokenizerError> { let mut reader = std::io::BufReader::new(input.as_bytes()); let mut tokenizer = crate::tokenizer::Tokenizer::new(&mut reader, &TokenizerOptions::default()); diff --git a/parser/src/word.rs b/parser/src/word.rs index 658f92fe..1726d2b6 100644 --- a/parser/src/word.rs +++ b/parser/src/word.rs @@ -2,130 +2,175 @@ use crate::ast; use crate::error; use crate::ParserOptions; +/// Represents a piece of a word. #[derive(Debug)] pub enum WordPiece { + /// A simple unquoted, unescaped string. Text(String), + /// A string that is single-quoted. SingleQuotedText(String), + /// A string that is ANSI-C quoted. AnsiCQuotedText(String), + /// A sequence of pieces that are embedded in double quotes. DoubleQuotedSequence(Vec), + /// A tilde prefix. TildePrefix(String), + /// A parameter expansion. ParameterExpansion(ParameterExpr), + /// A command substitution. CommandSubstitution(String), + /// An escape sequence. EscapeSequence(String), + /// An arithmetic expression. ArithmeticExpression(ast::UnexpandedArithmeticExpr), } +/// Type of a parameter test. #[derive(Debug)] pub enum ParameterTestType { + /// Check for unset or null. UnsetOrNull, + // Check for unset. Unset, } +/// A parameter, used in a parameter expansion. #[derive(Debug)] pub enum Parameter { + /// A 0-indexed positional parameter. Positional(u32), + /// A special parameter. Special(SpecialParameter), + /// A named variable. Named(String), + /// An index into a named variable. NamedWithIndex { name: String, index: String }, + /// A named array variable with all indices. NamedWithAllIndices { name: String, concatenate: bool }, } +/// A special parameter, used in a parameter expansion. #[derive(Debug)] pub enum SpecialParameter { + /// All positional parameters. AllPositionalParameters { concatenate: bool }, + /// The count of positional parameters. PositionalParameterCount, + /// The last exit status in the shell. LastExitStatus, + /// The current shell option flags. CurrentOptionFlags, + /// The current shell process ID. ProcessId, + /// The last background process ID managed by the shell. LastBackgroundProcessId, + /// The name of the shell. ShellName, } +/// A parameter expression, used in a parameter expansion. #[derive(Debug)] pub enum ParameterExpr { + /// A parameter, with optional indirection. Parameter { parameter: Parameter, indirect: bool, }, + /// Conditionally use default values. UseDefaultValues { parameter: Parameter, indirect: bool, test_type: ParameterTestType, default_value: Option, }, + /// Conditionally assign default values. AssignDefaultValues { parameter: Parameter, indirect: bool, test_type: ParameterTestType, default_value: Option, }, + /// Indicate error if null or unset. IndicateErrorIfNullOrUnset { parameter: Parameter, indirect: bool, test_type: ParameterTestType, error_message: Option, }, + /// Conditionally use an alternative value. UseAlternativeValue { parameter: Parameter, indirect: bool, test_type: ParameterTestType, alternative_value: Option, }, + /// Compute the length of the given parameter. ParameterLength { parameter: Parameter, indirect: bool, }, + /// Remove the smallest suffix from the given string matching the given pattern. RemoveSmallestSuffixPattern { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Remove the largest suffix from the given string matching the given pattern. RemoveLargestSuffixPattern { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Remove the smallest prefix from the given string matching the given pattern. RemoveSmallestPrefixPattern { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Remove the largest prefix from the given string matching the given pattern. RemoveLargestPrefixPattern { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Extract a substring from the given parameter. Substring { parameter: Parameter, indirect: bool, offset: ast::UnexpandedArithmeticExpr, length: Option, }, + /// Transform the given parameter. Transform { parameter: Parameter, indirect: bool, op: ParameterTransformOp, }, + /// Uppercase the first character of the given parameter. UppercaseFirstChar { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Uppercase the portion of the given parameter matching the given pattern. UppercasePattern { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Lowercase the first character of the given parameter. LowercaseFirstChar { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Lowercase the portion of the given parameter matching the given pattern. LowercasePattern { parameter: Parameter, indirect: bool, pattern: Option, }, + /// Replace occurrences of the given pattern in the given parameter. ReplaceSubstring { parameter: Parameter, indirect: bool, @@ -133,37 +178,57 @@ pub enum ParameterExpr { replacement: String, match_kind: SubstringMatchKind, }, - VariableNames { - prefix: String, - concatenate: bool, - }, + /// Select variable names from the environment with a given prefix. + VariableNames { prefix: String, concatenate: bool }, + /// Select member keys from the named array. MemberKeys { variable_name: String, concatenate: bool, }, } +/// Kind of substring match. #[derive(Debug)] pub enum SubstringMatchKind { + /// Match the prefix of the string. Prefix, + /// Match the suffix of the string. Suffix, + /// Match the first occurrence in the string. FirstOccurrence, + /// Match all instances in the string. Anywhere, } +/// Kind of operation to apply to a parameter. #[derive(Debug)] pub enum ParameterTransformOp { + /// Capitalizate initials. CapitalizeInitial, + /// Expand escape sequences. ExpandEscapeSequences, + /// Possibly quote with arrays expanded. PossiblyQuoteWithArraysExpanded { separate_words: bool }, + /// Apply prompt expansion. PromptExpand, + /// Quote the parameter. Quoted, + /// Translate to a format usable in an assignment/declaration. ToAssignmentLogic, + /// Translate to the parameter's attribute flags. ToAttributeFlags, + /// Translate to lowercase. ToLowerCase, + /// Translate to uppercase. ToUpperCase, } +/// Parse a word into its constituent pieces. +/// +/// # Arguments +/// +/// * `word` - The word to parse. +/// * `options` - The parser options to use. pub fn parse_word_for_expansion( word: &str, options: &ParserOptions, @@ -178,6 +243,12 @@ pub fn parse_word_for_expansion( Ok(pieces) } +/// Parse the given word into a parameter expression. +/// +/// # Arguments +/// +/// * `word` - The word to parse. +/// * `options` - The parser options to use. pub fn parse_parameter( word: &str, options: &ParserOptions, diff --git a/shell/benches/shell.rs b/shell/benches/shell.rs index 6499947a..01c76d78 100644 --- a/shell/benches/shell.rs +++ b/shell/benches/shell.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + #[cfg(unix)] mod unix { use criterion::{black_box, Criterion}; @@ -48,6 +50,7 @@ mod unix { .unwrap() } + /// This function defines core shell benchmarks. pub(crate) fn criterion_benchmark(c: &mut Criterion) { c.bench_function("instantiate_shell", |b| { b.to_async(tokio()).iter(|| black_box(instantiate_shell())); diff --git a/shell/src/arithmetic.rs b/shell/src/arithmetic.rs index aed90f6b..f70ee468 100644 --- a/shell/src/arithmetic.rs +++ b/shell/src/arithmetic.rs @@ -3,32 +3,47 @@ use std::borrow::Cow; use crate::{env, expansion, variables, Shell}; use parser::ast; +/// Represents an error that occurs during evaluation of an arithmetic expression. #[derive(Debug, thiserror::Error)] pub enum EvalError { + /// Division by zero. #[error("division by zero")] DivideByZero, + /// Failed to tokenize an arithmetic expression. #[error("failed to tokenize expression")] FailedToTokenizeExpression, + /// Failed to expand an arithmetic expression. #[error("failed to expand expression")] FailedToExpandExpression, + /// Failed to access an element of an array. #[error("failed to access array")] FailedToAccessArray, + /// Failed to update the shell environment in an assignment operator. #[error("failed to update environment")] FailedToUpdateEnvironment, + /// Failed to parse an arithmetic expression. #[error("failed to parse expression: '{0}'")] ParseError(String), + /// Failed to trace an arithmetic expression. #[error("failed tracing expression")] TraceError, } +/// Trait implemented by arithmetic expressions that can be evaluated. #[async_trait::async_trait] pub trait ExpandAndEvaluate { + /// Evaluate the given expression, returning the resulting numeric value. + /// + /// # Arguments + /// + /// * `shell` - The shell to use for evaluation. + /// * `trace_if_needed` - Whether to trace the evaluation. async fn eval(&self, shell: &mut Shell, trace_if_needed: bool) -> Result; } @@ -56,8 +71,14 @@ impl ExpandAndEvaluate for ast::UnexpandedArithmeticExpr { } } +/// Trait implemented by evaluatable arithmetic expressions. #[async_trait::async_trait] pub trait Evaluatable { + /// Evaluate the given arithmetic expression, returning the resulting numeric value. + /// + /// # Arguments + /// + /// * `shell` - The shell to use for evaluation. async fn eval(&self, shell: &mut Shell) -> Result; } diff --git a/shell/src/builtin.rs b/shell/src/builtin.rs index 0c0378a3..03fc5d73 100644 --- a/shell/src/builtin.rs +++ b/shell/src/builtin.rs @@ -7,6 +7,8 @@ use crate::context; use crate::error; use crate::ExecutionResult; +/// Macro to define a struct that represents a shell built-in flag argument that can be +/// enabled or disabled by specifying an option with a leading '+' or '-' character. #[macro_export] macro_rules! minus_or_plus_flag_arg { ($struct_name:ident, $flag_char:literal, $desc:literal) => { @@ -43,20 +45,31 @@ macro_rules! minus_or_plus_flag_arg { pub(crate) use minus_or_plus_flag_arg; +/// Result of executing a built-in command. #[allow(clippy::module_name_repetitions)] pub struct BuiltinResult { + /// The exit code from the command. pub exit_code: BuiltinExitCode, } +/// Exit codes for built-in commands. #[allow(clippy::module_name_repetitions)] pub enum BuiltinExitCode { + /// The command was successful. Success, + /// The inputs to the command were invalid. InvalidUsage, + /// The command is not implemented. Unimplemented, + /// The command returned a specific custom numerical exit code. Custom(u8), + /// The command is requesting to exit the shell, yielding the given exit code. ExitShell(u8), + /// The command is requesting to return from a function or script, yielding the given exit code. ReturnFromFunctionOrScript(u8), + /// The command is requesting to continue a loop, identified by the given nesting count. ContinueLoop(u8), + /// The command is requesting to break a loop, identified by the given nesting count. BreakLoop(u8), } @@ -78,15 +91,27 @@ impl From for BuiltinExitCode { } } +/// Type of a function implementing a built-in command. +/// +/// # Arguments +/// +/// * The context in which the command is being executed. +/// * The arguments to the command. #[allow(clippy::module_name_repetitions)] pub type BuiltinCommandExecuteFunc = fn( context::CommandExecutionContext<'_>, Vec, ) -> BoxFuture<'_, Result>; +/// Trait implemented by built-in shell commands. #[allow(clippy::module_name_repetitions)] #[async_trait::async_trait] pub trait BuiltinCommand: Parser { + /// Instantiates the built-in command with the given arguments. + /// + /// # Arguments + /// + /// * `args` - The arguments to the command. fn new(args: I) -> Result where I: IntoIterator, @@ -108,15 +133,27 @@ pub trait BuiltinCommand: Parser { } } + /// Returns whether or not the command takes options with a leading '+' or '-' character. fn takes_plus_options() -> bool { false } + /// Executes the built-in command in the provided context. + /// + /// # Arguments + /// + /// * `context` - The context in which the command is being executed. async fn execute( &self, context: context::CommandExecutionContext<'_>, ) -> Result; + /// Returns the textual help content associated with the command. + /// + /// # Arguments + /// + /// * `name` - The name of the command. + /// * `content_type` - The type of content to retrieve. fn get_content(name: &str, content_type: BuiltinContentType) -> String { let mut clap_command = Self::command().styles(brush_help_styles()); clap_command.set_bin_name(name); @@ -131,19 +168,31 @@ pub trait BuiltinCommand: Parser { } } +/// Trait implemented by built-in shell commands that take specially handled declarations +/// as arguments. #[allow(clippy::module_name_repetitions)] #[async_trait::async_trait] pub trait BuiltinDeclarationCommand: BuiltinCommand { + /// Stores the declarations within the command instance. + /// + /// # Arguments + /// + /// * `declarations` - The declarations to store. fn set_declarations(&mut self, declarations: Vec); } +/// Type of help content, typically associated with a built-in command. #[allow(clippy::module_name_repetitions)] pub enum BuiltinContentType { + /// Detailed help content for the command. DetailedHelp, + /// Short usage information for the command. ShortUsage, + /// Short description for the command. ShortDescription, } +/// Encapsulates a registration for a built-in command. #[allow(clippy::module_name_repetitions)] #[derive(Clone)] pub struct BuiltinRegistration { diff --git a/shell/src/commands.rs b/shell/src/commands.rs index fe563f89..fcd0edd4 100644 --- a/shell/src/commands.rs +++ b/shell/src/commands.rs @@ -9,9 +9,13 @@ use crate::{ Shell, }; +/// An argument to a command. #[derive(Clone, Debug)] pub enum CommandArg { + /// A simple string argument. String(String), + /// An assignment/declaration; typically treated as a string, but will + /// be specially handled by a limited set of built-in commands. Assignment(ast::Assignment), } diff --git a/shell/src/completion.rs b/shell/src/completion.rs index 847afd1e..5aecb4f8 100644 --- a/shell/src/completion.rs +++ b/shell/src/completion.rs @@ -111,84 +111,123 @@ pub enum CompleteOption { PlusDirs, } +/// Encapsulates the shell's programmable command completion configuration. #[allow(clippy::module_name_repetitions)] #[derive(Clone, Default)] pub struct CompletionConfig { commands: HashMap, + /// Optionally, a completion spec to be used as a default, when earlier + /// matches yield no candidates. pub default: Option, + /// Optionally, a completion spec to be used when the command line is empty. pub empty_line: Option, + /// Optionally, a completion spec to be used for the initial word of a command line. pub initial_word: Option, + /// Optionally, stores the current completion options in effect. May be mutated + /// while a completion generation is in-flight. pub current_completion_options: Option, } +/// Options for generating completions. #[allow(clippy::module_name_repetitions)] #[derive(Clone, Debug, Default)] pub struct CompletionOptions { // // Options // + /// Perform rest of default completions if no completions are generated. pub bash_default: bool, + /// Use default filename completion if no completions are generated. pub default: bool, + /// Treat completions as directory names. pub dir_names: bool, + /// Treat completions as filenames. pub file_names: bool, + /// Do not add usual quoting for completions. pub no_quote: bool, + /// Do not sort completions. pub no_sort: bool, + /// Do not append typical space to a completion at the end of the input line. pub no_space: bool, + /// Also complete with directory names. pub plus_dirs: bool, } +/// Encapsulates a command completion specification; provides policy for how to +/// generate completions for a given input. #[allow(clippy::module_name_repetitions)] #[derive(Clone, Debug, Default)] pub struct CompletionSpec { // // Options // + /// Options to use for completion. pub options: CompletionOptions, // // Generators // + /// Actions to take to generate completions. pub actions: Vec, + /// Optionally, a glob pattern whose expansion will be used as completions. pub glob_pattern: Option, + /// Optionally, a list of words to use as completions. pub word_list: Option, + /// Optionally, the name of a shell function to invoke to generate completions. pub function_name: Option, + /// Optionally, the name of a command to execute to generate completions. pub command: Option, // // Filters // + /// Optionally, a pattern to filter completions. pub filter_pattern: Option, + /// If true, completion candidates matching `filter_pattern` are removed; + /// otherwise, those not matching it are removed. pub filter_pattern_excludes: bool, // // Transformers // + /// Optionally, provides a prefix to be prepended to all completion candidates. pub prefix: Option, + /// Optionally, provides a suffix to be prepended to all completion candidates. pub suffix: Option, } +/// Encapsulates context used during completion generation. #[allow(clippy::module_name_repetitions)] #[derive(Debug)] pub struct CompletionContext<'a> { /// The token to complete. pub token_to_complete: &'a str, - /// Other potentially relevant tokens. + /// If available, the name of the command being invoked. pub command_name: Option<&'a str>, + /// If there was one, the token preceding the one being completed. pub preceding_token: Option<&'a str>, - /// The index of the token to complete. + /// The 0-based index of the token to complete. pub token_index: usize, - /// The input. + /// The input line. pub input_line: &'a str, + /// The 0-based index of the cursor in the input line. pub cursor_index: usize, + /// The tokens in the input line. pub tokens: &'a [&'a parser::Token], } impl CompletionSpec { + /// Generates completion candidates using this specification. + /// + /// # Arguments + /// + /// * `shell` - The shell instance to use for completion generation. + /// * `context` - The context in which completion is being generated. #[allow(clippy::too_many_lines)] pub async fn get_completions( &self, @@ -492,16 +531,21 @@ impl CompletionSpec { } } +/// Represents a set of generated command completions. #[derive(Debug, Default)] pub struct Completions { + /// The index in the input line where the completions should be inserted. pub start: usize, + /// The ordered list of completions. pub candidates: Vec, + /// Options for processing the candidates. pub options: CandidateProcessingOptions, } +/// Options governing how command completion candidates are processed. #[derive(Debug)] pub struct CandidateProcessingOptions { - /// Treat completions as file names + /// Treat completions as file names. pub treat_as_filenames: bool, /// Don't auto-quote completions that are file names. pub no_autoquote_filenames: bool, @@ -519,9 +563,13 @@ impl Default for CandidateProcessingOptions { } } +/// Encapsulates a completion result. #[allow(clippy::module_name_repetitions)] pub enum CompletionResult { + /// The completion process generated a set of candidates along with options + /// controlling how to process them. Candidates(Vec, CandidateProcessingOptions), + /// The completion process needs to be restarted. RestartCompletionProcess, } @@ -530,6 +578,11 @@ const DEFAULT_COMMAND: &str = "_DefaultCmD_"; const INITIAL_WORD: &str = "_InitialWorD_"; impl CompletionConfig { + /// Removes a completion spec by name. + /// + /// # Arguments + /// + /// * `name` - The name of the completion spec to remove. pub fn remove(&mut self, name: &str) { match name { EMPTY_COMMAND => { @@ -547,10 +600,16 @@ impl CompletionConfig { } } + /// Returns an iterator over the completion specs. pub fn iter(&self) -> impl Iterator { self.commands.iter() } + /// If present, returns the completion spec for the command of the given name. + /// + /// # Arguments + /// + /// * `name` - The name of the command. pub fn get(&self, name: &str) -> Option<&CompletionSpec> { match name { EMPTY_COMMAND => self.empty_line.as_ref(), @@ -560,6 +619,13 @@ impl CompletionConfig { } } + /// If present, sets the provided completion spec to be associated with the + /// command of the given name. + /// + /// # Arguments + /// + /// * `name` - The name of the command. + /// * `spec` - The completion spec to associate with the command. pub fn set(&mut self, name: &str, spec: CompletionSpec) { match name { EMPTY_COMMAND => { @@ -577,6 +643,14 @@ impl CompletionConfig { } } + /// Returns a mutable reference to the completion spec for the command of the + /// given name; if the command already was associated with a spec, returns + /// a reference to that existing spec. Otherwise registers a new default + /// spec and returns a mutable reference to it. + /// + /// # Arguments + /// + /// * `name` - The name of the command. pub fn get_or_add_mut(&mut self, name: &str) -> &mut CompletionSpec { match name { EMPTY_COMMAND => { @@ -601,6 +675,13 @@ impl CompletionConfig { } } + /// Generates completions for the given input line and cursor position. + /// + /// # Arguments + /// + /// * `shell` - The shell instance to use for completion generation. + /// * `input` - The input line for which completions are being generated. + /// * `position` - The 0-based index of the cursor in the input line. #[allow(clippy::cast_sign_loss)] pub async fn get_completions( &self, diff --git a/shell/src/context.rs b/shell/src/context.rs index 311bf64a..bc9f87cc 100644 --- a/shell/src/context.rs +++ b/shell/src/context.rs @@ -1,21 +1,28 @@ use crate::Shell; +/// Represents the context for executing a command. #[allow(clippy::module_name_repetitions)] pub struct CommandExecutionContext<'a> { + /// The shell in which the command is being executed. pub shell: &'a mut Shell, + /// The name of the command being executed. pub command_name: String, + /// The open files tracked by the current context. pub open_files: crate::openfiles::OpenFiles, } impl CommandExecutionContext<'_> { + /// Returns the standard input file; usable with `write!` et al. pub fn stdin(&self) -> crate::openfiles::OpenFile { self.open_files.files.get(&0).unwrap().try_dup().unwrap() } + /// Returns the standard output file; usable with `write!` et al. pub fn stdout(&self) -> crate::openfiles::OpenFile { self.open_files.files.get(&1).unwrap().try_dup().unwrap() } + /// Returns the standard error file; usable with `write!` et al. pub fn stderr(&self) -> crate::openfiles::OpenFile { self.open_files.files.get(&2).unwrap().try_dup().unwrap() } diff --git a/shell/src/env.rs b/shell/src/env.rs index faf81352..fac05601 100644 --- a/shell/src/env.rs +++ b/shell/src/env.rs @@ -4,14 +4,20 @@ use std::collections::HashMap; use crate::error; use crate::variables::{self, ShellValue, ShellValueUnsetType, ShellVariable}; +/// Represents the policy for looking up variables in a shell environment. #[derive(Clone, Copy)] pub enum EnvironmentLookup { + /// Look anywhere. Anywhere, + /// Look only in the global scope. OnlyInGlobal, + /// Look only in the current local scope. OnlyInCurrentLocal, + /// Look only in local scopes. OnlyInLocal, } +/// Represents a shell environment scope. #[derive(Clone, Copy, Debug, PartialEq)] pub enum EnvironmentScope { /// Scope local to a function instance @@ -22,8 +28,10 @@ pub enum EnvironmentScope { Command, } +/// Represents the shell variable environment, composed of a stack of scopes. #[derive(Clone, Debug)] pub struct ShellEnvironment { + /// Stack of scopes, with the top of the stack being the current scope. pub(crate) scopes: Vec<(EnvironmentScope, ShellVariableMap)>, } @@ -34,16 +42,27 @@ impl Default for ShellEnvironment { } impl ShellEnvironment { + /// Returns a new shell environment. pub fn new() -> Self { Self { scopes: vec![(EnvironmentScope::Global, ShellVariableMap::new())], } } + /// Pushes a new scope of the given type onto the environment's scope stack. + /// + /// # Arguments + /// + /// * `scope_type` - The type of scope to push. pub fn push_scope(&mut self, scope_type: EnvironmentScope) { self.scopes.push((scope_type, ShellVariableMap::new())); } + /// Pops the top-most scope off the environment's scope stack. + /// + /// # Arguments + /// + /// * `expected_scope_type` - The type of scope that is expected to be atop the stack. pub fn pop_scope(&mut self, expected_scope_type: EnvironmentScope) -> Result<(), error::Error> { // TODO: Should we panic instead on failure? It's effectively a broken invariant. match self.scopes.pop() { @@ -56,10 +75,17 @@ impl ShellEnvironment { // Iterators/Getters // + /// Returns an iterator over all the variables defined in the environment. pub fn iter(&self) -> impl Iterator { self.iter_using_policy(EnvironmentLookup::Anywhere) } + /// Returns an iterator over all the variables defined in the environment, + /// using the given lookup policy. + /// + /// # Arguments + /// + /// * `lookup_policy` - The policy to use when looking up variables. pub fn iter_using_policy( &self, lookup_policy: EnvironmentLookup, @@ -107,6 +133,12 @@ impl ShellEnvironment { visible_vars.into_iter() } + /// Tries to retrieve an immutable reference to the variable with the given name + /// in the environment. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to retrieve. pub fn get>(&self, name: S) -> Option<(EnvironmentScope, &ShellVariable)> { // Look through scopes, from the top of the stack on down. for (scope_type, map) in self.scopes.iter().rev() { @@ -118,6 +150,12 @@ impl ShellEnvironment { None } + /// Tries to retrieve a mutable reference to the variable with the given name + /// in the environment. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to retrieve. pub fn get_mut>( &mut self, name: S, @@ -132,11 +170,22 @@ impl ShellEnvironment { None } + /// Tries to retrieve the string value of the variable with the given name in the + /// environment. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to retrieve. pub fn get_str>(&self, name: S) -> Option> { self.get(name.as_ref()) .map(|(_, v)| v.value().to_cow_string()) } + /// Checks if a variable of the given name is set in the environment. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to check. pub fn is_set>(&self, name: S) -> bool { if let Some((_, var)) = self.get(name) { !matches!(var.value(), ShellValue::Unset(_)) @@ -149,6 +198,12 @@ impl ShellEnvironment { // Setters // + /// Tries to unset the variable with the given name in the environment, returning + /// whether or not such a variable existed. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to unset. pub fn unset(&mut self, name: &str) -> Result { let mut local_count = 0; for (scope_type, map) in self.scopes.iter_mut().rev() { @@ -173,6 +228,13 @@ impl ShellEnvironment { Ok(false) } + /// Tries to unset an array element from the environment, using the given name and + /// element index for lookup. Returns whether or not an element was unset. + /// + /// # Arguments + /// + /// * `name` - The name of the array variable to unset an element from. + /// * `index` - The index of the element to unset. pub fn unset_index(&mut self, name: &str, index: &str) -> Result { if let Some((_, var)) = self.get_mut(name) { var.unset_index(index) @@ -189,6 +251,13 @@ impl ShellEnvironment { } } + /// Tries to retrieve an immutable reference to a variable from the environment, + /// using the given name and lookup policy. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to retrieve. + /// * `lookup_policy` - The policy to use when looking up the variable. pub fn get_using_policy>( &self, name: N, @@ -233,6 +302,13 @@ impl ShellEnvironment { None } + /// Tries to retrieve a mutable reference to a variable from the environment, + /// using the given name and lookup policy. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to retrieve. + /// * `lookup_policy` - The policy to use when looking up the variable. pub fn get_mut_using_policy>( &mut self, name: N, @@ -277,6 +353,15 @@ impl ShellEnvironment { None } + /// Update a variable in the environment, or add it if it doesn't already exist. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to update or add. + /// * `value` - The value to assign to the variable. + /// * `updater` - A function to call to update the variable after assigning the value. + /// * `lookup_policy` - The policy to use when looking up the variable. + /// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist. pub fn update_or_add>( &mut self, name: N, @@ -299,6 +384,16 @@ impl ShellEnvironment { } } + /// Update an array element in the environment, or add it if it doesn't already exist. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to update or add. + /// * `index` - The index of the element to update or add. + /// * `value` - The value to assign to the variable. + /// * `updater` - A function to call to update the variable after assigning the value. + /// * `lookup_policy` - The policy to use when looking up the variable. + /// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist. pub fn update_or_add_array_element>( &mut self, name: N, @@ -328,6 +423,13 @@ impl ShellEnvironment { } } + /// Adds a variable to the environment. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to add. + /// * `var` - The variable to add. + /// * `target_scope` - The scope to add the variable to. pub fn add>( &mut self, name: N, @@ -344,6 +446,12 @@ impl ShellEnvironment { Err(error::Error::MissingScope) } + /// Sets a global variable in the environment. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to set. + /// * `var` - The variable to set. pub fn set_global>( &mut self, name: N, @@ -353,12 +461,14 @@ impl ShellEnvironment { } } +/// Represents a map from names to shell variables. #[derive(Clone, Debug)] pub struct ShellVariableMap { variables: HashMap, } impl ShellVariableMap { + /// Returns a new shell variable map. pub fn new() -> Self { Self { variables: HashMap::new(), @@ -369,14 +479,25 @@ impl ShellVariableMap { // Iterators/Getters // + /// Returns an iterator over all the variables in the map. pub fn iter(&self) -> impl Iterator { self.variables.iter() } + /// Tries to retrieve an immutable reference to the variable with the given name. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to retrieve. pub fn get(&self, name: &str) -> Option<&ShellVariable> { self.variables.get(name) } + /// Tries to retrieve a mutable reference to the variable with the given name. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to retrieve. pub fn get_mut(&mut self, name: &str) -> Option<&mut ShellVariable> { self.variables.get_mut(name) } @@ -385,10 +506,22 @@ impl ShellVariableMap { // Setters // + /// Tries to unset the variable with the given name, returning whether or not + /// such a variable existed. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to unset. pub fn unset(&mut self, name: &str) -> bool { self.variables.remove(name).is_some() } + /// Sets a variable in the map. + /// + /// # Arguments + /// + /// * `name` - The name of the variable to set. + /// * `var` - The variable to set. pub fn set>(&mut self, name: N, var: ShellVariable) { self.variables.insert(name.into(), var); } diff --git a/shell/src/error.rs b/shell/src/error.rs index 20d6e42a..eb152ff1 100644 --- a/shell/src/error.rs +++ b/shell/src/error.rs @@ -1,119 +1,162 @@ use std::path::PathBuf; +/// Monolithic error type for the shell #[derive(thiserror::Error, Debug)] pub enum Error { + /// A local variable was set outside of a function #[error("can't set local variable outside of function")] SetLocalVarOutsideFunction, + /// A tilde expression was used without a valid HOME variable #[error("cannot expand tilde expression with HOME not set")] TildeWithoutValidHome, + /// An attempt was made to assign a list to an array member #[error("cannot assign list to array member")] AssigningListToArrayMember, + /// An attempt was made to convert an associative array to an indexed array. #[error("cannot convert associative array to indexed array")] ConvertingAssociativeArrayToIndexedArray, + /// An attempt was made to convert an indexed array to an associative array. #[error("cannot convert indexed array to associative array")] ConvertingIndexedArrayToAssociativeArray, + /// An error occurred while sourcing the indicated script file. #[error("failed to source file: {0}; {1}")] FailedSourcingFile(PathBuf, std::io::Error), + /// The shell failed to send a signal to a process. #[error("failed to send signal to process")] FailedToSendSignal, + /// An attempt was made to assign a value to a special parameter. #[error("cannot assign in this way")] CannotAssignToSpecialParameter, + /// Checked expansion error. #[error("expansion error: {0}")] CheckedExpansionError(String), + /// A reference was made to an unknown shell function. #[error("function not found: {0}")] FunctionNotFound(String), + /// The requested functionality has not yet been implemented in this shell. #[error("UNIMPLEMENTED: {0}")] Unimplemented(&'static str), + /// An expected environment scope could not be found. #[error("missing scope")] MissingScope, + /// The given path is not a directory. #[error("not a directory: {0}")] NotADirectory(PathBuf), + /// The given variable is not an array. #[error("variable is not an array")] NotArray, + /// The current user could not be determined. #[error("no current user")] NoCurrentUser, + /// The requested input or output redirection is invalid. #[error("invalid redirection")] InvalidRedirection, + /// An error occurred while redirecting input or output with the given file. #[error("failed to redirect to {0}: {1}")] RedirectionFailure(String, std::io::Error), + /// An error occurred evaluating an arithmetic expression. #[error("arithmetic evaluation error: {0}")] EvalError(#[from] crate::arithmetic::EvalError), + /// The given string could not be parsed as an integer. #[error("failed to parse integer")] IntParseError(#[from] std::num::ParseIntError), + /// The given string could not be parsed as an integer. #[error("failed to parse integer")] TryIntParseError(#[from] std::num::TryFromIntError), + /// A byte sequence could not be decoded as a valid UTF-8 string. #[error("failed to decode utf-8")] FromUtf8Error(#[from] std::string::FromUtf8Error), + /// A byte sequence could not be decoded as a valid UTF-8 string. #[error("failed to decode utf-8")] Utf8Error(#[from] std::str::Utf8Error), + /// An attempt was made to modify a readonly variable. #[error("cannot mutate readonly variable")] ReadonlyVariable, + /// The indicated pattern is invalid. #[error("invalid pattern: '{0}'")] InvalidPattern(String), + /// An invalid regular expression was provided. #[error("invalid regex: {0}")] RegexError(#[from] fancy_regex::Error), + /// An I/O error occurred. #[error("i/o error: {0}")] IoError(#[from] std::io::Error), + /// Invalid substitution syntax. #[error("bad substitution")] BadSubstitution, + /// Invalid arguments were provided to the command. #[error("invalid arguments")] InvalidArguments, + /// An error occurred while creating a child process. #[error("failed to create child process")] ChildCreationFailure, + /// An error occurred while formatting a string. #[error("{0}")] FormattingError(#[from] std::fmt::Error), + /// An error occurred while parsing a word. #[error("{0}")] WordParseError(#[from] parser::WordParseError), + /// Unable to parse a test command. #[error("{0}")] TestCommandParseError(#[from] parser::TestCommandParseError), + /// A threading error occurred. #[error("threading error")] ThreadingError(#[from] tokio::task::JoinError), + /// An invalid signal was referenced. #[error("invalid signal")] InvalidSignal, + /// A system error occurred. #[error("system error: {0}")] ErrnoError(#[from] nix::errno::Errno), + /// An invalid umask was provided. #[error("invalid umask value")] InvalidUmask, + /// An error occurred reading from procfs. #[error("procfs error: {0}")] ProcfsError(#[from] procfs::ProcError), } +/// Convenience function for returning an error for unimplemented functionality. +/// +/// # Arguments +/// +/// * `msg` - The message to include in the error pub(crate) fn unimp(msg: &'static str) -> Result { Err(Error::Unimplemented(msg)) } diff --git a/shell/src/expansion.rs b/shell/src/expansion.rs index 65896b66..069f9779 100644 --- a/shell/src/expansion.rs +++ b/shell/src/expansion.rs @@ -1463,8 +1463,12 @@ impl<'a> WordExpander<'a> { Ok(result) } parser::word::ParameterTransformOp::PossiblyQuoteWithArraysExpanded { - separate_words: _, - } => error::unimp("parameter transformation: PossiblyQuoteWithArraysExpanded"), + separate_words: _separate_words, + } => { + // TODO: This isn't right for arrays. + // TODO: This doesn't honor 'separate_words' + Ok(variables::quote_str_for_assignment(s)) + } parser::word::ParameterTransformOp::Quoted => { Ok(variables::quote_str_for_assignment(s)) } diff --git a/shell/src/functions.rs b/shell/src/functions.rs index 574fa856..7bff096f 100644 --- a/shell/src/functions.rs +++ b/shell/src/functions.rs @@ -1,30 +1,50 @@ use std::{collections::HashMap, sync::Arc}; +/// An environment for defined, named functions. #[derive(Clone, Default)] pub struct FunctionEnv { functions: HashMap, } impl FunctionEnv { + /// Tries to retrieve the registration for a function by name. + /// + /// # Arguments + /// + /// * `name` - The name of the function to retrieve. pub fn get(&self, name: &str) -> Option<&FunctionRegistration> { self.functions.get(name) } + /// Unregisters a function from the environment. + /// + /// # Arguments + /// + /// * `name` - The name of the function to remove. pub fn remove(&mut self, name: &str) -> Option { self.functions.remove(name) } + /// Updates a function registration in this environment. + /// + /// # Arguments + /// + /// * `name` - The name of the function to update. + /// * `definition` - The new definition for the function. pub fn update(&mut self, name: String, definition: Arc) { self.functions .insert(name, FunctionRegistration { definition }); } + /// Returns an iterator over the functions registered in this environment. pub fn iter(&self) -> impl Iterator { self.functions.iter() } } +/// Encapsulates a registration for a defined function. #[derive(Clone)] pub struct FunctionRegistration { + /// The definition of the function. pub definition: Arc, } diff --git a/shell/src/interp.rs b/shell/src/interp.rs index 8b12fc32..038698af 100644 --- a/shell/src/interp.rs +++ b/shell/src/interp.rs @@ -18,12 +18,18 @@ use crate::variables::{ }; use crate::{builtin, context, error, expansion, extendedtests, jobs, openfiles, traps}; +/// Encapsulates the result of executing a command. #[derive(Debug, Default)] pub struct ExecutionResult { + /// The numerical exit code of the command. pub exit_code: u8, + /// Whether the shell should exit after this command. pub exit_shell: bool, + /// Whether the shell should return from the current function or script. pub return_from_function_or_script: bool, + /// If the command was executed in a loop, this is the number of levels to break out of. pub break_loop: Option, + /// If the command was executed in a loop, this is the number of levels to continue. pub continue_loop: Option, } @@ -46,6 +52,10 @@ impl From for ExecutionResult { } impl ExecutionResult { + /// Returns a new `ExecutionResult` with the given exit code. + /// + /// # Parameters + /// - `exit_code` - The exit code of the command. pub fn new(exit_code: u8) -> ExecutionResult { ExecutionResult { exit_code, @@ -53,25 +63,36 @@ impl ExecutionResult { } } + /// Returns a new `ExecutionResult` with an exit code of 0. pub fn success() -> ExecutionResult { Self::new(0) } + /// Returns whether the command was successful. pub fn is_success(&self) -> bool { self.exit_code == 0 } } +/// Represents the result of spawning a command. pub(crate) enum SpawnResult { + /// The child process was spawned. SpawnedChild(process::Child), + /// The command immediatedly exited with the given numeric exit code. ImmediateExit(u8), + /// The shell should exit after this command, yielding the given numeric exit code. ExitShell(u8), + /// The shell should return from the current function or script, yielding the given numeric exit code. ReturnFromFunctionOrScript(u8), + /// The shell should break out of the containing loop, identified by the given depth count. BreakLoop(u8), + /// The shell should continue the containing loop, identified by the given depth count. ContinueLoop(u8), } +/// Encapsulates the context of execution in a command pipeline. struct PipelineExecutionContext<'a> { + /// The shell in which the command is being executed. shell: &'a mut Shell, current_pipeline_index: usize, @@ -81,8 +102,10 @@ struct PipelineExecutionContext<'a> { params: ExecutionParameters, } +/// Parameters for execution. #[derive(Clone, Default)] pub struct ExecutionParameters { + /// The open files tracked by the current context. pub open_files: openfiles::OpenFiles, } diff --git a/shell/src/jobs.rs b/shell/src/jobs.rs index 2a38a8f5..247966a2 100644 --- a/shell/src/jobs.rs +++ b/shell/src/jobs.rs @@ -9,16 +9,25 @@ use crate::ExecutionResult; pub(crate) type JobJoinHandle = tokio::task::JoinHandle>; pub(crate) type JobResult = (Job, Result); +/// Manages the jobs that are currently managed by the shell. #[derive(Default)] pub struct JobManager { + /// The jobs that are currently managed by the shell. pub jobs: Vec, } impl JobManager { + /// Returns a new job manager. pub fn new() -> Self { Self::default() } + /// Adds a job to the job manager and marks it as the current job; + /// returns an immutable reference to the job. + /// + /// # Arguments + /// + /// * `job` - The job to add. pub fn add_as_current(&mut self, mut job: Job) -> &Job { for j in &mut self.jobs { if matches!(j.annotation, JobAnnotation::Current) { @@ -34,30 +43,39 @@ impl JobManager { self.jobs.last().unwrap() } + /// Returns the current job, if there is one. pub fn current_job(&self) -> Option<&Job> { self.jobs .iter() .find(|j| matches!(j.annotation, JobAnnotation::Current)) } + /// Returns a mutable reference to the current job, if there is one. pub fn current_job_mut(&mut self) -> Option<&mut Job> { self.jobs .iter_mut() .find(|j| matches!(j.annotation, JobAnnotation::Current)) } + /// Returns the previous job, if there is one. pub fn prev_job(&self) -> Option<&Job> { self.jobs .iter() .find(|j| matches!(j.annotation, JobAnnotation::Previous)) } + /// Returns a mutable reference to the previous job, if there is one. pub fn prev_job_mut(&mut self) -> Option<&mut Job> { self.jobs .iter_mut() .find(|j| matches!(j.annotation, JobAnnotation::Previous)) } + /// Tries to resolve the given job specification to a job. + /// + /// # Arguments + /// + /// * `job_spec` - The job specification to resolve. pub fn resolve_job_spec(&mut self, job_spec: &str) -> Option<&mut Job> { if !job_spec.starts_with('%') { return None; @@ -77,6 +95,7 @@ impl JobManager { } } + /// Waits for all managede jobs to complete. pub async fn wait_all(&mut self) -> Result, error::Error> { for job in &mut self.jobs { job.wait().await?; @@ -85,6 +104,7 @@ impl JobManager { Ok(self.sweep_completed_jobs()) } + /// Polls all managed jobs for completion. pub fn poll(&mut self) -> Result, error::Error> { let mut results = vec![]; @@ -117,11 +137,16 @@ impl JobManager { } } +/// Represents the current execution state of a job. #[derive(Clone)] pub enum JobState { + /// Unknown state. Unknown, + /// The job is running. Running, + /// The job is stopped. Stopped, + /// The job has completed. Done, } @@ -136,10 +161,14 @@ impl Display for JobState { } } +/// Represents an annotation for a job. #[derive(Clone)] pub enum JobAnnotation { + /// No annotation. None, + /// The job is the current job. Current, + /// The job is the previous job. Previous, } @@ -188,6 +217,14 @@ impl Display for Job { } impl Job { + /// Returns a new job object. + /// + /// # Arguments + /// + /// * `join_handles` - The join handles for the tasks that are waiting on the job's processes. + /// * `pids` - If available, the process IDs of the job's processes. + /// * `command_line` - The command line of the job. + /// * `state` - The current operational state of the job. pub(crate) fn new( join_handles: VecDeque, pids: Vec, @@ -204,6 +241,7 @@ impl Job { } } + /// Returns a pid-style string for the job. pub fn to_pid_style_string(&self) -> String { let display_pid = self .get_representative_pid() @@ -211,10 +249,12 @@ impl Job { std::format!("[{}]{}\t{}", self.id, self.annotation, display_pid) } + /// Returns the annotation of the job. pub fn get_annotation(&self) -> JobAnnotation { self.annotation.clone() } + /// Returns the command name of the job. pub fn get_command_name(&self) -> &str { self.command_line .split_ascii_whitespace() @@ -222,14 +262,17 @@ impl Job { .unwrap_or_default() } + /// Returns whether the job is the current job. pub fn is_current(&self) -> bool { matches!(self.annotation, JobAnnotation::Current) } + /// Returns whether the job is the previous job. pub fn is_prev(&self) -> bool { matches!(self.annotation, JobAnnotation::Previous) } + /// Polls whether the job has completed. pub fn poll_done( &mut self, ) -> Result>, error::Error> { @@ -255,6 +298,7 @@ impl Job { Ok(result) } + /// Waits for the job to complete. pub async fn wait(&mut self) -> Result { let mut result = ExecutionResult::success(); @@ -265,11 +309,13 @@ impl Job { Ok(result) } + /// Moves the job to execute in the background. #[allow(clippy::unused_self)] pub fn move_to_background(&mut self) -> Result<(), error::Error> { error::unimp("move job to background") } + /// Moves the job to execute in the foreground. #[cfg(unix)] pub fn move_to_foreground(&mut self) -> Result<(), error::Error> { if !matches!(self.state, JobState::Stopped) { @@ -291,11 +337,13 @@ impl Job { } } + /// Moves the job to execute in the foreground. #[cfg(not(unix))] pub fn move_to_foreground(&mut self) -> Result<(), error::Error> { error::unimp("move job to foreground") } + /// Kills the job. #[cfg(unix)] pub fn kill(&mut self) -> Result<(), error::Error> { if let Some(pid) = self.get_representative_pid() { @@ -311,11 +359,13 @@ impl Job { } } + /// Kills the job. #[cfg(not(unix))] pub fn kill(&mut self) -> Result<(), error::Error> { error::unimp("kill job") } + /// Tries to retrieve a "representative" pid for the job. pub fn get_representative_pid(&self) -> Option { self.pids.first().copied() } diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 7a8a274b..d4f50341 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -1,3 +1,5 @@ +//! Core implementation of the brush shell + mod arithmetic; mod builtin; mod builtins; diff --git a/shell/src/openfiles.rs b/shell/src/openfiles.rs index 2d476b65..eb87f013 100644 --- a/shell/src/openfiles.rs +++ b/shell/src/openfiles.rs @@ -9,18 +9,28 @@ use std::process::Stdio; use crate::error; +/// Represents a file open in a shell context. pub enum OpenFile { + /// The original standard input this process was started with. Stdin, + /// The original standard output this process was started with. Stdout, + /// The original standard error this process was started with. Stderr, + /// A null file that discards all input. Null, + /// A file open for reading or writing. File(std::fs::File), + /// A read end of a pipe. PipeReader(os_pipe::PipeReader), + /// A write end of a pipe. PipeWriter(os_pipe::PipeWriter), + /// A here document. HereDocument(String), } impl OpenFile { + /// Tries to duplicate the open file. pub fn try_dup(&self) -> Result { let result = match self { OpenFile::Stdin => OpenFile::Stdin, @@ -36,6 +46,7 @@ impl OpenFile { Ok(result) } + /// Converts the open file into an `OwnedFd`. #[cfg(unix)] pub(crate) fn into_owned_fd(self) -> Result { match self { @@ -50,6 +61,7 @@ impl OpenFile { } } + /// Retrieves the raw file descriptor for the open file. #[cfg(unix)] #[allow(dead_code)] pub(crate) fn as_raw_fd(&self) -> Result { @@ -138,7 +150,9 @@ impl std::io::Write for OpenFile { } } +/// Represents the open files in a shell context. pub struct OpenFiles { + /// Maps shell file descriptors to open files. pub files: HashMap, } @@ -161,6 +175,7 @@ impl Default for OpenFiles { } impl OpenFiles { + /// Tries to clone the open files. pub fn try_clone(&self) -> Result { let mut files = HashMap::new(); for (fd, file) in &self.files { diff --git a/shell/src/options.rs b/shell/src/options.rs index 11db3716..8696ef20 100644 --- a/shell/src/options.rs +++ b/shell/src/options.rs @@ -1,5 +1,6 @@ use crate::CreateOptions; +/// Runtime changeable options for a shell instance. #[derive(Clone, Default)] #[allow(clippy::module_name_repetitions)] pub struct RuntimeOptions { @@ -64,69 +65,130 @@ pub struct RuntimeOptions { // // Options set through shopt. // + /// `assoc_expand_once` pub assoc_expand_once: bool, + /// 'autocd' pub auto_cd: bool, + /// `cdable_vars` pub cdable_vars: bool, + /// 'cdspell' pub cd_autocorrect_spelling: bool, + /// 'checkhash' pub check_hashtable_before_command_exec: bool, + /// 'checkjobs' pub check_jobs_before_exit: bool, + /// 'checkwinsize' pub check_window_size_after_external_commands: bool, + /// 'cmdhist' pub save_multiline_cmds_in_history: bool, + /// 'compat31' pub compat31: bool, + /// 'compat32' pub compat32: bool, + /// 'compat40' pub compat40: bool, + /// 'compat41' pub compat41: bool, + /// 'compat42' pub compat42: bool, + /// 'compat43' pub compat43: bool, + /// 'compat44' pub compat44: bool, + /// `complete_fullquote` pub quote_all_metachars_in_completion: bool, + /// 'direxpand' pub expand_dir_names_on_completion: bool, + /// 'dirspell' pub autocorrect_dir_spelling_on_completion: bool, + /// 'dotglob' pub glob_matches_dotfiles: bool, + /// 'execfail' pub exit_on_exec_fail: bool, + /// `expand_aliases` pub expand_aliases: bool, + /// 'extdebug' pub enable_debugger: bool, + /// 'extglob' pub extended_globbing: bool, + /// 'extquote' pub extquote: bool, + /// 'failglob' pub fail_expansion_on_globs_without_match: bool, + /// `force_fignore` pub force_fignore: bool, + /// 'globasciiranges' pub glob_ranges_use_c_locale: bool, + /// 'globstar' pub enable_star_star_glob: bool, + /// `gnu_errfmt` pub errors_in_gnu_format: bool, + /// 'histappend' pub append_to_history_file: bool, + /// 'histreedit' pub allow_reedit_failed_history_subst: bool, + /// 'histverify' pub allow_modifying_history_substitution: bool, + /// 'hostcomplete' pub enable_hostname_completion: bool, + /// 'huponexit' pub send_sighup_to_all_jobs_on_exit: bool, + /// `inherit_errexit` pub command_subst_inherits_errexit: bool, + /// `interactive_comments` pub interactive_comments: bool, + /// 'lastpipe' pub run_last_pipeline_cmd_in_current_shell: bool, + /// 'lithist' pub embed_newlines_in_multiline_cmds_in_history: bool, + /// `localvar_inherit` pub local_vars_inherit_value_and_attrs: bool, + /// `localvar_unset` pub localvar_unset: bool, + /// `login_shell` pub login_shell: bool, + /// 'mailwarn' pub mail_warn: bool, + /// `no_empty_cmd_completion` pub case_insensitive_pathname_expansion: bool, + /// 'nocaseglob' pub case_insensitive_conditionals: bool, + /// 'nocasematch' pub no_empty_cmd_completion: bool, + /// 'nullglob' pub expand_non_matching_patterns_to_null: bool, + /// 'progcomp' pub programmable_completion: bool, + /// `progcomp_alias` pub programmable_completion_alias: bool, + /// 'promptvars' pub expand_prompt_strings: bool, + /// `restricted_shell` pub restricted_shell: bool, + /// `shift_verbose` pub shift_verbose: bool, + /// `sourcepath` pub source_builtin_searches_path: bool, + /// `xpg_echo` pub echo_builtin_expands_escape_sequences: bool, // // Options set by the shell. // + /// Whether or not the shell is interactive. pub interactive: bool, + /// Whether or not the shell is reading commands from standard input. pub read_commands_from_stdin: bool, + /// Whether or not the shell is in maximal `sh` compatibility mode. pub sh_mode: bool, } impl RuntimeOptions { + /// Creates a default set of runtime options based on the given creation options. + /// + /// # Arguments + /// + /// * `create_options` - The options used to create the shell. pub fn defaults_from(create_options: &CreateOptions) -> RuntimeOptions { // There's a set of options enabled by default for all shells. let mut options = Self { diff --git a/shell/src/patterns.rs b/shell/src/patterns.rs index 454f2357..c6ef7f69 100644 --- a/shell/src/patterns.rs +++ b/shell/src/patterns.rs @@ -4,9 +4,12 @@ use std::{ path::{Path, PathBuf}, }; +/// Represents a piece of a shell pattern. #[derive(Clone, Debug)] pub(crate) enum PatternPiece { + /// A pattern that should be interpreted as a shell pattern. Pattern(String), + /// A literal string that should be matched exactly. Literal(String), } @@ -21,6 +24,7 @@ impl PatternPiece { type PatternWord = Vec; +/// Encapsulates a shell pattern. #[derive(Clone, Debug)] pub struct Pattern { pieces: PatternWord, @@ -57,14 +61,23 @@ impl From for Pattern { } impl Pattern { + /// Returns whether or not the pattern is empty. pub fn is_empty(&self) -> bool { self.pieces.iter().all(|p| p.as_str().is_empty()) } + /// Placeholder function that always returns true. pub(crate) fn accept_all_expand_filter(_path: &Path) -> bool { true } + /// Expands the pattern into a list of matching file paths. + /// + /// # Arguments + /// + /// * `working_dir` - The current working directory, used for relative paths. + /// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). + /// * `path_filter` - Optionally provides a function that filters paths after expansion. #[allow(clippy::too_many_lines)] pub(crate) fn expand( &self, @@ -203,6 +216,13 @@ impl Pattern { Ok(results) } + /// Converts the pattern to a regular expression string. + /// + /// # Arguments + /// + /// * `strict_prefix_match` - Whether or not the pattern should strictly match the beginning of the string. + /// * `strict_suffix_match` - Whether or not the pattern should strictly match the end of the string. + /// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn to_regex_str( &self, strict_prefix_match: bool, @@ -233,6 +253,13 @@ impl Pattern { Ok(regex_str) } + /// Converts the pattern to a regular expression. + /// + /// # Arguments + /// + /// * `strict_prefix_match` - Whether or not the pattern should strictly match the beginning of the string. + /// * `strict_suffix_match` - Whether or not the pattern should strictly match the end of the string. + /// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn to_regex( &self, strict_prefix_match: bool, @@ -251,6 +278,12 @@ impl Pattern { Ok(re) } + /// Checks if the pattern exactly matches the given string. + /// + /// # Arguments + /// + /// * `value` - The string to check for a match. + /// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn exactly_matches( &self, value: &str, @@ -277,6 +310,14 @@ fn escape_for_regex(s: &str) -> String { escaped } +/// Converts a shell pattern to a regular expression. +/// +/// # Arguments +/// +/// * `pattern` - The shell pattern to convert. +/// * `strict_prefix_match` - Whether or not the pattern should strictly match the beginning of the string. +/// * `strict_suffix_match` - Whether or not the pattern should strictly match the end of the string. +/// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn pattern_to_regex( pattern: &str, strict_prefix_match: bool, @@ -318,6 +359,13 @@ fn pattern_to_regex_str( Ok(regex_str) } +/// Removes the largest matching prefix from a string that matches the given pattern. +/// +/// # Arguments +/// +/// * `s` - The string to remove the prefix from. +/// * `pattern` - The pattern to match. +/// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn remove_largest_matching_prefix<'a>( s: &'a str, pattern: &Option, @@ -334,6 +382,13 @@ pub(crate) fn remove_largest_matching_prefix<'a>( Ok(s) } +/// Removes the smallest matching prefix from a string that matches the given pattern. +/// +/// # Arguments +/// +/// * `s` - The string to remove the prefix from. +/// * `pattern` - The pattern to match. +/// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn remove_smallest_matching_prefix<'a>( s: &'a str, pattern: &Option, @@ -350,6 +405,13 @@ pub(crate) fn remove_smallest_matching_prefix<'a>( Ok(s) } +/// Removes the largest matching suffix from a string that matches the given pattern. +/// +/// # Arguments +/// +/// * `s` - The string to remove the suffix from. +/// * `pattern` - The pattern to match. +/// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn remove_largest_matching_suffix<'a>( s: &'a str, pattern: &Option, @@ -366,6 +428,13 @@ pub(crate) fn remove_largest_matching_suffix<'a>( Ok(s) } +/// Removes the smallest matching suffix from a string that matches the given pattern. +/// +/// # Arguments +/// +/// * `s` - The string to remove the suffix from. +/// * `pattern` - The pattern to match. +/// * `enable_extended_globbing` - Whether or not to enable extended globbing (extglob). pub(crate) fn remove_smallest_matching_suffix<'a>( s: &'a str, pattern: &Option, diff --git a/shell/src/regex.rs b/shell/src/regex.rs index b93bd1b0..0ce8adec 100644 --- a/shell/src/regex.rs +++ b/shell/src/regex.rs @@ -2,9 +2,12 @@ use std::borrow::Cow; use crate::error; +/// Represents a piece of a regular expression. #[derive(Clone, Debug)] pub(crate) enum RegexPiece { + /// A pattern that should be interpreted as a regular expression. Pattern(String), + /// A literal string that should be matched exactly. Literal(String), } @@ -19,6 +22,7 @@ impl RegexPiece { type RegexWord = Vec; +/// Encapsulates a regular expression usable in the shell. #[derive(Clone, Debug)] pub struct Regex { pieces: RegexWord, @@ -31,6 +35,11 @@ impl From for Regex { } impl Regex { + /// Computes if the regular expression matches the given string. + /// + /// # Arguments + /// + /// * `value` - The string to check for a match. pub fn matches(&self, value: &str) -> Result>>, error::Error> { let regex_pattern: String = self .pieces diff --git a/shell/src/shell.rs b/shell/src/shell.rs index 478b3897..b57819b0 100644 --- a/shell/src/shell.rs +++ b/shell/src/shell.rs @@ -16,50 +16,59 @@ use crate::{ keywords, openfiles, patterns, prompt, traps, users, }; +/// Represents an instance of a shell. pub struct Shell { // // Core state required by specification // + /// Trap handler configuration for the shell. pub traps: traps::TrapHandlerConfig, + /// Manages files opened and accessible via redirection operators. pub open_files: openfiles::OpenFiles, + /// The current working directory. pub working_dir: PathBuf, - pub file_size_limit: u64, + /// The shell environment, containing shell variables. pub env: ShellEnvironment, + /// Shell function definitions. pub funcs: functions::FunctionEnv, + /// Runtime shell options. pub options: RuntimeOptions, + /// State of managed jobs. pub jobs: jobs::JobManager, + /// Shell aliases. pub aliases: HashMap, // // Additional state // + /// The status of the last completed command. pub last_exit_status: u8, - // Track clone depth from main shell + /// Clone depth from the original ancestor shell. pub depth: usize, - // Positional parameters ($1 and beyond) + /// Positional parameters ($1 and beyond) pub positional_parameters: Vec, - // Shell name + /// Shell name pub shell_name: Option, - // Script call stack. + /// Script call stack. pub script_call_stack: VecDeque, - // Function call stack. + /// Function call stack. pub function_call_stack: VecDeque, - // Directory stack used by pushd et al. + /// Directory stack used by pushd et al. pub directory_stack: Vec, - // Current line number being processed. + /// Current line number being processed. pub current_line_number: u32, - // Completion configuration. + /// Completion configuration. pub completion_config: completion::CompletionConfig, - // Builtins. + /// Shell built-in commands. pub builtins: HashMap, } @@ -69,7 +78,6 @@ impl Clone for Shell { traps: self.traps.clone(), open_files: self.open_files.clone(), working_dir: self.working_dir.clone(), - file_size_limit: self.file_size_limit, env: self.env.clone(), funcs: self.funcs.clone(), options: self.options.clone(), @@ -89,35 +97,54 @@ impl Clone for Shell { } } +/// Options for creating a new shell. #[derive(Debug, Default)] pub struct CreateOptions { + /// Whether the shell is a login shell. pub login: bool, + /// Whether the shell is interactive. pub interactive: bool, + /// Whether to skip using a readline-like interface for input. pub no_editing: bool, + /// Whether to skip sourcing the system profile. pub no_profile: bool, + /// Whether to skip sourcing the user's rc file. pub no_rc: bool, + /// Whether the shell is in POSIX compliance mode. pub posix: bool, + /// Whether to print commands and arguments as they are read. pub print_commands_and_arguments: bool, + /// Whether commands are being read from stdin. pub read_commands_from_stdin: bool, + /// The name of the shell. pub shell_name: Option, + /// Whether to run in maximal POSIX sh compatibility mode. pub sh_mode: bool, + /// Whether to print verbose output. pub verbose: bool, } +/// Represents an active shell function call. #[derive(Clone, Debug)] pub struct FunctionCall { + /// The name of the function invoked. function_name: String, + /// The definition of the invoked function. function_definition: Arc, } impl Shell { + /// Returns a new shell instance created with the given options. + /// + /// # Arguments + /// + /// * `options` - The options to use when creating the shell. pub async fn new(options: &CreateOptions) -> Result { // Instantiate the shell with some defaults. let mut shell = Shell { traps: traps::TrapHandlerConfig::default(), open_files: openfiles::OpenFiles::default(), working_dir: std::env::current_dir()?, - file_size_limit: Default::default(), // TODO: populate file size limit env: Self::initialize_vars(options)?, funcs: functions::FunctionEnv::default(), options: RuntimeOptions::defaults_from(options), @@ -314,6 +341,13 @@ impl Shell { } } + /// Source the given file as a shell script, returning the execution result. + /// + /// # Arguments + /// + /// * `path` - The path to the file to source. + /// * `args` - The arguments to pass to the script as positional parameters. + /// * `params` - Execution parameters. pub async fn source>( &mut self, path: &Path, @@ -347,6 +381,14 @@ impl Shell { .await } + /// Source the given file as a shell script, returning the execution result. + /// + /// # Arguments + /// + /// * `file` - The file to source. + /// * `source_info` - Information about the source of the script. + /// * `args` - The arguments to pass to the script as positional parameters. + /// * `params` - Execution parameters. pub async fn source_file>( &mut self, file: &std::fs::File, @@ -391,6 +433,12 @@ impl Shell { result } + /// Invokes a function defined in this shell, returning the resulting exit status. + /// + /// # Arguments + /// + /// * `name` - The name of the function to invoke. + /// * `args` - The arguments to pass to the function. pub async fn invoke_function(&mut self, name: &str, args: &[&str]) -> Result { let open_files = self.open_files.clone(); let command_name = String::from(name); @@ -426,6 +474,12 @@ impl Shell { } } + /// Executes the given string as a shell program, returning the resulting exit status. + /// + /// # Arguments + /// + /// * `command` - The command to execute. + /// * `params` - Execution parameters. pub async fn run_string( &mut self, command: String, @@ -444,10 +498,21 @@ impl Shell { .await } + /// Parses the given string as a shell program, returning the resulting Abstract Syntax Tree + /// for the program. + /// + /// # Arguments + /// + /// * `s` - The string to parse as a program. pub fn parse_string(&self, s: String) -> Result { parse_string_impl(s, self.parser_options()) } + /// Applies basic shell expansion to the provided string. + /// + /// # Arguments + /// + /// * `s` - The string to expand. pub async fn basic_expand_string>( &mut self, s: S, @@ -456,6 +521,12 @@ impl Shell { Ok(result) } + /// Applies full shell expansion and field splitting to the provided string; returns + /// a sequence of fields. + /// + /// # Arguments + /// + /// * `s` - The string to expand and split. pub async fn full_expand_and_split_string>( &mut self, s: S, @@ -464,12 +535,19 @@ impl Shell { Ok(result) } + /// Returns the default execution parameters for this shell. pub fn default_exec_params(&self) -> ExecutionParameters { ExecutionParameters { open_files: self.open_files.clone(), } } + /// Executes the given script file, returning the resulting exit status. + /// + /// # Arguments + /// + /// * `script_path` - The path to the script file to execute. + /// * `args` - The arguments to pass to the script as positional parameters. pub async fn run_script>( &mut self, script_path: &Path, @@ -541,6 +619,12 @@ impl Shell { Ok(result) } + /// Executes the given parsed shell program, returning the resulting exit status. + /// + /// # Arguments + /// + /// * `program` - The program to execute. + /// * `params` - Execution parameters. pub async fn run_program( &mut self, program: parser::ast::Program, @@ -549,6 +633,7 @@ impl Shell { program.execute(self, params).await } + /// Composes the shell's prompt, applying all appropriate expansions. pub async fn compose_prompt(&mut self) -> Result { const DEFAULT_PROMPT: &str = "$ "; @@ -569,6 +654,7 @@ impl Shell { Ok(formatted_prompt) } + /// Returns the exit status of the last command executed in this shell. pub fn last_result(&self) -> u8 { self.last_exit_status } @@ -580,7 +666,8 @@ impl Shell { ) } - pub fn current_option_flags(&self) -> String { + /// Returns a string representing the current `set`-style option flags set in the shell. + pub(crate) fn current_option_flags(&self) -> String { let mut cs = vec![]; for (x, y) in crate::namedoptions::SET_OPTIONS.iter() { @@ -592,7 +679,9 @@ impl Shell { cs.into_iter().collect() } - pub fn parser_options(&self) -> parser::ParserOptions { + /// Returns the options that should be used for parsing shell programs; reflects + /// the current configuration state of the shell and may change over time. + pub(crate) fn parser_options(&self) -> parser::ParserOptions { parser::ParserOptions { enable_extended_globbing: self.options.extended_globbing, posix_mode: self.options.posix_mode, @@ -601,11 +690,19 @@ impl Shell { } } - pub fn in_function(&self) -> bool { + /// Returns whether or not the shell is actively executing in a shell function. + pub(crate) fn in_function(&self) -> bool { !self.function_call_stack.is_empty() } - pub fn enter_function( + /// Updates the shell's internal tracking state to reflect that a new shell + /// function is being entered. + /// + /// # Arguments + /// + /// * `name` - The name of the function being entered. + /// * `function_def` - The definition of the function being entered. + pub(crate) fn enter_function( &mut self, name: &str, function_def: &Arc, @@ -619,7 +716,9 @@ impl Shell { Ok(()) } - pub fn leave_function(&mut self) -> Result<(), error::Error> { + /// Updates the shell's internal tracking state to reflect that the shell + /// has exited the top-most function on its call stack. + pub(crate) fn leave_function(&mut self) -> Result<(), error::Error> { self.env.pop_scope(env::EnvironmentScope::Local)?; self.function_call_stack.pop_front(); self.update_funcname_var()?; @@ -673,6 +772,7 @@ impl Shell { Ok(()) } + /// Returns the path to the history file used by the shell, if one is set. pub fn get_history_file_path(&self) -> Option { self.env.get("HISTFILE").map(|(_, var)| { let histfile_str: String = var.value().to_cow_string().to_string(); @@ -680,17 +780,25 @@ impl Shell { }) } - pub fn get_current_input_line_number(&self) -> u32 { + /// Returns the number of the line being executed in the currently executing program. + pub(crate) fn get_current_input_line_number(&self) -> u32 { self.current_line_number } - pub fn get_ifs(&self) -> Cow<'_, str> { + /// Returns the current value of the IFS variable, or the default value if it is not set. + pub(crate) fn get_ifs(&self) -> Cow<'_, str> { self.env.get("IFS").map_or_else( || Cow::Borrowed(" \t\n"), |(_, v)| v.value().to_cow_string(), ) } + /// Generates command completions for the shell. + /// + /// # Arguments + /// + /// * `input` - The input string to generate completions for. + /// * `position` - The position in the input string to generate completions at. pub async fn get_completions( &mut self, input: &str, @@ -702,8 +810,13 @@ impl Shell { .await } + /// Finds executables in the shell's current default PATH, matching the given glob pattern. + /// + /// # Arguments + /// + /// * `required_glob_pattern` - The glob pattern to match against. #[allow(clippy::manual_flatten)] - pub fn find_executables_in_path(&self, required_glob_pattern: &str) -> Vec { + pub(crate) fn find_executables_in_path(&self, required_glob_pattern: &str) -> Vec { let is_executable = |path: &Path| path.executable(); let mut executables = vec![]; @@ -724,7 +837,12 @@ impl Shell { executables } - pub fn set_working_dir(&mut self, target_dir: &Path) -> Result<(), error::Error> { + /// Sets the shell's current working directory to the given path. + /// + /// # Arguments + /// + /// * `target_dir` - The path to set as the working directory. + pub(crate) fn set_working_dir(&mut self, target_dir: &Path) -> Result<(), error::Error> { let abs_path = if target_dir.is_absolute() { PathBuf::from(target_dir) } else { @@ -763,7 +881,12 @@ impl Shell { Ok(()) } - pub fn tilde_shorten(&self, s: String) -> String { + /// Tilde-shortens the given string, replacing the user's home directory with a tilde. + /// + /// # Arguments + /// + /// * `s` - The string to shorten. + pub(crate) fn tilde_shorten(&self, s: String) -> String { if let Some(home_dir) = self.get_home_dir() { if let Some(stripped) = s.strip_prefix(home_dir.to_string_lossy().as_ref()) { return format!("~{stripped}"); @@ -772,7 +895,8 @@ impl Shell { s } - pub fn get_home_dir(&self) -> Option { + /// Returns the shell's current home directory, if available. + pub(crate) fn get_home_dir(&self) -> Option { Self::get_home_dir_with_env(&self.env) } @@ -785,15 +909,24 @@ impl Shell { } } + /// Returns a value that can be used to write to the shell's currently configured + /// standard output stream using `write!` at al. pub fn stdout(&self) -> openfiles::OpenFile { self.open_files.files.get(&1).unwrap().try_dup().unwrap() } + /// Returns a value that can be used to write to the shell's currently configured + /// standard error stream using `write!` et al. pub fn stderr(&self) -> openfiles::OpenFile { self.open_files.files.get(&2).unwrap().try_dup().unwrap() } - pub fn trace_command>(&self, command: S) -> Result<(), std::io::Error> { + /// Outputs `set -x` style trace output for a command. + /// + /// # Arguments + /// + /// * `command` - The command to trace. + pub(crate) fn trace_command>(&self, command: S) -> Result<(), std::io::Error> { // TODO: get prefix from PS4 const DEFAULT_PREFIX: &str = "+ "; @@ -809,7 +942,8 @@ impl Shell { writeln!(self.stderr(), "{prefix}{}", command.as_ref()) } - pub fn get_keywords(&self) -> Vec { + /// Returns the keywords that are reserved by the shell. + pub(crate) fn get_keywords(&self) -> Vec { if self.options.sh_mode { keywords::SH_MODE_KEYWORDS.iter().cloned().collect() } else { @@ -817,6 +951,7 @@ impl Shell { } } + /// Checks for completed jobs in the shell, reporting any changes found. pub fn check_for_completed_jobs(&mut self) -> Result<(), error::Error> { let results = self.jobs.poll()?; diff --git a/shell/src/traps.rs b/shell/src/traps.rs index 82eb535a..e46efc39 100644 --- a/shell/src/traps.rs +++ b/shell/src/traps.rs @@ -1,10 +1,15 @@ use std::{collections::HashMap, fmt::Display}; +/// Type of signal that can be trapped in the shell. #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub enum TrapSignal { + /// A system signal. Signal(nix::sys::signal::Signal), + /// The `DEBUG` trap. Debug, + /// The `ERR` trap. Err, + /// The `EXIT` trap. Exit, } @@ -20,6 +25,7 @@ impl Display for TrapSignal { } impl TrapSignal { + /// Returns all possible values of `TrapSignal`. pub fn all_values() -> Vec { let mut signals = vec![TrapSignal::Debug, TrapSignal::Err, TrapSignal::Exit]; @@ -31,17 +37,31 @@ impl TrapSignal { } } +/// Configuration for trap handlers in the shell. #[derive(Clone, Default)] pub struct TrapHandlerConfig { + /// Registered handlers for traps; maps signal type to command. pub handlers: HashMap, + /// Current depth of the handler stack. pub handler_depth: i32, } impl TrapHandlerConfig { + /// Registers a handler for a trap signal. + /// + /// # Arguments + /// + /// * `signal_type` - The type of signal to register a handler for. + /// * `command` - The command to execute when the signal is trapped. pub fn register_handler(&mut self, signal_type: TrapSignal, command: String) { let _ = self.handlers.insert(signal_type, command); } + /// Removes handlers for a trap signal. + /// + /// # Arguments + /// + /// * `signal_type` - The type of signal to remove handlers for. pub fn remove_handlers(&mut self, signal_type: TrapSignal) { self.handlers.remove(&signal_type); } diff --git a/shell/src/variables.rs b/shell/src/variables.rs index 353ad6aa..ec3848ff 100644 --- a/shell/src/variables.rs +++ b/shell/src/variables.rs @@ -5,22 +5,35 @@ use std::fmt::{Display, Write}; use crate::error; +/// A shell variable. #[derive(Clone, Debug)] pub struct ShellVariable { + /// The value currently associated with the variable. value: ShellValue, + /// Whether or not the variable is marked as exported to child processes. exported: bool, + /// Whether or not the variable is marked as read-only. readonly: bool, + /// Whether or not the variable should be enumerated in the shell's environment. enumerable: bool, + /// The transformation to apply to the variable's value when it is updated. transform_on_update: ShellVariableUpdateTransform, + /// Whether or not the variable is marked as being traced. trace: bool, + /// Whether or not the variable should be treated as an integer. treat_as_integer: bool, + /// Whether or not the variable should be treated as a name reference. treat_as_nameref: bool, } +/// Kind of transformation to apply to a variable's value when it is updated. #[derive(Clone, Debug)] pub enum ShellVariableUpdateTransform { + /// No transformation. None, + /// Convert the value to lowercase. Lowercase, + /// Convert the value to uppercase. Uppercase, } @@ -40,6 +53,11 @@ impl Default for ShellVariable { } impl ShellVariable { + /// Returns a new shell variable, initialized with the given value. + /// + /// # Arguments + /// + /// * `value` - The value to associate with the variable. pub fn new(value: ShellValue) -> Self { Self { value, @@ -47,30 +65,37 @@ impl ShellVariable { } } + /// Returns the value associated with the variable. pub fn value(&self) -> &ShellValue { &self.value } + /// Returns whether or not the variable is exported to child processes. pub fn is_exported(&self) -> bool { self.exported } + /// Marks the variable as exported to child processes. pub fn export(&mut self) { self.exported = true; } + /// Marks the variable as not exported to child processes. pub fn unexport(&mut self) { self.exported = false; } + /// Returns whether or not the variable is read-only. pub fn is_readonly(&self) -> bool { self.readonly } + /// Marks the variable as read-only. pub fn set_readonly(&mut self) { self.readonly = true; } + /// Marks the variable as not read-only. pub fn unset_readonly(&mut self) -> Result<(), error::Error> { if self.readonly { return Err(error::Error::ReadonlyVariable); @@ -80,58 +105,72 @@ impl ShellVariable { Ok(()) } + /// Returns whether or not the variable is traced. pub fn is_trace_enabled(&self) -> bool { self.trace } + /// Marks the variable as traced. pub fn enable_trace(&mut self) { self.trace = true; } + /// Marks the variable as not traced. pub fn disable_trace(&mut self) { self.trace = false; } + /// Returns whether or not the variable should be enumerated in the shell's environment. pub fn is_enumerable(&self) -> bool { self.enumerable } + /// Marks the variable as not enumerable in the shell's environment. pub fn hide_from_enumeration(&mut self) { self.enumerable = false; } + /// Return the update transform associated with the variable. pub fn get_update_transform(&self) -> ShellVariableUpdateTransform { self.transform_on_update.clone() } + /// Set the update transform associated with the variable. pub fn set_update_transform(&mut self, transform: ShellVariableUpdateTransform) { self.transform_on_update = transform; } + /// Returns whether or not the variable should be treated as an integer. pub fn is_treated_as_integer(&self) -> bool { self.treat_as_integer } + /// Marks the variable as being treated as an integer. pub fn treat_as_integer(&mut self) { self.treat_as_integer = true; } + /// Marks the variable as not being treated as an integer. pub fn unset_treat_as_integer(&mut self) { self.treat_as_integer = false; } + /// Returns whether or not the variable should be treated as a name reference. pub fn is_treated_as_nameref(&self) -> bool { self.treat_as_nameref } + /// Marks the variable as being treated as a name reference. pub fn treat_as_nameref(&mut self) { self.treat_as_nameref = true; } + /// Marks the variable as not being treated as a name reference. pub fn unset_treat_as_nameref(&mut self) { self.treat_as_nameref = false; } + /// Converts the variable to an indexed array. pub fn convert_to_indexed_array(&mut self) -> Result<(), error::Error> { match self.value() { ShellValue::IndexedArray(_) => Ok(()), @@ -147,6 +186,7 @@ impl ShellVariable { } } + /// Converts the variable to an associative array. pub fn convert_to_associative_array(&mut self) -> Result<(), error::Error> { match self.value() { ShellValue::AssociativeArray(_) => Ok(()), @@ -162,6 +202,12 @@ impl ShellVariable { } } + /// Assign the given value to the variable, conditionally appending to the preexisting value. + /// + /// # Arguments + /// + /// * `value` - The value to assign to the variable. + /// * `append` - Whether or not to append the value to the preexisting value. pub fn assign(&mut self, value: ShellValueLiteral, append: bool) -> Result<(), error::Error> { if self.is_readonly() { return Err(error::Error::ReadonlyVariable); @@ -281,6 +327,14 @@ impl ShellVariable { } } + /// Assign the given value to the variable at the given index, conditionally appending to the + /// preexisting value present at that element within the value. + /// + /// # Arguments + /// + /// * `array_index` - The index at which to assign the value. + /// * `value` - The value to assign to the variable at the given index. + /// * `append` - Whether or not to append the value to the preexisting value stored at the given index. #[allow(clippy::needless_pass_by_value)] pub fn assign_at_index( &mut self, @@ -353,6 +407,12 @@ impl ShellVariable { } } + /// Tries to unset the value stored at the given index in the variable. Returns + /// whether or not a value was unset. + /// + /// # Arguments + /// + /// * `index` - The index at which to unset the value. pub fn unset_index(&mut self, index: &str) -> Result { match &mut self.value { ShellValue::Unset(ty) => match ty { @@ -370,6 +430,7 @@ impl ShellVariable { } } + /// Returns the canonical attribute flag string for this variable. pub fn get_attribute_flags(&self) -> String { let mut result = String::new(); @@ -412,25 +473,38 @@ impl ShellVariable { } } +/// A shell value. #[derive(Clone, Debug)] pub enum ShellValue { + /// A value that has been typed but not yet set. Unset(ShellValueUnsetType), + /// A string. String(String), + /// An associative array. AssociativeArray(BTreeMap), + /// An indexed array. IndexedArray(BTreeMap), + /// A special value that yields a different random number each time its read. Random, } +/// The type of an unset shell value. #[derive(Clone, Debug)] pub enum ShellValueUnsetType { + /// The value is untyped. Untyped, + /// The value is an associative array. AssociativeArray, + /// The value is an indexed array. IndexedArray, } +/// A shell value literal; used for assignment. #[derive(Clone, Debug)] pub enum ShellValueLiteral { + /// A scalar value. Scalar(String), + /// An array value. Array(ArrayLiteral), } @@ -476,16 +550,21 @@ impl From> for ShellValueLiteral { } } +/// An array literal. #[derive(Clone, Debug)] pub struct ArrayLiteral(pub Vec<(Option, String)>); +/// Style for formatting a shell variable's value. #[derive(Copy, Clone, Debug)] pub enum FormatStyle { + /// Basic formatting. Basic, + /// Formatting as appropriate in the `declare` built-in command. DeclarePrint, } impl ShellValue { + /// Returns whether or not the value is an array. pub fn is_array(&self) -> bool { matches!( self, @@ -497,6 +576,11 @@ impl ShellValue { ) } + /// Returns a new indexed array value constructed from the given slice of strings. + /// + /// # Arguments + /// + /// * `values` - The slice of strings to construct the indexed array from. pub fn indexed_array_from_slice(values: &[&str]) -> Self { let mut owned_values = BTreeMap::new(); for (i, value) in values.iter().enumerate() { @@ -506,6 +590,11 @@ impl ShellValue { ShellValue::IndexedArray(owned_values) } + /// Returns a new indexed array value constructed from the given literals. + /// + /// # Arguments + /// + /// * `literals` - The literals to construct the indexed array from. pub fn indexed_array_from_literals(literals: ArrayLiteral) -> Result { let mut values = BTreeMap::new(); ShellValue::update_indexed_array_from_literals(&mut values, literals)?; @@ -536,6 +625,11 @@ impl ShellValue { Ok(()) } + /// Returns a new associative array value constructed from the given literals. + /// + /// # Arguments + /// + /// * `literals` - The literals to construct the associative array from. pub fn associative_array_from_literals( literals: ArrayLiteral, ) -> Result { @@ -571,6 +665,11 @@ impl ShellValue { Ok(()) } + /// Formats the value using the given style. + /// + /// # Arguments + /// + /// * `style` - The style to use for formatting the value. pub fn format(&self, style: FormatStyle) -> Result, error::Error> { match self { ShellValue::Unset(_) => Ok("".into()), @@ -616,6 +715,11 @@ impl ShellValue { } } + /// Tries to retrieve the value stored at the given index in this variable. + /// + /// # Arguments + /// + /// * `index` - The index at which to retrieve the value. #[allow(clippy::unnecessary_wraps)] pub fn get_at(&self, index: &str) -> Result>, error::Error> { match self { @@ -638,6 +742,7 @@ impl ShellValue { } } + /// Returns the keys of the elements in this variable. pub fn get_element_keys(&self) -> Vec { match self { ShellValue::Unset(_) => vec![], @@ -647,6 +752,7 @@ impl ShellValue { } } + /// Returns the values of the elements in this variable. pub fn get_element_values(&self) -> Vec { match self { ShellValue::Unset(_) => vec![], @@ -657,6 +763,7 @@ impl ShellValue { } } + /// Converts this value to a string. pub fn to_cow_string(&self) -> Cow<'_, str> { match self { ShellValue::Unset(_) => Cow::Borrowed(""), @@ -671,6 +778,11 @@ impl ShellValue { } } + /// Formats this value as a program string usable in an assignment. + /// + /// # Arguments + /// + /// * `index` - The index at which to retrieve the value, if indexing is to be performed. pub fn to_assignable_str(&self, index: Option<&str>) -> String { match self { ShellValue::Unset(_) => String::new(),