Implemented the 'help' builtin, and support for :h <topic> in ex mode
:h is an alias for the 'help' builtin. 'help' takes a single argument and tries to find a suitable match among the files in '$SHED_HPATH' if a match is found, this file is opened in your pager calling the 'help' builtin using :h in ex mode will preserve your current pending line
This commit is contained in:
@@ -35,6 +35,7 @@ rand = "0.10.0"
|
||||
regex = "1.11.1"
|
||||
scopeguard = "1.2.0"
|
||||
serde_json = "1.0.149"
|
||||
tempfile = "3.24.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
unicode-width = "0.2.0"
|
||||
vte = "0.15"
|
||||
|
||||
76
doc/arith.txt
Normal file
76
doc/arith.txt
Normal file
@@ -0,0 +1,76 @@
|
||||
*arith* *arithmetic* *arithmetic-expansion*
|
||||
|
||||
#ARITHMETIC EXPANSION#
|
||||
|
||||
Arithmetic expansion evaluates a mathematical expression and substitutes
|
||||
the result. The expression is subject to parameter expansion and command
|
||||
substitution before evaluation.
|
||||
|
||||
`$((expression))`
|
||||
|
||||
Example:
|
||||
`echo $((2 + 3))` # prints: 5
|
||||
`x=$((width * height))`
|
||||
|
||||
==============================================================================
|
||||
1. Operators *arith-operators*
|
||||
|
||||
The following operators are supported, listed from highest to lowest
|
||||
precedence:
|
||||
|
||||
`( )` *arith-parens*
|
||||
|
||||
Grouping. Override default precedence.
|
||||
|
||||
Example:
|
||||
`echo $(( (2+3) * 4 ))` # prints: 20
|
||||
|
||||
`*` `/` `%` *arith-muldivmod*
|
||||
|
||||
Multiplication, division, and modulo (remainder).
|
||||
|
||||
Example:
|
||||
`echo $((10 / 3))` # prints: 3
|
||||
`echo $((10 % 3))` # prints: 1
|
||||
|
||||
`+` `-` *arith-addsub*
|
||||
|
||||
Addition and subtraction.
|
||||
|
||||
Example:
|
||||
`echo $((10 - 3 + 1))` # prints: 8
|
||||
|
||||
==============================================================================
|
||||
2. Variables in Expressions *arith-variables*
|
||||
|
||||
Variables can be referenced by name inside arithmetic expressions.
|
||||
They are expanded and converted to numbers.
|
||||
|
||||
`x=10`
|
||||
`echo $(($x + 5))` # prints: 15
|
||||
`echo $((x + 5))` # also works
|
||||
|
||||
If a variable is unset or not a valid number, an error is reported.
|
||||
|
||||
==============================================================================
|
||||
3. Nesting *arith-nesting*
|
||||
|
||||
Arithmetic expressions can be nested with parentheses to any depth:
|
||||
|
||||
`echo $(( (1+2) * (3+4) ))` # prints: 21
|
||||
|
||||
Arithmetic expansion can also appear inside other expansions:
|
||||
|
||||
`echo "Total: $((price * qty))"`
|
||||
|
||||
==============================================================================
|
||||
4. Whitespace *arith-whitespace*
|
||||
|
||||
Whitespace inside `$((...))` is ignored and can be used freely for
|
||||
readability:
|
||||
|
||||
`echo $((2+3))` # prints: 5
|
||||
`echo $(( 2 + 3 ))` # same result
|
||||
|
||||
==============================================================================
|
||||
See also: |param| |redirect| |glob|
|
||||
155
doc/glob.txt
Normal file
155
doc/glob.txt
Normal file
@@ -0,0 +1,155 @@
|
||||
*glob* *globbing* *pathname-expansion* *filename-expansion*
|
||||
|
||||
#PATHNAME EXPANSION#
|
||||
|
||||
After word splitting, the shell scans each word for the characters `*`,
|
||||
`?`, and `[`. If any appear (and are not quoted), the word is treated as a
|
||||
pattern and replaced with an alphabetically sorted list of matching file
|
||||
names. If no files match, the pattern is left unchanged.
|
||||
|
||||
==============================================================================
|
||||
1. Wildcards *glob-wildcards*
|
||||
|
||||
`*` *glob-star*
|
||||
|
||||
Matches any string of zero or more characters, except that it does
|
||||
not match a leading `.` (see |glob-dotglob|) or a `/`.
|
||||
|
||||
Example:
|
||||
`echo *.txt` # all .txt files
|
||||
`ls src/*.rs` # all .rs files in src/
|
||||
|
||||
`?` *glob-question*
|
||||
|
||||
Matches exactly one character, with the same restrictions as `*`.
|
||||
|
||||
Example:
|
||||
`ls file?.txt` # file1.txt, fileA.txt, etc.
|
||||
|
||||
`[...]` *glob-bracket*
|
||||
|
||||
Matches any one of the enclosed characters. A range can be specified
|
||||
with a hyphen.
|
||||
|
||||
`[abc]` matches `a`, `b`, or `c`
|
||||
`[a-z]` matches any lowercase letter
|
||||
`[0-9]` matches any digit
|
||||
`[A-Za-z]` matches any letter
|
||||
|
||||
`[!...]` `[^...]` *glob-bracket-negate*
|
||||
|
||||
Matches any character NOT in the set.
|
||||
|
||||
Example:
|
||||
`ls [!.]*.txt` # .txt files not starting with dot
|
||||
`echo file[^0-9].txt` # files without a digit
|
||||
|
||||
==============================================================================
|
||||
2. Hidden Files *glob-dotglob*
|
||||
|
||||
By default, patterns do not match files whose names begin with `.`
|
||||
(hidden files). A leading dot must be matched explicitly:
|
||||
|
||||
`echo .*` # only hidden files
|
||||
`echo .* *` # hidden and non-hidden files
|
||||
|
||||
The `dotglob` shell option changes this behavior:
|
||||
|
||||
`shopt core.dotglob true`
|
||||
|
||||
When enabled, `*` and `?` will also match files starting with `.`.
|
||||
|
||||
==============================================================================
|
||||
3. Brace Expansion *brace* *brace-expansion*
|
||||
|
||||
Brace expansion is performed before globbing and generates multiple
|
||||
words from a single pattern. It is not a POSIX feature.
|
||||
|
||||
`{a,b,c}` *brace-list*
|
||||
|
||||
Comma-separated list. Each item becomes a separate word.
|
||||
|
||||
Example:
|
||||
`echo {a,b,c}` # prints: a b c
|
||||
`echo file.{txt,log}` # prints: file.txt file.log
|
||||
`mkdir -p src/{bin,lib}`
|
||||
|
||||
`{N..M}` *brace-range*
|
||||
|
||||
Numeric or character range.
|
||||
|
||||
Example:
|
||||
`echo {1..5}` # prints: 1 2 3 4 5
|
||||
`echo {a..f}` # prints: a b c d e f
|
||||
`echo {5..1}` # prints: 5 4 3 2 1
|
||||
|
||||
`{N..M..S}` *brace-range-step*
|
||||
|
||||
Numeric range with step {S}.
|
||||
|
||||
Example:
|
||||
`echo {0..10..2}` # prints: 0 2 4 6 8 10
|
||||
`echo {1..20..5}` # prints: 1 6 11 16
|
||||
|
||||
`{01..10}` *brace-range-pad*
|
||||
|
||||
Zero-padded ranges. If either endpoint has leading zeros, all
|
||||
generated values are padded to the same width.
|
||||
|
||||
Example:
|
||||
`echo {01..05}` # prints: 01 02 03 04 05
|
||||
`echo {001..3}` # prints: 001 002 003
|
||||
|
||||
Brace expansion can be nested and combined with other expansions:
|
||||
|
||||
`echo {a,b{1..3},c}` # prints: a b1 b2 b3 c
|
||||
|
||||
==============================================================================
|
||||
4. Quoting and Escaping *glob-quoting*
|
||||
|
||||
Glob characters lose their special meaning when quoted:
|
||||
|
||||
`echo "*"` # prints literal *
|
||||
`echo '*.txt'` # prints literal *.txt
|
||||
`echo \*` # prints literal *
|
||||
|
||||
This is important when passing patterns to commands like `find` or
|
||||
`grep` where you want the command (not the shell) to interpret the
|
||||
pattern.
|
||||
|
||||
==============================================================================
|
||||
5. Tilde Expansion *tilde* *tilde-expansion*
|
||||
|
||||
Tilde expansion is performed before pathname expansion.
|
||||
|
||||
`~` *tilde-home*
|
||||
|
||||
Expands to the value of `$HOME`.
|
||||
|
||||
`~/path` *tilde-home-path*
|
||||
|
||||
Expands `~` to `$HOME`, then appends the path.
|
||||
|
||||
Example:
|
||||
`cd ~/projects`
|
||||
`ls ~/.config`
|
||||
|
||||
`~user` *tilde-user*
|
||||
|
||||
Expands to the home directory of {user}.
|
||||
|
||||
Example:
|
||||
`ls ~root` # /root
|
||||
`cat ~nobody/.profile`
|
||||
|
||||
`~uid` *tilde-uid*
|
||||
|
||||
Expands to the home directory of the user with numeric uid {uid}.
|
||||
This is a shed-specific extension.
|
||||
|
||||
Example:
|
||||
`echo ~0` # /root (uid 0)
|
||||
`echo ~1000` # first normal user's home
|
||||
|
||||
==============================================================================
|
||||
See also: |param| |redirect| |arith|
|
||||
197
doc/param.txt
Normal file
197
doc/param.txt
Normal file
@@ -0,0 +1,197 @@
|
||||
*param* *parameter-expansion* *param-expansion*
|
||||
|
||||
#PARAMETER EXPANSION#
|
||||
|
||||
The shell provides several forms of parameter expansion for working with
|
||||
variables. In each form, {word} is subject to tilde expansion, parameter
|
||||
expansion, command substitution, and arithmetic expansion.
|
||||
|
||||
If {parameter} is unset or null, the behavior depends on the operator used.
|
||||
"Unset" means the variable has never been assigned. "Null" means the variable
|
||||
is set but its value is the empty string.
|
||||
|
||||
==============================================================================
|
||||
1. Basic Forms *param-basic*
|
||||
|
||||
`$var` Value of {var}
|
||||
`${var}` Same, with explicit braces (needed for `${var}foo`)
|
||||
|
||||
Braces are required when {var} is followed by characters that could be part
|
||||
of the name, or when using any of the operators below.
|
||||
|
||||
==============================================================================
|
||||
2. Default Values *param-default*
|
||||
|
||||
`${var:-word}` *param-default-val*
|
||||
|
||||
Use default value. If {var} is unset or null, expand to {word}.
|
||||
Otherwise, expand to the value of {var}.
|
||||
|
||||
Example:
|
||||
`name=${1:-world}`
|
||||
`echo "hello $name"` # prints "hello world" if \$1 is unset
|
||||
|
||||
`${var-word}` *param-default-nonnull*
|
||||
|
||||
Like `:-` but only substitutes {word} if {var} is completely unset,
|
||||
not if it is null.
|
||||
|
||||
==============================================================================
|
||||
3. Assign Defaults *param-assign*
|
||||
|
||||
`${var:=word}` *param-assign-val*
|
||||
|
||||
Assign default value. If {var} is unset or null, assign {word} to
|
||||
{var} and then expand to the new value.
|
||||
|
||||
Note: This cannot be used with positional parameters or special
|
||||
parameters.
|
||||
|
||||
Example:
|
||||
`echo ${cache:=/tmp/cache}` # sets and uses \$cache
|
||||
|
||||
`${var=word}` *param-assign-nonnull*
|
||||
|
||||
Like `:=` but only assigns if {var} is completely unset.
|
||||
|
||||
==============================================================================
|
||||
4. Error on Unset *param-error*
|
||||
|
||||
`${var:?word}` *param-error-val*
|
||||
|
||||
Display error. If {var} is unset or null, print {word} to stderr
|
||||
and exit (in a non-interactive shell). If {word} is omitted, a
|
||||
default message is printed.
|
||||
|
||||
Example:
|
||||
`input=${1:?usage: myscript \<filename\>}`
|
||||
|
||||
`${var?word}` *param-error-nonnull*
|
||||
|
||||
Like `:?` but only errors if {var} is completely unset.
|
||||
|
||||
==============================================================================
|
||||
5. Alternate Value *param-alt*
|
||||
|
||||
`${var:+word}` *param-alt-val*
|
||||
|
||||
Use alternate value. If {var} is unset or null, expand to nothing.
|
||||
Otherwise, expand to {word}.
|
||||
|
||||
Example:
|
||||
`echo ${verbose:+--verbose}` # flag only if \$verbose is set
|
||||
|
||||
`${var+word}` *param-alt-nonnull*
|
||||
|
||||
Like `:+` but substitutes {word} only if {var} is set (even if null).
|
||||
|
||||
==============================================================================
|
||||
6. String Length *param-length*
|
||||
|
||||
`${#var}` *param-strlen*
|
||||
|
||||
Expands to the length of the value of {var} in characters.
|
||||
|
||||
Example:
|
||||
`str="hello"`
|
||||
`echo ${#str}` # prints 5
|
||||
|
||||
==============================================================================
|
||||
7. Substring Removal *param-substring*
|
||||
|
||||
`${var#pattern}` *param-trim-short-left*
|
||||
|
||||
Remove shortest matching prefix. Removes the shortest match of
|
||||
{pattern} from the beginning of the value of {var}.
|
||||
|
||||
`${var##pattern}` *param-trim-long-left*
|
||||
|
||||
Remove longest matching prefix.
|
||||
|
||||
Example:
|
||||
`path="/home/user/file.txt"`
|
||||
`echo ${path##*/}` # prints "file.txt"
|
||||
|
||||
`${var%pattern}` *param-trim-short-right*
|
||||
|
||||
Remove shortest matching suffix. Removes the shortest match of
|
||||
{pattern} from the end of the value of {var}.
|
||||
|
||||
`${var%%pattern}` *param-trim-long-right*
|
||||
|
||||
Remove longest matching suffix.
|
||||
|
||||
Example:
|
||||
`file="archive.tar.gz"`
|
||||
`echo ${file%%.*}` # prints "archive"
|
||||
`echo ${file%.*}` # prints "archive.tar"
|
||||
|
||||
==============================================================================
|
||||
8. Search and Replace *param-replace*
|
||||
|
||||
`${var/pattern/replacement}` *param-replace-first*
|
||||
|
||||
Replace first match. Replaces the first occurrence of {pattern}
|
||||
in the value of {var} with {replacement}.
|
||||
|
||||
`${var//pattern/replacement}` *param-replace-all*
|
||||
|
||||
Replace all matches.
|
||||
|
||||
Example:
|
||||
`str="hello world"`
|
||||
`echo ${str/o/0}` # prints "hell0 world"
|
||||
`echo ${str//o/0}` # prints "hell0 w0rld"
|
||||
|
||||
`${var/#pattern/replacement}` *param-replace-prefix*
|
||||
|
||||
Replace if matching at the beginning.
|
||||
|
||||
`${var/%pattern/replacement}` *param-replace-suffix*
|
||||
|
||||
Replace if matching at the end.
|
||||
|
||||
==============================================================================
|
||||
9. Case Modification *param-case*
|
||||
|
||||
`${var^}` *param-upper-first*
|
||||
|
||||
Uppercase the first character of {var}.
|
||||
|
||||
`${var^^}` *param-upper-all*
|
||||
|
||||
Uppercase all characters.
|
||||
|
||||
`${var,}` *param-lower-first*
|
||||
|
||||
Lowercase the first character of {var}.
|
||||
|
||||
`${var,,}` *param-lower-all*
|
||||
|
||||
Lowercase all characters.
|
||||
|
||||
Example:
|
||||
`name="john doe"`
|
||||
`echo ${name^}` # prints "John doe"
|
||||
`echo ${name^^}` # prints "JOHN DOE"
|
||||
|
||||
==============================================================================
|
||||
10. Substrings *param-slice*
|
||||
|
||||
`${var:offset}` *param-slice-from*
|
||||
|
||||
Substring starting at {offset} (0-indexed).
|
||||
|
||||
`${var:offset:length}` *param-slice-range*
|
||||
|
||||
Substring of {length} characters starting at {offset}.
|
||||
|
||||
Negative offsets count from the end (note the space before the minus
|
||||
to distinguish from `:-`):
|
||||
|
||||
`str="hello world"`
|
||||
`echo ${str: -5}` # prints "world"
|
||||
`echo ${str:0:5}` # prints "hello"
|
||||
|
||||
==============================================================================
|
||||
See also: |redirect| |glob| |arith|
|
||||
181
doc/redirect.txt
Normal file
181
doc/redirect.txt
Normal file
@@ -0,0 +1,181 @@
|
||||
*redirect* *redirection* *redir*
|
||||
|
||||
#REDIRECTION#
|
||||
|
||||
Redirections allow you to control where a command reads its input from and
|
||||
where it sends its output. A redirection applies to a specific file
|
||||
descriptor; if no descriptor number is given, output redirections default
|
||||
to stdout (fd 1) and input redirections default to stdin (fd 0).
|
||||
|
||||
==============================================================================
|
||||
1. Output Redirection *redir-output*
|
||||
|
||||
`command > file` *redir-out*
|
||||
|
||||
Redirect stdout to {file}, creating it if it does not exist or
|
||||
truncating it if it does.
|
||||
|
||||
Example:
|
||||
`echo hello > out.txt`
|
||||
`ls 2> errors.txt` # redirect stderr
|
||||
|
||||
`command >| file` *redir-out-force*
|
||||
|
||||
Like `>` but overrides the {noclobber} option. If {noclobber} is set,
|
||||
`>` will refuse to overwrite an existing file; `>|` forces the
|
||||
overwrite.
|
||||
|
||||
`command >> file` *redir-append*
|
||||
|
||||
Append stdout to {file}, creating it if it does not exist.
|
||||
|
||||
Example:
|
||||
`echo line >> log.txt`
|
||||
|
||||
==============================================================================
|
||||
2. Input Redirection *redir-input*
|
||||
|
||||
`command < file` *redir-in*
|
||||
|
||||
Redirect {file} to stdin.
|
||||
|
||||
Example:
|
||||
`sort < unsorted.txt`
|
||||
|
||||
==============================================================================
|
||||
3. Read-Write Redirection *redir-readwrite*
|
||||
|
||||
`command <> file` *redir-rw*
|
||||
|
||||
Open {file} for both reading and writing on the specified file
|
||||
descriptor (default fd 0). The file is created if it does not exist
|
||||
but is not truncated.
|
||||
|
||||
Useful with the `seek` builtin for random-access file operations.
|
||||
|
||||
Example:
|
||||
`exec 3<> data.bin`
|
||||
`seek 3 0 set` # seek to beginning
|
||||
|
||||
==============================================================================
|
||||
4. File Descriptor Duplication *redir-dup*
|
||||
|
||||
`command N>&M` *redir-dup-out*
|
||||
|
||||
Duplicate output file descriptor {M} onto {N}. After this, writing
|
||||
to fd {N} goes to the same place as fd {M}.
|
||||
|
||||
Example:
|
||||
`command > out.txt 2>&1` # stderr goes where stdout goes
|
||||
|
||||
`command N<&M` *redir-dup-in*
|
||||
|
||||
Duplicate input file descriptor {M} onto {N}.
|
||||
|
||||
`command N>&-` *redir-close-out*
|
||||
`command N<&-` *redir-close-in*
|
||||
|
||||
Close file descriptor {N}.
|
||||
|
||||
Example:
|
||||
`exec 3>&-` # close fd 3
|
||||
|
||||
==============================================================================
|
||||
5. Pipelines *redir-pipe*
|
||||
|
||||
`command1 | command2` *pipe*
|
||||
|
||||
Connect stdout of {command1} to stdin of {command2}. Both commands
|
||||
run concurrently.
|
||||
|
||||
Example:
|
||||
`cat file.txt | grep pattern | sort`
|
||||
|
||||
`command1 |& command2` *pipe-and*
|
||||
|
||||
Connect both stdout and stderr of {command1} to stdin of {command2}.
|
||||
Equivalent to `command1 2>&1 | command2`.
|
||||
|
||||
==============================================================================
|
||||
6. Here Documents *heredoc*
|
||||
|
||||
`command << DELIM` *redir-heredoc*
|
||||
|
||||
Read input from the script body until a line containing only {DELIM}
|
||||
is found. The text between is fed to stdin of {command}.
|
||||
|
||||
Parameter expansion, command substitution, and arithmetic expansion
|
||||
are performed in the body unless the delimiter is quoted.
|
||||
|
||||
Example:
|
||||
`cat << EOF`
|
||||
`Hello $USER`
|
||||
`EOF`
|
||||
|
||||
`command << 'DELIM'` *redir-heredoc-literal*
|
||||
|
||||
Quoting the delimiter (single or double quotes) suppresses all
|
||||
expansion in the heredoc body. The text is passed literally.
|
||||
|
||||
Example:
|
||||
`cat << 'EOF'`
|
||||
`This $variable is not expanded`
|
||||
`EOF`
|
||||
|
||||
`command <<- DELIM` *redir-heredoc-indent*
|
||||
|
||||
Like `<<` but strips leading tab characters from each line of the
|
||||
body and from the closing delimiter. This allows heredocs to be
|
||||
indented for readability without affecting the content.
|
||||
|
||||
Example:
|
||||
`if true; then`
|
||||
` cat <<- EOF`
|
||||
` indented content`
|
||||
` EOF`
|
||||
`fi`
|
||||
|
||||
==============================================================================
|
||||
7. Here Strings *herestring*
|
||||
|
||||
`command <<< word` *redir-herestring*
|
||||
|
||||
Feed {word} as a single string to stdin of {command}, with a
|
||||
trailing newline appended. {word} is subject to the usual expansions.
|
||||
|
||||
Example:
|
||||
`read first rest <<< "hello world"`
|
||||
`bc <<< "2 + 2"`
|
||||
|
||||
==============================================================================
|
||||
8. File Descriptor Numbers *redir-fd*
|
||||
|
||||
Any redirection operator can be prefixed with a file descriptor number:
|
||||
|
||||
`2> file` redirect stderr to file
|
||||
`3< file` open file on fd 3
|
||||
`4>> file` append to file on fd 4
|
||||
`5<> file` open file read-write on fd 5
|
||||
|
||||
Standard file descriptors:
|
||||
|
||||
0 stdin
|
||||
1 stdout
|
||||
2 stderr
|
||||
|
||||
File descriptors 3 and above are available for general use with `exec`.
|
||||
|
||||
==============================================================================
|
||||
9. Combining Redirections *redir-combine*
|
||||
|
||||
Multiple redirections can appear on a single command, processed left
|
||||
to right:
|
||||
|
||||
`command > out.txt 2>&1` # stdout to file, stderr to same file
|
||||
`command 2>&1 > out.txt` # different! stderr to terminal,
|
||||
# stdout to file
|
||||
|
||||
Order matters: each redirection is applied in sequence.
|
||||
|
||||
==============================================================================
|
||||
See also: |param| |glob| |arith|
|
||||
291
src/builtin/help.rs
Normal file
291
src/builtin/help.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use std::{env, io::Write, path::Path};
|
||||
|
||||
use ariadne::Span as ASpan;
|
||||
use nix::libc::STDIN_FILENO;
|
||||
|
||||
use crate::{
|
||||
libsh::{error::{ShErr, ShErrKind, ShResult}, guards::RawModeGuard}, parse::{NdRule, Node, Redir, RedirType, execute::{exec_input, prepare_argv}, lex::{QuoteState, Span}}, procio::{IoFrame, IoMode}, readline::{complete::ScoredCandidate, markers}, state
|
||||
};
|
||||
|
||||
const TAG_SEQ: &str = "\x1b[1;33m"; // bold yellow — searchable tags
|
||||
const REF_SEQ: &str = "\x1b[4;36m"; // underline cyan — cross-references
|
||||
const RESET_SEQ: &str = "\x1b[0m";
|
||||
const HEADER_SEQ: &str = "\x1b[1;35m"; // bold magenta — section headers
|
||||
const CODE_SEQ: &str = "\x1b[32m"; // green — inline code
|
||||
const KEYWORD_2_SEQ: &str = "\x1b[1;32m"; // bold green — {keyword}
|
||||
const KEYWORD_3_SEQ: &str = "\x1b[3;37m"; // italic white — [optional]
|
||||
|
||||
pub fn help(node: Node) -> ShResult<()> {
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let mut argv = prepare_argv(argv)?.into_iter().peekable();
|
||||
let help = argv.next().unwrap(); // drop 'help'
|
||||
|
||||
// Join all of the word-split arguments into a single string
|
||||
// Preserve the span too
|
||||
let (topic, span) = if argv.peek().is_none() {
|
||||
("help.txt".to_string(), help.1)
|
||||
} else {
|
||||
argv.fold((String::new(), Span::default()), |mut acc, arg| {
|
||||
if acc.1 == Span::default() {
|
||||
acc.1 = arg.1.clone();
|
||||
} else {
|
||||
let new_end = arg.1.end();
|
||||
let start = acc.1.start();
|
||||
acc.1.set_range(start..new_end);
|
||||
}
|
||||
|
||||
if acc.0.is_empty() {
|
||||
acc.0 = arg.0;
|
||||
} else {
|
||||
acc.0 = acc.0 + &format!(" {}",arg.0);
|
||||
}
|
||||
acc
|
||||
})
|
||||
};
|
||||
|
||||
let hpath = env::var("SHED_HPATH").unwrap_or_default();
|
||||
|
||||
for path in hpath.split(':') {
|
||||
let path = Path::new(&path).join(&topic);
|
||||
if path.is_file() {
|
||||
let Ok(contents) = std::fs::read_to_string(&path) else {
|
||||
continue;
|
||||
};
|
||||
let filename = path.file_stem()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let unescaped = unescape_help(&contents);
|
||||
let expanded = expand_help(&unescaped);
|
||||
open_help(&expanded, None, Some(filename))?;
|
||||
state::set_status(0);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// didn't find an exact filename match, its probably a tag search
|
||||
for path in hpath.split(':') {
|
||||
let path = Path::new(path);
|
||||
if let Ok(entries) = path.read_dir() {
|
||||
for entry in entries {
|
||||
let Ok(entry) = entry else { continue };
|
||||
let path = entry.path();
|
||||
let filename = path.file_stem()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(contents) = std::fs::read_to_string(&path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let unescaped = unescape_help(&contents);
|
||||
let expanded = expand_help(&unescaped);
|
||||
let tags = read_tags(&expanded);
|
||||
|
||||
for (tag, line) in &tags {
|
||||
}
|
||||
|
||||
if let Some((matched_tag, line)) = get_best_match(&topic, &tags) {
|
||||
open_help(&expanded, Some(line), Some(filename))?;
|
||||
state::set_status(0);
|
||||
return Ok(());
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state::set_status(1);
|
||||
Err(ShErr::at(
|
||||
ShErrKind::NotFound,
|
||||
span,
|
||||
"No relevant help page found for this topic",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn open_help(content: &str, line: Option<usize>, file_name: Option<String>) -> ShResult<()> {
|
||||
let pager = env::var("PAGER").unwrap_or("less -R".into());
|
||||
let line_arg = line.map(|ln| format!("+{ln}")).unwrap_or_default();
|
||||
let prompt_arg = file_name.map(|name| format!("-Ps'{name}'")).unwrap_or_default();
|
||||
|
||||
let mut tmp = tempfile::NamedTempFile::new()?;
|
||||
let tmp_path = tmp.path().to_string_lossy().to_string();
|
||||
tmp.write_all(content.as_bytes())?;
|
||||
tmp.flush()?;
|
||||
|
||||
RawModeGuard::with_cooked_mode(|| {
|
||||
exec_input(
|
||||
format!("{pager} {line_arg} {prompt_arg} {tmp_path}"),
|
||||
None,
|
||||
true,
|
||||
Some("help".into()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_best_match(topic: &str, tags: &[(String, usize)]) -> Option<(String, usize)> {
|
||||
let mut candidates: Vec<_> = tags.iter()
|
||||
.map(|(tag,line)| (ScoredCandidate::new(tag.to_string()), *line))
|
||||
.collect();
|
||||
|
||||
for (cand,_) in candidates.iter_mut() {
|
||||
cand.fuzzy_score(topic);
|
||||
}
|
||||
|
||||
candidates.retain(|(c,_)| c.score.unwrap_or(i32::MIN) > i32::MIN);
|
||||
candidates.sort_by_key(|(c,_)| c.score.unwrap_or(i32::MIN));
|
||||
|
||||
candidates.first().map(|(c,line)| (c.content.clone(), *line))
|
||||
}
|
||||
|
||||
pub fn read_tags(raw: &str) -> Vec<(String, usize)> {
|
||||
let mut tags = vec![];
|
||||
|
||||
for (line_num, line) in raw.lines().enumerate() {
|
||||
let mut rest = line;
|
||||
|
||||
while let Some(pos) = rest.find(TAG_SEQ) {
|
||||
let after_seq = &rest[pos + TAG_SEQ.len()..];
|
||||
if let Some(end) = after_seq.find(RESET_SEQ) {
|
||||
let tag = &after_seq[..end];
|
||||
tags.push((tag.to_string(), line_num + 1));
|
||||
rest = &after_seq[end + RESET_SEQ.len()..];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags
|
||||
}
|
||||
|
||||
pub fn expand_help(raw: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut chars = raw.chars();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
markers::RESET => result.push_str(RESET_SEQ),
|
||||
markers::TAG => result.push_str(TAG_SEQ),
|
||||
markers::REFERENCE => result.push_str(REF_SEQ),
|
||||
markers::HEADER => result.push_str(HEADER_SEQ),
|
||||
markers::CODE => result.push_str(CODE_SEQ),
|
||||
markers::KEYWORD_2 => result.push_str(KEYWORD_2_SEQ),
|
||||
markers::KEYWORD_3 => result.push_str(KEYWORD_3_SEQ),
|
||||
_ => result.push(ch),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn unescape_help(raw: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut chars = raw.chars().peekable();
|
||||
let mut qt_state = QuoteState::default();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
if let Some(next_ch) = chars.next() {
|
||||
result.push(next_ch);
|
||||
}
|
||||
}
|
||||
'\n' => {
|
||||
result.push(ch);
|
||||
qt_state = QuoteState::default();
|
||||
}
|
||||
'"' => {
|
||||
result.push(ch);
|
||||
qt_state.toggle_double();
|
||||
}
|
||||
'\'' => {
|
||||
result.push(ch);
|
||||
qt_state.toggle_single();
|
||||
}
|
||||
_ if qt_state.in_quote() || chars.peek().is_none_or(|ch| ch.is_whitespace()) => {
|
||||
result.push(ch);
|
||||
}
|
||||
'*' => {
|
||||
result.push(markers::TAG);
|
||||
while let Some(next_ch) = chars.next() {
|
||||
if next_ch == '*' {
|
||||
result.push(markers::RESET);
|
||||
break;
|
||||
} else {
|
||||
result.push(next_ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
'|' => {
|
||||
result.push(markers::REFERENCE);
|
||||
while let Some(next_ch) = chars.next() {
|
||||
if next_ch == '|' {
|
||||
result.push(markers::RESET);
|
||||
break;
|
||||
} else {
|
||||
result.push(next_ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
'#' => {
|
||||
result.push(markers::HEADER);
|
||||
while let Some(next_ch) = chars.next() {
|
||||
if next_ch == '#' {
|
||||
result.push(markers::RESET);
|
||||
break;
|
||||
} else {
|
||||
result.push(next_ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
'`' => {
|
||||
result.push(markers::CODE);
|
||||
while let Some(next_ch) = chars.next() {
|
||||
if next_ch == '`' {
|
||||
result.push(markers::RESET);
|
||||
break;
|
||||
} else {
|
||||
result.push(next_ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
'{' => {
|
||||
result.push(markers::KEYWORD_2);
|
||||
while let Some(next_ch) = chars.next() {
|
||||
if next_ch == '}' {
|
||||
result.push(markers::RESET);
|
||||
break;
|
||||
} else {
|
||||
result.push(next_ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
'[' => {
|
||||
result.push(markers::KEYWORD_3);
|
||||
while let Some(next_ch) = chars.next() {
|
||||
if next_ch == ']' {
|
||||
result.push(markers::RESET);
|
||||
break;
|
||||
} else {
|
||||
result.push(next_ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => result.push(ch),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -25,13 +25,14 @@ pub mod source;
|
||||
pub mod test; // [[ ]] thing
|
||||
pub mod trap;
|
||||
pub mod varcmds;
|
||||
pub mod help;
|
||||
|
||||
pub const BUILTINS: [&str; 50] = [
|
||||
pub const BUILTINS: [&str; 51] = [
|
||||
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
|
||||
"disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
|
||||
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
||||
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
|
||||
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek",
|
||||
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek", "help",
|
||||
];
|
||||
|
||||
pub fn true_builtin() -> ShResult<()> {
|
||||
|
||||
@@ -479,21 +479,28 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
||||
let ch = chars.next().unwrap();
|
||||
username.push(ch);
|
||||
}
|
||||
|
||||
let home = if username.is_empty() {
|
||||
// standard '~' expansion
|
||||
env::var("HOME").unwrap_or_default()
|
||||
}
|
||||
else if let Ok(result) = User::from_name(&username)
|
||||
&& let Some(user) = result {
|
||||
// username expansion like '~user'
|
||||
user.dir.to_string_lossy().to_string()
|
||||
}
|
||||
else if let Ok(id) = username.parse::<u32>()
|
||||
&& let Ok(result) = User::from_uid(Uid::from_raw(id))
|
||||
&& let Some(user) = result {
|
||||
// uid expansion like '~1000'
|
||||
// shed only feature btw B)
|
||||
user.dir.to_string_lossy().to_string()
|
||||
}
|
||||
else {
|
||||
// no match, use literal
|
||||
format!("~{username}")
|
||||
};
|
||||
|
||||
result.push_str(&home);
|
||||
}
|
||||
markers::PROC_SUB_OUT => {
|
||||
@@ -1399,6 +1406,7 @@ pub fn unescape_str(raw: &str) -> String {
|
||||
/// Like unescape_str but for heredoc bodies. Only processes:
|
||||
/// - $var / ${var} / $(cmd) substitution markers
|
||||
/// - Backslash escapes (only before $, `, \, and newline)
|
||||
///
|
||||
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
|
||||
pub fn unescape_heredoc(raw: &str) -> String {
|
||||
let mut chars = raw.chars().peekable();
|
||||
@@ -1576,6 +1584,10 @@ pub fn unescape_math(raw: &str) -> String {
|
||||
#[derive(Debug)]
|
||||
pub enum ParamExp {
|
||||
Len, // #var_name
|
||||
ToUpperFirst, // ^var_name
|
||||
ToUpperAll, // ^^var_name
|
||||
ToLowerFirst, // ,var_name
|
||||
ToLowerAll, // ,,var_name
|
||||
DefaultUnsetOrNull(String), // :-
|
||||
DefaultUnset(String), // -
|
||||
SetDefaultUnsetOrNull(String), // :=
|
||||
@@ -1611,6 +1623,11 @@ impl FromStr for ParamExp {
|
||||
))
|
||||
};
|
||||
|
||||
if s == "^^" { return Ok(ToUpperAll) }
|
||||
if s == "^" { return Ok(ToUpperFirst) }
|
||||
if s == ",," { return Ok(ToLowerAll) }
|
||||
if s == "," { return Ok(ToLowerFirst) }
|
||||
|
||||
// Handle indirect var expansion: ${!var}
|
||||
if let Some(var) = s.strip_prefix('!') {
|
||||
if var.ends_with('*') || var.ends_with('@') {
|
||||
@@ -1716,7 +1733,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'!' | '#' | '%' | ':' | '-' | '+' | '=' | '/' | '?' => {
|
||||
'!' | '#' | '%' | ':' | '-' | '+' | '^' | ',' | '=' | '/' | '?' => {
|
||||
rest.push(ch);
|
||||
rest.push_str(&chars.collect::<String>());
|
||||
break;
|
||||
@@ -1728,6 +1745,32 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
||||
if let Ok(expansion) = rest.parse::<ParamExp>() {
|
||||
match expansion {
|
||||
ParamExp::Len => unreachable!(),
|
||||
ParamExp::ToUpperAll => {
|
||||
let value = vars.get_var(&var_name);
|
||||
Ok(value.to_uppercase())
|
||||
}
|
||||
ParamExp::ToUpperFirst => {
|
||||
let value = vars.get_var(&var_name);
|
||||
let mut chars = value.chars();
|
||||
let first = chars.next()
|
||||
.map(|c| c.to_uppercase()
|
||||
.to_string())
|
||||
.unwrap_or_default();
|
||||
Ok(first + chars.as_str())
|
||||
|
||||
}
|
||||
ParamExp::ToLowerAll => {
|
||||
let value = vars.get_var(&var_name);
|
||||
Ok(value.to_lowercase())
|
||||
}
|
||||
ParamExp::ToLowerFirst => {
|
||||
let value = vars.get_var(&var_name);
|
||||
let mut chars = value.chars();
|
||||
let first = chars.next()
|
||||
.map(|c| c.to_lowercase().to_string())
|
||||
.unwrap_or_default();
|
||||
Ok(first + chars.as_str())
|
||||
}
|
||||
ParamExp::DefaultUnsetOrNull(default) => {
|
||||
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
|
||||
Some(val) => Ok(val),
|
||||
|
||||
@@ -8,29 +8,7 @@ use ariadne::Fmt;
|
||||
|
||||
use crate::{
|
||||
builtin::{
|
||||
alias::{alias, unalias},
|
||||
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
|
||||
autocmd::autocmd,
|
||||
cd::cd,
|
||||
complete::{compgen_builtin, complete_builtin},
|
||||
dirstack::{dirs, popd, pushd},
|
||||
echo::echo,
|
||||
eval, exec,
|
||||
flowctl::flowctl,
|
||||
getopts::getopts,
|
||||
intro,
|
||||
jobctl::{self, JobBehavior, continue_job, disown, jobs},
|
||||
keymap, map,
|
||||
pwd::pwd,
|
||||
read::{self, read_builtin},
|
||||
resource::{ulimit, umask_builtin},
|
||||
seek::seek,
|
||||
shift::shift,
|
||||
shopt::shopt,
|
||||
source::source,
|
||||
test::double_bracket_test,
|
||||
trap::{TrapTarget, trap},
|
||||
varcmds::{export, local, readonly, unset},
|
||||
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, help::help, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, seek::seek, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}
|
||||
},
|
||||
expand::{expand_aliases, expand_case_pattern, glob_to_regex},
|
||||
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
||||
@@ -1017,6 +995,7 @@ impl Dispatcher {
|
||||
"ulimit" => ulimit(cmd),
|
||||
"umask" => umask_builtin(cmd),
|
||||
"seek" => seek(cmd),
|
||||
"help" => help(cmd),
|
||||
"true" | ":" => {
|
||||
state::set_status(0);
|
||||
Ok(())
|
||||
|
||||
@@ -88,7 +88,6 @@ impl ParsedSrc {
|
||||
Err(error) => return Err(vec![error]),
|
||||
}
|
||||
}
|
||||
log::trace!("Tokens: {:#?}", tokens);
|
||||
|
||||
let mut errors = vec![];
|
||||
let mut nodes = vec![];
|
||||
@@ -1038,7 +1037,6 @@ impl ParseStream {
|
||||
Ok(Some(node))
|
||||
}
|
||||
fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult<Option<Node>> {
|
||||
log::debug!("Trying to parse a brace group");
|
||||
let mut node_tks: Vec<Tk> = vec![];
|
||||
let mut body: Vec<Node> = vec![];
|
||||
let mut redirs: Vec<Redir> = vec![];
|
||||
@@ -1051,7 +1049,6 @@ impl ParseStream {
|
||||
self.catch_separator(&mut node_tks);
|
||||
|
||||
loop {
|
||||
log::debug!("Parsing a brace group body");
|
||||
if *self.next_tk_class() == TkRule::BraceGrpEnd {
|
||||
node_tks.push(self.next_tk().unwrap());
|
||||
break;
|
||||
@@ -1078,7 +1075,6 @@ impl ParseStream {
|
||||
}
|
||||
self.catch_separator(&mut node_tks);
|
||||
if !self.next_tk_is_some() {
|
||||
log::debug!("Hit end of input while parsing a brace group body, entering panic mode");
|
||||
self.panic_mode(&mut node_tks);
|
||||
return Err(parse_err_full(
|
||||
"Expected a closing brace for this brace group",
|
||||
@@ -1088,15 +1084,11 @@ impl ParseStream {
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Finished parsing brace group body, now looking for redirections if it's not a function definition"
|
||||
);
|
||||
|
||||
if !from_func_def {
|
||||
self.parse_redir(&mut redirs, &mut node_tks)?;
|
||||
}
|
||||
|
||||
log::debug!("Finished parsing brace group redirections, constructing node");
|
||||
|
||||
let node = Node {
|
||||
class: NdRule::BraceGrp { body },
|
||||
|
||||
@@ -115,6 +115,11 @@ impl IoMode {
|
||||
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
|
||||
Ok(Self::Buffer { tgt_fd, buf, flags })
|
||||
}
|
||||
pub fn loaded_pipe(tgt_fd: RawFd, buf: &[u8]) -> ShResult<Self> {
|
||||
let (rpipe, wpipe) = nix::unistd::pipe()?;
|
||||
write(wpipe, buf)?;
|
||||
Ok(Self::Pipe { tgt_fd, pipe: rpipe.into() })
|
||||
}
|
||||
pub fn get_pipes() -> (Self, Self) {
|
||||
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
|
||||
(
|
||||
|
||||
@@ -739,7 +739,7 @@ impl QueryEditor {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct FuzzySelector {
|
||||
query: QueryEditor,
|
||||
filtered: Vec<ScoredCandidate>,
|
||||
|
||||
@@ -203,6 +203,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default,Clone,Debug)]
|
||||
pub struct History {
|
||||
path: PathBuf,
|
||||
pub pending: Option<LineBuf>, // command, cursor_pos
|
||||
@@ -214,6 +215,7 @@ pub struct History {
|
||||
//search_direction: Direction,
|
||||
ignore_dups: bool,
|
||||
max_size: Option<u32>,
|
||||
stateless: bool
|
||||
}
|
||||
|
||||
impl History {
|
||||
@@ -229,6 +231,7 @@ impl History {
|
||||
//search_direction: Direction::Backward,
|
||||
ignore_dups: false,
|
||||
max_size: None,
|
||||
stateless: true,
|
||||
}
|
||||
}
|
||||
pub fn new() -> ShResult<Self> {
|
||||
@@ -266,6 +269,7 @@ impl History {
|
||||
//search_direction: Direction::Backward,
|
||||
ignore_dups,
|
||||
max_size,
|
||||
stateless: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -450,6 +454,9 @@ impl History {
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> ShResult<()> {
|
||||
if self.stateless {
|
||||
return Ok(());
|
||||
}
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
|
||||
@@ -421,7 +421,7 @@ impl IndentCtx {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LineBuf {
|
||||
pub buffer: String,
|
||||
pub hint: Option<String>,
|
||||
@@ -440,6 +440,28 @@ pub struct LineBuf {
|
||||
pub redo_stack: Vec<Edit>,
|
||||
}
|
||||
|
||||
impl Default for LineBuf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer: String::new(),
|
||||
hint: None,
|
||||
grapheme_indices: Some(vec![]),
|
||||
cursor: ClampedUsize::new(0, 0, false),
|
||||
|
||||
select_mode: None,
|
||||
select_range: None,
|
||||
last_selection: None,
|
||||
|
||||
insert_mode_start_pos: None,
|
||||
saved_col: None,
|
||||
indent_ctx: IndentCtx::new(),
|
||||
|
||||
undo_stack: vec![],
|
||||
redo_stack: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LineBuf {
|
||||
pub fn new() -> Self {
|
||||
let mut new = Self {
|
||||
|
||||
@@ -132,6 +132,18 @@ pub mod markers {
|
||||
pub fn is_marker(c: Marker) -> bool {
|
||||
('\u{e000}'..'\u{efff}').contains(&c)
|
||||
}
|
||||
|
||||
// Help command formatting markers
|
||||
pub const TAG: Marker = '\u{e180}';
|
||||
pub const REFERENCE: Marker = '\u{e181}';
|
||||
pub const HEADER: Marker = '\u{e182}';
|
||||
pub const CODE: Marker = '\u{e183}';
|
||||
/// angle brackets
|
||||
pub const KEYWORD_1: Marker = '\u{e184}';
|
||||
/// curly brackets
|
||||
pub const KEYWORD_2: Marker = '\u{e185}';
|
||||
/// square brackets
|
||||
pub const KEYWORD_3: Marker = '\u{e186}';
|
||||
}
|
||||
type Marker = char;
|
||||
|
||||
@@ -256,6 +268,7 @@ pub struct ShedVi {
|
||||
|
||||
pub old_layout: Option<Layout>,
|
||||
pub history: History,
|
||||
pub ex_history: History,
|
||||
|
||||
pub needs_redraw: bool,
|
||||
}
|
||||
@@ -277,6 +290,7 @@ impl ShedVi {
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new(),
|
||||
history: History::new()?,
|
||||
ex_history: History::empty(),
|
||||
needs_redraw: true,
|
||||
};
|
||||
write_vars(|v| {
|
||||
@@ -308,6 +322,7 @@ impl ShedVi {
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new(),
|
||||
history: History::empty(),
|
||||
ex_history: History::empty(),
|
||||
needs_redraw: true,
|
||||
};
|
||||
write_vars(|v| {
|
||||
@@ -798,7 +813,8 @@ impl ShedVi {
|
||||
|
||||
let Ok(cmd) = self.mode.handle_key_fallible(key) else {
|
||||
// it's an ex mode error
|
||||
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
|
||||
self.swap_mode(&mut (Box::new(ViNormal::new()) as Box<dyn ViMode>));
|
||||
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -844,9 +860,22 @@ impl ShedVi {
|
||||
}
|
||||
|
||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
||||
let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
|
||||
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
|
||||
log::debug!("is_ex_cmd: {is_ex_cmd}");
|
||||
if is_shell_cmd {
|
||||
self.old_layout = None;
|
||||
}
|
||||
if is_ex_cmd {
|
||||
self.ex_history.push(cmd.raw_seq.clone());
|
||||
self.ex_history.reset();
|
||||
log::debug!("ex_history: {:?}", self.ex_history.entries());
|
||||
}
|
||||
|
||||
let before = self.editor.buffer.clone();
|
||||
|
||||
self.exec_cmd(cmd, false)?;
|
||||
|
||||
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
|
||||
for key in keys {
|
||||
self.handle_key(key)?;
|
||||
@@ -1131,7 +1160,7 @@ impl ShedVi {
|
||||
)
|
||||
}
|
||||
|
||||
Verb::ExMode => Box::new(ViEx::new()),
|
||||
Verb::ExMode => Box::new(ViEx::new(self.ex_history.clone())),
|
||||
|
||||
Verb::VerbatimMode => {
|
||||
self.reader.verbatim_single = true;
|
||||
@@ -1221,7 +1250,7 @@ impl ShedVi {
|
||||
ModeReport::Normal => Box::new(ViNormal::new()),
|
||||
ModeReport::Insert => Box::new(ViInsert::new()),
|
||||
ModeReport::Visual => Box::new(ViVisual::new()),
|
||||
ModeReport::Ex => Box::new(ViEx::new()),
|
||||
ModeReport::Ex => Box::new(ViEx::new(self.ex_history.clone())),
|
||||
ModeReport::Replace => Box::new(ViReplace::new()),
|
||||
ModeReport::Verbatim => Box::new(ViVerbatim::new()),
|
||||
ModeReport::Unknown => unreachable!(),
|
||||
@@ -1266,7 +1295,7 @@ impl ShedVi {
|
||||
ModeReport::Normal => Box::new(ViNormal::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Insert => Box::new(ViInsert::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Visual => Box::new(ViVisual::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Ex => Box::new(ViEx::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Ex => Box::new(ViEx::new(self.ex_history.clone())) as Box<dyn ViMode>,
|
||||
ModeReport::Replace => Box::new(ViReplace::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Unknown => unreachable!(),
|
||||
|
||||
@@ -64,6 +64,7 @@ bitflags! {
|
||||
const VISUAL_LINE = 1<<1;
|
||||
const VISUAL_BLOCK = 1<<2;
|
||||
const EXIT_CUR_MODE = 1<<3;
|
||||
const IS_EX_CMD = 1<<4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use itertools::Itertools;
|
||||
|
||||
use crate::bitflags;
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
use crate::readline::history::History;
|
||||
use crate::readline::keys::KeyEvent;
|
||||
use crate::readline::linebuf::LineBuf;
|
||||
use crate::readline::vicmd::{
|
||||
@@ -33,16 +34,64 @@ bitflags! {
|
||||
struct ExEditor {
|
||||
buf: LineBuf,
|
||||
mode: ViInsert,
|
||||
history: History
|
||||
}
|
||||
|
||||
impl ExEditor {
|
||||
pub fn new(history: History) -> Self {
|
||||
let mut new = Self {
|
||||
history,
|
||||
..Default::default()
|
||||
};
|
||||
new.buf.update_graphemes();
|
||||
new
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
*self = Self::default()
|
||||
}
|
||||
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
|
||||
cmd.verb().is_none()
|
||||
&& (cmd
|
||||
.motion()
|
||||
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise)))
|
||||
&& self.buf.start_of_line() == 0)
|
||||
|| (cmd
|
||||
.motion()
|
||||
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
|
||||
&& self.buf.end_of_line() == self.buf.cursor_max())
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
let count = match motion {
|
||||
Motion::LineUpCharwise => -(*count as isize),
|
||||
Motion::LineDownCharwise => *count as isize,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let entry = self.history.scroll(count);
|
||||
if let Some(entry) = entry {
|
||||
let buf = std::mem::take(&mut self.buf);
|
||||
self.buf.set_buffer(entry.command().to_string());
|
||||
if self.history.pending.is_none() {
|
||||
self.history.pending = Some(buf);
|
||||
}
|
||||
self.buf.set_hint(None);
|
||||
self.buf.move_cursor_to_end();
|
||||
} else if let Some(pending) = self.history.pending.take() {
|
||||
self.buf = pending;
|
||||
}
|
||||
}
|
||||
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> {
|
||||
let Some(cmd) = self.mode.handle_key(key) else {
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
return Ok(());
|
||||
};
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
log::debug!("ExEditor got cmd: {:?}", cmd);
|
||||
if self.should_grab_history(&cmd) {
|
||||
log::debug!("Grabbing history for cmd: {:?}", cmd);
|
||||
self.scroll_history(cmd);
|
||||
return Ok(())
|
||||
}
|
||||
self.buf.exec_cmd(cmd)
|
||||
}
|
||||
}
|
||||
@@ -53,8 +102,8 @@ pub struct ViEx {
|
||||
}
|
||||
|
||||
impl ViEx {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
pub fn new(history: History) -> Self {
|
||||
Self { pending_cmd: ExEditor::new(history) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,18 +111,14 @@ impl ViMode for ViEx {
|
||||
// Ex mode can return errors, so we use this fallible method instead of the normal one
|
||||
fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> {
|
||||
use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M};
|
||||
log::debug!("[ViEx] handle_key_fallible: key={:?}", key);
|
||||
match key {
|
||||
E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => {
|
||||
let input = self.pending_cmd.buf.as_str();
|
||||
log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input);
|
||||
match parse_ex_cmd(input) {
|
||||
Ok(cmd) => {
|
||||
log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd);
|
||||
Ok(cmd)
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e);
|
||||
let msg = e.unwrap_or(format!("Not an editor command: {}", input));
|
||||
write_meta(|m| m.post_system_message(msg.clone()));
|
||||
Err(ShErr::simple(ShErrKind::ParseErr, msg))
|
||||
@@ -81,12 +126,10 @@ impl ViMode for ViEx {
|
||||
}
|
||||
}
|
||||
E(C::Char('C'), M::CTRL) => {
|
||||
log::debug!("[ViEx] Ctrl-C, clearing");
|
||||
self.pending_cmd.clear();
|
||||
Ok(None)
|
||||
}
|
||||
E(C::Esc, M::NONE) => {
|
||||
log::debug!("[ViEx] Esc, returning to normal mode");
|
||||
Ok(Some(ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: Some(VerbCmd(1, Verb::NormalMode)),
|
||||
@@ -96,14 +139,12 @@ impl ViMode for ViEx {
|
||||
}))
|
||||
}
|
||||
_ => {
|
||||
log::debug!("[ViEx] forwarding key to ExEditor");
|
||||
self.pending_cmd.handle_key(key).map(|_| None)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> {
|
||||
let result = self.handle_key_fallible(key);
|
||||
log::debug!("[ViEx] handle_key result: {:?}", result);
|
||||
result.ok().flatten()
|
||||
}
|
||||
fn is_repeatable(&self) -> bool {
|
||||
@@ -177,7 +218,7 @@ fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>, Option<String>> {
|
||||
verb,
|
||||
motion,
|
||||
raw_seq: raw.to_string(),
|
||||
flags: CmdFlags::EXIT_CUR_MODE,
|
||||
flags: CmdFlags::EXIT_CUR_MODE | CmdFlags::IS_EX_CMD,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -224,6 +265,10 @@ fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Opt
|
||||
let cmd = unescape_shell_cmd(&cmd);
|
||||
Ok(Some(Verb::ShellCmd(cmd)))
|
||||
}
|
||||
_ if "help".starts_with(&cmd_name) => {
|
||||
let cmd = "help ".to_string() + chars.collect::<String>().trim();
|
||||
Ok(Some(Verb::ShellCmd(cmd)))
|
||||
}
|
||||
"normal!" => parse_normal(chars),
|
||||
_ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)),
|
||||
_ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)),
|
||||
|
||||
@@ -1098,6 +1098,8 @@ impl VarTab {
|
||||
.map(|hname| hname.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let help_paths = format!("/usr/share/shed/doc:{home}/.local/share/shed/doc");
|
||||
|
||||
unsafe {
|
||||
env::set_var("IFS", " \t\n");
|
||||
env::set_var("HOST", hostname.clone());
|
||||
@@ -1114,6 +1116,7 @@ impl VarTab {
|
||||
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
|
||||
env::set_var("SHED_HIST", format!("{}/.shedhist", home));
|
||||
env::set_var("SHED_RC", format!("{}/.shedrc", home));
|
||||
env::set_var("SHED_HPATH", help_paths);
|
||||
}
|
||||
}
|
||||
pub fn init_sh_argv(&mut self) {
|
||||
|
||||
Reference in New Issue
Block a user