Compare commits

..

34 Commits
4.3.0 ... 4.3.2

Author SHA1 Message Date
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
62 changed files with 896 additions and 524 deletions

View File

@@ -1,9 +1,29 @@
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 +50,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 +65,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

2
Cargo.lock generated
View File

@@ -152,7 +152,7 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "fish"
version = "4.3.0"
version = "4.3.2"
dependencies = [
"bitflags",
"cc",

View File

@@ -79,7 +79,7 @@ debug = true
[package]
name = "fish"
version = "4.3.0"
version = "4.3.2"
edition.workspace = true
rust-version.workspace = true
default-run = "fish"

View File

@@ -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

@@ -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

@@ -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

@@ -1,3 +1,19 @@
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

@@ -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

@@ -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

@@ -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

@@ -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 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 -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,7 +81,7 @@ 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

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
@@ -340,7 +340,7 @@ 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
for ct in $color_themes
@@ -354,8 +354,8 @@ 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

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

@@ -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,
@@ -498,9 +498,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 +545,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 +580,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

@@ -2,6 +2,7 @@
use crate::abbrs::{self, Abbreviation, Position};
use crate::common::{EscapeStringStyle, escape, escape_string, valid_func_name};
use crate::env::{EnvMode, EnvStackSetResult};
use crate::parser::ParserEnvSetMode;
use crate::re::{regex_make_anchored, to_boxed_chars};
use pcre2::utf32::{Regex, RegexBuilder};
@@ -460,7 +461,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)

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_");
@@ -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,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,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();
}

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

@@ -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
@@ -620,7 +622,7 @@ 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;
}
};

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(
@@ -916,11 +951,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 +1019,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

@@ -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

@@ -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);
}
}

View File

@@ -1951,7 +1951,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

@@ -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::*,
@@ -401,7 +401,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 +415,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),
}
}
@@ -606,7 +606,7 @@ struct Completer<'ctx> {
condition_cache: HashMap<WString, bool>,
}
static completion_autoloader: Lazy<Mutex<Autoload>> =
static COMPLETION_AUTOLOADER: Lazy<Mutex<Autoload>> =
Lazy::new(|| Mutex::new(Autoload::new(L!("fish_complete_path"))));
impl<'ctx> Completer<'ctx> {
@@ -1257,7 +1257,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 +1793,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 +1817,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 +1914,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;
}
@@ -2479,14 +2481,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 +2549,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 +2632,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::*;
@@ -3221,9 +3224,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 +3336,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();
}

255
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;
@@ -212,7 +212,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 +238,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 +256,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 +276,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 +322,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 +357,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 +375,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 +407,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 +511,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 +541,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 +568,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,18 +576,18 @@ 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);
}
}
@@ -581,15 +614,11 @@ 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());
}
}
@@ -600,6 +629,9 @@ fn setup_path() {
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 +651,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 +663,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 +688,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 +745,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 +772,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 +780,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 +802,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 +885,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 +926,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 +941,7 @@ fn test_env_snapshot() {
L!("reallyglobal")
);
vars.pop();
vars.pop(false);
parser.popd();
}
@@ -914,6 +955,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;
@@ -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,
}
}
@@ -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
@@ -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,40 @@ 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.
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 +334,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

@@ -11,7 +11,7 @@
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;
@@ -32,7 +32,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::{
@@ -96,14 +96,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 +234,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));
}
}
@@ -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
}
@@ -1308,9 +1317,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

@@ -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]

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

@@ -1274,7 +1274,7 @@ pub struct HighlightSpec {
mod tests {
use super::{HighlightColorResolver, HighlightRole, HighlightSpec, highlight_shell};
use crate::common::ScopeGuard;
use crate::env::{EnvMode, Environment};
use crate::env::{EnvMode, EnvSetMode, Environment};
use crate::future_feature_flags::{self, FeatureFlag};
use crate::highlight::parse_text_face_for_highlight;
use crate::operation_context::{EXPANSION_LIMIT_BACKGROUND, OperationContext};
@@ -1386,26 +1386,15 @@ macro_rules! validate {
// Verify variables and wildcards in commands using /bin/cat.
let vars = parser.vars();
vars.set_one(
L!("CDPATH"),
EnvMode::LOCAL,
L!("./cdpath-entry").to_owned(),
);
let local_mode = EnvSetMode::new_at_early_startup(EnvMode::LOCAL);
vars.set_one(L!("CDPATH"), local_mode, L!("./cdpath-entry").to_owned());
vars.set_one(
L!("VARIABLE_IN_COMMAND"),
EnvMode::LOCAL,
L!("a").to_owned(),
);
vars.set_one(
L!("VARIABLE_IN_COMMAND2"),
EnvMode::LOCAL,
L!("at").to_owned(),
);
vars.set_one(L!("VARIABLE_IN_COMMAND"), local_mode, L!("a").to_owned());
vars.set_one(L!("VARIABLE_IN_COMMAND2"), local_mode, L!("at").to_owned());
let _cleanup = ScopeGuard::new((), |_| {
vars.remove(L!("VARIABLE_IN_COMMAND"), EnvMode::default());
vars.remove(L!("VARIABLE_IN_COMMAND2"), EnvMode::default());
vars.remove(L!("VARIABLE_IN_COMMAND"), EnvSetMode::default());
vars.remove(L!("VARIABLE_IN_COMMAND2"), EnvSetMode::default());
});
validate!(
@@ -1798,7 +1787,7 @@ fn test_trailing_spaces_after_command() {
// First, set up fish_color_command to include underline
vars.set_one(
L!("fish_color_command"),
EnvMode::LOCAL,
EnvSetMode::new_at_early_startup(EnvMode::LOCAL),
L!("--underline").to_owned(),
);
@@ -1850,7 +1839,7 @@ fn test_resolve_role() {
let vars = parser.vars();
let set = |var: &wstr, value: Vec<WString>| {
vars.set(var, EnvMode::LOCAL, value);
vars.set(var, EnvSetMode::new(EnvMode::LOCAL, false), value);
};
set(L!("fish_color_normal"), vec![L!("normal").into()]);
set(

View File

@@ -16,7 +16,7 @@
use crate::{
common::cstr2wcstring,
env::EnvVar,
env::{EnvSetMode, EnvVar},
fs::{
LOCKED_FILE_MODE, LockedFile, LockingMode, PotentialUpdate, WriteMethod, lock_and_load,
rewrite_via_temporary_file,
@@ -1760,8 +1760,9 @@ pub fn all_paths_are_valid<P: IntoIterator<Item = WString>>(
/// Sets private mode on. Once in private mode, it cannot be turned off.
pub fn start_private_mode(vars: &EnvStack) {
vars.set_one(L!("fish_history"), EnvMode::GLOBAL, L!("").to_owned());
vars.set_one(L!("fish_private_mode"), EnvMode::GLOBAL, L!("1").to_owned());
let global_mode = EnvSetMode::new_at_early_startup(EnvMode::GLOBAL);
vars.set_one(L!("fish_history"), global_mode, L!("").to_owned());
vars.set_one(L!("fish_private_mode"), global_mode, L!("1").to_owned());
}
/// Queries private mode status.
@@ -1777,7 +1778,7 @@ mod tests {
};
use crate::common::ESCAPE_TEST_CHAR;
use crate::common::{ScopeGuard, bytes2wcstring, wcs2bytes, wcs2osstring};
use crate::env::{EnvMode, EnvStack};
use crate::env::{EnvMode, EnvSetMode, EnvStack};
use crate::fs::{LockedFile, WriteMethod};
use crate::path::path_get_data;
use crate::prelude::*;
@@ -2270,8 +2271,9 @@ fn test_history_path_detection() {
let wdir_path = WString::from(tmpdir.path().to_str().unwrap());
let test_vars = EnvStack::new();
test_vars.set_one(L!("PWD"), EnvMode::GLOBAL, wdir_path.clone());
test_vars.set_one(L!("HOME"), EnvMode::GLOBAL, wdir_path.clone());
let global_mode = EnvSetMode::new(EnvMode::GLOBAL, false);
test_vars.set_one(L!("PWD"), global_mode, wdir_path.clone());
test_vars.set_one(L!("HOME"), global_mode, wdir_path.clone());
let history = History::with_name(L!("path_detection"));
history.clear();

View File

@@ -35,7 +35,9 @@
MaybeParentheses::CommandSubstitution, parse_util_locate_cmdsubst_range,
parse_util_unescape_wildcards,
};
use crate::parser::{Block, BlockData, BlockId, BlockType, LoopStatus, Parser, ProfileItem};
use crate::parser::{
Block, BlockData, BlockId, BlockType, LoopStatus, Parser, ParserEnvSetMode, ProfileItem,
};
use crate::parser_keywords::parser_keywords_is_subcommand;
use crate::path::{path_as_implicit_cd, path_try_get_path};
use crate::prelude::*;
@@ -648,8 +650,11 @@ fn apply_variable_assignments(
vals.clone(),
));
}
ctx.parser()
.set_var_and_fire(variable_name, EnvMode::LOCAL | EnvMode::EXPORT, vals);
ctx.parser().set_var_and_fire(
variable_name,
ParserEnvSetMode::new(EnvMode::LOCAL | EnvMode::EXPORT),
vals,
);
}
EndExecutionReason::Ok
}
@@ -926,9 +931,9 @@ fn run_for_statement(
);
}
let retval = ctx.parser().vars().set(
let retval = ctx.parser().set_var(
&for_var_name,
EnvMode::LOCAL | EnvMode::USER,
ParserEnvSetMode::user(EnvMode::LOCAL),
var.map_or(vec![], |var| var.as_list().to_owned()),
);
assert!(retval == EnvStackSetResult::Ok);
@@ -946,10 +951,11 @@ fn run_for_statement(
break;
}
let retval = ctx
.parser()
.vars()
.set(&for_var_name, EnvMode::USER, vec![val]);
let retval = ctx.parser().set_var(
&for_var_name,
ParserEnvSetMode::user(EnvMode::empty()),
vec![val],
);
assert!(
retval == EnvStackSetResult::Ok,
"for loop variable should have been successfully set"

View File

@@ -8,7 +8,8 @@
};
use crate::complete::CompletionList;
use crate::env::{
EnvMode, EnvStack, EnvStackSetResult, Environment, FISH_TERMINAL_COLOR_THEME_VAR, Statuses,
EnvMode, EnvSetMode, EnvStack, EnvStackSetResult, Environment, FISH_TERMINAL_COLOR_THEME_VAR,
Statuses,
};
use crate::event::{self, Event};
use crate::expand::{
@@ -437,6 +438,21 @@ pub struct Parser {
pub blocking_query_timeout: RefCell<Option<Duration>>,
}
#[derive(Copy, Clone, Default)]
pub struct ParserEnvSetMode {
pub mode: EnvMode,
pub user: bool,
}
impl ParserEnvSetMode {
pub fn new(mode: EnvMode) -> Self {
Self { mode, user: false }
}
pub fn user(mode: EnvMode) -> Self {
Self { mode, user: true }
}
}
impl Parser {
/// Create a parser.
pub fn new(variables: EnvStack, cancel_behavior: CancelBehavior) -> Parser {
@@ -945,27 +961,59 @@ pub fn set_last_statuses(&self, s: Statuses) {
pub fn set_var_and_fire(
&self,
key: &wstr,
mode: EnvMode,
mode: ParserEnvSetMode,
vals: Vec<WString>,
) -> EnvStackSetResult {
let res = self.vars().set(key, mode, vals);
let res = self.set_var(key, mode, vals);
if res == EnvStackSetResult::Ok {
event::fire(self, Event::variable_set(key.to_owned()));
}
res
}
pub fn is_repainting(&self) -> bool {
self.libdata().is_repaint
}
pub fn convert_env_set_mode(&self, mode: ParserEnvSetMode) -> EnvSetMode {
EnvSetMode::new_with(mode.mode, mode.user, self.is_repainting())
}
/// Cover of vars().set(), without firing events
pub fn set_var(&self, key: &wstr, mode: EnvMode, vals: Vec<WString>) -> EnvStackSetResult {
pub fn set_var(
&self,
key: &wstr,
mode: ParserEnvSetMode,
vals: Vec<WString>,
) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().set(key, mode, vals)
}
/// Cover of vars().set_one(), without firing events
pub fn set_one(&self, key: &wstr, mode: ParserEnvSetMode, val: WString) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().set_one(key, mode, val)
}
/// Cover of vars().set_empty(), without firing events
pub fn set_empty(&self, key: &wstr, mode: ParserEnvSetMode) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().set_empty(key, mode)
}
/// Cover of vars().remove(), without firing events
pub fn remove_var(&self, key: &wstr, mode: ParserEnvSetMode) -> EnvStackSetResult {
let mode = self.convert_env_set_mode(mode);
self.vars().remove(key, mode)
}
/// Update any universal variables and send event handlers.
/// If `always` is set, then do it even if we have no pending changes (that is, look for
/// changes from other fish instances); otherwise only sync if this instance has changed uvars.
pub fn sync_uvars_and_fire(&self, always: bool) {
if self.syncs_uvars.load() {
let evts = self.vars().universal_sync(always);
let evts = self.vars().universal_sync(always, self.is_repainting());
for evt in evts {
event::fire(self, evt);
}
@@ -994,7 +1042,7 @@ pub fn pop_block(&self, expected: BlockId) {
block_list.pop().unwrap()
};
if block.wants_pop_env() {
self.vars().pop();
self.vars().pop(self.is_repainting());
}
}
@@ -1247,7 +1295,7 @@ pub fn set_color_theme(&self, background_color: Option<&xterm_color::Color>) {
);
self.set_var_and_fire(
FISH_TERMINAL_COLOR_THEME_VAR,
EnvMode::GLOBAL,
ParserEnvSetMode::new(EnvMode::GLOBAL),
vec![color_theme.to_owned()],
);
}

View File

@@ -3,7 +3,7 @@
//! path-related issues.
use crate::common::{wcs2osstring, wcs2zstring};
use crate::env::{EnvMode, EnvStack, Environment, FALLBACK_PATH};
use crate::env::{EnvMode, EnvSetMode, EnvStack, Environment, FALLBACK_PATH};
use crate::expand::{HOME_DIRECTORY, expand_tilde};
use crate::flog::{flog, flogf};
use crate::prelude::*;
@@ -134,15 +134,13 @@ fn maybe_issue_path_warning(
vars: &EnvStack,
) {
let warning_var_name = L!("_FISH_WARNED_").to_owned() + which_dir;
if vars
.getf(&warning_var_name, EnvMode::GLOBAL | EnvMode::EXPORT)
.is_some()
{
let global_exported_mode = EnvMode::GLOBAL | EnvMode::EXPORT;
if vars.getf(&warning_var_name, global_exported_mode).is_some() {
return;
}
vars.set_one(
&warning_var_name,
EnvMode::GLOBAL | EnvMode::EXPORT,
EnvSetMode::new_at_early_startup(global_exported_mode),
L!("1").to_owned(),
);

View File

@@ -113,6 +113,7 @@
parse_util_get_line_from_offset, parse_util_get_offset, parse_util_get_offset_from_line,
parse_util_lineno, parse_util_locate_cmdsubst_range, parse_util_token_extent,
};
use crate::parser::ParserEnvSetMode;
use crate::parser::{BlockType, EvalRes, Parser};
use crate::prelude::*;
use crate::proc::{
@@ -392,9 +393,11 @@ pub fn reader_push<'a>(parser: &'a Parser, history_name: &wstr, conf: ReaderConf
// Provide value for `status current-command`
parser.libdata_mut().status_vars.command = L!("fish").to_owned();
// Also provide a value for the deprecated fish 2.0 $_ variable
parser
.vars()
.set_one(L!("_"), EnvMode::GLOBAL, L!("fish").to_owned());
parser.set_one(
L!("_"),
ParserEnvSetMode::new(EnvMode::GLOBAL),
L!("fish").to_owned(),
);
let old = parser
.blocking_query_timeout
.replace(input_data.blocking_query_timeout);
@@ -1099,9 +1102,7 @@ pub fn reader_set_autosuggestion_enabled(vars: &dyn Environment) {
let enable = check_bool_var(vars, L!("fish_autosuggestion_enabled"), true);
if data.conf.autosuggest_ok != enable {
data.conf.autosuggest_ok = enable;
data.force_exec_prompt_and_repaint = true;
data.input_data
.queue_char(CharEvent::from_readline(ReadlineCmd::Repaint));
data.schedule_prompt_repaint();
}
}
}
@@ -1121,11 +1122,7 @@ pub fn reader_schedule_prompt_repaint() {
let Some(data) = current_data() else {
return;
};
if !data.force_exec_prompt_and_repaint {
data.force_exec_prompt_and_repaint = true;
data.input_data
.queue_char(CharEvent::from_readline(ReadlineCmd::Repaint));
}
data.schedule_prompt_repaint();
}
pub fn reader_update_termsize(parser: &Parser) {
@@ -1622,6 +1619,15 @@ pub fn mouse_left_click(&mut self, click_position: ViewportPosition) {
CharOffset::Pager(_) | CharOffset::None => {}
}
}
pub fn schedule_prompt_repaint(&mut self) {
if self.force_exec_prompt_and_repaint {
return;
}
self.force_exec_prompt_and_repaint = true;
self.input_data
.queue_char(CharEvent::from_readline(ReadlineCmd::Repaint));
}
}
pub fn reader_save_screen_state() {
@@ -3503,9 +3509,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
| rl::PrevdOrBackwardWord => {
if c == rl::PrevdOrBackwardWord && self.command_line.is_empty() {
self.eval_bind_cmd(L!("prevd"));
self.force_exec_prompt_and_repaint = true;
self.input_data
.queue_char(CharEvent::from_readline(ReadlineCmd::Repaint));
self.schedule_prompt_repaint();
return;
}
@@ -3529,9 +3533,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
| rl::NextdOrForwardWord => {
if c == rl::NextdOrForwardWord && self.command_line.is_empty() {
self.eval_bind_cmd(L!("nextd"));
self.force_exec_prompt_and_repaint = true;
self.input_data
.queue_char(CharEvent::from_readline(ReadlineCmd::Repaint));
self.schedule_prompt_repaint();
return;
}
@@ -5989,9 +5991,11 @@ fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes {
parser.libdata_mut().status_vars.command = ft.to_owned();
parser.libdata_mut().status_vars.commandline = cmd.to_owned();
// Also provide a value for the deprecated fish 2.0 $_ variable
parser
.vars()
.set_one(L!("_"), EnvMode::GLOBAL, ft.to_owned());
parser.set_one(
L!("_"),
ParserEnvSetMode::new(EnvMode::GLOBAL),
ft.to_owned(),
);
}
reader_write_title(cmd, parser, true);
@@ -6009,9 +6013,9 @@ fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes {
if !ft.is_empty() {
let time_after = Instant::now();
let duration = time_after.duration_since(time_before);
parser.vars().set_one(
parser.set_one(
ENV_CMD_DURATION,
EnvMode::UNEXPORT,
ParserEnvSetMode::new(EnvMode::UNEXPORT),
duration.as_millis().to_wstring(),
);
}
@@ -6021,9 +6025,11 @@ fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes {
// Provide value for `status current-command`
parser.libdata_mut().status_vars.command = get_program_name().to_owned();
// Also provide a value for the deprecated fish 2.0 $_ variable
parser
.vars()
.set_one(L!("_"), EnvMode::GLOBAL, get_program_name().to_owned());
parser.set_one(
L!("_"),
ParserEnvSetMode::new(EnvMode::GLOBAL),
get_program_name().to_owned(),
);
// Provide value for `status current-commandline`
parser.libdata_mut().status_vars.commandline = L!("").to_owned();

View File

@@ -2,7 +2,7 @@
use crate::common::assert_sync;
use crate::env::{EnvMode, EnvVar, Environment};
use crate::flog::flog;
use crate::parser::Parser;
use crate::parser::{Parser, ParserEnvSetMode};
use crate::prelude::*;
use crate::wutil::fish_wcstoi;
use std::mem::MaybeUninit;
@@ -200,12 +200,12 @@ fn set_columns_lines_vars(&self, val: Termsize, parser: &Parser) {
let saved = self.setting_env_vars.swap(true, Ordering::Relaxed);
parser.set_var_and_fire(
L!("COLUMNS"),
EnvMode::GLOBAL,
ParserEnvSetMode::new(EnvMode::GLOBAL),
vec![val.width().to_wstring()],
);
parser.set_var_and_fire(
L!("LINES"),
EnvMode::GLOBAL,
ParserEnvSetMode::new(EnvMode::GLOBAL),
vec![val.height().to_wstring()],
);
self.setting_env_vars.store(saved, Ordering::Relaxed);
@@ -262,7 +262,7 @@ pub fn safe_termsize_invalidate_tty() {
#[cfg(test)]
mod tests {
use crate::env::{EnvMode, Environment};
use crate::env::{EnvMode, EnvSetMode, Environment};
use crate::termsize::*;
use crate::tests::prelude::*;
use std::sync::Mutex;
@@ -272,7 +272,7 @@ mod tests {
#[serial]
fn test_termsize() {
let _cleanup = test_init();
let env_global = EnvMode::GLOBAL;
let env_global = EnvSetMode::new(EnvMode::GLOBAL, false);
let parser = TestParser::new();
let vars = parser.vars();

View File

@@ -343,18 +343,18 @@ unsafe impl Sync for TopicMonitor {}
/// The principal topic monitor.
/// Do not attempt to move this into a lazy_static, it must be accessed from a signal handler.
static mut s_principal: *const TopicMonitor = std::ptr::null();
static mut PRINCIPAL: *const TopicMonitor = std::ptr::null();
impl TopicMonitor {
/// Initialize the principal monitor, and return it.
/// This should be called only on the main thread.
pub fn initialize() -> &'static Self {
unsafe {
if s_principal.is_null() {
if PRINCIPAL.is_null() {
// We simply leak.
s_principal = Box::into_raw(Box::default());
PRINCIPAL = Box::into_raw(Box::default());
}
&*s_principal
&*PRINCIPAL
}
}
@@ -595,10 +595,10 @@ pub fn topic_monitor_init() {
pub fn topic_monitor_principal() -> &'static TopicMonitor {
unsafe {
assert!(
!s_principal.is_null(),
!PRINCIPAL.is_null(),
"Principal topic monitor not initialized"
);
&*s_principal
&*PRINCIPAL
}
}

View File

@@ -64,6 +64,8 @@ pub enum TtyQuirks {
PreCsiMidnightCommander,
// Running in iTerm2 before 3.5.12, which causes issues when using the kitty keyboard protocol.
PreKittyIterm2,
// Whether we are running under tmux.
Tmux,
// Whether we are running under WezTerm.
Wezterm,
}
@@ -78,6 +80,8 @@ fn detect(vars: &dyn Environment, xtversion: &wstr) -> Self {
PreCsiMidnightCommander
} else if get_iterm2_version(xtversion).is_some_and(|v| v < (3, 5, 12)) {
PreKittyIterm2
} else if xtversion.starts_with(L!("tmux ")) {
Tmux
} else if xtversion.starts_with(L!("WezTerm ")) {
Wezterm
} else {
@@ -172,16 +176,16 @@ fn safe_get_supported_protocol(&self) -> ProtocolKind {
// Return the protocols set to enable or disable TTY protocols.
fn get_protocols(self) -> TtyProtocolsSet {
let on_chain = vec![
DecsetFocusReporting,
DecsetBracketedPaste,
DecsetColorThemeReporting,
];
let off_chain = vec![
DecrstFocusReporting,
DecrstBracketedPaste,
DecrstColorThemeReporting,
];
let mut on_chain = vec![];
let mut off_chain = vec![];
// Enable focus reporting under tmux
if self == TtyQuirks::Tmux {
on_chain.push(DecsetFocusReporting);
off_chain.push(DecrstFocusReporting);
}
on_chain.extend_from_slice(&[DecsetBracketedPaste, DecsetColorThemeReporting]);
off_chain.extend_from_slice(&[DecrstBracketedPaste, DecrstColorThemeReporting]);
let on_chain = || on_chain.clone().into_iter();
let off_chain = || off_chain.clone().into_iter();

View File

@@ -19,11 +19,25 @@ pub fn count_newlines(s: &wstr) -> usize {
count
}
fn is_prefix(mut lhs: impl Iterator<Item = char>, mut rhs: impl Iterator<Item = char>) -> bool {
loop {
match (lhs.next(), rhs.next()) {
(None, _) => return true,
(Some(_), None) => return false,
(Some(lhs), Some(rhs)) => {
if lhs != rhs {
return false;
}
}
}
}
}
/// Test if a string prefixes another without regard to case. Returns true if a is a prefix of b.
pub fn string_prefixes_string_case_insensitive(proposed_prefix: &wstr, value: &wstr) -> bool {
let mut proposed_prefix = lowercase(proposed_prefix.chars());
let proposed_prefix = lowercase(proposed_prefix.chars());
let value = lowercase(value.chars());
proposed_prefix.by_ref().zip(value).all(|(a, b)| a == b) && proposed_prefix.next().is_none()
is_prefix(proposed_prefix, value)
}
pub fn string_prefixes_string_maybe_case_insensitive(
@@ -48,9 +62,9 @@ pub fn strip_executable_suffix(path: &wstr) -> Option<&wstr> {
/// Test if a string is a suffix of another.
pub fn string_suffixes_string_case_insensitive(proposed_suffix: &wstr, value: &wstr) -> bool {
let mut proposed_suffix = lowercase_rev(proposed_suffix.chars());
let proposed_suffix = lowercase_rev(proposed_suffix.chars());
let value = lowercase_rev(value.chars());
proposed_suffix.by_ref().zip(value).all(|(a, b)| a == b) && proposed_suffix.next().is_none()
is_prefix(proposed_suffix, value)
}
/// Test if a string prefixes another. Returns true if a is a prefix of b.
@@ -573,6 +587,8 @@ macro_rules! validate {
validate!("İ", "i\u{307}_", true);
validate!("i\u{307}", "İ", true); // prefix is longer
validate!("i", "İ", true);
validate!("gs", "gs_", true);
validate!("gs_", "gs", false);
}
#[test]
@@ -590,6 +606,8 @@ macro_rules! validate {
validate!("İ", "i\u{307}", true); // suffix is longer
validate!("İ", "", true);
validate!("i", "", false);
validate!("gs", "_gs", true);
validate!("_gs ", "gs", false);
}
#[test]

View File

@@ -566,7 +566,7 @@ fn wgetopt_inner(&mut self, longopt_index: &mut usize) -> Option<char> {
#[cfg(test)]
mod tests {
use super::{ArgType, WGetopter, WOption, wopt};
use super::{ArgType, WGetopter, wopt};
use crate::prelude::*;
use crate::wcstringutil::join_strings;
@@ -611,8 +611,8 @@ fn test_exchange() {
#[test]
fn test_wgetopt() {
// Regression test for a crash.
const short_options: &wstr = L!("-a");
const long_options: &[WOption] = &[wopt(L!("add"), ArgType::NoArgument, 'a')];
let short_options = L!("-a");
let long_options = &[wopt(L!("add"), ArgType::NoArgument, 'a')];
let mut argv = [
L!("abbr"),
L!("--add"),

View File

@@ -30,7 +30,7 @@ echo no default universal variables
# CHECK: ok
provoke-migration
$fish -c __fish_theme_migrate
$fish -c __fish_migrate
# CHECK: {{\x1b\[1m}}fish:{{\x1b\[m}} {{upgraded.*}}
# CHECK: {{.*Color.*no.longer.*universal.*}}
# CHECK: Migrated {{.*}} {{\S*}}/xdg_config_home/fish/conf.d/fish_frozen_theme.fish{{\x1b\[m}}
@@ -49,7 +49,7 @@ echo no default universal variables
# But the migration is only done once, in case the user really wants these as universals.
set -U fish_color_autosuggestion 8e8e8e
$fish -c '
__fish_theme_migrate
__fish_migrate
set -eg fish_color_autosuggestion
echo $fish_color_autosuggestion
# CHECK: 8e8e8e
@@ -63,7 +63,7 @@ echo no default universal variables
echo yes | fish_config theme save default
fake-old-uvars
provoke-migration
$fish -c __fish_theme_migrate
$fish -c __fish_migrate
# CHECK: {{\x1b\[1m}}fish:{{\x1b\[m}} {{upgraded.*}}
# CHECK: {{.*Color.*no.longer.*universal.*}}
# CHECK: {{.*restart.*}}
@@ -80,7 +80,7 @@ echo no default universal variables
$fish -c '
set -g fish_color_autosuggestion red
set -g fish_color_command green --theme=default
__fish_theme_migrate
__fish_migrate
for cmd in "" "__fish_color_theme=unknown __fish_apply_theme"
eval $cmd
echo fish_color_autosuggestion $fish_color_autosuggestion
@@ -98,7 +98,7 @@ echo no default universal variables
{
set -U fish_key_bindings fish_vi_key_bindings
provoke-migration
$fish -c __fish_theme_migrate
$fish -c __fish_migrate
# CHECK: {{\x1b\[1m}}fish:{{\x1b\[m}} {{upgraded.*}}
# CHECK: {{.*fish_key_bindings.*no.longer.*universal.*}}
# CHECK: Migrated {{.*}} {{\S*}}/xdg_config_home/fish/conf.d/fish_frozen_key_bindings.fish{{\x1b\[m}}

View File

@@ -4,7 +4,7 @@
# No output is good output
for f in (status list-files completions | string match 'completions/*.fish')
if type -q (string replace -r '.*/([^/]+).fish' '$1' $f)
set -l out (__fish_data_with_file $f source 2>&1 | string collect)
set -l out (status get-file $f | source 2>&1 | string collect)
test -n "$out"
and echo -- OUTPUT from $f: $out
end

View File

@@ -1,4 +1,5 @@
#RUN: fish=%fish %fish %s
__fish_migrate # make sure the interactive fish doesn't need mkdir in PATH
set -g PATH
$fish -c "nonexistent-command-1234 banana rama"
#CHECKERR: fish: Unknown command: nonexistent-command-1234

View File

@@ -164,8 +164,7 @@ fish_config theme show | grep -E 'default-rgb|base16-default|custom-from-usercon
# CHECK: {{.*}}base16-default (dark color theme){{\x1b\[m}}
# Override the default theme with different colors.
__fish_data_with_file themes/none.theme \
cat >$__fish_config_dir/themes/default.theme
status get-file themes/none.theme >$__fish_config_dir/themes/default.theme
fish_config theme show default ayu | grep -E 'default|ayu.*dark' -A1
# CHECK: {{\x1b\[m}}{{\x1b\[4m}}default (unknown color theme){{\x1b\[m}}
# CHECK: /bright/vixens{{.*}}

View File

@@ -2,8 +2,7 @@
touch $__fish_config_dir/functions/delta-test-custom-function.fish
for path in fish_greeting fish_job_summary
__fish_data_with_file functions/$path.fish \
cat >$__fish_config_dir/functions/$path.fish
status get-file functions/$path.fish >$__fish_config_dir/functions/$path.fish
end
set -l tmp (sed 's/$/ # Modified/' $__fish_config_dir/functions/fish_greeting.fish)
string join -- \n $tmp >$__fish_config_dir/functions/fish_greeting.fish

View File

@@ -162,6 +162,11 @@ function foo --argument-names status; end
# CHECKERR: function foo --argument-names status; end
# CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
function foo --inherit-variable status; end
# CHECKERR: {{.*}}function.fish (line {{\d+}}): function: variable 'status' is read-only
# CHECKERR: function foo --inherit-variable status; end
# CHECKERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
echo status $status
# CHECK: status 2

View File

@@ -2,9 +2,9 @@
# REQUIRES: command -v sphinx-build
# REQUIRES: command -v diff
__fish_data_with_file help_sections (command -v cat) | grep -v ^cmds/ >expected
status get-file help_sections | grep -v ^cmds/ >expected
__fish_data_with_file completions/help.fish cat |
status get-file completions/help.fish |
awk '
/ case / && $2 != "'\''cmds/*'\''" {
sub(/^introduction/, "index", $2);

View File

@@ -5,7 +5,7 @@
# REQUIRES: test "$FISH_BUILD_DOCS" != "0"
# Override the test-override again.
__fish_data_with_file functions/__fish_print_help.fish source
status get-file functions/__fish_print_help.fish | source
set -l deroff col -b -p -x

View File

@@ -0,0 +1,49 @@
#RUN: %fish %s
#REQUIRES: command -v tmux
isolated-tmux-start -C '
function fish_prompt
set -g counter (math $counter + 1)
set -g fish_color_status red
set -g fish_pager_color_background --background=white
echo "$counter> "
set -ga TERM .
set -ga TERMINFO .
set -ga TERMINFO_DIRS .
set -ga COLORTERM .
set -ga fish_term256 .
set -ga fish_term24bit .
end
set -eg fish_color_param
bind ctrl-g,A "{ set -l fish_color_command 111 }"
bind ctrl-g,B "set -l fish_color_command 222"
bind ctrl-g,C "set -e fish_color_command"
bind ctrl-g,D "set -eg fish_color_param"
bind ctrl-g,E "set -g fish_color_command 333"
bind ctrl-g,F "set -U fish_color_param 444"
bind ctrl-g,G "set -eg fish_color_command"
'
isolated-tmux capture-pane -p
# The weird global assignments in fish prompt cause an initial repaint.
# CHECK: 2>
isolated-tmux send-keys C-g A C-g B C-g C C-g D
tmux-sleep
isolated-tmux capture-pane -p
# CHECK: 2>
isolated-tmux send-keys C-g E
tmux-sleep
isolated-tmux capture-pane -p
# CHECK: 3>
isolated-tmux send-keys C-g F
tmux-sleep
isolated-tmux capture-pane -p
# CHECK: 4>
isolated-tmux send-keys C-g G
tmux-sleep
isolated-tmux capture-pane -p
# CHECK: 5>