Compare commits

...

112 Commits
4.3.0 ... 4.3.3

Author SHA1 Message Date
Johannes Altmanninger
c98fd886fd Release 4.3.3
Created by ./build_tools/release.sh 4.3.3
2026-01-07 08:34:20 +01:00
Johannes Altmanninger
94fdb36f6b Revert "cmake: rename WITH_GETTEXT to WITH_MESSAGE_LOCALIZATION"
This reverts commit 77e1aead40 for the
patch release.
2026-01-07 08:32:33 +01:00
Johannes Altmanninger
d1a40ace7d changelog: fix RST syntax 2026-01-07 08:32:20 +01:00
Johannes Altmanninger
dd4d69a288 cirrus: fix FreeBSD pkg install failure
Multiple PRs fail with

	pkg: Repository FreeBSD-ports cannot be opened. 'pkg update' required
	Updating database digests format: . done
	pkg: No packages available to install matching 'cmake-core' have been found in the repositories
2026-01-07 08:01:21 +01:00
Daniel Rainer
e16ea8df11 sync: once_cell::sync::OnceCell -> std::sync::OnceLock
Rust 1.70 stabilized `std::sync::OnceLock`, which replaces
`once_cell::sync::OnceCell`.

With this, we only have a single remaining direct dependency on
`once_cell`: `VAR_DISPATCH_TABLE` in `src/env_dispatch.rs`, where we use
`Lazy::get`. This can be replaced with `LazyLock::get` once our MSRV
reaches 1.94, where the function is stabilized.

At the moment, `serial_test` depends on `once_cell`, so even if we
eliminate it as a direct dependency, it will remain a transitive
dependency.

Closes #12289
2026-01-07 07:59:24 +01:00
Daniel Rainer
80e1942980 sync: once_cell::sync::Lazy -> std::sync::LazyLock
Rust 1.80 stabilized `std::sync::LazyLock`, which replaces
`once_cell::sync::Lazy`. There is one exception in
`src/env_dispatch.rs`, which still uses the `once_cell` variant, since
the code there relies on `Lazy::get`, which also exists for `LazyLock`,
but will only be stabilized in Rust 1.94, so we can't use it yet.

Part of #12289
2026-01-07 07:59:24 +01:00
Johannes Altmanninger
99109278a6 Enable color theme reporting again on tmux >= 3.7
Color theme reporting has race conditions, so we might want to disable
it until we have fixed those. Not sure.

At least the tmux-specific issue hsa been fixed,
so treat new tmux like other terminals.
See https://github.com/tmux/tmux/issues/4787#issuecomment-3716135010

See #12261
2026-01-07 07:59:24 +01:00
Johannes Altmanninger
f924a880c8 Update changelog 2026-01-07 07:59:23 +01:00
Johannes Altmanninger
5683d26d24 github: update pull request template
Repeat here that 'Fixes #' should go into commit message, and remove
redundant PR description.
2026-01-07 07:35:30 +01:00
Lumynous
740aef06df l10n(zh-TW): Complete untranslated strings
Closes #12288
2026-01-07 07:35:30 +01:00
Daniel Rainer
557f6d1743 check: allow overriding default Rust toolchain
This is useful for running the checks with a toolchain which is
different from the default toolchain, for example to check if everything
works with our MSRV, or on beta/nightly toolchains. Additionally,
providing a way to run using the nightly toolchain allows writing
wrappers around `check.sh` which make use of nightly-only features.

The toolchain could be changed using `rustup toolchain default`, but if
the toolchain should only be used for a specific run, this is
inconvenient, and it does not allow for concurrent builds using
different toolchains.

Closes #12281
2026-01-06 17:54:56 +00:00
Johannes Altmanninger
8d257f5c57 completions/fastboot: one item per line 2026-01-06 14:29:32 +01:00
Johannes Altmanninger
d880a14b1a changelog: add sections 2026-01-06 14:29:32 +01:00
Next Alone
d4fcc00821 completions(fastboot): sync partitions from Xiaomi images
Signed-off-by: Next Alone <12210746+NextAlone@users.noreply.github.com>

Closes #12283
2026-01-06 14:29:32 +01:00
Johannes Altmanninger
6d8bb292ec fish_config theme choose: overwrite color-aware theme's vars also if called from config
Webconfig persists themes to ~/.config/fish/conf.d/fish_frozen_theme.fish
(the name is due to historical reasons).

That file's color variables have no "--theme=foo" annotations, which
means that fish_config can't distinguish them from other "user-set"
values.  We can change this in future, but that doesn't affect the
following fix.

A "fish_config theme choose foo" command is supposed to
overwrite all variables that are defined in "foo.theme".
If the theme is color-theme-aware *and* this command runs before
$fish_terminal_color_theme is initialized, we delay loading of the
theme until that initialization happens.  But the --on-variable
invocation won't have the override bit set, thus it will not touch
variables that don't have "--theme=*" value.  Fix this by clearing
immediately the variables mentioned in the theme.

Fixes #12278

While at it, tweak the error message for this command because it's
not an internal error:

	fish -c 'echo yes | fish_config theme save tomorrow'
2026-01-06 14:29:32 +01:00
Lennard Hofmann
c638401469 Speedup syntax highlighting of redirection targets
Instead of checking twice whether the redirection target is a valid file,
use the return value from test_redirection_target().

Closes #12276
2026-01-06 10:39:58 +01:00
Fabian Boehm
5930574d8a README: Mention cargo
A bit pedantic, we're also not mentioning that you need a linker, but
oh well.

Fixes #12277
2026-01-05 17:16:33 +01:00
Daniel Rainer
fdef7c8689 l10n: add initialize_localization function
This replaces `initialize_gettext`. It is only defined when the
`localize-messages` feature is enabled, to avoid giving the impression
that it does anything useful when the feature is disabled.

With this change, Fluent will be initialized as well once it is added,
without requiring any additional code for initialization.

Closes #12190
2026-01-05 15:12:34 +00:00
Daniel Rainer
5c36a1be1b l10n: create gettext submodule
Put the gettext-specific code into `localization/gettext`.

Part of #12190
2026-01-05 15:12:34 +00:00
Daniel Rainer
14f747019b l10n: create localization/settings
Extract the language selection code from the gettext crate, and to a
lesser extent from `src/localization/mod.rs` and put it into
`src/localization/settings.rs`. No functional changes are intended.

Aside from better separation of concerns, this refactoring makes it
feasible to reuse the language selection logic for Fluent later on.

Part of #12190
2026-01-05 15:12:34 +00:00
Johannes Altmanninger
d7d5d2a9be completions/make: fix on OpenBSD/FreeBSD
Tested with the POSIX Makefile from https://github.com/mawww/kakoune

Closes #12272
2026-01-05 12:52:00 +01:00
Johannes Altmanninger
750955171a __fish_migrate: don't leak standard file descriptors
The __fish_migrate.fish function spawns a "sh -c 'sleep 7' &" child
process that inherits stdin/stdout/stderr file descriptors fish.

This means that if the app running "fish
tests/checks/__fish_migrate.fish" actually waits for fish to close its
standard file descriptors, it will appear to hang for 7 seconds. Fix
that by closing the file descriptors in the background job when
creating it.

Closes #12271
2026-01-05 12:52:00 +01:00
Denys Zhak
36fd93215b fish_indent: Keep braces on same line in if/while conditions
Closes #12270
2026-01-05 12:50:19 +01:00
Lennard Hofmann
6e7353170a Highlight valid paths in redirection targets
Closes #12260
2026-01-05 12:50:19 +01:00
Peter Ammon
62cc117c12 Minor refactoring of add_to_history
Preparation for other refactoring in the future.
2026-01-04 12:33:53 -08:00
Peter Ammon
af00695383 Clean up replace_home_directory_with_tilde
Fix a stale comment and add a test.
2026-01-04 11:08:21 -08:00
Johannes Altmanninger
85ac91eb2b fish_config theme choose: fix Tomorrow always using light version
The backward compat hack canonicalization caused us to always treat
"tomorrow" light theme.

Restrict this hack to the legacy name (Tomorrow); don't do it when
the new canonical name (tomorrow) is used.  The same issue does not
affect other themes because their legacy names always have a "light"
or 'dark' suffix, which means that the canonical name is different,
so the legacy hacks don't affect the canonical name.

Fixes #12266
2026-01-04 13:08:26 +01:00
Johannes Altmanninger
d1ed582919 fish_config theme choose: apply backwards compat hacks only to sample themes
This logic exists to not break user configurations as we renamed
themes.  But user-sourced themes haven't been renamed.

(It's also questionable whether we should really have these compat
hacks; they might cause confusion in the long run).
2026-01-04 13:08:26 +01:00
xtqqczze
06a14c4a76 clippy: fix assigning_clones lint
https://rust-lang.github.io/rust-clippy/master/index.html#assigning_clones

Closes #12267
2026-01-04 13:08:26 +01:00
takeokunn
400d5281f4 feat(git-completion): add missing options and completions for commands
- Add missing options and completions for fetch, show-branch, am,
  checkout, archive, grep, pull, push, revert, rm, config, clean, and
  other commands
- Replace TODO comments with actual option completions for improved
  usability
- Ensure all new options have appropriate descriptions and argument
  handling for fish shell completion

Closes #12263
2026-01-04 13:08:26 +01:00
Johannes Altmanninger
50778670fb Disable color theme reporting in tmux for now
Due to the way tmux implements it, color theme reporting
causes issues when typing commands really quickly (such as
when synthesizing keys).  We're working on fixing this, see
https://github.com/tmux/tmux/issues/4787#issuecomment-3707866550
Disable it for now. AFAIK other terminals are not affected.

Closes #12261
2026-01-04 13:08:26 +01:00
WitherZuo
9037cd779d Optimize functions page style of fish_config.
- Fix the background color of .function-body in dark mode to improve readability.
- Switch to Tomorrow Night Bright color theme for better contrast and readability in dark mode.
- Format all stylesheets of fish_config.

Closes #12257
2026-01-04 13:08:26 +01:00
phanium
c23a4cbd9f Add --color option for some builtins
Fixes #9716

Closes #12252
2026-01-04 13:08:26 +01:00
Johannes Altmanninger
5d8f7801f7 builtin fish_indent/fish_key_reader: call help from existing fish process
Something like

	PATH=mypath builtin fish_indent --help

runs "fish -c '__fish_print_help fish_indent'" internally.  Since we
don't call setenv(), the PATH update doesn't reach the child shell.
Fix this by using what other builtins use if we are one (i.e. if we
have a Parser in context).

Fixes #12229
Maybe also #12085
2026-01-04 13:08:26 +01:00
Johannes Altmanninger
756134cf2b test/tmux-complete3: fail more reliably 2026-01-04 13:08:26 +01:00
Johannes Altmanninger
c16677fd6f tty_handoff: use Drop consistently
We sometimes use explicit reclaim() and sometimes rely on the drop
implementation. This adds an unnecesary step to reading all uses of
this code.  Make this consistent. Use drop everywhere though we could
use explicit reclaim too.
2026-01-04 13:08:26 +01:00
Johannes Altmanninger
13bc514aa6 __fish_complete_directories: remove use of empty variable
Closes #12248
2026-01-04 09:42:25 +01:00
Johannes Altmanninger
1c3403825c completions/signify: consistent style
Also, replace use of "ls" with globbing.
2026-01-04 09:42:25 +01:00
LunarEclipse
6f1ac7c949 Prioritize files with matching extensions for flag arguments in signify completions
Closes #12243
2026-01-04 09:42:25 +01:00
LunarEclipse
f5d3fd8a82 Full completions for openbsd signify
Part of #12243
2026-01-04 09:42:25 +01:00
Johannes Altmanninger
0a23a78523 Soft-wrapped autosuggestion to hide right prompt for now
Prior to f417cbc981 (Show soft-wrapped portions in autosuggestions,
2025-12-11), we'd truncate autosuggestions before the right prompt.
We no longer do that for autosuggestions that soft-wrap, which means
we try to draw both right prompt and suggestion in the same space.

Make suggestion paint over right prompt for now, since this seems to
be a simple and robust solution.  We can revisit this later.

Fixes #12255
2026-01-03 15:54:04 +01:00
xtqqczze
725cf33f1a fix: remove never read collection from parse_cmd_opts
Closes #12251
2026-01-03 15:54:04 +01:00
xtqqczze
2d6db3f980 clippy: fix implicit_clone lint
https://rust-lang.github.io/rust-clippy/master/index.html#implicit_clone

Closes #12245
2026-01-03 15:54:04 +01:00
xtqqczze
41b9584bb3 clippy: fix cloned_instead_of_copied lint
https://rust-lang.github.io/rust-clippy/master/index.html#cloned_instead_of_copied

Closes #12244
2026-01-03 15:54:04 +01:00
Tin Lai
c915435417 respect canonical config for untracked files
Signed-off-by: Tin Lai <tin@tinyiu.com>

Closes #11709
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
afcde1222b Update cargo dependencies
cargo update && cargo +nightly -Zunstable-options update --breaking
2026-01-03 15:54:04 +01:00
Benjamin A. Beasley
a3cb512628 Update phf from 0.12 to 0.13
Closes #12222
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
fc71ba07da share: fix typo 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
9c867225ee reader handle_completions(): remove code duplication
We fail to flash the command line if we filter out completions due
to !reader_can_replace. Fix that and de-duplicate the logic
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
972355e2fc reader handle_completions(): remove stale comment
See 656b39a0b3 (Also show case-insensitive prefix matches in completion pager, 2025-11-23).
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
8f4c80699f reader handle_completions(): don't allocate a second completion list 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
e79b00d9d1 reader handle_completions(): also truncate common prefix when replacing
I don't know why we don't apply the common-prefix truncation logic
when all completions are replacing.
Let's do that.
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
2f6b1eaaf9 reader handle_completions(): don't consider odd replacing completions for common prefix
If "will_replace_token" is set, we generally only consider
appending completions.  This changed in commit 656b39a0b3 (Also show
case-insensitive prefix matches in completion pager, 2025-11-23) which
also allowed icase completions as long as they are also prefix matches.

Such replacing completions might cause the common prefix to be empty,
which breaks the appending completions.

Fix this by not considering these replacing completions for the
common-prefix computation. The pager already doesn't show the prefix
for these completions specifically.

Fixes #12249
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
3546ffa3ef reader handle_completions(): remove dead filtering code
We skip completions where "will_replace_token != c.replaces_token()".
This means that
- if will_replace_token, we filter out non-replacing completions.
  But those do not exist because, by definition, will_replace_token
  is true iff there are no non-replacing completions.
- if !will_replace_token, we filter out replacing completions.
  From the definition of will_replace_token follows that there is
  some non-replacing completion, which must be a prefix or exact match.
  Since we've filtered by rank, any replacing ones must have the same rank.
  So the replacement bit must be due to smartcase.  Smartcase
  completions are already passed through explicitly here since
  656b39a0b3 (Also show case-insensitive prefix matches in completion
  pager, 2025-11-23).

So the cases where we 'continue' here can never happen.
Remove this redundant check.
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
30f96860a7 reader handle_completions(): closed form for pager prefix bool 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
41d50f1a71 reader handle_completions(): don't duplicate pager prefix allocation
While at it, use Cow I guess?
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
1e9c80f34c reader handle_completions(): don't allocate common prefix 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
b88d2ed812 reader handle_completions(): don't clone surviving completions 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
92dd37d3c7 reader handle_completions(): remove dead code for skipping to add prefix
The tuple (will_replace_token, all_matches_exact_or_prefix) can never
be (false, false).

Proof by contraction:
1. Goal: show unsatisfiability of: !will_replace_token && !all_matches_exact_or_prefix
2. Substitute defintions: !all(replaces) && !all(is_exact_or_prefix)
3. wlog, !replaces(c1) && !is_exact_or_prefix(c2)
4. since c1 and c2 have same rank we know that !is_exact_or_prefix(c1)
5. !is_exact_or_prefix() implies requires_full_replacement()
6. all callers that create a Completion from StringFuzzyMatch::try_create(),
   set CompleteFlags::REPLACE_TOKEN if requires_full_replacement(),
   so requires_full_replacement() implies replaces()
7. From 4-6 follows: !is_exact_or_prefix(c1) implies replaces(c1), which is a contradiction
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
f24cc6a8fc reader handle_completions(): remove code clone
While at it,
1. add assertions to tighten some screws
2. migrate to closed form / inline computation.
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
3117a488ec complete: reuse replaces_token() 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
185b91de13 reader rls: remove redundant initial value
This initial value is weird and None works the same way so use that.
2026-01-03 15:54:04 +01:00
Johannes Altmanninger
e20024f0f0 reader rls: minimize state for tracking the completion pager 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
501ec1905e Test completion pager invalidation behavior 2026-01-03 15:54:04 +01:00
Johannes Altmanninger
7ebd2011ff update-dependencies.sh: fix uv lock --check command 2026-01-03 15:54:04 +01:00
Daniel Rainer
8004f354aa changelog: put new changes into upcoming release
The update to `WITH_GETTEXT` was not released in 4.3.2, so remove it
from there and put it into the section for the upcoming release.

Closes #12254
2026-01-02 00:11:36 +00:00
Daniel Rainer
77e1aead40 cmake: rename WITH_GETTEXT to WITH_MESSAGE_LOCALIZATION
This change is made to make the option name appropriate for Fluent
localization.

While at it, add a feature summary for this feature.

Closes #12208
2026-01-01 23:46:07 +00:00
Peter Ammon
e48a88a4b3 Minor refactoring of history item deletion
Clean this up.
2026-01-01 12:00:18 -08:00
Peter Ammon
1154d9f663 Correct error reporting when rewriting history files
A recent change attempted this:

    let result: std::io::Result<()> = { code()? }

However this doesn't initialize the Result on error - instead it
returns from the function, meaning that the error would be silently
dropped.

Fix that by reporting the error at the call site instead.
2025-12-31 10:20:31 -08:00
Johannes Altmanninger
810a707069 Fix PROMPT_SP hack regression
Commit fbad0ab50a (reset_abandoning_line: remove redundant
allocations, 2025-11-13) uses byte count of ⏎ (3) instead of char
count (1), thus overestimating the number of spaces this symbol takes.

Fixes #12246
2025-12-31 07:46:50 +01:00
Peter Ammon
7fa9e9bfb9 History: use Rust's buffered writing instead of our own
Simplify some code.
2025-12-30 20:26:07 -08:00
Alan Somers
48b0e7e695 Fix installation of prompts and theme files after 4.3.0
Installation of these files was accidentally broken by d8f1a2a.

Fixes #12241
2025-12-31 11:10:34 +08:00
Peter Ammon
848fa57144 Introduce and adopt BorrowedFdFile
Rust has this annoying design where all of the syscall conveniences on
File assume that it owns its fd; in particular this means that we can't
easily construct File from stdin, a raw file descriptor, etc.

The usual workarounds are to construct a File and then mem::forget it
(this is apparently idiomatic Rust!). But this has problems of its own:
for example it can't easily be used in Drop.

Introduce BorrowedFdFile which wraps File with ManuallyDrop and then
never drops the file (i.e. it's always forgotten). Replace some raw FDs
with BorrowedFdFile.
2025-12-30 14:07:39 -08:00
Peter Ammon
4101e831af Make fish_indent stop panicing on closed stdin
Prior to this commit, this code:

    fish_indent <&-

would panic as we would construct a File with a negative fd.
Check for a closed fd as other builtins do.
2025-12-30 14:07:39 -08:00
Peter Ammon
eb803ba6a7 Minor cleanup of shared::Arguments 2025-12-30 14:07:39 -08:00
Fabian Boehm
248a8e7c54 Fix doc test 2025-12-30 20:40:55 +01:00
Peter Ammon
2fa8c8cd7f Allow ctrl-C to work in fish_indent builtin
Since fish_indent became a builtin, it cannot be canceled with control-C,
because Rust's `read_to_end` retries on EINTR. Add our own function which
propagates EINTR and use it.

Fixes #12238
2025-12-30 10:39:19 -08:00
Johannes Altmanninger
8d5f5586dc start new cycle
Created by ./build_tools/release.sh 4.3.2
2025-12-30 17:43:15 +01:00
Johannes Altmanninger
c5bc7bd5f9 Release 4.3.2
Created by ./build_tools/release.sh 4.3.2
2025-12-30 17:21:04 +01:00
Johannes Altmanninger
2b3bd29588 Fix infinite repaint when setting magic variables in prompt
Commit 7996637db5 (Make fish immediately show color changes again,
2025-12-01) repaints unnecessarily when a local unexported color
variable changes.  Also, it repaints when the change comes from
fish_prompt, causing an easy infinite loop.  Same when changing TERM,
COLORTERM and others.

This feature is relevant when using a color-theme aware theme, so
try to keep it. Repaint only on global/universal changes.
Also ignore changes if already repainting fish prompt.

This change may be at odds with concurrent execution (parser should
not care about whether we are repainting) but that's intentional
because of 1. time constraints and 2. I'm not sure what the solution
will look like; we could use the event infrastructure.  But a lot of
existing variable listeners don't use that.

Extract a context object we pass whenever we mutate the environment; While
at it, use it to pass EnvMode::USER, to reduce EnvMode responsibilities.

Fixes #12233
2025-12-30 17:20:42 +01:00
Johannes Altmanninger
0be3f9e57e Fix some non_upper_case_globals warnings 2025-12-30 17:20:42 +01:00
Johannes Altmanninger
2524ece2cc builtin function: error when trying to inherit read-only variable
Also, deduplicate error checks and do them as early as possible since
we always return on error.
2025-12-30 17:20:42 +01:00
Johannes Altmanninger
b975472828 builtin function: improve option parsing structure 2025-12-30 17:20:42 +01:00
Johannes Altmanninger
20427ff1f6 env: check cheaper condition first 2025-12-30 17:20:42 +01:00
Johannes Altmanninger
5b3b825ab2 env: dispatch variable changes again if global was modified explicitly
We set "global_modified" to true if the global exist, or if the
default scope is global but not if EnvMode::GLOBAL.

This is an accident from 77aeb6a2a8 (Port execution, 2023-10-08).
Restore it. Tested in a following commit.
2025-12-30 17:20:42 +01:00
Johannes Altmanninger
ccd3348eed env_init: early return 2025-12-30 17:20:42 +01:00
Johannes Altmanninger
845b9be1f5 builtin set: remove unused argument
The "user" bit is only for getting errors when trying to set read-only
variables.  It's not relevant for reading from variables.
2025-12-30 17:20:42 +01:00
Johannes Altmanninger
400f2b130a set --erase: simplify erasing from multiple scopes in one go
We have pretty weird behavior:

	   $ set --path somepath 1 2 3
	     set --erase --unpath somepath[2]
	[1]$ set --path somepath 1 2 3
	     set --erase --unpath somepath
	   $

The first command fails to erase from the variable, because the
--path/--unpath mismatch prevents us from accessing the variable.
The second succeeds at erasing because we ignore --path/--unpath.

We should probably fix this; for now only simplify the unrelated
change added by fed64999bc (Allow erasing in multiple scopes in one
go, 2022-10-15):
we implement "set --erase --global --path" as

	try_erase(scope="--global")
	try_erase(scope="--path")

Do this instead, which is closer to historical behavior.

	try_erase(scope="--global --path")

This also allows us to express more obviously the behavior if no scope
(out of -lfgU) was specified.
2025-12-30 17:20:42 +01:00
Johannes Altmanninger
362f7cedf6 builtin set: fix inconsistent name
The --path and --export flags are not scopes, so use "mode" name
as elsewhere.
2025-12-30 17:20:42 +01:00
Johannes Altmanninger
2c959469f0 env mode: extract constant for scope bits 2025-12-30 17:20:42 +01:00
Johannes Altmanninger
6c34bcf8f6 parser: reuse set_var() 2025-12-30 17:20:42 +01:00
Johannes Altmanninger
f510b62b7f Don't schedule redundant repaints as autosuggestions are toggled
Tweak 86b8cc2097 (Allow turning off autosuggestions, 2021-10-21)
to avoid redundantly executing the prompt and repainting.
2025-12-30 17:20:42 +01:00
Fabian Boehm
b31387416d Optimize fish_config theme choose
Just following basic shellscript optimization:

- Remove a useless use of cat (`status get-file` takes microseconds,
`status get-file | cat` is on the order of a millisecond - slower with
bigger $PATH)
- Pipe, don't run in a loop
- Filter early

This reduces the time taken from 12ms to 6ms on one of my systems, and
6.5ms to 4.5ms on another.

This is paid on every single shell startup, including
non-interactively, so it's worth it.

There's more to investigate, but this is a good first pass.
2025-12-29 17:26:55 +01:00
Johannes Altmanninger
941a6cb434 changelog for 4.3.2 2025-12-29 16:25:23 +01:00
Johannes Altmanninger
931072f5d1 macos packages: don't add redundant hardlinks to fat binary
Commit 7b4802091a installs fish_indent and fish_key_reader as
hardlinks to fish.  When we create our fat binary for macOS, we add
3 of these X86 binaries to the fattened one,
resulting in a corrupted Mach-O binary. Fix that.

Fixes #12224
2025-12-29 16:19:48 +01:00
Johannes Altmanninger
f4f9db73da Add a CMake option to work around broken cross-compilation builds
Commit 135fc73191 (Remove man/HTML docs from tarball, require Sphinx
instead, 2025-11-20) broke cross compilation of tarballs.

Add an option to allow users to pick any fish_indent (such as
"target/debug/fish_indent" as created by "cargo build"), to allow
cross compilation.

In future, we should remove this option in favor of doing all of this
transparently at build type (in build.rs).

Ref: https://matrix.to/#/!YLTeaulxSDauOOxBoR:matrix.org/$psPcu-ogWK5q9IkgvfdvBGTdJ2XGhNq5z_Ug0iTCx2Q
2025-12-29 16:19:48 +01:00
Johannes Altmanninger
9ef3f30c56 Revert "Re-enable focus reporting on non-tmux"
When I ssh to a macOS system, typing ctrl-p ctrl-j in quick succession
sometimes causes ^[[I (focus in) to be echoed.  Looks like we fail to
disable terminal-echo in time. Possible race condition?  Revert until
we find out more.

This reverts commit 7dd2004da7.

Closes #12232
2025-12-29 16:19:48 +01:00
Johannes Altmanninger
d19c927760 status get-file: simplify wrapper
The __fish_data_with_file wrapper was born out of a desire to simplify
handling of file paths that may or may not be embedded into the
fish binary.

Since 95aeb16ca2 (Remove embed-data feature flag, 2025-11-20) this is
no longer needed since almost everything is embedded unconditionally.
The exception is man pages (see a1baf97f54 (Do not embed man pages
in CMake builds, 2025-11-20)), but they use __fish_data_with_directory.
2025-12-29 16:19:48 +01:00
Misty De Meo
22e5b21f10 changelog: fix minor typo
Closes #12227
2025-12-29 16:19:48 +01:00
Johannes Altmanninger
f0d2444769 docs: removed dead code around FISH_BUILD_VERSION
Man pages used to be built by "build.rs" but now are built by a
dependent "crates/build-man-pages/build.rs". This means that changing
the environment of build.rs is ineffective.

In future, "fn get_version" should probably be a part of
"crates/build-helper/", so Cargo builds only need to compute the
version once.

Lack of this dependency means that "build-man-pages" does not
pass FISH_BUILD_VERSION, which means that Sphinx will fall back to
build_tools/git_version_gen.sh.  This acceptable for now given that
"build-man-pages" is not used in CMake builds.
2025-12-29 16:19:48 +01:00
Johannes Altmanninger
7975060e4a docs: consistently use FISH_BUILD_VERSION_FILE
Commit 2343a6b1f1 passed the FISH_BUILD_VERSION_FILE to
sphinx-manpages to remove the fish_indent dependency.

For sphinx-docs this has been solved in another way in e895f96f8a
(Do not rely on `fish_indent` for version in Sphinx, 2025-08-19).

This is a needless inconsistency.

Remove it. Use FISH_BUILD_VERSION_FILE whenever possible, since that
means that a full build process only needs to call git_version_gen.sh
once.

Keep the fallback to git_version_gen.sh, in case someone calls
sphinx-build directly.
2025-12-29 16:19:47 +01:00
Johannes Altmanninger
354dc3d272 docs: use correct version file for HTML docs from tarball
Prior to commit 135fc73191 (Remove man/HTML docs from tarball, require
Sphinx instead, 2025-11-20), HTML docs were built from a Git worktree.

Now they are built in the tarball.  We call
build_tools/git_version_gen.sh from doc_src so it fails to find the
version file. Fix that.

Fixes #12228
2025-12-29 16:19:47 +01:00
Johannes Altmanninger
7640e95bd7 Create user config file/directories only on first startup again
Not being able to delete these for good (if unused) seems to be
a nuisance.  Let's go back to storing universal __fish_initialized
also on fresh installations, which I guess is a small price to to
avoid recreating these files.

Closes #12230
2025-12-29 16:19:47 +01:00
Johannes Altmanninger
767115a93d build_tools/make_macos_pkg.sh: fix when no CMake option is passed 2025-12-29 16:19:47 +01:00
David Adam
f0c8788a52 exec: add custom message for EBADMACHO error on Apple platforms
Otherwise the error is 'unknown error number 88'.
2025-12-29 22:46:47 +08:00
Fabian Boehm
a3cbb01b27 source __fish_build_paths directly
This didn't work because the cheesy helper function added an extra
scope block.

Just skip it.

Fixes #12226
2025-12-28 19:57:08 +01:00
Johannes Altmanninger
d630b4ae8a start new cycle
Created by ./build_tools/release.sh 4.3.1
2025-12-28 17:16:50 +01:00
Johannes Altmanninger
a2c5b2a567 Release 4.3.1
Created by ./build_tools/release.sh 4.3.1
2025-12-28 16:54:44 +01:00
Johannes Altmanninger
18295f4402 Fix icase prefix/suffix checks
Commit 30942e16dc (Fix prefix/suffix icase comparisons, 2025-12-27)
incorrectly treated "gs " as prefix of "gs" which causes a crash
immediately after expanding that abbreviation iff "gs" is our
autosuggestion (i.e. there's no history autosuggestion taking
precedence).

Fixes #12223
2025-12-28 16:54:34 +01:00
Johannes Altmanninger
443fd604cc build_tools/release.sh: don't add dch entry for snapshot version 2025-12-28 10:55:09 +01:00
Johannes Altmanninger
9e022ff7cf build_tools/release.sh: fix undefined variable for next milestone 2025-12-28 10:52:20 +01:00
Johannes Altmanninger
aba927054f start new cycle
Created by ./build_tools/release.sh 4.3.0
2025-12-28 10:45:34 +01:00
146 changed files with 9633 additions and 2141 deletions

View File

@@ -29,6 +29,7 @@ freebsd_task:
freebsd_instance:
image: freebsd-15-0-release-amd64-ufs # updatecli.d/cirrus-freebsd.yml
tests_script:
- pkg update
- pkg install -y cmake-core devel/pcre2 devel/ninja gettext git-lite lang/rust misc/py-pexpect
# libclang.so is a required build dependency for rust-c++ ffi bridge
- pkg install -y llvm

View File

@@ -1,11 +1,8 @@
## Description
Talk about your changes here.
Fixes issue #
## TODOs:
<!-- Just check off what what we know been done so far. We can help you with this stuff. -->
<!-- Check off what what has been done so far. -->
- [ ] If addressing an issue, a commit message mentions `Fixes issue #<issue-number>`
- [ ] Changes to fish usage are reflected in user documentation/manpages.
- [ ] Tests have been added for regressions fixed
- [ ] User-visible changes noted in CHANGELOG.rst <!-- Don't document changes for completions inside CHANGELOG.rst, there are lot of such edits -->
- [ ] User-visible changes noted in CHANGELOG.rst <!-- Usually skipped for changes to completions -->

View File

@@ -1,9 +1,47 @@
fish 4.3.3 (released January 07, 2026)
======================================
This release fixes the following problems identified in fish 4.3.0:
- Selecting a completion could insert only part of the token (:issue:`12249`).
- Glitch with soft-wrapped autosuggestions and :doc:`fish_right_prompt <cmds/fish_right_prompt>` (:issue:`12255`).
- Spurious echo in tmux when typing a command really fast (:issue:`12261`).
- ``tomorrow`` theme always using the light variant (:issue:`12266`).
- ``fish_config theme choose`` sometimes not shadowing themes set by e.g. webconfig (:issue:`12278`).
- The sample prompts and themes are correctly installed (:issue:`12241`).
- Last line of command output could be hidden when missing newline (:issue:`12246`).
Other improvements include:
- The ``abbr``, ``bind``, ``complete``, ``functions``, ``history`` and ``type`` commands now support a ``--color`` option to control syntax highlighting in their output. Valid values are ``auto`` (default), ``always``, or ``never``.
- Existing file paths in redirection targets such as ``> file.txt`` are now highlighted using :envvar:`fish_color_valid_path`, indicating that ``file.txt`` will be clobbered (:issue:`12260`).
fish 4.3.2 (released December 30, 2025)
=======================================
This release fixes the following problems identified in 4.3.0:
- Pre-built macOS packages failed to start due to a ``Malformed Mach-O file`` error (:issue:`12224`).
- ``extra_functionsdir`` (usually ``vendor_functions.d``) and friends were not used (:issue:`12226`).
- Sample config file ``~/.config/fish/config.fish/`` and config directories ``~/.config/fish/conf.d/``, ``~/.config/fish/completions/`` and ``~/.config/fish/functions/`` were recreated on every startup instead of only the first time fish runs on a system (:issue:`12230`).
- Spurious echo of ``^[[I`` in some scenarios (:issue:`12232`).
- Infinite prompt redraw loop on some prompts (:issue:`12233`).
- The removal of pre-built HTML docs from tarballs revealed that cross compilation is broken because we use ``${CMAKE_BINARY_DIR}/fish_indent`` for building HTML docs.
As a workaround, the new CMake build option ``FISH_INDENT_FOR_BUILDING_DOCS`` can be set to the path of a runnable ``fish_indent`` binary.
fish 4.3.1 (released December 28, 2025)
=======================================
This release fixes the following problem identified in 4.3.0:
- Possible crash after expanding an abbreviation (:issue:`12223`).
fish 4.3.0 (released December 28, 2025)
=======================================
Deprecations and removed features
---------------------------------
- fish no longer sets :ref:`universal variables <variables-universal>` by default, making the configuration easier to understand.
- fish no longer sets user-facing :ref:`universal variables <variables-universal>` by default, making the configuration easier to understand.
Specifically, the ``fish_color_*``, ``fish_pager_color_*`` and ``fish_key_bindings`` variables are now set in the global scope by default.
After upgrading to 4.3.0, fish will (once and never again) migrate these universals to globals set at startup in the
``~/.config/fish/conf.d/fish_frozen_theme.fish`` and
@@ -30,7 +68,7 @@ Interactive improvements
- Completion accuracy was improved for file paths containing ``=`` or ``:`` (:issue:`5363`).
- Prefix-matching completions are now shown even if they don't match the case typed by the user (:issue:`7944`).
- On Cygwin/MSYS, command name completion will favor the non-exe name (``foo``) unless the user started typing the extension.
- When using the exe name (``foo.exe``), fish will use to the description and completions for ``foo`` if there are none for ``foo.exe``.
- When using the exe name (``foo.exe``), fish will use the description and completions for ``foo`` if there are none for ``foo.exe``.
- Autosuggestions now also show soft-wrapped portions (:issue:`12045`).
New or improved bindings
@@ -45,8 +83,6 @@ Improved terminal support
- The working directory is now reported on every fresh prompt (via OSC 7), fixing scenarios where a child process (like ``ssh``) left behind a stale working directory (:issue:`12191`).
- OSC 133 prompt markers now also mark the prompt end, which improves shell integration with terminals like iTerm2 (:issue:`11837`).
- Operating-system-specific key bindings are now decided based on the :ref:`terminal's host OS <status-terminal-os>`.
- Focus reporting is enabled unconditionally, not just inside tmux.
To use it, define functions that handle the ``fish_focus_in`` or ``fish_focus_out`` :ref:`events <event>`.
- New :ref:`feature flag <featureflags>` ``omit-term-workarounds`` can be turned on to prevent fish from trying to work around some incompatible terminals.
For distributors and developers

121
Cargo.lock generated
View File

@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
@@ -40,9 +40,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.12.0"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
@@ -50,9 +50,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.41"
version = "1.2.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -83,9 +83,9 @@ dependencies = [
[[package]]
name = "crypto-common"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
@@ -146,13 +146,13 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
[[package]]
name = "fish"
version = "4.3.0"
version = "4.3.3"
dependencies = [
"bitflags",
"cc",
@@ -176,7 +176,7 @@ dependencies = [
"num-traits",
"once_cell",
"pcre2",
"phf_codegen 0.12.1",
"phf_codegen 0.13.1",
"portable-atomic",
"rand 0.9.2",
"rsconf",
@@ -209,7 +209,6 @@ version = "0.0.0"
dependencies = [
"libc",
"nix",
"once_cell",
]
[[package]]
@@ -221,7 +220,6 @@ dependencies = [
"fish-wchar",
"fish-widecharwidth",
"libc",
"once_cell",
"rsconf",
"widestring",
]
@@ -231,8 +229,7 @@ name = "fish-gettext"
version = "0.0.0"
dependencies = [
"fish-gettext-maps",
"once_cell",
"phf 0.12.1",
"phf 0.13.1",
]
[[package]]
@@ -250,8 +247,8 @@ version = "0.0.0"
dependencies = [
"fish-build-helper",
"fish-gettext-mo-file-parser",
"phf 0.12.1",
"phf_codegen 0.12.1",
"phf 0.13.1",
"phf_codegen 0.13.1",
"rsconf",
]
@@ -297,15 +294,15 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "generic-array"
version = "0.14.9"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
@@ -336,9 +333,9 @@ dependencies = [
[[package]]
name = "globset"
version = "0.4.16"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
@@ -349,9 +346,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.5"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
@@ -370,15 +367,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "libredox"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
dependencies = [
"bitflags",
"libc",
@@ -395,15 +392,15 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.28"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.13.0"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465"
checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f"
dependencies = [
"hashbrown",
]
@@ -539,11 +536,11 @@ dependencies = [
[[package]]
name = "phf"
version = "0.12.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_shared 0.12.1",
"phf_shared 0.13.1",
]
[[package]]
@@ -558,12 +555,12 @@ dependencies = [
[[package]]
name = "phf_codegen"
version = "0.12.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbdcb6f01d193b17f0b9c3360fa7e0e620991b193ff08702f78b3ce365d7e61"
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
dependencies = [
"phf_generator 0.12.1",
"phf_shared 0.12.1",
"phf_generator 0.13.1",
"phf_shared 0.13.1",
]
[[package]]
@@ -578,12 +575,12 @@ dependencies = [
[[package]]
name = "phf_generator"
version = "0.12.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared 0.12.1",
"phf_shared 0.13.1",
]
[[package]]
@@ -597,9 +594,9 @@ dependencies = [
[[package]]
name = "phf_shared"
version = "0.12.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
@@ -612,9 +609,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.1"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd"
[[package]]
name = "ppv-lite86"
@@ -627,18 +624,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.101"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.41"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
@@ -732,9 +729,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rsconf"
version = "0.2.2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd2af859f1af0401e7fc7577739c87b0d239d8a5da400d717183bca92336bcdc"
checksum = "06cbd984e96cc891aa018958ac3d09986c0ea7635eedfff670b99a90970f159f"
dependencies = [
"cc",
]
@@ -897,9 +894,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.107"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
@@ -946,9 +943,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.20"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-segmentation"
@@ -1052,18 +1049,18 @@ checksum = "4de5f056fb9dc8b7908754867544e26145767187aaac5a98495e88ad7cb8a80f"
[[package]]
name = "zerocopy"
version = "0.8.27"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -31,7 +31,7 @@ libc = "0.2.177"
# lru pulls in hashbrown by default, which uses a faster (though less DoS resistant) hashing algo.
# disabling default features uses the stdlib instead, but it doubles the time to rewrite the history
# files as of 22 April 2024.
lru = "0.13.0"
lru = "0.16.2"
nix = { version = "0.30.1", default-features = false, features = [
"event",
"inotify",
@@ -44,8 +44,8 @@ once_cell = "1.19.0"
pcre2 = { git = "https://github.com/fish-shell/rust-pcre2", tag = "0.2.9-utf32", default-features = false, features = [
"utf32",
] }
phf = { version = "0.12", default-features = false }
phf_codegen = { version = "0.12" }
phf = { version = "0.13", default-features = false }
phf_codegen = "0.13"
portable-atomic = { version = "1", default-features = false, features = [
"fallback",
] }
@@ -54,7 +54,7 @@ rand = { version = "0.9.2", default-features = false, features = [
"small_rng",
"thread_rng",
] }
rsconf = "0.2.2"
rsconf = "0.3.0"
rust-embed = { version = "8.9.0", features = [
"deterministic-timestamps",
"include-exclude",
@@ -79,7 +79,7 @@ debug = true
[package]
name = "fish"
version = "4.3.0"
version = "4.3.3"
edition.workspace = true
rust-version.workspace = true
default-run = "fish"
@@ -182,19 +182,24 @@ rust.non_upper_case_globals = "allow"
rust.unknown_lints = "allow"
rust.unstable_name_collisions = "allow"
rustdoc.private_intra_doc_links = "allow"
clippy.len_without_is_empty = "allow" # we're not a library crate
clippy.let_and_return = "allow"
clippy.manual_range_contains = "allow"
clippy.map_unwrap_or = "warn"
clippy.needless_lifetimes = "allow"
clippy.new_without_default = "allow"
clippy.option_map_unit_fn = "allow"
[workspace.lints.clippy]
assigning_clones = "warn"
implicit_clone = "warn"
cloned_instead_of_copied = "warn"
len_without_is_empty = "allow" # we're not a library crate
let_and_return = "allow"
manual_range_contains = "allow"
map_unwrap_or = "warn"
needless_lifetimes = "allow"
new_without_default = "allow"
option_map_unit_fn = "allow"
# We do not want to use the e?print(ln)?! macros.
# These lints flag their use.
# In the future, they might change to flag other methods of printing.
clippy.print_stdout = "deny"
clippy.print_stderr = "deny"
print_stdout = "deny"
print_stderr = "deny"
[lints]
workspace = true

View File

@@ -117,7 +117,7 @@ Dependencies
Compiling fish requires:
- Rust (version 1.85 or later)
- Rust (version 1.85 or later), including cargo
- CMake (version 3.15 or later)
- a C compiler (for system feature detection and the test helper binary)
- PCRE2 (headers and libraries) - optional, this will be downloaded if missing
@@ -158,6 +158,11 @@ In addition to the normal CMake build options (like ``CMAKE_INSTALL_PREFIX``), f
- Rust_CARGO=path - the path to cargo. If not set, cmake will check $PATH and ~/.cargo/bin
- Rust_CARGO_TARGET=target - the target to pass to cargo. Set this for cross-compilation.
- WITH_DOCS=ON|OFF - whether to build the documentation. By default, this is ON when Sphinx is installed.
- FISH_INDENT_FOR_BUILDING_DOCS - useful for cross-compilation.
Set this to the path to the ``fish_indent`` executable to use for building HTML docs.
By default, ``${CMAKE_BINARY_DIR}/fish_indent`` will be used.
If that's not runnable on the compile host,
you can build a native one with ``cargo build --bin fish_indent`` and set this to ``$PWD/target/debug/fish_indent``.
- FISH_USE_SYSTEM_PCRE2=ON|OFF - whether to use an installed pcre2. This is normally autodetected.
- WITH_GETTEXT=ON|OFF - whether to include translations.
- extra_functionsdir, extra_completionsdir and extra_confdir - to compile in an additional directory to be searched for functions, completions and configuration snippets

View File

@@ -40,9 +40,6 @@ fn main() {
// the source directory is the current working directory of the build script
rsconf::set_env_value("FISH_BUILD_VERSION", version);
// safety: single-threaded code.
unsafe { std::env::set_var("FISH_BUILD_VERSION", version) };
fish_build_helper::rebuild_if_embedded_path_changed("share");
let build = cc::Build::new();

View File

@@ -33,8 +33,13 @@ fi
cargo() {
subcmd=$1
shift
# shellcheck disable=2086
command cargo "$subcmd" $cargo_args "$@"
if [ -n "$FISH_CHECK_RUST_TOOLCHAIN" ]; then
# shellcheck disable=2086
command cargo "+$FISH_CHECK_RUST_TOOLCHAIN" "$subcmd" $cargo_args "$@"
else
# shellcheck disable=2086
command cargo "$subcmd" $cargo_args "$@"
fi
}
cleanup () {

View File

@@ -13,9 +13,9 @@ git_permission_failed=0
# First see if there is a version file (included in release tarballs),
# then try git-describe, then default.
if test -f version
if test -f "$FISH_BASE_DIR"/version
then
VN=$(cat version) || VN="$DEF_VER"
VN=$(cat "$FISH_BASE_DIR"/version) || VN="$DEF_VER"
else
if VN=$(git -C "$FISH_BASE_DIR" describe --always --dirty 2>/dev/null); then
:

View File

@@ -25,7 +25,7 @@ NOTARIZE=
ARM64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=11.0'
X86_64_DEPLOY_TARGET='MACOSX_DEPLOYMENT_TARGET=10.12'
cmake_args=
cmake_args=()
while getopts "c:sf:i:p:e:nj:" opt; do
case $opt in
@@ -82,17 +82,16 @@ do_cmake() {
&& env DESTDIR="$PKGDIR/root/" $ARM64_DEPLOY_TARGET make install;
}
# Build for x86-64 but do not install; instead we will make some fat binaries inside the root.
# Build for x86-64 but do not install; instead we will make a fat binary inside the root.
{ cd "$PKGDIR/build_x86_64" \
&& do_cmake -DRust_CARGO_TARGET=x86_64-apple-darwin \
&& env $X86_64_DEPLOY_TARGET make VERBOSE=1 -j 12; }
# Fatten them up.
for FILE in "$PKGDIR"/root/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
chmod 755 "$FILE"
done
# Fatten it up.
FILE=$PKGDIR/root/usr/local/bin/fish
X86_FILE=$PKGDIR/build_x86_64/$(basename "$FILE")
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
chmod 755 "$FILE"
if test -n "$SIGN"; then
echo "Signing executables"
@@ -105,9 +104,7 @@ if test -n "$SIGN"; then
if [ -n "$ENTITLEMENTS_FILE" ]; then
ARGS+=(--entitlements-xml-file "$ENTITLEMENTS_FILE")
fi
for FILE in "$PKGDIR"/root/usr/local/bin/*; do
(set +x; rcodesign sign "${ARGS[@]}" "$FILE")
done
(set +x; rcodesign sign "${ARGS[@]}" "$PKGDIR"/root/usr/local/bin/fish)
fi
pkgbuild --scripts "$SRC_DIR/build_tools/osx_package_scripts" --root "$PKGDIR/root/" --identifier 'com.ridiculousfish.fish-shell-pkg' --version "$VERSION" "$PKGDIR/intermediates/fish.pkg"
@@ -128,15 +125,13 @@ fi
(cd "$PKGDIR/build_arm64" && env $ARM64_DEPLOY_TARGET make -j 12 fish_macapp)
(cd "$PKGDIR/build_x86_64" && env $X86_64_DEPLOY_TARGET make -j 12 fish_macapp)
# Make the app's /usr/local/bin binaries universal. Note fish.app/Contents/MacOS/fish already is, courtesy of CMake.
# Make the app's /usr/local/bin/fish binary universal. Note fish.app/Contents/MacOS/fish already is, courtesy of CMake.
cd "$PKGDIR/build_arm64"
for FILE in fish.app/Contents/Resources/base/usr/local/bin/*; do
X86_FILE="$PKGDIR/build_x86_64/fish.app/Contents/Resources/base/usr/local/bin/$(basename "$FILE")"
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
# macho-universal-create screws up the permissions.
chmod 755 "$FILE"
done
FILE=fish.app/Contents/Resources/base/usr/local/bin/fish
X86_FILE=$PKGDIR/build_x86_64/fish.app/Contents/Resources/base/usr/local/bin/$(basename "$FILE")
rcodesign macho-universal-create --output "$FILE" "$FILE" "$X86_FILE"
# macho-universal-create screws up the permissions.
chmod 755 "$FILE"
if test -n "$SIGN"; then
echo "Signing app"

View File

@@ -86,9 +86,10 @@ sed -i \
CommitVersion() {
sed -i "s/^version = \".*\"/version = \"$1\"/g" Cargo.toml
cargo fetch --offline
# debchange is a Debian script to manage the Debian changelog, but
# it's too annoying to install everywhere. Just do it by hand.
cat - contrib/debian/changelog > contrib/debian/changelog.new <<EOF
if [ "$1" = "$version" ]; then
# debchange is a Debian script to manage the Debian changelog, but
# it's too annoying to install everywhere. Just do it by hand.
cat - contrib/debian/changelog > contrib/debian/changelog.new <<EOF
fish (${version}-1) stable; urgency=medium
* Release of new version $version.
@@ -98,8 +99,10 @@ fish (${version}-1) stable; urgency=medium
-- $committer $(date -R)
EOF
mv contrib/debian/changelog.new contrib/debian/changelog
git add CHANGELOG.rst Cargo.toml Cargo.lock contrib/debian/changelog
mv contrib/debian/changelog.new contrib/debian/changelog
git add contrib/debian/changelog
fi
git add CHANGELOG.rst Cargo.toml Cargo.lock
git commit -m "$2
Created by ./build_tools/release.sh $version"
@@ -296,6 +299,8 @@ milestone_number() {
gh_api_repo milestones/"$(milestone_number "$milestone_version")" \
--method PATCH --raw-field state=closed
next_minor_version=$(echo "$minor_version" |
awk -F. '{ printf "%s.%s", $1, $2+1 }')
if [ -z "$(milestone_number "$next_minor_version")" ]; then
gh_api_repo milestones --method POST \
--raw-field title="fish $next_minor_version"

View File

@@ -10,7 +10,8 @@ command -v updatecli
command -v uv
sort --version-sort </dev/null
uv lock --check
# TODO This is copied from .github/actions/install-sphinx/action.yml
uv lock --check --exclude-newer="$(awk -F'"' <uv.lock '/^exclude-newer[[:space:]]*=/ {print $2}')"
updatecli "${@:-apply}"

View File

@@ -12,11 +12,21 @@ set(SPHINX_BUILD_DIR "${SPHINX_ROOT_DIR}/build")
set(SPHINX_HTML_DIR "${SPHINX_ROOT_DIR}/html")
set(SPHINX_MANPAGE_DIR "${SPHINX_ROOT_DIR}/man")
# sphinx-docs uses fish_indent for highlighting.
# Prepend the output dir of fish_indent to PATH.
set(FISH_INDENT_FOR_BUILDING_DOCS "" CACHE FILEPATH "Path to fish_indent executable for building HTML docs")
if(FISH_INDENT_FOR_BUILDING_DOCS)
get_filename_component(FISH_INDENT_DIR "${FISH_INDENT_FOR_BUILDING_DOCS}" DIRECTORY)
set(SPHINX_HTML_FISH_INDENT_PATH ${FISH_INDENT_DIR})
set(SPHINX_HTML_FISH_INDENT_DEP)
else()
set(SPHINX_HTML_FISH_INDENT_PATH ${CMAKE_BINARY_DIR})
set(SPHINX_HTML_FISH_INDENT_DEP fish_indent)
endif()
add_custom_target(sphinx-docs
mkdir -p ${SPHINX_HTML_DIR}/_static/
COMMAND env PATH="${CMAKE_BINARY_DIR}:$$PATH"
COMMAND env FISH_BUILD_VERSION_FILE=${CMAKE_CURRENT_BINARY_DIR}/${FBVF}
PATH="${SPHINX_HTML_FISH_INDENT_PATH}:$$PATH"
${SPHINX_EXECUTABLE}
-j auto
-q -b html
@@ -24,7 +34,10 @@ add_custom_target(sphinx-docs
-d "${SPHINX_ROOT_DIR}/.doctrees-html"
"${SPHINX_SRC_DIR}"
"${SPHINX_HTML_DIR}"
DEPENDS ${SPHINX_SRC_DIR}/fish_indent_lexer.py fish_indent
DEPENDS
CHECK-FISH-BUILD-VERSION-FILE
${SPHINX_SRC_DIR}/fish_indent_lexer.py
${SPHINX_HTML_FISH_INDENT_DEP}
COMMENT "Building HTML documentation with Sphinx")
add_custom_target(sphinx-manpages

View File

@@ -131,6 +131,14 @@ install(DIRECTORY share/functions/
DESTINATION ${rel_datadir}/fish/functions
FILES_MATCHING PATTERN "*.fish")
install(DIRECTORY share/prompts/
DESTINATION ${rel_datadir}/fish/prompts
FILES_MATCHING PATTERN "*.fish")
install(DIRECTORY share/themes/
DESTINATION ${rel_datadir}/fish/themes
FILES_MATCHING PATTERN "*.theme")
# CONDEMNED_PAGE is managed by the conditional above
# Building the man pages is optional: if sphinx isn't installed, they're not built
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/user_doc/man/man1/
@@ -149,9 +157,7 @@ install(DIRECTORY share/tools/web_config
PATTERN "*.css"
PATTERN "*.html"
PATTERN "*.py"
PATTERN "*.js"
PATTERN "*.theme"
PATTERN "*.fish")
PATTERN "*.js")
# Building the man pages is optional: if Sphinx isn't installed, they're not built
install(FILES ${MANUALS} DESTINATION ${mandir}/man1/ OPTIONAL)

View File

@@ -1,3 +1,27 @@
fish (4.3.3-1) stable; urgency=medium
* Release of new version 4.3.3.
See https://github.com/fish-shell/fish-shell/releases/tag/4.3.3 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Wed, 07 Jan 2026 08:34:20 +0100
fish (4.3.2-1) stable; urgency=medium
* Release of new version 4.3.2.
See https://github.com/fish-shell/fish-shell/releases/tag/4.3.2 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Tue, 30 Dec 2025 17:21:04 +0100
fish (4.3.1-1) stable; urgency=medium
* Release of new version 4.3.1.
See https://github.com/fish-shell/fish-shell/releases/tag/4.3.1 for details.
-- Johannes Altmanninger <aclopte@gmail.com> Sun, 28 Dec 2025 16:54:44 +0100
fish (4.3.0-1) stable; urgency=medium
* Release of new version 4.3.0.

View File

@@ -9,7 +9,6 @@ license.workspace = true
[dependencies]
libc.workspace = true
nix.workspace = true
once_cell.workspace = true
[lints]
workspace = true

View File

@@ -1,7 +1,7 @@
use libc::STDIN_FILENO;
use once_cell::sync::OnceCell;
use std::env;
use std::os::unix::ffi::OsStrExt;
use std::sync::OnceLock;
// 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.
@@ -39,7 +39,7 @@ pub fn subslice_position<T: Eq>(a: &[T], b: &[T]) -> Option<usize> {
/// session. We err on the side of assuming it's not a console session. This approach isn't
/// bullet-proof and that's OK.
pub fn is_console_session() -> bool {
static IS_CONSOLE_SESSION: OnceCell<bool> = OnceCell::new();
static IS_CONSOLE_SESSION: OnceLock<bool> = OnceLock::new();
// TODO(terminal-workaround)
*IS_CONSOLE_SESSION.get_or_init(|| {
nix::unistd::ttyname(unsafe { std::os::fd::BorrowedFd::borrow_raw(STDIN_FILENO) })

View File

@@ -11,7 +11,6 @@ fish-common.workspace = true
fish-wchar.workspace = true
fish-widecharwidth.workspace = true
libc.workspace = true
once_cell.workspace = true
widestring.workspace = true
[build-dependencies]

View File

@@ -5,9 +5,11 @@
use fish_wchar::prelude::*;
use fish_widecharwidth::{WcLookupTable, WcWidth};
use once_cell::sync::Lazy;
use std::cmp;
use std::sync::atomic::{AtomicIsize, Ordering};
use std::sync::{
LazyLock,
atomic::{AtomicIsize, 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`.
@@ -25,7 +27,7 @@
// For some reason, this is declared here and exposed here, but is set in `env_dispatch`.
pub static FISH_EMOJI_WIDTH: AtomicIsize = AtomicIsize::new(1);
static WC_LOOKUP_TABLE: Lazy<WcLookupTable> = Lazy::new(WcLookupTable::new);
static WC_LOOKUP_TABLE: LazyLock<WcLookupTable> = LazyLock::new(WcLookupTable::new);
/// A safe wrapper around the system `wcwidth()` function
#[cfg(not(cygwin))]

View File

@@ -8,7 +8,6 @@ license.workspace = true
[dependencies]
fish-gettext-maps.workspace = true
once_cell.workspace = true
phf.workspace = true
[lints]

View File

@@ -1,259 +1,19 @@
use fish_gettext_maps::CATALOGS;
use once_cell::sync::Lazy;
use std::{collections::HashSet, sync::Mutex};
use std::{
collections::HashMap,
sync::{LazyLock, Mutex},
};
type Catalog = &'static phf::Map<&'static str, &'static str>;
pub struct SetLanguageLints<'a> {
pub duplicates: Vec<&'a str>,
pub non_existing: Vec<&'a str>,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LanguagePrecedenceOrigin {
Default,
LocaleVariable(LocaleVariable),
LanguageEnvVar,
StatusLanguage,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LocaleVariable {
#[allow(clippy::upper_case_acronyms)]
LANG,
#[allow(non_camel_case_types)]
LC_MESSAGES,
#[allow(non_camel_case_types)]
LC_ALL,
}
impl LocaleVariable {
fn as_language_precedence_origin(&self) -> LanguagePrecedenceOrigin {
LanguagePrecedenceOrigin::LocaleVariable(*self)
}
pub fn as_str(&self) -> &'static str {
match self {
Self::LANG => "LANG",
Self::LC_MESSAGES => "LC_MESSAGES",
Self::LC_ALL => "LC_ALL",
}
}
}
impl std::fmt::Display for LocaleVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
struct InternalLocalizationState {
precedence_origin: LanguagePrecedenceOrigin,
language_precedence: Vec<(String, Catalog)>,
}
pub struct PublicLocalizationState {
pub precedence_origin: LanguagePrecedenceOrigin,
pub language_precedence: Vec<String>,
}
/// Stores the current localization status.
/// `is_active` indicates whether localization is currently active, and the reason if it is
/// not.
/// The `origin` indicates where the values in `language_precedence` were taken from.
/// `language_precedence` stores the catalogs in the order they should be used.
///
/// This struct should be updated when the relevant variables change or `status language` is used
/// to modify the localization state.
static LOCALIZATION_STATE: Lazy<Mutex<InternalLocalizationState>> =
Lazy::new(|| Mutex::new(InternalLocalizationState::new()));
impl InternalLocalizationState {
fn new() -> Self {
Self {
precedence_origin: LanguagePrecedenceOrigin::Default,
language_precedence: vec![],
}
}
fn to_public(&self) -> PublicLocalizationState {
PublicLocalizationState {
precedence_origin: self.precedence_origin,
language_precedence: self
.language_precedence
.iter()
.map(|(lang, _)| lang.to_owned())
.collect(),
}
}
fn update_from_env(
&mut self,
message_locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
// Do not override values set via `status language`.
if self.precedence_origin == LanguagePrecedenceOrigin::StatusLanguage {
return;
}
if let Some((precedence_origin, locale)) = &message_locale {
// Regular locale names start with lowercase letters (`ll_CC`, followed by some suffix).
// The C or POSIX locale is special, and often used to disable localization.
// Their names are upper-case, but variants with suffixes (`C.UTF-8`) exist.
// To ensure that such variants are accounted for, we match on prefixes of the
// locale name.
// https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap07.html#tag_07_02
fn is_c_locale(locale: &str) -> bool {
locale.starts_with('C') || locale.starts_with("POSIX")
}
if is_c_locale(locale) {
self.precedence_origin =
LanguagePrecedenceOrigin::LocaleVariable(*precedence_origin);
self.language_precedence.clear();
return;
}
}
let (precedence_origin, language_list) = if let Some(list) = language_var {
(LanguagePrecedenceOrigin::LanguageEnvVar, list)
} else if let Some((precedence_origin, locale)) = message_locale {
let mut normalized_name = String::new();
// Strip off encoding and modifier. (We always expect UTF-8 and don't support modifiers.)
for c in locale.chars() {
if c.is_alphabetic() || c == '_' {
normalized_name.push(c);
} else {
break;
}
}
// At this point, the normalized_name should have the shape `ll` or `ll_CC`.
(
precedence_origin.as_language_precedence_origin(),
vec![normalized_name],
)
} else {
(LanguagePrecedenceOrigin::Default, vec![])
};
let mut seen_languages = HashSet::new();
self.language_precedence = language_list
.into_iter()
.flat_map(|lang| find_existing_catalogs(&lang))
.filter(|(lang, _)| seen_languages.insert(lang.to_owned()))
.collect();
self.precedence_origin = precedence_origin;
}
fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
&mut self,
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut seen = HashSet::new();
let mut duplicates = vec![];
for lang in langs {
let lang = lang.as_ref();
if !seen.insert(lang) {
duplicates.push(lang)
}
}
let mut existing_langs = vec![];
let mut non_existing = vec![];
for lang in langs {
let lang = lang.as_ref();
if let Some(catalog) = CATALOGS.get(lang) {
existing_langs.push((lang.to_owned(), *catalog));
} else {
non_existing.push(lang);
}
}
let mut seen = HashSet::new();
let unique_langs = existing_langs
.into_iter()
.filter(|(lang, _)| seen.insert(lang.to_owned()))
.collect();
self.language_precedence = unique_langs;
self.precedence_origin = LanguagePrecedenceOrigin::StatusLanguage;
SetLanguageLints {
duplicates,
non_existing,
}
}
}
/// Tries to find catalogs for `language`.
/// `language` must be an ISO 639 language code, optionally followed by an underscore and an ISO
/// 3166 country/territory code.
/// Uses the catalog with the exact same name as `language` if it exists.
/// If a country code is present (`ll_CC`), only the catalog named `ll` will be considered as a fallback.
/// If no country code is present (`ll`), all catalogs whose names start with `ll_` will be used in
/// arbitrary order.
fn find_existing_catalogs(language: &str) -> Vec<(String, Catalog)> {
// Try the exact name first.
// If there already is a corresponding catalog return the language.
if let Some(catalog) = CATALOGS.get(language) {
return vec![(language.to_owned(), catalog)];
}
let language_without_country_code = language.split_once('_').map_or(language, |(ll, _cc)| ll);
if language == language_without_country_code {
// We have `ll` format. In this case, try to find any catalog whose name starts with `ll_`.
// Note that it is important to include the underscore in the pattern, otherwise `ll` might
// fall back to `llx_CC`, where `llx` is a 3-letter language identifier.
let ll_prefix = format!("{language}_");
let mut lang_catalogs = vec![];
for (&lang_name, &catalog) in CATALOGS.entries() {
if lang_name.starts_with(&ll_prefix) {
lang_catalogs.push((lang_name.to_owned(), catalog));
}
}
lang_catalogs
} else {
// If `language` contained a country code, we only try to fall back to a catalog
// without a country code.
if let Some(catalog) = CATALOGS.get(language_without_country_code) {
vec![(language_without_country_code.to_owned(), catalog)]
} else {
vec![]
}
}
}
pub fn update_from_env(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_env(locale, language_var);
}
pub fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_status_language_builtin(langs)
}
pub fn unset_from_status_language_builtin(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.precedence_origin = LanguagePrecedenceOrigin::Default;
localization_state.update_from_env(locale, language_var);
}
pub fn status_language() -> PublicLocalizationState {
let localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.to_public()
}
static LANGUAGE_PRECEDENCE: LazyLock<Mutex<Vec<(&'static str, Catalog)>>> =
LazyLock::new(|| Mutex::new(vec![]));
pub fn gettext(message_str: &'static str) -> Option<&'static str> {
let localization_state = LOCALIZATION_STATE.lock().unwrap();
let language_precedence = LANGUAGE_PRECEDENCE.lock().unwrap();
// Use the localization from the highest-precedence language that has one available.
for (_, catalog) in localization_state.language_precedence.iter() {
for (_, catalog) in language_precedence.iter() {
if let Some(localized_str) = catalog.get(message_str) {
return Some(localized_str);
}
@@ -261,8 +21,40 @@ pub fn gettext(message_str: &'static str) -> Option<&'static str> {
None
}
pub fn list_available_languages() -> Vec<&'static str> {
let mut langs: Vec<_> = CATALOGS.entries().map(|(&lang, _)| lang).collect();
langs.sort();
langs
#[derive(Clone, Copy)]
pub struct GettextLocalizationLanguage {
language: &'static str,
}
static AVAILABLE_LANGUAGES: LazyLock<HashMap<&'static str, GettextLocalizationLanguage>> =
LazyLock::new(|| {
HashMap::from_iter(
CATALOGS
.entries()
.map(|(&language, _)| (language, GettextLocalizationLanguage { language })),
)
});
pub fn get_available_languages() -> &'static HashMap<&'static str, GettextLocalizationLanguage> {
&AVAILABLE_LANGUAGES
}
pub fn set_language_precedence(new_precedence: &[GettextLocalizationLanguage]) {
let catalogs = new_precedence
.iter()
.map(|lang| {
(
lang.language,
*CATALOGS
.get(lang.language)
.expect("Only languages for which catalogs exist may be passed to gettext."),
)
})
.collect();
*LANGUAGE_PRECEDENCE.lock().unwrap() = catalogs;
}
pub fn get_language_precedence() -> Vec<&'static str> {
let language_precedence = LANGUAGE_PRECEDENCE.lock().unwrap();
language_precedence.iter().map(|&(lang, _)| lang).collect()
}

View File

@@ -10,7 +10,7 @@ Synopsis
[--set-cursor[=MARKER]] ([-f | --function FUNCTION] | EXPANSION)
abbr --erase [ [-c | --command COMMAND]... ] NAME ...
abbr --rename [ [-c | --command COMMAND]... ] OLD_WORD NEW_WORD
abbr --show
abbr [--show] [--color WHEN]
abbr --list
abbr --query NAME ...
@@ -75,7 +75,6 @@ With **--set-cursor=MARKER**, the cursor is moved to the first occurrence of **M
With **-f FUNCTION** or **--function FUNCTION**, **FUNCTION** is treated as the name of a fish function instead of a literal replacement. When the abbreviation matches, the function will be called with the matching token as an argument. If the function's exit status is 0 (success), the token will be replaced by the function's output; otherwise the token will be left unchanged. No **EXPANSION** may be given separately.
Examples
########

View File

@@ -6,8 +6,8 @@ Synopsis
.. synopsis::
bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] KEYS COMMAND ...
bind [(-M | --mode) MODE] [--preset] [--user] [KEYS]
bind [-a | --all] [--preset] [--user]
bind [(-M | --mode) MODE] [--preset] [--user] [--color WHEN] [KEYS]
bind [-a | --all] [--preset] [--user] [--color WHEN]
bind (-f | --function-names)
bind (-K | --key-names)
bind (-L | --list-modes)
@@ -104,6 +104,10 @@ The following options are available:
**-s** or **--silent**
Silences some of the error messages, including for unknown key names and unbound sequences.
**--color** *WHEN*
Controls when to use syntax highlighting colors when listing bindings.
*WHEN* can be ``auto`` (the default, colorize if the output :doc:`is a terminal <isatty>`), ``always``, or ``never``.
**-h** or **--help**
Displays help about using this command.

View File

@@ -6,7 +6,7 @@ Synopsis
.. synopsis::
complete ((-c | --command) | (-p | --path)) COMMAND [OPTIONS]
complete ((-c | --command) | (-p | --path)) COMMAND [OPTIONS] [--color WHEN]
complete (-C | --do-complete) [--escape] STRING
Description
@@ -74,6 +74,10 @@ The following options are available:
**--escape**
When used with ``-C``, escape special characters in completions.
**--color** *WHEN*
Controls when to use syntax highlighting colors when printing completions.
*WHEN* can be ``auto`` (the default, colorize if the output :doc:`is a terminal <isatty>`), ``always``, or ``never``.
**-h** or **--help**
Displays help about using this command.

View File

@@ -6,8 +6,8 @@ Synopsis
.. synopsis::
functions [-a | --all] [-n | --names]
functions [-D | --details] [-v] FUNCTION
functions [-a | --all] [-n | --names] [--color WHEN]
functions [-D | --details] [-v] [--color WHEN] FUNCTION
functions -c OLDNAME NEWNAME
functions -d DESCRIPTION FUNCTION
functions [-e | -q] FUNCTION ...
@@ -60,6 +60,10 @@ The following options are available:
**-t** or **--handlers-type** *TYPE*
Show all event handlers matching the given *TYPE*.
**--color** *WHEN*
Controls when to use syntax highlighting colors when printing function definitions.
*WHEN* can be ``auto`` (the default, colorize if the output :doc:`is a terminal <isatty>`), ``always``, or ``never``.
**-h** or **--help**
Displays help about using this command.

View File

@@ -75,6 +75,10 @@ These flags can appear before or immediately after one of the sub-commands liste
**-R** or **--reverse**
Causes the history search results to be ordered oldest to newest. Which is the order used by most shells. The default is newest to oldest.
**--color** *WHEN*
Controls when to use syntax highlighting colors for the history entries.
*WHEN* can be ``auto`` (the default, colorize if the output :doc:`is a terminal <isatty>`), ``always``, or ``never``.
**-h** or **--help**
Displays help for this command.

View File

@@ -41,6 +41,10 @@ The following options are available:
**-q** or **--query**
Suppresses all output; this is useful when testing the exit status. For compatibility with old fish versions this is also **--quiet**.
**--color** *WHEN*
Controls when to use syntax highlighting colors when printing function definitions.
*WHEN* can be ``auto`` (the default, colorize if the output :doc:`is a terminal <isatty>`), ``always``, or ``never``.
**-h** or **--help**
Displays help about using this command.

View File

@@ -118,13 +118,12 @@ author = "fish-shell developers"
issue_url = "https://github.com/fish-shell/fish-shell/issues"
# Parsing FISH-BUILD-VERSION-FILE is possible but hard to ensure that it is in the right place
# fish_indent is guaranteed to be on PATH for the Pygments highlighter anyway
if "FISH_BUILD_VERSION_FILE" in os.environ:
# From Cmake
f = open(os.environ["FISH_BUILD_VERSION_FILE"], "r")
ret = f.readline().strip()
elif "FISH_BUILD_VERSION" in os.environ:
ret = os.environ["FISH_BUILD_VERSION"]
else:
# From Cargo, or no build system.
ret = subprocess.check_output(
("../build_tools/git_version_gen.sh", "--stdout"), stderr=subprocess.STDOUT
).decode("utf-8")

View File

@@ -133,7 +133,7 @@ Variable Meaning
.. envvar:: fish_color_end process separators like ``;`` and ``&``
.. envvar:: fish_color_error syntax errors
.. envvar:: fish_color_param ordinary command parameters
.. envvar:: fish_color_valid_path parameters that are filenames (if the file exists)
.. envvar:: fish_color_valid_path parameters and redirection targets that are filenames (if the file exists)
.. envvar:: fish_color_option options starting with "-", up to the first "--" parameter
.. envvar:: fish_color_comment comments like '# important'
.. envvar:: fish_color_selection selected text in vi visual mode

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

@@ -9,9 +9,12 @@ complete -c abbr -f -n $__fish_abbr_not_add_cond -l rename -d 'Rename an abbrevi
complete -c abbr -f -n $__fish_abbr_not_add_cond -s e -l erase -d 'Erase abbreviation' -xa '(abbr --list)'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s s -l show -d 'Print all abbreviations'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s l -l list -d 'Print all abbreviation names'
complete -c abbr -f -n $__fish_abbr_not_add_cond -l color -d 'When to colorize output' -xa 'always never auto'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s h -l help -d Help
complete -c abbr -f -n $__fish_abbr_add_cond -s p -l position -a 'command anywhere' -d 'Expand only as a command, or anywhere' -x
complete -c abbr -f -n $__fish_abbr_add_cond -s f -l function -d 'Treat expansion argument as a fish function' -xa '(functions)'
complete -c abbr -f -n $__fish_abbr_add_cond -s r -l regex -d 'Match a regular expression' -x
complete -c abbr -f -n $__fish_abbr_add_cond -l set-cursor -d 'Position the cursor at % post-expansion'
complete -c abbr -f -n '__fish_seen_subcommand_from -s --show' -l color -d 'When to colorize output' -xa 'always never auto'

View File

@@ -25,6 +25,7 @@ complete -c bind -s L -l list-modes -d 'Display a list of defined bind modes'
complete -c bind -s s -l silent -d 'Operate silently'
complete -c bind -l preset -d 'Operate on preset bindings'
complete -c bind -l user -d 'Operate on user bindings'
complete -c bind -l color -d 'When to colorize output' -xa 'always never auto'
complete -c bind -n '__fish_bind_has_keys (commandline -pcx)' -a '(bind --function-names)' -d 'Function name' -x

View File

@@ -19,6 +19,7 @@ complete -c complete -l escape -d "Make -C escape special characters"
complete -c complete -s n -l condition -d "Completion only used if command has zero exit status" -x
complete -c complete -s w -l wraps -d "Inherit completions from specified command" -xa '(__fish_complete_command)'
complete -c complete -s k -l keep-order -d "Keep order of arguments instead of sorting alphabetically"
complete -c complete -l color -d "When to colorize output" -xa "always never auto"
# Deprecated options

View File

@@ -15,10 +15,72 @@ function __fish_fastboot_list_partition_or_file
end
function __fish_fastboot_list_partition
set -l partitions boot bootloader cache cust dtbo metadata misc modem odm odm_dlkm oem product pvmfw radio recovery system system_ext userdata vbmeta vendor vendor_dlkm vmbeta_system
for i in $partitions
echo $i
end
printf %s "\
abl
aop
aop_config
apdp
bluetooth
boot
countrycode
cpucp
cpucp_dtb
crclist
dcp
ddr
devcfg
dsp
dtbo
featenabler
hyp
hyp_ac_config
idmanager
imagefv
init_boot
keymaster
logfs
metadata
misc
modem
modemfirmware
multiimgoem
multiimgqti
pdp
pdp_cdb
pvmfw
qtvm_dtbo
qupfw
recovery
rescue
secretkeeper
shrm
soccp
soccp_dcd
soccp_debug
sparsecrclist
spuservice
storsec
super
tme_config
tme_fw
tme_seq_patch
toolsfv
tz
tz_ac_config
tz_qti_config
uefi
uefisecapp
userdata
vbmeta
vbmeta_system
vendor_boot
vm-bootsys
xbl
xbl_ac_config
xbl_config
xbl_ramdump
xbl_sc_test_mode
"
end
complete -c fastboot -s h -l help -d 'Show this message'

View File

@@ -19,3 +19,4 @@ complete -c functions -s D -l details -d "Display information about the function
complete -c functions -s v -l verbose -d "Print more output"
complete -c functions -s H -l handlers -d "Show event handlers"
complete -c functions -s t -l handlers-type -d "Show event handlers matching the given type" -x -a "signal variable exit job-id generic"
complete -c functions -l color -d 'When to colorize output' -x -a 'always never auto'

View File

@@ -1070,8 +1070,17 @@ complete -f -c git -n '__fish_git_using_command fetch' -l refetch -d 'Re-fetch w
__fish_git_add_revision_completion -n '__fish_git_using_command fetch' -l negotiation-tip -d 'Only report commits reachable from these tips' -x
complete -f -c git -n '__fish_git_using_command fetch' -l negotiate-only -d "Don't fetch, only show commits in common with the server"
complete -f -c git -n '__fish_git_using_command fetch' -l filter -ra '(__fish_git_filters)' -d 'Request a subset of objects from server'
# TODO other options
complete -f -c git -n '__fish_git_using_command fetch' -s 4 -l ipv4 -d 'Use IPv4 addresses only'
complete -f -c git -n '__fish_git_using_command fetch' -s 6 -l ipv6 -d 'Use IPv6 addresses only'
complete -x -c git -n '__fish_git_using_command fetch' -l depth -d 'Limit fetching to specified depth'
complete -x -c git -n '__fish_git_using_command fetch' -l deepen -d 'Deepen history of shallow repository'
complete -x -c git -n '__fish_git_using_command fetch' -l shallow-since -d 'Deepen history after specified date'
complete -x -c git -n '__fish_git_using_command fetch' -l shallow-exclude -d 'Deepen history excluding branch'
complete -f -c git -n '__fish_git_using_command fetch' -l unshallow -d 'Convert shallow repository to complete one'
complete -f -c git -n '__fish_git_using_command fetch' -l update-shallow -d 'Accept refs that update .git/shallow'
complete -x -c git -n '__fish_git_using_command fetch' -l refmap -d 'Specify refspec to map the refs to remote-tracking branches'
complete -f -c git -n '__fish_git_using_command fetch' -l write-fetch-head -d 'Write FETCH_HEAD file (default)'
complete -f -c git -n '__fish_git_using_command fetch' -l no-write-fetch-head -d 'Do not write FETCH_HEAD file'
#### filter-branch
complete -f -c git -n __fish_git_needs_command -a filter-branch -d 'Rewrite branches'
@@ -1151,7 +1160,18 @@ complete -f -c git -n '__fish_git_using_command show-branch' -l no-color -d "Tur
complete -f -c git -n '__fish_git_using_command show-branch' -l merge-base -d "Determine merge bases for the given commits"
complete -f -c git -n '__fish_git_using_command show-branch' -l independent -d "Show which refs can't be reached from any other"
complete -f -c git -n '__fish_git_using_command show-branch' -l topics -d "Show only commits that are not on the first given branch"
# TODO options
complete -f -c git -n '__fish_git_using_command show-branch' -l all -d 'Show all refs under refs/heads and refs/remotes'
complete -f -c git -n '__fish_git_using_command show-branch' -s r -l remotes -d 'Show all refs under refs/remotes'
complete -f -c git -n '__fish_git_using_command show-branch' -l current -d 'Include the current branch to the list of revs'
complete -f -c git -n '__fish_git_using_command show-branch' -l topo-order -d 'Do not show commits in reverse chronological order'
complete -f -c git -n '__fish_git_using_command show-branch' -l date-order -d 'Show commits in chronological order'
complete -f -c git -n '__fish_git_using_command show-branch' -l sparse -d 'Show merges only reachable from one tip'
complete -x -c git -n '__fish_git_using_command show-branch' -l more -d 'Go N more commits beyond common ancestor'
complete -f -c git -n '__fish_git_using_command show-branch' -l list -d 'Show branches and their commits'
complete -f -c git -n '__fish_git_using_command show-branch' -l sha1-name -d 'Name commits with unique prefix of SHA-1'
complete -f -c git -n '__fish_git_using_command show-branch' -l no-name -d 'Do not show naming strings for each commit'
complete -x -c git -n '__fish_git_using_command show-branch' -l color -a 'always never auto' -d 'Color the status sign'
complete -f -c git -n '__fish_git_using_command show-branch' -l no-color -d 'Turn off colored output'
### add
complete -c git -n __fish_git_needs_command -a add -d 'Add file contents to the staging area'
@@ -1170,10 +1190,10 @@ complete -c git -n '__fish_git_using_command add' -l ignore-errors -d 'Ignore er
complete -c git -n '__fish_git_using_command add' -l ignore-missing -d 'Check if any of the given files would be ignored'
# Renames also show up as untracked + deleted, and to get git to show it as a rename _both_ need to be added.
# However, we can't do that as it is two tokens, so we don't need renamed here.
complete -f -c git -n '__fish_git_using_command add; and test "$(__fish_git config --get bash.showUntrackedFiles)" != 0' -a '(__fish_git_files modified untracked deleted unmerged modified-staged-deleted)'
complete -f -c git -n '__fish_git_using_command add; and test "$(__fish_git config --type bool --get status.showUntrackedFiles)" != false' -a '(__fish_git_files modified untracked deleted unmerged modified-staged-deleted)'
# If we have so many files that you disable untrackedfiles, let's add file completions,
# to avoid slurping megabytes of git output.
complete -F -c git -n '__fish_git_using_command add; and test "$(__fish_git config --get bash.showUntrackedFiles)" = 0' -a '(__fish_git_files modified deleted unmerged modified-staged-deleted)'
complete -F -c git -n '__fish_git_using_command add; and test "$(__fish_git config --type bool --get status.showUntrackedFiles)" = false' -a '(__fish_git_files modified deleted unmerged modified-staged-deleted)'
### am
complete -c git -n __fish_git_needs_command -a am -d 'Apply patches from a mailbox'
@@ -1201,6 +1221,7 @@ complete -f -c git -n '__fish_git_using_command am' -l ignore-date -d 'Treat aut
complete -f -c git -n '__fish_git_using_command am' -l skip -d 'Skip current patch'
complete -x -c git -n '__fish_git_using_command am' -s S -l gpg-sign -a '(type -q gpg && __fish_complete_gpg_key_id gpg)' -d 'Sign commits with gpg'
complete -f -c git -n '__fish_git_using_command am' -l no-gpg-sign -d 'Do not sign commits'
complete -f -c git -n '__fish_git_using_command am' -s n -l no-verify -d 'Do not run pre-applypatch and applypatch-msg hooks'
complete -f -c git -n '__fish_git_using_command am' -s r -l resolved -l continue -d 'Mark patch failures as resolved'
complete -x -c git -n '__fish_git_using_command am' -l resolvemsg -d 'Message to print after patch failure'
complete -f -c git -n '__fish_git_using_command am' -l abort -d 'Abort patch operation and restore branch'
@@ -1229,7 +1250,15 @@ complete -f -c git -n '__fish_git_using_command checkout' -l no-recurse-submodul
complete -f -c git -n '__fish_git_using_command checkout' -l progress -d 'Report progress even if not connected to a terminal'
complete -f -c git -n '__fish_git_using_command checkout' -l no-progress -d "Don't report progress"
complete -f -c git -n '__fish_git_using_command checkout' -s f -l force -d 'Switch even if working tree differs or unmerged files exist'
# TODO options
complete -f -c git -n '__fish_git_using_command checkout' -s q -l quiet -d 'Suppress feedback messages'
complete -f -c git -n '__fish_git_using_command checkout' -l detach -d 'Detach HEAD at named commit'
complete -f -c git -n '__fish_git_using_command checkout' -l guess -d 'Guess remote tracking branch (default)'
complete -f -c git -n '__fish_git_using_command checkout' -l no-guess -d 'Do not guess remote tracking branch'
complete -f -c git -n '__fish_git_using_command checkout' -l overlay -d 'Never remove files from working tree in checkout'
complete -f -c git -n '__fish_git_using_command checkout' -l no-overlay -d 'Remove files from working tree not in tree-ish'
complete -F -c git -n '__fish_git_using_command checkout' -l pathspec-from-file -d 'Read pathspec from file'
complete -f -c git -n '__fish_git_using_command checkout' -l pathspec-file-nul -d 'NUL separator for pathspec-from-file'
complete -f -c git -n '__fish_git_using_command checkout' -l ignore-skip-worktree-bits -d 'Check out all files including sparse entries'
### sparse-checkout
# `init` subcommand is deprecated
@@ -1281,7 +1310,12 @@ complete -f -c git -n __fish_git_needs_command -a archive -d 'Create an archive
complete -f -c git -n '__fish_git_using_command archive' -s l -l list -d "Show all available formats"
complete -f -c git -n '__fish_git_using_command archive' -s v -l verbose -d "Be verbose"
complete -f -c git -n '__fish_git_using_command archive' -l worktree-attributes -d "Look for attributes in .gitattributes files in the working tree as well"
# TODO options
complete -x -c git -n '__fish_git_using_command archive' -l format -a 'tar zip tar.gz tgz' -d 'Format of the resulting archive'
complete -F -c git -n '__fish_git_using_command archive' -s o -l output -d 'Write the archive to file instead of stdout'
complete -x -c git -n '__fish_git_using_command archive' -l prefix -d 'Prepend prefix to each pathname in the archive'
complete -F -c git -n '__fish_git_using_command archive' -l add-file -d 'Add non-tracked file to the archive'
complete -x -c git -n '__fish_git_using_command archive' -l remote -d 'Retrieve a tar archive from a remote repository'
complete -x -c git -n '__fish_git_using_command archive' -l exec -d 'Path to git-upload-archive on the remote side'
### bisect
complete -f -c git -n __fish_git_needs_command -a bisect -d 'Use binary search to find what introduced a bug'
@@ -1442,7 +1476,21 @@ complete -f -c git -n '__fish_git_using_command commit' -l no-signoff -d 'Do not
complete -f -c git -n '__fish_git_using_command commit' -s C -l reuse-message -d 'Reuse log message and authorship of an existing commit'
complete -f -c git -n '__fish_git_using_command commit' -s c -l reedit-message -d 'Like --reuse-message, but allow editing commit message'
complete -f -c git -n '__fish_git_using_command commit' -s e -l edit -d 'Further edit the message taken from -m, -C, or -F'
# TODO options
complete -f -c git -n '__fish_git_using_command commit' -l no-edit -d 'Do not edit the commit message'
complete -f -c git -n '__fish_git_using_command commit' -s q -l quiet -d 'Suppress commit summary message'
complete -f -c git -n '__fish_git_using_command commit' -l dry-run -d 'Show what would be committed without committing'
complete -f -c git -n '__fish_git_using_command commit' -l short -d 'Show short format output for dry-run'
complete -f -c git -n '__fish_git_using_command commit' -l porcelain -d 'Show porcelain format output for dry-run'
complete -f -c git -n '__fish_git_using_command commit' -l long -d 'Show long format output for dry-run (default)'
complete -f -c git -n '__fish_git_using_command commit' -s z -l null -d 'Terminate entries with NUL in dry-run'
complete -f -c git -n '__fish_git_using_command commit' -l status -d 'Include git status output in commit message template'
complete -f -c git -n '__fish_git_using_command commit' -l no-status -d 'Do not include git status output in commit message template'
complete -f -c git -n '__fish_git_using_command commit' -s i -l include -d 'Stage contents of given paths before committing'
complete -f -c git -n '__fish_git_using_command commit' -s o -l only -d 'Commit only from paths specified on command line'
complete -f -c git -n '__fish_git_using_command commit' -l trailer -d 'Add trailer to commit message'
complete -F -c git -n '__fish_git_using_command commit' -s t -l template -d 'Use file as commit message template'
complete -F -c git -n '__fish_git_using_command commit' -l pathspec-from-file -d 'Read pathspec from file'
complete -f -c git -n '__fish_git_using_command commit' -l pathspec-file-nul -d 'NUL separator for pathspec-from-file'
### count-objects
complete -f -c git -n __fish_git_needs_command -a count-objects -d 'Count number of objects and disk consumption'
@@ -1550,7 +1598,13 @@ complete -f -c git -n '__fish_git_using_command difftool' -l tool-help -d 'Print
complete -f -c git -n '__fish_git_using_command difftool' -l trust-exit-code -d 'Exit when an invoked diff tool returns a non-zero exit code'
complete -f -c git -n '__fish_git_using_command difftool' -s x -l extcmd -d 'Specify a custom command for viewing diffs'
complete -f -c git -n '__fish_git_using_command difftool' -l no-gui -d 'Overrides --gui setting'
# TODO options
complete -f -c git -n '__fish_git_using_command difftool' -l tool-help -d 'Print a list of diff tools that may be used'
complete -f -c git -n '__fish_git_using_command difftool' -s d -l dir-diff -d 'Copy modified files to a temporary location and perform dir diff'
complete -f -c git -n '__fish_git_using_command difftool' -l symlinks -d 'Use symlinks in dir-diff mode'
complete -f -c git -n '__fish_git_using_command difftool' -l no-symlinks -d 'Do not use symlinks in dir-diff mode'
complete -f -c git -n '__fish_git_using_command difftool' -l no-trust-exit-code -d 'Do not exit when diff tool returns non-zero'
complete -f -c git -n '__fish_git_using_command difftool' -s y -l no-prompt -d 'Do not prompt before launching diff tool'
complete -f -c git -n '__fish_git_using_command difftool' -l prompt -d 'Prompt before each invocation of diff tool'
### gc
complete -f -c git -n __fish_git_needs_command -a gc -d 'Collect garbage (unreachable commits etc)'
@@ -1603,13 +1657,29 @@ complete -f -c git -n '__fish_git_using_command grep' -l not -d 'Combine pattern
complete -f -c git -n '__fish_git_using_command grep' -l all-match -d 'Only match files that can match all the pattern expressions when giving multiple'
complete -f -c git -n '__fish_git_using_command grep' -s q -l quiet -d 'Just exit with status 0 when there is a match and with non-zero status when there isn\'t'
complete -c git -n '__fish_git_using_command grep' -n 'not contains -- -- (commandline -xpc)' -ka '(__fish_git_refs)'
# TODO options, including max-depth, h, open-files-in-pager, contexts, threads, file
complete -x -c git -n '__fish_git_using_command grep' -l max-depth -d 'Maximum depth of directory traversal'
complete -f -c git -n '__fish_git_using_command grep' -s h -d 'Do not output the filename for each match'
complete -f -c git -n '__fish_git_using_command grep' -s H -d 'Show filename for each match (default)'
complete -x -c git -n '__fish_git_using_command grep' -s O -l open-files-in-pager -d 'Open matching files in pager'
complete -x -c git -n '__fish_git_using_command grep' -s C -l context -d 'Show context lines before and after matches'
complete -x -c git -n '__fish_git_using_command grep' -s A -l after-context -d 'Show context lines after matches'
complete -x -c git -n '__fish_git_using_command grep' -s B -l before-context -d 'Show context lines before matches'
complete -x -c git -n '__fish_git_using_command grep' -l threads -d 'Number of grep worker threads to use'
complete -f -c git -n '__fish_git_using_command grep' -l break -d 'Print empty line between matches from different files'
complete -f -c git -n '__fish_git_using_command grep' -l heading -d 'Show filename above matches in that file'
complete -f -c git -n '__fish_git_using_command grep' -l untracked -d 'Search in untracked files'
complete -f -c git -n '__fish_git_using_command grep' -l no-index -d 'Search files in current directory that is not managed by Git'
complete -f -c git -n '__fish_git_using_command grep' -l recurse-submodules -d 'Recursively search in each submodule'
### init
complete -f -c git -n __fish_git_needs_command -a init -d 'Create an empty git repository'
complete -f -c git -n '__fish_git_using_command init' -s q -l quiet -d 'Only print error and warning messages'
complete -f -c git -n '__fish_git_using_command init' -l bare -d 'Create a bare repository'
# TODO options
complete -F -c git -n '__fish_git_using_command init' -l template -d 'Directory from which templates will be used'
complete -F -c git -n '__fish_git_using_command init' -l separate-git-dir -d 'Create git dir at specified path'
complete -x -c git -n '__fish_git_using_command init' -l object-format -a 'sha1 sha256' -d 'Specify hash algorithm for objects'
complete -x -c git -n '__fish_git_using_command init' -s b -l initial-branch -d 'Use specified name for initial branch'
complete -x -c git -n '__fish_git_using_command init' -l shared -a 'false true umask group all world everybody' -d 'Specify that the repository is shared'
### shortlog
complete -c git -n __fish_git_needs_command -a shortlog -d 'Show commit shortlog'
@@ -1951,7 +2021,7 @@ complete -f -c git -n __fish_git_needs_command -a prune -d 'Prune unreachable ob
complete -f -c git -n '__fish_git_using_command prune' -s n -l dry-run -d 'Just report what it would remove'
complete -f -c git -n '__fish_git_using_command prune' -s v -l verbose -d 'Report all removed objects'
complete -f -c git -n '__fish_git_using_command prune' -l progress -d 'Show progress'
# TODO options
complete -x -c git -n '__fish_git_using_command prune' -l expire -d 'Only expire loose objects older than date'
### pull
complete -f -c git -n __fish_git_needs_command -a pull -d 'Fetch from and merge with another repo or branch'
@@ -1994,7 +2064,18 @@ complete -f -c git -n '__fish_git_using_command pull' -s r -l rebase -d 'Rebase
complete -f -c git -n '__fish_git_using_command pull' -l no-rebase -d 'Do not rebase the current branch on top of the upstream branch'
complete -f -c git -n '__fish_git_using_command pull' -l autostash -d 'Before starting rebase, stash local changes, and apply stash when done'
complete -f -c git -n '__fish_git_using_command pull' -l no-autostash -d 'Do not stash local changes before starting rebase'
# TODO other options
complete -f -c git -n '__fish_git_using_command pull' -l verify -d 'Allow the pre-merge and commit-msg hooks to run (default)'
complete -f -c git -n '__fish_git_using_command pull' -l no-verify -d 'Do not run pre-merge and commit-msg hooks'
complete -x -c git -n '__fish_git_using_command pull' -l upload-pack -d 'Path to git-upload-pack on remote'
complete -x -c git -n '__fish_git_using_command pull' -l depth -d 'Limit fetching to specified number of commits'
complete -x -c git -n '__fish_git_using_command pull' -l deepen -d 'Deepen history of shallow repository by specified commits'
complete -x -c git -n '__fish_git_using_command pull' -l shallow-since -d 'Deepen history after specified date'
complete -x -c git -n '__fish_git_using_command pull' -l shallow-exclude -d 'Deepen history excluding commits reachable from branch'
complete -f -c git -n '__fish_git_using_command pull' -l unshallow -d 'Convert shallow repository to complete one'
complete -f -c git -n '__fish_git_using_command pull' -l update-shallow -d 'Accept refs that update .git/shallow'
complete -x -c git -n '__fish_git_using_command pull' -s j -l jobs -d 'Number of parallel children for fetching'
complete -f -c git -n '__fish_git_using_command pull' -s 4 -l ipv4 -d 'Use IPv4 addresses only'
complete -f -c git -n '__fish_git_using_command pull' -s 6 -l ipv6 -d 'Use IPv6 addresses only'
### range-diff
complete -f -c git -n __fish_git_needs_command -a range-diff -d 'Compare two commit ranges'
@@ -2031,7 +2112,21 @@ complete -f -c git -n '__fish_git_using_command push' -s u -l set-upstream -d 'A
complete -f -c git -n '__fish_git_using_command push' -s q -l quiet -d 'Be quiet'
complete -f -c git -n '__fish_git_using_command push' -s v -l verbose -d 'Be verbose'
complete -f -c git -n '__fish_git_using_command push' -l progress -d 'Force progress status'
# TODO --recurse-submodules=check|on-demand
complete -f -c git -n '__fish_git_using_command push' -l verify -d 'Allow the pre-push hook to run (default)'
complete -f -c git -n '__fish_git_using_command push' -l no-verify -d 'Do not run the pre-push hook'
complete -x -c git -n '__fish_git_using_command push' -l recurse-submodules -a 'check on-demand only no' -d 'Control recursive pushing of submodules'
complete -f -c git -n '__fish_git_using_command push' -l signed -d 'GPG-sign the push'
complete -f -c git -n '__fish_git_using_command push' -l no-signed -d 'Do not GPG-sign the push'
complete -f -c git -n '__fish_git_using_command push' -l atomic -d 'Request atomic transaction on remote side'
complete -f -c git -n '__fish_git_using_command push' -l no-atomic -d 'Do not request atomic transaction'
complete -f -c git -n '__fish_git_using_command push' -l thin -d 'Spend extra cycles to minimize number of objects'
complete -f -c git -n '__fish_git_using_command push' -l no-thin -d 'Do not use thin pack'
complete -f -c git -n '__fish_git_using_command push' -s 4 -l ipv4 -d 'Use IPv4 addresses only'
complete -f -c git -n '__fish_git_using_command push' -s 6 -l ipv6 -d 'Use IPv6 addresses only'
complete -x -c git -n '__fish_git_using_command push' -s o -l push-option -d 'Transmit string to server'
complete -x -c git -n '__fish_git_using_command push' -l repo -d 'Override configured repository'
complete -x -c git -n '__fish_git_using_command push' -l receive-pack -d 'Path to git-receive-pack on remote'
complete -x -c git -n '__fish_git_using_command push' -l exec -d 'Same as --receive-pack'
### rebase
complete -f -c git -n __fish_git_needs_command -a rebase -d 'Reapply commit sequence on a new base'
@@ -2087,7 +2182,12 @@ __fish_git_add_revision_completion -n '__fish_git_using_command reset'
complete -f -c git -n '__fish_git_using_command reset' -n 'not contains -- -- (commandline -xpc)' -a '(__fish_git_files all-staged modified)'
complete -F -c git -n '__fish_git_using_command reset' -n 'contains -- -- (commandline -xpc)'
complete -f -c git -n '__fish_git_using_command reset' -n 'not contains -- -- (commandline -xpc)' -a '(__fish_git_reflog)' -d Reflog
# TODO options
complete -f -c git -n '__fish_git_using_command reset' -s q -l quiet -d 'Be quiet, only report errors'
complete -f -c git -n '__fish_git_using_command reset' -s p -l patch -d 'Interactively select hunks to reset'
complete -f -c git -n '__fish_git_using_command reset' -l merge -d 'Reset index and update files in working tree that differ'
complete -f -c git -n '__fish_git_using_command reset' -l keep -d 'Reset index but keep changes in working tree'
complete -F -c git -n '__fish_git_using_command reset' -l pathspec-from-file -d 'Read pathspec from file'
complete -f -c git -n '__fish_git_using_command reset' -l pathspec-file-nul -d 'NUL separator for pathspec-from-file'
### restore and switch
# restore options
@@ -2155,7 +2255,17 @@ complete -f -c git -n '__fish_git_using_command revert' -s n -l no-commit -d 'Ap
complete -f -c git -n '__fish_git_using_command revert' -s s -l signoff -d 'Add a Signed-off-by trailer at the end of the commit message'
complete -f -c git -n '__fish_git_using_command revert' -l rerere-autoupdate -d 'Allow the rerere mechanism to update the index automatically'
complete -f -c git -n '__fish_git_using_command revert' -l no-rerere-autoupdate -d 'Prevent the rerere mechanism from updating the index with auto-conflict resolution'
# TODO options
complete -f -c git -n '__fish_git_using_command revert' -l abort -d 'Cancel the operation and return to pre-sequence state'
complete -f -c git -n '__fish_git_using_command revert' -l continue -d 'Continue the operation after resolving conflicts'
complete -f -c git -n '__fish_git_using_command revert' -l quit -d 'Clear the sequencer state after a failed revert'
complete -f -c git -n '__fish_git_using_command revert' -l skip -d 'Skip the current commit and continue with the rest'
complete -f -c git -n '__fish_git_using_command revert' -s e -l edit -d 'Edit the commit message before committing'
complete -f -c git -n '__fish_git_using_command revert' -l no-edit -d 'Do not edit the commit message'
complete -x -c git -n '__fish_git_using_command revert' -s m -l mainline -d 'Select parent number for reverting merge commits'
complete -x -c git -n '__fish_git_using_command revert' -s S -l gpg-sign -a '(type -q gpg && __fish_complete_gpg_key_id gpg)' -d 'GPG-sign commits'
complete -f -c git -n '__fish_git_using_command revert' -l no-gpg-sign -d 'Do not GPG-sign commits'
complete -x -c git -n '__fish_git_using_command revert' -s s -l strategy -d 'Use the given merge strategy'
complete -x -c git -n '__fish_git_using_command revert' -s X -l strategy-option -d 'Pass option to the merge strategy'
### rm
complete -c git -n __fish_git_needs_command -a rm -d 'Remove files from the working tree and/or staging area'
@@ -2167,7 +2277,8 @@ complete -c git -n '__fish_git_using_command rm' -s q -l quiet -d 'Be quiet'
complete -c git -n '__fish_git_using_command rm' -s f -l force -d 'Override the up-to-date check'
complete -c git -n '__fish_git_using_command rm' -s n -l dry-run -d 'Dry run'
complete -c git -n '__fish_git_using_command rm' -l sparse -d 'Allow updating index entries outside of the sparse-checkout cone'
# TODO options
complete -F -c git -n '__fish_git_using_command rm' -l pathspec-from-file -d 'Read pathspec from file'
complete -f -c git -n '__fish_git_using_command rm' -l pathspec-file-nul -d 'NUL separator for pathspec-from-file'
### status
complete -f -c git -n __fish_git_needs_command -a status -d 'Show the working tree status'
@@ -2181,7 +2292,13 @@ complete -f -c git -n '__fish_git_using_command status' -s v -l verbose -d 'Also
complete -f -c git -n '__fish_git_using_command status' -l no-ahead-behind -d 'Do not display detailed ahead/behind upstream-branch counts'
complete -f -c git -n '__fish_git_using_command status' -l renames -d 'Turn on rename detection regardless of user configuration'
complete -f -c git -n '__fish_git_using_command status' -l no-renames -d 'Turn off rename detection regardless of user configuration'
# TODO options
complete -f -c git -n '__fish_git_using_command status' -l ahead-behind -d 'Display detailed ahead/behind upstream-branch counts'
complete -f -c git -n '__fish_git_using_command status' -l long -d 'Give the output in the long-format (default)'
complete -f -c git -n '__fish_git_using_command status' -l show-stash -d 'Show the number of entries currently stashed'
complete -x -c git -n '__fish_git_using_command status' -l column -d 'Display untracked files in columns'
complete -f -c git -n '__fish_git_using_command status' -l no-column -d 'Do not display untracked files in columns'
complete -F -c git -n '__fish_git_using_command status' -l pathspec-from-file -d 'Read pathspec from file'
complete -f -c git -n '__fish_git_using_command status' -l pathspec-file-nul -d 'NUL separator for pathspec-from-file'
### stripspace
complete -f -c git -n __fish_git_needs_command -a stripspace -d 'Remove unnecessary whitespace'
@@ -2199,7 +2316,19 @@ complete -f -c git -n '__fish_git_using_command tag' -s f -l force -d 'Force ove
complete -f -c git -n '__fish_git_using_command tag' -s l -l list -d 'List tags'
complete -f -c git -n '__fish_git_using_command tag' -l contains -xka '(__fish_git_commits)' -d 'List tags that contain a commit'
complete -f -c git -n '__fish_git_using_command tag' -n '__fish_git_contains_opt -s d delete -s v verify -s f force' -ka '(__fish_git_tags)' -d Tag
# TODO options
complete -x -c git -n '__fish_git_using_command tag' -s m -l message -d 'Use the given tag message'
complete -F -c git -n '__fish_git_using_command tag' -s F -l file -d 'Read tag message from file'
complete -x -c git -n '__fish_git_using_command tag' -s u -l local-user -d 'Use this key to sign tag'
complete -x -c git -n '__fish_git_using_command tag' -l cleanup -a 'strip whitespace verbatim' -d 'How to clean up the tag message'
complete -f -c git -n '__fish_git_using_command tag' -l create-reflog -d 'Create reflog for the tag'
complete -f -c git -n '__fish_git_using_command tag' -l no-create-reflog -d 'Do not create reflog for the tag'
complete -x -c git -n '__fish_git_using_command tag' -l color -a 'always never auto' -d 'Respect any colors in format'
complete -f -c git -n '__fish_git_using_command tag' -l column -d 'Display tag listing in columns'
complete -f -c git -n '__fish_git_using_command tag' -l no-column -d 'Do not display tag listing in columns'
complete -x -c git -n '__fish_git_using_command tag' -l sort -d 'Sort tags based on the given key'
complete -f -c git -n '__fish_git_using_command tag' -l merged -d 'List tags whose commits are reachable from specified commit'
complete -f -c git -n '__fish_git_using_command tag' -l no-merged -d 'List tags whose commits are not reachable from specified commit'
complete -x -c git -n '__fish_git_using_command tag' -l points-at -d 'List tags of the given object'
### update-index
complete -c git -n __fish_git_needs_command -a update-index -d 'Register file contents in the working tree to the index'
@@ -2303,7 +2432,37 @@ complete -f -c git -n '__fish_git_using_command stash' -n '__fish_git_stash_usin
### config
complete -f -c git -n __fish_git_needs_command -a config -d 'Set and read git configuration variables'
# TODO options
complete -f -c git -n '__fish_git_using_command config' -l local -d 'Write to repository .git/config'
complete -f -c git -n '__fish_git_using_command config' -l global -d 'Write to global ~/.gitconfig'
complete -f -c git -n '__fish_git_using_command config' -l system -d 'Write to system-wide /etc/gitconfig'
complete -f -c git -n '__fish_git_using_command config' -l worktree -d 'Write to .git/config.worktree'
complete -F -c git -n '__fish_git_using_command config' -s f -l file -d 'Use given config file'
complete -F -c git -n '__fish_git_using_command config' -l blob -d 'Read config from given blob object'
complete -f -c git -n '__fish_git_using_command config' -s l -l list -d 'List all variables set in config'
complete -f -c git -n '__fish_git_using_command config' -s e -l edit -d 'Open config file in editor'
complete -f -c git -n '__fish_git_using_command config' -l get -d 'Get value for given key'
complete -f -c git -n '__fish_git_using_command config' -l get-all -d 'Get all values for a multi-valued key'
complete -f -c git -n '__fish_git_using_command config' -l get-regexp -d 'Get values for keys matching regex'
complete -f -c git -n '__fish_git_using_command config' -l get-urlmatch -d 'Get value for URL-specific key'
complete -f -c git -n '__fish_git_using_command config' -l add -d 'Add new line without altering existing values'
complete -f -c git -n '__fish_git_using_command config' -l unset -d 'Remove line matching key'
complete -f -c git -n '__fish_git_using_command config' -l unset-all -d 'Remove all lines matching key'
complete -f -c git -n '__fish_git_using_command config' -l replace-all -d 'Replace all matching lines'
complete -f -c git -n '__fish_git_using_command config' -l rename-section -d 'Rename given section'
complete -f -c git -n '__fish_git_using_command config' -l remove-section -d 'Remove given section'
complete -x -c git -n '__fish_git_using_command config' -l type -a 'bool int bool-or-int path expiry-date color' -d 'Ensure value is of given type'
complete -f -c git -n '__fish_git_using_command config' -l bool -d 'Value is true or false'
complete -f -c git -n '__fish_git_using_command config' -l int -d 'Value is a decimal number'
complete -f -c git -n '__fish_git_using_command config' -l bool-or-int -d 'Value is bool or int'
complete -f -c git -n '__fish_git_using_command config' -l path -d 'Value is a path'
complete -f -c git -n '__fish_git_using_command config' -s z -l null -d 'Terminate values with NUL byte'
complete -f -c git -n '__fish_git_using_command config' -l name-only -d 'Output only names of config variables'
complete -f -c git -n '__fish_git_using_command config' -l show-origin -d 'Show origin of config'
complete -f -c git -n '__fish_git_using_command config' -l show-scope -d 'Show scope of config'
complete -f -c git -n '__fish_git_using_command config' -l includes -d 'Respect include directives'
complete -f -c git -n '__fish_git_using_command config' -l no-includes -d 'Do not respect include directives'
complete -x -c git -n '__fish_git_using_command config' -l default -d 'Use default value when variable is missing'
complete -f -c git -n '__fish_git_using_command config' -a '(__fish_git_config_keys)' -d 'Config key'
### format-patch
complete -f -c git -n __fish_git_needs_command -a format-patch -d 'Generate patch series to send upstream'
@@ -2386,7 +2545,7 @@ complete -f -c git -n '__fish_git_using_command clean' -s q -l quiet -d 'Be quie
complete -f -c git -n '__fish_git_using_command clean' -s d -d 'Remove untracked directories in addition to untracked files'
complete -f -c git -n '__fish_git_using_command clean' -s x -d 'Remove ignored files, as well'
complete -f -c git -n '__fish_git_using_command clean' -s X -d 'Remove only ignored files'
# TODO -e option
complete -x -c git -n '__fish_git_using_command clean' -s e -l exclude -d 'Add pattern to exclude from removal'
### git blame
complete -f -c git -n __fish_git_needs_command -a blame -d 'Show what last modified each line of a file'
@@ -2644,6 +2803,150 @@ function __fish_git_sort_keys
end
complete -f -c git -n "__fish_seen_subcommand_from $sortcommands" -l sort -d 'Sort results by' -a "(__fish_git_sort_keys)"
### Plumbing commands
### cat-file
complete -f -c git -n __fish_git_needs_command -a cat-file -d 'Provide content or type info for repository objects'
complete -f -c git -n '__fish_git_using_command cat-file' -s t -d 'Show object type'
complete -f -c git -n '__fish_git_using_command cat-file' -s s -d 'Show object size'
complete -f -c git -n '__fish_git_using_command cat-file' -s e -d 'Exit with zero if object exists and is valid'
complete -f -c git -n '__fish_git_using_command cat-file' -s p -d 'Pretty-print object content'
complete -f -c git -n '__fish_git_using_command cat-file' -l textconv -d 'Show content as transformed by a textconv filter'
complete -f -c git -n '__fish_git_using_command cat-file' -l filters -d 'Show content as transformed by filters'
complete -f -c git -n '__fish_git_using_command cat-file' -l batch -d 'Read objects from stdin and print info'
complete -f -c git -n '__fish_git_using_command cat-file' -l batch-check -d 'Read objects from stdin and print type/size info'
complete -f -c git -n '__fish_git_using_command cat-file' -l batch-all-objects -d 'Print info for all objects'
complete -f -c git -n '__fish_git_using_command cat-file' -l follow-symlinks -d 'Follow symlinks when using --batch'
### ls-remote
complete -f -c git -n __fish_git_needs_command -a ls-remote -d 'List references in a remote repository'
complete -f -c git -n '__fish_git_using_command ls-remote' -s h -l heads -d 'Limit to refs/heads'
complete -f -c git -n '__fish_git_using_command ls-remote' -s t -l tags -d 'Limit to refs/tags'
complete -f -c git -n '__fish_git_using_command ls-remote' -l refs -d 'Do not show peeled tags or pseudorefs'
complete -f -c git -n '__fish_git_using_command ls-remote' -s q -l quiet -d 'Do not print remote URL'
complete -x -c git -n '__fish_git_using_command ls-remote' -l upload-pack -d 'Path to git-upload-pack on remote'
complete -f -c git -n '__fish_git_using_command ls-remote' -l exit-code -d 'Exit with status 2 if no matching refs are found'
complete -f -c git -n '__fish_git_using_command ls-remote' -l get-url -d 'Print URL of remote'
complete -f -c git -n '__fish_git_using_command ls-remote' -l symref -d 'Show underlying ref for symbolic refs'
complete -x -c git -n '__fish_git_using_command ls-remote' -l sort -d 'Sort based on the given key'
complete -f -c git -n '__fish_git_using_command ls-remote' -a '(__fish_git_remotes)' -d Remote
### ls-tree
complete -f -c git -n __fish_git_needs_command -a ls-tree -d 'List contents of a tree object'
complete -f -c git -n '__fish_git_using_command ls-tree' -s d -d 'Only show trees'
complete -f -c git -n '__fish_git_using_command ls-tree' -s r -d 'Recurse into subtrees'
complete -f -c git -n '__fish_git_using_command ls-tree' -s t -d 'Show tree entries even when recursing'
complete -f -c git -n '__fish_git_using_command ls-tree' -l name-only -d 'Show only names'
complete -f -c git -n '__fish_git_using_command ls-tree' -l name-status -d 'Show only names (same as name-only)'
complete -f -c git -n '__fish_git_using_command ls-tree' -l full-name -d 'Show full path names'
complete -f -c git -n '__fish_git_using_command ls-tree' -l full-tree -d 'Do not limit listing to current directory'
complete -f -c git -n '__fish_git_using_command ls-tree' -s z -d 'NUL line termination on output'
complete -f -c git -n '__fish_git_using_command ls-tree' -l long -d 'Show object size'
complete -f -c git -n '__fish_git_using_command ls-tree' -l abbrev -d 'Show abbreviated object names'
complete -f -c git -n '__fish_git_using_command ls-tree' -a '(__fish_git_refs)' -d Ref
### show-ref
complete -f -c git -n __fish_git_needs_command -a show-ref -d 'List references in a local repository'
complete -f -c git -n '__fish_git_using_command show-ref' -l head -d 'Show HEAD reference'
complete -f -c git -n '__fish_git_using_command show-ref' -l heads -d 'Limit to refs/heads'
complete -f -c git -n '__fish_git_using_command show-ref' -l tags -d 'Limit to refs/tags'
complete -f -c git -n '__fish_git_using_command show-ref' -s d -l dereference -d 'Dereference tags'
complete -f -c git -n '__fish_git_using_command show-ref' -s s -l hash -d 'Only show SHA-1 hash'
complete -f -c git -n '__fish_git_using_command show-ref' -l verify -d 'Enable stricter reference checking'
complete -f -c git -n '__fish_git_using_command show-ref' -l abbrev -d 'Show abbreviated object names'
complete -f -c git -n '__fish_git_using_command show-ref' -s q -l quiet -d 'Do not print any results'
complete -f -c git -n '__fish_git_using_command show-ref' -l exclude-existing -d 'Check refs from stdin that do not exist'
### symbolic-ref
complete -f -c git -n __fish_git_needs_command -a symbolic-ref -d 'Read, modify, delete symbolic refs'
complete -f -c git -n '__fish_git_using_command symbolic-ref' -s d -l delete -d 'Delete the symbolic ref'
complete -f -c git -n '__fish_git_using_command symbolic-ref' -s q -l quiet -d 'Do not issue error if ref is not a symbolic ref'
complete -f -c git -n '__fish_git_using_command symbolic-ref' -l short -d 'Shorten the ref name'
complete -x -c git -n '__fish_git_using_command symbolic-ref' -s m -d 'Update reflog with given reason'
### check-ignore
complete -f -c git -n __fish_git_needs_command -a check-ignore -d 'Debug gitignore / exclude files'
complete -f -c git -n '__fish_git_using_command check-ignore' -s q -l quiet -d 'Do not output anything, just set exit status'
complete -f -c git -n '__fish_git_using_command check-ignore' -s v -l verbose -d 'Show matching pattern for each file'
complete -f -c git -n '__fish_git_using_command check-ignore' -l stdin -d 'Read pathnames from stdin'
complete -f -c git -n '__fish_git_using_command check-ignore' -s z -d 'NUL line termination'
complete -f -c git -n '__fish_git_using_command check-ignore' -s n -l non-matching -d 'Show given paths which do not match any pattern'
complete -f -c git -n '__fish_git_using_command check-ignore' -l no-index -d 'Do not look in the index when undertaking checks'
### checkout-index
complete -c git -n __fish_git_needs_command -a checkout-index -d 'Copy files from index to working tree'
complete -f -c git -n '__fish_git_using_command checkout-index' -s a -l all -d 'Check out all files in the index'
complete -f -c git -n '__fish_git_using_command checkout-index' -s f -l force -d 'Force overwrite existing files'
complete -f -c git -n '__fish_git_using_command checkout-index' -s n -l no-create -d 'Do not create files that do not exist'
complete -f -c git -n '__fish_git_using_command checkout-index' -s q -l quiet -d 'Be quiet if files exist or are not in the index'
complete -f -c git -n '__fish_git_using_command checkout-index' -s u -l index -d 'Update stat information in the index'
complete -f -c git -n '__fish_git_using_command checkout-index' -s z -d 'NUL line termination'
complete -x -c git -n '__fish_git_using_command checkout-index' -l prefix -d 'Prefix to use when creating files'
complete -x -c git -n '__fish_git_using_command checkout-index' -l stage -a 'all 1 2 3' -d 'Which stage to check out'
complete -f -c git -n '__fish_git_using_command checkout-index' -l temp -d 'Write files to temporary files'
### commit-tree
complete -f -c git -n __fish_git_needs_command -a commit-tree -d 'Create a new commit object'
complete -x -c git -n '__fish_git_using_command commit-tree' -s p -d 'Parent commit object'
complete -x -c git -n '__fish_git_using_command commit-tree' -s m -d 'Commit message'
complete -F -c git -n '__fish_git_using_command commit-tree' -s F -d 'Read commit message from file'
complete -x -c git -n '__fish_git_using_command commit-tree' -s S -l gpg-sign -d 'GPG-sign commit'
complete -f -c git -n '__fish_git_using_command commit-tree' -l no-gpg-sign -d 'Do not GPG-sign commit'
### diff-index
complete -f -c git -n __fish_git_needs_command -a diff-index -d 'Compare tree to working tree or index'
complete -f -c git -n '__fish_git_using_command diff-index' -l cached -d 'Compare tree to index'
complete -f -c git -n '__fish_git_using_command diff-index' -s m -d 'Ignore changes in submodules'
complete -f -c git -n '__fish_git_using_command diff-index' -l raw -d 'Generate raw diff output'
complete -f -c git -n '__fish_git_using_command diff-index' -s p -l patch -d 'Generate patch'
complete -f -c git -n '__fish_git_using_command diff-index' -s q -l quiet -d 'Disable diff output, only set exit status'
complete -f -c git -n '__fish_git_using_command diff-index' -a '(__fish_git_refs)' -d Ref
### hash-object
complete -f -c git -n __fish_git_needs_command -a hash-object -d 'Compute object ID and optionally create an object'
complete -x -c git -n '__fish_git_using_command hash-object' -s t -a 'blob tree commit tag' -d 'Specify object type'
complete -f -c git -n '__fish_git_using_command hash-object' -s w -d 'Actually write object into object database'
complete -f -c git -n '__fish_git_using_command hash-object' -l stdin -d 'Read object from stdin'
complete -f -c git -n '__fish_git_using_command hash-object' -l stdin-paths -d 'Read file paths from stdin'
complete -F -c git -n '__fish_git_using_command hash-object' -l path -d 'Hash object as if it were at the given path'
complete -f -c git -n '__fish_git_using_command hash-object' -l no-filters -d 'Hash contents as-is, without any input filters'
complete -f -c git -n '__fish_git_using_command hash-object' -l literally -d 'Allow hashing any garbage'
### read-tree
complete -f -c git -n __fish_git_needs_command -a read-tree -d 'Read tree info into the index'
complete -f -c git -n '__fish_git_using_command read-tree' -s m -d 'Perform a merge'
complete -f -c git -n '__fish_git_using_command read-tree' -l reset -d 'Same as -m, but discard unmerged entries'
complete -f -c git -n '__fish_git_using_command read-tree' -s u -d 'Update working tree with merge result'
complete -f -c git -n '__fish_git_using_command read-tree' -s i -d 'Update only the index, leave working tree alone'
complete -f -c git -n '__fish_git_using_command read-tree' -s n -l dry-run -d 'Do not update index or working tree'
complete -f -c git -n '__fish_git_using_command read-tree' -s v -d 'Show progress'
complete -f -c git -n '__fish_git_using_command read-tree' -l trivial -d 'Restrict three-way merge to trivial cases'
complete -f -c git -n '__fish_git_using_command read-tree' -l aggressive -d 'Try harder to resolve trivial cases'
complete -x -c git -n '__fish_git_using_command read-tree' -l prefix -d 'Read tree into subdirectory'
complete -f -c git -n '__fish_git_using_command read-tree' -l index-output -d 'Write index to specified file'
complete -f -c git -n '__fish_git_using_command read-tree' -l empty -d 'Instead of reading tree object, empty the index'
complete -f -c git -n '__fish_git_using_command read-tree' -l no-sparse-checkout -d 'Disable sparse checkout support'
complete -f -c git -n '__fish_git_using_command read-tree' -a '(__fish_git_refs)' -d Ref
### update-ref
complete -f -c git -n __fish_git_needs_command -a update-ref -d 'Update object name stored in a ref safely'
complete -f -c git -n '__fish_git_using_command update-ref' -s d -d 'Delete the reference'
complete -f -c git -n '__fish_git_using_command update-ref' -l no-deref -d 'Do not dereference symbolic refs'
complete -x -c git -n '__fish_git_using_command update-ref' -s m -d 'Update reflog with given reason'
complete -f -c git -n '__fish_git_using_command update-ref' -l create-reflog -d 'Create a reflog'
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-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'
### 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'
complete -x -c git -n '__fish_git_using_command write-tree' -l prefix -d 'Write a tree object for a subdirectory'
## Custom commands (git-* commands installed in the PATH)
complete -c git -n __fish_git_needs_command -a '(__fish_git_custom_commands)' -d 'Custom command'

View File

@@ -1,6 +1,6 @@
complete -c help -n __fish_is_first_arg -x -a '(
{
__fish_data_with_file help_sections (command -v cat) |
status get-file help_sections |
string replace -r "^index(#|\$)" introduction\$1
printf cmds/%s\n ! . : \[ \{
} |

View File

@@ -22,6 +22,8 @@ complete -c history -n '__fish_seen_subcommand_from search; or not __fish_seen_s
-s z -l null -d "Terminate entries with NUL character"
complete -c history -n '__fish_seen_subcommand_from search; or not __fish_seen_subcommand_from $__fish_history_all_commands' \
-s R -l reverse -d "Output the oldest results first" -x
complete -c history -n '__fish_seen_subcommand_from search; or not __fish_seen_subcommand_from $__fish_history_all_commands' \
-l color -d "When to colorize output" -xa "always never auto"
# We don't include a completion for the "save" subcommand because it should not be used
# interactively.

View File

@@ -21,8 +21,10 @@ function __fish_print_make_targets --argument-names directory file
is_continuation = $0 ~ "^([^#]*[^#" bs_regex "])?(" bs_regex bs_regex ")*" bs_regex "$";
}' 2>/dev/null
else
# BSD make
make $makeflags -d g1 -rn >/dev/null 2>| awk -F, '/^#\*\*\* Input graph:/,/^$/ {if ($1 !~ "^#... ") {gsub(/# /,"",$1); print $1}}' 2>/dev/null
# FreeBSD/NetBSD
make $makeflags -V .ALLTARGETS 2>/dev/null | string split ' '
# OpenBSD
or make $makeflags -d g1 -rn 2>/dev/null | awk '/^[^#. \t].+:/ { gsub(/:.*/, "", $1); print $1 }'
end
end

View File

@@ -1,4 +1,78 @@
complete -c signify -n __fish_seen_subcommand_from -s C -d 'Verify a signed checksum list'
complete -c signify -n __fish_seen_subcommand_from -s G -d 'Generate a new key pair'
complete -c signify -n __fish_seen_subcommand_from -s S -d 'Sign specified message'
complete -c signify -n __fish_seen_subcommand_from -s V -d 'Verify a signed message and sig'
# Tab completion for openbsd-signify
set -l subcommands -C -G -S -V
set -l subcommands_desc (echo -s \
-C\t"Verify a signed checksum list."\n \
-G\t"Generate a new key pair."\n \
-S\t"Sign the specified message file."\n \
-V\t"Verify the message and signature match."\n \
| string escape)
complete -c signify -f
complete -c signify \
-n "not __fish_seen_subcommand_from $subcommands" \
-a "$subcommands_desc"
complete -c signify -F \
-n '__fish_seen_subcommand_from -C'
complete -c signify -s c -f -r \
-d 'Specify the comment to be added during key generation' \
-n '__fish_seen_subcommand_from -G'
complete -c signify -s e -f \
-d 'Use embedded signatures' \
-n '__fish_seen_subcommand_from -S -V'
complete -c signify -s m -F -r \
-d 'Message file to sign or verify' \
-n '__fish_seen_subcommand_from -S -V'
# The -n option has two context-dependent meanings
complete -c signify -s n -f \
-d 'When generating a key pair, do not ask for a passphrase' \
-n '__fish_seen_argument -s G'
complete -c signify -s n -f \
-d 'When signing with -z, store a zero timestamp in the gzip header' \
-n '__fish_seen_subcommand_from -S && __fish_seen_argument -s z'
# This is deliberately split up to only add a description to the flag and not all its argument completions
complete -c signify -s p -f -k -r \
-a '(__fish_complete_suffix .pub)' \
-n '__fish_seen_subcommand_from -C -G -V'
complete -c signify -s p -f -k -r \
-d 'Public key produced by -G, and used by -V to check a signature' \
-n '__fish_seen_subcommand_from -C -G -V'
complete -c signify -s q -f \
-d 'Quiet mode. Suppress informational output' \
-n '__fish_seen_subcommand_from -C -V'
complete -c signify -s s -f -k -r \
-a '(__fish_complete_suffix .sec)' \
-n '__fish_seen_subcommand_from -G -S'
complete -c signify -s s -f -k -r \
-d 'Secret (private) key produced by -G, and used by -S to sign a message' \
-n '__fish_seen_subcommand_from -G -S'
complete -c signify -s t -f -r \
-a '(
set -l files /etc/signify/*
string replace -rf -- \'\\.pub$\' "" $files | string replace -r \'.*-\' ""
)' \
-n '__fish_seen_subcommand_from -C -V'
complete -c signify -s t -f -r \
-d 'When inferring a key to verify with, only use keys with this keytype suffix' \
-n '__fish_seen_subcommand_from -C -V'
complete -c signify -s x -f -k -r \
-a '(__fish_complete_suffix .sig)' \
-n '__fish_seen_subcommand_from -C -S -V'
complete -c signify -s x -f -k -r \
-d 'The signature file to create or verify. The default is message.sig' \
-n '__fish_seen_subcommand_from -C -S -V'
complete -c signify -s z -f \
-d 'Sign and verify gzip (1) archives, embed signature in the header' \
-n '__fish_seen_subcommand_from -S -V'

View File

@@ -6,6 +6,7 @@ complete -c type -s p -l path -d "Print path to command, or nothing if name is n
complete -c type -s P -l force-path -d "Print path to command"
complete -c type -s q -l query -l quiet -d "Check if something exists without output"
complete -c type -s s -l short -d "Don't print function definition"
complete -c type -l color -d "When to colorize output" -xa "always never auto"
complete -c type -a "(builtin -n)" -d Builtin
complete -c type -a "(functions -n)" -d Function

View File

@@ -32,7 +32,7 @@ end
set -l __extra_completionsdir
set -l __extra_functionsdir
set -l __extra_confdir
__fish_data_with_file __fish_build_paths.fish source
status get-file __fish_build_paths.fish | source
# Compute the directories for vendor configuration. We want to include
# all of XDG_DATA_DIRS, as well as the __extra_* dirs defined above.
@@ -205,7 +205,7 @@ if command -q kill
end
if status is-interactive
__fish_theme_migrate
__fish_migrate
end
fish_config theme choose default --no-override

View File

@@ -14,7 +14,7 @@ function __fish_complete_directories -d "Complete directory prefixes" --argument
# If we have a --name=val option, we need to remove it,
# or the complete -C below would keep it, and then whatever complete
# called us would add it again (assuming it's in the current token)
set comp (commandline -ct | string replace -r -- '^-[^=]*=' '' $comp)
set comp (commandline -ct | string replace -r -- '^-[^=]*=' '')
end
# HACK: We call into the file completions by using an empty command

View File

@@ -6,15 +6,6 @@
#
function __fish_config_interactive -d "Initializations that should be performed when entering interactive mode"
functions -e __fish_config_interactive
# Create empty configuration directores if they do not already exist
test -e $__fish_config_dir/completions/ -a -e $__fish_config_dir/conf.d/ -a -e $__fish_config_dir/functions/ ||
mkdir -p $__fish_config_dir/{completions, conf.d, functions}
# Create config.fish with some boilerplate if it does not exist
test -e $__fish_config_dir/config.fish || echo "\
if status is-interactive
# Commands to run in interactive sessions can go here
end" >$__fish_config_dir/config.fish
set -g __fish_active_key_bindings

View File

@@ -1,5 +1,5 @@
# localization: skip(private)
function __fish_data_with_file
function __fish_config_with_file
set -l path $argv[1]
set -l cmd $argv[2..]
if string match -rq -- ^/ $path

View File

@@ -1,9 +1,27 @@
# localization: skip(private)
function __fish_theme_migrate
functions -e __fish_theme_migrate
function __fish_migrate
functions -e __fish_migrate
set -l migration_version 4300
# Maybe migrate.
if not set -q __fish_initialized || test $__fish_initialized -ge 4300
if set -q __fish_initialized && test $__fish_initialized -ge $migration_version
return
end
# Create empty configuration directories if they do not already exist
test -e $__fish_config_dir/completions/ -a -e $__fish_config_dir/conf.d/ -a -e $__fish_config_dir/functions/ ||
mkdir -p $__fish_config_dir/{completions, conf.d, functions}
# Create config.fish with some boilerplate if it does not exist
test -e $__fish_config_dir/config.fish || echo "\
if status is-interactive
# Commands to run in interactive sessions can go here
end" >$__fish_config_dir/config.fish
set -l mark_migration_done set -U __fish_initialized $migration_version
if not set -q __fish_initialized
$mark_migration_done
return
end
@@ -22,7 +40,7 @@ function __fish_theme_migrate
for varname in $theme_uvars
set -a theme_data "$(string escape -- $varname $$varname | string join " ")"
end
__fish_theme_freeze __fish_theme_migrate $theme_data
__fish_theme_freeze __fish_migrate $theme_data
set msg_suffix " by default."\n" Migrated them to global variables set in $(set_color --underline)$(
__fish_unexpand_tilde $__fish_config_dir/conf.d/fish_frozen_theme.fish
)$(set_color normal)"
@@ -35,6 +53,7 @@ function __fish_theme_migrate
set -l relative_filename conf.d/fish_frozen_key_bindings.fish
set -l filename $__fish_config_dir/$relative_filename
__fish_backup_config_files $relative_filename
mkdir -p -- (path dirname -- $filename)
echo >$filename "\
# This file was created by fish when upgrading to version 4.3, to migrate
# the 'fish_key_bindings' variable from its old default scope (universal)
@@ -62,14 +81,14 @@ set --erase --universal fish_key_bindings"
(set_color normal))
source $__fish_config_dir/$relative_filename
end
set -U __fish_initialized 4300
$mark_migration_done
if $removing_uvars
echo -s (set_color --bold) 'fish:' (set_color normal) " upgraded to version 4.3:"
string join \n -- $msg
echo 'See also the release notes (type `help relnotes`).'
set -Ue fish_key_bindings $theme_uvars
set -l sh (__fish_posix_shell)
eval "$sh -c 'sleep 7 # Please read above notice about universal variables' &"
eval "$sh -c 'sleep 7 # Please read above notice about universal variables' </dev/null &>/dev/null &"
end
end

View File

@@ -6,15 +6,16 @@ function __fish_theme_cat -a theme_name
echo >&2 Searched (__fish_theme_dir) "and `status list-files themes`"
return 1
end
set -l theme_data (__fish_data_with_file $theme_path cat)
set -l theme_data (if string match -q '/*' -- $theme_path; cat $theme_path; else status get-file $theme_path; end)
set -l allowed_lines \
'\s*' \
'\s*#.*' \
'\[(dark|light|unknown)\]' \
(__fish_theme_variable_filter)
set allowed_lines "^($(string join -- '|' $allowed_lines))\$"
for line in $theme_data
string match -rq -- $allowed_lines $line
printf '%s\n' $theme_data | string match -rvq -- $allowed_lines
and for line in $theme_data
string match -rq -- $allowed_lines $theme_data
or printf >&2 "error: unsupported line in theme '%s': %s\n" $theme_name $line
end
string join \n $theme_data

View File

@@ -6,11 +6,12 @@ function __fish_theme_freeze
__fish_backup_config_files $relative_path
set -l help_section interactive#syntax-highlighting
__fish_data_with_file help_sections $(command -v grep) -Fxq $help_section
status get-file help_sections | string match -q $help_section
or echo "fish: internal error: missing help section '$help_section'"
mkdir -p -- (path dirname -- $__fish_config_dir/conf.d)
printf >$__fish_config_dir/$relative_path %s\n \
$(test $data_source = __fish_theme_migrate &&
$(test $data_source = __fish_migrate &&
echo "\
# This file was created by fish when upgrading to version 4.3, to migrate
# theme variables from universal to global scope.") \
@@ -21,7 +22,7 @@ function __fish_theme_freeze
# or
# man fish-interactive | less +/^SYNTAX.HIGHLIGHTING
# for appropriate commands to add to ~/.config/fish/config.fish instead." \
$(test $data_source = __fish_theme_migrate &&
$(test $data_source = __fish_migrate &&
echo '# See also the release notes for fish 4.3.0 (run `help relnotes`).') \
"" \
'set --global '$theme_data

View File

@@ -91,7 +91,7 @@ function fish_config --description "Launch fish's web based configuration"
echo -s (set_color --underline) $promptname (set_color normal)
$fish -c '
functions -e fish_right_prompt
__fish_data_with_file $argv[1] source
__fish_config_with_file $argv[1] source
false
fish_prompt
echo (set_color normal)
@@ -120,9 +120,9 @@ function fish_config --description "Launch fish's web based configuration"
return 1
end
__fish_config_prompt_reset
__fish_data_with_file $prompt_path source
__fish_config_with_file $prompt_path source
if not functions -q fish_mode_prompt
__fish_data_with_file functions/fish_mode_prompt.fish source
status get-file functions/fish_mode_prompt.fish | source
end
case save
if begin
@@ -142,7 +142,7 @@ function fish_config --description "Launch fish's web based configuration"
return 1
end
__fish_config_prompt_reset
__fish_data_with_file $prompt_path source
__fish_config_with_file $prompt_path source
end
funcsave fish_prompt
@@ -156,7 +156,7 @@ function fish_config --description "Launch fish's web based configuration"
end
end
if not functions -q fish_mode_prompt
__fish_data_with_file functions/fish_mode_prompt.fish source
status get-file functions/fish_mode_prompt.fish | source
end
return
end
@@ -287,7 +287,7 @@ function __fish_config_theme_choose
set -l color_theme
__fish_config_theme_canonicalize
set -l theme_data (type -q cat && __fish_theme_cat $theme_name)
set -l theme_data (__fish_theme_cat $theme_name)
or return
set -l color_themes dark light unknown
set -l theme_is_color_theme_aware false
@@ -321,10 +321,11 @@ function __fish_config_theme_choose
else
set desired_color_theme $fish_terminal_color_theme
if not set -q desired_color_theme[1]
echo >&2 "fish_config theme choose: internal error: \$fish_terminal_color_theme not yet initialized"
return 1
end
if not contains -- "[$desired_color_theme]" $theme_data
if test $scope = -U
echo >&2 "fish_config theme save: error: \$fish_terminal_color_theme not yet initialized"
return 1
end
else if not contains -- "[$desired_color_theme]" $theme_data
__fish_config_theme_choose_bad_color_theme $theme_name "$desired_color_theme" \$fish_terminal_color_theme = $desired_color_theme
echo >&2 "fish_config theme choose: hint: if your terminal does not report colors, pass --color-theme=light or --color-theme=dark when using color-theme-aware themes"
return 1
@@ -340,9 +341,9 @@ function __fish_config_theme_choose
end
set -l color_theme
string join \n -- $theme_data |
string match -re -- (__fish_theme_variable_filter)'|^\[.*\]$' $theme_data |
while read -lat toks
if $theme_is_color_theme_aware
if $theme_is_color_theme_aware && set -q desired_color_theme[1]
for ct in $color_themes
if test "$toks" = [$ct]
set color_theme $ct
@@ -354,15 +355,19 @@ function __fish_config_theme_choose
end
end
set -l varname $toks[1]
string match -rq -- (__fish_theme_variable_filter) "$varname"
or continue
string match -q '[*' -- $varname
and continue
# If we're supposed to set universally, remove any shadowing globals
# so the change takes effect immediately (and there's no warning).
if test $scope = -U; and set -qg $varname
set -eg $varname
end
if $override || not set -q $varname || string match -rq -- '--theme=.*' $$varname
set $scope $toks (test $scope != -U && echo --theme=$theme_name)
set $scope $toks[1] (
if not $theme_is_color_theme_aware || set -q desired_color_theme[1]
string join \n -- $toks[2..]
end
) (test $scope != -U && echo --theme=$theme_name)
end
end
if $override
@@ -376,31 +381,38 @@ function __fish_config_theme_choose
end
end
end
if test -n "$fish_terminal_color_theme" || not $need_hook
if set -q _flag_no_override[1]
__fish_apply_theme
else
__fish_override=true __fish_apply_theme
if not $need_hook || test -n "$fish_terminal_color_theme" ||
# comment to work around fish_indent bug
{
$theme_is_color_theme_aware && test -z "$fish_terminal_color_theme"
}
if not set -q _flag_no_override[1]
set -fx __fish_override true
end
__fish_apply_theme
end
end
function __fish_config_theme_canonicalize --no-scope-shadowing
# theme_name
# color_theme
if not path is (__fish_theme_dir)/$theme_name.theme
switch $theme_name
case 'fish default'
set theme_name default
case 'ayu Dark' 'ayu Light' 'ayu Mirage' \
'Base16 Default Dark' 'Base16 Default Light' 'Base16 Eighties' \
'Bay Cruise' Dracula Fairground 'Just a Touch' Lava \
'Mono Lace' 'Mono Smoke' \
None Nord 'Old School' Seaweed 'Snow Day' \
'Solarized Dark' 'Solarized Light' \
'Tomorrow Night Bright' 'Tomorrow Night' Tomorrow
set theme_name (string lower (string replace -a " " "-" $theme_name))
end
if path is (__fish_theme_dir)/$theme_name.theme
return
end
switch $theme_name
case 'fish default'
set theme_name default
case 'ayu Dark' 'ayu Light' 'ayu Mirage' \
'Base16 Default Dark' 'Base16 Default Light' 'Base16 Eighties' \
'Bay Cruise' Dracula Fairground 'Just a Touch' Lava \
'Mono Lace' 'Mono Smoke' \
None Nord 'Old School' Seaweed 'Snow Day' \
'Solarized Dark' 'Solarized Light' \
'Tomorrow Night Bright' 'Tomorrow Night' Tomorrow
if test $theme_name = Tomorrow
set color_theme light
end
set theme_name (string lower (string replace -a " " "-" $theme_name))
end
switch $theme_name
case \
@@ -408,8 +420,6 @@ function __fish_config_theme_canonicalize --no-scope-shadowing
base16-default-dark base16-default-light \
solarized-dark solarized-light
string match -rq -- '^(?<theme_name>.*)-(?<color_theme>dark|light)$' $theme_name
case tomorrow
set color_theme light
case tomorrow-night
set theme_name tomorrow
set color_theme dark

View File

@@ -131,21 +131,15 @@ function fish_delta
printf (_ "%sUnmodified%s: %s\n") $colors[4] $colors[1] $file
end
end
function __fish_delta_diff_maybe_file -a maybe_default_file
# TODO Use "set -l foo (cat)" instead of the temp file.
# https://github.com/fish-shell/fish-shell/issues/206
if $default_exists
set -l tmpfile (__fish_mktemp_relative fish-delta)
cat $maybe_default_file >$tmpfile
status get-file $dir/$bn >$tmpfile
__fish_delta_diff $tmpfile
command rm $tmpfile
end
if $default_exists
__fish_data_with_file $dir/$bn __fish_delta_diff_maybe_file
else
__fish_delta_diff /dev/null
end
functions --erase __fish_delta_diff
functions --erase __fish_delta_diff_maybe_file
else
# Without diff, we can't really tell if the contents are the same.
printf (_ "%sPossibly changed%s: %s\n") $colors[3] $colors[1] $file

View File

@@ -20,7 +20,7 @@ function fish_update_completions --description "Update man-page based completion
--cleanup-in $__fish_user_data_dir/generated_completions \
--cleanup-in $__fish_cache_dir/generated_completions
__fish_data_with_file tools/create_manpage_completions.py cat |
status get-file tools/create_manpage_completions.py |
if $detach
# Run python directly in the background and swallow all output
# Orphan the job so that it continues to run in case of an early exit (#6269)

View File

@@ -140,7 +140,7 @@ chromium-browser
switch "$fish_help_item"
case ''
set fish_help_page index.html
case (__fish_data_with_file help_sections (command -v cat) | string replace -r "^index(#|\$)" introduction\$1)
case (status get-file help_sections | string replace -r "^index(#|\$)" introduction\$1)
set fish_help_page (
printf %s $fish_help_item |
string replace -r '^introduction(#|$)' 'index$1' |

View File

@@ -6,7 +6,7 @@ function history --description "display or manipulate interactive command histor
set -l cmd history
set -l options --exclusive 'c,e,p' --exclusive 'S,D,M,V,X'
set -a options h/help c/contains e/exact p/prefix
set -a options C/case-sensitive R/reverse z/null 't/show-time=?' 'n#max'
set -a options C/case-sensitive R/reverse z/null 't/show-time=?' 'n#max' 'color='
# The following options are deprecated and will be removed in the next major release.
# Note that they do not have usable short flags.
set -a options S-search D-delete M-merge V-save X-clear
@@ -22,9 +22,12 @@ function history --description "display or manipulate interactive command histor
set -l show_time
set -l max_count
set -l search_mode
set -l color_opt
set -q _flag_max
set max_count -n$_flag_max
set color_opt --color=$_flag_color
set -q _flag_with_time
and set -l _flag_show_time $_flag_with_time
if set -q _flag_show_time[1]
@@ -78,7 +81,7 @@ function history --description "display or manipulate interactive command histor
# If the user hasn't preconfigured less with the $LESS environment variable,
# we do so to have it behave like cat if output fits on one screen.
if not set -qx LESS
set -fx LESS --quit-if-one-screen
set -fx LESS --quit-if-one-screen --RAW-CONTROL-CHARS
# Also set --no-init for less < v530, see #8157.
if type -q less; and test (less --version | string match -r 'less (\d+)')[2] -lt 530 2>/dev/null
set LESS $LESS --no-init
@@ -87,9 +90,15 @@ function history --description "display or manipulate interactive command histor
not set -qx LV # ask the pager lv not to strip colors
and set -fx LV -c
builtin history search $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv | $pager
if contains -- "$color_opt" '' '--color=auto'
and test "$pager" = less
and string match -rq -- '^(-\w*R|--RAW-CONTROL-CHARS$)' $LESS
set color_opt --color=always
end
builtin history search $color_opt $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv | $pager
else
builtin history search $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
builtin history search $color_opt $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
end
case delete # interactively delete history
@@ -100,15 +109,15 @@ function history --description "display or manipulate interactive command histor
end
if test "$search_mode" = --exact
builtin history delete $search_mode $_flag_case_sensitive -- $searchterm
builtin history delete $color_opt $search_mode $_flag_case_sensitive -- $searchterm
builtin history save
return
end
# TODO: Fix this so that requesting history entries with a timestamp works:
# set -l found_items (builtin history search $search_mode $show_time -- $argv)
# set -l found_items (builtin history search $color_opt $search_mode $show_time -- $argv)
set -l found_items
set found_items (builtin history search $search_mode $_flag_case_sensitive --null -- $searchterm | string split0)
set found_items (builtin history search $color_opt $search_mode $_flag_case_sensitive --null -- $searchterm | string split0)
if set -q found_items[1]
set -l found_items_count (count $found_items)
for i in (seq $found_items_count)
@@ -132,7 +141,7 @@ function history --description "display or manipulate interactive command histor
if test "$choice" = all
printf "Deleting all matching entries!\n"
for item in $found_items
builtin history delete --exact --case-sensitive -- $item
builtin history delete $color_opt --exact --case-sensitive -- $item
end
builtin history save
return
@@ -173,15 +182,15 @@ function history --description "display or manipulate interactive command histor
echo Deleting choices: $choices
for x in $choices
printf "Deleting history entry %s: \"%s\"\n" $x $found_items[$x]
builtin history delete --exact --case-sensitive -- "$found_items[$x]"
builtin history delete $color_opt --exact --case-sensitive -- "$found_items[$x]"
end
builtin history save
end
case save # save our interactive command history to the persistent history
builtin history save $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
builtin history save $color_opt $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
case merge # merge the persistent interactive command history with our history
builtin history merge $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
builtin history merge $color_opt $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
case clear # clear the interactive command history
if test -n "$search_mode"
or set -q show_time[1]
@@ -197,13 +206,13 @@ function history --description "display or manipulate interactive command histor
read --local --prompt "echo 'Are you sure you want to clear history? (yes/no) '" choice
or return $status
if test "$choice" = yes
builtin history clear $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
builtin history clear $color_opt $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
and printf (_ "Command history cleared!\n")
else
printf (_ "You did not say 'yes' so I will not clear your command history\n")
end
case clear-session # clears only session
builtin history clear-session $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
builtin history clear-session $color_opt $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $argv
and printf (_ "Command history for session cleared!\n")
case append
set -l newitem $argv
@@ -212,7 +221,7 @@ function history --description "display or manipulate interactive command histor
or return $status
end
builtin history append $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $newitem
builtin history append $color_opt $search_mode $show_time $max_count $_flag_case_sensitive $_flag_reverse $_flag_null -- $newitem
case '*'
printf "%s: unexpected subcommand '%s'\n" $cmd $hist_cmd
return 2

View File

@@ -5,7 +5,15 @@ body {
color: #222;
}
.prompt_demo, .prompt_demo_text, .data_table_row, .colorpicker_text_sample_tight, .colorpicker_text_sample, .history_text, pre, code, tt {
.prompt_demo,
.prompt_demo_text,
.data_table_row,
.colorpicker_text_sample_tight,
.colorpicker_text_sample,
.history_text,
pre,
code,
tt {
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, "Ubuntu Mono", "Hack", "Noto Sans Mono", Liberation Mono, monospace;
}
@@ -48,14 +56,14 @@ body {
.tab:hover,
#tab_contents .master_element:hover,
.color_scheme_choice_container:hover,
.prompt_choices_list > .ng-scope:hover
{
.prompt_choices_list > .ng-scope:hover {
background-color: #DDE;
}
#tab_parent{
#tab_parent {
border: red 1px;
}
#tab_parent :first-child {
border-top-left-radius: 8px;
border-left: none;
@@ -65,7 +73,8 @@ body {
border-top-right-radius: 8px;
}
.selected_tab, .selected_tab:hover {
.selected_tab,
.selected_tab:hover {
background-color: #eeeefa;
border-bottom: none;
}
@@ -139,30 +148,39 @@ body {
.detail_function .fish_color_autosuggestion {
color: #555;
}
.detail_function .fish_color_command {
color: #005fd7;
}
.detail_function .fish_color_param {
color: #00afff;
}
.detail_function .fish_color_redirection {
color: #00afff;
}
.detail_function .fish_color_comment {
color: #990000;
}
.detail_function .fish_color_error {
color: #ff0000;
}
.detail_function .fish_color_escape {
color: #00a6b2;
}
.detail_function .fish_color_operator {
color: #00a6b2;
}
.detail_function .fish_color_quote {
color: #999900;
}
.detail_function .fish_color_statement_terminator {
color: #009900;
}
@@ -221,6 +239,7 @@ body {
.master_element > br {
display: none;
}
.selected_master_elem > br {
display: inherit;
}
@@ -267,8 +286,7 @@ body {
padding-right: 10px;
}
.data_table_row {
}
.data_table_row {}
.data_table_cell {
padding-top: 5px;
@@ -625,6 +643,7 @@ button.delete_button:hover {
#parent {
margin-top: 0;
}
#tab_contents {
margin-bottom: 0;
}
@@ -632,27 +651,29 @@ button.delete_button:hover {
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(to top, #1f1f3f 0%,#051f3a 100%);
background: linear-gradient(to top, #1f1f3f 0%, #051f3a 100%);
color: #DDD;
}
#ancestor {
box-shadow: 0 0 5px 1px #000;
}
.tab {
background-color: black;
border: 1px solid #222;
border-top: none;
}
.tab:hover,
#tab_contents .master_element:hover,
.color_scheme_choice_container:hover,
.prompt_choices_list > .ng-scope:hover
{
.prompt_choices_list > .ng-scope:hover {
background-color: #223;
}
.selected_tab, .selected_tab:hover {
.selected_tab,
.selected_tab:hover {
background-color: #202028;
border-bottom: none;
}
@@ -660,15 +681,63 @@ button.delete_button:hover {
#tab_contents {
background-color: #202028;
}
.detail, .selected_master_elem {
.detail,
.selected_master_elem {
background-color: black;
}
input {
background-color: #222;
color: #AAA;
}
/* Fix .function-body background in dark mode */
.function-body {
background-color: black;
}
.detail_function .fish_color_autosuggestion {
color: #808080; /* brblack */
}
.detail_function .fish_color_command {
color: #c397d8;
}
.detail_function .fish_color_param {
color: #7aa6da;
}
.detail_function .fish_color_redirection {
color: #70c0b1;
}
.detail_function .fish_color_comment {
color: #e7c547;
}
.detail_function .fish_color_error {
color: #d54e53;
}
.detail_function .fish_color_escape {
color: #00a6b2;
}
.detail_function .fish_color_operator {
color: #00a6b2;
}
.detail_function .fish_color_quote {
color: #b9ca4a;
}
.detail_function .fish_color_statement_terminator {
color: #c397d8;
}
}
.print_only {
display: none;
}
}

View File

@@ -3,7 +3,8 @@ body {
font-size: 8pt;
}
.tab, .print_hidden {
.tab,
.print_hidden {
display: none;
}
@@ -27,4 +28,4 @@ body {
#ancestor {
width: 100%;
box-shadow: none;
}
}

View File

@@ -1,15 +1,14 @@
use std::{
collections::HashSet,
sync::{Mutex, MutexGuard},
sync::{LazyLock, Mutex, MutexGuard},
};
use crate::prelude::*;
use once_cell::sync::Lazy;
use crate::parse_constants::SourceRange;
use pcre2::utf32::Regex;
static ABBRS: Lazy<Mutex<AbbreviationSet>> = Lazy::new(|| Mutex::new(Default::default()));
static ABBRS: LazyLock<Mutex<AbbreviationSet>> = LazyLock::new(|| Mutex::new(Default::default()));
pub fn with_abbrs<R>(cb: impl FnOnce(&AbbreviationSet) -> R) -> R {
let abbrs_g = ABBRS.lock().unwrap();

View File

@@ -47,7 +47,7 @@
parse_constants::{ParseErrorList, ParseTreeFlags},
parse_tree::ParsedSource,
parse_util::parse_util_detect_errors_in_ast,
parser::{BlockType, CancelBehavior, Parser},
parser::{BlockType, CancelBehavior, Parser, ParserEnvSetMode},
path::path_get_config,
prelude::*,
printf,
@@ -385,7 +385,8 @@ fn throwing_main() -> i32 {
set_libc_locales(/*log_ok=*/ false)
};
fish::localization::initialize_gettext();
#[cfg(feature = "localize-messages")]
fish::localization::initialize_localization();
// Enable debug categories set in FISH_DEBUG.
// This is in *addition* to the ones given via --debug.
@@ -498,9 +499,9 @@ fn throwing_main() -> i32 {
if is_interactive_session() && opts.no_config && !opts.no_exec {
// If we have no config, we default to the default key bindings.
parser.vars().set_one(
parser.set_one(
L!("fish_key_bindings"),
EnvMode::UNEXPORT,
ParserEnvSetMode::new(EnvMode::UNEXPORT),
L!("fish_default_key_bindings").to_owned(),
);
if function::exists(L!("fish_default_key_bindings"), parser) {
@@ -545,9 +546,9 @@ fn throwing_main() -> i32 {
// Pass additional args as $argv.
// Note that we *don't* support setting argv[0]/$0, unlike e.g. bash.
let list = &args[my_optind..];
parser.vars().set(
parser.set_var(
L!("argv"),
EnvMode::default(),
ParserEnvSetMode::default(),
list.iter().map(|s| s.to_owned()).collect(),
);
res = run_command_list(parser, &opts.batch_cmds);
@@ -580,9 +581,9 @@ fn throwing_main() -> i32 {
}
Ok(f) => {
let list = &args[my_optind..];
parser.vars().set(
parser.set_var(
L!("argv"),
EnvMode::default(),
ParserEnvSetMode::default(),
list.iter().map(|s| s.to_owned()).collect(),
);
let rel_filename = &args[my_optind - 1];

View File

@@ -1,7 +1,9 @@
use super::prelude::*;
use crate::abbrs::{self, Abbreviation, Position};
use crate::common::{EscapeStringStyle, escape, escape_string, valid_func_name};
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 pcre2::utf32::{Regex, RegexBuilder};
@@ -21,6 +23,7 @@ struct Options {
position: Option<Position>,
set_cursor_marker: Option<WString>,
args: Vec<WString>,
color: ColorEnabled,
}
impl Options {
@@ -123,7 +126,7 @@ fn join(list: &[&wstr], sep: &wstr) -> WString {
}
// Print abbreviations in a fish-script friendly way.
fn abbr_show(streams: &mut IoStreams) -> BuiltinResult {
fn abbr_show(opts: &Options, streams: &mut IoStreams, parser: &Parser) -> BuiltinResult {
let style = EscapeStringStyle::Script(Default::default());
abbrs::with_abbrs(|abbrs| {
@@ -172,7 +175,15 @@ fn abbr_show(streams: &mut IoStreams) -> BuiltinResult {
));
}
result.push('\n');
streams.out.append(&result);
if opts.color.enabled(streams) {
streams.out.append(&bytes2wcstring(&highlight_and_colorize(
&result,
&parser.context(),
parser.vars(),
)));
} else {
streams.out.append(&result);
}
}
});
@@ -460,7 +471,8 @@ fn abbr_erase(opts: &Options, parser: &Parser) -> BuiltinResult {
let esc_src = escape(arg);
if !esc_src.is_empty() {
let var_name = WString::from_str("_fish_abbr_") + esc_src.as_utfstr();
let ret = parser.vars().remove(&var_name, EnvMode::UNIVERSAL);
let ret =
parser.remove_var(&var_name, ParserEnvSetMode::new(EnvMode::UNIVERSAL));
if ret == EnvStackSetResult::Ok {
result = Ok(SUCCESS)
@@ -506,6 +518,7 @@ pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
wopt(L!("global"), ArgType::NoArgument, 'g'),
wopt(L!("universal"), ArgType::NoArgument, 'U'),
wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("color"), ArgType::RequiredArgument, COLOR_OPTION_CHAR),
];
let mut opts = Options::default();
@@ -612,6 +625,9 @@ pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
builtin_unknown_option(parser, streams, cmd, argv[w.wopt_index - 1], false);
return Err(STATUS_INVALID_ARGS);
}
COLOR_OPTION_CHAR => {
opts.color = ColorEnabled::parse_from_opt(streams, cmd, w.woptarg.unwrap())?;
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
@@ -630,7 +646,7 @@ pub fn abbr(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
return abbr_add(&opts, streams);
};
if opts.show {
return abbr_show(streams);
return abbr_show(&opts, streams, parser);
};
if opts.list {
return abbr_list(&opts, streams);

View File

@@ -2,8 +2,9 @@
use super::prelude::*;
use crate::env::{EnvMode, EnvStack};
use crate::env::{EnvMode, EnvSetMode, EnvStack};
use crate::exec::exec_subshell;
use crate::parser::ParserEnvSetMode;
use crate::wutil::fish_iswalnum;
const VAR_NAME_PREFIX: &wstr = L!("_flag_");
@@ -195,7 +196,7 @@ fn parse_exclusive_args(opts: &mut ArgParseCmdOpts, streams: &mut IoStreams) ->
}
// Store the set of exclusive flags for use when parsing the supplied set of arguments.
opts.exclusive_flag_sets.push(exclusive_set.to_vec());
opts.exclusive_flag_sets.push(exclusive_set.clone());
}
Ok(SUCCESS)
}
@@ -699,24 +700,31 @@ fn validate_arg<'opts>(
return Ok(SUCCESS);
}
let vars = parser.vars();
vars.push(true /* new_scope */);
parser.vars().push(true /* new_scope */);
let env_mode = EnvMode::LOCAL | EnvMode::EXPORT;
vars.set_one(L!("_argparse_cmd"), env_mode, opts_name.to_owned());
let local_exported_mode = ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT);
parser.set_one(
L!("_argparse_cmd"),
local_exported_mode,
opts_name.to_owned(),
);
let flag_name = WString::from(VAR_NAME_PREFIX) + "name";
if is_long_flag {
vars.set_one(&flag_name, env_mode, opt_spec.long_flag.to_owned());
} else {
vars.set_one(
parser.set_one(
&flag_name,
env_mode,
local_exported_mode,
opt_spec.long_flag.to_owned(),
);
} else {
parser.set_one(
&flag_name,
local_exported_mode,
WString::from_chars(vec![opt_spec.short_flag]),
);
}
vars.set_one(
parser.set_one(
&(WString::from(VAR_NAME_PREFIX) + "value"),
env_mode,
local_exported_mode,
woptarg.to_owned(),
);
@@ -733,7 +741,7 @@ fn validate_arg<'opts>(
streams.err.append(&output);
streams.err.append_char('\n');
}
vars.pop();
parser.vars().pop(parser.is_repainting());
retval.map(|()| SUCCESS)
}
@@ -1138,7 +1146,7 @@ fn check_min_max_args_constraints(
}
/// Put the result of parsing the supplied args into the caller environment as local vars.
fn set_argparse_result_vars(vars: &EnvStack, opts: ArgParseCmdOpts) {
fn set_argparse_result_vars(vars: &EnvStack, local_mode: EnvSetMode, opts: ArgParseCmdOpts) {
for opt_spec in opts.options.values() {
if opt_spec.num_seen == 0 {
continue;
@@ -1147,7 +1155,7 @@ fn set_argparse_result_vars(vars: &EnvStack, opts: ArgParseCmdOpts) {
if opt_spec.short_flag_valid {
let mut var_name = WString::from(VAR_NAME_PREFIX);
var_name.push(opt_spec.short_flag);
vars.set(&var_name, EnvMode::LOCAL, opt_spec.vals.clone());
vars.set(&var_name, local_mode, opt_spec.vals.clone());
}
if !opt_spec.long_flag.is_empty() {
@@ -1158,14 +1166,14 @@ fn set_argparse_result_vars(vars: &EnvStack, opts: ArgParseCmdOpts) {
.chars()
.map(|c| if fish_iswalnum(c) { c } else { '_' });
let var_name_long: WString = VAR_NAME_PREFIX.chars().chain(long_flag).collect();
vars.set(&var_name_long, EnvMode::LOCAL, opt_spec.vals.clone());
vars.set(&var_name_long, local_mode, opt_spec.vals.clone());
}
}
let args = opts.args.into_iter().map(|s| s.into_owned()).collect();
vars.set(L!("argv"), EnvMode::LOCAL, args);
vars.set(L!("argv"), local_mode, args);
let args_opts = opts.args_opts.into_iter().map(|s| s.into_owned()).collect();
vars.set(L!("argv_opts"), EnvMode::LOCAL, args_opts);
vars.set(L!("argv_opts"), local_mode, args_opts);
}
/// The argparse builtin. This is explicitly not compatible with the BSD or GNU version of this
@@ -1213,7 +1221,11 @@ pub fn argparse(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
check_min_max_args_constraints(&opts, streams)?;
set_argparse_result_vars(parser.vars(), opts);
set_argparse_result_vars(
parser.vars(),
parser.convert_env_set_mode(ParserEnvSetMode::new(EnvMode::LOCAL)),
opts,
);
Ok(SUCCESS)
}

View File

@@ -4,12 +4,11 @@
use crate::common::{
EscapeFlags, EscapeStringStyle, bytes2wcstring, escape, escape_string, valid_var_name,
};
use crate::highlight::{colorize, highlight_shell};
use crate::highlight::highlight_and_colorize;
use crate::input::{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 crate::nix::isatty;
use std::sync::MutexGuard;
const DEFAULT_BIND_MODE: &wstr = L!("default");
@@ -32,6 +31,7 @@ struct Options {
mode: c_int,
bind_mode: WString,
sets_bind_mode: Option<WString>,
color: ColorEnabled,
}
impl Options {
@@ -49,6 +49,7 @@ fn new() -> Options {
mode: BIND_INSERT,
bind_mode: DEFAULT_BIND_MODE.to_owned(),
sets_bind_mode: None,
color: ColorEnabled::default(),
}
}
}
@@ -153,11 +154,12 @@ fn list_one(
}
out.push('\n');
if !streams.out_is_redirected && isatty(libc::STDOUT_FILENO) {
let mut colors = Vec::new();
highlight_shell(&out, &mut colors, &parser.context(), false, None);
let colored = colorize(&out, &colors, parser.vars());
streams.out.append(&bytes2wcstring(&colored));
if self.opts.color.enabled(streams) {
streams.out.append(&bytes2wcstring(&highlight_and_colorize(
&out,
&parser.context(),
parser.vars(),
)));
} else {
streams.out.append(&out);
}
@@ -370,8 +372,8 @@ fn insert(
if self.add(
seq,
&argv[optind + 1..],
self.opts.bind_mode.to_owned(),
self.opts.sets_bind_mode.to_owned(),
self.opts.bind_mode.clone(),
self.opts.sets_bind_mode.clone(),
self.opts.user,
streams,
) {
@@ -408,7 +410,7 @@ fn parse_cmd_opts(
) -> BuiltinResult {
let cmd = argv[0];
let short_options = L!("aehkKfM:Lm:s");
const long_options: &[WOption] = &[
let long_options: &[WOption] = &[
wopt(L!("all"), NoArgument, 'a'),
wopt(L!("erase"), NoArgument, 'e'),
wopt(L!("function-names"), NoArgument, 'f'),
@@ -421,9 +423,10 @@ fn parse_cmd_opts(
wopt(L!("sets-mode"), RequiredArgument, 'm'),
wopt(L!("silent"), NoArgument, 's'),
wopt(L!("user"), NoArgument, 'u'),
wopt(L!("color"), RequiredArgument, COLOR_OPTION_CHAR),
];
let mut check_mode_name = |mode_name: &wstr| -> Result<(), ErrorCode> {
let check_mode_name = |streams: &mut IoStreams, mode_name: &wstr| -> Result<(), ErrorCode> {
if !valid_var_name(mode_name) {
streams.err.append(&wgettext_fmt!(
BUILTIN_ERR_BIND_MODE,
@@ -457,13 +460,13 @@ fn parse_cmd_opts(
}
'M' => {
let applicable_mode = w.woptarg.unwrap();
check_mode_name(applicable_mode)?;
check_mode_name(streams, applicable_mode)?;
opts.bind_mode = applicable_mode.to_owned();
opts.bind_mode_given = true;
}
'm' => {
let new_mode = w.woptarg.unwrap();
check_mode_name(new_mode)?;
check_mode_name(streams, new_mode)?;
opts.sets_bind_mode = Some(new_mode.to_owned());
}
'p' => {
@@ -487,6 +490,9 @@ fn parse_cmd_opts(
builtin_unknown_option(parser, streams, cmd, argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
COLOR_OPTION_CHAR => {
opts.color = ColorEnabled::parse_from_opt(streams, cmd, w.woptarg.unwrap())?;
}
_ => {
panic!("unexpected retval from WGetopter")
}

View File

@@ -4,6 +4,7 @@
use crate::{
env::{EnvMode, Environment},
fds::{BEST_O_SEARCH, wopen_dir},
parser::ParserEnvSetMode,
path::path_apply_cdpath,
wutil::{normalize_path, wperror, wreadlink},
};
@@ -127,7 +128,11 @@ pub fn cd(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Built
// Stash the fd for the cwd in the parser.
parser.libdata_mut().cwd_fd = Some(dir_fd);
parser.set_var_and_fire(L!("PWD"), EnvMode::EXPORT | EnvMode::GLOBAL, vec![norm_dir]);
parser.set_var_and_fire(
L!("PWD"),
ParserEnvSetMode::new(EnvMode::EXPORT | EnvMode::GLOBAL),
vec![norm_dir],
);
return Ok(SUCCESS);
}

View File

@@ -263,7 +263,7 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
let mut override_buffer = None;
const short_options: &wstr = L!("abijpctfxorhI:CBELSsP");
let short_options = L!("abijpctfxorhI:CBELSsP");
let long_options: &[WOption] = &[
wopt(L!("append"), ArgType::NoArgument, 'a'),
wopt(L!("insert"), ArgType::NoArgument, 'i'),
@@ -399,7 +399,7 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr])
// Don't enqueue a repaint if we're currently in the middle of one,
// because that's an infinite loop.
if matches!(cmd, RL::RepaintMode | RL::ForceRepaint | RL::Repaint)
&& parser.libdata().is_repaint
&& parser.is_repainting()
{
continue;
}

View File

@@ -1,9 +1,7 @@
use super::prelude::*;
use crate::common::{ScopeGuard, UnescapeFlags, UnescapeStringStyle, unescape_string};
use crate::complete::{CompletionRequestOptions, complete_add_wrapper, complete_remove_wrapper};
use crate::highlight::colorize;
use crate::highlight::highlight_shell;
use crate::nix::isatty;
use crate::highlight::highlight_and_colorize;
use crate::operation_context::OperationContext;
use crate::parse_constants::ParseErrorList;
use crate::parse_util::parse_util_detect_errors_in_argument_list;
@@ -18,7 +16,6 @@
complete_remove, complete_remove_all,
},
};
use libc::STDOUT_FILENO;
// 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
@@ -221,16 +218,20 @@ fn builtin_complete_remove(
}
}
fn builtin_complete_print(cmd: &wstr, streams: &mut IoStreams, parser: &Parser) {
fn builtin_complete_print(
cmd: &wstr,
streams: &mut IoStreams,
parser: &Parser,
color: ColorEnabled,
) {
let repr = complete_print(cmd);
// colorize if interactive
if !streams.out_is_redirected && isatty(STDOUT_FILENO) {
let mut colors = vec![];
highlight_shell(&repr, &mut colors, &parser.context(), false, None);
streams
.out
.append(&bytes2wcstring(&colorize(&repr, &colors, parser.vars())));
if color.enabled(streams) {
streams.out.append(&bytes2wcstring(&highlight_and_colorize(
&repr,
&parser.context(),
parser.vars(),
)));
} else {
streams.out.append(&repr);
}
@@ -259,9 +260,10 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
let mut wrap_targets = vec![];
let mut preserve_order = false;
let mut unescape_output = true;
let mut color = ColorEnabled::default();
const short_options: &wstr = L!("a:c:p:s:l:o:d:fFrxeuAn:C::w:hk");
const long_options: &[WOption] = &[
let short_options: &wstr = L!("a:c:p:s:l:o:d:fFrxeuAn:C::w:hk");
let long_options: &[WOption] = &[
wopt(L!("exclusive"), ArgType::NoArgument, 'x'),
wopt(L!("no-files"), ArgType::NoArgument, 'f'),
wopt(L!("force-files"), ArgType::NoArgument, 'F'),
@@ -282,6 +284,7 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("keep-order"), ArgType::NoArgument, 'k'),
wopt(L!("escape"), ArgType::NoArgument, OPT_ESCAPE),
wopt(L!("color"), ArgType::RequiredArgument, COLOR_OPTION_CHAR),
];
let mut have_x = false;
@@ -401,6 +404,9 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
builtin_unknown_option(parser, streams, cmd, argv[w.wopt_index - 1], true);
return Err(STATUS_INVALID_ARGS);
}
COLOR_OPTION_CHAR => {
color = ColorEnabled::parse_from_opt(streams, cmd, w.woptarg.unwrap())?;
}
_ => panic!("unexpected retval from WGetopter"),
}
}
@@ -584,10 +590,10 @@ pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) ->
// No arguments that would add or remove anything specified, so we print the definitions of
// all matching completions.
if cmd_to_complete.is_empty() {
builtin_complete_print(L!(""), streams, parser);
builtin_complete_print(L!(""), streams, parser, color);
} else {
for cmd in cmd_to_complete {
builtin_complete_print(&cmd, streams, parser);
builtin_complete_print(&cmd, streams, parser, color);
}
}
} else {

View File

@@ -1,6 +1,7 @@
//! Implementation of the fg builtin.
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::wutil::perror;
@@ -123,7 +124,7 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
// Provide value for `status current-command`
parser.libdata_mut().status_vars.command = ft.clone();
// Also provide a value for the deprecated fish 2.0 $_ variable
parser.set_var_and_fire(L!("_"), EnvMode::EXPORT, vec![ft]);
parser.set_var_and_fire(L!("_"), ParserEnvSetMode::new(EnvMode::EXPORT), vec![ft]);
// Provide value for `status current-commandline`
parser.libdata_mut().status_vars.commandline = job.command().to_owned();
}
@@ -156,7 +157,6 @@ pub fn fg(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Built
if job.is_stopped() {
handoff.save_tty_modes();
}
handoff.reclaim();
if resumed {
Ok(SUCCESS)
} else {

View File

@@ -11,7 +11,7 @@
use super::prelude::*;
use crate::ast::{self, AsNode, Ast, Kind, Leaf, Node, NodeVisitor, SourceRangeList, Traversal};
use crate::common::{
PROGRAM_NAME, UnescapeFlags, UnescapeStringStyle, bytes2wcstring, get_program_name,
PROGRAM_NAME, ReadExt, UnescapeFlags, UnescapeStringStyle, bytes2wcstring, get_program_name,
unescape_string, wcs2bytes,
};
use crate::env::EnvStack;
@@ -756,6 +756,10 @@ fn brace_is_continuation(&self, node: &dyn ast::Token) -> bool {
};
conj.decorator.is_some()
|| matches!(
self.traversal.parent(conj.as_node()).kind(),
Kind::IfClause(_) | Kind::WhileHeader(_)
)
}
fn visit_left_brace(&mut self, node: &dyn ast::Token) {
@@ -910,10 +914,9 @@ pub fn main() {
fn throwing_main() -> i32 {
// TODO: Duplicated with fish_key_reader
use crate::io::FdOutputStream;
use crate::io::IoChain;
use crate::io::OutputStream::Fd;
use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
use crate::fds::BorrowedFdFile;
use crate::io::{FdOutputStream, IoChain, OutputStream::Fd};
use libc::{STDERR_FILENO, STDOUT_FILENO};
topic_monitor_init();
threads::init();
@@ -922,12 +925,13 @@ fn throwing_main() -> i32 {
let mut err = Fd(FdOutputStream::new(STDERR_FILENO));
let io_chain = IoChain::new();
let mut streams = IoStreams::new(&mut out, &mut err, &io_chain);
streams.stdin_fd = STDIN_FILENO;
streams.stdin_file = Some(BorrowedFdFile::stdin());
// Safety: single-threaded.
unsafe {
set_libc_locales(/*log_ok=*/ false)
};
crate::localization::initialize_gettext();
#[cfg(feature = "localize-messages")]
crate::localization::initialize_localization();
env_init(None, true, false);
// Only set these here so you can't set them via the builtin.
@@ -940,15 +944,19 @@ fn throwing_main() -> i32 {
let args: Vec<WString> = std::env::args_os()
.map(|osstr| bytes2wcstring(osstr.as_bytes()))
.collect();
do_indent(&mut streams, args).builtin_status_code()
do_indent(None, &mut streams, args).builtin_status_code()
}
pub fn fish_indent(_parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
pub fn fish_indent(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
let args = args.iter_mut().map(|x| x.to_owned()).collect();
do_indent(streams, args)
do_indent(Some(parser), streams, args)
}
fn do_indent(streams: &mut IoStreams, args: Vec<WString>) -> BuiltinResult {
fn do_indent(
parser: Option<&Parser>,
streams: &mut IoStreams,
args: Vec<WString>,
) -> BuiltinResult {
// Types of output we support
#[derive(Eq, PartialEq)]
enum OutputType {
@@ -988,7 +996,11 @@ enum OutputType {
match c {
'P' => DUMP_PARSE_TREE.store(true),
'h' => {
print_help("fish_indent");
if let Some(parser) = parser {
builtin_print_help(parser, streams, L!("fish_indent"));
} else {
print_help("fish_indent");
}
return Ok(SUCCESS);
}
'v' => {
@@ -1042,18 +1054,24 @@ enum OutputType {
));
return Err(STATUS_CMD_ERROR);
}
use std::os::fd::FromRawFd;
let mut fd = unsafe { std::fs::File::from_raw_fd(streams.stdin_fd) };
let Some(stdin_file) = streams.stdin_file.as_mut() else {
let cmd = "fish_indent";
streams
.err
.append(&wgettext_fmt!("%s: stdin is closed\n", cmd));
return Err(STATUS_CMD_ERROR);
};
let mut buf = vec![];
match fd.read_to_end(&mut buf) {
match stdin_file.read_to_end_interruptible(&mut buf) {
Ok(_) => {}
Err(_) => {
// Don't close the fd
std::mem::forget(fd);
return Err(STATUS_CMD_ERROR);
Err(err) => {
return if err.kind() == std::io::ErrorKind::Interrupted {
Err(128 + libc::SIGINT)
} else {
Err(STATUS_CMD_ERROR)
};
}
}
std::mem::forget(fd);
src = bytes2wcstring(&buf);
} else {
let arg = args[i];
@@ -1250,7 +1268,7 @@ struct TokenRange {
}
let mut token_ranges: Vec<TokenRange> = vec![];
for (i, color) in colors.iter().cloned().enumerate() {
for (i, color) in colors.iter().copied().enumerate() {
let role = color.foreground;
// See if we can extend the last range.
if let Some(last) = token_ranges.last_mut() {

View File

@@ -172,6 +172,7 @@ fn setup_and_process_keys(
}
fn parse_flags(
parser: Option<&Parser>,
streams: &mut IoStreams,
args: Vec<WString>,
continuous_mode: &mut bool,
@@ -184,7 +185,6 @@ fn parse_flags(
wopt(L!("version"), ArgType::NoArgument, 'v'),
wopt(L!("verbose"), ArgType::NoArgument, 'V'),
];
let mut shim_args: Vec<&wstr> = args.iter().map(|s| s.as_ref()).collect();
let mut w = WGetopter::new(short_opts, long_opts, &mut shim_args);
while let Some(opt) = w.next_opt() {
@@ -193,7 +193,11 @@ fn parse_flags(
*continuous_mode = true;
}
'h' => {
print_help("fish_key_reader");
if let Some(parser) = parser {
builtin_print_help(parser, streams, L!("fish_key_reader"));
} else {
print_help("fish_key_reader");
}
return ControlFlow::Break(Ok(SUCCESS));
}
'v' => {
@@ -239,7 +243,7 @@ fn parse_flags(
}
pub fn fish_key_reader(
_parser: &Parser,
parser: &Parser,
streams: &mut IoStreams,
args: &mut [&wstr],
) -> BuiltinResult {
@@ -247,11 +251,17 @@ pub fn fish_key_reader(
let mut verbose = false;
let args = args.iter_mut().map(|x| x.to_owned()).collect();
if let ControlFlow::Break(s) = parse_flags(streams, args, &mut continuous_mode, &mut verbose) {
if let ControlFlow::Break(s) = parse_flags(
Some(parser),
streams,
args,
&mut continuous_mode,
&mut verbose,
) {
return s;
}
if streams.stdin_fd < 0 || !isatty(streams.stdin_fd) {
if streams.stdin_fd() < 0 || !isatty(streams.stdin_fd()) {
streams.err.appendln("Stdin must be attached to a tty.");
return Err(STATUS_CMD_ERROR);
}
@@ -261,7 +271,7 @@ pub fn fish_key_reader(
continuous_mode,
verbose,
// Won't be querying, so no timeout value needed.
InputEventQueue::new(streams.stdin_fd, None),
InputEventQueue::new(streams.stdin_fd(), None),
)
}
@@ -279,7 +289,8 @@ fn throwing_main() -> i32 {
set_interactive_session(true);
topic_monitor_init();
threads::init();
crate::localization::initialize_gettext();
#[cfg(feature = "localize-messages")]
crate::localization::initialize_localization();
env_init(None, true, false);
reader_init(false);
if let Some(features_var) = EnvStack::globals().get(L!("fish_features")) {
@@ -300,7 +311,7 @@ fn throwing_main() -> i32 {
.map(|osstr| bytes2wcstring(osstr.as_bytes()))
.collect();
if let ControlFlow::Break(s) =
parse_flags(&mut streams, args, &mut continuous_mode, &mut verbose)
parse_flags(None, &mut streams, args, &mut continuous_mode, &mut verbose)
{
return s.builtin_status_code();
}

View File

@@ -74,7 +74,6 @@ fn job_id_for_pid(pid: Pid, parser: &Parser) -> Option<u64> {
/// Returns an exit status.
fn parse_cmd_opts(
opts: &mut FunctionCmdOpts,
optind: &mut usize,
argv: &mut [&wstr],
parser: &Parser,
streams: &mut IoStreams,
@@ -83,6 +82,34 @@ fn parse_cmd_opts(
let print_hints = false;
let mut handling_named_arguments = false;
let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, argv);
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));
return Err(STATUS_INVALID_ARGS);
}
if !read_only_ok && is_read_only(varname) {
streams.err.append(&wgettext_fmt!(
"%s: variable '%s' is read-only\n",
cmd,
varname
));
return Err(STATUS_INVALID_ARGS);
}
Ok(())
};
fn add_named_argument(
validate_variable_name: &mut impl FnMut(&mut IoStreams, &wstr, bool) -> Result<(), i32>,
streams: &mut IoStreams,
opts: &mut FunctionCmdOpts,
varname: &wstr,
) -> Result<(), i32> {
validate_variable_name(streams, varname, /*read_only_ok=*/ false)?;
opts.named_arguments.push(varname.to_owned());
Ok::<(), ErrorCode>(())
}
while let Some(opt) = w.next_opt() {
// NON_OPTION_CHAR is returned when we reach a non-permuted non-option.
if opt != 'a' && opt != NON_OPTION_CHAR {
@@ -91,17 +118,9 @@ fn parse_cmd_opts(
match opt {
NON_OPTION_CHAR => {
// A positional argument we got because we use RETURN_IN_ORDER.
let woptarg = w.woptarg.unwrap().to_owned();
let woptarg = w.woptarg.unwrap();
if handling_named_arguments {
if is_read_only(&woptarg) {
streams.err.append(&wgettext_fmt!(
"%s: variable '%s' is read-only\n",
cmd,
woptarg
));
return Err(STATUS_INVALID_ARGS);
}
opts.named_arguments.push(woptarg);
add_named_argument(&mut validate_variable_name, streams, opts, woptarg)?;
} else {
streams.err.append(&wgettext_fmt!(
"%s: %s: unexpected positional argument",
@@ -127,10 +146,7 @@ fn parse_cmd_opts(
}
'v' => {
let name = w.woptarg.unwrap().to_owned();
if !valid_var_name(&name) {
streams.err.append(&varname_error(cmd, &name));
return Err(STATUS_INVALID_ARGS);
}
validate_variable_name(streams, &name, /*read_only_ok=*/ true)?;
opts.events.push(EventDescription::Variable { name });
}
'e' => {
@@ -175,17 +191,13 @@ fn parse_cmd_opts(
opts.events.push(e);
}
'a' => {
let name = w.woptarg.unwrap().to_owned();
if is_read_only(&name) {
streams.err.append(&wgettext_fmt!(
"%s: variable '%s' is read-only\n",
cmd,
name
));
return Err(STATUS_INVALID_ARGS);
}
handling_named_arguments = true;
opts.named_arguments.push(name);
add_named_argument(
&mut validate_variable_name,
streams,
opts,
w.woptarg.unwrap(),
)?;
}
'S' => {
opts.shadow_scope = false;
@@ -195,10 +207,7 @@ fn parse_cmd_opts(
}
'V' => {
let woptarg = w.woptarg.unwrap();
if !valid_var_name(woptarg) {
streams.err.append(&varname_error(cmd, woptarg));
return Err(STATUS_INVALID_ARGS);
}
validate_variable_name(streams, woptarg, /*read_only_ok=*/ false)?;
opts.inherit_vars.push(woptarg.to_owned());
}
'h' => {
@@ -228,7 +237,23 @@ fn parse_cmd_opts(
}
}
*optind = w.wopt_index;
let optind = w.wopt_index;
if argv.len() != optind {
if !opts.named_arguments.is_empty() {
// Remaining arguments are named arguments.
for &arg in argv[optind..].iter() {
add_named_argument(&mut validate_variable_name, streams, opts, arg)?;
}
} else {
streams.err.append(&wgettext_fmt!(
"%s: %s: unexpected positional argument",
cmd,
argv[optind],
));
return Err(STATUS_INVALID_ARGS);
}
}
Ok(SUCCESS)
}
@@ -287,34 +312,13 @@ pub fn function(
let argv = &mut argv[1..];
let mut opts = FunctionCmdOpts::default();
let mut optind = 0;
parse_cmd_opts(&mut opts, &mut optind, argv, parser, streams)?;
parse_cmd_opts(&mut opts, argv, parser, streams)?;
if opts.print_help {
builtin_print_error_trailer(parser, streams.err, cmd);
return Ok(SUCCESS);
}
if argv.len() != optind {
if !opts.named_arguments.is_empty() {
// Remaining arguments are named arguments.
for &arg in argv[optind..].iter() {
if !valid_var_name(arg) {
streams.err.append(&varname_error(cmd, arg));
return Err(STATUS_INVALID_ARGS);
}
opts.named_arguments.push(arg.to_owned());
}
} else {
streams.err.append(&wgettext_fmt!(
"%s: %s: unexpected positional argument",
cmd,
argv[optind],
));
return Err(STATUS_INVALID_ARGS);
}
}
// Extract the current filename.
let definition_file = parser.libdata().current_filename.clone();
@@ -331,13 +335,6 @@ pub fn function(
})
.collect();
for named in &opts.named_arguments {
if !valid_var_name(named) {
streams.err.append(&varname_error(cmd, named));
return Err(STATUS_INVALID_ARGS);
}
}
// We have what we need to actually define the function.
let props = function::FunctionProperties {
func_node,

View File

@@ -6,8 +6,7 @@
use crate::common::{EscapeFlags, EscapeStringStyle};
use crate::event::{self};
use crate::function;
use crate::highlight::colorize;
use crate::highlight::highlight_shell;
use crate::highlight::highlight_and_colorize;
use crate::parse_util::apply_indents;
use crate::parse_util::parse_util_compute_indents;
use crate::parser_keywords::parser_keywords_is_reserved;
@@ -25,6 +24,7 @@ struct FunctionsCmdOpts<'args> {
no_metadata: bool,
verbose: bool,
handlers: bool,
color: ColorEnabled,
handlers_type: Option<&'args wstr>,
description: Option<&'args wstr>,
}
@@ -46,6 +46,7 @@ struct FunctionsCmdOpts<'args> {
wopt(L!("verbose"), ArgType::NoArgument, 'v'),
wopt(L!("handlers"), ArgType::NoArgument, 'H'),
wopt(L!("handlers-type"), ArgType::RequiredArgument, 't'),
wopt(L!("color"), ArgType::RequiredArgument, COLOR_OPTION_CHAR),
];
/// Parses options to builtin function, populating opts.
@@ -79,6 +80,9 @@ fn parse_cmd_opts<'args>(
opts.handlers = true;
opts.handlers_type = Some(w.woptarg.unwrap());
}
COLOR_OPTION_CHAR => {
opts.color = ColorEnabled::parse_from_opt(streams, cmd, w.woptarg.unwrap())?;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], print_hints);
return Err(STATUS_INVALID_ARGS);
@@ -278,7 +282,7 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
if opts.list || args.is_empty() {
let mut names = function::get_names(opts.show_hidden, parser.vars());
names.sort();
if streams.out_is_terminal() {
if opts.color.enabled(streams) {
let mut buff = WString::new();
let mut first: bool = true;
for name in names {
@@ -414,12 +418,12 @@ pub fn functions(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -
def = apply_indents(&def, &parse_util_compute_indents(&def));
}
if streams.out_is_terminal() {
let mut colors = vec![];
highlight_shell(&def, &mut colors, &parser.context(), false, None);
streams
.out
.append(&bytes2wcstring(&colorize(&def, &colors, parser.vars())));
if opts.color.enabled(streams) {
streams.out.append(&bytes2wcstring(&highlight_and_colorize(
&def,
&parser.context(),
parser.vars(),
)));
} else {
streams.out.append(&def);
}

View File

@@ -60,6 +60,7 @@ struct HistoryCmdOpts {
case_sensitive: bool,
null_terminate: bool,
reverse: bool,
color: ColorEnabled,
}
/// Note: Do not add new flags that represent subcommands. We're encouraging people to switch to
@@ -82,6 +83,7 @@ struct HistoryCmdOpts {
wopt(L!("clear"), ArgType::NoArgument, '\x04'),
wopt(L!("merge"), ArgType::NoArgument, '\x05'),
wopt(L!("reverse"), ArgType::NoArgument, 'R'),
wopt(L!("color"), ArgType::RequiredArgument, COLOR_OPTION_CHAR),
];
/// Remember the history subcommand and disallow selecting more than one history subcommand.
@@ -226,6 +228,9 @@ fn parse_cmd_opts(
}
w.remaining_text = L!("");
}
COLOR_OPTION_CHAR => {
opts.color = ColorEnabled::parse_from_opt(streams, cmd, w.woptarg.unwrap())?;
}
_ => {
panic!("unexpected retval from WGetopter");
}
@@ -278,6 +283,8 @@ pub fn history(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
match opts.hist_cmd {
HistCmd::None | HistCmd::Search => {
if !history.search(
parser,
streams,
opts.search_type
.unwrap_or(history::SearchType::ContainsGlob),
args,
@@ -287,7 +294,7 @@ pub fn history(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) ->
opts.null_terminate,
opts.reverse,
&parser.context().cancel_checker,
streams,
opts.color.enabled(streams),
) {
status = Err(STATUS_CMD_ERROR);
}

View File

@@ -2,13 +2,12 @@
use crate::util::get_seeded_rng;
use crate::wutil;
use once_cell::sync::Lazy;
use rand::rngs::SmallRng;
use rand::{Rng, RngCore};
use std::sync::Mutex;
use std::sync::{LazyLock, Mutex};
static RNG: Lazy<Mutex<SmallRng>> =
Lazy::new(|| Mutex::new(get_seeded_rng(rand::rng().next_u64())));
static RNG: LazyLock<Mutex<SmallRng>> =
LazyLock::new(|| Mutex::new(get_seeded_rng(rand::rng().next_u64())));
pub fn random(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
let cmd = argv[0];

View File

@@ -16,6 +16,7 @@
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};
@@ -42,7 +43,7 @@ pub(crate) enum TokenOutputMode {
#[derive(Default)]
struct Options {
print_help: bool,
place: EnvMode,
place: ParserEnvSetMode,
prompt: Option<WString>,
prompt_str: Option<WString>,
right_prompt: WString,
@@ -63,7 +64,7 @@ struct Options {
impl Options {
fn new() -> Self {
Options {
place: EnvMode::USER,
place: ParserEnvSetMode::user(EnvMode::empty()),
..Default::default()
}
}
@@ -129,10 +130,10 @@ fn parse_cmd_opts(
return Err(STATUS_INVALID_ARGS);
}
'f' => {
opts.place |= EnvMode::FUNCTION;
opts.place.mode |= EnvMode::FUNCTION;
}
'g' => {
opts.place |= EnvMode::GLOBAL;
opts.place.mode |= EnvMode::GLOBAL;
}
'h' => {
opts.print_help = true;
@@ -141,7 +142,7 @@ fn parse_cmd_opts(
opts.one_line = true;
}
'l' => {
opts.place |= EnvMode::LOCAL;
opts.place.mode |= EnvMode::LOCAL;
}
'n' => {
opts.nchars = match fish_wcstoi(w.woptarg.unwrap()) {
@@ -205,13 +206,13 @@ fn parse_cmd_opts(
opts.token_mode = Some(new_mode);
}
'U' => {
opts.place |= EnvMode::UNIVERSAL;
opts.place.mode |= EnvMode::UNIVERSAL;
}
'u' => {
opts.place |= EnvMode::UNEXPORT;
opts.place.mode |= EnvMode::UNEXPORT;
}
'x' => {
opts.place |= EnvMode::EXPORT;
opts.place.mode |= EnvMode::EXPORT;
}
'z' => {
opts.split_null = true;
@@ -483,7 +484,7 @@ fn validate_read_args(
opts.prompt = Some(DEFAULT_READ_PROMPT.to_owned());
}
if opts.place.contains(EnvMode::UNEXPORT) && opts.place.contains(EnvMode::EXPORT) {
if opts.place.mode.contains(EnvMode::UNEXPORT) && opts.place.mode.contains(EnvMode::EXPORT) {
streams
.err
.append(&wgettext_fmt!(BUILTIN_ERR_EXPUNEXP, cmd));
@@ -493,7 +494,8 @@ fn validate_read_args(
if opts
.place
.intersection(EnvMode::LOCAL | EnvMode::FUNCTION | EnvMode::GLOBAL | EnvMode::UNIVERSAL)
.mode
.intersection(EnvMode::ANY_SCOPE)
.iter()
.count()
> 1
@@ -602,7 +604,7 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
validate_read_args(cmd, &mut opts, argv, parser, streams)?;
// stdin may have been explicitly closed
if streams.stdin_fd < 0 {
if streams.is_stdin_closed() {
streams
.err
.append(&wgettext_fmt!("%s: stdin is closed\n", cmd));
@@ -620,12 +622,12 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
let vars_left = |var_ptr: usize| argc - var_ptr;
let clear_remaining_vars = |var_ptr: &mut usize| {
while vars_left(*var_ptr) != 0 {
parser.vars().set_empty(argv[*var_ptr], opts.place);
parser.set_empty(argv[*var_ptr], opts.place);
*var_ptr += 1;
}
};
let stream_stdin_is_a_tty = isatty(streams.stdin_fd);
let stream_stdin_is_a_tty = streams.stdin_fd() >= 0 && isatty(streams.stdin_fd());
// Normally, we either consume a line of input or all available input. But if we are reading a
// line at a time, we need a middle ground where we only consume as many lines as we need to
@@ -644,7 +646,7 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
opts.prompt.as_ref().unwrap(),
&opts.right_prompt,
&opts.commandline,
streams.stdin_fd,
streams.stdin_fd(),
);
} else if opts.nchars.is_none() && !stream_stdin_is_a_tty &&
// "one_line" is implemented as reading n-times to a new line,
@@ -653,7 +655,7 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
!opts.one_line &&
(
streams.stdin_is_directly_redirected ||
unsafe {libc::lseek(streams.stdin_fd, 0, SEEK_CUR)} != -1)
unsafe {libc::lseek(streams.stdin_fd(), 0, SEEK_CUR)} != -1)
{
// We read in chunks when we either can seek (so we put the bytes back),
// or we have the bytes to ourselves (because it's directly redirected).
@@ -663,14 +665,18 @@ pub fn read(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui
// You don't rewind VHS tapes before throwing them in the trash.
// TODO: Do this when nchars is set by seeking back.
exit_res = read_in_chunks(
streams.stdin_fd,
streams.stdin_fd(),
&mut buff,
opts.split_null,
!streams.stdin_is_directly_redirected,
);
} else {
exit_res =
read_one_char_at_a_time(streams.stdin_fd, &mut buff, opts.nchars, opts.split_null);
exit_res = read_one_char_at_a_time(
streams.stdin_fd(),
&mut buff,
opts.nchars,
opts.split_null,
);
}
if exit_res.is_err() {

View File

@@ -16,6 +16,7 @@
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},
wutil::wcstoi::wcstoi_partial,
@@ -77,8 +78,8 @@ fn default() -> Self {
}
impl Options {
fn scope(&self) -> EnvMode {
let mut scope = EnvMode::USER;
fn env_mode(&self) -> EnvMode {
let mut scope = EnvMode::empty();
for (is_mode, mode) in [
(self.local, EnvMode::LOCAL),
(self.function, EnvMode::FUNCTION),
@@ -367,15 +368,16 @@ fn env_set_reporting_errors(
cmd: &wstr,
opts: &Options,
key: &wstr,
scope: EnvMode,
mode: EnvMode,
list: Vec<WString>,
streams: &mut IoStreams,
parser: &Parser,
) -> EnvStackSetResult {
let mode = ParserEnvSetMode::user(mode);
let retval = if opts.no_event {
parser.set_var(key, scope | EnvMode::USER, list)
parser.set_var(key, mode, list)
} else {
parser.set_var_and_fire(key, scope | EnvMode::USER, list)
parser.set_var_and_fire(key, mode, list)
};
// If this returned OK, the parser already fired the event.
handle_env_return(retval, cmd, key, streams);
@@ -554,7 +556,7 @@ fn erased_at_indexes(mut input: Vec<WString>, mut indexes: Vec<isize>) -> Vec<WS
/// `set --names` flag was used.
fn list(opts: &Options, parser: &Parser, streams: &mut IoStreams) -> BuiltinResult {
let names_only = opts.list;
let mut names = parser.vars().get_names(opts.scope());
let mut names = parser.vars().get_names(opts.env_mode());
names.sort();
for key in names {
@@ -573,7 +575,7 @@ fn list(opts: &Options, parser: &Parser, streams: &mut IoStreams) -> BuiltinResu
}
val += &expand_escape_string(history.item_at_index(i).unwrap().str())[..]
}
} else if let Some(var) = parser.vars().getf_unless_empty(&key, opts.scope()) {
} else if let Some(var) = parser.vars().getf_unless_empty(&key, opts.env_mode()) {
val = expand_escape_variable(&var);
}
if !val.is_empty() {
@@ -606,7 +608,7 @@ fn query(
args: &[&wstr],
) -> BuiltinResult {
let mut retval = 0;
let scope = opts.scope();
let mode = opts.env_mode();
// No variables given, this is an error.
// 255 is the maximum return code we allow.
@@ -615,7 +617,7 @@ fn query(
}
for arg in args {
let Some(split) = split_var_and_indexes(arg, scope, parser.vars(), streams) else {
let Some(split) = split_var_and_indexes(arg, mode, parser.vars(), streams) else {
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_CMD_ERROR);
};
@@ -708,7 +710,7 @@ fn show(cmd: &wstr, parser: &Parser, streams: &mut IoStreams, args: &[&wstr]) ->
let vars = parser.vars();
if args.is_empty() {
// show all vars
let mut names = vars.get_names(EnvMode::USER);
let mut names = vars.get_names(EnvMode::empty());
names.sort();
for name in names {
if name == "history" {
@@ -776,15 +778,9 @@ fn erase(
args: &[&wstr],
) -> BuiltinResult {
let mut ret = Ok(SUCCESS);
let scopes = opts.scope();
// `set -e` is allowed to be called with multiple scopes.
for bit in (0..).take_while(|bit| 1 << bit <= EnvMode::USER.bits()) {
let scope = scopes.intersection(EnvMode::from_bits(1 << bit).unwrap());
if scope.bits() == 0 || (scope == EnvMode::USER && scopes != EnvMode::USER) {
continue;
}
let mut erase_with_mode = |mode| {
for arg in args {
let Some(split) = split_var_and_indexes(arg, scope, parser.vars(), streams) else {
let Some(split) = split_var_and_indexes(arg, mode, parser.vars(), streams) else {
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_CMD_ERROR);
};
@@ -797,7 +793,7 @@ fn erase(
let retval;
if split.indexes.is_empty() {
// unset the var
retval = parser.vars().remove(split.varname, scope);
retval = parser.remove_var(split.varname, ParserEnvSetMode::new(mode));
// When a non-existent-variable is unset, return NotFound as $status
// but do not emit any errors at the console as a compromise between user
// friendliness and correctness.
@@ -817,7 +813,7 @@ fn erase(
cmd,
opts,
split.varname,
scope,
mode,
result,
streams,
parser,
@@ -830,10 +826,49 @@ fn erase(
ret = retval.into();
}
}
Ok(())
};
// `set -e` is allowed to be called with multiple scopes.
let mode = opts.env_mode();
let any_scope = EnvMode::ANY_SCOPE;
let scopes = mode.intersection(any_scope);
if scopes.is_empty() {
erase_with_mode(mode)?;
} else {
// Historical behavior is to go from inner to outer, which may be relevant for scopes that
// collide with the function scope (i.e. local and global).
assert!(is_subsequence(
scopes.iter(),
[
EnvMode::LOCAL,
EnvMode::FUNCTION,
EnvMode::GLOBAL,
EnvMode::UNIVERSAL
]
.into_iter()
));
for scope in scopes.iter() {
let other_scopes = any_scope - scope;
erase_with_mode(mode - other_scopes)?;
}
}
ret
}
fn is_subsequence<T: Eq>(
mut lhs: impl Iterator<Item = T>,
mut rhs: impl Iterator<Item = T>,
) -> bool {
lhs.all(|l| {
for r in rhs.by_ref() {
if r == l {
return true;
}
}
false
})
}
/// Return a list of new values for the variable `varname`, respecting the `opts`.
/// This handles the simple case where there are no indexes.
fn new_var_values(
@@ -855,7 +890,7 @@ fn new_var_values(
// So do not use the given variable: we must re-fetch it.
// TODO: this races under concurrent execution.
if let Some(existing) = vars.get(varname) {
result = existing.as_list().to_owned();
existing.as_list().clone_into(&mut result);
}
if opts.prepend {
@@ -879,10 +914,12 @@ fn new_var_values_by_index(split: &SplitVar, argv: &[&wstr]) -> Vec<WString> {
// Inherit any existing values.
// Note unlike the append/prepend case, we start with a variable in the same scope as we are
// setting.
let mut result = vec![];
if let Some(var) = split.var.as_ref() {
result = var.as_list().to_owned();
}
let mut result = split
.var
.as_ref()
.map(EnvVar::as_list)
.unwrap_or_default()
.to_owned();
// For each (index, argument) pair, set the element in our `result` to the replacement string.
// Extend the list with empty strings as needed. The indexes are 1-based.
@@ -916,11 +953,11 @@ fn set_internal(
return Err(STATUS_INVALID_ARGS);
}
let scope = opts.scope();
let mode = opts.env_mode();
let var_expr = argv[0];
let argv = &argv[1..];
let Some(split) = split_var_and_indexes(var_expr, scope, parser.vars(), streams) else {
let Some(split) = split_var_and_indexes(var_expr, mode, parser.vars(), streams) else {
builtin_print_error_trailer(parser, streams.err, cmd);
return Err(STATUS_INVALID_ARGS);
};
@@ -984,7 +1021,7 @@ fn set_internal(
// Set the value back in the variable stack and fire any events.
let retval =
env_set_reporting_errors(cmd, opts, split.varname, scope, new_values, streams, parser);
env_set_reporting_errors(cmd, opts, split.varname, mode, new_values, streams, parser);
if retval == EnvStackSetResult::Ok {
warn_if_uvar_shadows_global(cmd, opts, split.varname, streams, parser);

View File

@@ -1,5 +1,6 @@
use super::prelude::*;
use crate::common::{Named, bytes2wcstring, escape, get_by_sorted_name};
use crate::fds::BorrowedFdFile;
use crate::io::OutputStream;
use crate::parse_constants::UNKNOWN_BUILTIN_ERR_MSG;
use crate::parse_util::parse_util_argument_is_help;
@@ -8,10 +9,7 @@
use crate::{builtins::*, wutil};
use errno::errno;
use fish_wchar::L;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::os::fd::FromRawFd;
pub type BuiltinCmd = fn(&Parser, &mut IoStreams, &mut [&wstr]) -> BuiltinResult;
@@ -814,25 +812,26 @@ pub fn new(arg: Cow<'args, wstr>, want_newline: bool) -> Self {
/// A helper type for extracting arguments from either argv or stdin.
pub struct Arguments<'args, 'iter> {
/// The list of arguments passed to the string builtin.
args: &'iter [&'args wstr],
/// If using argv, index of the next argument to return.
argidx: &'iter mut usize,
split_behavior: SplitBehavior,
/// Buffer to store what we read with the BufReader
/// Is only here to avoid allocating every time
buffer: Vec<u8>,
/// If not using argv, we read with a buffer
reader: Option<BufReader<File>>,
source: ArgvSource<'args, 'iter>,
}
impl Drop for Arguments<'_, '_> {
fn drop(&mut self) {
if let Some(r) = self.reader.take() {
// we should not close stdin
std::mem::forget(r.into_inner());
}
}
/// Either the arguments from argv, or from stdin.
enum ArgvSource<'args, 'iter> {
/// Read arguments from argv.
Args {
// The list of arguments passed to the builtin.
args: &'iter [&'args wstr],
// Index of the next argument to return.
argidx: &'iter mut usize,
},
/// Read arguments from stdin (possibly redirected).
Stdin {
/// Reused storage for reading.
buffer: Vec<u8>,
/// The reader to read from.
reader: BufReader<BorrowedFdFile>,
},
}
impl<'args, 'iter> Arguments<'args, 'iter> {
@@ -842,20 +841,21 @@ pub fn new(
streams: &mut IoStreams,
chunk_size: usize,
) -> Self {
let reader = streams.stdin_is_directly_redirected.then(|| {
let stdin_fd = streams.stdin_fd;
assert!(stdin_fd >= 0, "should have a valid fd");
// safety: this should be a valid fd, and already open
let fd = unsafe { File::from_raw_fd(stdin_fd) };
BufReader::with_capacity(chunk_size, fd)
});
let source: ArgvSource = if !streams.stdin_is_directly_redirected {
ArgvSource::Args { args, argidx }
} else {
let stdin_file = streams
.stdin_file
.clone()
.expect("should have stdin if redirected");
ArgvSource::Stdin {
buffer: Vec::new(),
reader: BufReader::with_capacity(chunk_size, stdin_file),
}
};
Arguments {
args,
argidx,
split_behavior: SplitBehavior::Newline,
buffer: Vec::new(),
reader,
source,
}
}
@@ -864,9 +864,23 @@ pub fn with_split_behavior(mut self, split_behavior: SplitBehavior) -> Self {
self
}
/// Return the next argument by reading from argv ArgvSource.
fn get_arg_argv(&mut self) -> Option<InputValue<'args>> {
let ArgvSource::Args { args, argidx } = &mut self.source else {
panic!("Not reading from argv")
};
let arg = args.get(**argidx)?;
**argidx += 1;
let retval = InputValue::new(Cow::Borrowed(arg), /*want_newline=*/ true);
Some(retval)
}
/// Return the next argument by reading from stdin ArgvSource.
fn get_arg_stdin(&mut self) -> Option<InputValue<'args>> {
use SplitBehavior::*;
let reader = self.reader.as_mut().unwrap();
let ArgvSource::Stdin { reader, buffer } = &mut self.source else {
panic!("Not reading from stdin")
};
if self.split_behavior == InferNull {
// we must determine if the first `PATH_MAX` bytes contains a null.
@@ -882,9 +896,9 @@ fn get_arg_stdin(&mut self) -> Option<InputValue<'args>> {
// NOTE: C++ wrongly commented that read_blocked retries for EAGAIN
let num_bytes: usize = match self.split_behavior {
Newline => reader.read_until(b'\n', &mut self.buffer),
Null => reader.read_until(b'\0', &mut self.buffer),
Never => reader.read_to_end(&mut self.buffer),
Newline => reader.read_until(b'\n', buffer),
Null => reader.read_until(b'\0', buffer),
Never => reader.read_to_end(buffer),
_ => unreachable!(),
}
.ok()?;
@@ -895,7 +909,7 @@ fn get_arg_stdin(&mut self) -> Option<InputValue<'args>> {
}
// assert!(num_bytes == self.buffer.len());
let (end, want_newline) = match (&self.split_behavior, self.buffer.last()) {
let (end, want_newline) = match (&self.split_behavior, buffer.last()) {
// remove the newline — consumers do not expect it
(Newline, Some(b'\n')) => (num_bytes - 1, true),
// we are missing a trailing newline!
@@ -909,8 +923,8 @@ fn get_arg_stdin(&mut self) -> Option<InputValue<'args>> {
_ => unreachable!(),
};
let parsed = bytes2wcstring(&self.buffer[..end]);
self.buffer.clear();
let parsed = bytes2wcstring(&buffer[..end]);
buffer.clear();
Some(InputValue::new(Cow::Owned(parsed), want_newline))
}
@@ -925,19 +939,10 @@ impl<'args> Iterator for Arguments<'args, '_> {
type Item = InputValue<'args>;
fn next(&mut self) -> Option<Self::Item> {
if self.reader.is_some() {
return self.get_arg_stdin();
match &mut self.source {
ArgvSource::Args { .. } => self.get_arg_argv(),
ArgvSource::Stdin { .. } => self.get_arg_stdin(),
}
if *self.argidx >= self.args.len() {
return None;
}
let retval = InputValue::new(
Cow::Borrowed(self.args[*self.argidx]),
/*want_newline=*/ true,
);
*self.argidx += 1;
Some(retval)
}
}
@@ -1048,3 +1053,51 @@ pub fn builtin_break_continue(
};
Ok(SUCCESS)
}
/// Option character for --color flag
pub const COLOR_OPTION_CHAR: char = '\x10';
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorEnabled {
#[default]
Auto,
Always,
Never,
}
impl TryFrom<&wstr> for ColorEnabled {
type Error = ();
fn try_from(s: &wstr) -> Result<Self, Self::Error> {
match s {
s if s == "auto" => Ok(ColorEnabled::Auto),
s if s == "always" => Ok(ColorEnabled::Always),
s if s == "never" => Ok(ColorEnabled::Never),
_ => Err(()),
}
}
}
impl ColorEnabled {
pub fn enabled(&self, streams: &crate::io::IoStreams) -> bool {
match self {
ColorEnabled::Always => true,
ColorEnabled::Never => false,
ColorEnabled::Auto => streams.out_is_terminal(),
}
}
pub fn parse_from_opt(
streams: &mut IoStreams,
cmd: &wstr,
arg: &wstr,
) -> Result<Self, ErrorCode> {
Self::try_from(arg).map_err(|()| {
streams.err.append(&wgettext_fmt!(
"%s: Invalid value for '--color' option: '%s'. Expected 'always', 'never', or 'auto'\n",
cmd,
arg
));
STATUS_INVALID_ARGS
})
}
}

View File

@@ -36,14 +36,14 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
let optind = opts.optind;
if argc == optind || args[optind] == "-" {
if streams.stdin_fd < 0 {
if streams.is_stdin_closed() {
streams
.err
.append(&wgettext_fmt!("%s: stdin is closed\n", cmd));
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) {
if argc == optind && isatty(streams.stdin_fd()) {
// Don't implicitly read from the terminal.
streams.err.append(&wgettext_fmt!(
"%s: missing filename argument or input redirection\n",
@@ -52,7 +52,7 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
return Err(STATUS_CMD_ERROR);
}
func_filename = FilenameRef::new(L!("-").to_owned());
fd = streams.stdin_fd;
fd = streams.stdin_fd();
} else {
match wopen_cloexec(args[optind], OFlag::O_RDONLY, Mode::empty()) {
Ok(file) => {
@@ -87,7 +87,7 @@ pub fn source(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> B
// points to the end of argv. Otherwise we want to skip the file name to get to the args if any.
let remaining_args = &args[optind + if argc == optind { 0 } else { 1 }..];
let argv_list = remaining_args.iter().map(|&arg| arg.to_owned()).collect();
parser.vars().set_argv(argv_list);
parser.vars().set_argv(argv_list, parser.is_repainting());
let retval = reader_read(parser, fd, streams.io_chain);

View File

@@ -202,10 +202,6 @@ fn parse_cmd_opts(
streams: &mut IoStreams,
) -> BuiltinResult {
let cmd = args[0];
let mut args_read = Vec::with_capacity(args.len());
args_read.extend_from_slice(args);
let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, args);
while let Some(c) = w.next_opt() {
match c {

View File

@@ -3,9 +3,10 @@
use std::num::NonZeroUsize;
use super::*;
use crate::env::{EnvMode, EnvVar, EnvVarFlags};
use crate::env::{EnvVar, EnvVarFlags};
use crate::flog::flog;
use crate::parse_util::parse_util_unescape_wildcards;
use crate::parser::ParserEnvSetMode;
use crate::wildcard::{ANY_STRING, wildcard_match};
#[derive(Default)]
@@ -145,9 +146,8 @@ fn handle(
..
}) = matcher
{
let vars = parser.vars();
for (name, vals) in first_match_captures.into_iter() {
vars.set(&WString::from(name), EnvMode::default(), vals);
parser.set_var(&WString::from(name), ParserEnvSetMode::default(), vals);
}
}
@@ -295,7 +295,7 @@ fn populate_captures_from_match<'a>(
// empty/null members so we're going to have to use an empty string as the
// sentinel value.
if let Some(m) = cg.as_ref().and_then(|cg| cg.name(&name.to_string())) {
if let Some(m) = cg.as_ref().and_then(|cg| cg.name(&name.clone())) {
captures.push(WString::from(m.as_bytes()));
} else if opts.all {
captures.push(WString::new());

View File

@@ -10,9 +10,9 @@ mod test_expressions {
Error, Options, file_id_for_path, fish_wcswidth, lwstat, waccess, wcstod::wcstod,
wcstoi_opts, wstat,
};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::os::unix::prelude::*;
use std::sync::LazyLock;
#[derive(Copy, Clone, PartialEq, Eq)]
pub(super) enum Token {
@@ -164,7 +164,7 @@ fn isatty(&self, streams: &mut IoStreams) -> bool {
}
let bint = self.base as i32;
if bint == 0 {
match streams.stdin_fd {
match streams.stdin_fd() {
-1 => false,
fd => isatty(fd),
}
@@ -182,7 +182,7 @@ fn token_for_string(str: &wstr) -> Token {
TOKEN_INFOS.get(str).copied().unwrap_or(Token::Unknown)
}
static TOKEN_INFOS: Lazy<HashMap<&'static wstr, Token>> = Lazy::new(|| {
static TOKEN_INFOS: LazyLock<HashMap<&'static wstr, Token>> = LazyLock::new(|| {
let pairs = [
(L!(""), Token::Unknown),
(L!("!"), Token::UnaryBoolean(UnaryBooleanToken::Bang)),

View File

@@ -1,8 +1,7 @@
use super::prelude::*;
use crate::common::bytes2wcstring;
use crate::function;
use crate::highlight::{colorize, highlight_shell};
use crate::highlight::highlight_and_colorize;
use crate::parse_util::{apply_indents, parse_util_compute_indents};
use crate::path::{path_get_path, path_get_paths};
@@ -15,6 +14,7 @@ struct type_cmd_opts_t {
path: bool,
force_path: bool,
query: bool,
color: ColorEnabled,
}
pub fn r#type(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
@@ -34,6 +34,7 @@ pub fn r#type(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> B
wopt(L!("force-path"), ArgType::NoArgument, 'P'),
wopt(L!("query"), ArgType::NoArgument, 'q'),
wopt(L!("quiet"), ArgType::NoArgument, 'q'),
wopt(L!("color"), ArgType::RequiredArgument, COLOR_OPTION_CHAR),
];
let mut w = WGetopter::new(shortopts, longopts, argv);
@@ -68,6 +69,9 @@ pub fn r#type(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> B
builtin_unknown_option(parser, streams, cmd, argv[w.wopt_index - 1], print_hints);
return Err(STATUS_INVALID_ARGS);
}
COLOR_OPTION_CHAR => {
opts.color = ColorEnabled::parse_from_opt(streams, cmd, w.woptarg.unwrap())?;
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
@@ -143,17 +147,12 @@ pub fn r#type(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> B
def = apply_indents(&def, &parse_util_compute_indents(&def));
}
if streams.out_is_terminal() {
let mut color = vec![];
highlight_shell(
if opts.color.enabled(streams) {
streams.out.append(&bytes2wcstring(&highlight_and_colorize(
&def,
&mut color,
&parser.context(),
/*io_ok=*/ false,
/*cursor=*/ None,
);
let col = bytes2wcstring(&colorize(&def, &color, parser.vars()));
streams.out.append(&col);
parser.vars(),
)));
} else {
streams.out.append(&def);
}

View File

@@ -1,9 +1,8 @@
use std::cmp::Ordering;
use std::{cmp::Ordering, sync::LazyLock};
use libc::{RLIM_INFINITY, c_uint, rlim_t};
use nix::errno::Errno;
use nix::sys::resource::Resource as ResourceEnum;
use once_cell::sync::Lazy;
use crate::wutil::perror;
use fish_fallback::{fish_wcswidth, wcscasecmp};
@@ -434,7 +433,7 @@ fn new(
}
/// Array of resource_t structs, describing all known resource types.
static RESOURCE_ARR: Lazy<Box<[Resource]>> = Lazy::new(|| {
static RESOURCE_ARR: LazyLock<Box<[Resource]>> = LazyLock::new(|| {
let resources_info = [
(
limits::SBSIZE,

View File

@@ -20,15 +20,15 @@
use fish_fallback::fish_wcwidth;
use fish_wchar::{decode_byte_from_char, encode_byte_to_char};
use libc::{SIG_IGN, SIGTTOU, STDIN_FILENO};
use once_cell::sync::OnceCell;
use std::cell::{Cell, RefCell};
use std::env;
use std::ffi::{CStr, CString, OsString};
use std::io::Read;
use std::mem;
use std::ops::{Deref, DerefMut};
use std::os::unix::prelude::*;
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
use std::sync::{Arc, MutexGuard};
use std::sync::{Arc, MutexGuard, OnceLock};
use std::time;
pub const BUILD_DIR: &str = env!("FISH_RESOLVED_BUILD_DIR");
@@ -1030,10 +1030,6 @@ pub fn get_omitted_newline_str() -> &'static str {
static OMITTED_NEWLINE_STR: AtomicRef<str> = AtomicRef::new(&"");
pub fn get_omitted_newline_width() -> usize {
OMITTED_NEWLINE_STR.load().len()
}
static OBFUSCATION_READ_CHAR: AtomicU32 = AtomicU32::new(0);
pub fn get_obfuscation_read_char() -> char {
@@ -1044,7 +1040,7 @@ pub fn get_obfuscation_read_char() -> char {
pub static PROFILING_ACTIVE: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
/// Name of the current program. Should be set at startup. Used by the debug function.
pub static PROGRAM_NAME: OnceCell<&'static wstr> = OnceCell::new();
pub static PROGRAM_NAME: OnceLock<&'static wstr> = OnceLock::new();
pub fn get_program_name() -> &'static wstr {
PROGRAM_NAME.get().unwrap()
@@ -1237,6 +1233,23 @@ pub fn read_blocked(fd: RawFd, buf: &mut [u8]) -> nix::Result<usize> {
}
}
pub trait ReadExt {
/// Like [`std::io::Read::read_to_end`], but does not retry on EINTR.
fn read_to_end_interruptible(&mut self, buf: &mut Vec<u8>) -> std::io::Result<()>;
}
impl<T: Read + ?Sized> ReadExt for T {
fn read_to_end_interruptible(&mut self, buf: &mut Vec<u8>) -> std::io::Result<()> {
let mut chunk = [0_u8; 4096];
loop {
match self.read(&mut chunk)? {
0 => return Ok(()),
n => buf.extend_from_slice(&chunk[..n]),
}
}
}
}
/// Test if the string is a valid function name.
pub fn valid_func_name(name: &wstr) -> bool {
!(name.is_empty()
@@ -1951,7 +1964,7 @@ macro_rules! env_stack_set_from_env {
if let Some(var) = std::env::var_os($var_name) {
$vars.set_one(
L!($var_name),
$crate::env::EnvMode::GLOBAL,
$crate::env::EnvSetMode::new_at_early_startup($crate::env::EnvMode::GLOBAL),
$crate::common::bytes2wcstring(var.as_bytes()),
);
}

View File

@@ -4,7 +4,7 @@
mem,
ops::{Deref, DerefMut},
sync::{
Mutex, MutexGuard,
LazyLock, Mutex, MutexGuard,
atomic::{self, AtomicUsize},
},
time::{Duration, Instant},
@@ -32,7 +32,7 @@
parse_util::{
parse_util_cmdsubst_extent, parse_util_process_extent, parse_util_unescape_wildcards,
},
parser::{Block, Parser},
parser::{Block, Parser, ParserEnvSetMode},
parser_keywords::parser_keywords_is_subcommand,
path::{path_get_path, path_try_get_path},
prelude::*,
@@ -55,7 +55,6 @@
};
use bitflags::bitflags;
use fish_wchar::WExt;
use once_cell::sync::Lazy;
// Completion description strings, mostly for different types of files, such as sockets, block
// devices, etc.
@@ -207,7 +206,7 @@ pub fn rank(&self) -> u32 {
/// If this completion replaces the entire token, prepend a prefix. Otherwise do nothing.
pub fn prepend_token_prefix(&mut self, prefix: &wstr) {
if self.flags.contains(CompleteFlags::REPLACES_TOKEN) {
if self.replaces_token() {
self.completion.insert_utfstr(0, prefix)
}
}
@@ -401,7 +400,7 @@ pub fn expected_dash_count(&self) -> usize {
}
/// Last value used in the order field of [`CompletionEntry`].
static complete_order: AtomicUsize = AtomicUsize::new(0);
static COMPLETE_ORDER: AtomicUsize = AtomicUsize::new(0);
struct CompletionEntry {
/// List of all options.
@@ -415,7 +414,7 @@ impl CompletionEntry {
pub fn new() -> Self {
Self {
options: vec![],
order: complete_order.fetch_add(1, atomic::Ordering::Relaxed),
order: COMPLETE_ORDER.fetch_add(1, atomic::Ordering::Relaxed),
}
}
@@ -451,7 +450,7 @@ struct CompletionEntryIndex {
/// Completion "wrapper" support. The map goes from wrapping-command to wrapped-command-list.
type WrapperMap = HashMap<WString, Vec<WString>>;
static WRAPPER_MAP: Lazy<Mutex<WrapperMap>> = Lazy::new(|| Mutex::new(HashMap::new()));
static WRAPPER_MAP: LazyLock<Mutex<WrapperMap>> = LazyLock::new(|| Mutex::new(HashMap::new()));
/// Clear the [`CompleteFlags::AUTO_SPACE`] flag, and set [`CompleteFlags::NO_SPACE`] appropriately
/// depending on the suffix of the string.
@@ -606,8 +605,8 @@ struct Completer<'ctx> {
condition_cache: HashMap<WString, bool>,
}
static completion_autoloader: Lazy<Mutex<Autoload>> =
Lazy::new(|| Mutex::new(Autoload::new(L!("fish_complete_path"))));
static COMPLETION_AUTOLOADER: LazyLock<Mutex<Autoload>> =
LazyLock::new(|| Mutex::new(Autoload::new(L!("fish_complete_path"))));
impl<'ctx> Completer<'ctx> {
pub fn new(ctx: &'ctx OperationContext<'ctx>, flags: CompletionRequestOptions) -> Self {
@@ -1257,7 +1256,7 @@ fn complete_param_for_command(
flog!(complete, "Skipping completions for non-existent command");
} else if let Some(parser) = self.ctx.maybe_parser() {
complete_load(&cmd, parser);
} else if !completion_autoloader
} else if !COMPLETION_AUTOLOADER
.lock()
.unwrap()
.has_attempted_autoload(&cmd)
@@ -1793,7 +1792,7 @@ fn try_complete_user(&mut self, s: &wstr) -> bool {
}
#[cfg(not(target_os = "android"))]
{
static s_setpwent_lock: Mutex<()> = Mutex::new(());
static SETPWENT_LOCK: Mutex<()> = Mutex::new(());
if s.char_at(0) != '~' || s.contains('/') {
return false;
@@ -1817,7 +1816,7 @@ fn getpwent_name() -> Option<WString> {
Some(charptr2wcstring(pw.pw_name))
}
let _guard = s_setpwent_lock.lock().unwrap();
let _guard = SETPWENT_LOCK.lock().unwrap();
unsafe { libc::setpwent() };
while let Some(pw_name) = getpwent_name() {
@@ -1914,9 +1913,11 @@ fn apply_var_assignments<T: AsRef<wstr>>(
} else {
Vec::new()
};
parser
.vars()
.set(variable_name, EnvMode::LOCAL | EnvMode::EXPORT, vals);
parser.set_var(
variable_name,
ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT),
vals,
);
if self.ctx.check_cancel() {
break;
}
@@ -2091,7 +2092,7 @@ fn escape_opening_brackets(completions: &mut [Completion], argument: &wstr) {
return;
};
for comp in completions {
if comp.flags.contains(CompleteFlags::REPLACES_TOKEN) {
if comp.replaces_token() {
continue;
}
comp.flags |= CompleteFlags::REPLACES_TOKEN;
@@ -2126,7 +2127,7 @@ fn mark_completions_duplicating_arguments(
let mut comp_str;
for comp in self.completions.get_list_mut() {
comp_str = comp.completion.clone();
if !comp.flags.contains(CompleteFlags::REPLACES_TOKEN) {
if !comp.replaces_token() {
comp_str.insert_utfstr(0, prefix);
}
if arg_strs.binary_search(&comp_str).is_ok() {
@@ -2479,14 +2480,14 @@ pub fn complete_load(cmd: &wstr, parser: &Parser) -> bool {
// We need to take the lock to decide what to load, drop it to perform the load, then reacquire
// it.
// Note we only look at the global fish_function_path and fish_complete_path.
let path_to_load = completion_autoloader
let path_to_load = COMPLETION_AUTOLOADER
.lock()
.expect("mutex poisoned")
.resolve_command(cmd, EnvStack::globals());
match path_to_load {
AutoloadResult::Path(path_to_load) => {
Autoload::perform_autoload(&path_to_load, parser);
completion_autoloader
COMPLETION_AUTOLOADER
.lock()
.expect("mutex poisoned")
.mark_autoload_finished(cmd);
@@ -2547,7 +2548,7 @@ pub fn complete_invalidate_path() {
// unload any completions that the user may specified on the command line. We should in
// principle track those completions loaded by the autoloader alone.
let cmds = completion_autoloader
let cmds = COMPLETION_AUTOLOADER
.lock()
.expect("mutex poisoned")
.get_autoloaded_commands();
@@ -2630,11 +2631,12 @@ mod tests {
sort_and_prioritize,
};
use crate::abbrs::{self, Abbreviation, with_abbrs_mut};
use crate::env::{EnvMode, Environment};
use crate::env::{EnvMode, EnvSetMode, Environment};
use crate::io::IoChain;
use crate::operation_context::{
EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT, OperationContext, no_cancel,
};
use crate::parser::ParserEnvSetMode;
use crate::prelude::*;
use crate::reader::completion_apply_to_command_line;
use crate::tests::prelude::*;
@@ -2997,7 +2999,7 @@ macro_rules! unique_completion_applies_as {
let completions = do_complete(L!("cat te"), CompletionRequestOptions::default());
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].completion, L!("stfile"));
assert!(!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN)));
assert!(!completions[0].replaces_token());
assert!(
!(completions[0]
.flags
@@ -3014,7 +3016,7 @@ macro_rules! unique_completion_applies_as {
let completions = do_complete(L!("cat testfile TE"), CompletionRequestOptions::default());
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].completion, L!("testfile"));
assert!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN));
assert!(completions[0].replaces_token());
assert!(
completions[0]
.flags
@@ -3221,9 +3223,9 @@ macro_rules! perform_one_completion_cd_test {
// This is to ensure tilde expansion is handled. See the `cd ~/test_autosuggest_suggest_specia`
// test below.
// Fake out the home directory
parser.vars().set_one(
parser.set_one(
L!("HOME"),
EnvMode::LOCAL | EnvMode::EXPORT,
ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT),
L!("test/test-home").to_owned(),
);
std::fs::create_dir_all("test/test-home/test_autosuggest_suggest_special/").unwrap();
@@ -3333,9 +3335,10 @@ macro_rules! perform_one_completion_cd_test {
perform_one_completion_cd_test!("cd ~absolutelynosuchus", "er/");
perform_one_completion_cd_test!("cd ~absolutelynosuchuser/", "path1/");
parser
.vars()
.remove(L!("HOME"), EnvMode::LOCAL | EnvMode::EXPORT);
parser.vars().remove(
L!("HOME"),
EnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT, false),
);
parser.popd();
}

View File

@@ -1,10 +1,10 @@
use crate::common::{BUILD_DIR, get_program_name};
use crate::{flog, flogf};
use fish_build_helper::workspace_root;
use once_cell::sync::OnceCell;
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
/// A struct of configuration directories, determined in main() that fish will optionally pass to
/// env_init.
@@ -163,7 +163,7 @@ pub enum FishPath {
LookUpInPath,
}
static FISH_PATH: OnceCell<FishPath> = OnceCell::new();
static FISH_PATH: OnceLock<FishPath> = OnceLock::new();
/// Get the absolute path to the fish executable itself
pub fn get_fish_path() -> &'static FishPath {

262
src/env/environment.rs vendored
View File

@@ -7,8 +7,8 @@
use crate::builtins::shared::{BuiltinResult, SUCCESS};
use crate::common::{UnescapeStringStyle, bytes2wcstring, unescape_string, wcs2zstring};
use crate::env::config_paths::ConfigPaths;
use crate::env::{EnvMode, EnvVar, Statuses};
use crate::env_dispatch::{env_dispatch_init, env_dispatch_var_change};
use crate::env::{EnvMode, EnvSetMode, EnvVar, Statuses};
use crate::env_dispatch::{VarChangeMilieu, env_dispatch_init, env_dispatch_var_change};
use crate::event::Event;
use crate::flog::flog;
use crate::global_safety::RelaxedAtomicBool;
@@ -28,13 +28,12 @@
use crate::wutil::{fish_wcstol, wgetcwd};
use libc::{c_int, uid_t};
use once_cell::sync::{Lazy, OnceCell};
use std::collections::HashMap;
use std::ffi::CStr;
use std::mem::MaybeUninit;
use std::os::unix::prelude::*;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::{Arc, LazyLock, OnceLock};
/// Set when a universal variable has been modified but not yet been written to disk via sync().
static UVARS_LOCALLY_MODIFIED: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
@@ -212,7 +211,7 @@ pub fn set_last_statuses(&self, statuses: Statuses) {
}
/// Sets the variable with the specified name to the given values.
pub fn set(&self, key: &wstr, mode: EnvMode, mut vals: Vec<WString>) -> EnvStackSetResult {
pub fn set(&self, key: &wstr, mode: EnvSetMode, mut vals: Vec<WString>) -> EnvStackSetResult {
// Historical behavior.
if vals.len() == 1 && (key == "PWD" || key == "HOME") {
path_make_canonical(vals.first_mut().unwrap());
@@ -238,7 +237,14 @@ pub fn set(&self, key: &wstr, mode: EnvMode, mut vals: Vec<WString>) -> EnvStack
// Dispatch changes if we modified the global state or have 'dispatches_var_changes' set.
// Important to not hold the lock here.
if ret.global_modified || self.dispatches_var_changes {
env_dispatch_var_change(key, self);
env_dispatch_var_change(
VarChangeMilieu {
is_repainting: mode.is_repainting,
global_or_universal: ret.global_modified || ret.uvar_modified,
},
key,
self,
);
}
}
// Mark if we modified a uvar.
@@ -249,12 +255,12 @@ pub fn set(&self, key: &wstr, mode: EnvMode, mut vals: Vec<WString>) -> EnvStack
}
/// Sets the variable with the specified name to a single value.
pub fn set_one(&self, key: &wstr, mode: EnvMode, val: WString) -> EnvStackSetResult {
pub fn set_one(&self, key: &wstr, mode: EnvSetMode, val: WString) -> EnvStackSetResult {
self.set(key, mode, vec![val])
}
/// Sets the variable with the specified name to no values.
pub fn set_empty(&self, key: &wstr, mode: EnvMode) -> EnvStackSetResult {
pub fn set_empty(&self, key: &wstr, mode: EnvSetMode) -> EnvStackSetResult {
self.set(key, mode, Vec::new())
}
@@ -269,24 +275,32 @@ pub fn set_pwd_from_getcwd(&self) {
)
);
}
self.set_one(L!("PWD"), EnvMode::EXPORT | EnvMode::GLOBAL, cwd);
let global_exported_mode =
EnvSetMode::new_at_early_startup(EnvMode::GLOBAL | EnvMode::EXPORT);
self.set_one(L!("PWD"), global_exported_mode, cwd);
}
/// Remove environment variable.
///
/// \param key The name of the variable to remove
/// \param mode should be ENV_USER if this is a remove request from the user, 0 otherwise. If
/// this is a user request, read-only variables can not be removed. The mode may also specify
/// the scope of the variable that should be erased.
/// \param mode If this is a user request, read-only variables can not be removed. The mode
/// may also specify the scope of the variable that should be erased.
///
/// Return the set result.
pub fn remove(&self, key: &wstr, mode: EnvMode) -> EnvStackSetResult {
pub fn remove(&self, key: &wstr, mode: EnvSetMode) -> EnvStackSetResult {
let ret = self.lock().remove(key, mode);
#[allow(clippy::collapsible_if)]
if ret.status == EnvStackSetResult::Ok {
if ret.global_modified || self.dispatches_var_changes {
// Important to not hold the lock here.
env_dispatch_var_change(key, self);
env_dispatch_var_change(
VarChangeMilieu {
is_repainting: mode.is_repainting,
global_or_universal: ret.global_modified || ret.uvar_modified,
},
key,
self,
);
}
}
if ret.uvar_modified {
@@ -307,14 +321,21 @@ pub fn push(&self, new_scope: bool) {
}
/// Pop the variable stack. Used for implementing local variables for functions and for-loops.
pub fn pop(&self) {
pub fn pop(&self, is_repainting: bool) {
assert!(self.can_push_pop, "push/pop not allowed on global stack");
let popped = self.lock().pop();
if self.dispatches_var_changes {
// TODO: we would like to coalesce locale changes, so that we only re-initialize
// once.
for key in popped {
env_dispatch_var_change(&key, self);
env_dispatch_var_change(
VarChangeMilieu {
is_repainting,
global_or_universal: false,
},
&key,
self,
);
}
}
}
@@ -335,7 +356,7 @@ pub fn snapshot(&self) -> EnvDyn {
/// If `always` is set, perform synchronization even if there's no pending changes from this
/// instance (that is, look for changes from other fish instances).
/// Return a list of events for changed variables.
pub fn universal_sync(&self, always: bool) -> Vec<Event> {
pub fn universal_sync(&self, always: bool, is_repainting: bool) -> Vec<Event> {
if UVAR_SCOPE_IS_GLOBAL.load() {
return Vec::new();
}
@@ -353,7 +374,14 @@ pub fn universal_sync(&self, always: bool) -> Vec<Event> {
if let Some(callbacks) = callbacks {
for callback in callbacks {
let name = callback.key;
env_dispatch_var_change(&name, self);
env_dispatch_var_change(
VarChangeMilieu {
is_repainting,
global_or_universal: true,
},
&name,
self,
);
let evt = if callback.val.is_none() {
Event::variable_erase(name)
} else {
@@ -378,8 +406,12 @@ pub fn globals() -> &'static EnvStack {
})
}
pub fn set_argv(&self, argv: Vec<WString>) {
self.set(L!("argv"), EnvMode::LOCAL, argv);
pub fn set_argv(&self, argv: Vec<WString>, is_repainting: bool) {
self.set(
L!("argv"),
EnvSetMode::new(EnvMode::LOCAL, is_repainting),
argv,
);
}
}
@@ -478,7 +510,7 @@ pub fn get_home() -> Option<String> {
}
/// Set up the USER and HOME variable.
fn setup_user(vars: &EnvStack) {
fn setup_user(global_exported_mode: EnvSetMode, vars: &EnvStack) {
let uid: uid_t = geteuid();
let user_var = vars.get_unless_empty(L!("USER"));
@@ -508,11 +540,11 @@ fn setup_user(vars: &EnvStack) {
let s = unsafe { CStr::from_ptr(userinfo.pw_dir) };
vars.set_one(
L!("HOME"),
EnvMode::GLOBAL | EnvMode::EXPORT,
global_exported_mode,
bytes2wcstring(s.to_bytes()),
);
} else {
vars.set_empty(L!("HOME"), EnvMode::GLOBAL | EnvMode::EXPORT);
vars.set_empty(L!("HOME"), global_exported_mode);
}
}
return;
@@ -535,7 +567,7 @@ fn setup_user(vars: &EnvStack) {
let userinfo = unsafe { userinfo.assume_init() };
let s = unsafe { CStr::from_ptr(userinfo.pw_name) };
let uname = bytes2wcstring(s.to_bytes());
vars.set_one(L!("USER"), EnvMode::GLOBAL | EnvMode::EXPORT, uname);
vars.set_one(L!("USER"), global_exported_mode, uname);
// Only change $HOME if it's empty, so we allow e.g. `HOME=(mktemp -d)`.
// This is okay with common `su` and `sudo` because they set $HOME.
if vars.get_unless_empty(L!("HOME")).is_none() {
@@ -543,22 +575,22 @@ fn setup_user(vars: &EnvStack) {
let s = unsafe { CStr::from_ptr(userinfo.pw_dir) };
vars.set_one(
L!("HOME"),
EnvMode::GLOBAL | EnvMode::EXPORT,
global_exported_mode,
bytes2wcstring(s.to_bytes()),
);
} else {
// We cannot get $HOME. This triggers warnings for history and config.fish already,
// so it isn't necessary to warn here as well.
vars.set_empty(L!("HOME"), EnvMode::GLOBAL | EnvMode::EXPORT);
vars.set_empty(L!("HOME"), global_exported_mode);
}
}
} else if vars.get_unless_empty(L!("HOME")).is_none() {
// If $USER is empty as well (which we tried to set above), we can't get $HOME.
vars.set_empty(L!("HOME"), EnvMode::GLOBAL | EnvMode::EXPORT);
vars.set_empty(L!("HOME"), global_exported_mode);
}
}
pub(crate) static FALLBACK_PATH: Lazy<&[WString]> = Lazy::new(|| {
pub(crate) static FALLBACK_PATH: LazyLock<&[WString]> = LazyLock::new(|| {
// _CS_PATH: colon-separated paths to find POSIX utilities. Same as USER_CS_PATH.
let cs_path = libc::_CS_PATH;
@@ -581,25 +613,24 @@ fn setup_user(vars: &EnvStack) {
});
/// Make sure the PATH variable contains something.
fn setup_path() {
fn setup_path(global_exported_mode: EnvSetMode) {
let vars = EnvStack::globals();
let path = vars.get_unless_empty(L!("PATH"));
if path.is_none() {
vars.set(
L!("PATH"),
EnvMode::GLOBAL | EnvMode::EXPORT,
FALLBACK_PATH.to_vec(),
);
vars.set(L!("PATH"), global_exported_mode, FALLBACK_PATH.to_vec());
}
}
/// The originally inherited variables and their values.
/// This is a simple key->value map and not e.g. cut into paths.
pub static INHERITED_VARS: OnceCell<HashMap<WString, WString>> = OnceCell::new();
pub static INHERITED_VARS: OnceLock<HashMap<WString, WString>> = OnceLock::new();
pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool) {
let vars = EnvStack::globals();
let global_mode = EnvSetMode::new_at_early_startup(EnvMode::GLOBAL);
let global_exported_mode = EnvSetMode::new_at_early_startup(EnvMode::GLOBAL | EnvMode::EXPORT);
let env_iter: Vec<_> = std::env::vars_os()
.map(|(k, v)| (bytes2wcstring(k.as_bytes()), bytes2wcstring(v.as_bytes())))
.collect();
@@ -619,7 +650,7 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
// a value we previously (due to user error) exported will cause impossibly
// difficult to debug PATH problems.
if key != "fish_user_paths" {
vars.set(&key, EnvMode::EXPORT | EnvMode::GLOBAL, vec![val.clone()]);
vars.set(&key, global_exported_mode, vec![val.clone()]);
}
}
inherited_vars.insert(key, val);
@@ -631,14 +662,14 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
// Set $USER, $HOME and $EUID
// This involves going to passwd and stuff.
vars.set_one(L!("EUID"), EnvMode::GLOBAL, geteuid().to_wstring());
setup_user(vars);
vars.set_one(L!("EUID"), global_mode, geteuid().to_wstring());
setup_user(global_exported_mode, vars);
if let Some(paths) = paths {
let set_path = |key: &wstr, maybe_path: Option<&PathBuf>| {
vars.set(
key,
EnvMode::GLOBAL,
global_mode,
maybe_path
.map(|path| vec![bytes2wcstring(path.as_os_str().as_bytes())])
.unwrap_or_default(),
@@ -656,45 +687,49 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
let user_config_dir = path_get_config();
vars.set_one(
FISH_CONFIG_DIR,
EnvMode::GLOBAL,
global_mode,
user_config_dir.unwrap_or_default(),
);
let user_data_dir = path_get_data();
vars.set_one(
FISH_USER_DATA_DIR,
EnvMode::GLOBAL,
global_mode,
user_data_dir.unwrap_or_default(),
);
let user_cache_dir = path_get_cache();
vars.set_one(
FISH_CACHE_DIR,
EnvMode::GLOBAL,
global_mode,
user_cache_dir.unwrap_or_default(),
);
// Set up a default PATH
setup_path();
setup_path(global_exported_mode);
// Set up $IFS - this used to be in share/config.fish, but really breaks if it isn't done.
vars.set_one(L!("IFS"), EnvMode::GLOBAL, "\n \t".into());
vars.set_one(L!("IFS"), global_mode, "\n \t".into());
// Ensure this var is present even before an interactive command is run so that if it is used
// in a function like `fish_prompt` or `fish_right_prompt` it is defined at the time the first
// prompt is written.
vars.set_one(L!("CMD_DURATION"), EnvMode::UNEXPORT, "0".into());
vars.set_one(
L!("CMD_DURATION"),
EnvSetMode::new_at_early_startup(EnvMode::UNEXPORT),
"0".into(),
);
// Set up the version variable.
let version = bytes2wcstring(crate::BUILD_VERSION.as_bytes());
vars.set_one(L!("version"), EnvMode::GLOBAL, version.clone());
vars.set_one(L!("FISH_VERSION"), EnvMode::GLOBAL, version);
vars.set_one(L!("version"), global_mode, version.clone());
vars.set_one(L!("FISH_VERSION"), global_mode, version);
// Set the $fish_pid variable.
vars.set_one(L!("fish_pid"), EnvMode::GLOBAL, getpid().to_wstring());
vars.set_one(L!("fish_pid"), global_mode, getpid().to_wstring());
// Set the $hostname variable
let hostname: WString = get_hostname_identifier().unwrap_or("fish".into());
vars.set_one(L!("hostname"), EnvMode::GLOBAL, hostname);
vars.set_one(L!("hostname"), global_mode, hostname);
// Set up SHLVL variable. Not we can't use vars.get() because SHLVL is read-only, and therefore
// was not inherited from the environment.
@@ -709,13 +744,13 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
} else {
L!("1").to_owned()
};
vars.set_one(L!("SHLVL"), EnvMode::GLOBAL | EnvMode::EXPORT, nshlvl_str);
vars.set_one(L!("SHLVL"), global_exported_mode, nshlvl_str);
} else {
// If we're not interactive, simply pass the value along.
if let Some(shlvl_var) = std::env::var_os("SHLVL") {
vars.set_one(
L!("SHLVL"),
EnvMode::GLOBAL | EnvMode::EXPORT,
global_exported_mode,
bytes2wcstring(shlvl_var.as_os_str().as_bytes()),
);
}
@@ -736,7 +771,7 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
&& incoming_pwd.char_at(0) == '/'
&& paths_are_same_file(&incoming_pwd, L!("."))
{
vars.set_one(L!("PWD"), EnvMode::EXPORT | EnvMode::GLOBAL, incoming_pwd);
vars.set_one(L!("PWD"), global_exported_mode, incoming_pwd);
} else {
vars.set_pwd_from_getcwd();
}
@@ -744,18 +779,14 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
// Initialize termsize variables.
let termsize = termsize::SHARED_CONTAINER.initialize(vars as &dyn Environment);
if vars.get_unless_empty(L!("COLUMNS")).is_none() {
vars.set_one(
L!("COLUMNS"),
EnvMode::GLOBAL,
termsize.width().to_wstring(),
);
vars.set_one(L!("COLUMNS"), global_mode, termsize.width().to_wstring());
}
if vars.get_unless_empty(L!("LINES")).is_none() {
vars.set_one(L!("LINES"), EnvMode::GLOBAL, termsize.height().to_wstring());
vars.set_one(L!("LINES"), global_mode, termsize.height().to_wstring());
}
// Set fish_bind_mode to "default".
vars.set_one(FISH_BIND_MODE_VAR, EnvMode::GLOBAL, "default".into());
vars.set_one(FISH_BIND_MODE_VAR, global_mode, "default".into());
// Allow changes to variables to produce events.
env_dispatch_init(vars);
@@ -770,65 +801,74 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
if !do_uvars {
UVAR_SCOPE_IS_GLOBAL.store(true);
} else {
// Set up universal variables using the default path.
let callbacks = uvars().initialize().unwrap_or_default();
for callback in callbacks {
env_dispatch_var_change(&callback.key, vars);
}
return;
}
// Do not import variables that have the same name and value as
// an exported universal variable. See issues #5258 and #5348.
let globals_to_skip = {
let mut to_skip = vec![];
let uvars_locked = uvars();
for (name, uvar) in uvars_locked.get_table() {
if !uvar.exports() {
continue;
}
// Set up universal variables using the default path.
let callbacks = uvars().initialize().unwrap_or_default();
for callback in callbacks {
env_dispatch_var_change(
VarChangeMilieu {
is_repainting: false,
global_or_universal: true,
},
&callback.key,
vars,
);
}
// Look for a global exported variable with the same name.
let global = EnvStack::globals().getf(name, EnvMode::GLOBAL | EnvMode::EXPORT);
if global.is_some_and(|x| x.as_string() == uvar.as_string()) {
to_skip.push(name.to_owned());
}
}
to_skip
};
for name in &globals_to_skip {
EnvStack::globals().remove(name, EnvMode::GLOBAL | EnvMode::EXPORT);
}
// Import any abbreviations from uvars.
// Note we do not dynamically react to changes.
let prefix = L!("_fish_abbr_");
let prefix_len = prefix.char_count();
let from_universal = true;
let mut abbrs = abbrs_get_set();
// Do not import variables that have the same name and value as
// an exported universal variable. See issues #5258 and #5348.
let globals_to_skip = {
let mut to_skip = vec![];
let uvars_locked = uvars();
for (name, uvar) in uvars_locked.get_table() {
if !name.starts_with(prefix) {
if !uvar.exports() {
continue;
}
let escaped_name = name.slice_from(prefix_len);
if let Some(name) = unescape_string(escaped_name, UnescapeStringStyle::Var) {
let key = name.clone();
let replacement: WString = join_strings(uvar.as_list(), ' ');
abbrs.add(Abbreviation::new(
name,
key,
replacement,
Position::Command,
from_universal,
));
// Look for a global exported variable with the same name.
let global = EnvStack::globals().getf(name, EnvMode::GLOBAL | EnvMode::EXPORT);
if global.is_some_and(|x| x.as_string() == uvar.as_string()) {
to_skip.push(name.to_owned());
}
}
to_skip
};
for name in &globals_to_skip {
EnvStack::globals().remove(name, global_exported_mode);
}
// Import any abbreviations from uvars.
// Note we do not dynamically react to changes.
let prefix = L!("_fish_abbr_");
let prefix_len = prefix.char_count();
let from_universal = true;
let mut abbrs = abbrs_get_set();
let uvars_locked = uvars();
for (name, uvar) in uvars_locked.get_table() {
if !name.starts_with(prefix) {
continue;
}
let escaped_name = name.slice_from(prefix_len);
if let Some(name) = unescape_string(escaped_name, UnescapeStringStyle::Var) {
let key = name.clone();
let replacement: WString = join_strings(uvar.as_list(), ' ');
abbrs.add(Abbreviation::new(
name,
key,
replacement,
Position::Command,
from_universal,
));
}
}
}
#[cfg(test)]
mod tests {
use super::{EnvMode, EnvStack, Environment};
use crate::env::EnvSetMode;
use crate::prelude::*;
use crate::tests::prelude::*;
@@ -844,19 +884,19 @@ fn test_env_snapshot() {
let before_pwd = vars.get(L!("PWD")).unwrap().as_string();
vars.set_one(
L!("test_env_snapshot_var"),
EnvMode::default(),
EnvSetMode::default(),
L!("before").to_owned(),
);
let snapshot = vars.snapshot();
vars.set_one(L!("PWD"), EnvMode::default(), L!("/newdir").to_owned());
vars.set_one(L!("PWD"), EnvSetMode::default(), L!("/newdir").to_owned());
vars.set_one(
L!("test_env_snapshot_var"),
EnvMode::default(),
EnvSetMode::default(),
L!("after").to_owned(),
);
vars.set_one(
L!("test_env_snapshot_var_2"),
EnvMode::default(),
EnvSetMode::default(),
L!("after").to_owned(),
);
@@ -885,7 +925,7 @@ fn test_env_snapshot() {
// snapshots see global var changes except for perproc like PWD
vars.set_one(
L!("test_env_snapshot_var_3"),
EnvMode::GLOBAL,
EnvSetMode::new(EnvMode::GLOBAL, false),
L!("reallyglobal").to_owned(),
);
assert_eq!(
@@ -900,7 +940,7 @@ fn test_env_snapshot() {
L!("reallyglobal")
);
vars.pop();
vars.pop(false);
parser.popd();
}
@@ -914,6 +954,6 @@ fn test_no_global_push() {
#[test]
#[should_panic]
fn test_no_global_pop() {
EnvStack::globals().pop();
EnvStack::globals().pop(false);
}
}

View File

@@ -1,6 +1,6 @@
use crate::common::wcs2zstring;
use crate::env::{
ELECTRIC_VARIABLES, ElectricVar, EnvMode, EnvStackSetResult, EnvVar, EnvVarFlags,
ELECTRIC_VARIABLES, ElectricVar, EnvMode, EnvSetMode, EnvStackSetResult, EnvVar, EnvVarFlags,
PATH_ARRAY_SEP, Statuses, VarTable, is_read_only,
};
use crate::env_universal_common::EnvUniversal;
@@ -15,13 +15,13 @@
use crate::threads::{is_forked_child, is_main_thread};
use crate::wutil::fish_wcstol_radix;
use once_cell::sync::Lazy;
use std::cell::{RefCell, UnsafeCell};
use std::collections::HashSet;
use std::ffi::CString;
use std::marker::PhantomData;
use std::mem;
use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
#[cfg(not(target_has_atomic = "64"))]
use portable_atomic::AtomicU64;
@@ -116,11 +116,20 @@ struct Query {
pub user: bool,
}
impl From<EnvMode> for Query {
fn from(mode: EnvMode) -> Self {
Self::new(mode, false)
}
}
impl From<EnvSetMode> for Query {
fn from(mode: EnvSetMode) -> Self {
Self::new(mode.mode, mode.user)
}
}
impl Query {
/// Creates a `Query` from env mode flags.
fn new(mode: EnvMode) -> Self {
let has_scope = mode
.intersects(EnvMode::LOCAL | EnvMode::FUNCTION | EnvMode::GLOBAL | EnvMode::UNIVERSAL);
fn new(mode: EnvMode, user: bool) -> Self {
let has_scope = mode.intersects(EnvMode::ANY_SCOPE);
let has_export_unexport = mode.intersects(EnvMode::EXPORT | EnvMode::UNEXPORT);
Query {
has_scope,
@@ -138,7 +147,7 @@ fn new(mode: EnvMode) -> Self {
pathvar: mode.contains(EnvMode::PATHVAR),
unpathvar: mode.contains(EnvMode::UNPATHVAR),
user: mode.contains(EnvMode::USER),
user,
}
}
@@ -285,7 +294,7 @@ fn next(&mut self) -> Option<EnvNodeRef> {
}
}
static GLOBAL_NODE: Lazy<EnvNodeRef> = Lazy::new(|| EnvNodeRef::new(false, None));
static GLOBAL_NODE: LazyLock<EnvNodeRef> = LazyLock::new(|| EnvNodeRef::new(false, None));
/// Recursive helper to snapshot a series of nodes.
fn copy_node_chain(node: &EnvNodeRef) -> EnvNodeRef {
@@ -459,7 +468,7 @@ fn try_get_universal(&self, key: &wstr) -> Option<EnvVar> {
}
pub fn getf(&self, key: &wstr, mode: EnvMode) -> Option<EnvVar> {
let query = Query::new(mode);
let query = Query::from(mode);
let mut result: Option<EnvVar> = None;
// Computed variables are effectively global and can't be shadowed.
if query.global {
@@ -489,7 +498,7 @@ pub fn getf(&self, key: &wstr, mode: EnvMode) -> Option<EnvVar> {
}
pub fn get_names(&self, flags: EnvMode) -> Vec<WString> {
let query = Query::new(flags);
let query = Query::from(flags);
let mut names: HashSet<WString> = HashSet::new();
// Helper to add the names of variables from `envs` to names, respecting show_exported and
@@ -586,7 +595,7 @@ fn export_array_needs_regeneration(&self) -> bool {
let mut cursor = self.export_array_generations.iter().fuse();
let mut mismatch = false;
self.enumerate_generations(|r#gen| {
if cursor.next().cloned() != Some(r#gen) {
if cursor.next().copied() != Some(r#gen) {
mismatch = true;
}
});
@@ -721,8 +730,8 @@ pub fn new() -> EnvMutex<EnvStackImpl> {
}
/// Set a variable under the name `key`, using the given `mode`, setting its value to `val`.
pub fn set(&mut self, key: &wstr, mode: EnvMode, mut val: Vec<WString>) -> ModResult {
let query = Query::new(mode);
pub fn set(&mut self, key: &wstr, mode: EnvSetMode, mut val: Vec<WString>) -> ModResult {
let query = Query::from(mode);
// Handle electric and read-only variables.
if let Some(ret) = self.try_set_electric(key, &query, &mut val) {
return ModResult::new(ret);
@@ -754,6 +763,7 @@ pub fn set(&mut self, key: &wstr, mode: EnvMode, mut val: Vec<WString>) -> ModRe
result.uvar_modified = true;
} else if query.global || (query.universal && UVAR_SCOPE_IS_GLOBAL.load()) {
Self::set_in_node(&mut self.base.globals, key, val, flags);
result.global_modified = true;
} else if query.local {
assert!(
!self.base.locals.ptr_eq(&self.base.globals),
@@ -802,8 +812,8 @@ pub fn set(&mut self, key: &wstr, mode: EnvMode, mut val: Vec<WString>) -> ModRe
}
/// Remove a variable under the name `key`.
pub fn remove(&mut self, key: &wstr, mode: EnvMode) -> ModResult {
let query = Query::new(mode);
pub fn remove(&mut self, key: &wstr, mode: EnvSetMode) -> ModResult {
let query = Query::from(mode);
// Users can't remove read-only keys.
if query.user && is_read_only(key) {
return ModResult::new(EnvStackSetResult::Scope);

48
src/env/var.rs vendored
View File

@@ -31,20 +31,51 @@ pub struct EnvMode: u16 {
const PATHVAR = 1 << 6;
/// Flag to unmark a variable as a path variable.
const UNPATHVAR = 1 << 7;
/// Flag for variable update request from the user. All variable changes that are made directly
/// by the user, such as those from the `read` and `set` builtin must have this flag set. It
/// serves one purpose: to indicate that an error should be returned if the user is attempting
/// to modify a var that should not be modified by direct user action; e.g., a read-only var.
const USER = 1 << 8;
}
}
impl EnvMode {
pub const ANY_SCOPE: EnvMode = EnvMode::LOCAL
.union(EnvMode::FUNCTION)
.union(EnvMode::GLOBAL)
.union(EnvMode::UNIVERSAL);
}
impl From<EnvMode> for u16 {
fn from(val: EnvMode) -> Self {
val.bits()
}
}
#[derive(Copy, Clone, Default)]
pub struct EnvSetMode {
pub mode: EnvMode,
/// Flag for variable update request from the user. All variable changes that are made directly
/// by the user, such as those from the `read` and `set` builtin must have this flag set. It
/// serves to indicate that an error should be returned if the user is attempting to modify
/// a var that should not be modified by direct user action; e.g., a read-only var.
pub user: bool,
pub is_repainting: bool,
}
impl EnvSetMode {
pub fn new(mode: EnvMode, is_repainting: bool) -> Self {
Self::new_with(mode, false, is_repainting)
}
pub fn new_with(mode: EnvMode, user: bool, is_repainting: bool) -> Self {
Self {
mode,
user,
is_repainting,
}
}
pub fn new_at_early_startup(mode: EnvMode) -> Self {
Self::new_with(mode, false, false)
}
}
/// A collection of status and pipestatus.
#[derive(Clone, Debug)]
pub struct Statuses {
@@ -287,6 +318,7 @@ pub fn is_read_only(name: &wstr) -> bool {
#[cfg(test)]
mod tests {
use super::{EnvMode, EnvVar, EnvVarFlags};
use crate::env::EnvSetMode;
use crate::env::environment::{EnvStack, Environment};
use crate::prelude::*;
use crate::tests::prelude::*;
@@ -299,7 +331,11 @@ mod tests {
fn return_timezone_hour(tstamp: SystemTime, timezone: &wstr) -> libc::c_int {
let vars = EnvStack::globals().create_child(true /* dispatches_var_changes */);
vars.set_one(L!("TZ"), EnvMode::EXPORT, timezone.to_owned());
vars.set_one(
L!("TZ"),
EnvSetMode::new(EnvMode::EXPORT, false),
timezone.to_owned(),
);
let _var = vars.get(L!("TZ"));

View File

@@ -8,7 +8,8 @@
use crate::prelude::*;
use crate::reader::{
reader_change_cursor_end_mode, reader_change_cursor_selection_mode, reader_change_history,
reader_schedule_prompt_repaint, reader_set_autosuggestion_enabled, reader_set_transient_prompt,
reader_current_data, reader_schedule_prompt_repaint, reader_set_autosuggestion_enabled,
reader_set_transient_prompt,
};
use crate::screen::{
IS_DUMB, LAYOUT_CACHE_SHARED, ONLY_GRAYSCALE, screen_set_midnight_commander_hack,
@@ -47,8 +48,14 @@
once_cell::sync::Lazy::new(|| {
let mut table = VarDispatchTable::default();
macro_rules! vars {
( $f:ident ) => {
|vars: &EnvStack, _suppress_repaint: bool| $f(vars)
};
}
for name in LOCALE_VARIABLES {
table.add_anon(name, handle_locale_change);
table.add_anon(name, vars!(handle_locale_change));
}
for name in CURSES_VARIABLES {
@@ -59,43 +66,49 @@
table.add_anon(L!("COLORTERM"), handle_fish_term_change);
table.add_anon(L!("fish_term256"), handle_fish_term_change);
table.add_anon(L!("fish_term24bit"), handle_fish_term_change);
table.add_anon(L!("fish_escape_delay_ms"), update_wait_on_escape_ms);
table.add_anon(L!("fish_escape_delay_ms"), vars!(update_wait_on_escape_ms));
table.add_anon(
L!("fish_sequence_key_delay_ms"),
update_wait_on_sequence_key_ms,
vars!(update_wait_on_sequence_key_ms),
);
table.add_anon(L!("fish_emoji_width"), guess_emoji_width);
table.add_anon(L!("fish_ambiguous_width"), handle_change_ambiguous_width);
table.add_anon(L!("LINES"), handle_term_size_change);
table.add_anon(L!("COLUMNS"), handle_term_size_change);
table.add_anon(L!("fish_complete_path"), handle_complete_path_change);
table.add_anon(L!("fish_function_path"), handle_function_path_change);
table.add_anon(L!("fish_read_limit"), handle_read_limit_change);
table.add_anon(L!("fish_history"), handle_fish_history_change);
table.add_anon(L!("fish_emoji_width"), vars!(guess_emoji_width));
table.add_anon(
L!("fish_ambiguous_width"),
vars!(handle_change_ambiguous_width),
);
table.add_anon(L!("LINES"), vars!(handle_term_size_change));
table.add_anon(L!("COLUMNS"), vars!(handle_term_size_change));
table.add_anon(L!("fish_complete_path"), vars!(handle_complete_path_change));
table.add_anon(L!("fish_function_path"), vars!(handle_function_path_change));
table.add_anon(L!("fish_read_limit"), vars!(handle_read_limit_change));
table.add_anon(L!("fish_history"), vars!(handle_fish_history_change));
table.add_anon(
L!("fish_autosuggestion_enabled"),
handle_autosuggestion_change,
vars!(handle_autosuggestion_change),
);
table.add_anon(
L!("fish_transient_prompt"),
vars!(handle_transient_prompt_change),
);
table.add_anon(L!("fish_transient_prompt"), handle_transient_prompt_change);
table.add_anon(
L!("fish_use_posix_spawn"),
handle_fish_use_posix_spawn_change,
vars!(handle_fish_use_posix_spawn_change),
);
table.add_anon(L!("fish_trace"), handle_fish_trace);
table.add_anon(L!("fish_trace"), vars!(handle_fish_trace));
table.add_anon(
L!("fish_cursor_selection_mode"),
handle_fish_cursor_selection_mode_change,
vars!(handle_fish_cursor_selection_mode_change),
);
table.add_anon(
L!("fish_cursor_end_mode"),
handle_fish_cursor_end_mode_change,
vars!(handle_fish_cursor_end_mode_change),
);
table
});
type NamedEnvCallback = fn(name: &wstr, env: &EnvStack);
type AnonEnvCallback = fn(env: &EnvStack);
type AnonEnvCallback = fn(env: &EnvStack, suppress_repaint: bool);
enum EnvCallback {
Named(NamedEnvCallback),
@@ -120,10 +133,10 @@ pub fn add_anon(&mut self, name: &'static wstr, callback: AnonEnvCallback) {
assert!(prev.is_none(), "Already observing {}", name);
}
pub fn dispatch(&self, key: &wstr, vars: &EnvStack) {
pub fn dispatch(&self, key: &wstr, vars: &EnvStack, suppress_repaint: bool) {
match self.table.get(key) {
Some(EnvCallback::Named(named)) => (named)(key, vars),
Some(EnvCallback::Anon(anon)) => (anon)(vars),
Some(EnvCallback::Anon(anon)) => (anon)(vars, suppress_repaint),
None => (),
}
}
@@ -206,26 +219,41 @@ pub fn guess_emoji_width(vars: &EnvStack) {
}
}
pub struct VarChangeMilieu {
pub is_repainting: bool,
pub global_or_universal: bool,
}
/// React to modifying the given variable.
pub fn env_dispatch_var_change(key: &wstr, vars: &EnvStack) {
pub fn env_dispatch_var_change(milieu: VarChangeMilieu, key: &wstr, vars: &EnvStack) {
use once_cell::sync::Lazy;
let suppress_repaint = milieu.is_repainting || !milieu.global_or_universal;
// We want to ignore variable changes until the dispatch table is explicitly initialized.
// TODO(MSRV>=1.94): Use std::sync::LazyLock. (LazyLock::get is stabilized in Rust 1.94)
if let Some(dispatch_table) = Lazy::get(&VAR_DISPATCH_TABLE) {
dispatch_table.dispatch(key, vars);
dispatch_table.dispatch(key, vars, suppress_repaint);
}
if string_prefixes_string(L!("fish_color_"), key)
// TODO Don't re-exec prompt when only pager color changed.
|| string_prefixes_string(L!("fish_pager_color_"), key)
{
reader_schedule_prompt_repaint();
// TODO(MSRV>=1.88): if-let
if !suppress_repaint {
if let Some(data) = reader_current_data() {
if string_prefixes_string(L!("fish_color_"), key) || {
// TODO Don't re-exec prompt when only pager color changed.
string_prefixes_string(L!("fish_pager_color_"), key)
} {
data.schedule_prompt_repaint();
}
}
}
}
fn handle_fish_term_change(vars: &EnvStack) {
fn handle_fish_term_change(vars: &EnvStack, suppress_repaint: bool) {
update_fish_color_support(vars);
reader_schedule_prompt_repaint();
if !suppress_repaint {
reader_schedule_prompt_repaint();
}
}
fn handle_change_ambiguous_width(vars: &EnvStack) {
@@ -307,11 +335,13 @@ fn handle_locale_change(vars: &EnvStack) {
init_locale(vars);
}
fn handle_term_change(vars: &EnvStack) {
fn handle_term_change(vars: &EnvStack, suppress_repaint: bool) {
guess_emoji_width(vars);
init_terminal(vars);
read_terminfo_database(vars);
reader_schedule_prompt_repaint();
if !suppress_repaint {
reader_schedule_prompt_repaint();
}
}
fn handle_fish_use_posix_spawn_change(vars: &EnvStack) {

View File

@@ -862,7 +862,7 @@ fn test_universal() {
let mut handles = Vec::new();
for i in 0..threads {
let path = test_path.to_owned();
let path = test_path.clone();
handles.push(std::thread::spawn(move || {
test_universal_helper(i, &path);
}));
@@ -873,7 +873,7 @@ fn test_universal() {
}
let mut uvars = EnvUniversal::new();
uvars.initialize_at_path(test_path.to_owned());
uvars.initialize_at_path(test_path.clone());
for i in 0..threads {
for j in 0..UVARS_PER_THREAD {
@@ -1037,11 +1037,11 @@ fn test_universal_callbacks() {
let mut uvars1 = EnvUniversal::new();
let mut uvars2 = EnvUniversal::new();
let mut callbacks = uvars1
.initialize_at_path(test_path.to_owned())
.initialize_at_path(test_path.clone())
.unwrap_or_default();
callbacks.append(
&mut uvars2
.initialize_at_path(test_path.to_owned())
.initialize_at_path(test_path.clone())
.unwrap_or_default(),
);
@@ -1134,7 +1134,7 @@ fn test_universal_ok_to_save() {
let mut uvars = EnvUniversal::new();
uvars
.initialize_at_path(test_path.to_owned())
.initialize_at_path(test_path.clone())
.unwrap_or_default();
assert!(!uvars.is_ok_to_save(), "Should not be OK to save");
uvars.sync();

View File

@@ -11,11 +11,12 @@
ScopeGuard, bytes2wcstring, exit_without_destructors, truncate_at_nul, wcs2bytes, wcs2zstring,
write_loop,
};
use crate::env::{EnvMode, EnvStack, Environment, READ_BYTE_LIMIT, Statuses};
use crate::env::{EnvMode, EnvSetMode, EnvStack, Environment, READ_BYTE_LIMIT, Statuses};
#[cfg(have_posix_spawn)]
use crate::env_dispatch::use_posix_spawn;
use crate::fds::make_fd_blocking;
use crate::fds::{PIPE_ERROR, make_autoclose_pipes, open_cloexec};
use crate::fds::{
BorrowedFdFile, PIPE_ERROR, make_autoclose_pipes, make_fd_blocking, open_cloexec,
};
use crate::flog::{flog, flogf};
use crate::fork_exec::PATH_BSHELL;
use crate::fork_exec::blocked_signals_for_job;
@@ -32,7 +33,7 @@
};
use crate::nix::{getpid, isatty};
use crate::null_terminated_array::OwningNullTerminatedArray;
use crate::parser::{Block, BlockId, BlockType, EvalRes, Parser};
use crate::parser::{Block, BlockId, BlockType, EvalRes, Parser, ParserEnvSetMode};
use crate::prelude::*;
use crate::proc::Pid;
use crate::proc::{
@@ -57,7 +58,7 @@
use std::io::{Read, Write};
use std::mem::MaybeUninit;
use std::num::NonZeroU32;
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::slice;
use std::sync::{
Arc, OnceLock,
@@ -96,14 +97,14 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
// Apply foo=bar variable assignments
for assignment in &job.processes()[0].variable_assignments {
parser.vars().set(
parser.set_var(
&assignment.variable_name,
EnvMode::LOCAL | EnvMode::EXPORT,
ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT),
assignment.values.clone(),
);
}
internal_exec(parser.vars(), job, block_io);
internal_exec(parser.vars(), parser.is_repainting(), job, block_io);
// internal_exec only returns if it failed to set up redirections.
// In case of an successful exec, this code is not reached.
let status = if job.flags().negate { 0 } else { 1 };
@@ -234,11 +235,13 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
// a pgroup, so error out before setting last_pid.
if !job.is_foreground() {
if let Some(last_pid) = job.get_last_pid() {
parser
.vars()
.set_one(L!("last_pid"), EnvMode::GLOBAL, last_pid.to_wstring());
parser.set_one(
L!("last_pid"),
ParserEnvSetMode::new(EnvMode::GLOBAL),
last_pid.to_wstring(),
);
} else {
parser.vars().set_empty(L!("last_pid"), EnvMode::GLOBAL);
parser.set_empty(L!("last_pid"), ParserEnvSetMode::new(EnvMode::GLOBAL));
}
}
@@ -249,7 +252,6 @@ pub fn exec_job(parser: &Parser, job: &Job, block_io: IoChain) -> bool {
if job.is_stopped() {
handoff.save_tty_modes();
}
handoff.reclaim();
true
}
@@ -325,8 +327,8 @@ fn exit_code_from_exec_error(err: libc::c_int) -> libc::c_int {
STATUS_NOT_EXECUTABLE
}
#[cfg(apple)]
libc::EBADARCH => {
// This is for e.g. running ARM app on Intel Mac.
libc::EBADARCH | libc::EBADMACHO => {
// This is for e.g. running ARM app on Intel Mac or a bad Mach-O executable
STATUS_NOT_EXECUTABLE
}
_ => {
@@ -479,7 +481,7 @@ fn can_use_posix_spawn_for_job(job: &Job, dup2s: &Dup2List) -> bool {
!wants_terminal
}
fn internal_exec(vars: &EnvStack, j: &Job, block_io: IoChain) {
fn internal_exec(vars: &EnvStack, is_repainting: bool, j: &Job, block_io: IoChain) {
// Do a regular launch - but without forking first...
let mut all_ios = block_io;
if !all_ios.append_from_specs(j.processes()[0].redirection_specs(), &vars.get_pwd_slash()) {
@@ -508,7 +510,8 @@ fn internal_exec(vars: &EnvStack, j: &Job, block_io: IoChain) {
{
// Decrement SHLVL as we're removing ourselves from the shell "stack".
if is_interactive_session() {
let shlvl_var = vars.getf(L!("SHLVL"), EnvMode::GLOBAL | EnvMode::EXPORT);
let global_exported_mode = EnvMode::GLOBAL | EnvMode::EXPORT;
let shlvl_var = vars.getf(L!("SHLVL"), global_exported_mode);
let mut shlvl_str = L!("0").to_owned();
if let Some(shlvl_var) = shlvl_var {
if let Ok(shlvl) = fish_wcstol(&shlvl_var.as_string()) {
@@ -517,7 +520,11 @@ fn internal_exec(vars: &EnvStack, j: &Job, block_io: IoChain) {
}
}
}
vars.set_one(L!("SHLVL"), EnvMode::GLOBAL | EnvMode::EXPORT, shlvl_str);
vars.set_one(
L!("SHLVL"),
EnvSetMode::new(global_exported_mode, is_repainting),
shlvl_str,
);
}
// launch_process _never_ returns.
@@ -957,15 +964,17 @@ fn function_prepare_environment(
// 2. inherited variables
// 3. argv
let mode = parser.convert_env_set_mode(ParserEnvSetMode::user(EnvMode::LOCAL));
let mut overwrite_argv = false;
for (idx, named_arg) in props.named_arguments.iter().enumerate() {
if named_arg == L!("argv") {
overwrite_argv = true
};
if idx < argv.len() {
vars.set_one(named_arg, EnvMode::LOCAL | EnvMode::USER, argv[idx].clone());
vars.set_one(named_arg, mode, argv[idx].clone());
} else {
vars.set_empty(named_arg, EnvMode::LOCAL | EnvMode::USER);
vars.set_empty(named_arg, mode);
}
}
@@ -973,11 +982,11 @@ fn function_prepare_environment(
if key == L!("argv") {
overwrite_argv = true
};
vars.set(key, EnvMode::LOCAL | EnvMode::USER, value.clone());
vars.set(key, mode, value.clone());
}
if !overwrite_argv {
vars.set_argv(argv);
vars.set_argv(argv, mode.is_repainting);
}
fb
}
@@ -1151,23 +1160,28 @@ fn get_performer_for_builtin(p: &Process, j: &Job, io_chain: &IoChain) -> Box<Pr
let err_io = io_chain.io_for_fd(STDERR_FILENO);
// Figure out what fd to use for the builtin's stdin.
let mut local_builtin_stdin = STDIN_FILENO;
let mut local_builtin_stdin = Some(BorrowedFdFile::stdin());
if let Some(inp) = io_chain.io_for_fd(STDIN_FILENO) {
// An fd of -1 is treated as closing stdin.
// Ignore fd redirections from an fd other than the
// standard ones. e.g. in source <&3 don't actually read from fd 3,
// which is internal to fish. We still respect this redirection in
// that we pass it on as a block IO to the code that source runs,
// and therefore this is not an error.
let ignore_redirect = inp.io_mode() == IoMode::Fd && inp.source_fd() >= 3;
if !ignore_redirect {
local_builtin_stdin = inp.source_fd();
let fd = inp.source_fd();
let ignore_redirect = fd >= 3 && inp.io_mode() == IoMode::Fd;
if fd == -1 {
local_builtin_stdin = None;
} else if !ignore_redirect {
// Safety: the fd may in principal be closed, but this only panics on negative values.
local_builtin_stdin = Some(unsafe { BorrowedFdFile::from_raw_fd(fd) });
}
}
// Populate our IoStreams. This is a bag of information for the builtin.
let mut streams = IoStreams::new(output_stream, errput_stream, &io_chain);
streams.job_group = job_group;
streams.stdin_fd = local_builtin_stdin;
streams.stdin_file = local_builtin_stdin;
streams.stdin_is_directly_redirected = stdin_is_directly_redirected;
streams.out_is_redirected = out_io.is_some();
streams.err_is_redirected = err_io.is_some();
@@ -1308,9 +1322,9 @@ fn exec_process_in_job(
}
});
for assignment in &p.variable_assignments {
parser.vars().set(
parser.set_var(
&assignment.variable_name,
EnvMode::LOCAL | EnvMode::EXPORT,
ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT),
assignment.values.clone(),
);
}

View File

@@ -297,9 +297,9 @@ pub fn expand_tilde(input: &mut WString, vars: &dyn Environment) {
}
}
/// Perform the opposite of tilde expansion on the string, which is modified in place.
pub fn replace_home_directory_with_tilde(s: &wstr, vars: &dyn Environment) -> WString {
let mut result = s.to_owned();
/// Perform the opposite of tilde expansion on the string.
pub fn replace_home_directory_with_tilde(s: impl Into<WString>, vars: &dyn Environment) -> WString {
let mut result = s.into();
// Only absolute paths get this treatment.
if result.starts_with(L!("/")) {
let mut home_directory = L!("~").to_owned();
@@ -737,7 +737,7 @@ fn expand_variables(
// here, So tmp < 1 means it's definitely not in.
// Note we are 1-based.
if item_index >= 1 && item_index <= all_var_items.len() {
var_item_list.push(all_var_items[item_index - 1].to_owned());
var_item_list.push(all_var_items[item_index - 1].clone());
}
}
}
@@ -1038,7 +1038,7 @@ pub fn expand_cmdsubst(
continue;
}
// -1 to convert from 1-based slice index to 0-based vector index.
sub_res2.push(sub_res[idx as usize - 1].to_owned());
sub_res2.push(sub_res[idx as usize - 1].clone());
}
sub_res = sub_res2;
}
@@ -1597,6 +1597,7 @@ mod tests {
use crate::expand::{ExpandResultCode, expand_to_receiver};
use crate::operation_context::{EXPANSION_LIMIT_DEFAULT, no_cancel};
use crate::parse_constants::ParseErrorList;
use crate::parser::ParserEnvSetMode;
use crate::tests::prelude::*;
use crate::wildcard::ANY_STRING;
use crate::{
@@ -1957,7 +1958,7 @@ fn test_expand_overflow() {
let parser = TestParser::new();
parser.vars().push(true);
let set = parser.vars().set(L!("bigvar"), EnvMode::LOCAL, vals);
let set = parser.set_var(L!("bigvar"), ParserEnvSetMode::new(EnvMode::LOCAL), vals);
assert_eq!(set, EnvStackSetResult::Ok);
let mut errors = ParseErrorList::new();
@@ -1977,7 +1978,7 @@ fn test_expand_overflow() {
assert_ne!(errors, vec![]);
assert_eq!(res, ExpandResultCode::error);
parser.vars().pop();
parser.vars().pop(false);
}
#[test]
@@ -2048,4 +2049,22 @@ fn test_abbreviations() {
assert_eq!(abbr_expand_1(L!("foo"), cmd), Some(L!("bar").into()));
}
#[test]
fn test_replace_home_directory_with_tilde() {
use super::replace_home_directory_with_tilde as rhdwt;
use crate::env::{EnvMode, EnvSetMode, EnvStack};
let vars = EnvStack::new();
vars.set_one(
L!("HOME"),
EnvSetMode::new(EnvMode::GLOBAL, false),
L!("/home/testuser").to_owned(),
);
assert_eq!(rhdwt("/home/testuser/", &vars), "~/");
assert_eq!(rhdwt("/home/testuser/Documents/", &vars), "~/Documents/");
assert_eq!(rhdwt("/home/testuser", &vars), "/home/testuser");
assert_eq!(rhdwt("/other/path/", &vars), "/other/path/");
assert_eq!(rhdwt("relative/path", &vars), "relative/path");
}
}

View File

@@ -10,6 +10,9 @@
use std::ffi::CStr;
use std::fs::File;
use std::io;
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
use std::os::unix::prelude::*;
localizable_consts!(
@@ -240,12 +243,84 @@ pub fn make_fd_blocking(fd: RawFd) -> Result<(), io::Error> {
Ok(())
}
/// A helper type for a File that does not close on drop.
/// Note the underlying file is never dropped; this is equivalent to mem::forget.
pub struct BorrowedFdFile(ManuallyDrop<File>);
impl Deref for BorrowedFdFile {
type Target = File;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BorrowedFdFile {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromRawFd for BorrowedFdFile {
// Note this does NOT take ownership.
unsafe fn from_raw_fd(fd: RawFd) -> Self {
Self(ManuallyDrop::new(unsafe { File::from_raw_fd(fd) }))
}
}
impl AsRawFd for BorrowedFdFile {
#[inline]
fn as_raw_fd(&self) -> RawFd {
self.0.as_raw_fd()
}
}
impl IntoRawFd for BorrowedFdFile {
#[inline]
fn into_raw_fd(self) -> RawFd {
ManuallyDrop::into_inner(self.0).into_raw_fd()
}
}
impl BorrowedFdFile {
/// Return a BorrowedFdFile from stdin.
pub fn stdin() -> Self {
unsafe { Self::from_raw_fd(libc::STDIN_FILENO) }
}
}
impl Clone for BorrowedFdFile {
// BorrowedFdFile may be cloned: this just shares the borrowed fd.
// It does NOT duplicate the underlying fd.
fn clone(&self) -> Self {
// Safety: just re-borrow the same fd.
unsafe { Self::from_raw_fd(self.as_raw_fd()) }
}
}
impl std::io::Read for BorrowedFdFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.deref_mut().read(buf)
}
fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
self.deref_mut().read_vectored(bufs)
}
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
self.deref_mut().read_to_end(buf)
}
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
self.deref_mut().read_to_string(buf)
}
}
#[cfg(test)]
mod tests {
use super::{FIRST_HIGH_FD, make_autoclose_pipes};
use super::{BorrowedFdFile, FIRST_HIGH_FD, make_autoclose_pipes};
use crate::tests::prelude::*;
use libc::{F_GETFD, FD_CLOEXEC};
use std::os::fd::AsRawFd;
use std::os::fd::{AsRawFd, FromRawFd};
#[test]
#[serial]
@@ -269,4 +344,18 @@ fn test_pipes() {
}
}
}
#[test]
fn test_borrowed_fd_file_does_not_close() {
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
let borrowed = unsafe { BorrowedFdFile::from_raw_fd(fd) };
#[allow(clippy::drop_non_drop)]
drop(borrowed);
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
assert!(flags >= 0);
drop(file);
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
assert!(flags < 0);
}
}

View File

@@ -472,6 +472,16 @@ fn err_or_no_exec_handling(interpreter: &CStr, actual_cmd: &CStr) {
);
}
#[cfg(apple)]
libc::EBADMACHO => {
flog_safe!(
exec,
"Failed to execute process '",
actual_cmd,
"': Malformed Mach-O file."
);
}
err => {
flog_safe!(
exec,

View File

@@ -14,10 +14,9 @@
use crate::parser_keywords::parser_keywords_is_reserved;
use crate::prelude::*;
use crate::wutil::dir_iter::DirIter;
use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU32;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, LazyLock, Mutex};
#[derive(Clone)]
pub struct FunctionProperties {
@@ -100,7 +99,7 @@ fn allow_autoload(&self, name: &wstr) -> bool {
}
/// The big set of all functions.
static FUNCTION_SET: Lazy<Mutex<FunctionSet>> = Lazy::new(|| {
static FUNCTION_SET: LazyLock<Mutex<FunctionSet>> = LazyLock::new(|| {
Mutex::new(FunctionSet {
funcs: HashMap::new(),
autoload_tombstones: HashSet::new(),

View File

@@ -88,7 +88,7 @@ pub fn test_path(&self, token: &wstr, prefix: bool) -> bool {
is_potential_path(
&token,
prefix,
&[self.working_directory.to_owned()],
std::slice::from_ref(&self.working_directory),
self.ctx,
PathFlags {
expand_tilde: true,
@@ -129,17 +129,17 @@ pub fn test_cd_path(&self, token: &wstr, is_prefix: bool) -> FileTestResult {
}
}
// Test if a the given string is a valid redirection target, given the mode.
// Note we return bool, because we never underline redirection targets.
pub fn test_redirection_target(&self, target: &wstr, mode: RedirectionMode) -> bool {
// Test if a the given string is a valid redirection target, and if so, whether
// it is a path to an existing file.
pub fn test_redirection_target(&self, target: &wstr, mode: RedirectionMode) -> FileTestResult {
// Skip targets exceeding PATH_MAX. See #7837.
if target.len() > (PATH_MAX as usize) {
return false;
return Err(IsErr);
}
let mut target = target.to_owned();
if !expand_one(&mut target, ExpandFlags::FAIL_ON_CMDSUBST, self.ctx, None) {
// Could not be expanded.
return false;
return Err(IsErr);
}
// Ok, we successfully expanded our target. Now verify that it works with this
// redirection. We will probably need it as a path (but not in the case of fd
@@ -148,29 +148,33 @@ pub fn test_redirection_target(&self, target: &wstr, mode: RedirectionMode) -> b
match mode {
RedirectionMode::Fd => {
if target == "-" {
return true;
return Ok(IsFile(false));
}
match fish_wcstoi(&target) {
Ok(fd) => fd >= 0,
Err(_) => false,
Ok(fd) if fd >= 0 => Ok(IsFile(false)),
_ => Err(IsErr),
}
}
RedirectionMode::Input | RedirectionMode::TryInput => {
// Input redirections must have a readable non-directory.
// Note we color "try_input" files as errors if they are invalid,
// even though it's possible to execute these (replaced via /dev/null).
waccess(&target_path, libc::R_OK) == 0
if waccess(&target_path, libc::R_OK) == 0
&& wstat(&target_path).is_ok_and(|md| !md.file_type().is_dir())
{
Ok(IsFile(true))
} else {
Err(IsErr)
}
}
RedirectionMode::Overwrite | RedirectionMode::Append | RedirectionMode::NoClob => {
if string_suffixes_string(L!("/"), &target) {
// Redirections to things that are directories is definitely not
// allowed.
return false;
return Err(IsErr);
}
// Test whether the file exists, and whether it's writable (possibly after
// creating it). access() returns failure if the file does not exist.
// TODO: we do not need to compute file_exists for an 'overwrite' redirection.
let file_exists;
let file_is_writable;
match wstat(&target_path) {
@@ -206,7 +210,10 @@ pub fn test_redirection_target(&self, target: &wstr, mode: RedirectionMode) -> b
}
}
// NoClob means that we must not overwrite files that exist.
file_is_writable && !(file_exists && mode == RedirectionMode::NoClob)
if !file_is_writable || (mode == RedirectionMode::NoClob && file_exists) {
return Err(IsErr);
}
Ok(IsFile(file_exists))
}
}
}
@@ -546,57 +553,57 @@ fn test_redirections() {
// Normal redirection.
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::Input);
assert!(result);
assert_eq!(result, Ok(IsFile(true)));
// Can't redirect from a missing file
let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::Input);
assert!(!result);
assert_eq!(result, Err(IsErr));
let result =
tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::Input);
assert!(!result);
assert_eq!(result, Err(IsErr));
// Can't redirect from a directory.
let result = tester.test_redirection_target(L!("somedir"), RedirectionMode::Input);
assert!(!result);
assert_eq!(result, Err(IsErr));
// Can't redirect from an unreadable file.
#[cfg(not(cygwin))] // Can't mark a file write-only on MSYS, this may work on true Cygwin
{
fs::set_permissions(&file_path, Permissions::from_mode(0o200)).unwrap();
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::Input);
assert!(!result);
assert_eq!(result, Err(IsErr));
fs::set_permissions(&file_path, Permissions::from_mode(0o600)).unwrap();
}
// try_input syntax highlighting reports an error even though the command will succeed.
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::TryInput);
assert!(result);
assert_eq!(result, Ok(IsFile(true)));
let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::TryInput);
assert!(!result);
assert_eq!(result, Err(IsErr));
let result =
tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::TryInput);
assert!(!result);
assert_eq!(result, Err(IsErr));
// Test write redirections.
// Overwrite an existing file.
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::Overwrite);
assert!(result);
assert_eq!(result, Ok(IsFile(true)));
// Append to an existing file.
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::Append);
assert!(result);
assert_eq!(result, Ok(IsFile(true)));
// Write to a missing file.
let result = tester.test_redirection_target(L!("newfile.txt"), RedirectionMode::Overwrite);
assert!(result);
assert_eq!(result, Ok(IsFile(false)));
// No-clobber write to existing file should fail.
let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::NoClob);
assert!(!result);
assert_eq!(result, Err(IsErr));
// No-clobber write to missing file should succeed.
let result = tester.test_redirection_target(L!("unique.txt"), RedirectionMode::NoClob);
assert!(result);
assert_eq!(result, Ok(IsFile(false)));
let write_modes = &[
RedirectionMode::Overwrite,
@@ -606,8 +613,9 @@ fn test_redirections() {
// Can't write to a directory.
for mode in write_modes {
assert!(
!tester.test_redirection_target(L!("somedir"), *mode),
assert_eq!(
tester.test_redirection_target(L!("somedir"), *mode),
Err(IsErr),
"Should not be able to write to a directory with mode {:?}",
mode
);
@@ -616,8 +624,9 @@ fn test_redirections() {
// Can't write without write permissions.
fs::set_permissions(&file_path, Permissions::from_mode(0o400)).unwrap(); // Read-only.
for mode in write_modes {
assert!(
!tester.test_redirection_target(L!("file.txt"), *mode),
assert_eq!(
tester.test_redirection_target(L!("file.txt"), *mode),
Err(IsErr),
"Should not be able to write to a read-only file with mode {:?}",
mode
);
@@ -629,8 +638,9 @@ fn test_redirections() {
{
fs::set_permissions(&dir_path, Permissions::from_mode(0o500)).unwrap(); // Read and execute, no write.
for mode in write_modes {
assert!(
!tester.test_redirection_target(L!("somedir/newfile.txt"), *mode),
assert_eq!(
tester.test_redirection_target(L!("somedir/newfile.txt"), *mode),
Err(IsErr),
"Should not be able to create/write in a read-only directory with mode {:?}",
mode
);
@@ -639,28 +649,82 @@ fn test_redirections() {
}
// Test fd redirections.
assert!(tester.test_redirection_target(L!("-"), RedirectionMode::Fd));
assert!(tester.test_redirection_target(L!("0"), RedirectionMode::Fd));
assert!(tester.test_redirection_target(L!("1"), RedirectionMode::Fd));
assert!(tester.test_redirection_target(L!("2"), RedirectionMode::Fd));
assert!(tester.test_redirection_target(L!("3"), RedirectionMode::Fd));
assert!(tester.test_redirection_target(L!("500"), RedirectionMode::Fd));
assert_eq!(
tester.test_redirection_target(L!("-"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
assert_eq!(
tester.test_redirection_target(L!("0"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
assert_eq!(
tester.test_redirection_target(L!("1"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
assert_eq!(
tester.test_redirection_target(L!("2"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
assert_eq!(
tester.test_redirection_target(L!("3"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
assert_eq!(
tester.test_redirection_target(L!("500"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
// We are base 10, despite the leading 0.
assert!(tester.test_redirection_target(L!("000"), RedirectionMode::Fd));
assert!(tester.test_redirection_target(L!("01"), RedirectionMode::Fd));
assert!(tester.test_redirection_target(L!("07"), RedirectionMode::Fd));
assert_eq!(
tester.test_redirection_target(L!("000"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
assert_eq!(
tester.test_redirection_target(L!("01"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
assert_eq!(
tester.test_redirection_target(L!("07"), RedirectionMode::Fd),
Ok(IsFile(false)),
);
// Invalid fd redirections.
assert!(!tester.test_redirection_target(L!("0x2"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("0x3F"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("0F"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("-1"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("-0009"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("--"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("derp"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("123boo"), RedirectionMode::Fd));
assert!(!tester.test_redirection_target(L!("18446744073709551616"), RedirectionMode::Fd));
assert_eq!(
tester.test_redirection_target(L!("0x2"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("0x3F"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("0F"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("-1"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("-0009"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("--"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("derp"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("123boo"), RedirectionMode::Fd),
Err(IsErr),
);
assert_eq!(
tester.test_redirection_target(L!("18446744073709551616"), RedirectionMode::Fd),
Err(IsErr),
);
}
#[test]

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