Compare commits

..

79 Commits

Author SHA1 Message Date
David Adam
8dbbe71bc6 disable Linux development builds for now
I'll add the rest of the infrastructure later.
2026-04-13 21:12:51 +08:00
David Adam
d9d9eced98 workflow: build development builds on master branch 2026-04-12 21:11:26 +08:00
David Adam
64a829f0df add workflow to create development build source packages 2026-04-12 18:50:56 +08:00
Nahor
440e7fcbc1 Fix failing system tests on Cygwin
The main changes are:
- disabling some checks related to POSIX file permissions when a filesystem is
mounted with "noacl" (default on MSYS2)
- disabling some checks related to symlinks when using fake ones (file copy)

Windows with acl hasn't been tested because 1) Cygwin itself does not have any
Rust package yet to compile fish, and 2) MSYS2 defaults to `noacl`

Part of #12171
2026-04-11 18:55:00 +08:00
Nahor
52495c8124 tests: make realpath tests easier to debug
- Use the different strings for different checks to more easily narrow down
where a failure happens
- Move CHECK comments outside a `if...else...end` to avoid giving the impression
that the check only runs in the `if` case.

Part of #12171
2026-04-11 18:44:22 +08:00
Vishrut Sachan
467b03d715 git completions: prioritize recent commits for rebase --interactive
Fixes #12537

Closes #12619
2026-04-11 18:33:05 +08:00
Johannes Altmanninger
b5c40478f6 Fix typo, closes #12586, closes #12577 2026-04-11 18:33:05 +08:00
Johannes Altmanninger
1286745e78 Remove bits for async-signal-safety of old SIGTERM handler
This is implied by the parent commit.

To enable this, stop trying to run cleanup in panic handlers if we
panic on a background thread.
2026-04-11 18:25:26 +08:00
Yakov Till
b99ae291d6 Save history on SIGTERM and SIGHUP before exit
Previously, SIGTERM immediately re-raised with SIG_DFL, killing
fish without saving history. SIGHUP deferred via a flag but never
re-raised, so the parent saw a normal exit instead of signal death.

Unify both signals: the handler stores the signal number in a single
AtomicI32, the reader loop exits normally, throwing_main() saves
history and re-raises with SIG_DFL so the parent sees WIFSIGNALED.

Fixes #10300

Closes #12615
2026-04-11 18:06:02 +08:00
Daniel Rainer
8ae71c80f4 refactor: extract string escape and unescape funcs
Move the functions for escaping and unescaping strings from
`src/common.rs` into `fish_common`. It might make sense to move them
into a dedicated crate at some point, but for now just move them to the
preexisting crate to unblock other extraction.

Closes #12625
2026-04-11 17:49:50 +08:00
Daniel Rainer
cf6170200c refactor: move const to fish_widestring
Another step to eliminate dependency cycles between `src/expand.rs` and
`src/common.rs`.

Part of #12625
2026-04-11 17:49:49 +08:00
Daniel Rainer
c13038b968 refactor: move move char consts to widestring
This time, move char constants from `src/expand.rs` to
`fish_widestring`, which resolves a dependency cycle between
`src/expand.rs` and `src/common.rs`.

Part of #12625
2026-04-11 17:49:48 +08:00
Daniel Rainer
816077281d refactor: move encoding functions to widestring
The decoding functions for our widestrings are already in the
`fish_widestring` crate, so by symmetry, it makes sense to put the
encoding functions there as well. This also makes it easier to depend on
these functions, giving more options when it comes to further code
extraction.

Part of #12625
2026-04-11 17:49:47 +08:00
Daniel Rainer
78ea24a262 refactor: move char definitions into widestring
Use `fish_widestring` as the place where char definitions live. This has
the advantage that all our code can depend on `fish_widestring` without
introducing dependency cycles. Having a common place for character
definitions also makes it easier to see which chars have a special
meaning assigned to them.

This change also unblocks some follow-up refactoring by removing a
dependency cycle between `src/common.rs` and `src/wildcard.rs`.

Part of #12625
2026-04-11 17:49:46 +08:00
Daniel Rainer
6a5b9bcde1 refactor: move PUA-decoding function to widestring
These functions don't depend on `wcstringutil` functionality, so there
is no need for them to be there. The advantage of putting them into our
`widestring` crate is that quite a lot of code depends on it, and
extracting some of that code would result in crate dependency cycles if
the functions stayed in the `wcstringutil` crate. Our `widestring` crate
does not depend on any of our other crates, so there won't be any cyclic
dependency issues with code in it.

Part of #12625
2026-04-11 17:49:45 +08:00
Daniel Rainer
b7b786aabf cleanup: move escape_string_with_quote
It makes a lot more sense to have this function in the same module as
the other escaping functions. There was no usage of this function in
`parse_util` except for the test, so it makes little sense to keep the
function there. Moving it also eliminates a pointless cyclic dependency
between `common` and `parse_util`.

Part of #12625
2026-04-11 17:49:43 +08:00
Daniel Rainer
dc63b7bb20 cleanup: don't export write_loop twice
Exporting it as both `safe_write_loop` and `write_loop` is redundant and
causes inconsistencies. Remove the `pub use` and use `write_loop` for
the function name. It is shorter, and in Rust the default assumption is
that code is safe unless otherwise indicated, so there is no need to be
explicit about it.

Part of #12625
2026-04-11 17:49:42 +08:00
Daniel Rainer
9c819c020e refactor: don't reexport fish_common in common
Not reexporting means that imports have to change to directly import
from `fish_common`. This makes it easier to see which dependencies on
`src/common.rs` actually remain, which helps with identifying candidates
for extraction.

While at it, group some imports.

Part of #12625
2026-04-11 17:49:41 +08:00
Daniel Rainer
dc9b1141c8 refactor: extract fish_reserved_codepoint
Part of #12625
2026-04-11 17:49:40 +08:00
Daniel Rainer
3d364478ee refactor: remove dep on key mod from common
Removing this dependency allows extracting the `fish_reserved_codepoint`
function, and other code depending on it in subsequent commits.

Part of #12625
2026-04-11 17:49:39 +08:00
Daniel Rainer
faf331fdad refactor: use macro for special key char def
Reduce verbosity of const definitions. Define a dedicated const for the
base of the special key encoding range. This range is 256 bytes wide, so
by defining consts via an u8 offset from the base, we can guarantee that
the consts fall into the allocated range. Ideally, we would also check
for collisions, but Rust's const capabilities don't allow for that as
far as I'm aware.

Having `SPECIAL_KEY_ENCODE_BASE` in the `rust-widestring` crate allows
getting rid of the dependency on `key::Backspace` in the
`fish_reserved_codepoint` function, which unblocks code extraction.

Part of #12625
2026-04-11 17:49:37 +08:00
Daniel Rainer
47b6c0aec2 cleanup: rename and document UTF-8 decoding function
While the function is only used to decode single codepoints, nothing in
its implementation limits it to single codepoints, so the name
`decode_one_codepoint_utf8` is misleading. Change it to the simpler and
more accurate `decode_utf8`. Add a doc comment to describe the
function's behavior.

Part of #12625
2026-04-11 17:49:36 +08:00
Daniel Rainer
eb478bfc3e refactor: remove syntactic deps on main crate
Part of #12625
2026-04-11 17:49:35 +08:00
Johannes Altmanninger
63cf79f5f6 Reuse function for creating sighupint topic monitor 2026-04-11 17:27:25 +08:00
Johannes Altmanninger
ff284d642e tests/checks/tmux-abbr.fish: fix on BusyBox less (alpine CI) 2026-04-11 17:03:46 +08:00
Johannes Altmanninger
cc64da62a9 fish_color_valid_path: also apply bg and underline colors
Closes #12622
2026-04-11 17:03:46 +08:00
Johannes Altmanninger
a974fe990f fish_color_valid_path: respect explicit normal foreground 2026-04-11 17:03:46 +08:00
David Adam
39239724ec make_vendor_tarball: drop unused tar search 2026-04-10 05:56:00 +08:00
Daniel Rainer
524a7bac6e l10n: restore translations from error rewrite
Most translations were adjusted correctly, but a few were missed, so
restore them here.

Closes #12623
2026-04-09 01:36:53 +08:00
Nahor
d649c2aab4 string: remove StringError::NotANumber
Use the more generic `StringError::InvalidArgs` instead

Closes #12556
2026-04-08 14:11:31 +08:00
Nahor
30e6aa85e2 error rewrite: use new Error to report errors
To homogenize error reporting format, use a new Error struct. Currently this
is used for builtins and ensuring a common cmd/subcmd prefix.

Part of #12556
2026-04-08 14:11:31 +08:00
Nahor
abd7442521 printf: de-duplicate code
Part of #12556
2026-04-08 14:05:05 +08:00
Johannes Altmanninger
f5c48038b5 Skip tmux-abbr's "pipe builtin into less" test on BusyBox less
Fails on Alpine CI.
2026-04-08 14:04:15 +08:00
Daniel Rainer
895a6e7034 l10n(fr): use non-breaking space before colon
Closes #12617
2026-04-08 13:54:04 +08:00
Johannes Altmanninger
2193e88423 Pass job group down to builtin_print_help
Repro:

	$ abbr -h | less
	fish: Job 1, 'abbr --help | less' has stopped

Bisects to 5ece9bec6c (__fish_print_help: use man as-is, 2025-08-29).

Root cause is similar to
https://github.com/fish-shell/fish-shell/issues/8699#issuecomment-1030762748
except it's specific to how __fish_print_help is invoked, since
"__fish_print_help abbr | less" works fine.

Fix this by letting __fish_print_help access the terminal.

Fixes #12612

Solved by Claude in a single prompt:

---

 ▐▛███▜▌   Claude Code v2.1.92
▝▜█████▛▘  Sonnet 4.6
  ▘▘ ▝▝    ~/git/fish-shell

❯ "abbr -h | less" gets a signal, resulting in the message "fish: Job 1, 'abbr -h | less' has stopped"
whereas "__fish_print_help abbr | less" works just fine, even though "abbr -h" should run exactly that.
Why this difference?

[...]

● Now I can give you a clear answer.

  ---
  The difference comes down to job group inheritance when man is
  spawned.

  The deferred execution order

  In abbr -h | less, fish uses the "deferred process" optimization:
  since abbr is a non-external process piping into an external one,
  fish launches less first,
   transfers the terminal to less's process group, then runs the
   deferred abbr builtin. By the time abbr's help function spawns man,
   the terminal already
  belongs to less's pgroup.

  Same thing happens with __fish_print_help abbr | less — the outer
  less is launched first and gets the terminal.

  Where the paths diverge

  Function case (__fish_print_help abbr | less):
  get_performer_for_function (exec.rs:1050) captures job.group.clone()
  and passes it to eval_node. So when command man runs inside the
  function, it's given the outer pipeline's job group — the same
  one that owns the terminal. man's inner less pager joins that group
  and can read from the terminal freely. ✓

  Builtin case (abbr -h | less): builtin_print_help calls
  parser.eval(&cmd, streams.io_chain) (shared.rs:652). parser.eval
  is defined as:

  pub fn eval(&self, cmd: &wstr, io: &IoChain) -> EvalRes {
      self.eval_with(cmd, io, None, BlockType::top, false)
      //                      ^^^^ job_group = None
  }

  job_group = None means man creates a brand-new job group with no
  terminal access. When man spawns its less pager, the pager tries
  to read keyboard input from
   the terminal, but the terminal belongs to the outer less's process
   group. The pager receives SIGTTIN → stops → the whole job is
   reported as stopped.

  The fix

  builtin_print_help should pass streams.job_group to the eval call
  (which is already populated at exec.rs:1192). Changing line 652 from:

  let res = parser.eval(&cmd, streams.io_chain);

  to:

  let res = parser.eval_with(&cmd, streams.io_chain, streams.job_group.as_ref(), BlockType::top, false);

  would give man the outer job's job group, letting its pager access
  the terminal just like the function case.

✻ Sautéed for 13m 0s
2026-04-07 20:21:38 +08:00
Kayce Basques
86c3778c2a docs: Format path as inline code
Closes #12609
2026-04-07 18:52:36 +08:00
Nahor
d2653b7cac tmux-set.fish: fix spurious CI failure
Part of #12556
2026-04-07 17:49:57 +08:00
Johannes Altmanninger
cf16949ce7 contrib/debian/control: remove insufficient mdoc dependency
On Debian, mandoc provides "/usr/bin/mman", not "/usr/bin/man", so that
package alone is not enough.  Users that want to use mandoc could use a
package that "Provides: man", for example by creating symlink to "mman".

See https://github.com/fish-shell/fish-shell/issues/12596#issuecomment-4188332803
2026-04-07 17:49:57 +08:00
Daniel Rainer
85311546de refactor: extract fish-feature-flags crate
Another step in splitting up the main library crate.

Note that this change requires removing the `#[cfg(test)]` annotations
around the `LOCAL_OVERRIDE_STACK` code, because otherwise the code would
be removed in test builds for other packages, making the `#[cfg(test)]`
functions unusable from other packages, and functions with such feature
gates in their body would have the code guarded by these gates removed
in test builds for tests in other packages.

Closes #12494
2026-04-07 17:49:57 +08:00
Daniel Rainer
c44aa32a15 cleanup: remove syntactic dependency on main crate
This is done in preparation for extracting this file into its own crate.

Part of #12494
2026-04-07 17:49:57 +08:00
Daniel Rainer
65bc9b9e3e refactor: stop aliasing feature_test function
Having a public function named `test` is quite unspecific. Exporting it
both as `test` and `feature_test` results in inconsistent usage. Fix
this by renaming the function to `feature_test` and removing the alias.

Part of #12494
2026-04-07 17:49:57 +08:00
Daniel Rainer
8125f78a84 refactor: use override stack for feature tests
Several features of fish can be toggled at runtime (in practice at
startup). To keep track of the active features, `FEATURES`, an array of
`AtomicBool` is used. This can safely be shared across threads without
requiring locks.

Some of our tests override certain features to test behavior with a
specific value of the feature. Prior to this commit, they did this by
using thread-local versions of `FEATURES` instead of the process-wide
version used in non-test builds. This approach has two downsides:
- It does not allow nested overrides.
- It prevents using the code across package boundaries.
The former is a fairly minor issue, since I don't think we need nested
overrides. The latter prevents splitting up our large library crate,
since `#[cfg(test)]`-guarded code can only be used within a single
package.

To resolve these issues, a new approach to feature overrides in
tests is introduced in this commit: Instead of having a thread-local
version of `FEATURES`, all code, whether test or not, uses the
process-wide `FEATURES`. For non-test code, there is no change. For test
code, `FEATURES` is now also used. To override features in tests, a new
`with_overridden_feature` function is added, which replaces
`scoped_test` and `set`. It works by maintaining a thread-local stack of
feature overrides (`LOCAL_OVERRIDE_STACK`). The overridden `FeatureFlag`
and its new value are pushed to the stack, then the code for which the
override should be active is run, and finally the stack is popped again.
Feature tests now have to scan the stack for the first appearance of the
`FeatureFlag`, or use the value in `FEATURES` if the stack does not
contain the `FeatureFlag`. In most cases, the stack will be empty or
contain very few elements, so scanning it should not take long. For now,
it's only active in test code, so non-test code is unaffected. The plan
is to change this when the feature flag code is extracted from the main
library crate. This would slightly slow down feature tests in non-test
code, but there the stack will always be empty, since we only override
features in tests.

Part of #12494
2026-04-07 17:49:57 +08:00
Daniel Rainer
f0f48b4859 cleanup: stop needlessly exporting struct fields
Part of #12494
2026-04-07 17:49:57 +08:00
David Adam
a009f87630 build_tools: add sh script to build linux packages 2026-04-07 10:57:41 +08:00
David Adam
edb66d4d4e remove dput_cf_gen, not actually helpful 2026-04-07 10:57:41 +08:00
Nahor
f3f675b4cc Standardized error messages: constant names
Part of #12556
2026-04-05 13:15:47 +08:00
Nahor
434610494f argparse: fix error status code
To homogenize error reporting format, use a new Error struct. Currently this
is used for builtins and ensuring a common cmd/subcmd prefix.

Part of #12556
2026-04-05 13:15:47 +08:00
Dennis Yildirim
3cce1f3f4c Added completions for git verify-commit and verify-tag
Closes #12607
2026-04-05 13:14:31 +08:00
Johannes Altmanninger
a5bde7954e Update changelog 2026-04-05 13:07:29 +08:00
Nahor
99d63c21f1 Add tests to exercise all builtin error messages
With a few exceptions, only one test is added for a given message, even
when there are multiple ways to trigger the same message (e.g. different
invalid option combinations, or triggered in shared functions such as
`builtin_unknown_option`)

Includes a few very minor fixes, such as missing a newline, or using the
wrong var name.

Closes #12603
2026-04-05 00:22:42 +08:00
Nahor
c3e3658157 ulimit: remove unreachable error message
When there is no limit value, ulimit will have printed the current one
and exited

Part of #12603
2026-04-05 00:22:41 +08:00
Nahor
8d92016e72 string: error messages fixes
- fix wrong pattern used in `string replace` error message
- replace unreachable error with `unreachable!` in `string`
- fix cmd being used in place of subcmd 

Part of #12603
2026-04-05 00:22:41 +08:00
Nahor
f6a72b4e19 status: replace unreachable code with an assert!
Part of #12603
2026-04-05 00:22:40 +08:00
Nahor
2f9c2df10d set: report an error when called with -a,-p and no NAME
Previously executing `set -a` or `set -p` would just list all the
variables, which does not make sense since the user specifically ask
for an action (append/prepend).

Update the help page synopsis

Part of #12603
2026-04-05 00:22:40 +08:00
Johannes Altmanninger
0367aaea7d Disable relocatable tree logic when DATADIR or SYSCONFDIR are non-default
If all of

	$PREFIX/bin/fish
	$PREFIX/share/fish
	$PREFIX/etc/fish

exist, then fish assumes it's in a relocatable directory tree.
This is used by homebrew (PREFIX=/usr/local) and maybe also nix(?).

Other Linux distros prefer to use /etc/fish instead of $PREFIX/etc/fish
[1].  To do so, they need to pass -DCMAKE_INSTALL_SYSCONFDIR=/etc.
The relocatable tree logic assumes default data and sysconf dirs
(relative to a potentially relocatable prefix). If the user changes
any of those, and the relocatable tree logic happens to kick in,
that'd overrule user preference, which is surprising.

So a non-default data or sysconf path is a strong enough signal that
we want to disable the relocatable tree logic. Do that.

Closes #10748

[1]: ff2f69cd56/PKGBUILD (L43)
2026-04-04 01:22:54 +08:00
Johannes Altmanninger
e25b4b6f05 tests/checks/realpath.fish: fix on macOS 2026-04-03 23:26:37 +08:00
Yakov Till
90cbfd288e Fix test_history_path_detection panic: call test_init()
test_history_path_detection calls add_pending_with_file_detection(),
which spawns a thread pool task via ThreadPool::perform(). This
requires threads::init() to have been called, otherwise
assert_is_background_thread() panics.

Add the missing test_init() call, matching other tests that use
subsystems requiring initialization.

Closes #12604
2026-04-03 14:48:53 +08:00
Nahor
68453843d4 set: fix unreachable error messages
- Remove unreachable error message in `handle_env_return()`
While we could have put an empty block in `handle_env_return()` and
removed the condition on `NotFound` in `erase()`, we prefered to use
`unreachable!` in case `handle_env_return()` gets called in new scenarios
in the future

- Make reachable the error message when asking to show a slice

Part of #12603
2026-04-03 13:53:42 +08:00
Nahor
fb57f95391 realpath: fix random error message
With empty argument, `realpath` skips all processing, so the error
message, based on `errno`, was unrelated and changed depending on what
failed before. E.g:

```
$ builtin realpath "" /tmp "" /no-exist ""
builtin realpath: : Resource temporarily unavailable
/tmp
builtin realpath: : Invalid argument
/dont-exist
builtin realpath: : No such file or directory
```

Part of #12603
2026-04-03 13:53:41 +08:00
Nahor
1ed276292b read: remove unnecessary code
`to_stdout` is set to `true` if and only if `argv` is not empty.
- `argv` length and `to_stdout` are redundant, so we can remove `to_stdout`
- some tests in `validate_read_args` are necessarily false

Part of #12603
2026-04-03 13:53:41 +08:00
Branch Vincent
09e46b00cc completions: update ngrok
Closes #12598
2026-04-03 13:53:41 +08:00
Johannes Altmanninger
695bc293a9 contrib/debian/control: allow any "man" virtual pkg (man-db/mandoc)
We support multiple "man" implementations; at least man-db's and
mandoc's.

So we can relax the mandoc dependency to a dependency on the virtual
package providing "man". Note that as of today, "mandoc" fails to
have a "Provides: man".

However since Debian policy says in
https://www.debian.org/doc/debian-policy/ch-relationships.html

> To specify which of a set of real packages should be the default
> to satisfy a particular dependency on a virtual package, list the
> real package as an alternative before the virtual one.

we want to list possible real packages anyway, so do that.

Closes #12596
2026-04-03 12:20:08 +08:00
Nahor
344ff7be88 printf: remove unreachable code
Remove an unreachable, yet translated, error string and make the code
more idiomatic

Closes #12594
2026-04-01 09:37:57 +08:00
Nahor
014e3b3aff math: fix error message
Fix badly formatted error message, and make it translatable

Part of #12594
2026-04-01 09:37:57 +08:00
Nahor
68c7baff90 read: remove deprecation error for -i
`-i` has been an error and undocumented for 8 years now (86362e7) but
still requires some translating today. Time to retire it fully.

Part of #12594
2026-04-01 09:37:56 +08:00
Johannes Altmanninger
6eaad2cd80 Remove some redundant translation sources 2026-03-31 14:55:04 +08:00
Johannes Altmanninger
b321e38f5a psub: add missing line endings to error messages
Fixes #12593
2026-03-31 14:43:34 +08:00
Daniel Rainer
a32dd63163 fix: simplify and correct trimming of features
The previous handling was unnecessarily complex and had a bug introduced
by porting from C++ to Rust: The substrings `\0x0B` and `\0x0C` in Rust
mean `\0` (the NUL character) followed by the regular characters `0B`
and `0C`, respectively, so feature names starting or ending with these
characters would have these characters stripped away.

Replace this handling by built-in functionality, and simplify some
syntax. We now trim all whitespace, instead of just certain ASCII
characters, but I think there is no reason to limit trimming to ASCII.

Closes #12592
2026-03-31 14:43:34 +08:00
Bacal Mesfin
ef90afa5b9 feat: add completions for dnf copr
Closes #12585
2026-03-31 14:43:34 +08:00
r-vdp
7bd37dfe55 create_manpage_completions: handle groff \X'...' device control escapes
help2man 1.50 added \X'tty: link URL' hyperlink escapes to generated
man pages. coreutils 9.10 is the first widely-deployed package to ship
these, and it broke completion generation for most of its commands
(only 17/106 man pages parsed successfully).

The escape wraps option text like this:

  \X'tty: link https://example.com/a'\fB\-a, \-\-all\fP\X'tty: link'

Two places needed fixing:

- remove_groff_formatting() didn't strip \X'...', so Type1-4 parsers
  extracted garbage option names like "--all\X'tty"

- Deroffer.esc_char_backslash() didn't recognize \X, falling through
  to the generic single-char escape which stripped only the \, leaving
  "X'tty: link ...'" as literal text. Option lines then started with
  X instead of -, so TypeDeroffManParser's is_option() check failed.

Also handle \Z'...' (zero-width string) which has identical syntax.

Closes #12578
2026-03-31 13:15:52 +08:00
Johannes Altmanninger
14ce56d2a5 Remove unused fish_wcwidth wrapper 2026-03-30 13:57:27 +08:00
Johannes Altmanninger
01ee6f968d Support backward-word-end when cursor is past end
Closes #12581
2026-03-30 13:57:27 +08:00
Johannes Altmanninger
7f6dcde5e0 Fix backward-delete-char not stopping after control characters
Fixes 146384abc6 (Stop using wcwidth entirely, 2026-03-15)

Fixes #12583
2026-03-30 13:57:27 +08:00
Johannes Altmanninger
34fc573668 Modernize wcwidth API
Return None rather than -1 for nonprintables.  We probably still
differ from wcwidth which is bad (given we use the same name), but
hopefully not in a way that matters.

Fixes 146384abc6 (Stop using wcwidth entirely, 2026-03-15).
2026-03-30 13:57:10 +08:00
Johannes Altmanninger
93cbf2a0e8 Reuse wcswidth logic for rendered characters 2026-03-29 17:04:14 +08:00
Nahor
8194c6eb79 cd: replace unreachable code with assert
Closes #12584
2026-03-29 16:49:24 +08:00
Nahor
3194572156 bind: replace fake enum (c_int) with a real Rust enum
Part of #12584
2026-03-29 16:49:24 +08:00
Johannes Altmanninger
e635816b7f Fix Vi mode dl deleting from wrong character
Fixes b9b32ad157 (Fix vi mode dl and dh regressions, 2026-02-25).

Closes #12580
(which describes only the issue already fixed by b9b32ad157).
2026-03-28 21:45:33 +08:00
Johannes Altmanninger
2b3ecf22da start new cycle
Created by ./build_tools/release.sh 4.6.0
2026-03-28 13:16:34 +08:00
218 changed files with 12802 additions and 12093 deletions

View File

@@ -0,0 +1,32 @@
name: Linux development builds
on:
push:
branches:
- buildscript
jobs:
deploy:
runs-on: ubuntu-latest
environment: linux-development
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, build_tools/update-dependencies.sh
- uses: astral-sh/setup-uv@v7
- name: Update package database
run: sudo apt-get update
- name: Install deps
run: sudo apt install debhelper devscripts dpkg-dev
- name: Create tarball and source packages
run: |
version=$(build_tools/git_version_gen.sh --stdout 2>/dev/null)
mkdir /tmp/gpg
echo "$SIGNING_GPG_KEY" > /tmp/gpg/signing-gpg-key
mkdir /tmp/fish-built
FISH_ARTEFACT_PATH=/tmp/fish-built ./build_tools/make_tarball.sh
FISH_ARTEFACT_PATH=/tmp/fish-built DEB_SIGN_KEYFILE=/tmp/gpg/signing-gpg-key ./build_tools/make_linux_packages.sh $version
- uses: actions/upload-artifact@v6
with:
name: linux-source-packages
path: |
/tmp/fish-built
! /tmp/fish-built/fish-*/* # don't include the unpacked source directory

View File

@@ -1,3 +1,30 @@
fish ?.?.? (released ???)
=========================
Notable improvements and fixes
------------------------------
Deprecations and removed features
---------------------------------
Interactive improvements
------------------------
Improved terminal support
-------------------------
Other improvements
------------------
- History is no longer corrupted with NUL bytes when fish receives SIGTERM or SIGHUP (:issue:`10300`).
- ``fish_update_completions`` now handles groff ``\X'...'`` device control escapes, fixing completion generation for man pages produced by help2man 1.50 and later (such as coreutils 9.10).
For distributors and developers
-------------------------------
- When the default global config directory (``$PREFIX/etc/fish``) exists but has been overridden with ``-DCMAKE_INSTALL_SYSCONFDIR``, fish will now respect that override (:issue:`10748`).
Regression fixes:
-----------------
fish 4.6.0 (released March 28, 2026)
====================================

10
Cargo.lock generated
View File

@@ -272,6 +272,7 @@ dependencies = [
"fish-color",
"fish-common",
"fish-fallback",
"fish-feature-flags",
"fish-gettext",
"fish-gettext-extraction",
"fish-gettext-mo-file-parser",
@@ -329,6 +330,7 @@ version = "0.0.0"
dependencies = [
"bitflags",
"fish-build-helper",
"fish-feature-flags",
"fish-widestring",
"libc",
"nix",
@@ -348,6 +350,13 @@ dependencies = [
"widestring",
]
[[package]]
name = "fish-feature-flags"
version = "0.0.0"
dependencies = [
"fish-widestring",
]
[[package]]
name = "fish-gettext"
version = "0.0.0"
@@ -437,6 +446,7 @@ version = "0.0.0"
name = "fish-widestring"
version = "0.0.0"
dependencies = [
"libc",
"unicode-width",
"widestring",
]

View File

@@ -23,6 +23,7 @@ fish-build-man-pages = { path = "crates/build-man-pages" }
fish-color = { path = "crates/color" }
fish-common = { path = "crates/common" }
fish-fallback = { path = "crates/fallback" }
fish-feature-flags = { path = "crates/feature-flags" }
fish-gettext = { path = "crates/gettext" }
fish-gettext-extraction = { path = "crates/gettext-extraction" }
fish-gettext-maps = { path = "crates/gettext-maps" }
@@ -108,6 +109,7 @@ fish-build-man-pages = { workspace = true, optional = true }
fish-color.workspace = true
fish-common.workspace = true
fish-fallback.workspace = true
fish-feature-flags.workspace = true
fish-gettext = { workspace = true, optional = true }
fish-gettext-extraction = { workspace = true, optional = true }
fish-printf.workspace = true

View File

@@ -8,11 +8,29 @@ if [ "$FISH_CHECK_LINT" = false ]; then
lint=false
fi
case "$(uname)" in
MSYS*)
is_cygwin=true
cygwin_var=MSYS
;;
CYGWIN*)
is_cygwin=true
cygwin_var=CYGWIN
;;
*)
is_cygwin=false
;;
esac
check_dependency_versions=false
if [ "${FISH_CHECK_DEPENDENCY_VERSIONS:-false}" != false ]; then
check_dependency_versions=true
fi
green='\e[0;32m'
yellow='\e[1;33m'
reset='\e[m'
if $check_dependency_versions; then
command -v curl
command -v jq
@@ -83,12 +101,45 @@ if $lint; then
cargo clippy --workspace --all-targets $features
done
fi
cargo test --no-default-features --workspace --all-targets
# When running `cargo test`, some binaries (e.g. `fish_gettext_extraction`)
# are dynamically linked against Rust's `std-xxx.dll` instead of being
# statically link as they usually are.
# On Cygwin, `PATH`is not properly updated to point to the `std-xxx.dll`
# location, so we have to do it manually.
# See:
# - https://github.com/rust-lang/rust/issues/149050
# - https://github.com/msys2/MSYS2-packages/issues/5784
(
if $is_cygwin; then
export PATH="$PATH:$(rustc --print target-libdir)"
fi
cargo test --no-default-features --workspace --all-targets
)
cargo test --doc --workspace
if $lint; then
cargo doc --workspace --no-deps
fi
FISH_GETTEXT_EXTRACTION_DIR=$gettext_template_dir "$workspace_root/tests/test_driver.py" "$build_dir"
# Using "()" not "{}" because we do want a subshell (for the export)
system_tests() (
[ -n "$@" ] && export "$@"
export FISH_GETTEXT_EXTRACTION_DIR="$gettext_template_dir"
"$workspace_root/tests/test_driver.py" "$build_dir"
)
test_cmd='FISH_GETTEXT_EXTRACTION_DIR="$gettext_template_dir" "$workspace_root/tests/test_driver.py" "$build_dir"'
if $is_cygwin; then
echo -e "=== Running ${green}integration tests ${yellow}with${green} symlinks${reset}"
system_tests $cygwin_var=winsymlinks
echo -e "=== Running ${green}integration tests ${yellow}without${green} symlinks${reset}"
system_tests $cygwin_var=winsymlinks
else
echo -e "=== Running ${green}integration tests${reset}"
system_tests
fi
exit
}

View File

@@ -1,28 +0,0 @@
#!/bin/sh
# Script to generate the dput.cf for a set of Ubuntu series, prints the filename
# Arguments are the PPA followed by the series names
set -e
outfile=$(mktemp --tmpdir dput.XXXXX.cf)
[ $# -lt 2 ] &&
echo "$0: at least two arguments (a PPA and at least one series) are required" >&2 &&
exit 1
ppa=$1
shift
for series in "$@"; do
cat >> "$outfile" <<EOF
[fish-$ppa-$series]
fqdn = ppa.launchpad.net
method = ftp
login = anonymous
incoming = ~fish-shell/$ppa/ubuntu/$series
EOF
done
echo "$outfile"

View File

@@ -0,0 +1,83 @@
#!/bin/sh
# This script takes a source tarball (from build_tools/make_tarball.sh) and a vendor tarball (from
# build_tools/make_vendor_tarball.sh, generated if not present), and produces:
# * Appropriately-named symlinks to look like a Debian package
# * Debian .changes and .dsc files with plain names ($version-1) and supported Ubuntu prefixes
# ($version-1~somedistro)
# * An RPM spec file
# By default, input and output files go in ~/fish_built, but this can be controlled with the
# FISH_ARTEFACT_PATH environment variable.
{
set -e
version=$1
[ -n "$version" ] || { echo "Version number required as argument" >&2; exit 1; }
[ -n "$DEB_SIGN_KEYID$DEB_SIGN_KEYFILE" ] ||
echo "Warning: neither DEB_SIGN_KEYID or DEB_SIGN_KEYFILE environment variables are set; you
will need a signing key for the author of the most recent debian/changelog entry." >&2
workpath=${FISH_ARTEFACT_PATH:-~/fish_built}
source_tarball="$workpath"/fish-"$version".tar.xz
vendor_tarball="$workpath"/fish-"$version"-vendor.tar.xz
[ -e "$source_tarball" ] || { echo "Missing source tarball, expected at $source_tarball" >&2; exit 1; }
cd "$workpath"
# Unpack the sources
tar xf "$source_tarball"
sourcepath="$workpath"/fish-"$version"
# Generate the vendor tarball if it is not already present
[ -e "$vendor_tarball" ] || (cd "$sourcepath"; build_tools/make_vendor_tarball.sh;)
# This step requires network access, so do it early in case it fails
# sh has no real array support
ubuntu_versions=$(uv run --script "$sourcepath"/build_tools/supported_ubuntu_versions.py)
# Write the specfile
[ -e "$workpath"/fish.spec ] && { echo "Cowardly refusing to overwite an existing fish.spec" >&2;
exit 1; }
rpmversion=$(echo "$version" |sed -e 's/-/+/' -e 's/-/./g')
sed -e "s/@version@/$version/g" -e "s/@rpmversion@/$rpmversion/g" \
< "$sourcepath"/fish.spec.in > "$workpath"/fish.spec
# Make the symlinks for Debian
ln -s "$source_tarball" "$workpath"/fish_"$version".orig.tar.xz
ln -s "$vendor_tarball" "$workpath"/fish_"$version".orig-cargo-vendor.tar.xz
# Set up the Debian source tree
cd "$sourcepath"
mkdir cargo-vendor
tar -C cargo-vendor -x -f "$vendor_tarball"
cp -r contrib/debian debian
# The vendor tarball contains a new .cargo/config.toml, which has the
# vendoring overrides appended to it. dpkg-source will add this as a
# patch using the flags in debian/
cp cargo-vendor/.cargo/config.toml .cargo/config.toml
# Update the Debian changelog
# The release scripts do this for release builds - skip if it has already been done
if head -n1 debian/changelog | grep --invert-match --quiet --fixed-strings "$version"; then
debchange --newversion "$version-1" --distribution unstable "Snapshot build"
fi
# Builds the "plain" Debian package
# debuild runs lintian, which takes ten minutes to run over the vendor directories
# just use dpkg-buildpackage directly
dpkg-buildpackage --build=source -d
# Build the Ubuntu packages
# deb-reversion does not work on source packages, so do the whole thing ourselves
for series in $ubuntu_versions; do
sed -i -e "1 s/$version-1)/$version-1~$series)/" -e "1 s/unstable/$series/" debian/changelog
dpkg-buildpackage --build=source -d
sed -i -e "1 s/$version-1~$series)/$version-1)/" -e "1 s/$series/unstable/" debian/changelog
done
}

View File

@@ -8,20 +8,6 @@
# Exit on error
set -e
# We need GNU tar as that supports the --mtime and --transform options
TAR=notfound
for try in tar gtar gnutar; do
if $try -Pcf /dev/null --mtime now /dev/null >/dev/null 2>&1; then
TAR=$try
break
fi
done
if [ "$TAR" = "notfound" ]; then
echo 'No suitable tar (supporting --mtime) found as tar/gtar/gnutar in PATH'
exit 1
fi
# Get the current directory, which we'll use for telling Cargo where to find the sources
wd="$PWD"

View File

@@ -13,7 +13,7 @@ Build-Depends: debhelper-compat (= 13),
python3-sphinx,
# Test dependencies
locales-all,
man-db,
man-db | man,
python3
# 4.6.2 is Debian 12/Ubuntu Noble 24.04; Ubuntu Jammy is 4.6.0.1
Standards-Version: 4.6.2
@@ -26,8 +26,8 @@ Architecture: any
# for col and lock
Depends: bsdextrautils,
file,
# for man
man-db,
# for showing built-in help pages
man-db | man,
# for kill
procps,
python3 (>=3.5),

View File

@@ -8,6 +8,7 @@ license.workspace = true
[dependencies]
bitflags.workspace = true
fish-feature-flags.workspace = true
fish-widestring.workspace = true
libc.workspace = true
nix.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,12 @@
use std::cmp;
use std::sync::{
LazyLock,
atomic::{AtomicIsize, Ordering},
atomic::{AtomicUsize, Ordering},
};
/// Width of ambiguous East Asian characters and, as of TR11, all private-use characters.
/// 1 is the typical default, but we accept any non-negative override via `$fish_ambiguous_width`.
pub static FISH_AMBIGUOUS_WIDTH: AtomicIsize = AtomicIsize::new(1);
pub static FISH_AMBIGUOUS_WIDTH: AtomicUsize = AtomicUsize::new(1);
/// Width of emoji characters.
///
@@ -25,34 +25,33 @@
/// Valid values are 1, and 2. 1 is the typical emoji width used in Unicode 8 while some newer
/// terminals use a width of 2 since Unicode 9.
// For some reason, this is declared here and exposed here, but is set in `env_dispatch`.
pub static FISH_EMOJI_WIDTH: AtomicIsize = AtomicIsize::new(2);
pub static FISH_EMOJI_WIDTH: AtomicUsize = AtomicUsize::new(2);
static WC_LOOKUP_TABLE: LazyLock<WcLookupTable> = LazyLock::new(WcLookupTable::new);
// Big hack to use our versions of wcswidth where we know them to be broken, which is
// EVERYWHERE (https://github.com/fish-shell/fish-shell/issues/2199)
pub fn fish_wcwidth(c: char) -> isize {
pub fn fish_wcwidth(c: char) -> Option<usize> {
// Check for VS16 which selects emoji presentation. This "promotes" a character like U+2764
// (width 1) to an emoji (probably width 2). So treat it as width 1 so the sums work. See #2652.
// VS15 selects text presentation.
let variation_selector_16 = '\u{FE0F}';
let variation_selector_15 = '\u{FE0E}';
if c == variation_selector_16 {
return 1;
return Some(1);
} else if c == variation_selector_15 {
return 0;
return Some(0);
}
// Check for Emoji_Modifier property. Only the Fitzpatrick modifiers have this, in range
// 1F3FB..1F3FF. This is a hack because such an emoji appearing on its own would be drawn as
// width 2, but that's unlikely to be useful. See #8275.
if ('\u{1F3FB}'..='\u{1F3FF}').contains(&c) {
return 0;
return Some(0);
}
let width = WC_LOOKUP_TABLE.classify(c);
match width {
WcWidth::NonCharacter | WcWidth::NonPrint | WcWidth::Combining | WcWidth::Unassigned => 0,
Some(match width {
WcWidth::NonPrint => return None,
WcWidth::NonCharacter | WcWidth::Combining | WcWidth::Unassigned => 0,
WcWidth::Ambiguous | WcWidth::PrivateUse => {
// TR11: "All private-use characters are by default classified as Ambiguous".
FISH_AMBIGUOUS_WIDTH.load(Ordering::Relaxed)
@@ -60,25 +59,25 @@ pub fn fish_wcwidth(c: char) -> isize {
WcWidth::One => 1,
WcWidth::Two => 2,
WcWidth::WidenedIn9 => FISH_EMOJI_WIDTH.load(Ordering::Relaxed),
}
})
}
/// fish's internal versions of wcwidth and wcswidth
pub fn fish_wcswidth(s: &wstr) -> isize {
// ascii fast path; empty iterator returns true for .all()
if s.chars().all(|c| c.is_ascii() && !c.is_ascii_control()) {
return s.len() as isize;
pub fn fish_wcswidth(s: &wstr) -> Option<usize> {
fish_wcswidth_canonicalizing(s, std::convert::identity)
}
pub fn fish_wcswidth_canonicalizing(s: &wstr, canonicalize: fn(char) -> char) -> Option<usize> {
let chars = s.chars().map(canonicalize);
// ascii fast path
if chars.clone().all(|c| c.is_ascii() && !c.is_ascii_control()) {
return Some(s.len());
}
let mut result = 0;
for c in s.chars() {
let w = fish_wcwidth(c);
if w < 0 {
return -1;
}
result += w;
for c in chars {
result += fish_wcwidth(c)?;
}
result
Some(result)
}
pub fn wcscasecmp(lhs: &wstr, rhs: &wstr) -> cmp::Ordering {

View File

@@ -0,0 +1,13 @@
[package]
name = "fish-feature-flags"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
fish-widestring.workspace = true
[lints]
workspace = true

View File

@@ -1,15 +1,14 @@
//! Flags to enable upcoming features
use crate::prelude::*;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
#[cfg(test)]
use std::cell::RefCell;
use fish_widestring::{L, WExt as _, wstr};
use std::{
cell::RefCell,
sync::atomic::{AtomicBool, Ordering},
};
/// The list of flags.
#[repr(u8)]
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FeatureFlag {
/// Whether ^ is supported for stderr redirection.
StderrNoCaret,
@@ -63,10 +62,10 @@ pub struct FeatureMetadata {
pub description: &'static wstr,
/// Default flag value.
pub default_value: bool,
default_value: bool,
/// Whether the value can still be changed or not.
pub read_only: bool,
read_only: bool,
}
/// The metadata, indexed by flag.
@@ -156,31 +155,26 @@ pub struct FeatureMetadata {
];
thread_local!(
#[cfg(test)]
static LOCAL_FEATURES: RefCell<Option<Features>> = const { RefCell::new(None) };
static LOCAL_OVERRIDE_STACK: RefCell<Vec<(FeatureFlag, bool)>> =
const { RefCell::new(Vec::new()) };
);
/// The singleton shared feature set.
static FEATURES: Features = Features::new();
/// Perform a feature test on the global set of features.
pub fn test(flag: FeatureFlag) -> bool {
#[cfg(test)]
{
LOCAL_FEATURES.with(|fc| fc.borrow().as_ref().unwrap_or(&FEATURES).test(flag))
pub fn feature_test(flag: FeatureFlag) -> bool {
if let Some(value) = LOCAL_OVERRIDE_STACK.with(|stack| {
for &(overridden_feature, value) in stack.borrow().iter().rev() {
if flag == overridden_feature {
return Some(value);
}
}
None
}) {
return value;
}
#[cfg(not(test))]
{
FEATURES.test(flag)
}
}
pub use test as feature_test;
/// Set a flag.
#[cfg(test)]
pub fn set(flag: FeatureFlag, value: bool) {
LOCAL_FEATURES.with(|fc| fc.borrow().as_ref().unwrap_or(&FEATURES).set(flag, value));
FEATURES.test(flag)
}
/// Parses a comma-separated feature-flag string, updating ourselves with the values.
@@ -188,20 +182,7 @@ pub fn set(flag: FeatureFlag, value: bool) {
/// The special group name "all" may be used for those who like to live on the edge.
/// Unknown features are silently ignored.
pub fn set_from_string<'a>(str: impl Into<&'a wstr>) {
let wstr: &wstr = str.into();
#[cfg(test)]
{
LOCAL_FEATURES.with(|fc| {
fc.borrow()
.as_ref()
.unwrap_or(&FEATURES)
.set_from_string(wstr);
});
}
#[cfg(not(test))]
{
FEATURES.set_from_string(wstr);
}
FEATURES.set_from_string(str.into());
}
impl Features {
@@ -237,19 +218,14 @@ fn set(&self, flag: FeatureFlag, value: bool) {
}
fn set_from_string(&self, str: &wstr) {
let whitespace = L!("\t\n\0x0B\0x0C\r ").as_char_slice();
for entry in str.as_char_slice().split(|c| *c == ',') {
for entry in str.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
// Trim leading and trailing whitespace
let entry = &entry[entry.iter().take_while(|c| whitespace.contains(c)).count()..];
let entry =
&entry[..entry.len() - entry.iter().take_while(|c| whitespace.contains(c)).count()];
// A "no-" prefix inverts the sense.
let (name, value) = match entry.strip_prefix(L!("no-").as_char_slice()) {
let (name, value) = match entry.strip_prefix("no-") {
Some(suffix) => (suffix, false),
None => (entry, true),
};
@@ -275,28 +251,20 @@ fn set_from_string(&self, str: &wstr) {
}
}
#[cfg(test)]
pub fn scoped_test(flag: FeatureFlag, value: bool, test_fn: impl FnOnce()) {
LOCAL_FEATURES.with(|fc| {
assert!(
fc.borrow().is_none(),
"scoped_test() does not support nesting"
);
let f = Features::new();
f.set(flag, value);
*fc.borrow_mut() = Some(f);
/// Run code with a feature overridden.
/// This should only be used in tests.
pub fn with_overridden_feature(flag: FeatureFlag, value: bool, test_fn: impl FnOnce()) {
LOCAL_OVERRIDE_STACK.with(|stack| {
stack.borrow_mut().push((flag, value));
test_fn();
*fc.borrow_mut() = None;
stack.borrow_mut().pop();
});
}
#[cfg(test)]
mod tests {
use super::{FeatureFlag, Features, METADATA, scoped_test, set, test};
use crate::prelude::*;
use super::{FeatureFlag, Features, METADATA, feature_test, with_overridden_feature};
use fish_widestring::L;
#[test]
fn test_feature_flags() {
@@ -322,25 +290,19 @@ fn test_feature_flags() {
}
#[test]
fn test_scoped() {
scoped_test(FeatureFlag::QuestionMarkNoGlob, true, || {
assert!(test(FeatureFlag::QuestionMarkNoGlob));
fn test_overridden_feature() {
with_overridden_feature(FeatureFlag::QuestionMarkNoGlob, true, || {
assert!(feature_test(FeatureFlag::QuestionMarkNoGlob));
});
set(FeatureFlag::QuestionMarkNoGlob, true);
scoped_test(FeatureFlag::QuestionMarkNoGlob, false, || {
assert!(!test(FeatureFlag::QuestionMarkNoGlob));
with_overridden_feature(FeatureFlag::QuestionMarkNoGlob, false, || {
assert!(!feature_test(FeatureFlag::QuestionMarkNoGlob));
});
set(FeatureFlag::QuestionMarkNoGlob, false);
}
#[test]
#[should_panic]
fn test_nested_scopes_not_supported() {
scoped_test(FeatureFlag::QuestionMarkNoGlob, true, || {
scoped_test(FeatureFlag::QuestionMarkNoGlob, false, || {});
with_overridden_feature(FeatureFlag::QuestionMarkNoGlob, false, || {
with_overridden_feature(FeatureFlag::QuestionMarkNoGlob, true, || {
assert!(feature_test(FeatureFlag::QuestionMarkNoGlob));
});
});
}
}

View File

@@ -1,12 +1,7 @@
//! Helper functions for working with wcstring.
use std::{
ffi::{CStr, CString, OsString},
os::unix::ffi::OsStringExt as _,
};
use fish_fallback::{fish_wcwidth, lowercase, lowercase_rev, wcscasecmp, wcscasecmp_fuzzy};
use fish_widestring::{ELLIPSIS_CHAR, decode_byte_from_char, prelude::*};
use fish_widestring::{ELLIPSIS_CHAR, prelude::*};
/// Return the number of newlines in a string.
pub fn count_newlines(s: &wstr) -> usize {
@@ -340,145 +335,6 @@ pub fn string_fuzzy_match_string(
StringFuzzyMatch::try_create(string, match_against, anchor_start)
}
/// Implementation of wcs2bytes that accepts a callback.
/// The first argument can be either a `&str` or `&wstr`.
/// This invokes `func` with byte slices containing the UTF-8 encoding of the characters in the
/// input, doing one invocation per character.
/// If `func` returns false, it stops; otherwise it continues.
/// Return false if the callback returned false, otherwise true.
pub fn str2bytes_callback(input: impl IntoCharIter, mut func: impl FnMut(&[u8]) -> bool) -> bool {
// A `char` represents an Unicode scalar value, which takes up at most 4 bytes when encoded in UTF-8.
let mut converted = [0_u8; 4];
for c in input.chars() {
let bytes = if let Some(byte) = decode_byte_from_char(c) {
converted[0] = byte;
&converted[..=0]
} else {
c.encode_utf8(&mut converted).as_bytes()
};
if !func(bytes) {
return false;
}
}
true
}
/// Returns a newly allocated multibyte character string equivalent of the specified wide character
/// string.
///
/// This function decodes illegal character sequences in a reversible way using the private use
/// area.
pub fn wcs2bytes(input: impl IntoCharIter) -> Vec<u8> {
let mut result = vec![];
wcs2bytes_appending(&mut result, input);
result
}
pub fn wcs2osstring(input: &wstr) -> OsString {
if input.is_empty() {
return OsString::new();
}
let mut result = vec![];
wcs2bytes_appending(&mut result, input);
OsString::from_vec(result)
}
/// Same as [`wcs2bytes`]. Meant to be used when we need a zero-terminated string to feed legacy APIs.
/// Note: if `input` contains any interior NUL bytes, the result will be truncated at the first!
pub fn wcs2zstring(input: &wstr) -> CString {
if input.is_empty() {
return CString::default();
}
let mut vec = Vec::with_capacity(input.len() + 1);
str2bytes_callback(input, |buff| {
vec.extend_from_slice(buff);
true
});
vec.push(b'\0');
match CString::from_vec_with_nul(vec) {
Ok(cstr) => cstr,
Err(err) => {
// `input` contained a NUL in the middle; we can retrieve `vec`, though
let mut vec = err.into_bytes();
let pos = vec.iter().position(|c| *c == b'\0').unwrap();
vec.truncate(pos + 1);
// Safety: We truncated after the first NUL
unsafe { CString::from_vec_with_nul_unchecked(vec) }
}
}
}
/// Like [`wcs2bytes`], but appends to `output` instead of returning a new string.
pub fn wcs2bytes_appending(output: &mut Vec<u8>, input: impl IntoCharIter) {
str2bytes_callback(input, |buff| {
output.extend_from_slice(buff);
true
});
}
/// A trait to make it more convenient to pass ascii/Unicode strings to functions that can take
/// non-Unicode values. The result is nul-terminated and can be passed to OS functions.
///
/// This is only implemented for owned types where an owned instance will skip allocations (e.g.
/// `CString` can return `self`) but not implemented for owned instances where a new allocation is
/// always required (e.g. implemented for `&wstr` but not `WideString`) because you might as well be
/// left with the original item if we're going to allocate from scratch in all cases.
pub trait ToCString {
/// Correctly convert to a nul-terminated [`CString`] that can be passed to OS functions.
fn to_cstring(self) -> CString;
}
impl ToCString for CString {
fn to_cstring(self) -> CString {
self
}
}
impl ToCString for &CStr {
fn to_cstring(self) -> CString {
self.to_owned()
}
}
/// Safely converts from `&wstr` to a `CString` to a nul-terminated `CString` that can be passed to
/// OS functions, taking into account non-Unicode values that have been shifted into the private-use
/// range by using [`wcs2zstring()`].
impl ToCString for &wstr {
/// The wide string may contain non-Unicode bytes mapped to the private-use Unicode range, so we
/// have to use [`wcs2zstring()`](self::wcs2zstring) to convert it correctly.
fn to_cstring(self) -> CString {
self::wcs2zstring(self)
}
}
/// Safely converts from `&WString` to a nul-terminated `CString` that can be passed to OS
/// functions, taking into account non-Unicode values that have been shifted into the private-use
/// range by using [`wcs2zstring()`].
impl ToCString for &WString {
fn to_cstring(self) -> CString {
self.as_utfstr().to_cstring()
}
}
/// Convert a (probably ascii) string to CString that can be passed to OS functions.
impl ToCString for Vec<u8> {
fn to_cstring(mut self) -> CString {
self.push(b'\0');
CString::from_vec_with_nul(self).unwrap()
}
}
/// Convert a (probably ascii) string to nul-terminated CString that can be passed to OS functions.
impl ToCString for &[u8] {
fn to_cstring(self) -> CString {
CString::new(self).unwrap()
}
}
/// Split a string by runs of any of the separator characters provided in `seps`.
/// Note the delimiters are the characters in `seps`, not `seps` itself.
/// `seps` may contain the NUL character.
@@ -665,12 +521,12 @@ fn next(&mut self) -> Option<Self::Item> {
}
}
/// Like fish_wcwidth, but returns 0 for characters with no real width instead of -1.
/// Like fish_wcwidth, but returns 0 for characters with no real width instead of none.
pub fn fish_wcwidth_visible(c: char) -> isize {
if c == '\x08' {
return -1;
}
fish_wcwidth(c).max(0)
fish_wcwidth(c).unwrap_or_default().try_into().unwrap()
}
#[cfg(test)]

View File

@@ -7,6 +7,7 @@ repository.workspace = true
license.workspace = true
[dependencies]
libc.workspace = true
unicode-width.workspace = true
widestring.workspace = true

View File

@@ -6,16 +6,34 @@
pub mod word_char;
use std::{iter, slice};
use std::{
ffi::{CStr, CString, OsStr, OsString},
iter,
os::unix::ffi::{OsStrExt as _, OsStringExt as _},
slice,
};
pub use widestring::{Utf32Str as wstr, Utf32String as WString, utf32str as L, utfstr::CharsUtf32};
pub mod prelude {
pub use crate::{IntoCharIter, L, ToWString, WExt, WString, wstr};
}
// Highest legal ASCII value.
pub const ASCII_MAX: char = 127 as char;
// Highest legal 16-bit Unicode value.
pub const UCS2_MAX: char = '\u{FFFF}';
// Highest legal byte value.
pub const BYTE_MAX: char = 0xFF as char;
// Unicode BOM value.
pub const UTF8_BOM_WCHAR: char = '\u{FEFF}';
/// The character to use where the text has been truncated.
pub const ELLIPSIS_CHAR: char = '\u{2026}'; // ('…')
pub const SPECIAL_KEY_ENCODE_BASE: char = '\u{F500}';
// These are in the Unicode private-use range. We really shouldn't use this
// range but have little choice in the matter given how our lexer/parser works.
// We can't use non-characters for these two ranges because there are only 66 of
@@ -28,9 +46,79 @@ pub mod prelude {
// Note: We don't use the highest 8 bit range (0xF800 - 0xF8FF) because we know
// of at least one use of a codepoint in that range: the Apple symbol (0xF8FF)
// on Mac OS X. See http://www.unicode.org/faq/private_use.html.
pub const ENCODE_DIRECT_BASE: char = '\u{F600}';
pub const ENCODE_DIRECT_BASE: char = char_offset(SPECIAL_KEY_ENCODE_BASE, 256);
pub const ENCODE_DIRECT_END: char = char_offset(ENCODE_DIRECT_BASE, 256);
// Use Unicode "non-characters" for internal characters as much as we can. This
// gives us 32 "characters" for internal use that we can guarantee should not
// appear in our input stream. See http://www.unicode.org/faq/private_use.html.
pub const RESERVED_CHAR_BASE: char = '\u{FDD0}';
pub const RESERVED_CHAR_END: char = '\u{FDF0}';
// Split the available non-character values into two ranges to ensure there are
// no conflicts among the places we use these special characters.
pub const EXPAND_RESERVED_BASE: char = RESERVED_CHAR_BASE;
pub const EXPAND_RESERVED_END: char = char_offset(EXPAND_RESERVED_BASE, 16);
pub const WILDCARD_RESERVED_BASE: char = EXPAND_RESERVED_END;
pub const WILDCARD_RESERVED_END: char = char_offset(WILDCARD_RESERVED_BASE, 16);
// Make sure the ranges defined above don't exceed the range for non-characters.
// This is to make sure we didn't do something stupid in subdividing the
// Unicode range for our needs.
const _: () = assert!(WILDCARD_RESERVED_END <= RESERVED_CHAR_END);
/// Character representing any character except '/' (slash).
pub const ANY_CHAR: char = char_offset(WILDCARD_RESERVED_BASE, 0);
/// Character representing any character string not containing '/' (slash).
pub const ANY_STRING: char = char_offset(WILDCARD_RESERVED_BASE, 1);
/// Character representing any character string.
pub const ANY_STRING_RECURSIVE: char = char_offset(WILDCARD_RESERVED_BASE, 2);
/// This is a special pseudo-char that is not used other than to mark the
/// end of the special characters so we can sanity check the enum range.
#[allow(dead_code)]
pub const ANY_SENTINEL: char = char_offset(WILDCARD_RESERVED_BASE, 3);
/// Character representing a home directory.
pub const HOME_DIRECTORY: char = char_offset(EXPAND_RESERVED_BASE, 0);
/// Character representing process expansion for %self.
pub const PROCESS_EXPAND_SELF: char = char_offset(EXPAND_RESERVED_BASE, 1);
/// Character representing variable expansion.
pub const VARIABLE_EXPAND: char = char_offset(EXPAND_RESERVED_BASE, 2);
/// Character representing variable expansion into a single element.
pub const VARIABLE_EXPAND_SINGLE: char = char_offset(EXPAND_RESERVED_BASE, 3);
/// Character representing the start of a bracket expansion.
pub const BRACE_BEGIN: char = char_offset(EXPAND_RESERVED_BASE, 4);
/// Character representing the end of a bracket expansion.
pub const BRACE_END: char = char_offset(EXPAND_RESERVED_BASE, 5);
/// Character representing separation between two bracket elements.
pub const BRACE_SEP: char = char_offset(EXPAND_RESERVED_BASE, 6);
/// Character that takes the place of any whitespace within non-quoted text in braces
pub const BRACE_SPACE: char = char_offset(EXPAND_RESERVED_BASE, 7);
/// Separate subtokens in a token with this character.
pub const INTERNAL_SEPARATOR: char = char_offset(EXPAND_RESERVED_BASE, 8);
/// Character representing an empty variable expansion. Only used transitively while expanding
/// variables.
pub const VARIABLE_EXPAND_EMPTY: char = char_offset(EXPAND_RESERVED_BASE, 9);
const _: () = assert!(
EXPAND_RESERVED_END as u32 > VARIABLE_EXPAND_EMPTY as u32,
"Characters used in expansions must stay within private use area"
);
/// The string represented by PROCESS_EXPAND_SELF
pub const PROCESS_EXPAND_SELF_STR: &wstr = L!("%self");
/// Return true if the character is in a range reserved for fish's private use.
///
/// NOTE: This is used when tokenizing the input. It is also used when reading input, before
/// tokenization, to replace such chars with REPLACEMENT_WCHAR if they're not part of a quoted
/// string. We don't want external input to be able to feed reserved characters into our
/// lexer/parser or code evaluator.
//
// TODO: Actually implement the replacement as documented above.
pub fn fish_reserved_codepoint(c: char) -> bool {
(c >= RESERVED_CHAR_BASE && c < RESERVED_CHAR_END)
|| (c >= SPECIAL_KEY_ENCODE_BASE && c < ENCODE_DIRECT_END)
}
/// Encode a literal byte in a UTF-32 character. This is required for e.g. the echo builtin, whose
/// escape sequences can be used to construct raw byte sequences which are then interpreted as e.g.
/// UTF-8 by the terminal. If we were to interpret each of those bytes as a codepoint and encode it
@@ -43,6 +131,86 @@ pub fn encode_byte_to_char(byte: u8) -> char {
.expect("private-use codepoint should be valid char")
}
/// Returns a newly allocated multibyte character string equivalent of the specified wide character
/// string.
///
/// This function decodes illegal character sequences in a reversible way using the private use
/// area.
pub fn wcs2bytes(input: impl IntoCharIter) -> Vec<u8> {
let mut result = vec![];
wcs2bytes_appending(&mut result, input);
result
}
pub fn wcs2osstring(input: &wstr) -> OsString {
if input.is_empty() {
return OsString::new();
}
let mut result = vec![];
wcs2bytes_appending(&mut result, input);
OsString::from_vec(result)
}
/// Same as [`wcs2bytes`]. Meant to be used when we need a zero-terminated string to feed legacy APIs.
/// Note: if `input` contains any interior NUL bytes, the result will be truncated at the first!
pub fn wcs2zstring(input: &wstr) -> CString {
if input.is_empty() {
return CString::default();
}
let mut vec = Vec::with_capacity(input.len() + 1);
str2bytes_callback(input, |buff| {
vec.extend_from_slice(buff);
true
});
vec.push(b'\0');
match CString::from_vec_with_nul(vec) {
Ok(cstr) => cstr,
Err(err) => {
// `input` contained a NUL in the middle; we can retrieve `vec`, though
let mut vec = err.into_bytes();
let pos = vec.iter().position(|c| *c == b'\0').unwrap();
vec.truncate(pos + 1);
// Safety: We truncated after the first NUL
unsafe { CString::from_vec_with_nul_unchecked(vec) }
}
}
}
/// Like [`wcs2bytes`], but appends to `output` instead of returning a new string.
pub fn wcs2bytes_appending(output: &mut Vec<u8>, input: impl IntoCharIter) {
str2bytes_callback(input, |buff| {
output.extend_from_slice(buff);
true
});
}
/// Implementation of wcs2bytes that accepts a callback.
/// The first argument can be either a `&str` or `&wstr`.
/// This invokes `func` with byte slices containing the UTF-8 encoding of the characters in the
/// input, doing one invocation per character.
/// If `func` returns false, it stops; otherwise it continues.
/// Return false if the callback returned false, otherwise true.
pub fn str2bytes_callback(input: impl IntoCharIter, mut func: impl FnMut(&[u8]) -> bool) -> bool {
// A `char` represents an Unicode scalar value, which takes up at most 4 bytes when encoded in UTF-8.
let mut converted = [0_u8; 4];
for c in input.chars() {
let bytes = if let Some(byte) = decode_byte_from_char(c) {
converted[0] = byte;
&converted[..=0]
} else {
c.encode_utf8(&mut converted).as_bytes()
};
if !func(bytes) {
return false;
}
}
true
}
/// Decode a literal byte from a UTF-32 character.
pub fn decode_byte_from_char(c: char) -> Option<u8> {
if c >= ENCODE_DIRECT_BASE && c < ENCODE_DIRECT_END {
@@ -56,6 +224,65 @@ pub fn decode_byte_from_char(c: char) -> Option<u8> {
}
}
/// A trait to make it more convenient to pass ascii/Unicode strings to functions that can take
/// non-Unicode values. The result is nul-terminated and can be passed to OS functions.
///
/// This is only implemented for owned types where an owned instance will skip allocations (e.g.
/// `CString` can return `self`) but not implemented for owned instances where a new allocation is
/// always required (e.g. implemented for `&wstr` but not `WideString`) because you might as well be
/// left with the original item if we're going to allocate from scratch in all cases.
pub trait ToCString {
/// Correctly convert to a nul-terminated [`CString`] that can be passed to OS functions.
fn to_cstring(self) -> CString;
}
impl ToCString for CString {
fn to_cstring(self) -> CString {
self
}
}
impl ToCString for &CStr {
fn to_cstring(self) -> CString {
self.to_owned()
}
}
/// Safely converts from `&wstr` to a `CString` to a nul-terminated `CString` that can be passed to
/// OS functions, taking into account non-Unicode values that have been shifted into the private-use
/// range by using [`wcs2zstring()`].
impl ToCString for &wstr {
/// The wide string may contain non-Unicode bytes mapped to the private-use Unicode range, so we
/// have to use [`wcs2zstring()`](self::wcs2zstring) to convert it correctly.
fn to_cstring(self) -> CString {
self::wcs2zstring(self)
}
}
/// Safely converts from `&WString` to a nul-terminated `CString` that can be passed to OS
/// functions, taking into account non-Unicode values that have been shifted into the private-use
/// range by using [`wcs2zstring()`].
impl ToCString for &WString {
fn to_cstring(self) -> CString {
self.as_utfstr().to_cstring()
}
}
/// Convert a (probably ascii) string to CString that can be passed to OS functions.
impl ToCString for Vec<u8> {
fn to_cstring(mut self) -> CString {
self.push(b'\0');
CString::from_vec_with_nul(self).unwrap()
}
}
/// Convert a (probably ascii) string to nul-terminated CString that can be passed to OS functions.
impl ToCString for &[u8] {
fn to_cstring(self) -> CString {
CString::new(self).unwrap()
}
}
mod decoder {
use crate::{ENCODE_DIRECT_BASE, ENCODE_DIRECT_END, char_offset, wstr};
use buffer::Buffer;
@@ -276,6 +503,82 @@ pub const fn char_offset(base: char, offset: u32) -> char {
}
}
/// Encodes the bytes in `input` into a [`WString`], encoding non-UTF-8 bytes into private-use-area
/// code-points. Bytes which would be parsed into our reserved PUA range are encoded individually,
/// to allow for correct round-tripping.
pub fn bytes2wcstring(mut input: &[u8]) -> WString {
if input.is_empty() {
return WString::new();
}
let mut result = WString::with_capacity(input.len());
fn append_escaped_str(output: &mut WString, input: &str) {
for (i, c) in input.char_indices() {
if fish_reserved_codepoint(c) {
for byte in &input.as_bytes()[i..i + c.len_utf8()] {
output.push(encode_byte_to_char(*byte));
}
} else {
output.push(c);
}
}
}
while !input.is_empty() {
match std::str::from_utf8(input) {
Ok(parsed_str) => {
append_escaped_str(&mut result, parsed_str);
// The entire remaining input could be parsed, so we are done.
break;
}
Err(e) => {
let (valid, after_valid) = input.split_at(e.valid_up_to());
// SAFETY: The previous `str::from_utf8` call established that the prefix `valid`
// is valid UTF-8. This prefix may be empty.
let parsed_str = unsafe { std::str::from_utf8_unchecked(valid) };
append_escaped_str(&mut result, parsed_str);
// The length of the prefix of `after_valid` which is invalid UTF-8.
// The remaining bytes of `input` (if any) will be parsed in subsequent iterations
// of the loop, starting from the first byte that starts a valid UTF-8-encoded codepoint.
// `error_len` can return `None`, if it sees a byte sequence that could be the
// prefix of a valid code-point encoding at the end of the byte slice.
// This is useful when the input is chunked, but we don't do that, so in this case
// we use our custom encoding for all remaining bytes (at most 3).
let error_len = e.error_len().unwrap_or(after_valid.len());
for byte in &after_valid[..error_len] {
result.push(encode_byte_to_char(*byte));
}
input = &after_valid[error_len..];
}
}
}
result
}
/// Use this rather than [`WString::from_str`] when the input could contain PUA bytes we use to
/// encode non-UTF-8 bytes. Otherwise, when decoding the resulting [`WString`], the PUA bytes in
/// the input would be converted to non-UTF-8 bytes.
pub fn str2wcstring<S: AsRef<str>>(input: S) -> WString {
bytes2wcstring(input.as_ref().as_bytes())
}
pub fn cstr2wcstring<C: AsRef<CStr>>(input: C) -> WString {
bytes2wcstring(input.as_ref().to_bytes())
}
pub fn osstr2wcstring<O: AsRef<OsStr>>(input: O) -> WString {
bytes2wcstring(input.as_ref().as_bytes())
}
/// # SAFETY
///
/// `input` must point to a valid NUL-terminated string.
pub unsafe fn charptr2wcstring(input: *const libc::c_char) -> WString {
let input: &[u8] = unsafe { CStr::from_ptr(input).to_bytes() };
bytes2wcstring(input)
}
/// Finds `needle` in a `haystack` and returns the index of the first matching element, if any.
///
/// # Examples

View File

@@ -251,7 +251,7 @@ Some *OPTION_SPEC* examples:
- ``n/name=?`` means that both ``-n`` and ``--name`` are valid. It accepts an optional value and can be used at most once. If the flag is seen then ``_flag_n`` and ``_flag_name`` will be set with the value associated with the flag if one was provided else it will be set with no values.
- ``n/name=*`` is similar, but the flag can be used more than once. If the flag is seen then ``_flag_n`` and ``_flag_name`` will be set with the values associated with each occurence. Each value will be the value given to the option, or the empty string if no value was given.
- ``n/name=*`` is similar, but the flag can be used more than once. If the flag is seen then ``_flag_n`` and ``_flag_name`` will be set with the values associated with each occurrence. Each value will be the value given to the option, or the empty string if no value was given.
- ``name=+`` means that only ``--name`` is valid. It requires a value and can be used more than once. If the flag is seen then ``_flag_name`` will be set with the values associated with each occurrence.

View File

@@ -6,16 +6,17 @@ Synopsis
.. synopsis::
set
set (-f | --function) (-l | --local) (-g | --global) (-U | --universal) [--no-event]
set [-Uflg] NAME [VALUE ...]
set [-Uflg] NAME[[INDEX ...]] [VALUE ...]
set (-x | --export) (-u | --unexport) [-Uflg] NAME [VALUE ...]
set (-a | --append) (-p | --prepend) [-Uflg] NAME VALUE ...
set (-e | --erase) [-Uflg] [-xu] [NAME][[INDEX]] ...]
set (-q | --query) [-Uflg] [-xu] [NAME][[INDEX]] ...]
set [(-f | --function) (-l | --local) (-g | --global) (-U | --universal)]
[(-x | --export) (-u | --unexport)]
set (-S | --show) (-L | --long) [NAME ...]
set [-Uflg] [-xu] [--no-event] NAME [VALUE ...]
set [-Uflg] [--no-event] NAME[[INDEX ...]] [VALUE ...]
set (-a | --append) (-p | --prepend) [-Uflg] [--no-event] NAME VALUE ...
set (-e | --erase) [-Uflg] [--no-event] NAME[[INDEX]] ...
set (-q | --query) [-Uflg] [-xu] NAME[[INDEX]] ...
Description
-----------

View File

@@ -54,7 +54,7 @@ It also provides a large number of program specific scripted completions. Most o
You can also write your own completions or install some you got from someone else. For that, see :doc:`Writing your own completions <completions>`.
Completion scripts are loaded on demand, like :ref:`functions are <syntax-function-autoloading>`. The difference is the ``$fish_complete_path`` :ref:`list <variables-lists>` is used instead of ``$fish_function_path``. Typically you can drop new completions in ~/.config/fish/completions/name-of-command.fish and fish will find them automatically.
Completion scripts are loaded on demand, like :ref:`functions are <syntax-function-autoloading>`. The difference is the ``$fish_complete_path`` :ref:`list <variables-lists>` is used instead of ``$fish_function_path``. Typically you can drop new completions in ``~/.config/fish/completions/<name-of-command>.fish`` and fish will find them automatically.
.. _syntax-highlighting:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,19 @@ function __dnf_list_installed_packages
dnf repoquery --cacheonly "$cur*" --qf "%{name}\n" --installed </dev/null
end
function __dnf_list_copr_repos
set -l copr_repos (dnf copr list)
switch $argv[1]
case enable
string replace -f -- " (disabled)" "" $copr_repos
case disable
string match -v -- "*(disabled)*" $copr_repos
case '*'
string replace -- " (disabled)" "" $copr_repos
end
end
function __dnf_list_available_packages
set -l tok (commandline -ct | string collect)
set -l files (__fish_complete_suffix .rpm)
@@ -86,6 +99,20 @@ complete -c dnf -n "__fish_seen_subcommand_from clean" -xa metadata -d "Removes
complete -c dnf -n "__fish_seen_subcommand_from clean" -xa packages -d "Removes any cached packages"
complete -c dnf -n "__fish_seen_subcommand_from clean" -xa all -d "Removes all cache"
# Copr
set -l coprcommands list enable disable remove debug
complete -c dnf -n __fish_use_subcommand -xa copr -d "Manage Copr repositories"
complete -c dnf -n "__fish_seen_subcommand_from copr; and not __fish_seen_subcommand_from $coprcommands" -xa list -d "List Copr repositories"
complete -c dnf -n "__fish_seen_subcommand_from copr; and not __fish_seen_subcommand_from $coprcommands" -xa enable -d "Install a Copr repository"
complete -c dnf -n "__fish_seen_subcommand_from copr; and not __fish_seen_subcommand_from $coprcommands" -xa disable -d "Disable a Copr repository"
complete -c dnf -n "__fish_seen_subcommand_from copr; and not __fish_seen_subcommand_from $coprcommands" -xa remove -d "Remove a Copr repository"
complete -c dnf -n "__fish_seen_subcommand_from copr; and not __fish_seen_subcommand_from $coprcommands" -xa debug -d "Print system info for debugging"
complete -c dnf -n "__fish_seen_subcommand_from copr; and not __fish_seen_subcommand_from $coprcommands" -l hub -d "Copr hub hostname"
for i in enable disable remove
complete -c dnf -n "__fish_seen_subcommand_from copr; and __fish_seen_subcommand_from $i" -xa "(__dnf_list_copr_repos $i)"
end
# Distro-sync
complete -c dnf -n __fish_use_subcommand -xa distro-sync -d "Synchronizes packages to match the latest"

View File

@@ -2161,6 +2161,7 @@ complete -x -c git -n '__fish_git_using_command push' -l exec -d 'Same as --rece
### rebase
complete -f -c git -n __fish_git_needs_command -a rebase -d 'Reapply commit sequence on a new base'
__fish_git_add_revision_completion -n '__fish_git_using_command rebase'
complete -f -c git -n '__fish_git_using_command rebase' -n 'string match -rq -- "^-i|^--interactive" (commandline -xpc)' -ka '(__fish_git_recent_commits)'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l continue -d 'Restart the rebasing process'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l abort -d 'Abort the rebase operation'
complete -f -c git -n '__fish_git_using_command rebase' -n __fish_git_is_rebasing -l edit-todo -d 'Edit the todo list'
@@ -2969,11 +2970,24 @@ complete -f -c git -n '__fish_git_using_command update-ref' -l create-reflog -d
complete -f -c git -n '__fish_git_using_command update-ref' -l stdin -d 'Read instructions from stdin'
complete -f -c git -n '__fish_git_using_command update-ref' -s z -d 'NUL-terminated format for stdin'
### verify-commit
complete -f -c git -n __fish_git_needs_command -a verify-commit -d 'Check the GPG signature of commits'
complete -f -c git -n '__fish_git_using_command verify-commit' -ka '(__fish_git_commits)'
complete -f -c git -n '__fish_git_using_command verify-commit' -s v -l verbose -d 'Print commit contents'
complete -f -c git -n '__fish_git_using_command verify-commit' -l raw -d 'Print raw gpg status output'
### verify-pack
complete -f -c git -n __fish_git_needs_command -a verify-pack -d 'Validate packed Git archive files'
complete -f -c git -n '__fish_git_using_command verify-pack' -s v -l verbose -d 'Show objects contained in pack'
complete -f -c git -n '__fish_git_using_command verify-pack' -s s -l stat-only -d 'Only show histogram of delta chain length'
### verify-tag
complete -f -c git -n __fish_git_needs_command -a verify-tag -d 'Check the GPG signature of tags'
complete -f -c git -n '__fish_git_using_command verify-tag' -ka '(__fish_git_tags)'
complete -f -c git -n '__fish_git_using_command verify-tag' -s v -l verbose -d 'Print tag contents'
complete -f -c git -n '__fish_git_using_command verify-tag' -l raw -d 'Print raw gpg status output'
complete -x -c git -n '__fish_git_using_command verify-tag' -l format -d 'Format to use for the output'
### write-tree
complete -f -c git -n __fish_git_needs_command -a write-tree -d 'Create a tree object from the current index'
complete -f -c git -n '__fish_git_using_command write-tree' -l missing-ok -d 'Allow missing objects'

View File

@@ -1,44 +1 @@
# Commands
complete -c ngrok -f -a authtoken -d "Save authtoken to configuration file"
complete -c ngrok -f -a credits -d "Prints author and licensing information"
complete -c ngrok -f -a http -d "Start an HTTP tunnel"
complete -c ngrok -f -a start -d "Start tunnels by name from the configuration file"
complete -c ngrok -f -a tcp -d "Start a TCP tunnel"
complete -c ngrok -f -a tls -d "Start a TLS tunnel"
complete -c ngrok -f -a update -d "Update ngrok to the latest version"
complete -c ngrok -f -a version -d "Print the version string"
complete -c ngrok -f -a help -d "Shows a list of commands or help for one command"
# General Options
complete -c ngrok -l help -e -f
complete -c ngrok -l authtoken -r -d "ngrok.com authtoken identifying a user"
complete -c ngrok -l config -r -d "path to config files; they are merged if multiple"
complete -c ngrok -l log -x -a "false stderr stdout" -d "path to log file, 'stdout', 'stderr' or 'false'"
complete -c ngrok -l log-format -x -a "term logfmt json" -d "log record format: 'term', 'logfmt', 'json'"
complete -c ngrok -l log-level -r -a info -d "logging level"
complete -c ngrok -l region -x -a "us eu au ap" -d "ngrok server region [us , eu, au, ap] (default: us)"
# http & tls's options
complete -c ngrok -l hostname -r -d "host tunnel on custom hostname (requires DNS CNAME)"
complete -c ngrok -l subdomain -r -d "host tunnel on a custom subdomain"
# http's options
complete -c ngrok -l auth -r -d "enforce basic auth on tunnel endpoint, 'user:password'"
complete -c ngrok -l bind-tls -x -a "both https http" -d "listen for http, https or both: true/false/both"
complete -c ngrok -l host-header -r -d "set Host header; if 'rewrite' use local address hostname"
complete -c ngrok -l inspect -d "enable/disable http introspection"
# tls's options
complete -c ngrok -l client-cas -r -d "path to TLS certificate authority to verify client certs"
complete -c ngrok -l crt -r -d "path to a TLS certificate for TLS termination"
complete -c ngrok -l key -r -d "path to a TLS key for TLS termination"
# start's options
complete -c ngrok -l all -d "start all tunnels in the configuration file"
complete -c ngrok -l none -d "start running no tunnels"
# tcp's options
complete -c ngrok -l remote-addr -r -d "bind remote address (requires you reserve an address)"
# update's options
complete -c ngrok -l channel -x -a "stable beta" -d "update channel (stable, beta)"
SHELL=/bin/fish ngrok completion 2>/dev/null | source

View File

@@ -0,0 +1,10 @@
# localization: skip(private)
function __fish_cygwin_noacl
# MSYS (default) and Cygwin (non-default) mounts may not support POSIX permissions.
__fish_is_cygwin
and {
mount |
string match "*on $(stat -c %m -- $argv[1]) *" |
string match -qr "[(,]noacl[),]"
}
end

View File

@@ -0,0 +1,4 @@
# localization: skip(private)
function __fish_is_cygwin
__fish_uname | string match -qr "^(MSYS|CYGWIN)"
end

View File

@@ -9,6 +9,21 @@ function __fish_make_cache_dir --description "Create and return XDG_CACHE_HOME"
# So if you call `__fish_make_cache_dir completions`,
# this creates e.g. ~/.cache/fish/completions
if not path is -d $xdg_cache_home/fish/"$argv[1]"
mkdir -m 700 -p $xdg_cache_home/fish/"$argv[1]"
set -l mkdir_options -m 700
# Can't set the permission in Cygwin on a `noacl` mount
if __fish_is_cygwin
# Find the first existing parent so we can `stat` it and get its mountpoint
set -l existing_parent $xdg_cache_home/fish/"$argv[1]"
while not path is -d $existing_parent
set existing_parent (path dirname $existing_parent)
end
if __fish_cygwin_noacl "$existing_parent"
set mkdir_options
end
end
mkdir $mkdir_options -p $xdg_cache_home/fish/"$argv[1]"
end; and echo $xdg_cache_home/fish/"$argv[1]"
end

View File

@@ -129,7 +129,9 @@ function fish_vi_exec_motion
set motion_cmd commandline -f $motion
end
switch $motion[1]
case forward-char backward-char
case forward-char
set -e seq_total[1]
case backward-char
$motion_cmd
set -e seq_total[1]
end

View File

@@ -10,7 +10,10 @@ function isatty -d "Tests if a file descriptor is a tty"
end
if set -q argv[2]
printf (_ "%s: Too many arguments") isatty >&2
{
printf (_ "%s: Too many arguments") isatty
echo
} >&2
return 1
end

View File

@@ -14,7 +14,10 @@ function psub --description "Read from stdin into a file and output the filename
set -l funcname
if not status --is-command-substitution
printf (_ "%s: Not inside of command substitution") psub >&2
{
printf (_ "%s: Not inside of command substitution") psub
echo
} >&2
return 1
end

View File

@@ -15,7 +15,10 @@ function setenv
# `setenv` accepts only two arguments: the var name and the value. If there are more than two
# args it is an error. The error message is verbatim from csh.
if set -q argv[3]
printf (_ '%s: Too many arguments\n') setenv >&2
{
printf (_ '%s: Too many arguments') setenv
echo
} >&2
return 1
end

View File

@@ -182,7 +182,10 @@ function umask --description "Set default file permission mask"
return 1
case '*'
printf (_ '%s: Too many arguments\n') umask >&2
{
printf (_ '%s: Too many arguments') umask
echo
} >&2
return 1
end
end

View File

@@ -501,6 +501,19 @@ class Deroffer:
return True
return False
def device_control(self):
# groff \X'...' device control escape (and \Z'...' zero-width).
# help2man 1.50+ uses \X'tty: link URL' for hyperlinks.
# We just skip the entire escape.
if self.str_at(1) in "XZ" and self.str_at(2) == "'":
self.skip_char(3)
while self.str_at(0) and self.str_at(0) != "'":
self.skip_char()
if self.str_at(0) == "'":
self.skip_char()
return True
return False
def var(self):
reg = ""
s0s1 = self.s[0:2]
@@ -650,6 +663,8 @@ class Deroffer:
return self.size()
elif c in "hvwud":
return self.numreq()
elif c in "XZ":
return self.device_control()
elif c in "n*":
return self.var()
elif c == "(":
@@ -1314,6 +1329,9 @@ def built_command(options, description):
def remove_groff_formatting(data):
# Strip groff \X'...' device control escapes (help2man 1.50+ hyperlinks)
# and \Z'...' zero-width escapes.
data = re.sub(r"\\[XZ]'[^']*'", "", data)
data = data.replace("\\fI", "")
data = data.replace("\\fP", "")
data = data.replace("\\f1", "")

View File

@@ -207,7 +207,7 @@ tt {
border-top-left-radius: 5;
border-bottom-left-radius: 5;
/* Pad one less than .master_element, to accomodate our border. */
/* Pad one less than .master_element, to accommodate our border. */
padding-top: 5px;
padding-bottom: 10px;
padding-left: 4px;

View File

@@ -9,19 +9,21 @@
*
* Most clients will be interested in visiting the nodes of an ast.
*/
use crate::common::{UnescapeStringStyle, unescape_string};
use crate::flog::{flog, flogf};
use crate::parse_constants::{
ERROR_BAD_COMMAND_ASSIGN_ERR_MSG, INVALID_PIPELINE_CMD_ERR_MSG, ParseError, ParseErrorCode,
ParseErrorList, ParseKeyword, ParseTokenType, ParseTreeFlags, SOURCE_OFFSET_INVALID,
SourceRange, StatementDecoration, token_type_user_presentable_description,
};
use crate::parse_tree::ParseToken;
use crate::prelude::*;
use crate::tokenizer::{
TOK_ACCEPT_UNFINISHED, TOK_ARGUMENT_LIST, TOK_CONTINUE_AFTER_ERROR, TOK_SHOW_COMMENTS,
TokFlags, TokenType, Tokenizer, TokenizerError, variable_assignment_equals_pos,
use crate::{
flog::{flog, flogf},
parse_constants::{
ERROR_BAD_COMMAND_ASSIGN_ERR_MSG, INVALID_PIPELINE_CMD_ERR_MSG, ParseError, ParseErrorCode,
ParseErrorList, ParseKeyword, ParseTokenType, ParseTreeFlags, SOURCE_OFFSET_INVALID,
SourceRange, StatementDecoration, token_type_user_presentable_description,
},
parse_tree::ParseToken,
prelude::*,
tokenizer::{
TOK_ACCEPT_UNFINISHED, TOK_ARGUMENT_LIST, TOK_CONTINUE_AFTER_ERROR, TOK_SHOW_COMMENTS,
TokFlags, TokenType, Tokenizer, TokenizerError, variable_assignment_equals_pos,
},
};
use fish_common::{UnescapeStringStyle, unescape_string};
use macro_rules_attribute::derive;
use std::borrow::Cow;
use std::convert::AsMut;

View File

@@ -1,13 +1,14 @@
//! The classes responsible for autoloading functions and completions.
use crate::common::{ScopeGuard, escape};
use crate::env::Environment;
use crate::flogf;
use crate::io::IoChain;
use crate::parser::Parser;
use crate::wutil::{FileId, INVALID_FILE_ID, file_id_for_path};
use fish_wcstringutil::wcs2bytes;
use fish_widestring::{L, WExt as _, WString, wstr};
use crate::{
env::Environment,
flogf,
io::IoChain,
parser::Parser,
wutil::{FileId, INVALID_FILE_ID, file_id_for_path},
};
use fish_common::{ScopeGuard, escape};
use fish_widestring::{L, WExt as _, WString, wcs2bytes, wstr};
use lru::LruCache;
use rust_embed::RustEmbed;
use std::collections::{HashMap, HashSet};
@@ -133,7 +134,7 @@ pub fn perform_autoload(path: &AutoloadPath, parser: &Parser) {
parser.eval(&script_source, &IoChain::new());
}
AutoloadPath::Embedded(name) => {
use crate::common::bytes2wcstring;
use fish_widestring::bytes2wcstring;
use std::sync::Arc;
flogf!(autoload, "Loading embedded: %s", name);
let emfile = Asset::get(name).expect("Embedded file not found");
@@ -464,7 +465,7 @@ mod tests {
fn test_autoload() {
let _cleanup = test_init();
use crate::fds::wopen_cloexec;
use fish_wcstringutil::wcs2zstring;
use fish_widestring::wcs2zstring;
use nix::fcntl::OFlag;
macro_rules! run {

View File

@@ -20,27 +20,22 @@
use fish::{
ast,
builtins::{
error::Error,
fish_indent, fish_key_reader,
shared::{
BUILTIN_ERR_MISSING, BUILTIN_ERR_UNEXP_ARG, BUILTIN_ERR_UNKNOWN, STATUS_CMD_ERROR,
STATUS_CMD_OK, STATUS_CMD_UNKNOWN, VERSION_STRING_TEMPLATE,
},
},
common::{
PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME, bytes2wcstring, escape, osstr2wcstring,
save_term_foreground_process_group,
shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_CMD_UNKNOWN, VERSION_STRING_TEMPLATE},
},
common::{PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME},
env::{
EnvMode, Statuses,
config_paths::ConfigPaths,
environment::{EnvStack, Environment as _, env_init},
},
eprintf,
eprintf, err_fmt,
event::{self, Event},
flog::{self, activate_flog_categories_by_pattern, flog, flogf, set_flog_file_fd},
fprintf, function, future_feature_flags as features,
fprintf, function,
history::{self, start_private_mode},
io::IoChain,
io::{FdOutputStream, IoChain, OutputStream},
locale::set_libc_locales,
nix::isatty,
panic::panic_handler,
@@ -55,25 +50,28 @@
Pid, get_login, is_interactive_session, mark_login, mark_no_exec, proc_init,
set_interactive_session,
},
reader::{reader_init, reader_read, term_copy_modes},
reader::{reader_exit_signal, reader_init, reader_read, term_copy_modes},
signal::{signal_clear_cancel, signal_unblock_all},
threads::{self},
topic_monitor,
wutil::waccess,
};
use fish_wcstringutil::wcs2bytes;
use libc::STDIN_FILENO;
use fish_common::{escape, save_term_foreground_process_group};
use fish_widestring::{bytes2wcstring, osstr2wcstring, wcs2bytes};
use libc::{STDERR_FILENO, STDIN_FILENO};
use nix::{
sys::resource::{UsageWho, getrusage},
unistd::{AccessFlags, getpid},
};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::os::unix::prelude::*;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::{env, ops::ControlFlow};
use std::{
env,
ffi::{OsStr, OsString},
fs::File,
ops::ControlFlow,
os::unix::prelude::*,
path::Path,
sync::{Arc, atomic::Ordering},
};
/// container to hold the options specified within the command line
#[derive(Default, Debug)]
@@ -318,24 +316,24 @@ fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i
// Either remove it or make it work with flog.
}
'?' => {
eprintf!(
"%s\n\n",
wgettext_fmt!(BUILTIN_ERR_UNKNOWN, "fish", args[w.wopt_index - 1])
);
err_fmt!(Error::UNKNOWN_OPT, args[w.wopt_index - 1])
.cmd(L!("fish"))
.append_to_msg('\n')
.write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
return ControlFlow::Break(1);
}
':' => {
eprintf!(
"%s\n\n",
wgettext_fmt!(BUILTIN_ERR_MISSING, "fish", args[w.wopt_index - 1])
);
err_fmt!(Error::MISSING_OPT_ARG, args[w.wopt_index - 1])
.cmd(L!("fish"))
.append_to_msg('\n')
.write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
return ControlFlow::Break(1);
}
';' => {
eprintf!(
"%s\n\n",
wgettext_fmt!(BUILTIN_ERR_UNEXP_ARG, "fish", args[w.wopt_index - 1])
);
err_fmt!(Error::UNEXP_OPT_ARG, args[w.wopt_index - 1])
.cmd(L!("fish"))
.append_to_msg('\n')
.write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
return ControlFlow::Break(1);
}
_ => panic!("unexpected retval from WGetopter"),
@@ -482,10 +480,10 @@ fn throwing_main() -> i32 {
// command line takes precedence).
if let Some(features_var) = EnvStack::globals().get(L!("fish_features")) {
for s in features_var.as_list() {
features::set_from_string(s.as_utfstr());
fish_feature_flags::set_from_string(s.as_utfstr());
}
}
features::set_from_string(opts.features.as_utfstr());
fish_feature_flags::set_from_string(opts.features.as_utfstr());
proc_init();
reader_init(true);
@@ -627,6 +625,16 @@ fn throwing_main() -> i32 {
}
history::save_all();
// If we deferred a fatal signal, re-raise it now so the parent sees WIFSIGNALED.
let exit_sig = reader_exit_signal();
if exit_sig != 0 {
unsafe {
libc::signal(exit_sig, libc::SIG_DFL);
libc::raise(exit_sig);
}
}
if opts.print_rusage_self {
print_rusage_self();
}

View File

@@ -1,19 +1,24 @@
use super::prelude::*;
use crate::abbrs::{self, Abbreviation, Position};
use crate::common::{EscapeStringStyle, bytes2wcstring, escape, escape_string, valid_func_name};
use crate::env::{EnvMode, EnvStackSetResult};
use crate::highlight::highlight_and_colorize;
use crate::parser::ParserEnvSetMode;
use crate::re::{regex_make_anchored, to_boxed_chars};
use fish_common::help_section;
use crate::{
abbrs::{self, Abbreviation, Position},
builtins::error::Error,
common::valid_func_name,
env::{EnvMode, EnvStackSetResult},
err_fmt, err_str,
highlight::highlight_and_colorize,
parser::ParserEnvSetMode,
re::{regex_make_anchored, to_boxed_chars},
};
use fish_common::{EscapeStringStyle, escape, escape_string, help_section};
use fish_widestring::bytes2wcstring;
use pcre2::utf32::{Regex, RegexBuilder};
localizable_consts! {
NAME_CANNOT_BE_EMPTY
"%s %s: Name cannot be empty"
"Name cannot be empty"
ABBR_CANNOT_HAVE_SPACES
"%s %s: Abbreviation '%s' cannot have spaces in the word"
"Abbreviation '%s' cannot have spaces in the word"
}
const CMD: &wstr = L!("abbr");
@@ -36,7 +41,7 @@ struct Options {
}
impl Options {
fn validate(&mut self, streams: &mut IoStreams) -> bool {
fn validate(&mut self) -> Option<Error<'_>> {
// Duplicate options?
let mut cmds = vec![];
if self.add {
@@ -59,12 +64,7 @@ fn validate(&mut self, streams: &mut IoStreams) -> bool {
}
if cmds.len() > 1 {
streams.err.appendln(&wgettext_fmt!(
"%s: Cannot combine options %s",
CMD,
join(&cmds, L!(", "))
));
return false;
return Some(err_fmt!("Cannot combine options %s", join(&cmds, L!(", "))));
}
// If run with no options, treat it like --add if we have arguments,
@@ -76,54 +76,29 @@ fn validate(&mut self, streams: &mut IoStreams) -> bool {
localizable_consts! {
OPTION_REQUIRES_ARG
"%s: %s option requires %s"
"%s option requires %s"
}
if !self.add && self.position.is_some() {
streams.err.appendln(&wgettext_fmt!(
OPTION_REQUIRES_ARG,
CMD,
"--position",
"--add",
));
return false;
return Some(err_fmt!(OPTION_REQUIRES_ARG, "--position", "--add"));
}
if !self.add && self.regex_pattern.is_some() {
streams
.err
.appendln(&wgettext_fmt!(OPTION_REQUIRES_ARG, CMD, "--regex", "--add"));
return false;
return Some(err_fmt!(OPTION_REQUIRES_ARG, "--regex", "--add"));
}
if !self.add && self.function.is_some() {
streams.err.appendln(&wgettext_fmt!(
OPTION_REQUIRES_ARG,
CMD,
"--function",
"--add",
));
return false;
return Some(err_fmt!(OPTION_REQUIRES_ARG, "--function", "--add"));
}
if !self.add && self.set_cursor_marker.is_some() {
streams.err.appendln(&wgettext_fmt!(
OPTION_REQUIRES_ARG,
CMD,
"--set-cursor",
"--add",
));
return false;
return Some(err_fmt!(OPTION_REQUIRES_ARG, "--set-cursor", "--add"));
}
if self
.set_cursor_marker
.as_ref()
.is_some_and(|m| m.is_empty())
{
streams.err.appendln(&wgettext_fmt!(
"%s: --set-cursor argument cannot be empty",
CMD
));
return false;
return Some(err_str!("--set-cursor argument cannot be empty"));
}
true
None
}
}
@@ -213,12 +188,9 @@ fn abbr_show(opts: &Options, streams: &mut IoStreams, parser: &Parser) -> Builti
fn abbr_list(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
let subcmd = L!("--list");
if !opts.args.is_empty() {
streams.err.appendln(&wgettext_fmt!(
"%s %s: Unexpected argument -- '%s'",
CMD,
subcmd,
&opts.args[0]
));
err_fmt!("Unexpected argument -- '%s'", &opts.args[0])
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
abbrs::with_abbrs(|abbrs| {
@@ -237,29 +209,24 @@ fn abbr_rename(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
let subcmd = L!("--rename");
if opts.args.len() != 2 {
streams.err.appendln(&wgettext_fmt!(
"%s %s: Requires exactly two arguments",
CMD,
subcmd
));
err_str!("Requires exactly two arguments")
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let old_name = &opts.args[0];
let new_name = &opts.args[1];
if old_name.is_empty() || new_name.is_empty() {
streams
.err
.appendln(&wgettext_fmt!(NAME_CANNOT_BE_EMPTY, CMD, subcmd));
err_str!(NAME_CANNOT_BE_EMPTY)
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if contains_whitespace(new_name) {
streams.err.appendln(&wgettext_fmt!(
ABBR_CANNOT_HAVE_SPACES,
CMD,
subcmd,
new_name.as_utfstr()
));
err_fmt!(ABBR_CANNOT_HAVE_SPACES, new_name.as_utfstr())
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
abbrs::with_abbrs_mut(|abbrs| -> BuiltinResult {
@@ -268,12 +235,12 @@ fn abbr_rename(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
.iter()
.any(|a| a.name == *old_name && a.commands == opts.commands)
{
streams.err.appendln(&wgettext_fmt!(
"%s %s: No abbreviation named %s with the specified command restrictions",
CMD,
subcmd,
err_fmt!(
"No abbreviation named %s with the specified command restrictions",
old_name.as_utfstr()
));
)
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
if abbrs
@@ -282,13 +249,13 @@ fn abbr_rename(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
.any(|a| a.name == *new_name && a.commands == opts.commands)
{
if opts.commands.is_empty() {
streams.err.appendln(&wgettext_fmt!(
"%s %s: Abbreviation %s already exists, cannot rename %s",
CMD,
subcmd,
err_fmt!(
"Abbreviation %s already exists, cannot rename %s",
new_name.as_utfstr(),
old_name.as_utfstr()
));
)
.subcmd(CMD, subcmd)
.finish(streams);
} else {
let style = EscapeStringStyle::Script(Default::default());
let mut cmd_list = WString::new();
@@ -299,14 +266,14 @@ fn abbr_rename(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
cmd_list.push_utfstr(&escape_string(cmd, style));
}
streams.err.appendln(&wgettext_fmt!(
"%s %s: Abbreviation %s already exists for commands %s, cannot rename %s",
CMD,
subcmd,
err_fmt!(
"Abbreviation %s already exists for commands %s, cannot rename %s",
new_name.as_utfstr(),
cmd_list.as_utfstr(),
old_name.as_utfstr()
));
)
.subcmd(CMD, subcmd)
.finish(streams);
}
return Err(STATUS_INVALID_ARGS);
@@ -339,28 +306,23 @@ fn abbr_add(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
let subcmd = L!("--add");
if opts.args.len() < 2 && opts.function.is_none() {
streams.err.appendln(&wgettext_fmt!(
"%s %s: Requires at least two arguments",
CMD,
subcmd
));
err_str!("Requires at least two arguments")
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if opts.args.is_empty() || opts.args[0].is_empty() {
streams
.err
.appendln(&wgettext_fmt!(NAME_CANNOT_BE_EMPTY, CMD, subcmd));
err_str!(NAME_CANNOT_BE_EMPTY)
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let name = &opts.args[0];
if name.chars().any(|c| c.is_whitespace()) {
streams.err.appendln(&wgettext_fmt!(
ABBR_CANNOT_HAVE_SPACES,
CMD,
subcmd,
name.as_utfstr()
));
err_fmt!(ABBR_CANNOT_HAVE_SPACES, name.as_utfstr())
.subcmd(CMD, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -376,21 +338,16 @@ fn abbr_add(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
let result = builder.build(to_boxed_chars(regex_pattern));
if let Err(error) = result {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_REGEX_COMPILE,
CMD,
error.error_message(),
));
let mut err = err_fmt!(Error::REGEX_COMPILE, error.error_message());
if let Some(offset) = error.offset() {
streams
.err
.append(&sprintf!("%s: %s\n", CMD, regex_pattern.as_utfstr()));
err.append_assign_to_msg(&sprintf!("\n%s: %s", CMD, regex_pattern.as_utfstr()));
// TODO: This is misaligned if `regex_pattern` contains characters which are not
// exactly 1 terminal cell wide.
// exactly 1 terminal cell wide or not on a single line.
let mut marker = " ".repeat(offset.saturating_sub(1));
marker.push('^');
streams.err.append(&sprintf!("%s: %s\n", CMD, marker));
err.append_assign_to_msg(&sprintf!("\n%s: %s", CMD, marker));
}
err.cmd(CMD).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let anchored = regex_make_anchored(regex_pattern);
@@ -409,20 +366,16 @@ fn abbr_add(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
}
if opts.function.is_some() && opts.args.len() > 1 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, L!("abbr")));
err_str!(Error::TOO_MANY_ARGUMENTS).cmd(CMD).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let replacement = if let Some(ref function) = opts.function {
// Abbreviation function names disallow spaces.
// This is to prevent accidental usage of e.g. `--function 'string replace'`
if !valid_func_name(function) || contains_whitespace(function) {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid function name: %s",
CMD,
function.as_utfstr()
));
err_fmt!("Invalid function name: %s", function.as_utfstr())
.cmd(CMD)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
function.clone()
@@ -446,10 +399,9 @@ fn abbr_add(opts: &Options, streams: &mut IoStreams) -> BuiltinResult {
}
});
if !opts.commands.is_empty() && position == Position::Command {
streams.err.appendln(&wgettext_fmt!(
"%s: --command cannot be combined with --position=command",
CMD,
));
err_str!("--command cannot be combined with --position=command")
.cmd(CMD)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -564,9 +516,9 @@ pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
'c' => opts.commands.push(w.woptarg.map(|x| x.to_owned()).unwrap()),
'p' => {
if opts.position.is_some() {
streams
.err
.appendln(&wgettext_fmt!("%s: Cannot specify multiple positions", CMD));
err_str!("Cannot specify multiple positions")
.cmd(CMD)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if w.woptarg == Some(L!("command")) {
@@ -574,37 +526,34 @@ pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
} else if w.woptarg == Some(L!("anywhere")) {
opts.position = Some(Position::Anywhere);
} else {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid position '%s'",
CMD,
w.woptarg.unwrap_or_default()
));
streams.err.appendln(&wgettext_fmt!(
"Position must be one of: %s",
// Use a single argument here to avoid having to update translations when
// the number of options changes.
"command, anywhere",
));
err_fmt!("Invalid position '%s'", w.woptarg.unwrap_or_default())
.append_to_msg('\n')
.append_to_msg(&wgettext_fmt!(
"Position must be one of: %s",
// Use a single argument here to avoid having to update translations when
// the number of options changes.
"command, anywhere",
))
.cmd(CMD)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
'r' => {
if opts.regex_pattern.is_some() {
streams.err.appendln(&wgettext_fmt!(
"%s: Cannot specify multiple regex patterns",
CMD
));
err_str!("Cannot specify multiple regex patterns")
.cmd(CMD)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
opts.regex_pattern = w.woptarg.map(ToOwned::to_owned);
}
SET_CURSOR_SHORT => {
if opts.set_cursor_marker.is_some() {
streams.err.appendln(&wgettext_fmt!(
"%s: Cannot specify multiple set-cursor options",
CMD
));
err_str!("Cannot specify multiple set-cursor options")
.cmd(CMD)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// The default set-cursor indicator is '%'.
@@ -624,19 +573,20 @@ pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
'U' => {
// Kept and made ineffective, so we warn.
streams.err.append(&wgettext_fmt!(
"%s: Warning: Option '%s' was removed and is now ignored",
cmd,
err_fmt!(
"Warning: Option '%s' was removed and is now ignored",
argv_read[w.wopt_index - 1]
));
builtin_print_error_trailer(parser, streams.err, cmd);
)
.cmd(CMD)
.full_trailer(parser)
.finish(streams);
}
'h' => {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -660,7 +610,8 @@ pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
opts.args.push((*arg).into());
}
if !opts.validate(streams) {
if let Some(err) = opts.validate() {
err.cmd(cmd).finish(streams);
return Err(STATUS_INVALID_ARGS);
}

View File

@@ -2,19 +2,21 @@
use super::prelude::*;
use crate::builtins::error::Error;
use crate::env::{EnvMode, EnvSetMode, EnvStack};
use crate::exec::exec_subshell;
use crate::parser::ParserEnvSetMode;
use crate::wutil::fish_iswalnum;
use crate::{err_fmt, err_str};
const VAR_NAME_PREFIX: &wstr = L!("_flag_");
localizable_consts!(
BUILTIN_ERR_INVALID_OPT_SPEC
"%s: Invalid option spec '%s' at char '%c'"
"Invalid option spec '%s' at char '%c'"
MISSING_DOUBLE_HYPHEN_SEPARATOR
"%s: Missing -- separator"
"Missing -- separator"
);
#[derive(Default)]
@@ -149,13 +151,10 @@ fn check_for_mutually_exclusive_flags(
if flag1 > flag2 {
std::mem::swap(&mut flag1, &mut flag2);
}
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
opts.name,
flag1,
flag2
));
return Err(STATUS_CMD_ERROR);
err_fmt!(Error::COMBO_EXCLUSIVE, flag1, flag2)
.cmd(&opts.name)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
}
@@ -171,11 +170,9 @@ fn parse_exclusive_args(opts: &mut ArgParseCmdOpts, streams: &mut IoStreams) ->
for raw_xflags in &opts.raw_exclusive_flags {
let xflags: Vec<_> = raw_xflags.split(',').collect();
if xflags.len() < 2 {
streams.err.appendln(&wgettext_fmt!(
"%s: exclusive flag string '%s' is not valid",
opts.name,
raw_xflags
));
err_fmt!("exclusive flag string '%s' is not valid", raw_xflags)
.cmd(&opts.name)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
@@ -189,11 +186,9 @@ fn parse_exclusive_args(opts: &mut ArgParseCmdOpts, streams: &mut IoStreams) ->
// It's a long flag we store as its short flag equivalent.
exclusive_set.push(*short_equiv);
} else {
streams.err.appendln(&wgettext_fmt!(
"%s: exclusive flag '%s' is not valid",
opts.name,
flag
));
err_fmt!("exclusive flag '%s' is not valid", flag)
.cmd(&opts.name)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
}
@@ -218,12 +213,13 @@ fn parse_flag_modifiers<'args>(
&& s.char_at(0) != '!'
&& s.char_at(0) != '&'
{
streams.err.appendln(&wgettext_fmt!(
"%s: Implicit int short flag '%c' does not allow modifiers like '%c'",
opts.name,
err_fmt!(
"Implicit int short flag '%c' does not allow modifiers like '%c'",
opt_spec.short_flag,
s.char_at(0)
));
)
.cmd(&opts.name)
.finish(streams);
return false;
}
@@ -253,24 +249,18 @@ fn parse_flag_modifiers<'args>(
if s.char_at(0) == '!' {
if opt_spec.arg_type == ArgType::NoArgument {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_INVALID_OPT_SPEC,
opts.name,
option_spec,
s.char_at(0)
));
err_fmt!(BUILTIN_ERR_INVALID_OPT_SPEC, option_spec, s.char_at(0))
.cmd(&opts.name)
.finish(streams);
}
s = s.slice_from(1);
opt_spec.validation_command = s;
// Move cursor to the end so we don't expect a long flag.
s = s.slice_from(s.char_count());
} else if !s.is_empty() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_INVALID_OPT_SPEC,
opts.name,
option_spec,
s.char_at(0)
));
err_fmt!(BUILTIN_ERR_INVALID_OPT_SPEC, option_spec, s.char_at(0))
.cmd(&opts.name)
.finish(streams);
return false;
}
@@ -280,11 +270,9 @@ fn parse_flag_modifiers<'args>(
}
if opts.options.contains_key(&opt_spec.short_flag) {
streams.err.appendln(&wgettext_fmt!(
"%s: Short flag '%c' already defined",
opts.name,
opt_spec.short_flag
));
err_fmt!("Short flag '%c' already defined", opt_spec.short_flag)
.cmd(&opts.name)
.finish(streams);
return false;
}
@@ -303,7 +291,7 @@ fn parse_option_spec_sep<'args>(
) -> bool {
localizable_consts! {
IMPLICIT_INT_FLAG_ALREADY_DEFINED
"%s: Implicit int flag '%c' already defined"
"Implicit int flag '%c' already defined"
}
let mut s = *opt_spec_str;
let mut i = 1usize;
@@ -316,11 +304,9 @@ fn parse_option_spec_sep<'args>(
*counter += 1;
}
if opts.implicit_int_flag != '\0' {
streams.err.appendln(&wgettext_fmt!(
IMPLICIT_INT_FLAG_ALREADY_DEFINED,
opts.name,
opts.implicit_int_flag
));
err_fmt!(IMPLICIT_INT_FLAG_ALREADY_DEFINED, opts.implicit_int_flag)
.cmd(&opts.name)
.finish(streams);
return false;
}
opts.implicit_int_flag = opt_spec.short_flag;
@@ -335,34 +321,26 @@ fn parse_option_spec_sep<'args>(
opt_spec.short_flag_valid = false;
i += 1;
if i == s.char_count() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_INVALID_OPT_SPEC,
opts.name,
option_spec,
s.char_at(i - 1)
));
err_fmt!(BUILTIN_ERR_INVALID_OPT_SPEC, option_spec, s.char_at(i - 1))
.cmd(&opts.name)
.finish(streams);
return false;
}
}
'/' => {
i += 1; // the struct is initialized assuming short_flag_valid should be true
if i == s.char_count() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_INVALID_OPT_SPEC,
opts.name,
option_spec,
s.char_at(i - 1)
));
err_fmt!(BUILTIN_ERR_INVALID_OPT_SPEC, option_spec, s.char_at(i - 1))
.cmd(&opts.name)
.finish(streams);
return false;
}
}
'#' => {
if opts.implicit_int_flag != '\0' {
streams.err.appendln(&wgettext_fmt!(
IMPLICIT_INT_FLAG_ALREADY_DEFINED,
opts.name,
opts.implicit_int_flag
));
err_fmt!(IMPLICIT_INT_FLAG_ALREADY_DEFINED, opts.implicit_int_flag)
.cmd(&opts.name)
.finish(streams);
return false;
}
opts.implicit_int_flag = opt_spec.short_flag;
@@ -402,21 +380,21 @@ fn parse_option_spec<'args>(
streams: &mut IoStreams,
) -> bool {
if option_spec.is_empty() {
streams.err.appendln(&wgettext_fmt!(
"%s: An option spec must have at least a short or a long flag",
opts.name
));
err_str!("An option spec must have at least a short or a long flag")
.cmd(&opts.name)
.finish(streams);
return false;
}
let mut s = option_spec;
if !fish_iswalnum(s.char_at(0)) && s.char_at(0) != '#' && !(s.char_at(0) == '/' && s.len() > 1)
{
streams.err.appendln(&wgettext_fmt!(
"%s: Short flag '%c' invalid, must be alphanum or '#'",
opts.name,
err_fmt!(
"Short flag '%c' invalid, must be alphanum or '#'",
s.char_at(0)
));
)
.cmd(&opts.name)
.finish(streams);
return false;
}
@@ -439,11 +417,9 @@ fn parse_option_spec<'args>(
if long_flag_char_count > 0 {
opt_spec.long_flag = s.slice_to(long_flag_char_count);
if opts.long_to_short_flag.contains_key(opt_spec.long_flag) {
streams.err.appendln(&wgettext_fmt!(
"%s: Long flag '%s' already defined",
opts.name,
opt_spec.long_flag
));
err_fmt!("Long flag '%s' already defined", opt_spec.long_flag)
.cmd(&opts.name)
.finish(streams);
return false;
}
}
@@ -486,9 +462,9 @@ fn collect_option_specs<'args>(
loop {
if *optind == argc {
streams
.err
.appendln(&wgettext_fmt!(MISSING_DOUBLE_HYPHEN_SEPARATOR, cmd));
err_str!(MISSING_DOUBLE_HYPHEN_SEPARATOR)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -508,9 +484,9 @@ fn collect_option_specs<'args>(
let counter_max = 0xF8FFu32;
if counter > counter_max {
streams
.err
.appendln(&wgettext_fmt!("%s: Too many long-only options", cmd));
err_str!("Too many long-only options")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -540,12 +516,9 @@ fn parse_cmd_opts<'args>(
's' => opts.stop_nonopt = true,
'i' | 'u' => {
if opts.unknown_handling != UnknownHandling::Error {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--ignore-unknown",
"--move-unknown"
));
err_fmt!(Error::COMBO_EXCLUSIVE, "--ignore-unknown", "--move-unknown")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
opts.unknown_handling = if c == 'i' {
@@ -564,11 +537,9 @@ fn parse_cmd_opts<'args>(
} else if kind == L!("none") {
ArgType::NoArgument
} else {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid --unknown-arguments value '%s'",
cmd,
kind
));
err_fmt!("Invalid --unknown-arguments value '%s'", kind)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -581,11 +552,9 @@ fn parse_cmd_opts<'args>(
opts.min_args = {
let x = fish_wcstol(w.woptarg.unwrap()).unwrap_or(-1);
if x < 0 {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid --min-args value '%s'",
cmd,
w.woptarg.unwrap()
));
err_fmt!("Invalid --min-args value '%s'", w.woptarg.unwrap())
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
x.try_into().unwrap()
@@ -595,11 +564,9 @@ fn parse_cmd_opts<'args>(
opts.max_args = {
let x = fish_wcstol(w.woptarg.unwrap()).unwrap_or(-1);
if x < 0 {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid --max-args value '%s'",
cmd,
w.woptarg.unwrap()
));
err_fmt!("Invalid --max-args value '%s'", w.woptarg.unwrap())
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
x.try_into().unwrap()
@@ -610,6 +577,7 @@ fn parse_cmd_opts<'args>(
parser,
streams,
cmd,
None,
args[w.wopt_index - 1],
/* print_hints */ false,
);
@@ -648,9 +616,9 @@ fn parse_cmd_opts<'args>(
if argc == w.wopt_index {
// The user didn't specify any option specs.
streams
.err
.appendln(&wgettext_fmt!(MISSING_DOUBLE_HYPHEN_SEPARATOR, cmd));
err_str!(MISSING_DOUBLE_HYPHEN_SEPARATOR)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -932,6 +900,7 @@ fn argparse_parse_flags<'args>(
parser,
streams,
&opts.name,
None,
args_read[w.wopt_index - 1],
false,
);
@@ -961,11 +930,9 @@ fn argparse_parse_flags<'args>(
streams,
)?;
} else if opts.unknown_handling == UnknownHandling::Error {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_UNKNOWN,
opts.name,
args_read[w.wopt_index - 1]
));
err_fmt!(Error::UNKNOWN_OPT, args_read[w.wopt_index - 1])
.cmd(&opts.name)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
} else {
// The option is unknown, so there's no long opt index it could have used
@@ -1016,11 +983,9 @@ fn argparse_parse_flags<'args>(
Some(w.argv[w.wopt_index - 1])
} else {
// the option is at the end of argv, so it has no argument
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_MISSING,
opts.name,
args_read[w.wopt_index - 1]
));
err_fmt!(Error::MISSING_OPT_ARG, args_read[w.wopt_index - 1])
.cmd(&opts.name)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
} else {
@@ -1032,11 +997,9 @@ fn argparse_parse_flags<'args>(
&& is_long_flag
&& arg_contents.contains('=')
{
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_UNEXP_ARG,
opts.name,
args_read[w.wopt_index - 1]
));
err_fmt!(Error::UNEXP_OPT_ARG, args_read[w.wopt_index - 1])
.cmd(&opts.name)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -1129,22 +1092,16 @@ fn check_min_max_args_constraints(
let cmd = &opts.name;
if opts.args.len() < opts.min_args {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_MIN_ARG_COUNT1,
cmd,
opts.min_args,
opts.args.len()
));
err_fmt!(Error::MIN_ARG_COUNT, opts.min_args, opts.args.len())
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
if opts.max_args != usize::MAX && opts.args.len() > opts.max_args {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_MAX_ARG_COUNT1,
cmd,
opts.max_args,
opts.args.len()
));
err_fmt!(Error::MAX_ARG_COUNT, opts.max_args, opts.args.len())
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}

View File

@@ -2,7 +2,7 @@
use std::{collections::HashSet, rc::Rc};
use crate::proc::Pid;
use crate::{builtins::error::Error, err_fmt, err_str, proc::Pid};
use super::prelude::*;
@@ -17,12 +17,13 @@ fn send_to_bg(
let jobs = parser.jobs();
if !jobs[job_pos].wants_job_control() {
let job = &jobs[job_pos];
streams.err.appendln(&wgettext_fmt!(
"%s: Can't put job %s, '%s' to background because it is not under job control",
cmd,
err_fmt!(
"Can't put job %s, '%s' to background because it is not under job control",
job.job_id().to_wstring(),
job.command()
));
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
@@ -66,9 +67,7 @@ pub fn bg(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Built
};
let Some(job_pos) = job_pos else {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NO_SUITABLE_JOBS, cmd));
err_str!(Error::NO_SUITABLE_JOBS).cmd(cmd).finish(streams);
return Err(STATUS_CMD_ERROR);
};
@@ -101,9 +100,9 @@ pub fn bg(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Built
send_to_bg(parser, streams, cmd, job_pos)?;
}
} else {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_COULD_NOT_FIND_JOB, cmd, pid));
err_fmt!(Error::COULD_NOT_FIND_JOB, pid)
.cmd(cmd)
.finish(streams);
}
}

View File

@@ -1,25 +1,30 @@
//! Implementation of the bind builtin.
use super::prelude::*;
use crate::common::{
EscapeFlags, EscapeStringStyle, bytes2wcstring, escape, escape_string, valid_var_name,
use crate::{
builtins::error::Error,
common::valid_var_name,
err_fmt, err_raw, err_str,
highlight::highlight_and_colorize,
input::{
InputMapping, InputMappingSet, KeyNameStyle, input_function_get_names, input_mappings,
},
key::{
self, KEY_NAMES, Key, MAX_FUNCTION_KEY, Modifiers, char_to_symbol, function_key, parse_keys,
},
};
use crate::highlight::highlight_and_colorize;
use crate::input::{
InputMapping, InputMappingSet, KeyNameStyle, input_function_get_names, input_mappings,
};
use crate::key::{
self, KEY_NAMES, Key, MAX_FUNCTION_KEY, Modifiers, char_to_symbol, function_key, parse_keys,
};
use fish_common::help_section;
use fish_common::{EscapeFlags, EscapeStringStyle, escape, escape_string, help_section};
use fish_widestring::bytes2wcstring;
use std::sync::MutexGuard;
const DEFAULT_BIND_MODE: &wstr = L!("default");
const BIND_INSERT: c_int = 0;
const BIND_ERASE: c_int = 1;
const BIND_KEY_NAMES: c_int = 2;
const BIND_FUNCTION_NAMES: c_int = 3;
enum BindMode {
Insert,
Erase,
KeyNames,
FunctionNames,
}
struct Options {
all: bool,
@@ -30,7 +35,7 @@ struct Options {
user: bool,
have_preset: bool,
preset: bool,
mode: c_int,
mode: BindMode,
bind_mode: Option<WString>,
sets_bind_mode: Option<WString>,
color: ColorEnabled,
@@ -47,7 +52,7 @@ fn new() -> Options {
user: false,
have_preset: false,
preset: false,
mode: BIND_INSERT,
mode: BindMode::Insert,
bind_mode: None,
sets_bind_mode: None,
color: ColorEnabled::default(),
@@ -261,7 +266,7 @@ fn compute_seq(&self, streams: &mut IoStreams, seq: &wstr) -> Option<Vec<Key>> {
match parse_keys(seq) {
Ok(keys) => Some(keys),
Err(err) => {
streams.err.append(&sprintf!("bind: %s\n", err));
err_raw!(err).cmd(L!("bind")).finish(streams);
None
}
}
@@ -312,12 +317,9 @@ fn insert(
} else {
// Inserting both on the other hand makes no sense.
if self.opts.have_preset && self.opts.have_user {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--preset",
"--user"
));
err_fmt!(Error::COMBO_EXCLUSIVE, "--preset", "--user")
.cmd(cmd)
.finish(streams);
return true;
}
}
@@ -353,17 +355,13 @@ fn insert(
);
if !self.opts.silent {
if seq.len() == 1 {
streams.err.appendln(&wgettext_fmt!(
"%s: No binding found for key '%s'",
cmd,
seq[0]
));
err_fmt!("No binding found for key '%s'", seq[0])
.cmd(cmd)
.finish(streams);
} else {
streams.err.appendln(&wgettext_fmt!(
"%s: No binding found for key sequence '%s'",
cmd,
eseq
));
err_fmt!("No binding found for key sequence '%s'", eseq)
.cmd(cmd)
.finish(streams);
}
}
return true;
@@ -433,12 +431,13 @@ fn parse_cmd_opts(
let check_mode_name = |streams: &mut IoStreams, mode_name: &wstr| -> Result<(), ErrorCode> {
if !valid_var_name(mode_name) {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_BIND_MODE,
cmd,
err_fmt!(
Error::BIND_MODE,
mode_name,
help_section!("language#shell-variable-and-function-names")
));
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
Ok(())
@@ -448,17 +447,18 @@ fn parse_cmd_opts(
while let Some(c) = w.next_opt() {
match c {
'a' => opts.all = true,
'e' => opts.mode = BIND_ERASE,
'f' => opts.mode = BIND_FUNCTION_NAMES,
'e' => opts.mode = BindMode::Erase,
'f' => opts.mode = BindMode::FunctionNames,
'h' => opts.print_help = true,
'k' => {
streams.err.appendln(&wgettext_fmt!(
"%s: the -k/--key syntax is no longer supported. See `bind --help` and `bind --key-names`",
cmd,
));
err_str!(
"the -k/--key syntax is no longer supported. See `bind --help` and `bind --key-names`"
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
'K' => opts.mode = BIND_KEY_NAMES,
'K' => opts.mode = BindMode::KeyNames,
'L' => {
opts.list_modes = true;
return Ok(SUCCESS);
@@ -483,7 +483,7 @@ fn parse_cmd_opts(
opts.user = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -534,7 +534,7 @@ pub fn bind(
}
match self.opts.mode {
BIND_ERASE => {
BindMode::Erase => {
// If we get both, we erase both.
if self.opts.user
&& self.erase(
@@ -557,19 +557,13 @@ pub fn bind(
return Err(STATUS_CMD_ERROR);
}
}
BIND_INSERT => {
BindMode::Insert => {
if self.insert(optind, argv, parser, streams) {
return Err(STATUS_CMD_ERROR);
}
}
BIND_KEY_NAMES => self.key_names(streams),
BIND_FUNCTION_NAMES => self.function_names(streams),
_ => {
streams
.err
.appendln(&wgettext_fmt!("%s: Invalid state", cmd));
return Err(STATUS_CMD_ERROR);
}
BindMode::KeyNames => self.key_names(streams),
BindMode::FunctionNames => self.function_names(streams),
}
Ok(SUCCESS)
}

View File

@@ -1,5 +1,7 @@
use std::sync::atomic::Ordering;
use crate::err_str;
// Implementation of the block builtin.
use super::prelude::*;
@@ -51,7 +53,7 @@ fn parse_options(
opts.erase = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], false);
builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -84,17 +86,14 @@ pub fn block(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Bu
if opts.erase {
if opts.scope != Scope::Unset {
streams.err.appendln(&wgettext_fmt!(
"%s: Can not specify scope when removing block",
cmd
));
err_str!("Can not specify scope when removing block")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if parser.global_event_blocks.load(Ordering::Relaxed) == 0 {
streams
.err
.appendln(&wgettext_fmt!("%s: No blocks defined", cmd));
err_str!("No blocks defined").cmd(cmd).finish(streams);
return Err(STATUS_CMD_ERROR);
}
parser.global_event_blocks.fetch_sub(1, Ordering::Relaxed);

View File

@@ -1,18 +1,17 @@
use super::prelude::*;
use crate::builtins::error::Error;
use crate::parser::{Block, BlockType};
use crate::reader::reader_read;
use crate::{err_fmt, err_str};
use libc::STDIN_FILENO;
/// Implementation of the builtin breakpoint command, used to launch the interactive debugger.
pub fn breakpoint(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
let cmd = argv[0];
if argv.len() != 1 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT1,
cmd,
0,
argv.len() - 1
));
err_fmt!(Error::UNEXP_ARG_COUNT, 0, argv.len() - 1)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -28,10 +27,9 @@ pub fn breakpoint(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr])
.block_at_index(1)
.is_none_or(|b| b.typ() == BlockType::breakpoint)
{
streams.err.appendln(&wgettext_fmt!(
"%s: Command not valid at an interactive prompt",
cmd,
));
err_str!("Command not valid at an interactive prompt")
.cmd(cmd)
.finish(streams);
return Err(STATUS_ILLEGAL_CMD);
}
}

View File

@@ -1,3 +1,5 @@
use crate::{builtins::error::Error, err_fmt};
use super::prelude::*;
#[derive(Default)]
@@ -29,7 +31,14 @@ pub fn r#builtin(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], print_hints);
builtin_missing_argument(
parser,
streams,
cmd,
None,
argv[w.wopt_index - 1],
print_hints,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -53,11 +62,12 @@ pub fn r#builtin(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -
}
if opts.query && opts.list_names {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--query and --names are mutually exclusive")
));
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}

View File

@@ -3,13 +3,13 @@
use super::prelude::*;
use crate::{
env::{EnvMode, Environment as _},
err_fmt, err_raw, err_str,
fds::{BEST_O_SEARCH, wopen_dir},
parser::ParserEnvSetMode,
path::path_apply_cdpath,
wutil::{normalize_path, wreadlink},
};
use errno::Errno;
use fish_util::perror;
use libc::{EACCES, ELOOP, ENOENT, ENOTDIR, EPERM};
use nix::unistd::fchdir;
use std::sync::Arc;
@@ -19,7 +19,7 @@
pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
localizable_consts! {
DIR_DOES_NOT_EXIST
"%s: The directory '%s' does not exist"
"The directory '%s' does not exist"
}
let Some(&cmd) = args.first() else {
@@ -45,9 +45,9 @@ pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Built
&tmpstr
}
None => {
streams
.err
.appendln(&wgettext_fmt!("%s: Could not find home directory", cmd));
err_str!("Could not find home directory")
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
}
@@ -55,31 +55,21 @@ pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Built
// Stop `cd ""` from crashing
if dir_in.is_empty() {
streams.err.appendln(&wgettext_fmt!(
"%s: Empty directory '%s' does not exist",
cmd,
dir_in
));
let mut err = err_fmt!("Empty directory '%s' does not exist", dir_in).cmd(cmd);
if !parser.is_interactive() {
streams.err.append(&parser.current_line());
err = err.stacktrace(parser);
}
err.finish(streams);
return Err(STATUS_CMD_ERROR);
}
let pwd = vars.get_pwd_slash();
let dirs = path_apply_cdpath(dir_in, &pwd, vars);
if dirs.is_empty() {
streams
.err
.appendln(&wgettext_fmt!(DIR_DOES_NOT_EXIST, cmd, dir_in));
if !parser.is_interactive() {
streams.err.append(&parser.current_line());
}
return Err(STATUS_CMD_ERROR);
}
assert!(
!dirs.is_empty(),
"dirs should always contains a least an abs path, or a rel path, or '<PWD>/...'"
);
let mut best_errno = 0;
let mut broken_symlink = WString::new();
@@ -142,44 +132,30 @@ pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Built
return Ok(SUCCESS);
}
if best_errno == ENOTDIR {
streams
.err
.appendln(&wgettext_fmt!("%s: '%s' is not a directory", cmd, dir_in));
let mut err = if best_errno == ENOTDIR {
err_fmt!("'%s' is not a directory", dir_in)
} else if !broken_symlink.is_empty() {
streams.err.appendln(&wgettext_fmt!(
"%s: '%s' is a broken symbolic link to '%s'",
cmd,
err_fmt!(
"'%s' is a broken symbolic link to '%s'",
broken_symlink,
broken_symlink_target
));
)
} else if best_errno == ELOOP {
streams.err.appendln(&wgettext_fmt!(
"%s: Too many levels of symbolic links: '%s'",
cmd,
dir_in
));
err_fmt!("Too many levels of symbolic links: '%s'", dir_in)
} else if best_errno == ENOENT {
streams
.err
.appendln(&wgettext_fmt!(DIR_DOES_NOT_EXIST, cmd, dir_in));
err_fmt!(DIR_DOES_NOT_EXIST, dir_in)
} else if best_errno == EACCES || best_errno == EPERM {
streams
.err
.appendln(&wgettext_fmt!("%s: Permission denied: '%s'", cmd, dir_in));
err_fmt!("Permission denied: '%s'", dir_in)
} else {
errno::set_errno(Errno(best_errno));
perror("cd");
streams.err.appendln(&wgettext_fmt!(
"%s: Unknown error trying to locate directory '%s'",
cmd,
dir_in
));
}
err_raw!(builtin_strerror()).cmd(L!("cd")).finish(streams);
err_fmt!("Unknown error trying to locate directory '%s'", dir_in)
};
if !parser.is_interactive() {
streams.err.append(&parser.current_line());
err = err.stacktrace(parser);
}
err.cmd(cmd).finish(streams);
Err(STATUS_CMD_ERROR)
}

View File

@@ -36,7 +36,14 @@ pub fn r#command(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], print_hints);
builtin_missing_argument(
parser,
streams,
cmd,
None,
argv[w.wopt_index - 1],
print_hints,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {

View File

@@ -1,25 +1,29 @@
use super::prelude::*;
use super::read::TokenOutputMode;
use crate::ast::{self, Kind, Leaf as _};
use crate::common::{UnescapeFlags, UnescapeStringStyle, unescape_string};
use crate::complete::Completion;
use crate::expand::{ExpandFlags, ExpandResultCode, expand_string};
use crate::input::input_function_get_code;
use crate::input_common::{CharEvent, ReadlineCmd};
use crate::operation_context::{OperationContext, no_cancel};
use crate::parse_constants::ParseTreeFlags;
use crate::parse_util::{
detect_parse_errors, get_job_extent, get_offset_from_line, get_process_extent,
get_token_extent, lineno,
use crate::{
ast::{self, Kind, Leaf as _},
builtins::error::Error,
complete::Completion,
err_fmt, err_str,
expand::{ExpandFlags, ExpandResultCode, expand_string},
input::input_function_get_code,
input_common::{CharEvent, ReadlineCmd},
operation_context::{OperationContext, no_cancel},
parse_constants::ParseTreeFlags,
parse_util::{
detect_parse_errors, get_job_extent, get_offset_from_line, get_process_extent,
get_token_extent, lineno,
},
prelude::*,
proc::is_interactive_session,
reader::{
JumpDirection, JumpPrecision, commandline_get_state, commandline_set_buffer,
commandline_set_search_field, reader_execute_readline_cmd, reader_jump,
reader_showing_suggestion,
},
tokenizer::{TOK_ACCEPT_UNFINISHED, TokenType, Tokenizer},
};
use crate::prelude::*;
use crate::proc::is_interactive_session;
use crate::reader::{
JumpDirection, JumpPrecision, commandline_get_state, commandline_set_buffer,
commandline_set_search_field, reader_execute_readline_cmd, reader_jump,
reader_showing_suggestion,
};
use crate::tokenizer::{TOK_ACCEPT_UNFINISHED, TokenType, Tokenizer};
use fish_common::{UnescapeFlags, UnescapeStringStyle, unescape_string};
use fish_wcstringutil::join_strings;
use std::ops::Range;
@@ -320,12 +324,13 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
'f' => function_mode = true,
'x' | '\x02' | 'o' => {
if token_mode.is_some() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--tokens options are mutually exclusive")
));
builtin_print_error_trailer(parser, streams.err, cmd);
)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
token_mode = Some(match c {
@@ -372,7 +377,14 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, w.argv[w.wopt_index - 1], true);
builtin_missing_argument(
parser,
streams,
cmd,
None,
w.argv[w.wopt_index - 1],
true,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -424,23 +436,25 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
|| selection_start_mode
|| selection_end_mode
{
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if positional_args == 0 {
builtin_missing_argument(parser, streams, cmd, L!("--function"), true);
builtin_missing_argument(parser, streams, cmd, None, L!("--function"), true);
return Err(STATUS_INVALID_ARGS);
}
type RL = ReadlineCmd;
for arg in &w.argv[w.wopt_index..] {
let Some(cmd) = input_function_get_code(arg) else {
streams
.err
.append(&wgettext_fmt!("%s: Unknown input function '%s'", cmd, arg));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!("Unknown input function '%s'", arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
// Don't enqueue a repaint if we're currently in the middle of one,
@@ -467,20 +481,20 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
// Check for invalid switch combinations.
if (selection_start_mode || selection_end_mode) && positional_args != 0 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::TOO_MANY_ARGUMENTS)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if (search_mode || line_mode || column_mode || cursor_mode || paging_mode)
&& positional_args > 1
{
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::TOO_MANY_ARGUMENTS)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -489,24 +503,29 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
// Special case - we allow to get/set cursor position relative to the process/job/token.
&& ((buffer_part.is_none() && !search_field_mode) || !cursor_mode)
{
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if (token_mode.is_some() || cut_at_cursor) && positional_args != 0 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
"--cut-at-cursor and token options can not be used when setting the commandline"
));
builtin_print_error_trailer(parser, streams.err, cmd);
)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if search_field_mode && (buffer_part.is_some() || token_mode.is_some()) {
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -522,26 +541,20 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
if append_mode == AppendMode::InsertSmart {
if search_field_mode {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--insert-smart",
"--search-field"
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::COMBO_EXCLUSIVE, "--insert-smart", "--search-field")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
match buffer_part {
TextScope::String | TextScope::Job | TextScope::Process => (),
TextScope::Token => {
// To-do: we can support it in command position.
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--insert-smart",
"--current-token"
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::COMBO_EXCLUSIVE, "--insert-smart", "--current-token")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -552,19 +565,19 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
let arg = w.argv[w.wopt_index];
let new_coord = match fish_wcstol(arg) {
Err(_) => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, arg));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::NOT_NUMBER, arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
0
}
Ok(num) => num - 1,
};
let Ok(new_coord) = usize::try_from(new_coord) else {
streams
.err
.append(&wgettext_fmt!("%s: line/column index starts at 1", cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!("line/column index starts at 1")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
@@ -572,10 +585,10 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
let Some(offset) =
get_offset_from_line(&rstate.text, i32::try_from(new_coord).unwrap())
else {
streams
.err
.appendln(&wgettext_fmt!("%s: there is no line %s", cmd, arg));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!("there is no line %s", arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
offset
@@ -587,12 +600,10 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
let next_line_offset =
get_offset_from_line(&rstate.text, line_index + 1).unwrap_or(rstate.text.len());
if line_offset + new_coord > next_line_offset {
streams.err.appendln(&wgettext_fmt!(
"%s: column %s exceeds line length",
cmd,
arg
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!("column %s exceeds line length", arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
line_offset + new_coord
@@ -675,11 +686,10 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
current_cursor_pos = current_buffer.len();
} else if parser.libdata().transient_commandline.is_some() {
if cursor_mode && positional_args != 0 {
streams.err.append(&wgettext_fmt!(
"%s: setting cursor while evaluating 'complete --arguments' is not yet supported",
cmd
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!("setting cursor while evaluating 'complete --arguments' is not yet supported")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
transient = parser.libdata().transient_commandline.clone().unwrap();
@@ -690,11 +700,10 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
current_cursor_pos = rstate.cursor_pos;
} else {
// There is no command line because we are not interactive.
streams.err.append(cmd);
streams
.err
.append(L!(": Can not set commandline in non-interactive mode\n"));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!("Can not set commandline in non-interactive mode")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
@@ -742,10 +751,10 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
let arg = w.argv[w.wopt_index];
let new_pos = match fish_wcstol(arg) {
Err(_) => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, arg));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::NOT_NUMBER, arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
0
}
Ok(num) => num,

View File

@@ -1,21 +1,22 @@
use super::prelude::*;
use crate::common::{ScopeGuard, UnescapeFlags, UnescapeStringStyle, unescape_string};
use crate::complete::{CompletionRequestOptions, complete_add_wrapper, complete_remove_wrapper};
use crate::highlight::highlight_and_colorize;
use crate::operation_context::OperationContext;
use crate::parse_constants::ParseErrorList;
use crate::parse_util::detect_errors_in_argument_list;
use crate::parse_util::{detect_parse_errors, get_token_extent};
use crate::proc::is_interactive_session;
use crate::reader::{commandline_get_state, completion_apply_to_command_line};
use crate::{
common::bytes2wcstring,
builtins::error::Error,
complete::{
CompleteFlags, CompleteOptionType, CompletionMode, complete_add, complete_print,
complete_remove, complete_remove_all,
CompleteFlags, CompleteOptionType, CompletionMode, CompletionRequestOptions, complete_add,
complete_add_wrapper, complete_print, complete_remove, complete_remove_all,
complete_remove_wrapper,
},
err_fmt, err_raw, err_str,
highlight::highlight_and_colorize,
operation_context::OperationContext,
parse_constants::ParseErrorList,
parse_util::{detect_errors_in_argument_list, detect_parse_errors, get_token_extent},
proc::is_interactive_session,
reader::{commandline_get_state, completion_apply_to_command_line},
};
use fish_common::{ScopeGuard, UnescapeFlags, UnescapeStringStyle, unescape_string};
use fish_wcstringutil::string_suffixes_string;
use fish_widestring::bytes2wcstring;
// builtin_complete_* are a set of rather silly looping functions that make sure that all the proper
// combinations of complete_add or complete_remove get called. This is needed since complete allows
@@ -245,7 +246,7 @@ fn builtin_complete_print(
pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
localizable_consts! {
OPTION_REQUIRES_NON_EMPTY_STRING
"%s: %s requires a non-empty string"
"%s requires a non-empty string"
}
let cmd = argv[0];
@@ -326,11 +327,9 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
cmd_to_complete.push(tmp);
}
} else {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid token '%s'",
cmd,
w.woptarg.unwrap()
));
err_fmt!("Invalid token '%s'", w.woptarg.unwrap())
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -347,11 +346,9 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
let arg = w.woptarg.unwrap();
short_opt.extend(arg.chars());
if arg.is_empty() {
streams.err.appendln(&wgettext_fmt!(
OPTION_REQUIRES_NON_EMPTY_STRING,
cmd,
"-s",
));
err_fmt!(OPTION_REQUIRES_NON_EMPTY_STRING, "-s",)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -359,11 +356,9 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
let arg = w.woptarg.unwrap();
gnu_opt.push(arg);
if arg.is_empty() {
streams.err.appendln(&wgettext_fmt!(
OPTION_REQUIRES_NON_EMPTY_STRING,
cmd,
"-l",
));
err_fmt!(OPTION_REQUIRES_NON_EMPTY_STRING, "-l",)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -371,11 +366,9 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
let arg = w.woptarg.unwrap();
old_opt.push(arg);
if arg.is_empty() {
streams.err.appendln(&wgettext_fmt!(
OPTION_REQUIRES_NON_EMPTY_STRING,
cmd,
"-o",
));
err_fmt!(OPTION_REQUIRES_NON_EMPTY_STRING, "-o",)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -404,7 +397,7 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -424,19 +417,21 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
if result_mode.no_files && result_mode.force_files {
if !have_x {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
"complete",
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
"'--no-files' and '--force-files'"
));
)
.cmd(cmd)
.finish(streams);
} else {
// The reason for us not wanting files is `-x`,
// which is short for `-rf`.
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
"complete",
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
"'--exclusive' and '--force-files'"
));
)
.cmd(cmd)
.finish(streams);
}
return Err(STATUS_INVALID_ARGS);
}
@@ -450,10 +445,10 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
// Or use one left-over arg as the command to complete
cmd_to_complete.push(argv[argc - 1].to_owned());
} else {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::TOO_MANY_ARGUMENTS)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -461,14 +456,16 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
for condition_string in &condition {
let mut errors = ParseErrorList::new();
if detect_parse_errors(condition_string, Some(&mut errors), false).is_err() {
let prefix = WString::from(L!("-n '")) + &condition_string[..] + L!("'");
for error in errors {
let prefix = cmd.to_owned() + L!(": -n '") + &condition_string[..] + L!("'");
streams.err.appendln(&error.describe_with_prefix(
err_raw!(&error.describe_with_prefix(
condition_string,
&prefix,
parser.is_interactive(),
false,
));
))
.cmd(cmd)
.finish(streams);
}
return Err(STATUS_CMD_ERROR);
}
@@ -476,10 +473,10 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
if !comp.is_empty() {
if let Err(err_text) = detect_errors_in_argument_list(&comp, cmd) {
streams
.err
.appendln(&wgettext_fmt!("%s: %s: contains a syntax error", cmd, comp));
streams.err.appendln(&err_text);
let mut err = err_fmt!("%s: contains a syntax error", comp);
err.append_assign_to_msg('\n');
err.append_assign_to_msg(&err_text);
err.cmd(cmd).finish(streams);
return Err(STATUS_CMD_ERROR);
}
}
@@ -491,10 +488,9 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
// No argument given, try to use the current commandline.
let commandline_state = commandline_get_state(true);
if !parser.interactive_initialized.load() && !is_interactive_session() {
streams.err.append(cmd);
streams
.err
.append(L!(": Can not get commandline in non-interactive mode\n"));
err_str!("Can not get commandline in non-interactive mode")
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
commandline_state.text

View File

@@ -1,3 +1,5 @@
use crate::err_str;
// Implementation of the contains builtin.
use super::prelude::*;
@@ -28,7 +30,7 @@ fn parse_options(
'h' => opts.print_help = true,
'i' => opts.print_index = true,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], false);
builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -71,9 +73,7 @@ pub fn contains(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
}
}
} else {
streams
.err
.appendln(&wgettext_fmt!("%s: Key not specified", cmd));
err_str!("Key not specified").cmd(cmd).finish(streams);
}
Err(STATUS_CMD_ERROR)

View File

@@ -1,10 +1,12 @@
// Implementation of the disown builtin.
use super::prelude::*;
use crate::builtins::error::Error;
use crate::builtins::shared::HelpOnlyCmdOpts;
use crate::io::IoStreams;
use crate::parser::Parser;
use crate::proc::{Job, add_disowned_job};
use crate::{builtins::shared::HelpOnlyCmdOpts, localization::wgettext_fmt};
use crate::{err_fmt, err_str};
use fish_widestring::wstr;
use nix::sys::signal::{Signal, killpg};
@@ -21,12 +23,13 @@ fn disown_job(cmd: &wstr, streams: &mut IoStreams, j: &Job) {
if let Some(pgid) = pgid {
let _ = killpg(pgid.as_nix_pid(), Some(Signal::SIGCONT));
}
streams.err.appendln(&wgettext_fmt!(
"%s: job %d ('%s') was stopped and has been signalled to continue.",
cmd,
err_fmt!(
"job %d ('%s') was stopped and has been signalled to continue.",
j.job_id(),
j.command()
));
)
.cmd(cmd)
.finish(streams);
}
// We cannot directly remove the job from the jobs() list as `disown` might be called
@@ -64,9 +67,7 @@ pub fn disown(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
disown_job(cmd, streams, &job);
retval = Ok(SUCCESS);
} else {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NO_SUITABLE_JOBS, cmd));
err_str!(Error::NO_SUITABLE_JOBS).cmd(cmd).finish(streams);
retval = Err(STATUS_CMD_ERROR);
}
} else {
@@ -87,9 +88,9 @@ pub fn disown(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
};
parser.job_get_from_pid(pid).or_else(|| {
// Valid identifier but no such job
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_COULD_NOT_FIND_JOB, cmd, pid));
err_fmt!(Error::COULD_NOT_FIND_JOB, pid)
.cmd(cmd)
.finish(streams);
None
})
})

View File

@@ -45,7 +45,7 @@ fn parse_options(
's' => opts.print_spaces = false,
'E' => opts.interpret_special_chars = false,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {

View File

@@ -1,5 +1,5 @@
use super::prelude::*;
use crate::event;
use crate::{err_str, event};
pub fn emit(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
let Some(&cmd) = argv.first() else {
@@ -14,9 +14,7 @@ pub fn emit(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
}
let Some(event_name) = argv.get(opts.optind) else {
streams
.err
.append(&sprintf!(L!("%s: expected event name\n"), cmd));
err_str!("expected event name").cmd(cmd).finish(streams);
return Err(STATUS_INVALID_ARGS);
};

View File

@@ -1,10 +1,12 @@
//! Implementation of the fg builtin.
use crate::builtins::error::Error;
use crate::fds::make_fd_blocking;
use crate::parser::ParserEnvSetMode;
use crate::reader::{reader_save_screen_state, reader_write_title};
use crate::tokenizer::tok_command;
use crate::{env::EnvMode, tty_handoff::TtyHandoff};
use crate::{err_fmt, err_str};
use fish_util::perror;
use libc::STDIN_FILENO;
use nix::sys::termios::{self, tcsetattr};
@@ -38,9 +40,7 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
&& ((job.is_stopped() || !job.is_foreground()) && job.wants_job_control())
}) {
None => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NO_SUITABLE_JOBS, cmd));
err_str!(Error::NO_SUITABLE_JOBS).cmd(cmd).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
Some((pos, j)) => {
@@ -57,17 +57,13 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
found_job = parser.job_get_from_pid(pid).is_some();
}
if found_job {
streams
.err
.appendln(&wgettext_fmt!("%s: Ambiguous job", cmd));
let err = if found_job {
err_str!("Ambiguous job")
} else {
streams
.err
.appendln(&wgettext_fmt!("%s: '%s' is not a job", cmd, argv[optind]));
}
err_fmt!("'%s' is not a job", argv[optind])
};
err.cmd(cmd).full_trailer(parser).finish(streams);
builtin_print_error_trailer(parser, streams.err, cmd);
job_pos = None;
job = None;
} else {
@@ -77,21 +73,21 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
if j.as_ref()
.is_none_or(|(_pos, j)| !j.is_constructed() || j.is_completed())
{
streams
.err
.appendln(&wgettext_fmt!("%s: No suitable job: %d", cmd, pid));
err_fmt!("No suitable job: %d", pid)
.cmd(cmd)
.finish(streams);
job_pos = None;
job = None;
} else {
let (pos, j) = j.unwrap();
job_pos = Some(pos);
job = if !j.wants_job_control() {
streams.err.appendln(&wgettext_fmt!(
"%s: Can't put job %d, '%s' to foreground because it is not under job control",
cmd,
err_fmt!("Can't put job %d, '%s' to foreground because it is not under job control",
pid,
j.command()
));
)
.cmd(cmd)
.finish(streams);
None
} else {
Some(j)

View File

@@ -1,41 +1,38 @@
//! The fish_indent program.
use std::ffi::OsStr;
use std::fs;
use std::io::{Read, Write as _};
use std::os::unix::ffi::OsStrExt as _;
use crate::locale::set_libc_locales;
use crate::panic::panic_handler;
use super::prelude::*;
use crate::ast::{
self, AsNode as _, Ast, Kind, Leaf as _, Node, NodeVisitor, SourceRangeList, Traversal,
use crate::{
ast::{self, AsNode as _, Ast, Kind, Leaf as _, Node, NodeVisitor, SourceRangeList, Traversal},
builtins::error::Error,
common::{PROGRAM_NAME, get_program_name},
env::{EnvStack, env_init, environment::Environment as _},
err_fmt, err_str,
global_safety::RelaxedAtomicBool,
highlight::{HighlightRole, HighlightSpec, colorize, highlight_shell},
locale::set_libc_locales,
operation_context::OperationContext,
panic::panic_handler,
parse_constants::{ParseTokenType, ParseTreeFlags, SourceRange},
parse_util::{SPACES_PER_INDENT, apply_indents, compute_indents},
prelude::*,
print_help::print_help,
threads,
tokenizer::{TOK_SHOW_BLANK_LINES, TOK_SHOW_COMMENTS, TokenType, Tokenizer},
topic_monitor::topic_monitor_init,
wutil::fish_iswalnum,
};
use crate::common::{
PROGRAM_NAME, ReadExt as _, UnescapeFlags, UnescapeStringStyle, bytes2wcstring,
get_program_name, osstr2wcstring, unescape_string,
};
use crate::env::EnvStack;
use crate::env::env_init;
use crate::env::environment::Environment as _;
use crate::expand::INTERNAL_SEPARATOR;
use crate::future_feature_flags;
use crate::global_safety::RelaxedAtomicBool;
use crate::highlight::{HighlightRole, HighlightSpec, colorize, highlight_shell};
use crate::operation_context::OperationContext;
use crate::parse_constants::{ParseTokenType, ParseTreeFlags, SourceRange};
use crate::parse_util::{SPACES_PER_INDENT, apply_indents, compute_indents};
use crate::prelude::*;
use crate::print_help::print_help;
use crate::threads;
use crate::tokenizer::{TOK_SHOW_BLANK_LINES, TOK_SHOW_COMMENTS, TokenType, Tokenizer};
use crate::topic_monitor::topic_monitor_init;
use crate::wutil::fish_iswalnum;
use assert_matches::assert_matches;
use fish_wcstringutil::{count_preceding_backslashes, wcs2bytes};
use fish_common::{ReadExt as _, UnescapeFlags, UnescapeStringStyle, unescape_string};
use fish_wcstringutil::count_preceding_backslashes;
use fish_wgetopt::{ArgType, WGetopter, WOption, wopt};
use std::fmt::Write as _;
use fish_widestring::{INTERNAL_SEPARATOR, bytes2wcstring, osstr2wcstring, wcs2bytes};
use std::{
ffi::OsStr,
fmt::Write as _,
fs,
io::{Read, Write as _},
os::unix::ffi::OsStrExt as _,
};
/// Note: this got somewhat more complicated after introducing the new AST, because that AST no
/// longer encodes detailed lexical information (e.g. every newline). This feels more complex
@@ -945,7 +942,7 @@ fn throwing_main() -> i32 {
// Only set these here so you can't set them via the builtin.
if let Some(features_var) = EnvStack::globals().get(L!("fish_features")) {
for s in features_var.as_list() {
future_feature_flags::set_from_string(s.as_utfstr());
fish_feature_flags::set_from_string(s.as_utfstr());
}
}
@@ -1026,19 +1023,15 @@ enum OutputType {
'\x03' => output_type = OutputType::PygmentsCsv,
'c' => output_type = OutputType::Check,
';' => {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_UNEXP_ARG,
"fish_indent",
w.argv[w.wopt_index - 1]
));
err_fmt!(Error::UNEXP_OPT_ARG, w.argv[w.wopt_index - 1])
.cmd(L!("fish_indent"))
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
'?' => {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_UNKNOWN,
"fish_indent",
w.argv[w.wopt_index - 1]
));
err_fmt!(Error::UNKNOWN_OPT, w.argv[w.wopt_index - 1])
.cmd(L!("fish_indent"))
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
_ => panic!(),
@@ -1054,18 +1047,14 @@ enum OutputType {
while i < args.len() || (args.is_empty() && i == 0) {
if args.is_empty() && i == 0 {
if output_type == OutputType::File {
streams.err.append(&sprintf!(
"%s\n\n $ %s -w foo.fish\n",
wgettext!("Expected file path to read/write for -w:"),
get_program_name()
));
err_str!("Expected file path to read/write for -w:")
.append_to_msg(&sprintf!("\n\n $ %s -w foo.fish\n", get_program_name()))
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
let Some(stdin_file) = streams.stdin_file.as_mut() else {
let cmd = "fish_indent";
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_STDIN_CLOSED, cmd));
let cmd = L!("fish_indent");
err_str!(Error::STDIN_CLOSED).cmd(cmd).finish(streams);
return Err(STATUS_CMD_ERROR);
};
let mut buf = vec![];
@@ -1091,11 +1080,7 @@ enum OutputType {
output_location = arg;
}
Err(err) => {
streams.err.appendln(&wgettext_fmt!(
"Opening \"%s\" failed: %s",
arg,
err.to_string()
));
err_fmt!("Opening \"%s\" failed: %s", arg, err.to_string()).finish(streams);
return Err(STATUS_CMD_ERROR);
}
}
@@ -1172,11 +1157,12 @@ enum OutputType {
let _ = file.write_all(&wcs2bytes(&output_wtext));
}
Err(err) => {
streams.err.appendln(&wgettext_fmt!(
err_fmt!(
"Opening \"%s\" failed: %s",
output_location,
err.to_string()
));
)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
}

View File

@@ -12,10 +12,10 @@
use libc::{STDIN_FILENO, VEOF, VINTR};
use crate::{
builtins::shared::BUILTIN_ERR_UNKNOWN,
common::{PROGRAM_NAME, get_program_name, osstr2wcstring, shell_modes},
builtins::error::Error,
common::{PROGRAM_NAME, get_program_name, shell_modes},
env::{EnvStack, Environment as _, env_init},
future_feature_flags,
err_fmt, err_str,
input_common::{
CharEvent, ImplicitEvent, InputEventQueue, InputEventQueuer as _, KeyEvent,
QueryResultEvent, match_key_event_to_key,
@@ -27,13 +27,15 @@
print_help::print_help,
proc::set_interactive_session,
reader::{
check_exit_loop_maybe_warning, reader_init, reader_sighup, set_shell_modes, terminal_init,
check_exit_loop_maybe_warning, reader_init, safe_reader_set_exit_signal, set_shell_modes,
terminal_init,
},
threads,
topic_monitor::topic_monitor_init,
tty_handoff::TtyHandoff,
};
use fish_wgetopt::{ArgType, WGetopter, WOption, wopt};
use fish_widestring::osstr2wcstring;
use super::prelude::*;
@@ -96,7 +98,7 @@ fn process_input(
use QueryResultEvent::*;
let kevt = match input_queue.readch() {
CharEvent::Implicit(ImplicitEvent::Eof) => {
reader_sighup();
safe_reader_set_exit_signal(libc::SIGHUP);
continue;
}
CharEvent::Key(kevt) => kevt,
@@ -212,19 +214,15 @@ fn parse_flags(
*verbose = true;
}
';' => {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_UNEXP_ARG,
"fish_key_reader",
w.argv[w.wopt_index - 1]
));
err_fmt!(Error::UNEXP_OPT_ARG, w.argv[w.wopt_index - 1])
.cmd(L!("fish_key_reader"))
.finish(streams);
return ControlFlow::Break(Err(STATUS_CMD_ERROR));
}
'?' => {
streams.err.append(&wgettext_fmt!(
BUILTIN_ERR_UNKNOWN,
"fish_key_reader",
w.argv[w.wopt_index - 1]
));
err_fmt!(Error::UNKNOWN_OPT, w.argv[w.wopt_index - 1])
.cmd(L!("fish_key_reader"))
.finish(streams);
return ControlFlow::Break(Err(STATUS_CMD_ERROR));
}
_ => panic!(),
@@ -233,9 +231,7 @@ fn parse_flags(
let argc = args.len() - w.wopt_index;
if argc != 0 {
streams
.err
.appendln(&wgettext_fmt!("Expected no arguments, got %d", argc));
err_fmt!("Expected no arguments, got %d", argc).finish(streams);
return ControlFlow::Break(Err(STATUS_CMD_ERROR));
}
@@ -262,7 +258,7 @@ pub fn fish_key_reader(
}
if streams.stdin_fd() < 0 || !isatty(streams.stdin_fd()) {
streams.err.appendln("Stdin must be attached to a tty.");
err_str!("Stdin must be attached to a tty.").finish(streams);
return Err(STATUS_CMD_ERROR);
}
@@ -295,29 +291,27 @@ fn throwing_main() -> i32 {
reader_init(false);
if let Some(features_var) = EnvStack::globals().get(L!("fish_features")) {
for s in features_var.as_list() {
future_feature_flags::set_from_string(s.as_utfstr());
fish_feature_flags::set_from_string(s.as_utfstr());
}
}
let mut out = Fd(FdOutputStream::new(STDOUT_FILENO));
let mut err = Fd(FdOutputStream::new(STDERR_FILENO));
let io_chain = IoChain::new();
let mut streams = IoStreams::new(&mut out, &mut err, &io_chain);
let streams = &mut IoStreams::new(&mut out, &mut err, &io_chain);
let mut continuous_mode = false;
let mut verbose = false;
let args: Vec<WString> = std::env::args_os().map(osstr2wcstring).collect();
if let ControlFlow::Break(s) =
parse_flags(None, &mut streams, args, &mut continuous_mode, &mut verbose)
parse_flags(None, streams, args, &mut continuous_mode, &mut verbose)
{
return s.builtin_status_code();
}
if !isatty(STDIN_FILENO) {
streams
.err
.appendln(wgettext!("Stdin must be attached to a tty."));
err_str!("Stdin must be attached to a tty.").finish(streams);
return 1;
}
@@ -328,6 +322,5 @@ fn throwing_main() -> i32 {
terminal_init(&vars, STDIN_FILENO).input_queue
};
setup_and_process_keys(&mut streams, continuous_mode, verbose, input_queue)
.builtin_status_code()
setup_and_process_keys(streams, continuous_mode, verbose, input_queue).builtin_status_code()
}

View File

@@ -5,13 +5,13 @@
use crate::env::environment::Environment as _;
use crate::env::is_read_only;
use crate::event::{self, EventDescription, EventHandler};
use crate::function;
use crate::global_safety::RelaxedAtomicBool;
use crate::parse_execution::varname_error;
use crate::parse_tree::NodeRef;
use crate::parser_keywords::parser_keywords_is_reserved;
use crate::proc::Pid;
use crate::signal::Signal;
use crate::{err_fmt, err_str, function};
use nix::unistd::getpid;
use std::sync::Arc;
@@ -86,15 +86,13 @@ fn parse_cmd_opts(
let mut validate_variable_name =
|streams: &mut IoStreams, varname: &wstr, read_only_ok: bool| {
if !valid_var_name(varname) {
streams.err.append(&varname_error(cmd, varname));
varname_error(cmd, varname).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if !read_only_ok && is_read_only(varname) {
streams.err.appendln(&wgettext_fmt!(
"%s: variable '%s' is read-only",
cmd,
varname
));
err_fmt!("variable '%s' is read-only", varname)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
Ok(())
@@ -122,11 +120,9 @@ fn add_named_argument(
if handling_named_arguments {
add_named_argument(&mut validate_variable_name, streams, opts, woptarg)?;
} else {
streams.err.append(&wgettext_fmt!(
"%s: %s: unexpected positional argument",
cmd,
woptarg
));
err_fmt!("%s: unexpected positional argument", woptarg)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -135,11 +131,9 @@ fn add_named_argument(
}
's' => {
let Some(signal) = Signal::parse(w.woptarg.unwrap()) else {
streams.err.append(&wgettext_fmt!(
"%s: Unknown signal '%s'",
cmd,
w.woptarg.unwrap()
));
err_fmt!("Unknown signal '%s'", w.woptarg.unwrap())
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
opts.events.push(EventDescription::Signal { signal });
@@ -163,10 +157,9 @@ fn add_named_argument(
0
};
if caller_id == 0 {
streams.err.append(&wgettext_fmt!(
"%s: calling job for event handler not found",
cmd
));
err_str!("calling job for event handler not found")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
e = EventDescription::CallerExit { caller_id };
@@ -214,7 +207,14 @@ fn add_named_argument(
opts.print_help = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], print_hints);
builtin_missing_argument(
parser,
streams,
cmd,
None,
argv[w.wopt_index - 1],
print_hints,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -245,11 +245,9 @@ fn add_named_argument(
add_named_argument(&mut validate_variable_name, streams, opts, arg)?;
}
} else {
streams.err.append(&wgettext_fmt!(
"%s: %s: unexpected positional argument",
cmd,
argv[optind],
));
err_fmt!("%s: unexpected positional argument", argv[optind],)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -271,19 +269,18 @@ fn validate_function_name(
}
*function_name = argv[1].to_owned();
if !valid_func_name(function_name) {
streams.err.append(&wgettext_fmt!(
"%s: %s: invalid function name",
cmd,
function_name,
));
err_fmt!("%s: invalid function name", function_name,)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if parser_keywords_is_reserved(function_name) {
streams.err.append(&wgettext_fmt!(
"%s: %s: cannot use reserved keyword as function name",
cmd,
err_fmt!(
"%s: cannot use reserved keyword as function name",
function_name
));
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
Ok(SUCCESS)

View File

@@ -1,16 +1,17 @@
use super::prelude::*;
use crate::common::bytes2wcstring;
use crate::common::escape_string;
use crate::common::reformat_for_screen;
use crate::common::valid_func_name;
use crate::common::{EscapeFlags, EscapeStringStyle};
use crate::event::{self};
use crate::function;
use crate::highlight::highlight_and_colorize;
use crate::parse_util::apply_indents;
use crate::parse_util::compute_indents;
use crate::parser_keywords::parser_keywords_is_reserved;
use crate::termsize::termsize_last;
use crate::{
builtins::error::Error,
common::{reformat_for_screen, valid_func_name},
err_fmt, err_str,
event::{self},
function,
highlight::highlight_and_colorize,
parse_util::{apply_indents, compute_indents},
parser_keywords::parser_keywords_is_reserved,
termsize::termsize_last,
};
use fish_common::{EscapeFlags, EscapeStringStyle, escape_string};
use fish_widestring::bytes2wcstring;
#[derive(Default)]
struct FunctionsCmdOpts<'args> {
@@ -84,7 +85,14 @@ fn parse_cmd_opts<'args>(
opts.color = ColorEnabled::parse_from_opt(streams, cmd, w.woptarg.unwrap())?;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], print_hints);
builtin_missing_argument(
parser,
streams,
cmd,
None,
argv[w.wopt_index - 1],
print_hints,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -118,7 +126,7 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
localizable_consts! {
FUNCTION_DOES_NOT_EXIST
"%s: Function '%s' does not exist"
"Function '%s' does not exist"
}
let mut opts = FunctionsCmdOpts::default();
@@ -141,14 +149,18 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
.count()
> 1
{
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if opts.report_metadata && opts.no_metadata {
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -162,20 +174,19 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
if let Some(desc) = opts.description {
if args.len() != 1 {
streams.err.appendln(&wgettext_fmt!(
"%s: Expected exactly one function name",
cmd
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!("Expected exactly one function name")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let current_func = args[0];
if !function::exists(current_func, parser) {
streams
.err
.appendln(&wgettext_fmt!(FUNCTION_DOES_NOT_EXIST, cmd, current_func));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(FUNCTION_DOES_NOT_EXIST, current_func)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
@@ -185,9 +196,8 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
if opts.report_metadata {
if args.len() != 1 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
err_fmt!(
Error::UNPEXP_ARG_COUNT_WITH_CTX,
// This error is
// functions: --details: expected 1 arguments; got 2
// The "--details" was "argv[optind - 1]" in the C++
@@ -196,7 +206,9 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
"--details",
1,
args.len()
));
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let props = function::get_props_autoload(args[0], parser);
@@ -268,12 +280,13 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
if !opts.handlers_type.unwrap_or(L!("")).is_empty()
&& !event::EVENT_FILTER_NAMES.contains(&opts.handlers_type.unwrap())
{
streams.err.appendln(&wgettext_fmt!(
"%s: Expected %s for %s",
cmd,
err_fmt!(
"Expected %s for %s",
"generic | variable | signal | exit | job-id",
"--handlers-type",
));
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
event::print(streams, opts.handlers_type.unwrap_or(L!("")));
@@ -310,42 +323,40 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
if opts.copy {
if args.len() != 2 {
streams.err.appendln(&wgettext_fmt!(
"%s: Expected exactly two names (current function name, and new function name)",
cmd
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!("Expected exactly two names (current function name, and new function name)")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let current_func = args[0];
let new_func = args[1];
if !function::exists(current_func, parser) {
streams
.err
.appendln(&wgettext_fmt!(FUNCTION_DOES_NOT_EXIST, cmd, current_func));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(FUNCTION_DOES_NOT_EXIST, current_func)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
if !valid_func_name(new_func) || parser_keywords_is_reserved(new_func) {
streams.err.appendln(&wgettext_fmt!(
"%s: Illegal function name '%s'",
cmd,
new_func
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!("Illegal function name '%s'", new_func)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if function::exists(new_func, parser) {
streams.err.appendln(&wgettext_fmt!(
"%s: Function '%s' already exists. Cannot create copy of '%s'",
cmd,
err_fmt!(
"Function '%s' already exists. Cannot create copy of '%s'",
new_func,
current_func
));
builtin_print_error_trailer(parser, streams.err, cmd);
)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
if function::copy(current_func, new_func.into(), parser) {

View File

@@ -1,8 +1,10 @@
//! Implementation of the history builtin.
use crate::builtins::error::Error;
use crate::history::in_private_mode;
use crate::history::{self, History, history_session_id};
use crate::reader::commandline_get_state;
use crate::{err_fmt, err_str};
use super::prelude::*;
@@ -94,12 +96,13 @@ fn set_hist_cmd(
streams: &mut IoStreams,
) -> bool {
if *hist_cmd != HistCmd::None {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
err_fmt!(
Error::COMBO_EXCLUSIVE,
hist_cmd.to_wstr(),
sub_cmd.to_wstr()
));
)
.cmd(cmd)
.finish(streams);
return false;
}
*hist_cmd = sub_cmd;
@@ -114,22 +117,16 @@ fn check_for_unexpected_hist_args(
) -> bool {
if opts.search_type.is_some() || opts.show_time_format.is_some() || opts.null_terminate {
let subcmd_str = opts.hist_cmd.to_wstr();
streams.err.appendln(&wgettext_fmt!(
"%s: %s: subcommand takes no options",
cmd,
subcmd_str
));
err_str!("subcommand takes no options")
.subcmd(cmd, subcmd_str)
.finish(streams);
return true;
}
if !args.is_empty() {
let subcmd_str = opts.hist_cmd.to_wstr();
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
subcmd_str,
0,
args.len()
));
err_fmt!(Error::UNEXP_ARG_COUNT, 0, args.len())
.subcmd(cmd, subcmd_str)
.finish(streams);
return true;
}
false
@@ -195,11 +192,9 @@ fn parse_cmd_opts(
'n' => match fish_wcstol(w.woptarg.unwrap()) {
Ok(x) => opts.max_items = Some(x as _), // todo!("historical behavior is to cast")
Err(_) => {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_NOT_NUMBER,
cmd,
w.woptarg.unwrap()
));
err_fmt!(Error::NOT_NUMBER, w.woptarg.unwrap())
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
},
@@ -210,7 +205,7 @@ fn parse_cmd_opts(
opts.print_help = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -305,15 +300,12 @@ pub fn history(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
// be handled only by the history function's interactive delete feature.
if opts.search_type.unwrap_or(history::SearchType::Exact) != history::SearchType::Exact
{
streams
.err
.appendln(wgettext!("builtin history delete only supports --exact"));
err_str!("builtin history delete only supports --exact").finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if !opts.case_sensitive {
streams.err.appendln(wgettext!(
"builtin history delete --exact requires --case-sensitive"
));
err_str!("builtin history delete --exact requires --case-sensitive")
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -341,10 +333,9 @@ pub fn history(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
}
if in_private_mode(parser.vars()) {
streams.err.appendln(&wgettext_fmt!(
"%s: can't merge history in private mode",
cmd
));
err_str!("can't merge history in private mode")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
history.incorporate_external_changes();

View File

@@ -1,13 +1,16 @@
// Functions for executing the jobs builtin.
use super::prelude::*;
use crate::common::{EscapeFlags, EscapeStringStyle, escape_string, timef};
use crate::io::IoStreams;
use crate::job_group::{JobId, MaybeJobId};
use crate::localization::{wgettext, wgettext_fmt};
use crate::parser::Parser;
use crate::proc::{HAVE_PROC_STAT, Job, clock_ticks_to_seconds, proc_get_jiffies};
use crate::wutil::fish_wcstoi;
use crate::{
err_fmt,
io::IoStreams,
job_group::{JobId, MaybeJobId},
localization::{wgettext, wgettext_fmt},
parser::Parser,
proc::{HAVE_PROC_STAT, Job, clock_ticks_to_seconds, proc_get_jiffies},
wutil::fish_wcstoi,
};
use fish_common::{EscapeFlags, EscapeStringStyle, escape_string, timef};
use fish_wgetopt::{ArgType, WGetopter, WOption, wopt};
use fish_widestring::{L, WExt as _, WString, wstr};
use std::num::NonZeroU32;
@@ -170,7 +173,7 @@ pub fn jobs(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -202,11 +205,9 @@ pub fn jobs(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
if arg.char_at(0) == '%' {
match fish_wcstoi(&arg[1..]).ok().filter(|&job_id| job_id >= 0) {
None => {
streams.err.appendln(&wgettext_fmt!(
"%s: '%s' is not a valid job ID",
cmd,
arg
));
err_fmt!("'%s' is not a valid job ID", arg)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
Some(job_id) => {
@@ -230,9 +231,9 @@ pub fn jobs(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
found = true;
} else {
if mode != JobsPrintMode::PrintNothing {
streams
.err
.appendln(&wgettext_fmt!("%s: No suitable job: %s", cmd, arg));
err_fmt!("No suitable job: %s", arg)
.cmd(cmd)
.finish(streams);
}
return Err(STATUS_CMD_ERROR);
}

View File

@@ -2,7 +2,7 @@
use num_traits::pow;
use super::prelude::*;
use crate::tinyexpr::te_interp;
use crate::{builtins::error::Error, err_fmt, tinyexpr::te_interp};
/// The maximum number of points after the decimal that we'll print.
const DEFAULT_SCALE: usize = 6;
@@ -67,9 +67,9 @@ fn parse_cmd_opts(
} else {
let scale = fish_wcstoi(optarg).unwrap_or(-1);
if scale < 0 || scale > 15 {
streams
.err
.appendln(&wgettext_fmt!("%s: %s: invalid scale", cmd, optarg));
err_fmt!("%s: invalid scale", optarg)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// We know the value is in the range [0, 15]
@@ -87,9 +87,9 @@ fn parse_cmd_opts(
} else if optarg.eq(L!("ceiling")) || optarg.eq(L!("ceil")) {
opts.scale_mode = ScaleMode::Ceiling;
} else {
streams
.err
.appendln(&wgettext_fmt!("%s: %s: invalid mode", cmd, optarg));
err_fmt!("%s: invalid mode", optarg)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -102,11 +102,9 @@ fn parse_cmd_opts(
} else {
let base = fish_wcstoi(optarg).unwrap_or(-1);
if base != 8 && base != 16 {
streams.err.appendln(&wgettext_fmt!(
"%s: %s: invalid base value",
cmd,
optarg
));
err_fmt!("%s: invalid base value", optarg)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// We know the value is 8 or 16.
@@ -117,7 +115,14 @@ fn parse_cmd_opts(
opts.print_help = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], print_hints);
builtin_missing_argument(
parser,
streams,
cmd,
None,
args[w.wopt_index - 1],
print_hints,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -142,12 +147,12 @@ fn parse_cmd_opts(
}
if have_scale && opts.scale != 0 && opts.base != 10 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
"non-zero scale value only valid
for base 10"
));
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
"non-zero scale value only valid for base 10"
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -243,27 +248,23 @@ fn evaluate_expression(
return Ok(SUCCESS);
};
streams
.err
.append(&sprintf!("%s: Error: %s\n", cmd, error_message));
streams.err.append(&sprintf!("'%s'\n", expression));
let mut err = err_fmt!("Error: %s", error_message);
err.append_assign_to_msg(&sprintf!("\n'%s'\n", expression));
err.cmd(cmd).finish(streams);
Err(STATUS_CMD_ERROR)
}
Err(err) => {
streams.err.append(&sprintf!(
L!("%s: Error: %s\n"),
cmd,
err.kind.describe_wstr()
));
streams.err.append(&sprintf!("'%s'\n", expression));
let mut error = err_fmt!("Error: %s", err.kind.describe_wstr());
error.append_assign_to_msg(&sprintf!("\n'%s'", expression));
let padding = WString::from_chars(vec![' '; err.position + 1]);
if err.len >= 2 {
let tildes = WString::from_chars(vec!['~'; err.len - 2]);
streams.err.append(&sprintf!("%s^%s^\n", padding, tildes));
error.append_assign_to_msg(&sprintf!("\n%s^%s^", padding, tildes));
} else {
streams.err.append(&sprintf!("%s^\n", padding));
error.append_assign_to_msg(&sprintf!("\n%s^", padding));
}
error.cmd(cmd).finish(streams);
Err(STATUS_CMD_ERROR)
}
@@ -293,9 +294,9 @@ pub fn math(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
}
if expression.is_empty() {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, 0));
err_fmt!(Error::MIN_ARG_COUNT, 1, 0)
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}

View File

@@ -66,3 +66,5 @@ mod prelude {
NON_OPTION_CHAR, WGetopter, WOption, wopt,
};
}
pub use shared::*;

View File

@@ -1,4 +1,6 @@
use crate::builtins::error::Error;
use crate::env::environment::Environment as _;
use crate::{err_fmt, err_str};
use std::fs::Metadata;
use std::os::unix::prelude::{FileTypeExt as _, MetadataExt as _};
use std::time::SystemTime;
@@ -15,23 +17,6 @@
use libc::{PATH_MAX, S_ISGID, S_ISUID, mode_t};
use nix::unistd::{AccessFlags, Gid, Uid};
macro_rules! path_error {
(
$streams:expr,
$string:expr
$(, $args:expr)+
$(,)?
) => {
$streams.err.append(L!("path "));
$streams.err.appendln(&wgettext_fmt!($string, $($args),*));
};
}
fn path_unknown_option(parser: &Parser, streams: &mut IoStreams, subcmd: &wstr, opt: &wstr) {
path_error!(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt);
builtin_print_error_trailer(parser, streams.err, L!("path"));
}
// How many bytes we read() at once.
// We use PATH_MAX here so we always get at least one path,
// and so we can automatically detect NULL-separated input.
@@ -233,7 +218,8 @@ fn parse_opts<'args>(
parser: &Parser,
streams: &mut IoStreams,
) -> BuiltinResult {
let cmd = args[0];
let cmd = L!("path");
let subcmd = args[0];
let mut args_read = Vec::with_capacity(args.len());
args_read.extend_from_slice(args);
@@ -243,23 +229,27 @@ fn parse_opts<'args>(
while let Some(c) = w.next_opt() {
match c {
':' => {
streams.err.append(L!("path ")); // clone of string_error
builtin_missing_argument(parser, streams, cmd, args_read[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
';' => {
streams.err.append(L!("path ")); // clone of string_error
builtin_unexpected_argument(
builtin_missing_argument(
parser,
streams,
cmd,
Some(subcmd),
args_read[w.wopt_index - 1],
false,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
err_fmt!(Error::UNEXP_OPT_ARG, args_read[w.wopt_index - 1])
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
path_unknown_option(parser, streams, cmd, args_read[w.wopt_index - 1]);
err_fmt!(Error::UNKNOWN_OPT, args_read[w.wopt_index - 1])
.subcmd(cmd, subcmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
'q' => {
@@ -283,7 +273,9 @@ fn parse_opts<'args>(
let types_args = split_string_tok(w.woptarg.unwrap(), L!(","), None);
for t in types_args {
let Ok(r#type) = t.try_into() else {
path_error!(streams, "%s: Invalid type '%s'", "path", t);
err_fmt!("Invalid type '%s'", t)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*types |= r#type;
@@ -295,7 +287,9 @@ fn parse_opts<'args>(
let perms_args = split_string_tok(w.woptarg.unwrap(), L!(","), None);
for p in perms_args {
let Ok(perm) = p.try_into() else {
path_error!(streams, "%s: Invalid permission '%s'", "path", p);
err_fmt!("Invalid permission '%s'", p)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*perms |= perm;
@@ -359,7 +353,10 @@ fn parse_opts<'args>(
continue;
}
_ => {
path_unknown_option(parser, streams, cmd, args_read[w.wopt_index - 1]);
err_fmt!(Error::UNKNOWN_OPT, args_read[w.wopt_index - 1])
.subcmd(cmd, subcmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -375,14 +372,18 @@ fn parse_opts<'args>(
}
if opts.arg1.is_none() && n_req_args == 1 {
path_error!(streams, BUILTIN_ERR_ARG_COUNT0, cmd);
err_str!(Error::MISSING_ARG)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
// At this point we should not have optional args and be reading args from stdin.
if streams.stdin_is_directly_redirected && args.len() > *optind {
path_error!(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd);
err_str!(Error::TOO_MANY_ARGUMENTS)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -698,7 +699,9 @@ fn path_sort(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Bu
}
None => wbasename,
Some(k) => {
path_error!(streams, "%s: Invalid sort key '%s'", args[0], k);
err_fmt!("Invalid sort key '%s'", k)
.subcmd(L!("path"), args[0])
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
};
@@ -949,10 +952,10 @@ pub fn path(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Bui
};
let argc = args.len();
if argc <= 1 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MISSING_SUBCMD, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::MISSING_SUBCMD)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -975,10 +978,10 @@ pub fn path(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Bui
"resolve" => path_resolve,
"sort" => path_sort,
_ => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, subcmd_name));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_SUBCMD)
.subcmd(cmd, subcmd_name)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
};

View File

@@ -49,6 +49,7 @@
// This file has been imported from source code of printf command in GNU Coreutils version 6.9.
use super::prelude::*;
use crate::builtins::error;
use crate::locale::{Locale, get_numeric_locale};
use crate::wutil::{
errors::Error,
@@ -56,6 +57,7 @@
wcstoi::{Options as WcstoiOpts, wcstoi_partial},
wstr_offset_in,
};
use crate::{err_fmt, err_str};
use fish_printf::{ToArg as _, sprintf_locale};
use fish_widestring::{decode_byte_from_char, encode_byte_to_char};
@@ -205,24 +207,21 @@ impl<'a, 'b> builtin_printf_state_t<'a, 'b> {
fn verify_numeric(&mut self, s: &wstr, end: &wstr, errcode: Option<Error>) {
// This check matches the historic `errcode != EINVAL` check from C++.
// Note that empty or missing values will be silently treated as 0.
if errcode != None && errcode != Some(Error::InvalidChar) && errcode != Some(Error::Empty) {
if errcode.is_some_and(|err| err != Error::InvalidChar && err != Error::Empty) {
match errcode.unwrap() {
Error::Overflow => {
self.fatal_error(wgettext_fmt!("%s: Number out of range", s));
self.fatal_error(err_fmt!("%s: Number out of range", s));
}
Error::Empty => {
self.fatal_error(wgettext_fmt!("%s: Number was empty", s));
}
Error::InvalidChar => {
panic!("Unreachable");
Error::InvalidChar | Error::Empty => {
unreachable!("Unreachable");
}
}
} else if !end.is_empty() {
if s.as_ptr() == end.as_ptr() {
self.fatal_error(wgettext_fmt!("%s: expected a numeric value", s));
self.fatal_error(err_fmt!("%s: expected a numeric value", s));
} else {
// This isn't entirely fatal - the value should still be printed.
self.nonfatal_error(wgettext_fmt!(
self.nonfatal_error(err_fmt!(
"%s: value not completely converted (can't convert '%s')",
s,
end
@@ -231,7 +230,7 @@ fn verify_numeric(&mut self, s: &wstr, end: &wstr, errcode: Option<Error>) {
// Do it if the unconverted digit is a valid hex digit,
// because it could also be an "0x" -> "0" typo.
if s.char_at(0) == '0' && iswxdigit(end.char_at(0)) {
self.nonfatal_error(wgettext!(
self.nonfatal_error(err_str!(
"Hint: a leading '0' without an 'x' indicates an octal number"
));
}
@@ -241,7 +240,7 @@ fn verify_numeric(&mut self, s: &wstr, end: &wstr, errcode: Option<Error>) {
fn handle_sprintf_error(&mut self, err: fish_printf::Error) {
match err {
fish_printf::Error::Overflow => self.fatal_error(wgettext!("Number out of range")),
fish_printf::Error::Overflow => self.fatal_error(err_str!("Number out of range")),
_ => panic!("unhandled error: {err:?}"),
}
}
@@ -472,7 +471,7 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
if (c_int::MIN as i64) <= width && width <= (c_int::MAX as i64) {
field_width = Some(width);
} else {
self.fatal_error(wgettext_fmt!("invalid field width: %s", argv[0]));
self.fatal_error(err_fmt!("invalid field width: %s", argv[0]));
}
argv = &argv[1..];
argc -= 1;
@@ -500,10 +499,7 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
// so -1 is safe here even if prec < INT_MIN.
precision = Some(-1);
} else if (c_int::MAX as i64) < prec {
self.fatal_error(wgettext_fmt!(
"invalid precision: %s",
argv[0]
));
self.fatal_error(err_fmt!("invalid precision: %s", argv[0]));
} else {
precision = Some(prec);
}
@@ -529,7 +525,7 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
let directive = &directive_start[0..directive_start
.len()
.min(wstr_offset_in(f, directive_start) + 1)];
self.fatal_error(wgettext_fmt!(
self.fatal_error(err_fmt!(
"%s: invalid conversion specification",
directive
));
@@ -563,8 +559,7 @@ fn print_formatted(&mut self, format: &wstr, mut argv: &[&wstr]) -> usize {
save_argc - argc
}
fn nonfatal_error<Str: AsRef<wstr>>(&mut self, errstr: Str) {
let errstr = errstr.as_ref();
fn nonfatal_error(&mut self, err: error::Error) {
// Don't error twice.
if self.early_exit {
return;
@@ -576,7 +571,8 @@ fn nonfatal_error<Str: AsRef<wstr>>(&mut self, errstr: Str) {
self.buff.clear();
}
self.streams.err.append(errstr);
let errstr = err.to_string();
self.streams.err.append(&errstr);
if !errstr.ends_with('\n') {
self.streams.err.append('\n');
}
@@ -586,26 +582,8 @@ fn nonfatal_error<Str: AsRef<wstr>>(&mut self, errstr: Str) {
self.exit_code = Err(STATUS_CMD_ERROR);
}
fn fatal_error<Str: AsRef<wstr>>(&mut self, errstr: Str) {
let errstr = errstr.as_ref();
// Don't error twice.
if self.early_exit {
return;
}
// If we have output, write it so it appears first.
if !self.buff.is_empty() {
self.streams.out.append(&self.buff);
self.buff.clear();
}
self.streams.err.append(errstr);
if !errstr.ends_with('\n') {
self.streams.err.append('\n');
}
self.exit_code = Err(STATUS_CMD_ERROR);
fn fatal_error(&mut self, err: error::Error) {
self.nonfatal_error(err);
self.early_exit = true;
}
@@ -629,7 +607,7 @@ fn print_esc(&mut self, escstart: &wstr, octal_0: bool) -> usize {
p = &p[1..];
}
if esc_length == 0 {
self.fatal_error(wgettext!("missing hexadecimal number in escape"));
self.fatal_error(err_str!("missing hexadecimal number in escape"));
}
self.append_output(encode_byte_to_char((esc_value % 256) as u8));
} else if is_octal_digit(p.char_at(0)) {
@@ -658,7 +636,7 @@ fn print_esc(&mut self, escstart: &wstr, octal_0: bool) -> usize {
if !iswxdigit(p.char_at(0)) {
// Escape sequence must be done. Complain if we didn't get anything.
if esc_length == 0 {
self.fatal_error(wgettext!("Missing hexadecimal number in Unicode escape"));
self.fatal_error(err_str!("Missing hexadecimal number in Unicode escape"));
}
break;
}
@@ -681,7 +659,7 @@ fn print_esc(&mut self, escstart: &wstr, octal_0: bool) -> usize {
}
None => {
let escaped_char_string = format!("\\{esc_char}{uni_value:0exp_esc_length$x}");
self.fatal_error(wgettext_fmt!(
self.fatal_error(err_fmt!(
"Not a valid Unicode character: %s",
escaped_char_string
));

View File

@@ -2,7 +2,7 @@
use errno::errno;
use super::prelude::*;
use crate::{env::Environment as _, wutil::wrealpath};
use crate::{builtins::error::Error, env::Environment as _, err_fmt, wutil::wrealpath};
// The pwd builtin. Respect -P to resolve symbolic links. Respect -L to not do that (the default).
const short_options: &wstr = L!("LPh");
@@ -38,9 +38,9 @@ pub fn pwd(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Buil
}
if w.wopt_index != argc {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_ARG_COUNT1, cmd, 0, argc - 1));
err_fmt!(Error::UNEXP_ARG_COUNT, 0, argc - 1)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -52,12 +52,9 @@ pub fn pwd(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Buil
if let Some(real_pwd) = wrealpath(&pwd) {
pwd = real_pwd;
} else {
streams.err.appendln(&wgettext_fmt!(
"%s: %s failed: %s",
cmd,
"realpath",
errno().to_string()
));
err_fmt!("%s failed: %s", "realpath", errno().to_string())
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
}

View File

@@ -1,6 +1,7 @@
use super::prelude::*;
use crate::wutil;
use crate::builtins::error::Error;
use crate::{err_fmt, err_str, wutil};
use fish_util::get_seeded_rng;
use rand::rngs::SmallRng;
use rand::{Rng as _, RngCore as _};
@@ -26,7 +27,14 @@ pub fn random(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> B
return Ok(SUCCESS);
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], print_hints);
builtin_missing_argument(
parser,
streams,
cmd,
None,
argv[w.wopt_index - 1],
print_hints,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -56,9 +64,7 @@ pub fn random(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> B
let i = w.wopt_index;
if arg_count >= 1 && argv[i] == "choice" {
if arg_count == 1 {
streams
.err
.appendln(&wgettext_fmt!("%s: nothing to choose from", cmd));
err_str!("nothing to choose from").cmd(cmd).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -69,18 +75,14 @@ pub fn random(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> B
fn parse_ll(streams: &mut IoStreams, cmd: &wstr, num: &wstr) -> Result<i64, wutil::Error> {
let res = fish_wcstol(num);
if res.is_err() {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, num));
err_fmt!(Error::NOT_NUMBER, num).cmd(cmd).finish(streams);
}
res
}
fn parse_ull(streams: &mut IoStreams, cmd: &wstr, num: &wstr) -> Result<u64, wutil::Error> {
let res = fish_wcstoul(num);
if res.is_err() {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, num));
err_fmt!(Error::NOT_NUMBER, num).cmd(cmd).finish(streams);
}
res
}
@@ -124,11 +126,9 @@ fn parse_ull(streams: &mut IoStreams, cmd: &wstr, num: &wstr) -> Result<u64, wut
match parse_ull(streams, cmd, argv[i + 1]) {
Err(_) => return Err(STATUS_INVALID_ARGS),
Ok(0) => {
streams.err.appendln(&wgettext_fmt!(
"%s: %s must be a positive integer",
cmd,
"STEP"
));
err_fmt!("%s must be a positive integer", "STEP")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
Ok(x) => step = x,
@@ -140,9 +140,7 @@ fn parse_ull(streams: &mut IoStreams, cmd: &wstr, num: &wstr) -> Result<u64, wut
}
}
_ => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
err_str!(Error::TOO_MANY_ARGUMENTS).cmd(cmd).finish(streams);
return Err(STATUS_CMD_ERROR);
}
}

View File

@@ -1,36 +1,28 @@
//! Implementation of the read builtin.
use super::prelude::*;
use crate::common::UnescapeStringStyle;
use crate::common::bytes2wcstring;
use crate::common::escape;
use crate::common::read_blocked;
use crate::common::unescape_string;
use crate::common::valid_var_name;
use crate::env::EnvMode;
use crate::env::Environment as _;
use crate::env::READ_BYTE_LIMIT;
use crate::env::{EnvVar, EnvVarFlags};
use crate::input_common::DecodeState;
use crate::input_common::InvalidPolicy;
use crate::input_common::decode_one_codepoint_utf8;
use crate::nix::isatty;
use crate::parse_execution::varname_error;
use crate::parser::ParserEnvSetMode;
use crate::reader::ReaderConfig;
use crate::reader::commandline_set_buffer;
use crate::reader::{reader_pop, reader_push, reader_readline, set_shell_modes_temporarily};
use crate::tokenizer::TOK_ACCEPT_UNFINISHED;
use crate::tokenizer::TOK_ARGUMENT_LIST;
use crate::tokenizer::Tok;
use crate::tokenizer::Tokenizer;
use crate::wutil;
use crate::{
builtins::error::Error,
common::valid_var_name,
env::{EnvMode, EnvVar, EnvVarFlags, Environment as _, READ_BYTE_LIMIT},
err_fmt, err_str,
input_common::{DecodeState, InvalidPolicy, decode_utf8},
nix::isatty,
parse_execution::varname_error,
parser::ParserEnvSetMode,
reader::{
ReaderConfig, commandline_set_buffer, reader_pop, reader_push, reader_readline,
set_shell_modes_temporarily,
},
tokenizer::{TOK_ACCEPT_UNFINISHED, TOK_ARGUMENT_LIST, Tok, Tokenizer},
wutil,
};
use fish_common::{UnescapeStringStyle, escape, read_blocked, unescape_string};
use fish_util::perror;
use fish_wcstringutil::{split_about, split_string_tok};
use fish_widestring::bytes2wcstring;
use libc::SEEK_CUR;
use std::num::NonZeroUsize;
use std::os::fd::RawFd;
use std::sync::atomic::Ordering;
use std::{num::NonZeroUsize, os::fd::RawFd, sync::atomic::Ordering};
#[derive(Clone, Copy, Eq, PartialEq)]
pub(crate) enum TokenOutputMode {
@@ -55,7 +47,6 @@ struct Options {
array: bool,
silent: bool,
split_null: bool,
to_stdout: bool,
nchars: Option<NonZeroUsize>,
one_line: bool,
}
@@ -69,7 +60,7 @@ fn new() -> Self {
}
}
const SHORT_OPTIONS: &wstr = L!("ac:d:fghiLln:p:sStuxzP:UR:L");
const SHORT_OPTIONS: &wstr = L!("ac:d:fghLln:p:sStuxzP:UR:L");
const LONG_OPTIONS: &[WOption] = &[
wopt(L!("array"), ArgType::NoArgument, 'a'),
wopt(L!("command"), ArgType::RequiredArgument, 'c'),
@@ -121,13 +112,6 @@ fn parse_cmd_opts(
'd' => {
opts.delimiter = Some(w.woptarg.unwrap().to_owned());
}
'i' => {
streams.err.appendln(&wgettext_fmt!(
"%s: usage of -i for --silent is deprecated. Please use -s or --silent instead.",
cmd
));
return Err(STATUS_INVALID_ARGS);
}
'f' => {
opts.place.mode |= EnvMode::FUNCTION;
}
@@ -147,21 +131,17 @@ fn parse_cmd_opts(
opts.nchars = match fish_wcstoi(w.woptarg.unwrap()) {
Ok(n) if n >= 0 => NonZeroUsize::new(n.try_into().unwrap()),
Err(wutil::Error::Overflow) => {
streams.err.appendln(&wgettext_fmt!(
"%s: Argument '%s' is out of range",
cmd,
w.woptarg.unwrap()
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!("Argument '%s' is out of range", w.woptarg.unwrap())
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
_ => {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_NOT_NUMBER,
cmd,
w.woptarg.unwrap()
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::NOT_NUMBER, w.woptarg.unwrap())
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -189,16 +169,17 @@ fn parse_cmd_opts(
};
if let Some(old_mode) = opts.token_mode {
if old_mode != new_mode {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext_fmt!(
"%s and %s are mutually exclusive",
tokenize_flag(old_mode),
tokenize_flag(new_mode),
)
));
builtin_print_error_trailer(parser, streams.err, cmd);
)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -217,7 +198,7 @@ fn parse_cmd_opts(
opts.split_null = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -404,7 +385,7 @@ fn read_one_char_at_a_time(
}
unconsumed.push(b[0]);
nbytes += 1;
match decode_one_codepoint_utf8(buff, InvalidPolicy::Passthrough, &unconsumed) {
match decode_utf8(buff, InvalidPolicy::Passthrough, &unconsumed) {
DecodeState::Incomplete => continue,
DecodeState::Complete => {
unconsumed.clear();
@@ -451,32 +432,26 @@ fn validate_read_args(
) -> BuiltinResult {
localizable_consts! {
OPTIONS_CANNOT_BE_COMBINED
"%s: Options %s and %s cannot be used together"
"Options %s and %s cannot be used together"
}
if opts.prompt.is_some() && opts.prompt_str.is_some() {
streams
.err
.appendln(&wgettext_fmt!(OPTIONS_CANNOT_BE_COMBINED, cmd, "-p", "-P",));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(OPTIONS_CANNOT_BE_COMBINED, "-p", "-P")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if opts.delimiter.is_some() && opts.one_line {
streams.err.appendln(&wgettext_fmt!(
OPTIONS_CANNOT_BE_COMBINED,
cmd,
"--delimiter",
"--line"
));
err_fmt!(OPTIONS_CANNOT_BE_COMBINED, "--delimiter", "--line")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if opts.one_line && opts.split_null {
streams.err.appendln(&wgettext_fmt!(
OPTIONS_CANNOT_BE_COMBINED,
cmd,
"-z",
"--line"
));
err_fmt!(OPTIONS_CANNOT_BE_COMBINED, "-z", "--line")
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -487,10 +462,10 @@ fn validate_read_args(
}
if opts.place.mode.contains(EnvMode::UNEXPORT) && opts.place.mode.contains(EnvMode::EXPORT) {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_EXPUNEXP, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::EXPORT_UNEXPORT)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -502,32 +477,17 @@ fn validate_read_args(
.count()
> 1
{
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_GLOCAL, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::MULTIPLE_SCOPES)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let argc = argv.len();
if !opts.array && argc < 1 && !opts.to_stdout {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1, argc));
return Err(STATUS_INVALID_ARGS);
}
if opts.array && argc != 1 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_ARG_COUNT1, cmd, 1, argc));
return Err(STATUS_INVALID_ARGS);
}
if opts.to_stdout && argc > 0 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MAX_ARG_COUNT1, cmd, 0, argc));
if opts.array && argv.len() != 1 {
err_fmt!(Error::UNEXP_ARG_COUNT, 1, argv.len())
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -541,22 +501,20 @@ fn tokenize_flag(token_mode: TokenOutputMode) -> &'static wstr {
if let Some(token_mode) = opts.token_mode {
if opts.delimiter.is_some() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
err_fmt!(
Error::COMBO_EXCLUSIVE,
"--delimiter",
tokenize_flag(token_mode),
));
tokenize_flag(token_mode)
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if opts.one_line {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--line",
tokenize_flag(token_mode),
));
err_fmt!(Error::COMBO_EXCLUSIVE, "--line", tokenize_flag(token_mode))
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -564,17 +522,14 @@ fn tokenize_flag(token_mode: TokenOutputMode) -> &'static wstr {
// Verify all variable names.
for arg in argv {
if !valid_var_name(arg) {
streams.err.append(&varname_error(cmd, arg));
builtin_print_error_trailer(parser, streams.err, cmd);
varname_error(cmd, arg).full_trailer(parser).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if EnvVar::flags_for(arg).contains(EnvVarFlags::READ_ONLY) {
streams.err.append(&wgettext_fmt!(
"%s: %s: cannot overwrite read-only variable",
cmd,
arg
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!("%s: cannot overwrite read-only variable", arg)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -590,16 +545,9 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
let (mut opts, optind) = parse_cmd_opts(argv, parser, streams)?;
let cmd = argv[0];
let mut argv: &[&wstr] = argv;
if !opts.to_stdout {
argv = &argv[optind..];
}
let argv = &argv[optind..];
let argc = argv.len();
if argv.is_empty() {
opts.to_stdout = true;
}
if opts.print_help {
builtin_print_help(parser, streams, cmd);
return Ok(SUCCESS);
@@ -609,9 +557,7 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
// stdin may have been explicitly closed
if streams.is_stdin_closed() {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_STDIN_CLOSED, cmd));
err_str!(Error::STDIN_CLOSED).cmd(cmd).finish(streams);
return Err(STATUS_CMD_ERROR);
}
@@ -688,7 +634,7 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
return exit_res;
}
if opts.to_stdout {
if argv.is_empty() {
streams.out.append(&buff);
return exit_res;
}

View File

@@ -4,6 +4,7 @@
use super::prelude::*;
use crate::env::Environment as _;
use crate::err_fmt;
use crate::{
path::path_apply_working_directory,
wutil::{normalize_path, wrealpath},
@@ -37,7 +38,7 @@ fn parse_options(
's' => opts.no_symlinks = true,
'h' => opts.print_help = true,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], false);
builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -82,16 +83,11 @@ pub fn realpath(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
} else {
let errno = errno();
if errno.0 != 0 {
streams.err.appendln(&wgettext_fmt!(
"builtin %s: %s: %s",
cmd,
arg,
errno.to_string()
));
err_fmt!("%s: %s", arg, errno.to_string())
.cmd(cmd)
.finish(streams);
} else {
streams
.err
.appendln(&wgettext_fmt!("builtin %s: Invalid arg: %s", cmd, arg));
err_fmt!("Invalid arg: %s", arg).cmd(cmd).finish(streams);
}
had_error = true;
}
@@ -106,12 +102,9 @@ pub fn realpath(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
};
streams.out.appendln(&normalize_path(&absolute_arg, false));
} else {
streams.err.appendln(&wgettext_fmt!(
"builtin %s: %s failed: %s",
cmd,
"realpath",
errno().to_string()
));
err_fmt!("%s failed: %s", "realpath", errno().to_string())
.cmd(cmd)
.finish(streams);
had_error = true;
}
}

View File

@@ -2,6 +2,8 @@
use std::ops::ControlFlow;
use crate::{builtins::error::Error, err_fmt, err_str};
use super::prelude::*;
#[derive(Debug, Clone, Copy, Default)]
@@ -27,7 +29,7 @@ fn parse_options(
match c {
'h' => opts.print_help = true,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], true);
builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], true);
return ControlFlow::Break(STATUS_INVALID_ARGS);
}
';' => {
@@ -97,10 +99,10 @@ pub fn parse_return_value(
return ControlFlow::Break(Ok(SUCCESS));
}
if optind + 1 < args.len() {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::TOO_MANY_ARGUMENTS)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return ControlFlow::Break(Err(STATUS_INVALID_ARGS));
}
if optind == args.len() {
@@ -109,10 +111,10 @@ pub fn parse_return_value(
match fish_wcstoi(args[optind]) {
Ok(i) => ControlFlow::Continue(i),
Err(_e) => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, args[1]));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::NOT_NUMBER, args[1])
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
ControlFlow::Break(Err(STATUS_INVALID_ARGS))
}
}

View File

@@ -1,25 +1,17 @@
use super::prelude::*;
use crate::common::EscapeFlags;
use crate::common::EscapeStringStyle;
use crate::common::escape;
use crate::common::escape_string;
use crate::common::valid_var_name;
use crate::env::EnvStackSetResult;
use crate::env::EnvVarFlags;
use crate::env::INHERITED_VARS;
use crate::event;
use crate::event::Event;
use crate::expand::expand_escape_string;
use crate::expand::expand_escape_variable;
use crate::history::History;
use crate::history::history_session_id;
use crate::parse_execution::varname_error;
use crate::parser::ParserEnvSetMode;
use crate::{
env::{EnvMode, EnvVar, Environment},
builtins::error::Error,
common::valid_var_name,
env::{EnvMode, EnvStackSetResult, EnvVar, EnvVarFlags, Environment, INHERITED_VARS},
err_fmt, err_str,
event::{self, Event},
expand::{expand_escape_string, expand_escape_variable},
history::{History, history_session_id},
parse_execution::varname_error,
parser::ParserEnvSetMode,
wutil::wcstoi::wcstoi_partial,
};
use fish_common::help_section;
use fish_common::{EscapeFlags, EscapeStringStyle, escape, escape_string, help_section};
use fish_widestring::ELLIPSIS_CHAR;
#[derive(Debug, Clone)]
@@ -158,7 +150,14 @@ fn parse(
opts.preserve_failure_exit_status = false;
}
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], false);
builtin_missing_argument(
parser,
streams,
cmd,
None,
args[w.wopt_index - 1],
false,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -176,22 +175,24 @@ fn parse(
let optind = w.wopt_index;
// implicit drop(w); here
if args[optind - 1].starts_with("-o") {
// TODO: translate this
streams.err.appendln(&sprintf!(
"Fish does not have shell options. See `help %s`.",
let mut err = err_fmt!(
"fish does not have shell options. See `help %s`.",
help_section!("fish_for_bash_users")
));
);
if optind < args.len() {
err.append_assign_to_msg('\n');
if args[optind] == "vi" {
// Tell the vi users how to get what they need.
streams
.err
.appendln(L!("To enable vi-mode, run `fish_vi_key_bindings`."));
err.append_assign_to_msg(wgettext!(
"To enable vi-mode, run `fish_vi_key_bindings`."
));
} else if args[optind] == "ed" {
// This should be enough for make ed users feel at home
streams.err.append(L!("?\n?\n?\n"));
err.append_assign_to_msg(L!("?\n?\n?\n"));
}
}
err.finish(streams);
}
builtin_unknown_option(parser, streams, cmd, args[optind - 1], false);
@@ -225,15 +226,19 @@ fn validate(
) -> Result<(), ErrorCode> {
// Can't query and erase or list.
if opts.query && (opts.erase || opts.list) {
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// We can't both list and erase variables.
if opts.erase && opts.list {
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -246,35 +251,37 @@ fn validate(
// ..unless we are erasing a variable, in which case we can erase from several in one go.
&& !opts.erase
{
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_GLOCAL, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::MULTIPLE_SCOPES)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// Variables can only have one export status.
if opts.exportv && opts.unexport {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_EXPUNEXP, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::EXPORT_UNEXPORT)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// Variables can only have one path status.
if opts.pathvar && opts.unpathvar {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_PATHUNPATH, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::PATH_UNPATH)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// Trying to erase and (un)export at the same time doesn't make sense.
if opts.erase && (opts.exportv || opts.unexport) {
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -288,16 +295,18 @@ fn validate(
|| opts.exportv
|| opts.universal)
{
streams.err.appendln(&wgettext_fmt!(BUILTIN_ERR_COMBO, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::INVALID_OPT_COMBO)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if args.len() == optind && opts.erase {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MISSING, cmd, L!("--erase")));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::MISSING_OPT_ARG, L!("--erase"))
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -318,11 +327,12 @@ fn warn_if_uvar_shadows_global(
&& parser.is_interactive()
&& parser.vars().getf(dest, EnvMode::GLOBAL).is_some()
{
streams.err.appendln(&wgettext_fmt!(
"%s: successfully set universal '%s'; but a global by that name shadows it",
cmd,
err_fmt!(
"successfully set universal '%s'; but a global by that name shadows it",
dest
));
)
.cmd(cmd)
.finish(streams);
}
}
@@ -330,32 +340,29 @@ fn handle_env_return(retval: EnvStackSetResult, cmd: &wstr, key: &wstr, streams:
match retval {
EnvStackSetResult::Ok => (),
EnvStackSetResult::Perm => {
streams.err.appendln(&wgettext_fmt!(
"%s: Tried to change the read-only variable '%s'",
cmd,
key
));
err_fmt!("Tried to change the read-only variable '%s'", key)
.cmd(cmd)
.finish(streams);
}
EnvStackSetResult::Scope => {
streams.err.appendln(&wgettext_fmt!(
"%s: Tried to modify the special variable '%s' with the wrong scope",
cmd,
err_fmt!(
"Tried to modify the special variable '%s' with the wrong scope",
key
));
)
.cmd(cmd)
.finish(streams);
}
EnvStackSetResult::Invalid => {
streams.err.appendln(&wgettext_fmt!(
"%s: Tried to modify the special variable '%s' to an invalid value",
cmd,
err_fmt!(
"Tried to modify the special variable '%s' to an invalid value",
key
));
)
.cmd(cmd)
.finish(streams);
}
EnvStackSetResult::NotFound => {
streams.err.appendln(&wgettext_fmt!(
"%s: The variable '%s' does not exist",
cmd,
key
));
// Only variable deletion can return a `NotFound` error, but that case is explicitly silenced
unreachable!("variable not found");
}
}
}
@@ -394,7 +401,9 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
"{}\n",
match self {
EnvArrayParseError::InvalidIndex(varname) =>
wgettext_fmt!("%s: Invalid index starting at '%s'", "set", varname).to_string(),
err_fmt!("Invalid index starting at '%s'", varname)
.cmd(L!("set"))
.to_string(),
}
)
}
@@ -729,18 +738,23 @@ fn show(cmd: &wstr, parser: &Parser, streams: &mut IoStreams, args: &[&wstr]) ->
}
} else {
for arg in args.iter().copied() {
let bracket = arg.find(L!("["));
let arg = if let Some(idx) = bracket {
&arg[..idx]
} else {
arg
};
if !valid_var_name(arg) {
streams.err.append(&varname_error(cmd, arg));
builtin_print_error_trailer(parser, streams.err, cmd);
varname_error(cmd, arg).full_trailer(parser).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if arg.contains('[') {
streams.err.appendln(&wgettext_fmt!(
"%s: `set --show` does not allow slices with the var names",
cmd
));
builtin_print_error_trailer(parser, streams.err, cmd);
if bracket.is_some() {
err_str!("`set --show` does not allow slices with the var names")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
@@ -778,8 +792,9 @@ fn erase(
};
if !valid_var_name(split.varname) {
streams.err.append(&varname_error(cmd, split.varname));
builtin_print_error_trailer(parser, streams.err, cmd);
varname_error(cmd, split.varname)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let retval;
@@ -939,10 +954,10 @@ fn set_internal(
argv: &[&wstr],
) -> BuiltinResult {
if argv.is_empty() {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MIN_ARG_COUNT1, cmd, 1));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::MIN_ARG_COUNT, 1, 0)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -957,16 +972,19 @@ fn set_internal(
// Is the variable valid?
if !valid_var_name(split.varname) {
streams.err.append(&varname_error(cmd, split.varname));
let mut err = varname_error(cmd, split.varname);
if let Some(pos) = split.varname.chars().position(|c| c == '=') {
streams.err.append(&wgettext_fmt!(
"%s: Did you mean `set %s %s`?",
cmd,
err.append_assign_to_msg('\n');
let extra = err_fmt!(
"Did you mean `set %s %s`?",
&escape(&split.varname[..pos]),
&escape(&split.varname[pos + 1..])
));
)
.cmd(cmd)
.to_string();
err.append_assign_to_msg(&extra);
}
builtin_print_error_trailer(parser, streams.err, cmd);
err.full_trailer(parser).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -975,31 +993,31 @@ fn set_internal(
// Indexes must be > 0. (Note split_var_and_indexes negates negative values).
for ind in &split.indexes {
if *ind <= 0 {
streams
.err
.appendln(&wgettext_fmt!("%s: array index out of bounds", cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!("array index out of bounds")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
// Append and prepend are disallowed.
if opts.append || opts.prepend {
streams.err.append(&wgettext_fmt!(
"%s: Cannot use --append or --prepend when assigning to a slice",
cmd
));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!("Cannot use --append or --prepend when assigning to a slice")
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
// Argument count and index count must agree.
if split.indexes.len() != argv.len() {
streams.err.appendln(&wgettext_fmt!(
"%s: given %d indexes but %d values",
cmd,
err_fmt!(
"given %d indexes but %d values",
split.indexes.len(),
argv.len()
));
)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -1041,7 +1059,7 @@ pub fn set(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Buil
list(&opts, parser, streams)
} else if opts.show {
show(cmd, parser, streams, args)
} else if args.is_empty() {
} else if args.is_empty() && !(opts.append || opts.prepend) {
list(&opts, parser, streams)
} else {
set_internal(cmd, &opts, parser, streams, args)

View File

@@ -1,11 +1,15 @@
// Implementation of the set_color builtin.
use super::prelude::*;
use crate::common::bytes2wcstring;
use crate::screen::{is_dumb, only_grayscale};
use crate::terminal::Outputter;
use crate::text_face::{self, PrintColorsArgs, TextFace, TextStyling, parse_text_face_and_options};
use crate::{
builtins::error::Error,
err_fmt,
screen::{is_dumb, only_grayscale},
terminal::Outputter,
text_face::{self, PrintColorsArgs, TextFace, TextStyling, parse_text_face_and_options},
};
use fish_color::Color;
use fish_widestring::bytes2wcstring;
fn print_colors(
streams: &mut IoStreams,
@@ -76,70 +80,40 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -
builtin_print_help(parser, streams, argv[0]);
return Ok(SUCCESS);
}
Err(MissingOptArg) => {
// Either "--background" or "--underline-color" are missing an argument.
// Don't print an error, for consistency with "set_color".
// In future we change both to actually print an error.
return Err(STATUS_INVALID_ARGS);
}
Err(UnexpectedOptArg(option_index)) => {
builtin_unexpected_argument(
parser,
streams,
L!("set_color"),
argv[option_index],
true, /* print_hints */
);
return Err(STATUS_INVALID_ARGS);
}
Err(InvalidOptArg(name, value)) => {
streams.err.appendln(&wgettext_fmt!(
"%s: %s: invalid option argument: %s",
argv[0],
name,
value
));
return Err(STATUS_INVALID_ARGS);
}
Err(UnknownColor(arg)) => {
streams
.err
.appendln(&wgettext_fmt!("%s: Unknown color '%s'", argv[0], arg));
return Err(STATUS_INVALID_ARGS);
}
Err(UnknownUnderlineStyle(arg)) => {
streams.err.appendln(&wgettext_fmt!(
"%s: invalid underline style: %s",
argv[0],
arg
));
return Err(STATUS_INVALID_ARGS);
}
Err(UnknownOption(unknown_option_index)) => {
builtin_unknown_option(
parser,
streams,
L!("set_color"),
argv[unknown_option_index],
true, /* print_hints */
);
return Err(STATUS_INVALID_ARGS);
}
Err(InvalidFgArgCombination) => {
streams.err.appendln(&wgettext_fmt!(
"%s: %s: option cannot be used with a non-option argument",
argv[0],
"--foreground",
));
return Err(STATUS_INVALID_ARGS);
}
Err(InvalidFgPrintColorCombination) => {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
argv[0],
"--foreground",
"--print-colors",
));
Err(err) => {
let error = match err {
MissingOptArg => {
// Either "--background" or "--underline-color" are missing an argument.
// Don't print an error, for consistency with "set_color".
// In future we change both to actually print an error.
return Err(STATUS_INVALID_ARGS);
}
UnexpectedOptArg(option_index) => {
err_fmt!(Error::UNEXP_OPT_ARG, argv[option_index]).full_trailer(parser)
}
InvalidOptArg(name, value) => {
err_fmt!("%s: invalid option argument: %s", name, value)
}
UnknownColor(arg) => {
err_fmt!("Unknown color '%s'", arg)
}
UnknownUnderlineStyle(arg) => {
err_fmt!("invalid underline style: %s", arg)
}
UnknownOption(unknown_option_index) => {
err_fmt!(Error::UNKNOWN_OPT, argv[unknown_option_index]).full_trailer(parser)
}
InvalidFgArgCombination => {
err_fmt!(
"%s: option cannot be used with a non-option argument",
"--foreground"
)
}
InvalidFgPrintColorCombination => {
err_fmt!(Error::COMBO_EXCLUSIVE, "--foreground", "--print-colors",)
}
};
error.cmd(argv[0]).finish(streams);
return Err(STATUS_INVALID_ARGS);
}
};

View File

@@ -0,0 +1,261 @@
use std::borrow::Cow;
use crate::{
io::{IoStreams, OutputStream, StringOutputStream},
parser::Parser,
prelude::*,
wgettext_fmt,
};
use fish_widestring::wstr;
#[macro_export]
macro_rules! err_fmt {
(
$string:expr // format string (literal or LocalizableString)
$(, $args:expr)* // list of expressions
$(,)? // optional trailing comma
) => {
$crate::builtins::error::Error::new(
$crate::wgettext_fmt!($string, $($args),*).into()
)
};
}
pub use err_fmt;
#[macro_export]
macro_rules! err_str {
(
$string:expr // format string (literal or LocalizableString)
$(,)? // optional trailing comma
) => {
$crate::builtins::error::Error::new(std::borrow::Cow::Borrowed($crate::wgettext!($string)))
};
}
pub use err_str;
/// Generate an `Error` from a string without localization. This is typically
/// for error messages from external sources (e.g. C `strerror()`)
#[macro_export]
macro_rules! err_raw {
(
$string:expr // owned WString
) => {
$crate::builtins::error::Error::new($string.into())
};
}
pub use err_raw;
pub struct Error<'a> {
msg: Cow<'a, wstr>,
cmd: Option<&'a wstr>,
subcmd: Option<&'a wstr>,
parser: Option<&'a Parser>,
hint: bool,
}
impl<'a> Error<'a> {
localizable_consts!(
/// Error message on missing argument.
pub MISSING_OPT_ARG
"%s: option requires an argument"
/// Error message on unexpected argument.
pub UNEXP_OPT_ARG
"%s: option does not take an argument"
/// Error message on missing man page.
pub MISSING_HELP
"missing man page\nDocumentation may not be installed.\n`help %s` will show an online version"
/// Error message on multiple scope levels for variables.
pub MULTIPLE_SCOPES
"scope can be only one of: universal function global local"
/// Error message for specifying both export and unexport to set/read.
pub EXPORT_UNEXPORT
"cannot both export and unexport"
/// Error message for specifying both path and unpath to set/read.
pub PATH_UNPATH
"cannot both path and unpath"
/// Error message for unknown switch.
pub UNKNOWN_OPT
"%s: unknown option"
/// Error message for invalid bind mode name.
pub BIND_MODE
"%s: invalid mode name. See `help %s`"
/// Error message when too many arguments are supplied to a builtin.
pub TOO_MANY_ARGUMENTS
"too many arguments"
/// Error message when integer expected
pub NOT_NUMBER
"%s: invalid integer"
/// Command that requires a subcommand was invoked without a recognized subcommand.
pub MISSING_SUBCMD
"missing subcommand"
pub INVALID_SUBCMD
"invalid subcommand"
pub INVALID_SUBSUBCMD
"%s: invalid subcommand"
/// Error messages for unexpected args.
pub MISSING_ARG
"missing argument"
pub UNEXP_ARG_COUNT
"expected %d arguments; got %d"
pub UNPEXP_ARG_COUNT_WITH_CTX
"%s: expected %d arguments; got %d"
pub MIN_ARG_COUNT
"expected >= %d arguments; got %d"
pub MAX_ARG_COUNT
"expected <= %d arguments; got %d"
/// Error message for invalid variable name.
pub INVALID_VARNAME
"%s: invalid variable name. See `help %s`"
/// Error message on invalid combination of options.
pub INVALID_OPT_COMBO
"invalid option combination"
pub INVALID_OPT_COMBO_WITH_CTX
"invalid option combination, %s"
pub COMBO_EXCLUSIVE
"%s %s: options cannot be used together"
pub REGEX_COMPILE
"Regular expression compile error: %s"
pub NO_SUITABLE_JOBS
"There are no suitable jobs"
pub COULD_NOT_FIND_JOB
"Could not find job '%d'"
pub STDIN_CLOSED
"stdin is closed"
pub INVALID_MAX_MATCHES
"Invalid max matches value '%s'"
pub INVALID_MAX_VALUE
"Invalid max value '%s'"
);
#[must_use]
pub fn new(msg: Cow<'a, wstr>) -> Self {
Error {
msg,
cmd: Default::default(),
subcmd: Default::default(),
parser: Default::default(),
hint: Default::default(),
}
}
#[must_use]
pub fn cmd(mut self, cmd: &'a wstr) -> Self {
self.cmd = Some(cmd);
self
}
#[must_use]
pub fn subcmd(mut self, cmd: &'a wstr, subcmd: &'a wstr) -> Self {
self.cmd = Some(cmd);
self.subcmd = Some(subcmd);
self
}
#[must_use]
pub fn stacktrace(mut self, parser: &'a Parser) -> Self {
self.parser = Some(parser);
self
}
#[must_use]
pub fn hint(mut self) -> Self {
self.hint = true;
self
}
// Convenience function for both stacktrace and hint
#[must_use]
pub fn full_trailer(self, parser: &'a Parser) -> Self {
self.stacktrace(parser).hint()
}
#[must_use]
pub fn append_to_msg(mut self, append: impl IntoCharIter) -> Self {
self.append_assign_to_msg(append);
self
}
pub fn append_assign_to_msg(&mut self, append: impl IntoCharIter) {
let s = self.msg.to_mut();
if !append.extend_wstring(s) {
s.extend(append.chars());
}
}
pub fn finish(self, streams: &mut IoStreams) {
self.write_to(streams.err);
}
pub fn to_string(&self) -> WString {
let mut out = OutputStream::String(StringOutputStream::new());
self.write_to(&mut out);
out.take()
}
pub fn write_to(&self, output: &mut OutputStream) {
self.write_msg(output);
self.write_stacktrace(output);
self.write_hint(output);
}
fn write_msg(&self, output: &mut OutputStream) {
let str: &wstr = match (self.cmd, self.subcmd) {
(None, _) => &self.msg,
(Some(cmd), None) => &wgettext_fmt!("%s: %s", cmd, &self.msg),
(Some(cmd), Some(subcmd)) => &wgettext_fmt!("%s %s: %s", cmd, subcmd, &self.msg),
};
output.appendln(str);
}
fn write_stacktrace(&self, output: &mut OutputStream) {
let Some(parser) = self.parser else {
return;
};
let stacktrace = parser.current_line();
if !stacktrace.is_empty() {
output.append('\n');
output.appendln(&stacktrace);
}
}
fn write_hint(&self, output: &mut OutputStream) {
if !self.hint {
return;
}
let Some(cmd) = self.cmd else {
return;
};
output.appendln(&wgettext_fmt!(
"(Type 'help %s' for related documentation)",
cmd
));
}
}

View File

@@ -1,15 +1,17 @@
use super::prelude::*;
use crate::common::{Named, bytes2wcstring, escape, get_by_sorted_name, str2wcstring};
use crate::fds::BorrowedFdFile;
use crate::io::OutputStream;
use crate::parse_constants::UNKNOWN_BUILTIN_ERR_MSG;
use crate::parse_util::argument_is_help;
use crate::parser::{BlockType, LoopStatus};
use crate::proc::{Pid, ProcStatus, no_exec};
use crate::{builtins::*, wutil};
use crate::{
builtins::{prelude::*, *},
err_fmt,
fds::BorrowedFdFile,
io::OutputStream,
parse_constants::UNKNOWN_BUILTIN_ERR_MSG,
parse_util::argument_is_help,
parser::{BlockType, LoopStatus},
proc::{Pid, ProcStatus, no_exec},
wutil,
};
use errno::errno;
use fish_common::assert_sorted_by_name;
use fish_widestring::L;
use fish_common::{Named, assert_sorted_by_name, escape, get_by_sorted_name};
use fish_widestring::{L, bytes2wcstring, str2wcstring};
use std::io::{BufRead as _, BufReader, Read as _};
pub type BuiltinCmd = fn(&Parser, &mut IoStreams, &mut [&wstr]) -> BuiltinResult;
@@ -19,104 +21,6 @@
L!("set_color green; echo -n read; set_color --reset; echo -n \"> \"");
localizable_consts!(
/// Error message on missing argument.
pub BUILTIN_ERR_MISSING
"%s: %s: option requires an argument"
/// Error message on unexpected argument.
pub BUILTIN_ERR_UNEXP_ARG
"%s: %s: option does not take an argument"
/// Error message on missing man page.
pub BUILTIN_ERR_MISSING_HELP
"fish: %s: missing man page\nDocumentation may not be installed.\n`help %s` will show an online version"
/// Error message on multiple scope levels for variables.
pub BUILTIN_ERR_GLOCAL
"%s: scope can be only one of: universal function global local"
/// Error message for specifying both export and unexport to set/read.
pub BUILTIN_ERR_EXPUNEXP
"%s: cannot both export and unexport"
/// Error message for specifying both path and unpath to set/read.
pub BUILTIN_ERR_PATHUNPATH
"%s: cannot both path and unpath"
/// Error message for unknown switch.
pub BUILTIN_ERR_UNKNOWN
"%s: %s: unknown option"
/// Error message for invalid bind mode name.
pub BUILTIN_ERR_BIND_MODE
"%s: %s: invalid mode name. See `help %s`"
/// Error message when too many arguments are supplied to a builtin.
pub BUILTIN_ERR_TOO_MANY_ARGUMENTS
"%s: too many arguments"
/// Error message when integer expected
pub BUILTIN_ERR_NOT_NUMBER
"%s: %s: invalid integer"
/// Command that requires a subcommand was invoked without a recognized subcommand.
pub BUILTIN_ERR_MISSING_SUBCMD
"%s: missing subcommand"
pub BUILTIN_ERR_INVALID_SUBCMD
"%s: %s: invalid subcommand"
pub BUILTIN_ERR_INVALID_SUBSUBCMD
"%s %s: %s: invalid subcommand"
/// Error messages for unexpected args.
pub BUILTIN_ERR_ARG_COUNT0
"%s: missing argument"
pub BUILTIN_ERR_ARG_COUNT1
"%s: expected %d arguments; got %d"
pub BUILTIN_ERR_ARG_COUNT2
"%s: %s: expected %d arguments; got %d"
pub BUILTIN_ERR_MIN_ARG_COUNT1
"%s: expected >= %d arguments; got %d"
pub BUILTIN_ERR_MAX_ARG_COUNT1
"%s: expected <= %d arguments; got %d"
/// Error message for invalid variable name.
pub BUILTIN_ERR_VARNAME
"%s: %s: invalid variable name. See `help %s`"
/// Error message on invalid combination of options.
pub BUILTIN_ERR_COMBO
"%s: invalid option combination"
pub BUILTIN_ERR_COMBO2
"%s: invalid option combination, %s"
pub BUILTIN_ERR_COMBO2_EXCLUSIVE
"%s: %s %s: options cannot be used together"
pub BUILTIN_ERR_REGEX_COMPILE
"%s: Regular expression compile error: %s"
pub BUILTIN_ERR_NO_SUITABLE_JOBS
"%s: There are no suitable jobs"
pub BUILTIN_ERR_COULD_NOT_FIND_JOB
"%s: Could not find job '%d'"
pub BUILTIN_ERR_STDIN_CLOSED
"%s: stdin is closed"
pub BUILTIN_ERR_INVALID_MAX_MATCHES
"%s: Invalid max matches value '%s'"
pub BUILTIN_ERR_INVALID_MAX_VALUE
"%s: Invalid max value '%s'"
/// The send stuff to foreground message.
pub FG_MSG
"Send job %d (%s) to foreground"
@@ -649,11 +553,17 @@ pub fn builtin_print_help(parser: &Parser, streams: &mut IoStreams, cmd: &wstr)
}
let name_esc = escape(cmd);
let cmd = sprintf!("__fish_print_help %s ", &name_esc);
let res = parser.eval(&cmd, streams.io_chain);
let res = parser.eval_with(
&cmd,
streams.io_chain,
streams.job_group.as_ref(),
BlockType::top,
false,
);
if res.status.normal_exited() && res.status.exit_code() == 2 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MISSING_HELP, name_esc, name_esc));
err_fmt!(Error::MISSING_HELP, name_esc)
.cmd(&name_esc)
.finish(streams);
}
}
@@ -665,38 +575,38 @@ pub fn builtin_unknown_option(
opt: &wstr,
print_hints: bool, /*=true*/
) {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_UNKNOWN, cmd, opt));
let mut err = err_fmt!(Error::UNKNOWN_OPT, opt).cmd(cmd);
if print_hints {
builtin_print_error_trailer(parser, streams.err, cmd);
err = err.full_trailer(parser);
}
err.finish(streams);
}
/// Perform error reporting for encounter with missing argument.
/// Perform error reporting for encounter with missing argument for subcommands.
pub fn builtin_missing_argument(
parser: &Parser,
streams: &mut IoStreams,
cmd: &wstr,
subcmd: Option<&wstr>,
mut opt: &wstr,
print_hints: bool, /*=true*/
) {
if opt.char_at(0) == '-' && opt.char_at(1) != '-' {
let mut err = if opt.char_at(0) == '-' && opt.char_at(1) != '-' {
// if c in -qc '-qc' is missing the argument, now opt is just 'c'
opt = &opt[opt.len() - 1..];
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_MISSING,
cmd,
L!("-").to_owned() + opt
));
err_fmt!(Error::MISSING_OPT_ARG, L!("-").to_owned() + opt)
} else {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MISSING, cmd, opt));
err_fmt!(Error::MISSING_OPT_ARG, opt)
};
if let Some(subcmd) = subcmd {
err = err.subcmd(cmd, subcmd);
} else {
err = err.cmd(cmd);
}
if print_hints {
builtin_print_error_trailer(parser, streams.err, cmd);
err = err.full_trailer(parser);
}
err.finish(streams);
}
/// Perform error reporting for encounter with an extra argument.
@@ -707,12 +617,11 @@ pub fn builtin_unexpected_argument(
opt: &wstr,
print_hints: bool, /*=true*/
) {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_UNEXP_ARG, cmd, opt));
let mut err = err_fmt!(Error::UNEXP_OPT_ARG, opt).cmd(cmd);
if print_hints {
builtin_print_error_trailer(parser, streams.err, cmd);
err = err.full_trailer(parser);
}
err.finish(streams);
}
/// Print the backtrace and call for help that we use at the end of error messages.
@@ -729,16 +638,8 @@ pub fn builtin_print_error_trailer(parser: &Parser, b: &mut OutputStream, cmd: &
));
}
/// This function works like perror, but it prints its result into the streams.err string instead
/// to stderr. Used by the builtin commands.
pub fn builtin_wperror(program_name: &wstr, streams: &mut IoStreams) {
let err = errno();
streams.err.append(program_name);
streams.err.append(L!(": "));
if err.0 != 0 {
let werr = str2wcstring(err.to_string());
streams.err.appendln(&werr);
}
pub fn builtin_strerror() -> WString {
str2wcstring(errno().to_string())
}
pub struct HelpOnlyCmdOpts {
@@ -770,6 +671,7 @@ pub fn parse(
parser,
streams,
cmd,
None,
args[w.wopt_index - 1],
print_hints,
);
@@ -991,11 +893,9 @@ fn parsed_pid(
match pid {
Ok(pid @ 1..) => Ok(Pid::new(pid)),
_ => {
streams.err.appendln(&wgettext_fmt!(
"%s: '%s' is not a valid process ID",
cmd,
arg
));
err_fmt!("'%s' is not a valid process ID", arg)
.cmd(cmd)
.finish(streams);
Err(STATUS_INVALID_ARGS)
}
}
@@ -1040,9 +940,9 @@ pub fn builtin_break_continue(
}
if argc != 1 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_UNKNOWN, argv[0], argv[1]));
err_fmt!(Error::UNKNOWN_OPT, argv[1])
.cmd(argv[0])
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -1112,11 +1012,13 @@ pub fn parse_from_opt(
arg: &wstr,
) -> Result<Self, ErrorCode> {
Self::try_from(arg).map_err(|()| {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid value for '--color' option: '%s'. Expected 'always', 'never', or 'auto'",
cmd,
err_fmt!(
"Invalid value for '--color' option: '%s'. Expected 'always', 'never', or 'auto'",
arg
));
)
.cmd(cmd)
.finish(streams);
STATUS_INVALID_ARGS
})
}

View File

@@ -0,0 +1,5 @@
pub mod error;
pub mod misc;
pub use error::*;
pub use misc::*;

View File

@@ -1,15 +1,11 @@
use std::os::fd::AsRawFd as _;
use crate::{
common::{FilenameRef, escape},
fds::wopen_cloexec,
nix::isatty,
parser::Block,
reader::reader_read,
};
use nix::{fcntl::OFlag, sys::stat::Mode};
use super::prelude::*;
use crate::{
builtins::error::Error, err_fmt, err_raw, err_str, fds::wopen_cloexec, nix::isatty,
parser::Block, reader::reader_read,
};
use fish_common::{FilenameRef, escape};
use nix::{fcntl::OFlag, sys::stat::Mode};
use std::os::fd::AsRawFd as _;
/// The source builtin, sometimes called `.`. Evaluates the contents of a file in the current
/// context.
@@ -37,18 +33,15 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
if argc == optind || args[optind] == "-" {
if streams.is_stdin_closed() {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_STDIN_CLOSED, cmd));
err_str!(Error::STDIN_CLOSED).cmd(cmd).finish(streams);
return Err(STATUS_CMD_ERROR);
}
// Either a bare `source` which means to implicitly read from stdin or an explicit `-`.
if argc == optind && isatty(streams.stdin_fd()) {
// Don't implicitly read from the terminal.
streams.err.appendln(&wgettext_fmt!(
"%s: missing filename argument or input redirection",
cmd
));
err_str!("missing filename argument or input redirection")
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
func_filename = FilenameRef::new(L!("-").to_owned());
@@ -60,12 +53,12 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
}
Err(_) => {
let esc = escape(args[optind]);
streams.err.appendln(&wgettext_fmt!(
"%s: Error encountered while sourcing file '%s':",
cmd,
&esc
));
builtin_wperror(cmd, streams);
err_fmt!("Error encountered while sourcing file '%s':", &esc)
.append_to_msg('\n')
.append_to_msg(&err_raw!(&builtin_strerror()).cmd(cmd).to_string())
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
}
}
@@ -97,11 +90,12 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
Ok(_) => BuiltinResult::from_dynamic(parser.get_last_status()),
Err(err) => {
let esc = escape(&func_filename);
streams.err.appendln(&wgettext_fmt!(
"%s: Error while reading file '%s'",
cmd,
err_fmt!(
"Error while reading file '%s'",
if esc == "-" { L!("<stdin>") } else { &esc }
));
)
.cmd(cmd)
.finish(streams);
Err(err)
}
}

View File

@@ -1,7 +1,10 @@
use super::prelude::*;
use crate::common::{bytes2wcstring, get_program_name, osstr2wcstring, str2wcstring};
use crate::builtins::error;
use crate::common::get_program_name;
use crate::env::config_paths::get_fish_path;
use crate::future_feature_flags::{self as features, feature_test};
use crate::err_fmt;
#[cfg(not(feature = "localize-messages"))]
use crate::err_raw;
use crate::proc::{
JobControl, get_job_control_mode, get_login, is_interactive_session, set_job_control_mode,
};
@@ -9,8 +12,9 @@
use crate::tty_handoff::{TERMINAL_OS_NAME, get_scroll_content_up_capability, xtversion};
use crate::wutil::{Error, waccess, wbasename, wdirname, wrealpath};
use cfg_if::cfg_if;
use fish_feature_flags::{self as features, feature_test};
use fish_util::wcsfilecmp_glob;
use fish_wcstringutil::wcs2bytes;
use fish_widestring::{bytes2wcstring, osstr2wcstring, str2wcstring, wcs2bytes};
use nix::unistd::AccessFlags;
use rust_embed::RustEmbed;
@@ -105,12 +109,13 @@ impl StatusCmdOpts {
fn try_set_status_cmd(&mut self, subcmd: StatusCmd, streams: &mut IoStreams) -> bool {
match self.status_cmd.replace(subcmd) {
Some(existing) => {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
"status",
err_fmt!(
error::Error::COMBO_EXCLUSIVE,
existing.to_wstr(),
subcmd.to_wstr(),
));
)
.cmd(L!("status"))
.finish(streams);
false
}
None => true,
@@ -171,7 +176,7 @@ fn default() -> Self {
];
localizable_consts! {
BUILTIN_INVALID_JOB_CONTROL_MODE "%s: Invalid job control mode '%s'"
BUILTIN_INVALID_JOB_CONTROL_MODE "Invalid job control mode '%s'"
}
/// Print the features and their values.
@@ -215,17 +220,15 @@ fn parse_cmd_opts(
match fish_wcstoi(arg) {
Ok(level) if level >= 0 => level,
Err(Error::Overflow) | Ok(_) => {
streams.err.appendln(&wgettext_fmt!(
"%s: Invalid level value '%s'",
cmd,
arg
));
err_fmt!("Invalid level value '%s'", arg)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
_ => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_NOT_NUMBER, cmd, arg));
err_fmt!(error::Error::NOT_NUMBER, arg)
.cmd(cmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -251,11 +254,9 @@ fn parse_cmd_opts(
return Err(STATUS_CMD_ERROR);
}
let Ok(job_mode) = w.woptarg.unwrap().try_into() else {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_INVALID_JOB_CONTROL_MODE,
cmd,
w.woptarg.unwrap()
));
err_fmt!(BUILTIN_INVALID_JOB_CONTROL_MODE, w.woptarg.unwrap())
.cmd(cmd)
.finish(streams);
return Err(STATUS_CMD_ERROR);
};
opts.new_job_control_mode = Some(job_mode);
@@ -287,7 +288,7 @@ fn parse_cmd_opts(
}
'h' => opts.print_help = true,
':' => {
builtin_missing_argument(parser, streams, cmd, args[w.wopt_index - 1], false);
builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
';' => {
@@ -363,9 +364,9 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
optind += 1;
}
None => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, args[1]));
err_fmt!(error::Error::INVALID_SUBCMD)
.subcmd(cmd, args[1])
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -399,35 +400,22 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
let job_control_mode = match opts.new_job_control_mode {
Some(j) => {
// Flag form used
if !args.is_empty() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
c.to_wstr(),
0,
args.len()
));
return Err(STATUS_INVALID_ARGS);
}
// Any extra args would have already failed, either as an
// unrecognized subcmd, or as a "subcmd combo"
assert!(args.is_empty(), "unexpected job-control args");
j
}
None => {
if args.len() != 1 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
c.to_wstr(),
1,
args.len()
));
err_fmt!(error::Error::UNEXP_ARG_COUNT, 1, args.len())
.subcmd(cmd, c.to_wstr())
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let Ok(new_mode) = args[0].try_into() else {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_INVALID_JOB_CONTROL_MODE,
cmd,
args[0]
));
err_fmt!(BUILTIN_INVALID_JOB_CONTROL_MODE, args[0])
.subcmd(cmd, c.to_wstr())
.finish(streams);
return Err(STATUS_CMD_ERROR);
};
new_mode
@@ -438,13 +426,9 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
STATUS_FEATURES => print_features(streams),
c @ STATUS_TEST_FEATURE => {
if args.len() != 1 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
c.to_wstr(),
1,
args.len()
));
err_fmt!(error::Error::UNEXP_ARG_COUNT, 1, args.len())
.subcmd(cmd, c.to_wstr())
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let mut retval = TestFeatureRetVal::TEST_FEATURE_NOT_RECOGNIZED;
@@ -460,13 +444,9 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
}
c @ STATUS_GET_FILE => {
if args.len() != 1 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
c.to_wstr(),
1,
args.len()
));
err_fmt!(error::Error::UNEXP_ARG_COUNT, 1, args.len())
.subcmd(cmd, c.to_wstr())
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
let arg = wcs2bytes(args[0]);
@@ -484,7 +464,8 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
STATUS_LANGUAGE => {
cfg_if! {
if #[cfg(not(feature = "localize-messages"))] {
streams.err.append(L!("fish was built with the `localize-messages` feature disabled. The `status language` command is unavailable.\n"));
err_raw!(L!("fish was built with the `localize-messages` feature disabled. The `status language` command is unavailable.").to_owned())
.finish(streams);
return Err(STATUS_CMD_ERROR);
} else {
if args.is_empty() {
@@ -513,9 +494,9 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
return Ok(SUCCESS);
}
invalid => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_INVALID_SUBSUBCMD, cmd, subcmd.to_wstr(), invalid));
err_fmt!(error::Error::INVALID_SUBSUBCMD, invalid)
.subcmd(cmd, subcmd.to_wstr())
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
}
@@ -557,22 +538,15 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
}
c @ STATUS_TEST_TERMINAL_FEATURE => {
if args.len() != 1 {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
c.to_wstr(),
1,
args.len()
));
err_fmt!(error::Error::UNEXP_ARG_COUNT, 1, args.len())
.subcmd(cmd, c.to_wstr())
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if args[0] != "scroll-content-up" {
streams.err.appendln(&wgettext_fmt!(
"%s %s: unrecognized feature '%s'",
cmd,
c.to_wstr(),
args[0]
));
err_fmt!("unrecognized feature '%s'", args[0])
.subcmd(cmd, c.to_wstr())
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
return if get_scroll_content_up_capability() == Some(true) {
@@ -584,13 +558,9 @@ pub fn status(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
ref s => {
if !args.is_empty() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_ARG_COUNT2,
cmd,
s.to_wstr(),
0,
args.len()
));
err_fmt!(error::Error::UNEXP_ARG_COUNT, 0, args.len())
.subcmd(cmd, s.to_wstr())
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
match s {

View File

@@ -1,4 +1,4 @@
use crate::screen::escape_code_length;
use crate::{err_fmt, err_raw, err_str, screen::escape_code_length};
use fish_wcstringutil::fish_wcwidth_visible;
// Forward some imports to make subcmd implementations easier
use super::prelude::*;
@@ -21,35 +21,12 @@
#[cfg(test)]
mod test_helpers;
macro_rules! string_error {
(
$streams:expr,
$string:expr
$(, $args:expr)+
$(,)?
) => {
$streams.err.append(L!("string "));
$streams.err.appendln(&wgettext_fmt!($string, $($args),*));
};
}
use string_error;
fn string_unknown_option(parser: &Parser, streams: &mut IoStreams, subcmd: &wstr, opt: &wstr) {
string_error!(streams, BUILTIN_ERR_UNKNOWN, subcmd, opt);
builtin_print_error_trailer(parser, streams.err, L!("string"));
}
trait StringSubCommand<'args> {
const SHORT_OPTIONS: &'static wstr;
const LONG_OPTIONS: &'static [WOption<'static>];
/// Parse and store option specified by the associated short or long option.
fn parse_opt(
&mut self,
name: &wstr,
c: char,
arg: Option<&'args wstr>,
) -> Result<(), StringError>;
fn parse_opt(&mut self, c: char, arg: Option<&'args wstr>) -> Result<(), StringError<'_>>;
fn parse_opts(
&mut self,
@@ -57,7 +34,8 @@ fn parse_opts(
parser: &Parser,
streams: &mut IoStreams,
) -> Result<usize, ErrorCode> {
let cmd = args[0];
let cmd = L!("string");
let subcmd = args[0];
let mut args_read = Vec::with_capacity(args.len());
args_read.extend_from_slice(args);
@@ -65,36 +43,34 @@ fn parse_opts(
while let Some(c) = w.next_opt() {
match c {
':' => {
streams.err.append(L!("string ")); // clone of string_error
builtin_missing_argument(
parser,
streams,
cmd,
Some(subcmd),
args_read[w.wopt_index - 1],
false,
);
return Err(STATUS_INVALID_ARGS);
}
';' => {
streams.err.append(L!("string ")); // clone of string_error
builtin_unexpected_argument(
parser,
streams,
cmd,
args_read[w.wopt_index - 1],
false,
);
err_fmt!(Error::UNEXP_OPT_ARG, args_read[w.wopt_index - 1])
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
'?' => {
string_unknown_option(parser, streams, cmd, args_read[w.wopt_index - 1]);
err_fmt!(Error::UNKNOWN_OPT, args_read[w.wopt_index - 1])
.subcmd(cmd, subcmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
c => {
let retval = self.parse_opt(cmd, c, w.woptarg);
let retval = self.parse_opt(c, w.woptarg);
if let Err(e) = retval {
e.print_error(&args_read, parser, streams, w.woptarg, w.wopt_index);
return Err(e.retval());
e.print_error(&args_read, streams, w.wopt_index);
return Err(STATUS_INVALID_ARGS);
}
}
}
@@ -103,6 +79,11 @@ fn parse_opts(
Ok(w.wopt_index)
}
fn parse_arg_number<'a, 'b>(arg: &'a wstr) -> Result<i64, StringError<'b>> {
let n = fish_wcstol(arg).map_err(|_| err_fmt!(Error::NOT_NUMBER, arg))?;
Ok(n)
}
/// Take any positional arguments after options have been parsed.
#[allow(unused_variables)]
fn take_args(
@@ -154,7 +135,9 @@ fn run_impl(
self.take_args(&mut optind, args, streams)?;
if streams.stdin_is_directly_redirected && args.len() > optind {
string_error!(streams, BUILTIN_ERR_TOO_MANY_ARGUMENTS, args[0]);
err_str!(Error::TOO_MANY_ARGUMENTS)
.subcmd(L!("string"), args[0])
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -163,9 +146,8 @@ fn run_impl(
}
/// This covers failing argument/option parsing
enum StringError {
InvalidArgs(WString),
NotANumber,
enum StringError<'a> {
InvalidArgs(Error<'a>),
UnknownOption,
}
@@ -177,88 +159,66 @@ enum RegexError {
impl RegexError {
fn print_error(&self, args: &[&wstr], streams: &mut IoStreams) {
let cmd = args[0];
let cmd = L!("string");
let subcmd = args[0];
use RegexError::*;
match self {
Compile(pattern, e) => {
string_error!(
streams,
BUILTIN_ERR_REGEX_COMPILE,
cmd,
&WString::from(e.error_message())
);
string_error!(streams, "%s: %s", cmd, pattern);
// TODO: This is misaligned if `pattern` contains characters which are not exactly 1
// terminal cell wide.
let mut marker = " ".repeat(e.offset().unwrap_or(0).saturating_sub(1));
let mut marker: WString =
" ".repeat(e.offset().unwrap_or(0).saturating_sub(1)).into();
marker.push('^');
string_error!(streams, "%s: %s", cmd, marker);
err_fmt!(Error::REGEX_COMPILE, e.error_message())
.append_to_msg('\n')
.append_to_msg(&err_raw!(pattern).subcmd(cmd, subcmd).to_string())
.append_to_msg('\n')
.append_to_msg(&err_raw!(marker).subcmd(cmd, subcmd).to_string())
.subcmd(cmd, subcmd)
.finish(streams);
}
InvalidCaptureGroupName(name) => {
streams.err.appendln(&wgettext_fmt!(
err_fmt!(
"Modification of read-only variable \"%s\" is not allowed",
name
));
)
.finish(streams);
}
InvalidEscape(pattern) => {
string_error!(
streams,
"%s",
sprintf!(
"%s: Invalid escape sequence in pattern \"%s\"",
cmd,
pattern
)
);
err_fmt!("Invalid escape sequence in pattern \"%s\"", pattern)
.subcmd(cmd, subcmd)
.finish(streams);
}
}
}
}
impl From<crate::wutil::wcstoi::Error> for StringError {
fn from(_: crate::wutil::wcstoi::Error) -> Self {
StringError::NotANumber
impl<'a> From<error::Error<'a>> for StringError<'a> {
fn from(error: error::Error<'a>) -> Self {
StringError::InvalidArgs(error)
}
}
macro_rules! invalid_args {
($msg:expr, $name:expr, $arg:expr) => {
StringError::InvalidArgs(crate::localization::wgettext_fmt!(
$msg,
$name,
$arg.unwrap()
))
};
}
use invalid_args;
impl StringError {
fn print_error(
&self,
args: &[&wstr],
parser: &Parser,
streams: &mut IoStreams,
optarg: Option<&wstr>,
optind: usize,
) {
let cmd = args[0];
impl<'a> StringError<'a> {
fn print_error(self, args: &[&wstr], streams: &mut IoStreams, optind: usize) {
let cmd = L!("string");
let subcmd = args[0];
use StringError::*;
match self {
InvalidArgs(msg) => {
streams.err.appendln("string ".chars().chain(msg.chars()));
}
NotANumber => {
string_error!(streams, BUILTIN_ERR_NOT_NUMBER, cmd, optarg.unwrap());
InvalidArgs(err) => {
err.subcmd(cmd, subcmd).finish(streams);
}
UnknownOption => {
string_unknown_option(parser, streams, cmd, args[optind - 1]);
// This would mean the subcmd's XXX_OPTIONS does not match
// the list in its `parse_opt()` implementation
unreachable!(
"unexpected option '{}' for 'string {subcmd}'",
args[optind - 1]
)
}
}
}
fn retval(&self) -> ErrorCode {
STATUS_INVALID_ARGS
}
}
#[derive(Default, PartialEq, Clone, Copy)]
@@ -317,10 +277,10 @@ pub fn string(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
let argc = args.len();
if argc <= 1 {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_MISSING_SUBCMD, cmd));
builtin_print_error_trailer(parser, streams.err, cmd);
err_str!(Error::MISSING_SUBCMD)
.cmd(cmd)
.full_trailer(parser)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -366,10 +326,10 @@ pub fn string(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
}
.run(parser, streams, args),
_ => {
streams
.err
.appendln(&wgettext_fmt!(BUILTIN_ERR_INVALID_SUBCMD, cmd, args[0]));
builtin_print_error_trailer(parser, streams.err, cmd);
err_fmt!(Error::INVALID_SUBCMD)
.subcmd(cmd, subcmd_name)
.full_trailer(parser)
.finish(streams);
Err(STATUS_INVALID_ARGS)
}
}

View File

@@ -13,7 +13,7 @@ impl StringSubCommand<'_> for Collect {
];
const SHORT_OPTIONS: &'static wstr = L!("Na");
fn parse_opt(&mut self, _n: &wstr, c: char, _arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, _arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'a' => self.allow_empty = true,
'N' => self.no_trim_newlines = true,

View File

@@ -1,5 +1,5 @@
use super::*;
use crate::common::{EscapeFlags, EscapeStringStyle, escape_string};
use fish_common::{EscapeFlags, EscapeStringStyle, escape_string};
#[derive(Default)]
pub struct Escape {
@@ -14,14 +14,14 @@ impl StringSubCommand<'_> for Escape {
];
const SHORT_OPTIONS: &'static wstr = L!("n");
fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'n' => self.no_quoted = true,
NON_OPTION_CHAR => {
self.style = arg
.unwrap()
.try_into()
.map_err(|_| invalid_args!("%s: Invalid escape style '%s'", name, arg))?;
.map_err(|_| err_fmt!("Invalid escape style '%s'", arg.unwrap()))?;
}
_ => return Err(StringError::UnknownOption),
}

View File

@@ -25,7 +25,7 @@ impl<'args> StringSubCommand<'args> for Join<'args> {
];
const SHORT_OPTIONS: &'static wstr = L!("qn");
fn parse_opt(&mut self, _n: &wstr, c: char, _arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, _arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'q' => self.quiet = true,
'n' => self.no_empty = true,
@@ -44,8 +44,13 @@ fn take_args(
return Ok(());
}
let cmd = L!("string");
let subcmd = args[0];
let Some(arg) = args.get(*optind).copied() else {
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]);
err_str!(Error::MISSING_ARG)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*optind += 1;

View File

@@ -13,7 +13,7 @@ impl StringSubCommand<'_> for Length {
];
const SHORT_OPTIONS: &'static wstr = L!("qV");
fn parse_opt(&mut self, _n: &wstr, c: char, _arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, _arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'q' => self.quiet = true,
'V' => self.visible = true,

View File

@@ -3,12 +3,14 @@
use std::num::NonZeroUsize;
use super::*;
use crate::common::str2wcstring;
use crate::env::{EnvVar, EnvVarFlags};
use crate::flog::flog;
use crate::parse_util::unescape_wildcards;
use crate::parser::ParserEnvSetMode;
use crate::wildcard::{ANY_STRING, wildcard_match};
use crate::{
env::{EnvVar, EnvVarFlags},
flog::flog,
parse_util::unescape_wildcards,
parser::ParserEnvSetMode,
wildcard::wildcard_match,
};
use fish_widestring::{ANY_STRING, str2wcstring};
#[derive(Default)]
pub struct Match<'args> {
@@ -38,7 +40,7 @@ impl<'args> StringSubCommand<'args> for Match<'args> {
];
const SHORT_OPTIONS: &'static wstr = L!("aegivqrnm:");
fn parse_opt(&mut self, _n: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'a' => self.all = true,
'e' => self.entire = true,
@@ -55,11 +57,7 @@ fn parse_opt(&mut self, _n: &wstr, c: char, arg: Option<&wstr>) -> Result<(), St
.ok()
.and_then(|v| NonZeroUsize::new(v as usize))
.ok_or_else(|| {
StringError::InvalidArgs(wgettext_fmt!(
BUILTIN_ERR_INVALID_MAX_MATCHES,
_n,
arg
))
StringError::InvalidArgs(err_fmt!(Error::INVALID_MAX_MATCHES, arg))
})?;
Some(max)
}
@@ -75,9 +73,12 @@ fn take_args(
args: &[&'args wstr],
streams: &mut IoStreams,
) -> Result<(), ErrorCode> {
let cmd = args[0];
let cmd = L!("string");
let subcmd = args[0];
let Some(arg) = args.get(*optind).copied() else {
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, cmd);
err_fmt!(Error::MISSING_ARG)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*optind += 1;
@@ -92,32 +93,36 @@ fn handle(
optind: &mut usize,
args: &[&wstr],
) -> Result<(), ErrorCode> {
let cmd = args[0];
let cmd = L!("string");
let subcmd = args[0];
if self.entire && self.index {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--entire and --index are mutually exclusive")
));
)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if self.invert_match && self.groups_only {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--invert and --groups-only are mutually exclusive")
));
)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
if self.entire && self.groups_only {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--entire and --groups-only are mutually exclusive")
));
)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
@@ -414,9 +419,9 @@ fn report_matches(&mut self, arg: &wstr, streams: &mut IoStreams) {
#[cfg(test)]
mod tests {
use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS};
use crate::future_feature_flags::{FeatureFlag, scoped_test};
use crate::tests::prelude::*;
use crate::validate;
use fish_feature_flags::{FeatureFlag, with_overridden_feature};
#[test]
#[serial]
@@ -487,7 +492,7 @@ fn plain() {
#[serial]
#[rustfmt::skip]
fn test_qmark_noglob_true() {
scoped_test(FeatureFlag::QuestionMarkNoGlob, true, || {
with_overridden_feature(FeatureFlag::QuestionMarkNoGlob, true, || {
validate!(["string", "match", "a*b?c", "axxb?c"], STATUS_CMD_OK, "axxb?c\n");
validate!(["string", "match", "*?", "a"], STATUS_CMD_ERROR, "");
validate!(["string", "match", "*?", "ab"], STATUS_CMD_ERROR, "");
@@ -515,7 +520,7 @@ fn test_qmark_noglob_true() {
#[serial]
#[rustfmt::skip]
fn test_qmark_glob() {
scoped_test(FeatureFlag::QuestionMarkNoGlob, false, || {
with_overridden_feature(FeatureFlag::QuestionMarkNoGlob, false, || {
validate!(["string", "match", "a*b?c", "axxbyc"], STATUS_CMD_OK, "axxbyc\n");
validate!(["string", "match", "*?", "a"], STATUS_CMD_OK, "a\n");
validate!(["string", "match", "*?", "ab"], STATUS_CMD_OK, "ab\n");

View File

@@ -32,32 +32,29 @@ impl StringSubCommand<'_> for Pad {
];
const SHORT_OPTIONS: &'static wstr = L!("c:rCw:");
fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'c' => {
let [pad_char] = arg.unwrap().as_char_slice() else {
return Err(invalid_args!(
"%s: Padding should be a character '%s'",
name,
arg
));
let arg = arg.unwrap();
let [pad_char] = arg.as_char_slice() else {
return Err(err_fmt!("Padding should be a character '%s'", arg).into());
};
self.pad_char_width = match fish_wcwidth(*pad_char) {
None | Some(0) => {
return Err(
err_fmt!("Invalid padding character of width zero '%s'", arg).into(),
);
}
Some(w) => w,
};
let pad_char_width = fish_wcwidth(*pad_char);
if pad_char_width <= 0 {
return Err(invalid_args!(
"%s: Invalid padding character of width zero '%s'",
name,
arg
));
}
self.pad_char_width = pad_char_width as usize;
self.char_to_pad = *pad_char;
}
'r' => self.pad_from = Direction::Right,
'w' => {
self.width = fish_wcstol(arg.unwrap())?
let arg = arg.unwrap();
self.width = Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!("%s: Invalid width value '%s'", name, arg))?;
.map_err(|_| err_fmt!("Invalid width value '%s'", arg))?;
}
'C' => self.center = true,
_ => return Err(StringError::UnknownOption),

View File

@@ -17,20 +17,22 @@ impl StringSubCommand<'_> for Repeat {
];
const SHORT_OPTIONS: &'static wstr = L!("n:m:qN");
fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'n' => {
let arg = arg.unwrap();
self.count = Some(
fish_wcstol(arg.unwrap())?
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!("%s: Invalid count value '%s'", name, arg))?,
.map_err(|_| err_fmt!("Invalid count value '%s'", arg))?,
);
}
'm' => {
let arg = arg.unwrap();
self.max = Some(
fish_wcstol(arg.unwrap())?
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!(BUILTIN_ERR_INVALID_MAX_VALUE, name, arg))?,
.map_err(|_| err_fmt!(Error::INVALID_MAX_VALUE, arg))?,
);
}
'q' => self.quiet = true,
@@ -50,16 +52,21 @@ fn take_args(
return Ok(());
}
let name = args[0];
let cmd = L!("string");
let subcmd = args[0];
let Some(arg) = args.get(*optind) else {
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, name);
err_fmt!(Error::MISSING_ARG)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*optind += 1;
let Ok(Ok(count)) = fish_wcstol(arg).map(|count| count.try_into()) else {
string_error!(streams, "%s: Invalid count value '%s'", name, arg);
err_fmt!("Invalid count value '%s'", arg)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};

View File

@@ -3,7 +3,7 @@
use pcre2::utf32::{Regex, RegexBuilder};
use super::*;
use crate::future_feature_flags::{FeatureFlag, feature_test};
use fish_feature_flags::{FeatureFlag, feature_test};
#[derive(Default)]
pub struct Replace<'args> {
@@ -28,7 +28,7 @@ impl<'args> StringSubCommand<'args> for Replace<'args> {
];
const SHORT_OPTIONS: &'static wstr = L!("afiqrm:");
fn parse_opt(&mut self, _n: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'a' => self.all = true,
'f' => self.filter = true,
@@ -42,11 +42,7 @@ fn parse_opt(&mut self, _n: &wstr, c: char, arg: Option<&wstr>) -> Result<(), St
.ok()
.and_then(|v| NonZeroUsize::new(v as usize))
.ok_or_else(|| {
StringError::InvalidArgs(wgettext_fmt!(
BUILTIN_ERR_INVALID_MAX_MATCHES,
_n,
arg
))
StringError::InvalidArgs(err_fmt!(Error::INVALID_MAX_MATCHES, arg))
})?;
Some(max)
}
@@ -62,14 +58,19 @@ fn take_args(
args: &[&'args wstr],
streams: &mut IoStreams,
) -> Result<(), ErrorCode> {
let cmd = args[0];
let cmd = L!("string");
let subcmd = args[0];
let Some(pattern) = args.get(*optind).copied() else {
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, cmd);
err_str!(Error::MISSING_ARG)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*optind += 1;
let Some(replacement) = args.get(*optind).copied() else {
string_error!(streams, BUILTIN_ERR_ARG_COUNT1, cmd, 1, 2);
err_fmt!(Error::UNEXP_ARG_COUNT, 1, 2)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*optind += 1;
@@ -86,7 +87,8 @@ fn handle(
optind: &mut usize,
args: &[&wstr],
) -> Result<(), ErrorCode> {
let cmd = args[0];
let cmd = L!("string");
let subcmd = args[0];
let replacer = match StringReplacer::new(self.pattern, self.replacement, self) {
Ok(x) => x,
@@ -102,12 +104,9 @@ fn handle(
let (replaced, result) = match replacer.replace(arg) {
Ok(x) => x,
Err(e) => {
string_error!(
streams,
"%s: Regular expression substitute error: %s",
cmd,
e.error_message()
);
err_fmt!("Regular expression substitute error: %s", e.error_message())
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}
};
@@ -155,7 +154,7 @@ enum StringReplacer<'args, 'opts> {
impl<'args, 'opts> StringReplacer<'args, 'opts> {
fn interpret_escape(arg: &'args wstr) -> Option<WString> {
use crate::common::read_unquoted_escape;
use fish_common::read_unquoted_escape;
let mut result: WString = WString::with_capacity(arg.len());
let mut cursor = arg;
@@ -194,7 +193,7 @@ fn new(
replacement.to_owned()
} else {
Self::interpret_escape(replacement)
.ok_or_else(|| RegexError::InvalidEscape(pattern.to_owned()))?
.ok_or_else(|| RegexError::InvalidEscape(replacement.to_owned()))?
};
Self::Regex {
replacement,

View File

@@ -37,22 +37,18 @@ impl<'args> StringSubCommand<'args> for Shorten<'args> {
];
const SHORT_OPTIONS: &'static wstr = L!("c:m:Nlq");
fn parse_opt(
&mut self,
name: &wstr,
c: char,
arg: Option<&'args wstr>,
) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&'args wstr>) -> Result<(), StringError<'_>> {
match c {
'c' => {
self.ellipsis = arg.unwrap();
self.ellipsis_width = width_without_escapes(self.ellipsis, 0);
}
'm' => {
let arg = arg.unwrap();
self.max = Some(
fish_wcstol(arg.unwrap())?
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!(BUILTIN_ERR_INVALID_MAX_VALUE, name, arg))?,
.map_err(|_| err_fmt!(Error::INVALID_MAX_VALUE, arg))?,
);
}
'N' => self.no_newline = true,

View File

@@ -112,24 +112,26 @@ impl<'args> StringSubCommand<'args> for Split<'args> {
];
const SHORT_OPTIONS: &'static wstr = L!("qrm:nf:a");
fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'q' => self.quiet = true,
'r' => self.split_from = Direction::Right,
'm' => {
self.max = fish_wcstol(arg.unwrap())?
let arg = arg.unwrap();
self.max = Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!(BUILTIN_ERR_INVALID_MAX_VALUE, name, arg))?;
.map_err(|_| err_fmt!(Error::INVALID_MAX_VALUE, arg))?;
}
'n' => self.no_empty = true,
'f' => {
self.fields = arg.unwrap().try_into().map_err(|e| match e {
FieldParseError::Number => StringError::NotANumber,
let arg = arg.unwrap();
self.fields = arg.try_into().map_err(|e| match e {
FieldParseError::Number => err_fmt!(Error::NOT_NUMBER, arg),
FieldParseError::Range => {
invalid_args!("%s: Invalid range value for field '%s'", name, arg)
err_fmt!("Invalid range value for field '%s'", arg)
}
FieldParseError::Field => {
invalid_args!("%s: Invalid fields value '%s'", name, arg)
err_fmt!("Invalid fields value '%s'", arg)
}
})?;
}
@@ -148,8 +150,13 @@ fn take_args(
if self.is_split0 {
return Ok(());
}
let cmd = L!("string");
let subcmd = args[0];
let Some(arg) = args.get(*optind).copied() else {
string_error!(streams, BUILTIN_ERR_ARG_COUNT0, args[0]);
err_str!(Error::MISSING_ARG)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*optind += 1;
@@ -164,12 +171,15 @@ fn handle(
optind: &mut usize,
args: &[&'args wstr],
) -> Result<(), ErrorCode> {
let cmd = L!("string");
let subcmd = args[0];
if self.fields.is_empty() && self.allow_empty {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
args[0],
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--allow-empty is only valid with --fields")
));
)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}

View File

@@ -19,27 +19,30 @@ impl StringSubCommand<'_> for Sub {
];
const SHORT_OPTIONS: &'static wstr = L!("l:qs:e:");
fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'l' => {
let arg = arg.unwrap();
self.length = Some(
fish_wcstol(arg.unwrap())?
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!("%s: Invalid length value '%s'", name, arg))?,
.map_err(|_| err_fmt!("Invalid length value '%s'", arg))?,
);
}
's' => {
let arg = arg.unwrap();
self.start = Some(
fish_wcstol(arg.unwrap())?
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!("%s: Invalid start value '%s'", name, arg))?,
.map_err(|_| err_fmt!("Invalid start value '%s'", arg))?,
);
}
'e' => {
let arg = arg.unwrap();
self.end = Some(
fish_wcstol(arg.unwrap())?
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| invalid_args!("%s: Invalid end value '%s'", name, arg))?,
.map_err(|_| err_fmt!("Invalid end value '%s'", arg))?,
);
}
'q' => self.quiet = true,
@@ -55,13 +58,15 @@ fn handle(
optind: &mut usize,
args: &[&wstr],
) -> Result<(), ErrorCode> {
let cmd = args[0];
let cmd = L!("string");
let subcmd = args[0];
if self.length.is_some() && self.end.is_some() {
streams.err.appendln(&wgettext_fmt!(
BUILTIN_ERR_COMBO2,
cmd,
err_fmt!(
Error::INVALID_OPT_COMBO_WITH_CTX,
wgettext!("--end and --length are mutually exclusive")
));
)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
}

View File

@@ -1,15 +1,16 @@
use super::string;
use crate::builtins::shared::BuiltinResultExt as _;
use crate::io::IoChain;
use crate::io::{IoStreams, OutputStream, StringOutputStream};
use crate::prelude::*;
use crate::tests::prelude::*;
use crate::{
builtins::shared::BuiltinResultExt as _,
io::{IoChain, IoStreams, OutputStream, StringOutputStream},
prelude::*,
tests::prelude::*,
};
#[macro_export]
macro_rules! validate {
( [$($argv:expr),*], $expected_rc:expr, $expected_out:expr ) => {
{
use $crate::common::escape;
use fish_common::escape;
use $crate::prelude::*;
use $crate::builtins::string::test_helpers::string_test;
let (actual_out, actual_rc) = string_test(vec![$(L!($argv)),*]);

View File

@@ -8,7 +8,7 @@ pub struct Transform {
impl StringSubCommand<'_> for Transform {
const LONG_OPTIONS: &'static [WOption<'static>] = &[wopt(L!("quiet"), NoArgument, 'q')];
const SHORT_OPTIONS: &'static wstr = L!("q");
fn parse_opt(&mut self, _n: &wstr, c: char, _arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, _arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'q' => self.quiet = true,
_ => return Err(StringError::UnknownOption),

View File

@@ -28,12 +28,7 @@ impl<'args> StringSubCommand<'args> for Trim<'args> {
];
const SHORT_OPTIONS: &'static wstr = L!("c:lrq");
fn parse_opt(
&mut self,
_n: &wstr,
c: char,
arg: Option<&'args wstr>,
) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&'args wstr>) -> Result<(), StringError<'_>> {
match c {
'c' => self.chars_to_trim = arg.unwrap(),
'l' => self.left = true,

View File

@@ -1,5 +1,5 @@
use super::*;
use crate::common::{UnescapeStringStyle, unescape_string};
use fish_common::{UnescapeStringStyle, unescape_string};
#[derive(Default)]
pub struct Unescape {
@@ -16,14 +16,14 @@ impl StringSubCommand<'_> for Unescape {
];
const SHORT_OPTIONS: &'static wstr = L!("n");
fn parse_opt(&mut self, name: &wstr, c: char, arg: Option<&wstr>) -> Result<(), StringError> {
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'n' => self.no_quoted = true,
NON_OPTION_CHAR => {
let arg = arg.unwrap();
self.style = arg
.unwrap()
.try_into()
.map_err(|_| invalid_args!("%s: Invalid style value '%s'", name, arg))?;
.map_err(|_| err_fmt!("Invalid style value '%s'", arg))?;
}
_ => return Err(StringError::UnknownOption),
}

Some files were not shown because too many files have changed in this diff Show More